glib-web 4.39.7 → 4.40.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/AGENTS.md ADDED
@@ -0,0 +1,21 @@
1
+
2
+ ## References
3
+ - Note that some of the references are located in directories pointed by symbolic links.
4
+ - Please report if you cannot access any of the referenced documentation or examples.
5
+
6
+ ## System architecture
7
+ - Read [ui_architecture.md](doc/common/ui_architecture.md)
8
+ - This project implements the frontend components of the `glib` UI framework mentioned
9
+ in the architecture above.
10
+
11
+ ## Project guidelines
12
+ - For working examples on how to use `glib` UI components, see all the jbuilder
13
+ files located in `doc/garage/`.
14
+
15
+ ## Behaviour and tendency
16
+ - Read [behavior.md](doc/common/ai/behavior.md)
17
+
18
+ ## Rules
19
+ * Always use `utils/type.js` to check types
20
+ - For example, avoid checking against `undefined` manually.
21
+
@@ -6,6 +6,7 @@ export default class {
6
6
 
7
7
  if (!targetId) console.error('targetId is required');
8
8
 
9
- vueApp.bottomBanners[targetId] ||= newView;
9
+ // Always replace the banner so page-specific specs stay in sync across navigation
10
+ vueApp.bottomBanners[targetId] = newView;
10
11
  }
11
12
  }
@@ -7,8 +7,11 @@ const wait = ms => new Promise((resolve) => {
7
7
  });
8
8
 
9
9
  const retryWithDelay = async (
10
- fn, retries = settings.http.retries, interval = settings.http.interval,
11
- finalErr = 'Retry failed'
10
+ fn,
11
+ retries = settings.http.retries,
12
+ interval = settings.http.interval,
13
+ finalErr = 'Retry failed',
14
+ onFinal
12
15
  ) => {
13
16
  try {
14
17
  // try
@@ -17,6 +20,7 @@ const retryWithDelay = async (
17
20
  // if no retries left
18
21
  // throw error
19
22
  if (retries <= 0) {
23
+ if (onFinal) onFinal(err);
20
24
  return Promise.reject(finalErr);
21
25
  }
22
26
 
@@ -24,7 +28,7 @@ const retryWithDelay = async (
24
28
  await wait(interval);
25
29
 
26
30
  //recursively call the same func
27
- return retryWithDelay(fn, (retries - 1), interval, finalErr);
31
+ return retryWithDelay(fn, (retries - 1), interval, finalErr, onFinal);
28
32
  }
29
33
  };
30
34
 
@@ -39,7 +43,9 @@ const httpExecuteWithRetry = (methodName, spec, component) => {
39
43
  Action.handleResponse(response, component);
40
44
  },
41
45
  error => {
42
- if (error == 'Server error') err = new Error(error);
46
+ if (error == 'Server error') {
47
+ err = new Error(error);
48
+ }
43
49
  }
44
50
  );
45
51
 
@@ -48,7 +54,17 @@ const httpExecuteWithRetry = (methodName, spec, component) => {
48
54
  if (err) throw err;
49
55
  };
50
56
 
51
- retryWithDelay(func, spec.retryLimit);
57
+ retryWithDelay(
58
+ func,
59
+ spec.retryLimit,
60
+ undefined,
61
+ undefined,
62
+ err => {
63
+ if (!spec?.silent) {
64
+ Utils.launch.snackbar.error("We're having trouble right now. Please try again.", component);
65
+ }
66
+ }
67
+ );
52
68
  };
53
69
 
54
- export { retryWithDelay, httpExecuteWithRetry };
70
+ export { retryWithDelay, httpExecuteWithRetry };
package/app.vue CHANGED
@@ -118,6 +118,9 @@ export default {
118
118
  title: "...",
119
119
  mainHeight: 0,
120
120
  actionCableConsumer: null,
121
+ tabWasBlurred: false,
122
+ onWindowFocus: null,
123
+ onWindowBlur: null,
121
124
  };
122
125
  },
