glib-web 4.41.0 → 4.42.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/.claude/settings.local.json +24 -0
- package/.nycrc.json +18 -0
- package/actions/forms/submit.js +11 -1
- package/components/fields/_select.vue +252 -6
- package/components/fields/check.vue +5 -1
- package/components/fields/text.vue +1 -1
- package/cypress/support/e2e.ts +2 -1
- package/cypress.config.ts +3 -1
- package/doc/TESTING.md +14 -0
- package/package.json +12 -3
- package/plugins/viteDefaultConfig.js +88 -0
- package/templates/thumbnail.vue +1 -0
- package/utils/type.js +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
"WebFetch(domain:vuetifyjs.com)",
|
|
15
|
+
"Bash(lsof:*)",
|
|
16
|
+
"Bash(readlink:*)",
|
|
17
|
+
"WebFetch(domain:github.com)",
|
|
18
|
+
"Bash(npm run dev)",
|
|
19
|
+
"Bash(npm run)"
|
|
20
|
+
],
|
|
21
|
+
"deny": [],
|
|
22
|
+
"ask": []
|
|
23
|
+
}
|
|
24
|
+
}
|
package/.nycrc.json
ADDED
package/actions/forms/submit.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export default class {
|
|
2
2
|
execute(properties, component) {
|
|
3
|
-
|
|
3
|
+
let target = component;
|
|
4
|
+
|
|
5
|
+
if (properties.targetId) {
|
|
6
|
+
target = GLib.component.findById(properties.targetId);
|
|
7
|
+
|
|
8
|
+
if (!target) {
|
|
9
|
+
console.warn("Component ID not found for form submission:", properties.targetId);
|
|
10
|
+
target = component;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
target.$dispatchEvent("forms/directSubmit", {
|
|
5
15
|
url: properties.overrideUrl,
|
|
6
16
|
method: properties.overrideMethod,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf">
|
|
2
|
+
<div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf" class="fields-select-wrapper">
|
|
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
5
|
<component ref="comp" :is="compName" :color="gcolor" v-model="fieldModel" :label="label" :items="normalizedOptions"
|
|
6
|
-
:
|
|
6
|
+
:disabled="inputDisabled" :multiple="spec.multiple" :readonly="spec.readOnly"
|
|
7
7
|
:clearable="spec.clearable" :placeholder="spec.placeholder" :rules="$validation()" persistent-hint
|
|
8
|
-
:append-icon="append.icon" validate-on="blur" item-title='text' :variant="variant"
|
|
8
|
+
:append-icon="append.icon" validate-on="blur" item-title='text' :variant="variant"
|
|
9
9
|
:density="density" persistent-placeholder @update:modelValue="onChange" @focus="focused = true"
|
|
10
10
|
@blur="focused = false" :menu-props="{ maxWidth: 0 }">
|
|
11
11
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<select-item-default v-else :context="props" :item="item" :spec="spec"></select-item-default>
|
|
24
24
|
</div>
|
|
25
25
|
</template>
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
<template v-slot:prepend-item>
|
|
28
28
|
<template v-if="spec.prependSelectAll">
|
|
29
29
|
<v-list-item title="Select All" @click="checkAll">
|
|
@@ -39,6 +39,41 @@
|
|
|
39
39
|
<common-responsive v-if="spec.header" :spec="spec.header" />
|
|
40
40
|
</template>
|
|
41
41
|
|
|
42
|
+
<template v-if="useChips" #selection="{ item, index }">
|
|
43
|
+
<v-chip
|
|
44
|
+
v-if="index < maxVisibleChips"
|
|
45
|
+
:density="density"
|
|
46
|
+
closable
|
|
47
|
+
@click:close="removeItem(item)"
|
|
48
|
+
>
|
|
49
|
+
<span>{{ item.title }}</span>
|
|
50
|
+
</v-chip>
|
|
51
|
+
<v-chip
|
|
52
|
+
v-if="!expanded && chipExceedsTwoLines && visibleChipCount < fieldModel.length && index === visibleChipCount"
|
|
53
|
+
:density="density"
|
|
54
|
+
clickable
|
|
55
|
+
@click="expanded = true"
|
|
56
|
+
class="text-caption expansion-chip"
|
|
57
|
+
variant="outlined"
|
|
58
|
+
color="primary"
|
|
59
|
+
>
|
|
60
|
+
{{ fieldModel.length - visibleChipCount }} more
|
|
61
|
+
<common-icon :spec="{ material: { name: 'expand_more' } }" class="ml-1" />
|
|
62
|
+
</v-chip>
|
|
63
|
+
<v-chip
|
|
64
|
+
v-if="expanded && chipExceedsTwoLines && index === fieldModel.length - 1"
|
|
65
|
+
:density="density"
|
|
66
|
+
clickable
|
|
67
|
+
@click="collapseChips"
|
|
68
|
+
class="text-caption expansion-chip"
|
|
69
|
+
variant="outlined"
|
|
70
|
+
color="primary"
|
|
71
|
+
>
|
|
72
|
+
Show less
|
|
73
|
+
<common-icon :spec="{ material: { name: 'expand_less' } }" class="ml-1" />
|
|
74
|
+
</v-chip>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
42
77
|
<template v-slot:append-item v-if="spec.footer">
|
|
43
78
|
<common-responsive :spec="footer" />
|
|
44
79
|
</template>
|
|
@@ -62,7 +97,7 @@ import { triggerOnChange, triggerOnInput, useGlibInput } from "../composable/for
|
|
|
62
97
|
import { isBoolean } from '../../utils/type';
|
|
63
98
|
|
|
64
99
|
import { useGlibSelectable, watchNoneOfAbove } from '../composable/selectable';
|
|
65
|
-
import { ref, defineExpose } from 'vue';
|
|
100
|
+
import { ref, defineExpose, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
|
66
101
|
import SelectItemDefault from "./_selectItemDefault.vue";
|
|
67
102
|
import SelectItemWithImage from "./_selectItemWithImage.vue";
|
|
68
103
|
import SelectItemWithIcon from "./_selectItemWithIcon.vue";
|
|
@@ -85,6 +120,166 @@ export default {
|
|
|
85
120
|
const options = ref(props.spec.options);
|
|
86
121
|
const comp = ref(null);
|
|
87
122
|
const append = props.spec.append || {};
|
|
123
|
+
const expanded = ref(false);
|
|
124
|
+
const chipExceedsMaxLines = ref(false);
|
|
125
|
+
const visibleChipCount = ref(Infinity);
|
|
126
|
+
|
|
127
|
+
// Constants for chip line calculation
|
|
128
|
+
const DEFAULT_MAX_LINES = 2;
|
|
129
|
+
const DEFAULT_LINE_HEIGHT = 40;
|
|
130
|
+
const CHIP_GAP = 12; // Gap between chips + margins
|
|
131
|
+
|
|
132
|
+
// Computed property for determining how many chips to show
|
|
133
|
+
const maxVisibleChips = computed(() => {
|
|
134
|
+
if (!expanded.value) {
|
|
135
|
+
return visibleChipCount.value;
|
|
136
|
+
}
|
|
137
|
+
// When expanded, show all chips except reserve space for "Show less" if needed
|
|
138
|
+
return chipExceedsMaxLines.value ? fieldModel.value.length - 1 : fieldModel.value.length;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Helper function to find the container that holds all chips
|
|
142
|
+
const findChipsContainer = (allChips) => {
|
|
143
|
+
if (allChips.length === 0) return null;
|
|
144
|
+
|
|
145
|
+
let container = allChips[0].parentElement;
|
|
146
|
+
// Walk up the DOM to find a container that has multiple chips
|
|
147
|
+
while (container && container.querySelectorAll('.v-chip').length <= 1) {
|
|
148
|
+
container = container.parentElement;
|
|
149
|
+
}
|
|
150
|
+
return container;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Method to check if chips exceed max lines
|
|
154
|
+
const checkChipLines = (isResizeTriggered = false) => {
|
|
155
|
+
if (!comp.value) return;
|
|
156
|
+
|
|
157
|
+
// Get max lines config: 0 = no limit, null/undefined = default 2
|
|
158
|
+
const maxLines = props.spec.maxChipLines ?? DEFAULT_MAX_LINES;
|
|
159
|
+
|
|
160
|
+
// If maxLines is 0, don't limit chips at all
|
|
161
|
+
if (maxLines === 0) {
|
|
162
|
+
chipExceedsMaxLines.value = false;
|
|
163
|
+
visibleChipCount.value = Infinity;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Skip the measurement logic if this is a resize-triggered event and we're collapsed
|
|
168
|
+
// This prevents incorrect measurements when collapsed with fewer chips visible
|
|
169
|
+
if (isResizeTriggered && !expanded.value && chipExceedsMaxLines.value) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Try different selectors to find the correct container
|
|
174
|
+
let selectionEl = comp.value.$el.querySelector('.v-select__selection, .v-autocomplete__selection');
|
|
175
|
+
if (!selectionEl) {
|
|
176
|
+
// Fallback: try to find the component root and look for chips there
|
|
177
|
+
selectionEl = comp.value.$el;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find all chips
|
|
181
|
+
const allChips = comp.value.$el.querySelectorAll('.v-chip');
|
|
182
|
+
|
|
183
|
+
// Early return if no chips
|
|
184
|
+
if (allChips.length === 0) {
|
|
185
|
+
chipExceedsMaxLines.value = false;
|
|
186
|
+
visibleChipCount.value = Infinity;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Find the common parent of all chips
|
|
191
|
+
const chipsContainer = findChipsContainer(allChips);
|
|
192
|
+
|
|
193
|
+
// Get the height of a single line dynamically from actual chip height
|
|
194
|
+
// This adapts to density settings, custom styles, and theme changes
|
|
195
|
+
const lineHeight = allChips[0].offsetHeight || DEFAULT_LINE_HEIGHT;
|
|
196
|
+
const currentHeight = chipsContainer ? chipsContainer.scrollHeight : selectionEl.scrollHeight;
|
|
197
|
+
const lines = Math.floor(currentHeight / lineHeight);
|
|
198
|
+
|
|
199
|
+
const exceedsMaxLines = lines > maxLines;
|
|
200
|
+
|
|
201
|
+
if (exceedsMaxLines) {
|
|
202
|
+
// Calculate how many chips fit in maxLines using line-by-line simulation
|
|
203
|
+
const chips = chipsContainer ? chipsContainer.querySelectorAll('.v-chip') : allChips;
|
|
204
|
+
const containerWidth = chipsContainer ? chipsContainer.offsetWidth : selectionEl.offsetWidth;
|
|
205
|
+
|
|
206
|
+
let currentLineWidth = 0;
|
|
207
|
+
let currentLine = 1;
|
|
208
|
+
let count = 0;
|
|
209
|
+
|
|
210
|
+
// Simulate actual chip wrapping line by line
|
|
211
|
+
for (const chip of chips) {
|
|
212
|
+
const chipWidth = chip.offsetWidth + CHIP_GAP;
|
|
213
|
+
|
|
214
|
+
// Check if chip fits on current line
|
|
215
|
+
if (currentLineWidth + chipWidth > containerWidth) {
|
|
216
|
+
// Move to next line
|
|
217
|
+
currentLine++;
|
|
218
|
+
currentLineWidth = chipWidth;
|
|
219
|
+
|
|
220
|
+
if (currentLine > maxLines) {
|
|
221
|
+
break; // Exceeded max lines
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// Fits on current line
|
|
225
|
+
currentLineWidth += chipWidth;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
count++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Only limit if we actually exceeded maxLines during simulation
|
|
232
|
+
if (currentLine > maxLines || count < chips.length) {
|
|
233
|
+
// Some chips didn't fit - reserve space for expansion chip
|
|
234
|
+
visibleChipCount.value = Math.max(1, count - 1);
|
|
235
|
+
chipExceedsMaxLines.value = true;
|
|
236
|
+
} else {
|
|
237
|
+
// All chips fit within maxLines - show them all!
|
|
238
|
+
visibleChipCount.value = Infinity;
|
|
239
|
+
chipExceedsMaxLines.value = false;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
visibleChipCount.value = Infinity;
|
|
243
|
+
chipExceedsMaxLines.value = false;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Watch for changes in model and expansion state
|
|
248
|
+
watch([fieldModel, expanded], () => {
|
|
249
|
+
nextTick(() => {
|
|
250
|
+
checkChipLines();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Add resize observer to recalculate on window resize
|
|
255
|
+
onMounted(() => {
|
|
256
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
257
|
+
// CRITICAL: Don't recalculate when collapsed
|
|
258
|
+
// The collapsed state has fewer chips visible, giving wrong measurements
|
|
259
|
+
// Only measure on resize when expanded (showing all chips)
|
|
260
|
+
if (!expanded.value && chipExceedsMaxLines.value) {
|
|
261
|
+
return; // Skip resize-triggered recalculation when collapsed
|
|
262
|
+
}
|
|
263
|
+
checkChipLines(true); // Pass true to indicate this is a resize-triggered calculation
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const observeComponent = () => {
|
|
267
|
+
if (comp.value && comp.value.$el) {
|
|
268
|
+
resizeObserver.observe(comp.value.$el);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Try to observe immediately, or wait a bit for component to mount
|
|
275
|
+
if (!observeComponent()) {
|
|
276
|
+
setTimeout(observeComponent, 100);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
onUnmounted(() => {
|
|
280
|
+
resizeObserver.disconnect();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
88
283
|
|
|
89
284
|
const valueForDisableAll = props.spec.valueForDisableAll;
|
|
90
285
|
const { checkAll, isAllSelected, isIndeterminate } = useGlibSelectable({ model: fieldModel, options: options, valueForDisableAll });
|
|
@@ -92,13 +287,41 @@ export default {
|
|
|
92
287
|
watchNoneOfAbove({ model: fieldModel, options: options, valueForDisableAll });
|
|
93
288
|
}
|
|
94
289
|
|
|
290
|
+
// Method to handle chip collapse
|
|
291
|
+
function collapseChips() {
|
|
292
|
+
expanded.value = false;
|
|
293
|
+
// IMPORTANT: Force recalculation with multiple nextTick calls to ensure DOM is fully updated
|
|
294
|
+
nextTick(() => {
|
|
295
|
+
checkChipLines(false);
|
|
296
|
+
// IMPORTANT: Second nextTick is necessary because DOM updates are asynchronous and
|
|
297
|
+
// the first measurement may occur before all chips are properly removed/added
|
|
298
|
+
// Do not remove this nested nextTick - it's required for correct functionality
|
|
299
|
+
nextTick(() => {
|
|
300
|
+
checkChipLines(false);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
95
305
|
// This is a public method that is called by other parts of the code, do not delete it.
|
|
96
306
|
function toggle() {
|
|
97
307
|
comp.value.menu = !comp.value.menu;
|
|
98
308
|
}
|
|
99
309
|
defineExpose(['toggle']);
|
|
100
310
|
|
|
101
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
fieldModel,
|
|
313
|
+
checkAll,
|
|
314
|
+
isIndeterminate,
|
|
315
|
+
isAllSelected,
|
|
316
|
+
append,
|
|
317
|
+
toggle,
|
|
318
|
+
comp,
|
|
319
|
+
expanded,
|
|
320
|
+
chipExceedsTwoLines: chipExceedsMaxLines,
|
|
321
|
+
visibleChipCount,
|
|
322
|
+
maxVisibleChips,
|
|
323
|
+
collapseChips
|
|
324
|
+
};
|
|
102
325
|
},
|
|
103
326
|
data() {
|
|
104
327
|
return {
|
|
@@ -178,6 +401,12 @@ export default {
|
|
|
178
401
|
triggerOnChange(containerEl);
|
|
179
402
|
}
|
|
180
403
|
},
|
|
404
|
+
removeItem(item) {
|
|
405
|
+
const index = this.fieldModel.indexOf(item.value);
|
|
406
|
+
if (index >= 0) {
|
|
407
|
+
this.fieldModel.splice(index, 1);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
181
410
|
$registryEnabled() {
|
|
182
411
|
return false;
|
|
183
412
|
}
|
|
@@ -212,4 +441,21 @@ export default {
|
|
|
212
441
|
display: flex;
|
|
213
442
|
align-items: center;
|
|
214
443
|
}
|
|
444
|
+
|
|
445
|
+
.expansion-chip {
|
|
446
|
+
transition: all 0.2s ease-in-out;
|
|
447
|
+
box-sizing: content-box;
|
|
448
|
+
margin-left: 1px;
|
|
449
|
+
|
|
450
|
+
&:hover {
|
|
451
|
+
background-color: rgba(0, 0, 0, 0.04) !important;
|
|
452
|
+
transform: translateY(-1px);
|
|
453
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
&:active {
|
|
457
|
+
transform: translateY(0);
|
|
458
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
215
461
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :class="$classes()" :style="$styles()" v-if="loadIf" @click="
|
|
2
|
+
<div :class="$classes()" :style="$styles()" v-if="loadIf" @click="onClick($event)">
|
|
3
3
|
<!-- This hidden field should always be there to make sure the submitted param is not empty,
|
|
4
4
|
which could cause "Not accessible" error on the server. -->
|
|
5
5
|
<input v-if="showUncheck" v-model="uncheckValue" type="hidden" :name="fieldName" />
|
|
@@ -70,6 +70,10 @@ export default {
|
|
|
70
70
|
};
|
|
71
71
|
},
|
|
72
72
|
methods: {
|
|
73
|
+
onClick(event) {
|
|
74
|
+
event.stopPropagation(); // Prevent event bubbling to parent thumbnail
|
|
75
|
+
this.$onClick();
|
|
76
|
+
},
|
|
73
77
|
_linkFieldModels(valueChanged) {
|
|
74
78
|
if (!this.parentModel && valueChanged) {
|
|
75
79
|
this.fieldModel = this.spec.value;
|
|
@@ -42,7 +42,7 @@ export default {
|
|
|
42
42
|
// This regex follows the RFC 5321 standard for email validation,
|
|
43
43
|
// with a slight improvement: it does not allow emails without a proper domain (e.g., "user@test" is invalid, must be "user@test.com").
|
|
44
44
|
//https://stackoverflow.com/questions/13992403/regex-validation-of-email-addresses-according-to-rfc5321-rfc5322
|
|
45
|
-
/^([!#-'*+\/-9=?A-Z^-~-]+(\.[!#-'*+\/-9=?A-Z^-~-]+)*|"([]!#-[^-~ \t]|(\\[\t -~]))+")@([
|
|
45
|
+
/^([!#-'*+\/-9=?A-Z^-~-]+(\.[!#-'*+\/-9=?A-Z^-~-]+)*|"([]!#-[^-~ \t]|(\\[\t -~]))+")@([A-Z0-9-]+\.)+[A-Z]{2,}$/i.test(
|
|
46
46
|
v
|
|
47
47
|
) ||
|
|
48
48
|
"E-mail must be valid",
|
package/cypress/support/e2e.ts
CHANGED
package/cypress.config.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { defineConfig } from "cypress";
|
|
2
|
+
import codeCoverageTask from "@cypress/code-coverage/task";
|
|
2
3
|
|
|
3
4
|
export default defineConfig({
|
|
4
5
|
e2e: {
|
|
5
6
|
setupNodeEvents(on, config) {
|
|
6
|
-
|
|
7
|
+
codeCoverageTask(on, config);
|
|
8
|
+
return config;
|
|
7
9
|
},
|
|
8
10
|
},
|
|
9
11
|
});
|
package/doc/TESTING.md
CHANGED
|
@@ -54,3 +54,17 @@ You **cannot write frontend tests in isolation**.
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
56
|
These 6 concepts explain **why the test structure exists** and **what you need to get started**. Everything else (selectors, assertions, etc.) is standard Cypress mechanics.
|
|
57
|
+
|
|
58
|
+
## Cypress Coverage Workflow
|
|
59
|
+
|
|
60
|
+
- **Prerequisites**: Keep the backend `glib-web` server running with the matching `test_page/*` fixtures (same as normal Cypress runs) and make sure the frontend package is linked into that app: `yarn link glib-web` (run from the backend repo).
|
|
61
|
+
- **Instrument code**: Coverage is only collected when `VITE_COVERAGE=true` because `vite-plugin-istanbul` has `requireEnv` enabled.
|
|
62
|
+
- **Step-by-step run**:
|
|
63
|
+
1. From the backend repo (`glib-web`), ensure the link is active: `yarn link glib-web`.
|
|
64
|
+
2. From the backend repo, start Vite with instrumentation: `VITE_COVERAGE=true bin/vite dev`.
|
|
65
|
+
3. From the backend repo, start Rails: `bin/rails s`.
|
|
66
|
+
4. From this repo (`glib-web-npm`), execute `yarn test:coverage` to run Cypress and emit coverage reports via `nyc` (uses `.nycrc.json`).
|
|
67
|
+
- **Manual run (if you need custom flags)**:
|
|
68
|
+
1. `VITE_COVERAGE=true yarn test`
|
|
69
|
+
2. `nyc report` (or add reporters, e.g. `nyc report --reporter=lcov`).
|
|
70
|
+
- **Where to read results**: HTML report at `coverage/index.html`; summary printed in the terminal; raw data in `.nyc_output` and `coverage/coverage-final.json`.
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
+
"type": "module",
|
|
2
3
|
"name": "glib-web",
|
|
3
|
-
"version": "4.
|
|
4
|
+
"version": "4.42.0",
|
|
4
5
|
"description": "",
|
|
5
6
|
"main": "index.js",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"test": "cypress run --browser chrome"
|
|
8
|
+
"test": "cypress run --browser chrome",
|
|
9
|
+
"test:coverage": "VITE_COVERAGE=true cypress run --browser chrome && nyc report"
|
|
8
10
|
},
|
|
9
11
|
"author": "",
|
|
10
12
|
"license": "ISC",
|
|
@@ -42,11 +44,18 @@
|
|
|
42
44
|
"vuetify": "3.8.6"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
47
|
+
"@cypress/code-coverage": "^3.14.7",
|
|
45
48
|
"@types/chart.js": "^2.9.34",
|
|
49
|
+
"@vitejs/plugin-vue": "^6.0.2",
|
|
46
50
|
"cypress": "^13.13.1",
|
|
47
51
|
"eslint": "^8.36.0",
|
|
48
52
|
"eslint-plugin-vue": "^9.26.0",
|
|
49
53
|
"prettier": "^1.18.2",
|
|
50
|
-
"typescript": "^4.9.5"
|
|
54
|
+
"typescript": "^4.9.5",
|
|
55
|
+
"vite": "^7.2.7",
|
|
56
|
+
"vite-plugin-compression": "^0.5.1",
|
|
57
|
+
"vite-plugin-environment": "^1.1.3",
|
|
58
|
+
"vite-plugin-istanbul": "^7.2.1",
|
|
59
|
+
"vite-plugin-ruby": "^5.1.1"
|
|
51
60
|
}
|
|
52
61
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { defineConfig } from "vite";
|
|
4
|
+
import RubyPlugin from "vite-plugin-ruby";
|
|
5
|
+
import vue from "@vitejs/plugin-vue";
|
|
6
|
+
import viteCompression from "vite-plugin-compression";
|
|
7
|
+
import EnvironmentPlugin from "vite-plugin-environment";
|
|
8
|
+
import istanbul from "vite-plugin-istanbul";
|
|
9
|
+
import * as TypeUtils from "../utils/type.js";
|
|
10
|
+
|
|
11
|
+
function pickStringArray(value) {
|
|
12
|
+
if (!TypeUtils.isArray(value)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const filtered = value.filter((item) => TypeUtils.isString(item));
|
|
17
|
+
return filtered.length > 0 ? filtered : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveGlibRoot(customRoot) {
|
|
21
|
+
if (TypeUtils.isString(customRoot)) {
|
|
22
|
+
return customRoot;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds the default Vite configuration used by glib projects.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} [options] Configuration overrides.
|
|
33
|
+
* @param {string} [options.glibRoot] Root directory for the glib package (defaults to repo root).
|
|
34
|
+
* @param {string} [options.aliasName] Module alias name for glib imports.
|
|
35
|
+
* @param {string} [options.aliasPath] Filesystem path used for the alias.
|
|
36
|
+
* @param {string} [options.workspaceRoot] Workspace root for coverage and path resolution.
|
|
37
|
+
* @param {string[]} [options.istanbulInclude] Globs to include when running Istanbul.
|
|
38
|
+
* @param {string[]} [options.environmentVariables] Environment variables exposed to the client.
|
|
39
|
+
* @param {string[]} [options.serverAllow] Additional filesystem paths allowed by Vite dev server.
|
|
40
|
+
* @param {import("vite").Plugin[]} [options.plugins] Extra Vite plugins to append.
|
|
41
|
+
* @returns {import("vite").UserConfig} Config object passed to `defineConfig`.
|
|
42
|
+
*/
|
|
43
|
+
function useGlibViteDefineConfig(options = {}) {
|
|
44
|
+
if (TypeUtils.isNotNull(globalThis.window)) {
|
|
45
|
+
throw new Error("useGlibViteDefineConfig is only available in Node environments.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const configOptions = TypeUtils.isObject(options) && !TypeUtils.isArray(options) ? options : {};
|
|
49
|
+
|
|
50
|
+
const glibRoot = resolveGlibRoot(configOptions.glibRoot);
|
|
51
|
+
const aliasName = TypeUtils.isString(configOptions.aliasName) ? configOptions.aliasName : "glib-web";
|
|
52
|
+
const aliasPath = TypeUtils.isString(configOptions.aliasPath) ? configOptions.aliasPath : glibRoot;
|
|
53
|
+
const workspaceRoot = TypeUtils.isString(configOptions.workspaceRoot)
|
|
54
|
+
? configOptions.workspaceRoot
|
|
55
|
+
: path.resolve(glibRoot, "..");
|
|
56
|
+
const istanbulInclude = pickStringArray(configOptions.istanbulInclude) ?? [`${path.basename(glibRoot)}/**/*`];
|
|
57
|
+
const environmentVariables = pickStringArray(configOptions.environmentVariables) ?? ["VITE_GMAPS_API_KEY"];
|
|
58
|
+
const serverAllow = pickStringArray(configOptions.serverAllow) ?? [glibRoot];
|
|
59
|
+
const additionalPlugins = TypeUtils.isArray(configOptions.plugins) ? configOptions.plugins : [];
|
|
60
|
+
|
|
61
|
+
return defineConfig({
|
|
62
|
+
server: {
|
|
63
|
+
fs: {
|
|
64
|
+
allow: serverAllow.map((allowedPath) => path.resolve(allowedPath))
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
resolve: {
|
|
68
|
+
alias: {
|
|
69
|
+
[aliasName]: path.resolve(aliasPath)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
plugins: [
|
|
73
|
+
vue(),
|
|
74
|
+
RubyPlugin(),
|
|
75
|
+
istanbul({
|
|
76
|
+
cwd: path.resolve(workspaceRoot),
|
|
77
|
+
include: istanbulInclude,
|
|
78
|
+
requireEnv: true
|
|
79
|
+
}),
|
|
80
|
+
viteCompression(),
|
|
81
|
+
EnvironmentPlugin(environmentVariables, { defineOn: "import.meta.env" }),
|
|
82
|
+
...additionalPlugins
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { useGlibViteDefineConfig };
|
|
88
|
+
export default useGlibViteDefineConfig;
|
package/templates/thumbnail.vue
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
</div> -->
|
|
12
12
|
|
|
13
|
+
<panels-responsive v-if="spec.left" :spec="spec.left" />
|
|
13
14
|
<v-list-item v-longclick="$onLongPress" class="item-content" :style="columnStyles()">
|
|
14
15
|
<!-- <v-icon v-if="spec.onReorder" class="handle">drag_indicator</v-icon> -->
|
|
15
16
|
|
package/utils/type.js
CHANGED