glib-web 5.0.6 → 6.0.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.
Files changed (60) hide show
  1. package/.claude/commands/gen-cypress-test.md +101 -0
  2. package/.github/dependabot.yml +84 -0
  3. package/.nycrc.json +4 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +4 -0
  6. package/action.js +6 -4
  7. package/actions/logics/set.js +5 -0
  8. package/actions/panels/scrollTo.js +2 -2
  9. package/actions/timeouts/set.js +1 -1
  10. package/app.vue +1 -1
  11. package/components/charts/series.js +2 -2
  12. package/components/composable/gmap.js +1 -1
  13. package/components/composable/upload.js +1 -4
  14. package/components/composable/upload_nothing.js +1 -1
  15. package/components/fields/_buttonDate.vue +2 -2
  16. package/components/fields/_patternText.vue +2 -8
  17. package/components/fields/_select.vue +12 -8
  18. package/components/fields/_selectItemDefault.vue +1 -3
  19. package/components/fields/_selectItemWithIcon.vue +1 -3
  20. package/components/fields/_selectItemWithImage.vue +1 -3
  21. package/components/fields/clipboardUpload.vue +115 -0
  22. package/components/fields/dynamicGroup2.vue +1 -1
  23. package/components/fields/multiUpload.vue +12 -19
  24. package/components/fields/placeholderUpload.vue +8 -4
  25. package/components/fields/radioGroup.vue +0 -7
  26. package/components/fields/sign.vue +1 -1
  27. package/components/fields/text.vue +1 -9
  28. package/components/fields/upload.vue +4 -0
  29. package/components/mixins/longClick.js +1 -1
  30. package/components/panels/bulkEdit2.vue +1 -1
  31. package/components/panels/list.vue +1 -1
  32. package/components/popover.vue +1 -1
  33. package/components/responsive.vue +1 -1
  34. package/cypress/e2e/glib-web/dialog.cy.js +0 -1
  35. package/cypress/e2e/glib-web/dirtyState.cy.js +37 -37
  36. package/cypress/e2e/glib-web/fieldsDateTime.cy.js +46 -3
  37. package/cypress/e2e/glib-web/fieldsRadio.cy.js +65 -0
  38. package/cypress/e2e/glib-web/fieldsSelect.cy.js +116 -76
  39. package/cypress/e2e/glib-web/fieldsText.cy.js +83 -0
  40. package/cypress/e2e/glib-web/fieldsUpload.cy.js +230 -120
  41. package/cypress/e2e/glib-web/image.cy.js +62 -33
  42. package/cypress/e2e/glib-web/listsAppend.cy.js +17 -17
  43. package/cypress/e2e/glib-web/switch.cy.js +13 -0
  44. package/cypress/e2e/glib-web/tabBar.cy.js +23 -0
  45. package/cypress/fixtures/document.pdf +12 -0
  46. package/cypress/fixtures/large.png +0 -0
  47. package/cypress/fixtures/upload.png +0 -0
  48. package/cypress/helper.js +13 -1
  49. package/cypress/support/component.js +1 -1
  50. package/cypress/support/e2e.js +20 -13
  51. package/doc/dependabot.md +22 -0
  52. package/eslint-rules/index.js +6 -6
  53. package/nav/dialog.vue +1 -1
  54. package/package.json +19 -17
  55. package/templates/_menu.vue +2 -2
  56. package/utils/http.js +2 -1
  57. package/.claude/settings.local.json +0 -39
  58. package/cypress/component/inputUpload.cy.js +0 -103
  59. package/cypress/component/multiUpload.cy.js +0 -107
  60. package/cypress/component/placeholderUpload.cy.js +0 -91
@@ -2,8 +2,8 @@
2
2
  <div v-if="loadIf" :style="$styles()" :class="$classes()">
3
3
  <div ref="container" class="gdrop-file border-[2px]" @click="handleClick" @drop="handleDrop" @dragover="handleDragOver"
4
4
  @dragleave="handleDragLeave">
