Changes for page LiveTable View Sheet

Last modified by Mark Kohlmann on 2025/01/19 06:47

From version 4.1
edited by Mark Kohlmann
on 2025/01/19 06:47
Change comment: Install extension [org.xwiki.platform:xwiki-platform-appwithinminutes-ui/16.10.2]
To version 2.1
edited by Mark Kohlmann
on 2020/04/03 03:53
Change comment: Install extension [org.xwiki.platform:xwiki-platform-appwithinminutes-ui/12.2]

Summary

Details

Page properties
Author
... ... @@ -1,1 +1,1 @@
1 -XWiki.mark\.kohlmann@chiefintegrations\.com
1 +XWiki.MarkKohlmann
Content
... ... @@ -11,8 +11,16 @@
11 11   ## Display the live table only if it was generated.
12 12   #if ($doc.content.length() > 0)
13 13   = $services.localization.render('platform.appwithinminutes.appLiveTableHeading') =
14 -
15 - {{include reference="" author="target"/}}
14 + ## We don't use the Include macro (with empty reference) because we want the content to be executed with the rights
15 + ## of the current document rather than the rights of the sheet. This is important because the user can modify the
16 + ## content of the application home page which means we could execute untrusted content with the rights of the sheet.
17 + ## Ideally we should use the Display macro with a parameter to disable the sheet, but we don't have this parameter.
18 + ## We don't clean the HTML content because getRenderedContent() should produce clean HTML, unless the user has
19 + ## disabled the HTML cleaning, in which case he will get what he asked for. Note that one good reason to disable
20 + ## HTML cleaning is to preserve the whitespaces in the attribute values.
21 + ## Escape {{ in the rendered content to be sure that the HTML macro is not closed unintentionally.
22 + {{html clean="false"}}$doc.getRenderedContent($doc.content,
23 + $doc.syntax.toIdString()).replace('{{', '{{'){{/html}}
16 16   #end
17 17  #end
18 18  
... ... @@ -51,7 +51,7 @@
51 51   (((
52 52   = $services.localization.render('platform.appwithinminutes.appHomePageActionsHeading') =
53 53   #if ($hasCreateData)
54 - * [[{{displayIcon name="add"/}} $services.localization.render('platform.appwithinminutes.appHomePageAddEntryHint')>>||anchor="AddNewEntry" class="action add"]]##
62 + * [[$services.localization.render('platform.appwithinminutes.appHomePageAddEntryHint')>>||anchor="AddNewEntry" class="action add"]]##
55 55   #if ("$!templateProvider.getValue('terminal')" == '1')
56 56   #set ($entryReference = $services.model.createDocumentReference('__entryName__', $dataSpaceRef))
57 57   #else
... ... @@ -60,7 +60,6 @@
60 60   #end
61 61   ## We need to set the title if we want to be able to sort or filter the doc.title live table column.
62 62   #set ($params = {
63 - 'form_token': $services.csrf.token,
64 64   'template': "${className}Template",
65 65   'title': '__entryName__',
66 66   'parent': $services.model.serialize($doc.documentReference, 'local')
... ... @@ -84,10 +84,10 @@
84 84   'appName': $doc.space,
85 85   'resolve': true
86 86   }))
87 - * [[{{displayIcon name="edit"/}} $services.localization.render('platform.appwithinminutes.appHomePageEditAppLabel')>>AppWithinMinutes.CreateApplication||queryString="$queryString" class="action edit"]]
94 + * [[$services.localization.render('platform.appwithinminutes.appHomePageEditAppLabel')>>AppWithinMinutes.CreateApplication||queryString="$queryString" class="action edit"]]
88 88   #end
89 89   #if ($hasEditTranslations)
90 - * [[{{displayIcon name="translate"/}} $services.localization.render('platform.appwithinminutes.appHomePageTranslateAppLabel')>>path:${xwiki.getURL($translationsRef, 'edit', 'editor=wiki')}||class="action translate"]]
97 + * [[$services.localization.render('platform.appwithinminutes.appHomePageTranslateAppLabel')>>path:${xwiki.getURL($translationsRef, 'edit', 'editor=wiki')}||class="action translate"]]
91 91   #end
92 92   #if ($hasDeleteData)
93 93   #set ($deleteDataURL = $xwiki.getURL('AppWithinMinutes.DeleteApplication', 'view', $escapetool.url({
... ... @@ -96,7 +96,7 @@
96 96   'scope': 'entries',
97 97   'xredirect': $doc.getURL()
98 98   })))
99 - * [[{{displayIcon name="cross"/}} $services.localization.render('platform.appwithinminutes.appHomePageDeleteEntriesLabel')>>path:${deleteDataURL}||class="action deleteData"]]
106 + * [[$services.localization.render('platform.appwithinminutes.appHomePageDeleteEntriesLabel')>>path:${deleteDataURL}||class="action deleteData"]]
100 100   #end
101 101   #if ($hasDeleteApplication)
102 102   #set ($deleteAppURL = $xwiki.getURL('AppWithinMinutes.DeleteApplication', 'view', $escapetool.url({
... ... @@ -104,7 +104,7 @@
104 104   'resolve': true,
105 105   'xredirect': $doc.getURL()
106 106   })))
107 - * [[{{displayIcon name="trash"/}} $services.localization.render('platform.appwithinminutes.appHomePageDeleteAppLabel')>>path:${deleteAppURL}||class="action delete"]]
114 + * [[$services.localization.render('platform.appwithinminutes.appHomePageDeleteAppLabel')>>path:${deleteAppURL}||class="action delete"]]
108 108   #end
109 109   )))
110 110   #end
... ... @@ -115,30 +115,23 @@
115 115   data-backdrop="static" data-keyboard="false">
116 116   <div class="modal-dialog" role="document">
117 117   <form class="modal-content xform">
118 - ## The fieldset allows us to disable and enable the entire form quickly and easy.
119 - <fieldset>
120 - <div class="modal-header">
121 - <button type="button" class="close" data-dismiss="modal"
122 - title="$escapetool.xml($services.localization.render('appWithinMinutes.renameApp.close'))"
123 - aria-label="$escapetool.xml($services.localization.render('appWithinMinutes.renameApp.close'))">
124 - <span aria-hidden="true">&times;</span>
125 - </button>
126 - <span class="modal-title" id="renameAppModal-label">
127 - $escapetool.xml($services.localization.render('appWithinMinutes.renameApp.label'))
128 - </span>
129 - </div>
130 - <div class="modal-body">
131 - #renameAppModalBody
132 - </div>
133 - <div class="modal-footer">
134 - <button type="button" class="btn btn-default" data-dismiss="modal">
135 - $escapetool.xml($services.localization.render('cancel'))
136 - </button>
137 - <button type="submit" class="btn btn-primary" disabled="disabled">
138 - $escapetool.xml($services.localization.render('core.rename.submit'))
139 - </button>
140 - </div>
141 - </fieldset>
125 + <div class="modal-header">
126 + <button type="button" class="close" data-dismiss="modal" aria-label="Close">
127 + <span aria-hidden="true">&times;</span>
128 + </button>
129 + <span class="modal-title" id="renameAppModal-label">Rename Application</span>
130 + </div>
131 + <div class="modal-body">
132 + #renameAppModalBody
133 + </div>
134 + <div class="modal-footer">
135 + <button type="button" class="btn btn-default" data-dismiss="modal">
136 + $escapetool.xml($services.localization.render('cancel'))
137 + </button>
138 + <button type="submit" class="btn btn-primary" disabled="disabled">
139 + $escapetool.xml($services.localization.render('core.rename.submit'))
140 + </button>
141 + </div>
142 142   </form>
143 143   </div>
144 144   </div>
... ... @@ -204,7 +204,7 @@
204 204   #set ($classFullName = $doc.getValue('class'))
205 205   #if ("$!classFullName" == '' || !$xwiki.exists($classFullName))
206 206   {{warning}}
207 - {{translation key="platform.appwithinminutes.appHomePageMovedWarning"/}}
207 + $services.icon.render('warning') {{translation key="platform.appwithinminutes.appHomePageMovedWarning"/}}
208 208   {{/warning}}
209 209  
210 210   #end
locate.png
Author
... ... @@ -1,0 +1,1 @@
1 +XWiki.MarkKohlmann
Size
... ... @@ -1,0 +1,1 @@
1 +746 bytes
Content
XWiki.JavaScriptExtension[0]
Code
... ... @@ -58,9 +58,8 @@
58 58  /**
59 59   * Rename Application
60 60   */
61 -require(['jquery', 'bootstrap', 'xwiki-form-validation-async'], function($) {
62 - ## Note: if not currentApp request param is not passed, we default to the wiki home page reference to avoid a NPE
63 - #set ($currentDocReference = $xwiki.getDocument("$!request.currentApp").getDocumentReference())
61 +require(['jquery', 'bootstrap'], function($) {
62 + #set ($currentDocReference = $xwiki.getDocument($request.currentApp).getDocumentReference())
64 64   // if we cannot find any extension related to this page app, it's not part of an extension.
65 65   var isNotAnExtension = $services.extension.xar.getInstalledExtensions($currentDocReference).isEmpty();
66 66  
... ... @@ -70,7 +70,7 @@
70 70   }
71 71   // Hijack the rename page action.
72 72   var renameAppModal = $('#renameAppModal');
73 - $('#tmActionRename').on('click', function(event) {
72 + $('#tmActionRename').click(function(event) {
74 74   event.preventDefault();
75 75   renameAppModal.modal();
76 76   });
... ... @@ -78,6 +78,7 @@
78 78   // Form validation.
79 79   var appNameInput = $('#renameAppTitle');
80 80   var appParentInput = $('#renameAppParentReference');
80 + var submitButton = renameAppModal.find('.btn-primary[type="submit"]');
81 81  
82 82   var appNameEmptyError = renameAppModal.find('.appNameEmptyError');
83 83   var pageExistsError = renameAppModal.find('.pageExistsError');
... ... @@ -100,44 +100,52 @@
100 100  
101 101   var startValidation = function() {
102 102   if (appNameInput.val() === '') {
103 - return Promise.reject(appNameEmptyError);
103 + endValidation(appNameEmptyError);
104 104   } else {
105 105   var newAppHomePage = getNewAppHomePage();
106 106   if (newAppHomePage.documentReference.equals(XWiki.currentDocument.documentReference)) {
107 - return Promise.reject(pageExistsError);
107 + endValidation(pageExistsError);
108 108   } else {
109 - return new Promise((resolve, reject) => {
110 - $.ajax({
111 - type: 'HEAD',
112 - url: newAppHomePage.getURL()
113 - }).then(reject.bind(null, pageExistsError), response => {
114 - if (response.status === 404) {
115 - $.ajax({
116 - type: 'HEAD',
117 - url: newAppHomePage.getURL('edit')
118 - }).then(
119 - () => resolve(),
120 - () => reject(locationForbiddenError)
121 - );
122 - } else if (response.status === 403) {
123 - reject(locationForbiddenError);
124 - } else {
125 - resolve();
126 - }
127 - });
109 + $.ajax({
110 + type: 'HEAD',
111 + url: newAppHomePage.getURL()
112 + }).done(function() {
113 + endValidation(pageExistsError);
114 + }).fail(function(response) {
115 + if (response.status === 404) {
116 + $.ajax({
117 + type: 'HEAD',
118 + url: newAppHomePage.getURL('edit')
119 + }).done(function() {
120 + endValidation();
121 + }).fail(function() {
122 + endValidation(locationForbiddenError);
123 + });
124 + } else if (response.status === 403) {
125 + endValidation(locationForbiddenError);
126 + } else {
127 + endValidation();
128 + }
128 128   });
129 129   }
130 130   }
131 131   };
132 132  
134 + var endValidation = function(error) {
135 + if (error) {
136 + error.show();
137 + }
138 + appNameInput.removeClass('loading');
139 + submitButton.prop('disabled', !!error);
140 + };
141 +
142 + var validationTimeout;
133 133   var scheduleValidation = function() {
134 - // Hide all error messages before starting the asynchronous validation.
144 + clearTimeout(validationTimeout);
135 135   renameAppModal.find('.xErrorMsg').hide();
136 - appNameInput.addClass('loading').validateAsync(startValidation, 500, 'awm').catch((error) => {
137 - error.show();
138 - }).finally(() => {
139 - appNameInput.removeClass('loading');
140 - });
146 + appNameInput.addClass('loading');
147 + submitButton.prop('disabled', true);
148 + validationTimeout = setTimeout(startValidation, 500);
141 141   };
142 142  
143 143   appNameInput.add(appParentInput).on('input', scheduleValidation);
... ... @@ -159,7 +159,7 @@
159 159   renameAppModal.modal('show');
160 160   });
161 161  
162 - renameAppModal.find('form').on('submit', function(event) {
170 + renameAppModal.find('form').submit(function(event) {
163 163   event.preventDefault();
164 164   renameApp(getRenameData($(this)));
165 165   });
... ... @@ -186,13 +186,13 @@
186 186  
187 187   var renameApp = function(data) {
188 188   // Disable the form to prevent it from being submitted twice.
189 - renameAppModal.find('fieldset').prop('disabled', true);
197 + renameAppModal.find(':input').prop('disabled', true);
190 190   var notification = new XWiki.widgets.Notification(
191 191   $jsontool.serialize($services.localization.render('appWithinMinutes.renameApp.inProgress')),
192 192   'inprogress'
193 193   );
194 194   var renameAppURL = new XWiki.Document('RenameApplication', 'AppWithinMinutes').getURL('get');
195 - Promise.resolve($.post(renameAppURL, data)).then(updateAppHomePage).then(function() {
203 + $.post(renameAppURL, data).then(updateAppHomePage).done(function() {
196 196   renameAppModal.modal('hide');
197 197   notification.replace(new XWiki.widgets.Notification(
198 198   $jsontool.serialize($services.localization.render('appWithinMinutes.renameApp.done')),
... ... @@ -202,29 +202,33 @@
202 202   setTimeout(function() {
203 203   window.location.href = getNewAppHomePage().getURL();
204 204   }, 0);
205 - }).catch(function() {
213 + }).fail(function() {
206 206   notification.replace(new XWiki.widgets.Notification(
207 207   $jsontool.serialize($services.localization.render('appWithinMinutes.renameApp.failed')),
208 208   'error'
209 209   ));
210 - }).finally(function() {
218 + }).always(function() {
211 211   // Re-enable the form.
212 - renameAppModal.find('fieldset').prop('disabled', false);
220 + renameAppModal.find(':input').prop('disabled', false);
213 213   });
214 214   };
215 215  
216 216   var updateAppHomePage = function() {
217 - return new Promise((resolve, reject) => {
218 - var newAppHomePageEditURL = getNewAppHomePage().getURL('edit');
219 - $('<div/>').load(newAppHomePageEditURL + ' #inline', function() {
220 - var formData = $(this).children('form#inline').serializeArray();
221 - if (formData.length) {
222 - formData.push({name: 'xaction_save', value: true});
223 - $.post(newAppHomePageEditURL, formData).then(resolve, reject);
224 - } else {
225 - reject();
226 - }
227 - });
225 + var deferred = $.Deferred();
226 + var newAppHomePageEditURL = getNewAppHomePage().getURL('edit');
227 + $('<div/>').load(newAppHomePageEditURL + ' #inline', function() {
228 + var formData = $(this).children('form#inline').serializeArray();
229 + if (formData.length > 0) {
230 + formData.push({name: 'xaction_save', value: true});
231 + $.post(newAppHomePageEditURL, formData).done(function() {
232 + deferred.resolve();
233 + }).fail(function() {
234 + deferred.reject();
235 + });
236 + } else {
237 + deferred.reject();
238 + }
228 228   });
240 + return deferred.promise();
229 229   }
230 230  });
XWiki.StyleSheetExtension[0]
Code
... ... @@ -16,10 +16,30 @@
16 16   background: none no-repeat scroll 0 center transparent;
17 17   display: block;
18 18   font-size: .8em;
19 - padding: .3em .3em .3em .3em;
19 + padding: .3em .3em .3em 20px;
20 20   text-transform: uppercase;
21 21  }
22 22  
23 +#actionBox .action.edit {
24 + background-image: url("$xwiki.getSkinFile('icons/silk/application_edit.png')");
25 +}
26 +
27 +#actionBox .action.translate {
28 + background-image: url("$doc.getAttachmentURL('locate.png')");
29 +}
30 +
31 +#actionBox .action.delete {
32 + background-image: url("$xwiki.getSkinFile('icons/silk/application_delete.png')");
33 +}
34 +
35 +#actionBox .action.deleteData {
36 + background-image: url("$xwiki.getSkinFile('icons/silk/application_form_delete.png')");
37 +}
38 +
39 +#actionBox .action.add {
40 + background-image: url("$xwiki.getSkinFile('icons/silk/add.png')");
41 +}
42 +
23 23  #entryNamePopup {
24 24   margin-right: 20px;
25 25  }