123
126
  computed: {
@@ -179,12 +182,38 @@ export default {
179
182
  if (isRerender) GLib.action.execute(this.page.onRerender, this);
180
183
  }
181
184
  });
185
+ this.bindForegroundListeners();
182
186
  watchGlibEvent();
183
187
  },
188
+ beforeUnmount() {
189
+ this.unbindForegroundListeners();
190
+ },
184
191
  methods: {
185
192
  closeSheet() {
186
193
  this.vueApp.sheet.show = false;
187
194
  },
195
+ bindForegroundListeners() {
196
+ this.onWindowBlur = () => {
197
+ this.tabWasBlurred = true;
198
+ };
199
+ this.onWindowFocus = () => {
200
+ this.maybeExecuteOnForeground();
201
+ };
202
+
203
+ window.addEventListener('blur', this.onWindowBlur);
204
+ window.addEventListener('focus', this.onWindowFocus);
205
+ },
206
+ unbindForegroundListeners() {
207
+ window.removeEventListener('blur', this.onWindowBlur);
208
+ window.removeEventListener('focus', this.onWindowFocus);
209
+ },
210
+ maybeExecuteOnForeground() {
211
+ if (!this.page || !this.page.onForeground) return;
212
+ if (!this.tabWasBlurred) return; // Only fire after the tab has lost focus/visibility.
213
+
214
+ this.tabWasBlurred = false;
215
+ GLib.action.execute(this.page.onForeground, this);
216
+ },
188
217
  handleActionCable(val) {
189
218
  if (val.actionCable) {
190
219
  const { customer } = useSocket(val.actionCable, this.actionCableConsumer);
@@ -1,4 +1,4 @@
1
- import { vueApp } from "../../store";
1
+ import eventBus from "../../utils/eventBus";
2
2
  import { useFileUtils, validateFiles } from "./file";
3
3
 
4
4
  const { makeKey } = useFileUtils();
@@ -8,8 +8,8 @@ function uploadFiles(obj) {
8
8
 
9
9
  if (!validateFiles({ newFiles: droppedFiles, files, spec })) return;
10
10
 
11
- Object.assign(
12
- vueApp.uploader,
11
+ eventBus.$emit(
12
+ 'uploader/delegate',
13
13
  {
14
14
  files: Array.from(droppedFiles).reduce((prev, curr) => {
15
15
  prev[makeKey()] = curr;
@@ -24,4 +24,4 @@ function uploadFiles(obj) {
24
24
  function submitOnAllUploaded(obj) { }
25
25
  function setBusyWhenUploading(obj) { }
26
26
 
27
- export { uploadFiles, submitOnAllUploaded, setBusyWhenUploading };
27
+ export { uploadFiles, submitOnAllUploaded, setBusyWhenUploading };
@@ -2,7 +2,7 @@
2
2
  <div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <!-- Set `menu-props` so the menu will never be wider than the select field.
4
4
  See https://github.com/vuetifyjs/vuetify/issues/17751 -->
5
- <component :is="compName" :color="gcolor" v-model="fieldModel" :label="label" :items="normalizedOptions"
5
+ <component ref="comp" :is="compName" :color="gcolor" v-model="fieldModel" :label="label" :items="normalizedOptions"
6
6
  :chips="useChips" :disabled="inputDisabled" :multiple="spec.multiple" :readonly="spec.readOnly"
7
7
  :clearable="spec.clearable" :placeholder="spec.placeholder" :rules="$validation()" persistent-hint
8
8
  :append-icon="append.icon" validate-on="blur" item-title='text' :variant="variant" :closable-chips="spec.multiple"
@@ -62,7 +62,7 @@ import { triggerOnChange, triggerOnInput, useGlibInput } from "../composable/for
62
62
  import { isBoolean } from '../../utils/type';
63
63
 
64
64
  import { useGlibSelectable, watchNoneOfAbove } from '../composable/selectable';
65
- import { ref } from 'vue';
65
+ import { ref, defineExpose } from 'vue';
66
66
  import SelectItemDefault from "./_selectItemDefault.vue";
67
67
  import SelectItemWithImage from "./_selectItemWithImage.vue";
68
68
  import SelectItemWithIcon from "./_selectItemWithIcon.vue";
@@ -83,6 +83,7 @@ export default {
83
83
 
84
84
  const fieldModel = ref(props.spec.value || props.defaultValue);
85
85
  const options = ref(props.spec.options);
86
+ const comp = ref(null);
86
87
  const append = props.spec.append || {};
87
88
 
88
89
  const valueForDisableAll = props.spec.valueForDisableAll;
@@ -91,7 +92,13 @@ export default {
91
92
  watchNoneOfAbove({ model: fieldModel, options: options, valueForDisableAll });
92
93
  }
93
94
 
94
- return { fieldModel, checkAll, isIndeterminate, isAllSelected, append };
95
+ // This is a public method that is called by other parts of the code, do not delete it.
96
+ function toggle() {
97
+ comp.value.menu = !comp.value.menu;
98
+ }
99
+ defineExpose(['toggle']);
100
+
101
+ return { fieldModel, checkAll, isIndeterminate, isAllSelected, append, toggle, comp };
95
102
  },
96
103
  data() {
97
104
  return {
@@ -260,7 +260,8 @@ export default defineComponent({
260
260
  quill.on('text-change', eventFiltering.debounce(function () {
261
261
  producedValue.value = sanitizedValue();
262
262
  nextTick(() => {
263
- Action.executeWithFormData(props.spec.onChange || props.spec.onChangeAndLoad, instance.ctx, producedValue.value);
263
+ Action.executeWithFormData(props.spec.onChange, instance.ctx, producedValue.value);
264
+ Action.executeWithFormData(props.spec.onChangeAndLoad, instance.ctx, producedValue.value);
264
265
  });
265
266
  }));
266
267
 
@@ -2,6 +2,7 @@ import Action from "../../action";
2
2
  import UrlUtils from "../../utils/url";
3
3
  import * as TypeUtils from "../../utils/type";
4
4
  import { isRerender, vueApp } from "../../store";
5
+ import { nextTick } from "vue";
5
6
 
6
7
  export default {
7
8
  data() {
@@ -39,7 +40,11 @@ export default {
39
40
  this._ready();
40
41
  this.$mounted();
41
42
  if (this.spec && this.spec.onChangeAndLoad && this.$registryEnabled()) {
42
- this.$executeOnChange();
43
+ nextTick(() => {
44
+ const value = this.fieldModel;
45
+ console.log("NEW VALUE1", value);
46
+ GLib.action.executeWithFormData(this.spec.onChangeAndLoad, this, value);
47
+ });
43
48
  }
44
49
 
45
50
  if (this.spec && this.$registryEnabled()) {
@@ -202,25 +207,16 @@ export default {
202
207
  },
203
208
  $executeOnChange(newValue) {
204
209
  this.$nextTick(() => {
205
- const value = newValue || this.fieldModel;
210
+ const value = newValue ?? this.fieldModel;
206
211
  console.log("NEW VALUE1", value);
207
- GLib.action.executeWithFormData(this.spec.onChange || this.spec.onChangeAndLoad, this, value);
208
- });
212
+ if (this.spec.onChange) {
213
+ GLib.action.executeWithFormData(this.spec.onChange, this, value);
214
+ }
215
+ if (this.spec.onChangeAndLoad) {
216
+ GLib.action.executeWithFormData(this.spec.onChangeAndLoad, this, value);
217
+ }
209
218
 
210
- // Utils.type.ifObject(this.spec.onChange || this.spec.onChangeAndLoad, onChange => {
211
- // this.$nextTick(() => {
212
- // const value = newValue || this.fieldModel;
213
- // const formData = {
214
- // [this.spec.paramNameForFieldName || this.fieldName]: value
215
- // };
216
- // const params = {
217
- // [this.spec.paramNameForFormData || 'formData']: formData
218
- // };
219
-
220
- // const data = merge({}, onChange, params);
221
- // GLib.action.execute(data, this);
222
- // });
223
- // });
219
+ });
224
220
  }
225
221
  }
226
222
  };
@@ -1,10 +1,9 @@
1
1
  <template>
2
2
  <!-- Paddings cannot be applied to v-timeline directly -->
3
3
  <div :style="$styles()" :class="$classes()" v-if="loadIf">
4
- <v-timeline v-if="events" :density="density" :truncate-line="spec.truncateLine" align-top>
5
- <!-- <v-timeline-item v-for="(item, index) in events" :key="index" :color="item.backgroundColor || 'white'" -->
4
+ <v-timeline v-if="events" :density="density" :truncate-line="spec.truncateLine" align-top :direction="spec.direction">
6
5
  <v-timeline-item v-for="(item, index) in events" :key="index" :density="density" :size="itemSize(item)"
7
- :dot-color="item.backgroundColor || 'transparent'" :hide-dot="item.hideDot" :fill-dot="item.fillDot">
6
+ :dot-color="item.backgroundColor || 'transparent'" :hide-dot="item.hideDot" :fill-dot="item.fillDot" :side="spec.side">
8
7
  <template v-slot:icon>
9
8
  <div :class="itemClasses(item).includes('outlined') ? 'outlined-dots' : ''">
10
9
  <div v-if="item.text" class="number-circle" :style="{ color: item.color }">
@@ -35,6 +34,10 @@ export default {
35
34
  density() {
36
35
  return determineDensity(this.spec.styleClasses);
37
36
  },
37
+ itemSide() {
38
+ // Options: 'start' (left), 'end' (right), or undefined (alternating)
39
+ return this.spec.alignSide || undefined;
40
+ }
38
41
  },
39
42
  data() {
40
43
  return {
@@ -28,4 +28,15 @@ describe('glib lifecycle hooks', () => {
28
28
  cy.get('.views-tooltip p').should('have.text', 'onRerender')
29
29
  cy.get('#icon').should('contain.text', 'person')
30
30
  })
31
- })
31
+
32
+ it('execute onForeground after leaving and returning to tab', () => {
33
+ cy.visit(url)
34
+
35
+ cy.window().then((win) => {
36
+ win.dispatchEvent(new Event('blur'))
37
+ win.dispatchEvent(new Event('focus'))
38
+ })
39
+
40
+ cy.contains('page.onForeground').should('be.exist')
41
+ })
42
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "4.39.7",
3
+ "version": "4.40.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -264,6 +264,17 @@ export default {
264
264
  .subsubtitle {
265
265
  margin-top: 5px;
266
266
  }
267
+
268
+ .templates-thumbnail {
269
+ &:hover {
270
+ // Enhance button hover effect within hoverable thumbnails.
271
+ .views-button:hover {
272
+ &> .v-btn__overlay {
273
+ opacity: 0.12;
274
+ }
275
+ }
276
+ }
277
+ }
267
278
  </style>
268
279
 
269
280
  <style lang="scss" scoped>
@@ -1,18 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Read(//home/hgani/workspace/glib-web/app/views/json_ui/garage/**)",
5
- "Read(//home/hgani/workspace/glib-web-npm/doc/garage/**)",
6
- "Read(//home/hgani/workspace/glib-web-npm/doc/common/**)",
7
- "Bash(find:*)",
8
- "Bash(npx cypress run:*)",
9
- "Read(//home/hgani/workspace/glib-web/**)",
10
- "Bash(curl:*)",
11
- "Bash(pkill:*)",
12
- "Bash(gh pr list:*)",
13
- "WebSearch"
14
- ],
15
- "deny": [],
16
- "ask": []
17
- }
18
- }
package/AGENT.md DELETED
@@ -1,11 +0,0 @@
1
- * System architecture
2
- * Read [ui_architecture.md](doc/common/ui_architecture.md)
3
- * This project implements the frontend components of the `glib` UI framework mentioned
4
- in the architecture above.
5
-
6
- * Project guidelines
7
- * For working examples on how to use `glib` UI components, see all the jbuilder
8
- files located in `doc/garage/`.
9
-
10
- * Always use `utils/type.js` to check types
11
- * For example, avoid checking against `undefined` manually.