@saltcorn/server 0.7.4-beta.3 → 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/markup/forms.js CHANGED
@@ -50,7 +50,7 @@ const editRoleForm = ({ url, current_role, roles, req }) =>
50
50
  * @param {object} req
51
51
  * @returns {Form}
52
52
  */
53
- const fileUploadForm = (req) =>
53
+ const fileUploadForm = (req, folder) =>
54
54
  form(
55
55
  {
56
56
  action: "/files/upload",
@@ -61,11 +61,12 @@ const fileUploadForm = (req) =>
61
61
  label(req.__("Upload file ")),
62
62
  input({
63
63
  name: "file",
64
- class: "form-control-file",
64
+ class: "form-control ms-1 w-unset d-inline",
65
65
  type: "file",
66
66
  onchange: "form.submit()",
67
67
  multiple: true,
68
- })
68
+ }),
69
+ folder && input({ type: "hidden", name: "folder", value: folder })
69
70
  );
70
71
 
71
72
  /**
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.7.4-beta.3",
3
+ "version": "0.8.0-beta.0",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@saltcorn/base-plugin": "0.7.4-beta.3",
10
- "@saltcorn/builder": "0.7.4-beta.3",
11
- "@saltcorn/data": "0.7.4-beta.3",
12
- "@saltcorn/admin-models": "0.7.4-beta.3",
13
- "@saltcorn/markup": "0.7.4-beta.3",
14
- "@saltcorn/sbadmin2": "0.7.4-beta.3",
9
+ "@saltcorn/base-plugin": "0.8.0-beta.0",
10
+ "@saltcorn/builder": "0.8.0-beta.0",
11
+ "@saltcorn/data": "0.8.0-beta.0",
12
+ "@saltcorn/admin-models": "0.8.0-beta.0",
13
+ "@saltcorn/filemanager": "0.7.4",
14
+ "@saltcorn/markup": "0.8.0-beta.0",
15
+ "@saltcorn/sbadmin2": "0.8.0-beta.0",
15
16
  "@socket.io/cluster-adapter": "^0.1.0",
16
17
  "@socket.io/sticky": "^1.0.1",
17
18
  "aws-sdk": "^2.1037.0",
@@ -0,0 +1,530 @@
1
+ const entityFilter = {
2
+ showViews: true,
3
+ showPages: true,
4
+ showTables: true,
5
+ showTrigger: true,
6
+ };
7
+ const tagFilterIds = [];
8
+ let tagFilterEnabled = false;
9
+
10
+ let activePopper = null;
11
+
12
+ function initMouseOver() {
13
+ cy.on("mouseover", "node", (event) => {
14
+ const node = event.target;
15
+ const cardPopper = node.popper({
16
+ content: () => {
17
+ const popperDiv = document.getElementById(`${node.id()}_popper`);
18
+ if (popperDiv) {
19
+ popperDiv.setAttribute("style", "");
20
+ return popperDiv;
21
+ } else return buildCard(node);
22
+ },
23
+ });
24
+ activePopper = cardPopper;
25
+ const update = () => {
26
+ cardPopper.update();
27
+ };
28
+ node.on("position", update);
29
+ cy.on("pan zoom resize", update);
30
+ buildPreview(node);
31
+ });
32
+
33
+ cy.on("mouseout", "node", (event) => {
34
+ const node = event.target;
35
+ activePopper.destroy();
36
+ const popperDiv = document.getElementById(`${node.id()}_popper`);
37
+ popperDiv.setAttribute("style", "display: none;");
38
+ });
39
+ }
40
+
41
+ function buildCard(node) {
42
+ const { type, label } = node.data();
43
+ const html = `
44
+ <div class="card" style="width: 18rem;">
45
+ <div class="card-header">
46
+ <h5 class="card-title">${type}</h5>
47
+ <h6 class="card-subtitle text-muted">${label}</h6>
48
+ </div>
49
+ <div class="card-body">
50
+ ${buildTagBadges(node)}
51
+ ${buildCardBody(node)}
52
+ ${type === "page" || type === "view" ? buildPreviewDiv(node) : ""}
53
+ ${type === "page" || type === "view" ? buildMinRoleSelect(node) : ""}
54
+ </div>
55
+ </div>
56
+ `;
57
+ const div = document.createElement("div");
58
+ div.id = `${node.id()}_popper`;
59
+ document.body.appendChild(div);
60
+ div.innerHTML = html;
61
+ return div;
62
+ }
63
+
64
+ function buildPreview(node) {
65
+ const { name, type } = node.data();
66
+ const previewId = `preview_${node.id()}`;
67
+ $.ajax(`/${type}/${name}/preview`, {
68
+ type: "POST",
69
+ headers: {
70
+ "CSRF-Token": _sc_globalCsrf,
71
+ },
72
+ success: (res) => {
73
+ $(`#${previewId}`).html(`
74
+ <div
75
+ id="preview_wrapper"
76
+ style="min-height: 70px;"
77
+ >
78
+ ${res}
79
+ </div></div></div>`);
80
+ const previewDiv = $(`#${previewId}`);
81
+ const pos = previewDiv.position();
82
+ const cssBase = `
83
+ position: absolute; top: ${pos.top}px; left: ${pos.left}px;
84
+ width: ${previewDiv.width()}px; height: ${previewDiv.height() + 12}px;`;
85
+ $(`#${previewId}`).after(`
86
+ <div
87
+ style="${cssBase}
88
+ background-color: black; opacity: 0.1;
89
+ z-index: 10;"
90
+ >
91
+ </div>
92
+ <div style="${cssBase} opacity: 0.5;">
93
+ <h2 class="preview-text fw-bold text-danger">
94
+ Preview
95
+ </h2>
96
+ </div>`);
97
+ },
98
+ error: (res) => {
99
+ console.log("error");
100
+ console.log(res);
101
+ },
102
+ });
103
+ }
104
+
105
+ function buildPreviewDiv(node) {
106
+ const previewId = `preview_${node.id()}`;
107
+ return `
108
+ <div class="my-2" id="${previewId}" style="min-height: 70px;">
109
+ <div style="opacity: 0.5;">
110
+ <h2>
111
+ <span class="fw-bold text-danger">Preview</span>
112
+ <i class="fas fa-spinner fa-spin"></i>
113
+ </h2>
114
+ </div>
115
+ </div>`;
116
+ }
117
+
118
+ function buildMinRoleSelect(node) {
119
+ let { type, objectId, min_role } = node.data();
120
+ min_role = parseInt(min_role);
121
+ const selectId = `_${type}_${objectId}_access_id`;
122
+ return `
123
+ <form
124
+ action="${type === "view" ? "viewedit" : "pageedit"}/setrole/${objectId}"
125
+ method="post"
126
+ >
127
+ <div class="row">
128
+ <div class="col-sm-3">
129
+ <label
130
+ class="form-label"
131
+ for="${selectId}"
132
+ >
133
+ Access
134
+ </label>
135
+ </div>
136
+ <div class="col-sm-7">
137
+ <select
138
+ class="form-select"
139
+ id="${selectId}"
140
+ name="role"
141
+ onchange="setRole(this, '${node.id()}')"
142
+ >
143
+ ${roles.map(
144
+ (role) =>
145
+ `<option
146
+ value="${role.id}"
147
+ ${role.id === min_role ? "selected" : ""}
148
+ >
149
+ ${role.role}
150
+ </option>`
151
+ )}
152
+ </select>
153
+ </div>
154
+ </form>`;
155
+ }
156
+
157
+ function setRole(srcElement, nodeId) {
158
+ const form = $(srcElement).closest("form");
159
+ const newRole = parseInt(form.serializeArray()[0].value);
160
+ const node = cy.nodes().find((node) => node.id() === nodeId);
161
+ const { type, objectId } = node.data();
162
+ $.ajax(`/${type}edit/setrole/${objectId}`, {
163
+ type: "POST",
164
+ headers: {
165
+ "CSRF-Token": _sc_globalCsrf,
166
+ },
167
+ data: { role: newRole },
168
+ success: (res) => {
169
+ node.data().min_role = newRole;
170
+ // TODO disabled alerts, the element conflicts with cy
171
+ console.log(res.responseText);
172
+ },
173
+ error: (res) => {
174
+ // TODO disabled alerts, the element conflicts with cy
175
+ console.log(res.responseText);
176
+ },
177
+ });
178
+ }
179
+
180
+ function buildTagBadges(node) {
181
+ const { type, tags, objectId } = node.data();
182
+ return `
183
+ <div
184
+ id="_${type}_${objectId}_badges_id"
185
+ class="mb-3"
186
+ >
187
+ ${existingTagBadges(node)}
188
+ <button
189
+ class="badge bg-primary"
190
+ data-bs-toggle="dropdown"
191
+ aria-expanded=false
192
+ >
193
+ <i class="fas fa-plus"></i>
194
+ <i class="fas fa-caret-down"></i>
195
+ </button>
196
+ <div class="dropdown-menu">
197
+ <form id="${type}_${objectId}_options_form">
198
+ <input
199
+ type="hidden"
200
+ name="objectType"
201
+ value="${type}"
202
+ />
203
+ <input
204
+ type="hidden"
205
+ name="objectId"
206
+ value="${objectId}"
207
+ />
208
+ ${newTagOptions(tags, type, objectId)}
209
+ <button
210
+ type="button"
211
+ onClick="addToTag(this, '${node.id()}')"
212
+ class="ms-3 mt-2 mb-1 btn btn-warning"
213
+ >
214
+ Add to tags
215
+ </button>
216
+ </form>
217
+ </div>
218
+ </div>
219
+ `;
220
+ }
221
+
222
+ function addToTag(srcButton, nodeId) {
223
+ const form = $(srcButton).closest("form");
224
+ let objectType;
225
+ let objectId;
226
+ const tag_ids = [];
227
+ for (const param of form.serializeArray()) {
228
+ switch (param.name) {
229
+ case "objectType": {
230
+ objectType = param.value;
231
+ break;
232
+ }
233
+ case "objectId": {
234
+ objectId = param.value;
235
+ break;
236
+ }
237
+ case "tagId": {
238
+ tag_ids.push(param.value);
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ if (tag_ids.length > 0) {
244
+ $.ajax(`/tag-entries/add/multiple_tags/${objectType}/${objectId}`, {
245
+ type: "POST",
246
+ headers: {
247
+ "CSRF-Token": _sc_globalCsrf,
248
+ },
249
+ data: { tag_ids },
250
+ success: (res) => {
251
+ const badgesDiv = document.getElementById(
252
+ `_${objectType}_${objectId}_badges_id`
253
+ );
254
+ for (const tag of res.tags) {
255
+ // create new badge
256
+ const tagBadge = document.createElement("div");
257
+ tagBadge.classList.add("badge", "bg-primary");
258
+ tagBadge.innerHTML = `${tag.name}
259
+ <i
260
+ class="fas fa-times ms-1"
261
+ onClick="removeObjectFromTag(this, '${objectType}', ${objectId},
262
+ ${tag.id}, '${tag.name}', '${nodeId}')"
263
+ ></i>`;
264
+ tagBadge.id = `_${objectType}_badge_${tag.id}_${objectId}`;
265
+ const allChildren = badgesDiv.childNodes;
266
+ badgesDiv.insertBefore(tagBadge, allChildren[allChildren.length - 4]);
267
+ // remove the add option
268
+ const addOptionDiv = document.getElementById(
269
+ `tag_add_div_${tag.id}_${objectType}_${objectId}`
270
+ );
271
+ addOptionDiv.remove();
272
+ // add the tag to the cy node
273
+ const node = cy.nodes().find((node) => node.id() === nodeId);
274
+ node.data().tags.push(tag);
275
+ }
276
+ },
277
+ error: (res) => {
278
+ // TODO disabled alerts, the element conflicts with cy
279
+ console.log(res.responseText);
280
+ },
281
+ });
282
+ }
283
+ }
284
+
285
+ function buildCardBody(node) {
286
+ switch (node.data().type) {
287
+ case "view": {
288
+ return buildViewContent(node);
289
+ }
290
+ case "page": {
291
+ return buildPageContent(node);
292
+ }
293
+ case "table": {
294
+ return buildTableContent(node);
295
+ }
296
+ case "trigger": {
297
+ return buildTriggerContent(node);
298
+ }
299
+ }
300
+ }
301
+
302
+ function buildViewContent(node) {
303
+ const { table, viewtemplate } = node.data();
304
+ return `
305
+ <div class="container mb-2">
306
+ <div class="row">
307
+ <div class="col">${viewtemplate}</div>
308
+ <div class="col">${table}</div>
309
+ </div>
310
+ </div>`;
311
+ }
312
+
313
+ function buildPageContent(node) {
314
+ // TODO
315
+ return `
316
+ `;
317
+ }
318
+
319
+ function buildTableContent(node) {
320
+ const { fields } = node.data();
321
+ return `
322
+ <div class="container">
323
+ ${fields
324
+ .map((field) => {
325
+ return `
326
+ <div class="row">
327
+ <div class="col">${field.name} :</div>
328
+ <div class="col ps-0">${field.typeName}</div>
329
+ </div>`;
330
+ })
331
+ .join("")}
332
+ </div>`;
333
+ }
334
+
335
+ function buildTriggerContent(node) {
336
+ // TODO
337
+ return `
338
+ `;
339
+ }
340
+
341
+ function newTagOptions(existingTags, type, objectId) {
342
+ const existingTagIds = new Set(existingTags.map((tag) => tag.id));
343
+ const newTagOptions = allTags.filter((tag) => !existingTagIds.has(tag.id));
344
+ return newTagOptions
345
+ .map((tag) => {
346
+ const inputId = `tag_add_box_${tag.id}`;
347
+ const divId = `tag_add_div_${tag.id}_${type}_${objectId}`;
348
+ return `
349
+ <div
350
+ class="ms-3 mt-3 form-check"
351
+ id="${divId}"
352
+ >
353
+ <label
354
+ class="form-check-label"
355
+ for="${inputId}"
356
+ >
357
+ ${tag.name}
358
+ </label>
359
+ <input
360
+ type="checkbox"
361
+ class="form-check-input"
362
+ id="${inputId}"
363
+ name="tagId"
364
+ value="${tag.id}"
365
+ checked=false
366
+ autocomplete="off"
367
+ />
368
+ </div>
369
+ `;
370
+ })
371
+ .join("");
372
+ }
373
+
374
+ function removeObjectFromTag(src, type, objectId, tagId, tagName, nodeId) {
375
+ $.ajax(`/tag-entries/remove/${type}/${objectId}/${tagId}`, {
376
+ type: "POST",
377
+ headers: {
378
+ "CSRF-Token": _sc_globalCsrf,
379
+ },
380
+ data: { tag_id: tagId },
381
+ success: (res) => {
382
+ // remove tag badge
383
+ src.parentNode.remove();
384
+ // add option checkbox
385
+ const divId = `tag_add_div_${tagId}_${type}_${objectId}`;
386
+ const inputId = `tag_add_box_${tagId}`;
387
+ const optionsDiv = document.createElement("div");
388
+ optionsDiv.innerHTML = `
389
+ <div
390
+ class="ms-3 mt-3 form-check"
391
+ id="${divId}"
392
+ >
393
+ <label
394
+ class="form-check-label"
395
+ for="${inputId}"
396
+ >
397
+ ${tagName}
398
+ </label>
399
+ <input
400
+ type="checkbox"
401
+ class="form-check-input"
402
+ id="${inputId}"
403
+ name="tagId"
404
+ value="${tagId}"
405
+ checked=false
406
+ autocomplete="off"
407
+ />
408
+ </div>
409
+ `;
410
+ const optionsContainer = document.getElementById(
411
+ `${type}_${objectId}_options_form`
412
+ );
413
+ const childNodes = optionsContainer.childNodes;
414
+ const addButton = childNodes[childNodes.length - 2];
415
+ optionsContainer.insertBefore(optionsDiv, addButton);
416
+ // remove tag from cy node
417
+ const node = cy.nodes().find((node) => node.id() === nodeId);
418
+ const { tags } = node.data();
419
+ const index = tags.findIndex((tag) => tag.id === tagId);
420
+ tags.splice(index, 1);
421
+ },
422
+ error: (res) => {
423
+ // TODO disabled alerts, the element conflicts with cy
424
+ console.log(res.responseText);
425
+ },
426
+ });
427
+ }
428
+
429
+ function existingTagBadges(node) {
430
+ const { tags, type, objectId } = node.data();
431
+ return tags
432
+ .map((tag) => {
433
+ return `
434
+ <div
435
+ class="badge bg-primary"
436
+ id="_${type}_badge_${tag.id}_${objectId}"
437
+ >
438
+ ${tag.name}
439
+ <i
440
+ class="fas fa-times ms-1"
441
+ onClick="removeObjectFromTag(
442
+ this, '${type}', ${objectId},
443
+ ${tag.id}, '${tag.name}', '${node.id()}')"
444
+ >
445
+ </i>
446
+ </div>`;
447
+ })
448
+ .join("");
449
+ }
450
+
451
+ function reloadCy(keepViewPos) {
452
+ const currentZoom = keepViewPos ? cy.zoom() : undefined;
453
+ const currentPan = keepViewPos ? cy.pan() : undefined;
454
+ $.ajax("/diagram/data", {
455
+ dataType: "json",
456
+ type: "GET",
457
+ headers: { "CSRF-Token": _sc_globalCsrf },
458
+ data: !tagFilterEnabled ? entityFilter : { ...entityFilter, tagFilterIds },
459
+ }).done((res) => {
460
+ const cfg = {
461
+ container: document.getElementById("cy"),
462
+ maxZoom: 2,
463
+ wheelSensitivity: 0.3,
464
+ ...res,
465
+ };
466
+ window.cy = cytoscape(cfg);
467
+ if (keepViewPos) {
468
+ cy.pan(currentPan);
469
+ cy.zoom(currentZoom);
470
+ }
471
+ initMouseOver();
472
+ });
473
+ }
474
+
475
+ function toggleEntityFilter(type) {
476
+ switch (type) {
477
+ case "views": {
478
+ entityFilter.showViews = !entityFilter.showViews;
479
+ break;
480
+ }
481
+ case "pages": {
482
+ entityFilter.showPages = !entityFilter.showPages;
483
+ break;
484
+ }
485
+ case "tables": {
486
+ entityFilter.showTables = !entityFilter.showTables;
487
+ break;
488
+ }
489
+ case "triggers": {
490
+ entityFilter.showTrigger = !entityFilter.showTrigger;
491
+ break;
492
+ }
493
+ }
494
+ }
495
+
496
+ function toggleTagFilter(id) {
497
+ if (!tagFilterEnabled) enableTagFilter();
498
+ const index = tagFilterIds.indexOf(id);
499
+ if (index > -1) {
500
+ tagFilterIds.splice(index, 1);
501
+ } else {
502
+ tagFilterIds.push(id);
503
+ }
504
+ }
505
+
506
+ function enableTagFilter() {
507
+ tagFilterEnabled = true;
508
+ for (const node of document.querySelectorAll('[id^="tagFilter_box_"]')) {
509
+ node.style = "";
510
+ }
511
+ for (const node of document.querySelectorAll('[id^="tagFilter_label_"]')) {
512
+ node.style = "";
513
+ }
514
+ const box = document.getElementById("noTagsId");
515
+ box.checked = false;
516
+ }
517
+
518
+ function toggleTagFilterMode() {
519
+ if (tagFilterEnabled) {
520
+ tagFilterEnabled = false;
521
+ for (const node of document.querySelectorAll('[id^="tagFilter_box_"]')) {
522
+ node.style = "opacity: 0.5;";
523
+ }
524
+ for (const node of document.querySelectorAll('[id^="tagFilter_label_"]')) {
525
+ node.style = "opacity: 0.5;";
526
+ }
527
+ } else {
528
+ enableTagFilter();
529
+ }
530
+ }
@@ -19,12 +19,15 @@ function deleteIcon() {
19
19
  function flatpickerEditor(cell, onRendered, success, cancel, editorParams) {
20
20
  var input = $("<input type='text'/>");
21
21
  const dayOnly = editorParams && editorParams.dayOnly;
22
+ let defaultDate = cell.getValue()
23
+
24
+ if (!defaultDate) defaultDate = new Date()
22
25
  input.flatpickr({
23
26
  enableTime: !dayOnly,
24
27
  dateFormat: dayOnly ? "Y-m-d" : "Z",
25
28
  time_24hr: true,
26
29
  locale: "en", // global variable with locale 'en', 'fr', ...
27
- defaultDate: cell.getValue(),
30
+ defaultDate,
28
31
  onClose: function (selectedDates, dateStr, instance) {
29
32
  evt = window.event;
30
33
  var isEscape = false;