glib-web 3.27.2 → 3.28.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/action.js CHANGED
@@ -12,6 +12,7 @@ import ActionsDialogsAlert from "./actions/dialogs/alert";
12
12
  import ActionsDialogsShow from "./actions/dialogs/show";
13
13
  import ActionsDialogsOpen from "./actions/dialogs/open";
14
14
  import ActionsDialogsClose from "./actions/dialogs/close";
15
+ import ActionsDialogsCloseAll from './actions/dialogs/closeAll';
15
16
  import ActionsDialogsReload from "./actions/dialogs/reload";
16
17
  import ActionsDialogsOptions from "./actions/dialogs/options";
17
18
  import ActionsDialogsNotification from "./actions/dialogs/notification";
@@ -25,6 +26,7 @@ import ActionsSheetsSelect from "./actions/sheets/select";
25
26
 
26
27
  import ActionsWindowsClose from "./actions/windows/close";
27
28
  import ActionsWindowsCloseAll from "./actions/windows/closeAll";
29
+ import ActionsWindowsCloseAllWithOpen from "./actions/windows/closeAllWithOpen";
28
30
  import ActionsWindowsOpen from "./actions/windows/open";
29
31
  import ActionsWindowsOpenWeb from "./actions/windows/openWeb";
30
32
  import ActionsWindowsReload from "./actions/windows/reload";
