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 +21 -0
- package/actions/bottom_banners/open.js +2 -1
- package/actions/http/retry.js +22 -6
- package/app.vue +29 -0
- package/components/composable/upload_delegator.js +4 -4
- package/components/fields/_select.vue +10 -3
- package/components/fields/richText2.vue +2 -1
- package/components/mixins/events.js +14 -18
- package/components/panels/timeline.vue +6 -3
- package/cypress/e2e/glib-web/lifecycle.cy.ts +12 -1
- package/package.json +1 -1
- package/templates/thumbnail.vue +11 -0
- package/.claude/settings.local.json +0 -18
- package/AGENT.md +0 -11
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
|
-
|
|
9
|
+
// Always replace the banner so page-specific specs stay in sync across navigation
|
|
10
|
+
vueApp.bottomBanners[targetId] = newView;
|
|
10
11
|
}
|
|
11
12
|
}
|
package/actions/http/retry.js
CHANGED
|
@@ -7,8 +7,11 @@ const wait = ms => new Promise((resolve) => {
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
const retryWithDelay = async (
|
|
10
|
-
fn,
|
|
11
|
-
|
|
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')
|
|
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(
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
210
|
+
const value = newValue ?? this.fieldModel;
|
|
206
211
|
console.log("NEW VALUE1", value);
|
|
207
|
-
|
|
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
|
-
|
|
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
package/templates/thumbnail.vue
CHANGED
|
@@ -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.
|