glib-web 4.16.1 → 4.18.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/actions/windows/closeAll.js +5 -0
- package/actions/windows/closeAllWithOpen.js +5 -0
- package/actions/windows/closeWithOpen.js +7 -1
- package/actions/windows/closeWithReload.js +6 -1
- package/components/_icon.vue +2 -1
- package/components/_internal_button.vue +5 -10
- package/components/composable/form.js +8 -1
- package/components/composable/upload.js +2 -2
- package/components/fields/radio.vue +0 -4
- package/components/fields/radioGroup.vue +6 -1
- package/components/fields/richText.vue +0 -2
- package/components/panels/bulkEdit2.vue +21 -17
- package/components/progressbar.vue +2 -2
- package/cypress/e2e/glib-web/dirtyState.cy.ts +75 -0
- package/package.json +1 -1
- package/store.js +1 -1
- package/utils/http.js +2 -0
- package/cypress/e2e/glib-web/formDynamic.cy.ts +0 -52
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { executeGlibEvent } from "../../store";
|
|
1
2
|
import { ifObject } from "../../utils/type";
|
|
2
3
|
|
|
3
4
|
export default class {
|
|
4
5
|
execute(properties, component) {
|
|
5
6
|
|
|
7
|
+
if (!executeGlibEvent('onbeforewindowsclose')) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
Utils.history.destroy();
|
|
7
12
|
|
|
8
13
|
ifObject(properties["onClose"], it => {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import Action from "../../action";
|
|
2
|
+
import { executeGlibEvent } from "../../store";
|
|
2
3
|
|
|
3
4
|
export default class {
|
|
4
5
|
execute(properties, component) {
|
|
6
|
+
if (!executeGlibEvent('onbeforewindowsclose')) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
Utils.history.destroy();
|
|
6
11
|
|
|
7
12
|
// Don't use nextTick so that there is no visible flicker.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { executeGlibEvent } from "../../store";
|
|
2
|
+
|
|
1
3
|
export default class {
|
|
2
4
|
execute(properties, component) {
|
|
3
|
-
|
|
5
|
+
if (!executeGlibEvent('onbeforewindowsclose')) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
Utils.history.backWithoutRender();
|
|
4
10
|
|
|
5
11
|
// Don't use nextTick so that there is no visible flicker.
|
|
6
12
|
Utils.http.load(properties, component, true, true);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { executeGlibEvent } from "../../store";
|
|
1
2
|
import Hash from "../../utils/hash";
|
|
2
3
|
|
|
3
4
|
export default class {
|
|
4
5
|
execute(properties, component) {
|
|
6
|
+
if (!executeGlibEvent('onbeforewindowsclose')) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
const fallbackUrl = new Hash(properties).remove("fallbackUrl");
|
|
6
11
|
const data = Object.assign({}, properties);
|
|
7
12
|
if (!Utils.history.back()) {
|
|
@@ -11,7 +16,7 @@ export default class {
|
|
|
11
16
|
|
|
12
17
|
// Allow time for history.back() to complete, which is important for actions that need
|
|
13
18
|
// to use window.location.href such as `windows/reload`
|
|
14
|
-
setTimeout(function() {
|
|
19
|
+
setTimeout(function () {
|
|
15
20
|
Utils.http.reload(data, component);
|
|
16
21
|
}, 100);
|
|
17
22
|
}
|
package/components/_icon.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-icon class="aligner" :style="$styles()" :class="cssClasses" :size="icon.size" :color="color">{{
|
|
3
3
|
icon.name
|
|
4
|
-
|
|
4
|
+
}}</v-icon>
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script>
|
|
@@ -36,6 +36,7 @@ export default {
|
|
|
36
36
|
return this.$classes();
|
|
37
37
|
},
|
|
38
38
|
value() {
|
|
39
|
+
// TODO: is this still relevant?
|
|
39
40
|
return this.isBusy ? "autorenew" : this.name;
|
|
40
41
|
},
|
|
41
42
|
icon() {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- Use `click.prevent` to prevent bubbling when clicking a dropdown button that is
|
|
3
3
|
located in a list row that has href. -->
|
|
4
|
-
<v-btn :type="type" :disabled="
|
|
5
|
-
:
|
|
6
|
-
:
|
|
7
|
-
@click.prevent="
|
|
4
|
+
<v-btn :type="type" :disabled="spec.disabled" :style="styles()" :class="$classes()" :href="$href()" :rel="$rel()"
|
|
5
|
+
:variant="variant" :rounded="$classes().includes('rounded') || null" :density="density" :size="size" :color="color"
|
|
6
|
+
:active="$classesInclude('active')" :icon="$classes().includes('icon') ? $vuetify : null" @click.prevent="
|
|
8
7
|
type == 'submit' ? $dispatchEvent('forms/submit') : $onClick()
|
|
9
8
|
">
|
|
10
9
|
<!-- <span v-if="spec.icon"><common-icon :spec="spec.icon || {}" /></span> -->
|
|
@@ -16,9 +15,9 @@
|
|
|
16
15
|
</template>
|
|
17
16
|
|
|
18
17
|
<script>
|
|
19
|
-
import {
|
|
18
|
+
import { nextTick } from "vue";
|
|
20
19
|
import { determineColor, determineDensity, determineVariant, determineSize } from '../utils/constant';
|
|
21
|
-
|
|
20
|
+
|
|
22
21
|
|
|
23
22
|
export default {
|
|
24
23
|
props: {
|
|
@@ -27,10 +26,6 @@ export default {
|
|
|
27
26
|
hideTextOnXs: { type: Boolean },
|
|
28
27
|
eventHandlers: { type: Object, default: null }
|
|
29
28
|
},
|
|
30
|
-
setup() {
|
|
31
|
-
const isBusy = computed(() => vueApp.isBusy);
|
|
32
|
-
return { isBusy };
|
|
33
|
-
},
|
|
34
29
|
computed: {
|
|
35
30
|
color() {
|
|
36
31
|
// return this.spec.color
|
|
@@ -41,6 +41,7 @@ const getFormData = (el, ignoredFields = new Set()) => {
|
|
|
41
41
|
function useGlibForm({ formRef }) {
|
|
42
42
|
const initFormData = ref({});
|
|
43
43
|
const currentFormData = ref({});
|
|
44
|
+
const changed = ref(false);
|
|
44
45
|
const el = ref(null);
|
|
45
46
|
const registeredInputs = ref([]);
|
|
46
47
|
const ignoredDirtyCheckFields = ref(new Set());
|
|
@@ -56,7 +57,12 @@ function useGlibForm({ formRef }) {
|
|
|
56
57
|
el.value = htmlElement(formRef.value);
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
watch(registeredInputs, (value) => {
|
|
60
|
+
const inputRegistrationWatcher = watch(registeredInputs, (value) => {
|
|
61
|
+
if (changed.value) {
|
|
62
|
+
inputRegistrationWatcher(); // remove watcher
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
initFormData.value = getFormData(el.value, ignoredDirtyCheckFields.value);
|
|
61
67
|
currentFormData.value = initFormData.value;
|
|
62
68
|
|
|
@@ -69,6 +75,7 @@ function useGlibForm({ formRef }) {
|
|
|
69
75
|
|
|
70
76
|
const updateDirtyState = (event) => {
|
|
71
77
|
currentFormData.value = getFormData(el.value, ignoredDirtyCheckFields.value);
|
|
78
|
+
changed.value = true;
|
|
72
79
|
dispatchEv();
|
|
73
80
|
};
|
|
74
81
|
|
|
@@ -4,12 +4,12 @@ import Uploader from "../../utils/glibDirectUpload";
|
|
|
4
4
|
import { vueApp } from "../../store";
|
|
5
5
|
import { useFilesState, useFileUtils, validateFile } from "./file";
|
|
6
6
|
|
|
7
|
-
function submitOnAllUploaded({ url, formData, files }) {
|
|
7
|
+
function submitOnAllUploaded({ url, formData, files, actionName }) {
|
|
8
8
|
const { uploaded } = useFilesState(files);
|
|
9
9
|
return watch(uploaded, (val) => {
|
|
10
10
|
if (val) {
|
|
11
11
|
GLib.action.execute({
|
|
12
|
-
action: 'http/post',
|
|
12
|
+
action: actionName || 'http/post',
|
|
13
13
|
url: url.value,
|
|
14
14
|
formData: formData.value
|
|
15
15
|
}, {});
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
|
|
20
20
|
import ThumbnailRadio from "../fields/radio/_thumbnail.vue";
|
|
21
21
|
import FeaturedRadio from "../fields/radio/_featured.vue";
|
|
22
|
-
import { useGlibInput } from "../composable/form";
|
|
23
22
|
|
|
24
23
|
export default {
|
|
25
24
|
components: {
|
|
@@ -29,9 +28,6 @@ export default {
|
|
|
29
28
|
props: {
|
|
30
29
|
spec: { type: Object, required: true }
|
|
31
30
|
},
|
|
32
|
-
setup(props) {
|
|
33
|
-
useGlibInput({ props });
|
|
34
|
-
},
|
|
35
31
|
inject: ['radioGroupCtx'],
|
|
36
32
|
computed: {
|
|
37
33
|
isActive() {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
16
|
<script>
|
|
17
|
+
import { useGlibInput } from '../composable/form';
|
|
18
|
+
|
|
17
19
|
export default {
|
|
18
20
|
props: {
|
|
19
21
|
spec: { type: Object, required: true },
|
|
@@ -23,11 +25,14 @@ export default {
|
|
|
23
25
|
radioGroupCtx: this
|
|
24
26
|
};
|
|
25
27
|
},
|
|
28
|
+
setup(props) {
|
|
29
|
+
useGlibInput({ props });
|
|
30
|
+
},
|
|
26
31
|
computed: {
|
|
27
32
|
childrenSpec() {
|
|
28
33
|
return {
|
|
29
34
|
childViews: this.spec.childViews
|
|
30
|
-
}
|
|
35
|
+
};
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
methods: {
|
|
@@ -303,7 +303,6 @@ export default {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
this.$executeOnChange(this.producedValue);
|
|
306
|
-
if (this.$refs.container) triggerOnInput(this.$refs.container);
|
|
307
306
|
|
|
308
307
|
}),
|
|
309
308
|
onRawTextEditorChanged: eventFiltering.debounce(function () {
|
|
@@ -314,7 +313,6 @@ export default {
|
|
|
314
313
|
);
|
|
315
314
|
|
|
316
315
|
this.$executeOnChange(this.producedValue);
|
|
317
|
-
if (this.$refs.container) triggerOnInput(this.$refs.container);
|
|
318
316
|
}),
|
|
319
317
|
insertImage: function (file, Editor, cursorLocation, blob) {
|
|
320
318
|
let vm = this;
|
|
@@ -92,6 +92,7 @@ import Action from "../../action";
|
|
|
92
92
|
import Hash from "../../utils/hash";
|
|
93
93
|
import Papa from "papaparse";
|
|
94
94
|
import launch from "../../utils/launch";
|
|
95
|
+
import { isPresent } from "../../utils/type";
|
|
95
96
|
|
|
96
97
|
class Row {
|
|
97
98
|
constructor({ id, columns, selected, index, status }) {
|
|
@@ -128,14 +129,22 @@ class Row {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
class Cell {
|
|
131
|
-
constructor({
|
|
132
|
-
this.viewHeaders = viewHeaders;
|
|
132
|
+
constructor({ cellIndex, rowId, dataRow, viewCell }) {
|
|
133
133
|
this.cellIndex = cellIndex;
|
|
134
134
|
this.rowId = rowId;
|
|
135
|
-
this.
|
|
135
|
+
this.cellId = props.spec.viewHeaders[this.cellIndex].id;
|
|
136
|
+
|
|
137
|
+
let value = dataRow.columns[cellIndex].value;
|
|
138
|
+
|
|
139
|
+
// clean value
|
|
140
|
+
if (!value) value = undefined;
|
|
141
|
+
if (value && value.includes(',')) value = value.split(',');
|
|
136
142
|
this.value = value;
|
|
137
143
|
|
|
138
|
-
|
|
144
|
+
const name = viewCell.multiple ? `${this.cellId}[]` : this.cellId;
|
|
145
|
+
|
|
146
|
+
this.view = Object.assign({}, viewCell, dataRow.columns[cellIndex], { value: value, name: name });
|
|
147
|
+
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
150
|
|
|
@@ -246,17 +255,8 @@ function makeRows(dataRows, selected = false) {
|
|
|
246
255
|
const rows = dataRows.map((dataRow, index) => {
|
|
247
256
|
// const id = dataRow.rowId || Math.random().toString(36).slice(2, 7);
|
|
248
257
|
const id = dataRow.rowId || `row_${index}`;
|
|
249
|
-
const columns = props.spec.viewCells.map((
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// clean the data
|
|
253
|
-
if (!value) value = undefined;
|
|
254
|
-
if (value && value.includes(',')) value = value.split(',');
|
|
255
|
-
|
|
256
|
-
const view = Object.assign({}, viewCells, dataRow.columns[i], { value: value });
|
|
257
|
-
const cellIndex = i;
|
|
258
|
-
const viewHeaders = props.spec.viewHeaders;
|
|
259
|
-
return new Cell({ rowId: id, view, cellIndex, viewHeaders, value });
|
|
258
|
+
const columns = props.spec.viewCells.map((viewCell, i) => {
|
|
259
|
+
return new Cell({ rowId: id, cellIndex: i, dataRow, viewCell });
|
|
260
260
|
});
|
|
261
261
|
return new Row({ selected, columns, id, index });
|
|
262
262
|
});
|
|
@@ -275,7 +275,8 @@ function handleCellChange(e, cell) {
|
|
|
275
275
|
if (e.target instanceof HTMLInputElement) {
|
|
276
276
|
value = e.target.value;
|
|
277
277
|
} else {
|
|
278
|
-
value = e.target.
|
|
278
|
+
value = Array.from(e.target.querySelectorAll(`input[name="${cell.view.name}"]`)).map((el) => el.value);
|
|
279
|
+
value = cell.view.multiple ? value : value[0];
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
cell.value = value;
|
|
@@ -361,8 +362,11 @@ function submitRows(rows) {
|
|
|
361
362
|
updateRow({ rowId: row.id, status: 'pending' });
|
|
362
363
|
|
|
363
364
|
const formData = row.columns.reduce((prev, curr) => {
|
|
365
|
+
let value = curr.value;
|
|
366
|
+
if (curr.view.multiple && isPresent(curr.value)) value = [value].flat();
|
|
367
|
+
if (curr.view.multiple && !isPresent(curr.value)) value = [''];
|
|
364
368
|
return Object.assign(prev, {
|
|
365
|
-
[curr.cellId]:
|
|
369
|
+
[curr.cellId]: value || '',
|
|
366
370
|
[props.spec.paramNameForRowId || 'rowId']: row.id
|
|
367
371
|
});
|
|
368
372
|
}, {});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-progress-linear :class="$classes()" :height="spec.height
|
|
2
|
+
<v-progress-linear :class="$classes()" :height="spec.height" :color="spec.color"
|
|
3
3
|
:background-color="spec.backgroundColor" :disabled="true" :model-value="value_in_percentage"
|
|
4
|
-
:striped="$classes().includes('striped')" :reverse="spec.reversed || false" rounded>
|
|
4
|
+
:striped="$classes().includes('striped')" :reverse="spec.reversed || false" :rounded="spec.rounded">
|
|
5
5
|
<strong v-if="!$classes().includes('no-text')">{{ Math.ceil(value_in_percentage) }}%</strong>
|
|
6
6
|
</v-progress-linear>
|
|
7
7
|
</template>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { testPageUrl } from "../../helper"
|
|
2
|
+
const url = testPageUrl('dirty_state')
|
|
3
|
+
|
|
4
|
+
describe('dirtyState', () => {
|
|
5
|
+
it('can be disabled', () => {
|
|
6
|
+
cy.visit(url)
|
|
7
|
+
|
|
8
|
+
cy.get('input[name="user[dirty_check_disabled]"]')
|
|
9
|
+
.type('TEST TEST TEST')
|
|
10
|
+
.blur()
|
|
11
|
+
|
|
12
|
+
cy.reload()
|
|
13
|
+
|
|
14
|
+
cy.get('input[name="user[dirty_check_disabled]"]').should('be.empty')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('dirty if not equal to init value', () => {
|
|
18
|
+
cy.visit(url)
|
|
19
|
+
|
|
20
|
+
cy.contains('Male').click();
|
|
21
|
+
|
|
22
|
+
cy.on('window:confirm', (str) => {
|
|
23
|
+
cy.then(() => expect(str).to.equal('Changes you made have not been saved. Are you sure?'))
|
|
24
|
+
return false;
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
cy.contains('multiupload').click()
|
|
28
|
+
cy.contains('Female').click()
|
|
29
|
+
cy.contains('multiupload').click() // Try to navigate to another page
|
|
30
|
+
cy.get('h2').should('contain.text', 'MultiUpload')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('have different context between window and dialog', () => {
|
|
34
|
+
cy.visit(url)
|
|
35
|
+
|
|
36
|
+
cy.contains('Dialog Form').click()
|
|
37
|
+
|
|
38
|
+
cy.get('input[name="user[name]"]').click().type('John Doe')
|
|
39
|
+
|
|
40
|
+
cy.contains('cancel').click()
|
|
41
|
+
|
|
42
|
+
let text = '';
|
|
43
|
+
cy.on('window:confirm', (str) => {
|
|
44
|
+
text = str;
|
|
45
|
+
return true;
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
cy.then(() => expect(text).to.equal('Changes you made have not been saved. Are you sure?'))
|
|
49
|
+
|
|
50
|
+
cy.contains('multiupload').click() // Try to navigate to another page
|
|
51
|
+
cy.get('h2').should('contain.text', 'MultiUpload')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('pop on history back', () => {
|
|
55
|
+
cy.visit(testPageUrl('multiupload'))
|
|
56
|
+
cy.contains('dirty_state').click()
|
|
57
|
+
|
|
58
|
+
cy.contains('choice2').click()
|
|
59
|
+
|
|
60
|
+
cy.go('back')
|
|
61
|
+
|
|
62
|
+
let text = ''
|
|
63
|
+
cy.on('window:confirm', (str) => {
|
|
64
|
+
text = str;
|
|
65
|
+
return false;
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
cy.then(() => expect(text).to.equal('Changes you made have not been saved. Are you sure?'))
|
|
69
|
+
|
|
70
|
+
// dirty state is removed after second try
|
|
71
|
+
cy.go('back')
|
|
72
|
+
|
|
73
|
+
cy.get('h2').should('contain.text', 'MultiUpload')
|
|
74
|
+
})
|
|
75
|
+
})
|
package/package.json
CHANGED
package/store.js
CHANGED
package/utils/http.js
CHANGED
|
@@ -289,11 +289,13 @@ export default class {
|
|
|
289
289
|
static startIndicator(component) {
|
|
290
290
|
if (component) realComponent(component)._disabled = true;
|
|
291
291
|
this._showIndicator();
|
|
292
|
+
vueApp.isBusy = true;
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
static stopIndicator(component) {
|
|
295
296
|
if (component) realComponent(component)._disabled = false;
|
|
296
297
|
this._hideIndicator();
|
|
298
|
+
vueApp.isBusy = false;
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
static _showIndicator() {
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { testPageUrl } from "../../helper"
|
|
2
|
-
const url = testPageUrl('form_dynamic')
|
|
3
|
-
|
|
4
|
-
describe('form', () => {
|
|
5
|
-
it('dynamic group', () => {
|
|
6
|
-
cy.visit(url)
|
|
7
|
-
|
|
8
|
-
cy.get('.text-success').click()
|
|
9
|
-
|
|
10
|
-
cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
|
|
11
|
-
|
|
12
|
-
cy.get('#input-17').type('Rate your breakfast')
|
|
13
|
-
cy.get('#input-19').click()
|
|
14
|
-
cy.get('.v-overlay-container').contains('Rating').click()
|
|
15
|
-
|
|
16
|
-
cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
|
|
17
|
-
|
|
18
|
-
cy.get(':nth-child(3) > :nth-child(1) > [style="display: block;"] > .group-wrapper > .text-error').click()
|
|
19
|
-
|
|
20
|
-
cy.contains('Confirm').click()
|
|
21
|
-
|
|
22
|
-
cy.contains('submit').click()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const result = `Method: POST
|
|
26
|
-
Form Data:
|
|
27
|
-
{
|
|
28
|
-
"evaluation": {
|
|
29
|
-
"0": {
|
|
30
|
-
"question": "Punctuality",
|
|
31
|
-
"type": "rating"
|
|
32
|
-
},
|
|
33
|
-
"1": {
|
|
34
|
-
"question": "Quality of work",
|
|
35
|
-
"type": "rating"
|
|
36
|
-
},
|
|
37
|
-
"2": {
|
|
38
|
-
"_destroy": "1",
|
|
39
|
-
"question": "Satisfied?",
|
|
40
|
-
"type": "yes_no"
|
|
41
|
-
},
|
|
42
|
-
"3": {
|
|
43
|
-
"question": "Rate your breakfast",
|
|
44
|
-
"type": "rating"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}`
|
|
48
|
-
|
|
49
|
-
cy.get('.unformatted').should('contain.text', result)
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
})
|