@@ -98,6 +100,7 @@ const actions = {
98
100
  "dialogs/show": ActionsDialogsShow,
99
101
  "dialogs/open": ActionsDialogsOpen,
100
102
  "dialogs/close": ActionsDialogsClose,
103
+ "dialogs/closeAll": ActionsDialogsCloseAll,
101
104
  "dialogs/reload": ActionsDialogsReload,
102
105
  "dialogs/options": ActionsDialogsOptions,
103
106
  "dialogs/notification": ActionsDialogsNotification,
@@ -111,6 +114,7 @@ const actions = {
111
114
 
112
115
  "windows/close": ActionsWindowsClose,
113
116
  "windows/closeAll": ActionsWindowsCloseAll,
117
+ "windows/closeAllWithOpen": ActionsWindowsCloseAllWithOpen,
114
118
  "windows/open": ActionsWindowsOpen,
115
119
  "windows/openWeb": ActionsWindowsOpenWeb,
116
120
  "windows/reload": ActionsWindowsReload,
@@ -0,0 +1,18 @@
1
+ import { nextTick } from "vue";
2
+ import { closeAllDialog } from "../../store";
3
+ import Action from "../../action";
4
+
5
+ export default class {
6
+ execute(spec, component) {
7
+ closeAllDialog();
8
+
9
+ const { onClose } = spec;
10
+
11
+ if (onClose) {
12
+ nextTick(() => {
13
+ Action.execute(onClose, component);
14
+ });
15
+ }
16
+
17
+ }
18
+ }
@@ -1,22 +1,23 @@
1
1
  import jsonLogic from 'json-logic-js';
2
+ import { fieldModels } from "../../components/composable/conditional";
2
3
 
3
- const subscript = function(a, b) {
4
+ const subscript = function (a, b) {
4
5
  if (a) {
5
6
  if (b) { // This is expected when the dependent field doesn't have a value
6
7
  return a[b];
7
8
  } else {
8
- console.info('Null subscript')
9
+ console.info('Null subscript');
9
10
  }
10
11
  } else {
11
- console.error('Left hand operator not found')
12
+ console.error('Left hand operator not found');
12
13
  }
13
14
  };
14
15
  jsonLogic.add_operation("[]", subscript);
15
16
 
16
17
  // Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing
17
- const nullishCoalescing = function(a, b) {
18
+ const nullishCoalescing = function (a, b) {
18
19
  return a ?? b;
19
- }
20
+ };
20
21
  jsonLogic.add_operation("??", nullishCoalescing);
21
22
 
22
23
  export default class {
@@ -28,9 +29,9 @@ export default class {
28
29
  }
29
30
 
30
31
  Utils.type.ifObject(spec.conditionalData, (properties) => {
31
- const data = {}
32
+ const data = {};
32
33
  for (const key in properties) {
33
- data[key] = jsonLogic.apply(properties[key], spec.variables);
34
+ data[key] = jsonLogic.apply(properties[key], spec.variables || fieldModels);
34
35
  }
35
36
  targetComponent.action_merge(data);
36
37
  });
@@ -0,0 +1,18 @@
1
+ import Action from "../../action";
2
+
3
+ export default class {
4
+ execute(properties, component) {
5
+ Utils.history.destroy();
6
+
7
+ // Don't use nextTick so that there is no visible flicker.
8
+ Utils.http.load(properties, component, true, true);
9
+
10
+ // const { url, onOpen } = properties;
11
+ // Action.execute({ action: 'windows/open', url }, component);
12
+
13
+ const { onOpen } = properties;
14
+ if (onOpen) {
15
+ Action.execute(onOpen, component);
16
+ }
17
+ }
18
+ }
@@ -2,7 +2,7 @@ export default class {
2
2
  execute(properties, component) {
3
3
  Utils.history.backWithoutRender()
4
4
 
5
- // Open right away so there is no visible flicker.
6
- Utils.http.load(properties, component, true);
5
+ // Don't use nextTick so that there is no visible flicker.
6
+ Utils.http.load(properties, component, true, true);
7
7
  }
8
8
  }
@@ -73,6 +73,7 @@ const RichTextField = defineAsyncComponent(() => import("./fields/richText.vue")
73
73
  // import NewRichTextField from "./fields/newRichText.vue";
74
74
  const FileField = defineAsyncComponent(() => import("./fields/file.vue"));
75
75
  const MultiUploadField = defineAsyncComponent(() => import("./fields/multiUpload.vue"));
76
+ import SignField from "./fields/sign.vue";
76
77
  import AutocompleteField from "./fields/autocomplete.vue";
77
78
  import SelectField from "./fields/select.vue";
78
79
  import TimeZoneField from "./fields/timeZone.vue";
@@ -168,6 +169,7 @@ export default {
168
169
  // "fields-newRichText": NewRichTextField,
169
170
  "fields-file": FileField,
170
171
  "fields-multiUpload": MultiUploadField,
172
+ "fields-sign": SignField,
171
173
  "fields-autocomplete": AutocompleteField,
172
174
  "fields-select": SelectField,
173
175
  "fields-timeZone": TimeZoneField,
@@ -0,0 +1,15 @@
1
+ import Action from '../../action';
2
+
3
+ export function showError(options) {
4
+ let { body, button, styleClasses } = options;
5
+ styleClasses ||= ['error', 'outlined'];
6
+ Action.execute(
7
+ {
8
+ action: 'snackbars/select',
9
+ message: body,
10
+ styleClasses,
11
+ buttons: [{ text: button }]
12
+ },
13
+ {}
14
+ );
15
+ }
@@ -103,6 +103,7 @@ function setBusyWhenUploading({ files }) {
103
103
  });
104
104
  }
105
105
 
106
+ // Deprecated, use alert.js
106
107
  const showError = (options) => {
107
108
  const { body, button } = options;
108
109
  Action.execute(
@@ -139,7 +140,13 @@ function uploadFiles({ droppedFiles, files, spec, container }) {
139
140
  function uploadOneFile({ files, key, spec, container, onAfterUploaded }) {
140
141
  const { directUploadUrl, accepts } = spec;
141
142
  let responseMessages = spec.responseMessages || {};
142
- const uploader = new Uploader(files.value[key].el, directUploadUrl, files.value[key].progress);
143
+ const uploader = new Uploader(
144
+ files.value[key].el,
145
+ directUploadUrl,
146
+ files.value[key].progress,
147
+ spec.storagePrefix,
148
+ spec.metadata
149
+ );
143
150
 
144
151
  // track progress
145
152
  uploader.directUploadWillStoreFileWithXHR = (request) => {
@@ -0,0 +1,21 @@
1
+ export function validationRules(validation) {
2
+ var augmentedRules = [];
3
+ Utils.type.ifObject(validation, val => {
4
+ Utils.type.ifObject(val.required, spec => {
5
+ augmentedRules = augmentedRules.concat([
6
+ v => (v ? true : spec.message)
7
+ ]);
8
+ });
9
+ Utils.type.ifObject(val.format, spec => {
10
+ augmentedRules = augmentedRules.concat([
11
+ v => {
12
+ if (v && !v.toString().match(spec.regex)) {
13
+ return spec.message;
14
+ }
15
+ return true;
16
+ }
17
+ ]);
18
+ });
19
+ });
20
+ return augmentedRules;
21
+ }
@@ -103,7 +103,9 @@ export default {
103
103
  const upload = new Uploader(
104
104
  file,
105
105
  this.spec.directUploadUrl,
106
- this.progress
106
+ this.progress,
107
+ this.spec.storagePrefix,
108
+ this.spec.metadata
107
109
  );
108
110
 
109
111
  const input = this.inputElement;
@@ -0,0 +1,132 @@
1
+ <template>
2
+ <div :class="$classes()" :style="$styles()">
3
+ <div class="glib-canvas-container">
4
+ <canvas style="width: 100%; height: 100%" @mousedown="handleDrawStart" @mousemove="handleDrawing"
5
+ @mouseup="handleDrawEnd" @touchstart="handleDrawStart" @touchmove="handleDrawing" @touchend="handleDrawEnd"
6
+ ref="canvas"></canvas>
7
+ <div class="action" v-show="!pen.sign && touched">
8
+ <v-icon color="success" class="bg-white" icon="check_circle" @click="upload()"></v-icon>
9
+ <v-icon color="error" class="bg-white" icon="cancel" @click="clear()"></v-icon>
10
+ </div>
11
+ <v-chip v-show="signedId" class="status" color="success" size="x-small">Saved</v-chip>
12
+ <input type="hidden" :name="fieldName" :value="signedId" :disabled="inputDisabled" />
13
+ <v-input :model-value="signedId" :rules="rules"></v-input>
14
+ </div>
15
+
16
+ </div>
17
+ </template>
18
+
19
+ <style>
20
+ .glib-canvas-container {
21
+ position: relative;
22
+ height: 100%;
23
+ width: 100%;
24
+ border: 2px solid #9ca3af;
25
+ }
26
+
27
+ .glib-canvas-container .action {
28
+ display: flex;
29
+ flex-direction: column;
30
+ position: absolute;
31
+ top: 0;
32
+ right: 0;
33
+ gap: 2px;
34
+ }
35
+
36
+ .glib-canvas-container .status {
37
+ display: flex;
38
+ flex-direction: column;
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+ margin: 4px;
43
+ }
44
+ </style>
45
+
46
+ <script setup>
47
+ import { ref, computed, defineProps } from "vue";
48
+ import * as uploader from "../composable/dropable";
49
+ import { validationRules } from "../composable/validation";
50
+
51
+ const { makeKey, Item } = uploader.useDropableUtils();
52
+ const props = defineProps(['spec']);
53
+
54
+ const rules = validationRules(props.spec.validation);
55
+
56
+ const canvas = ref(null);
57
+ const context = computed(() => {
58
+ if (!canvas.value) return;
59
+
60
+ return canvas.value.getContext('2d');
61
+ });
62
+ const pen = ref({ sign: false, prevX: null, prevY: null });
63
+ const touched = computed(() => pen.value.prevX && pen.value.prevY);
64
+
65
+ const files = ref({});
66
+ const signedId = ref(undefined);
67
+
68
+ uploader.setBusyWhenUploading({ files });
69
+
70
+ function clear() {
71
+ context.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
72
+ signedId.value = null;
73
+ }
74
+
75
+ function upload() {
76
+ files.value = {};
77
+ canvas.value.toBlob((blob) => {
78
+ const key = makeKey();
79
+ const file = new File([blob], 'sign.png');
80
+ files.value[key] = new Item({ el: file, status: 'pending' });
81
+
82
+ uploader.uploadOneFile({ files, key, spec: props.spec, onAfterUploaded: (file) => signedId.value = file.signedId });
83
+ });
84
+ }
85
+
86
+ function penSource(e) {
87
+ let source = {};
88
+ const touch = e.touches ? e.touches[0] : null;
89
+ if (touch) {
90
+ const canvasPos = e.target.getBoundingClientRect();
91
+ const posX = touch.clientX - canvasPos.x;
92
+ const posY = touch.clientY - canvasPos.y;
93
+ source = { posX, posY };
94
+ } else {
95
+ source = { posX: e.offsetX, posY: e.offsetY };
96
+ }
97
+
98
+ return source;
99
+ }
100
+
101
+ function handleDrawing(e) {
102
+ if (!pen.value.sign) return;
103
+
104
+ const source = penSource(e);
105
+ const { prevX, prevY } = pen.value;
106
+
107
+ context.value.beginPath();
108
+ context.value.moveTo(prevX, prevY);
109
+ context.value.lineTo(source.posX, source.posY);
110
+ context.value.stroke();
111
+
112
+ pen.value.prevX = source.posX;
113
+ pen.value.prevY = source.posY;
114
+ }
115
+
116
+ function handleDrawEnd(e) {
117
+ pen.value.sign = false;
118
+ context.value.closePath();
119
+ }
120
+
121
+ function handleDrawStart(e) {
122
+ const source = penSource(e);
123
+ pen.value = {
124
+ sign: true,
125
+ prevX: source.posX,
126
+ prevY: source.posY
127
+ };
128
+ context.value.lineWidth = 2;
129
+ context.value.fillRect(source.posX, source.posY, 2, 2);
130
+ }
131
+
132
+ </script>
@@ -1,15 +1,13 @@
1
1
  <template>
2
2
  <div :style="'width: 100%'" :class="$classes()">
3
- <v-tabs v-model="tab" :bg-color="spec.backgroundColor || 'white'" slider-color="primary" color="primary" show-arrows
3
+ <v-tabs class="bordered-tab" v-model="tab" :bg-color="spec.backgroundColor || 'white'" color="primary" show-arrows
4
4
  :height="spec.height || 60" :grow="$classes().includes('no-grow') ? false : true">
5
- <v-tab v-for="(item, index) in spec.buttons" @click="handleClick(index)" :key="index" height="100%"
6
- :disabled="item.disabled" :value="index">
5
+ <v-tab class="bordered-tab" v-for="(item, index) in spec.buttons" @click="handleClick(index)" :key="index" height="100%"
6
+ :disabled="item.disabled" :value="index">
7
7
  <common-badge :spec="item">
8
8
  <div class="tab-content">
9
- <common-icon :class="index == spec.activeIndex ? 'text-primary' : null" :spec="item.icon"
10
- v-if="item.icon" />
11
- <span :style="{ marginLeft: '2px' }" :class="index == spec.activeIndex ? 'text-primary' : null">{{ item.text
12
- }}</span>
9
+ <common-icon :class="index == spec.activeIndex ? 'text-primary' : null" :spec="item.icon" v-if="item.icon" />
10
+ <span :style="{ marginLeft: '2px' }" :class="index == spec.activeIndex ? 'text-primary' : null">{{ item.text }}</span>
13
11
  </div>
14
12
  </common-badge>
15
13
  </v-tab>
@@ -50,7 +48,6 @@ export default {
50
48
  this.$nextTick(function () {
51
49
  const activeTab = this.buttons[index];
52
50
  const onClick = activeTab?.onClick;
53
-
54
51
  if (onClick) {
55
52
  Action.execute(onClick, this);
56
53
  }
@@ -71,6 +68,7 @@ export default {
71
68
  }
72
69
  };
73
70
  </script>
71
+
74
72
  <style lang="scss" scoped>
75
73
  .v-tab .v-badge span {
76
74
  // Make sure tab label is middle aligned.
@@ -89,4 +87,19 @@ export default {
89
87
  display: flex;
90
88
  padding: 5px;
91
89
  }
90
+
91
+ //Need to use :deep in order to make the slider work
92
+ .bordered-tab {
93
+ :deep(.v-tab__slider) {
94
+ opacity: 1;
95
+ color: #E6E6E6;
96
+ }
97
+ }
98
+
99
+ .bordered-tab {
100
+ :deep(.v-tab--selected .v-tab__slider) {
101
+ opacity: 1;
102
+ color: #0A2A9E;
103
+ }
104
+ }
92
105
  </style>
@@ -43,13 +43,14 @@
43
43
  align-items: center;
44
44
  flex-wrap: nowrap;
45
45
  padding: 12px 24px;
46
- margin-bottom: 8px;
47
46
  cursor: pointer;
48
47
 
49
48
  .label {
50
49
  display: inline-flex;
51
50
  align-items: center;
52
51
  gap: 12px;
52
+ font-weight: 700;
53
+ color: #47495F
53
54
  }
54
55
  }
55
56
 
@@ -60,10 +61,6 @@
60
61
  background-color: #47495F0D;
61
62
  }
62
63
 
63
- .gtreeview .parent.active label {
64
- font-weight: 700;
65
- }
66
-
67
64
  .gtreeview .child.active,
68
65
  .gtreeview .child:hover,
69
66
  .gtreeview .child.hover {
@@ -77,7 +74,7 @@
77
74
  flex-wrap: nowrap;
78
75
  gap: 12px;
79
76
  align-items: center;
80
- margin-bottom: 2px;
77
+ margin-top: 4px;
81
78
  cursor: pointer;
82
79
  }
83
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "3.27.2",
3
+ "version": "3.28.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/utils/history.js CHANGED
@@ -51,7 +51,6 @@ export default class {
51
51
  let skipOnce = false;
52
52
  window.onpopstate = event => {
53
53
  if (skipRendering) {
54
- skipRendering = false
55
54
  return false;
56
55
  }
57
56
 
@@ -110,14 +109,18 @@ export default class {
110
109
  static backWithoutRender() {
111
110
  skipRendering = true
112
111
  this.back()
112
+ skipRendering = false
113
113
  }
114
114
 
115
115
  static destroy() {
116
116
  if (this.navigationCount <= 0) return false;
117
117
 
118
- window.onpopstate = () => { };
118
+ // window.onpopstate = () => { };
119
+ skipRendering = true
119
120
  window.history.go(-this.navigationCount);
120
- this.restoreOnBackOrForward();
121
+ skipRendering = false
122
+ // this.restoreOnBackOrForward();
123
+ this.navigationCount = 0
121
124
 
122
125
  return true;
123
126
  }
package/utils/http.js CHANGED
@@ -59,7 +59,8 @@ export default class {
59
59
  }
60
60
  }
61
61
 
62
- static load(properties, component, windowMode) {
62
+ // Set `forcePushHistory` when it is important to clear the forward history.
63
+ static load(properties, component, windowMode, forcePushHistory) {
63
64
  const urlString = properties["url"];
64
65
  let url;
65
66
  try {
@@ -81,7 +82,8 @@ export default class {
81
82
 
82
83
  Utils.http.execute(properties, "GET", component, (data, response) => {
83
84
  const pushHistory = windowMode ? true : !Utils.type.isObject(dialogs.last());
84
- if (htmlUrl !== currentUrl && pushHistory) {
85
+ // TODO: Check if it is okay to remove this `if` statement so we always push even if it's the same URL.
86
+ if (forcePushHistory || (htmlUrl !== currentUrl && pushHistory)) {
85
87
  const redirectUrl = Utils.url.htmlUrl(response.url);
86
88
  Utils.history.pushPage(data, redirectUrl);
87
89
  }
@@ -143,6 +145,9 @@ export default class {
143
145
  };
144
146
 
145
147
  Utils.http.execute(data, "GET", component, (page, response) => {
148
+ // The execution goes in the following order: onResponse, page render, onLoad
149
+ GLib.action.execute(page.onResponse, component);
150
+
146
151
  Utils.http.forceComponentUpdate(() => {
147
152
  vueApp.page = page;
148
153
  const redirectUrl = Utils.url.htmlUrl(response.url);
package/utils/uploader.js CHANGED
@@ -1,15 +1,79 @@
1
- import { DirectUpload } from "@rails/activestorage";
1
+ import { DirectUpload } from "@rails/activestorage/src/direct_upload";
2
+ import { BlobRecord } from "@rails/activestorage/src/blob_record";
3
+ import { FileChecksum } from "@rails/activestorage/src/file_checksum";
4
+ import { BlobUpload } from "@rails/activestorage/src/blob_upload";
2
5
  import Type from "../utils/type";
3
6
  import mimeType from "../utils/mime_type";
7
+ import { showError } from "../components/composable/alert";
4
8
 
5
9
  const MB_SIZE = 1000;
6
10
 
11
+ function notify(object, methodName, ...messages) {
12
+ if (object && typeof object[methodName] == "function") {
13
+ return object[methodName](...messages);
14
+ }
15
+ }
16
+
17
+ class GlibBlobRecord extends BlobRecord {
18
+ constructor(file, checksum, url, customHeaders = {}, prefix = '', metadata = {}) {
19
+ super(file, checksum, url, customHeaders);
20
+
21
+ this.attributes = Object.assign(this.attributes, { prefix }, { metadata });
22
+ }
23
+ }
24
+
25
+ class GlibDirectUpload extends DirectUpload {
26
+ constructor(file, url, delegate, customHeaders = {}, prefix = '', metadata = {}) {
27
+ super(file, url, delegate, customHeaders);
28
+ this.prefix = prefix;
29
+ this.metadata = { custom: metadata };
30
+ }
31
+
32
+ create(callback) {
33
+ FileChecksum.create(this.file, (error, checksum) => {
34
+ if (error) {
35
+ callback(error);
36
+ return;
37
+ }
38
+
39
+ const blob = new GlibBlobRecord(this.file, checksum, this.url, this.customHeaders, this.prefix, this.metadata);
40
+ notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
41
+
42
+ blob.create(error => {
43
+ if (error) {
44
+ callback(error);
45
+ } else {
46
+ const upload = new BlobUpload(blob);
47
+
48
+ // check if there is prefix
49
+
50
+ const { url } = blob.directUploadData;
51
+ if (this.prefix && !url.includes(this.prefix)) {
52
+ showError({ body: 'Something went wrong', button: 'Dismiss' });
53
+ console.error('prefix not match');
54
+ return;
55
+ }
56
+
57
+ notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr);
58
+ upload.create(error => {
59
+ if (error) {
60
+ callback(error);
61
+ } else {
62
+ callback(null, blob.toJSON());
63
+ }
64
+ });
65
+ }
66
+ });
67
+ });
68
+ }
69
+ }
70
+
7
71
  export default class Uploader {
8
- constructor(file, url, progress) {
72
+ constructor(file, url, progress, prefix, metadata) {
9
73
  this.file = file;
10
74
  this.url = url;
11
75
  this.progress = progress;
12
- this.upload = new DirectUpload(this.file, this.url, this);
76
+ this.upload = new GlibDirectUpload(this.file, this.url, this, {}, prefix, metadata);
13
77
  }
14
78
 
15
79
  start(callback) {