5
- <input ref="fileSelect" :name="props.spec.directUploadUrl ? '' : fieldName" type="file" multiple
6
- style="display: none">
5
+ <input ref="fileSelect" :name="props.spec.directUploadUrl ? '' : fieldName" type="file"
6
+ :multiple="isMultiple" style="display: none">
7
7
 
8
8
  <div class="cloud" style="pointer-events: none;">
9
9
  <VIcon ref="icon" size="48" class="icon">cloud_upload</VIcon>
@@ -87,6 +87,8 @@ export default defineComponent({
87
87
 
88
88
  const uploadTitle = props.spec.uploadTitle || 'File added';
89
89
 
90
+ const isMultiple = computed(() => props.spec.multiple ?? true);
91
+
90
92
  const files = ref({});
91
93
 
92
94
  function initFiles(fls) {
@@ -138,14 +140,9 @@ export default defineComponent({
138
140
 
139
141
  function handleDrop(e) {
140
142
  e.preventDefault();
141
- uploader.uploadFiles(
142
- {
143
- droppedFiles: e.dataTransfer.files,
144
- files: files,
145
- spec: props.spec,
146
- container: container
147
- }
148
- );
143
+ const droppedFiles = isMultiple.value ? e.dataTransfer.files : [e.dataTransfer.files[0]].filter(Boolean);
144
+ if (!isMultiple.value) files.value = {};
145
+ uploader.uploadFiles({ droppedFiles, files, spec: props.spec, container });
149
146
 
150
147
  e.currentTarget.classList.remove('border-[4px]');
151
148
  container.value.classList.add('border-[2px]');
@@ -182,16 +179,11 @@ export default defineComponent({
182
179
  }
183
180
  }
184
181
 
185
- function handleClick(e) {
182
+ function handleClick() {
186
183
  const onchange = () => {
187
- uploader.uploadFiles(
188
- {
189
- droppedFiles: e.target.files,
190
- files: files,
191
- spec: props.spec,
192
- container: container
193
- }
194
- );
184
+ const selectedFiles = isMultiple.value ? fileSelect.value.files : [fileSelect.value.files[0]].filter(Boolean);
185
+ if (!isMultiple.value) files.value = {};
186
+ uploader.uploadFiles({ droppedFiles: selectedFiles, files, spec: props.spec, container });
195
187
  };
196
188
 
197
189
  fileSelect.value.onchange = onchange;
@@ -201,6 +193,7 @@ export default defineComponent({
201
193
  useGlibInput({ props, cacheValue: false });
202
194
 
203
195
  return {
196
+ isMultiple,
204
197
  files,
205
198
  fileSelect,
206
199
  container,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div ref="container" class="placeholder-view" @click="trigger">
3
- <v-img v-if="props.spec.type == 'image' && !uploading" :style="style" :src="sourceUrl" />
3
+ <v-img v-if="props.spec.type == 'image' && !uploading" :src="sourceUrl" cover width="100%" />
4
4
  <v-avatar v-else-if="props.spec.type == 'avatar' && !uploading" :size="props.spec.width">
5
5
  <v-img :src="sourceUrl"></v-img>
6
6
  </v-avatar>
@@ -26,10 +26,11 @@ export default {
26
26
 
27
27
  <script setup>
28
28
  import { computed, ref, getCurrentInstance, watch, nextTick } from "vue";
29
- import { useFilesState } from "../composable/file";
29
+ import { useFilesState, useFileUtils } from "../composable/file";
30
30
  import { useUploader } from "../composable/uploader";
31
31
  import Action from "../../action";
32
32
 
33
+ const { makeKey, Item } = useFileUtils();
33
34
  const props = defineProps(['spec']);
34
35
  const files = ref({});
35
36
  const { uploading, uploaded } = useFilesState(files);
@@ -44,6 +45,8 @@ const inputRef = ref(null);
44
45
  const container = ref(null);
45
46
  const blobUrl = ref(undefined);
46
47
  const sourceUrl = computed(() => {
48
+ if (blobUrl.value === '') return '';
49
+
47
50
  return blobUrl.value || props.spec.url;
48
51
  });
49
52
  const style = computed(() => {
@@ -70,8 +73,8 @@ if (props.spec.onFinishUpload) {
70
73
  }
71
74
 
72
75
  function reset() {
73
- files.value = {};
74
- blobUrl.value = null;
76
+ files.value = { [makeKey()]: new Item({ signedId: '', status: null, el: null }) };
77
+ blobUrl.value = '';
75
78
  inputRef.value.value = null;
76
79
  }
77
80
 
@@ -86,5 +89,6 @@ defineExpose({ reset, trigger });
86
89
  .placeholder-view {
87
90
  width: 100%;
88
91
  cursor: pointer;
92
+ background-color: rgb(var(--v-theme-surface));
89
93
  }
90
94
  </style>
@@ -37,13 +37,6 @@ export default {
37
37
  };
38
38
  }
39
39
  },
40
- methods: {
41
- updateValue(variable) {
42
- if (variable.value) {
43
- this.fieldModel = variable.value;
44
- }
45
- },
46
- },
47
40
  };
48
41
  </script>
49
42
 
@@ -66,7 +66,7 @@ function upload() {
66
66
  }
67
67
 
68
68
  function penSource(e) {
69
- let source = {};
69
+ let source;
70
70
  const touch = e.touches ? e.touches[0] : null;
71
71
  if (touch) {
72
72
  const canvasPos = e.target.getBoundingClientRect();
@@ -80,15 +80,7 @@ export default {
80
80
  ) ||
81
81
  "Must be a valid URL",
82
82
  ],
83
- },
84
- search: {
85
- type: "url",
86
- prependIcon: "search",
87
- onRightIconClick: function () { },
88
- rules: [
89
- (v) => !v || /^\w+:\/\/[.-\w]+$/.test(v) || "Must be a valid URL",
90
- ],
91
- },
83
+ }
92
84
  },
93
85
  };
94
86
  },
@@ -3,6 +3,7 @@
3
3
  <placeholderUpload v-if="props.spec.placeholderView" ref="realComp" :spec="childSpec" />
4
4
  <inputUpload v-else-if="props.spec.inputView" ref="realComp" :spec="childSpec" />
5
5
  <multiUpload v-else-if="props.spec.multiProgressView" ref="realComp" :spec="childSpec" />
6
+ <clipboardUpload v-else-if="props.spec.clipboardView" ref="realComp" :spec="childSpec" />
6
7
  </div>
7
8
  </template>
8
9
 
@@ -18,6 +19,7 @@ export default {
18
19
  import placeholderUpload from "./placeholderUpload.vue";
19
20
  import inputUpload from "./inputUpload.vue";
20
21
  import multiUpload from "./multiUpload.vue";
22
+ import clipboardUpload from "./clipboardUpload.vue";
21
23
 
22
24
  import { computed, ref } from "vue";
23
25
 
@@ -31,6 +33,8 @@ const childSpec = computed(() => {
31
33
  return Object.assign({}, props.spec, { inputView: null }, props.spec.inputView);
32
34
  } else if (props.spec.multiProgressView) {
33
35
  return Object.assign({}, props.spec, { multiProgressView: null }, props.spec.multiProgressView);
36
+ } else if (props.spec.clipboardView) {
37
+ return Object.assign({}, props.spec, { clipboardView: null }, props.spec.clipboardView);
34
38
  }
35
39
 
36
40
  return props.spec;
@@ -7,7 +7,7 @@ export default ({ delay = 400, interval = 50 }) => ({
7
7
  if (compName) {
8
8
  warn += `Found in component '${compName}' `;
9
9
  }
10
- console.warn(warn) // eslint-disable-line
10
+ console.warn(warn)
11
11
  return;
12
12
  }
13
13
 
@@ -254,7 +254,7 @@ function handleCellChange(e, cell) {
254
254
  const rowId = cell.rowId;
255
255
  const columnId = cell.cellId;
256
256
 
257
- let value = null;
257
+ let value;
258
258
  if (e.target instanceof HTMLInputElement) {
259
259
  value = e.target.value;
260
260
  } else {
@@ -145,7 +145,7 @@ export default {
145
145
  const section = this.sections[index] || {};
146
146
 
147
147
  if (!section.rows) {
148
- this.$set(section, "rows", []);
148
+ section.rows = [];
149
149
  }
150
150
 
151
151
  Utils.type.ifArray(appendedSection.rows, (rows) => {
@@ -135,7 +135,7 @@ export default {
135
135
  }
136
136
  },
137
137
  handleClose(event) {
138
- let element = null;
138
+ let element;
139
139
  if (this.$refs.container) {
140
140
  element = this.$refs.container.$el;
141
141
  } else {
@@ -48,7 +48,7 @@ export default {
48
48
  },
49
49
  innerStyles() {
50
50
  const styles = {};
51
- let align = null;
51
+ let align;
52
52
  switch (this.spec.align) {
53
53
  case "center":
54
54
  align = "center";
@@ -1,5 +1,4 @@
1
1
  import { testPageUrl } from "../../helper.js";
2
- import * as Type from "../../../utils/type.js";
3
2
  const url = testPageUrl('dialog');
4
3
 
5
4
  describe('dialog', () => {
@@ -1,76 +1,76 @@
1
- import { testPageUrl } from "../../helper.js"
2
- const url = testPageUrl('dirty_state')
3
- const prompt = 'Changes have not been saved. Are you sure you want to leave this page?'
1
+ import { testPageUrl } from "../../helper.js";
2
+ const url = testPageUrl('dirty_state');
3
+ const prompt = 'Changes have not been saved. Are you sure you want to leave this page?';
4
4
 
5
5
  describe('dirtyState', () => {
6
6
  it('can be disabled', () => {
7
- cy.visit(url)
7
+ cy.visit(url);
8
8
 
9
9
  cy.get('input[name="user[dirty_check_disabled]"]')
10
10
  .type('TEST TEST TEST')
11
- .blur()
11
+ .blur();
12
12
 
13
- cy.reload()
13
+ cy.reload();
14
14
 
15
- cy.get('input[name="user[dirty_check_disabled]"]').should('be.empty')
16
- })
15
+ cy.get('input[name="user[dirty_check_disabled]"]').should('be.empty');
16
+ });
17
17
 
18
18
  it('dirty if not equal to init value', () => {
19
- cy.visit(url)
19
+ cy.visit(url);
20
20
 
21
21
  cy.contains('Male').click();
22
22
 
23
23
  cy.on('window:confirm', (str) => {
24
- cy.then(() => expect(str).to.equal(prompt))
24
+ cy.then(() => expect(str).to.equal(prompt));
25
25
  return false;
26
- })
26
+ });
27
27
 
28
- cy.contains('fields_upload').click()
29
- cy.contains('Female').click()
30
- cy.contains('fields_upload').click() // Try to navigate to another page
31
- cy.location('href').should('eq', testPageUrl('fields_upload'))
32
- })
28
+ cy.contains('fields_upload').click({ force: true });
29
+ cy.contains('Female').click();
30
+ cy.contains('fields_upload').click({ force: true }); // Try to navigate to another page
31
+ cy.location('href').should('eq', testPageUrl('fields_upload'));
32
+ });
33
33
 
34
34
  it('have different context between window and dialog', () => {
35
- cy.visit(url)
35
+ cy.visit(url);
36
36
 
37
- cy.contains('Dialog Form').click()
37
+ cy.contains('Dialog Form').click();
38
38
 
39
- cy.get('input[name="user[name]"]').click().type('John Doe')
39
+ cy.get('input[name="user[name]"]').click().type('John Doe');
40
40
 
41
- cy.contains('cancel').click()
41
+ cy.contains('cancel').click();
42
42
 
43
43
  let text = '';
44
44
  cy.on('window:confirm', (str) => {
45
45
  text = str;
46
46
  return true;
47
- })
47
+ });
48
48
 
49
- cy.then(() => expect(text).to.equal(prompt))
49
+ cy.then(() => expect(text).to.equal(prompt));
50
50
 
51
- cy.contains('fields_upload').click() // Try to navigate to another page
52
- cy.location('href').should('eq', testPageUrl('fields_upload'))
53
- })
51
+ cy.contains('fields_upload').click(); // Try to navigate to another page
52
+ cy.location('href').should('eq', testPageUrl('fields_upload'));
53
+ });
54
54
 
55
55
  it('pop on history back', () => {
56
- cy.visit(testPageUrl('fields_upload'))
57
- cy.contains('dirty_state').click()
56
+ cy.visit(testPageUrl('fields_upload'));
57
+ cy.contains('dirty_state').click();
58
58
 
59
- cy.contains('choice2').click()
59
+ cy.contains('choice2').click();
60
60
 
61
- cy.go('back')
62
-
63
- let text = ''
61
+ let text = '';
64
62
  cy.on('window:confirm', (str) => {
65
63
  text = str;
66
64
  return false;
67
- })
65
+ });
66
+
67
+ cy.go('back');
68
68
 
69
- cy.then(() => expect(text).to.equal(prompt))
69
+ cy.wrap(null).should(() => expect(text).to.equal(prompt));
70
70
 
71
71
  // dirty state is removed after second try
72
- cy.go('back')
72
+ cy.go('back');
73
73
 
74
- cy.location('href').should('eq', testPageUrl('fields_upload'))
75
- })
76
- })
74
+ cy.location('href').should('eq', testPageUrl('fields_upload'));
75
+ });
76
+ });
@@ -3,30 +3,38 @@ import { testPageUrl, withComponent } from "../../helper.js"
3
3
  const url = testPageUrl('fields_date_time')
4
4
 
5
5
  describe('fields_date_time', () => {
6
- it('updates date actions', () => {
6
+ it('sets date to today + 1 week', () => {
7
7
  cy.visit(url)
8
8
 
9
9
  cy.contains('Set today + 1 week').click()
10
10
  cy.get('#date_status').should('contain.text', 'Date changed')
11
+ })
12
+
13
+ it('clears date', () => {
14
+ cy.visit(url)
11
15
 
12
16
  cy.contains('Clear date').click()
13
17
  cy.get('#date_status').should('contain.text', 'Date changed')
14
18
  cy.get('input[name="user[date_basic]"]').should('have.value', '')
15
19
  })
16
20
 
17
- it('updates datetime actions', () => {
21
+ it('sets datetime to evening', () => {
18
22
  cy.visit(url)
19
23
 
20
24
  cy.contains('Set datetime (evening)').click()
21
25
  cy.get('#datetime_status').should('contain.text', 'Datetime changed')
22
26
  cy.get('input[name="user[datetime_basic]"]').should('have.value', '2024-12-12T18:45')
27
+ })
28
+
29
+ it('clears datetime', () => {
30
+ cy.visit(url)
23
31
 
24
32
  cy.contains('Clear datetime').click()
25
33
  cy.get('#datetime_status').should('contain.text', 'Datetime changed')
26
34
  cy.get('input[name="user[datetime_basic]"]').should('have.value', '')
27
35
  })
28
36
 
29
- it('toggles templates and disabled state', () => {
37
+ it('disables and enables date', () => {
30
38
  cy.visit(url)
31
39
 
32
40
  cy.contains('Disable date').click()
@@ -34,6 +42,41 @@ describe('fields_date_time', () => {
34
42
 
35
43
  cy.contains('Enable date').click()
36
44
  cy.get('#date_basic').find('.v-input--disabled').should('not.exist')
45
+ })
46
+
47
+ it('opens datetime picker on click and reflects selected value', () => {
48
+ cy.visit(url)
49
+
50
+ cy.get('#datetime_basic').find('.v-field__input').click()
51
+ cy.get('#datetime_basic').find('.v-input--focused').should('exist')
52
+
53
+ cy.get('#datetime_basic').find('input.native-picker').then($el => {
54
+ const nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
55
+ nativeSet.call($el[0], '2024-12-20T14:30')
56
+ $el[0].dispatchEvent(new Event('input', { bubbles: true }))
57
+ })
58
+
59
+ cy.get('#datetime_basic').find('.v-field__input').should('have.value', '12/20/2024 14:30')
60
+ cy.get('input[name="user[datetime_basic]"]').should('have.value', '2024-12-20T14:30')
61
+ })
62
+
63
+ it('opens plain template picker on click and reflects selected value', () => {
64
+ cy.visit(url)
65
+
66
+ cy.get('#datetime_plain').find('.date-button').click()
67
+ cy.get('#datetime_plain').find('input.date-input').should('be.focused')
68
+
69
+ cy.get('#datetime_plain').find('input.date-input').then($el => {
70
+ const nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
71
+ nativeSet.call($el[0], '2026-03-15T10:00')
72
+ $el[0].dispatchEvent(new Event('input', { bubbles: true }))
73
+ })
74
+
75
+ cy.get('input[name="user[datetime_plain]"]').should('have.value', '2026-03-15T10:00')
76
+ })
77
+
78
+ it('switches datetime_plain between text and plain templates', () => {
79
+ cy.visit(url)
37
80
 
38
81
  cy.contains('Use text template').click()
39
82
  withComponent('datetime_plain', (comp) => {
@@ -0,0 +1,65 @@
1
+ import { testPageUrl } from "../../helper.js";
2
+
3
+ const url = testPageUrl('fields_radio');
4
+
5
+ describe('fields_radio', () => {
6
+ it('selects Female in basic example and shows status changed', () => {
7
+ cy.visit(url);
8
+
9
+ cy.contains('Status: idle').should('be.visible');
10
+ cy.contains('Female').click();
11
+ cy.contains('Status: changed').should('be.visible');
12
+ });
13
+
14
+ it('programmatically sets Female then Male via action buttons', () => {
15
+ cy.visit(url);
16
+
17
+ cy.contains('Select Female').click();
18
+ cy.contains('Status: changed').should('be.visible');
19
+ cy.contains('Select Male').click();
20
+ cy.contains('Status: changed').should('be.visible');
21
+ });
22
+
23
+ it('clears basic example selection via Clear selection button', () => {
24
+ cy.visit(url);
25
+
26
+ cy.contains('Clear selection').click();
27
+ cy.contains('Status: changed').should('be.visible');
28
+ });
29
+
30
+ it('shows Phone number field when Phone is selected in onChangeAndLoad', () => {
31
+ cy.visit(url);
32
+
33
+ cy.contains('.v-field-label--floating', 'Email address').scrollIntoView().should('be.visible');
34
+ cy.contains('Phone').click();
35
+ cy.contains('.v-field-label--floating', 'Phone number').scrollIntoView().should('be.visible');
36
+ cy.contains('.v-field-label--floating', 'Email address').should('not.exist');
37
+ });
38
+
39
+ it('restores Email address field when Email is re-selected', () => {
40
+ cy.visit(url);
41
+
42
+ cy.contains('Phone').click();
43
+ cy.contains('.v-field-label--floating', 'Phone number').scrollIntoView().should('be.visible');
44
+ cy.contains('Email').click();
45
+ cy.contains('.v-field-label--floating', 'Email address').scrollIntoView().should('be.visible');
46
+ cy.contains('.v-field-label--floating', 'Phone number').should('not.exist');
47
+ });
48
+
49
+ it('toggles I agree single-option radio and submits form', () => {
50
+ cy.visit(url);
51
+
52
+ cy.contains('I agree to the terms and conditions').click();
53
+ cy.contains('Submit').click();
54
+ cy.contains('Method: POST').should('be.visible');
55
+ cy.contains('"agree": "yes"').should('be.visible');
56
+ });
57
+
58
+ it('submits form without I agree and agree is absent from form data', () => {
59
+ cy.visit(url);
60
+
61
+ cy.contains('Submit').click();
62
+ cy.contains('Method: POST').should('be.visible');
63
+ cy.contains('"agree"').should('not.exist');
64
+ });
65
+ });