quasar-ui-danx 0.0.10 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- package/package.json +8 -2
- package/src/components/ActionTable/ActionTable.vue +143 -0
- package/src/components/ActionTable/BatchActionMenu.vue +60 -0
- package/src/components/ActionTable/EmptyTableState.vue +33 -0
- package/src/components/ActionTable/Filters/CollapsableFiltersSidebar.vue +36 -0
- package/src/components/ActionTable/Filters/FilterGroupItem.vue +28 -0
- package/src/components/ActionTable/Filters/FilterGroupList.vue +76 -0
- package/src/components/ActionTable/Filters/FilterListToggle.vue +50 -0
- package/src/components/ActionTable/Filters/FilterableField.vue +143 -0
- package/src/components/ActionTable/Filters/index.ts +5 -0
- package/src/components/ActionTable/Form/Fields/BooleanField.vue +37 -0
- package/src/components/ActionTable/Form/Fields/ConfirmPasswordField.vue +46 -0
- package/src/components/ActionTable/Form/Fields/DateField.vue +59 -0
- package/src/components/ActionTable/Form/Fields/DateRangeField.vue +110 -0
- package/src/components/ActionTable/Form/Fields/DateTimeField.vue +50 -0
- package/src/components/ActionTable/Form/Fields/DateTimePicker.vue +59 -0
- package/src/components/ActionTable/Form/Fields/EditableDiv.vue +39 -0
- package/src/components/ActionTable/Form/Fields/FieldLabel.vue +32 -0
- package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +78 -0
- package/src/components/ActionTable/Form/Fields/InlineDateTimeField.vue +44 -0
- package/src/components/ActionTable/Form/Fields/IntegerField.vue +26 -0
- package/src/components/ActionTable/Form/Fields/LabelValueBlock.vue +22 -0
- package/src/components/ActionTable/Form/Fields/LabeledInput.vue +63 -0
- package/src/components/ActionTable/Form/Fields/MultiFileField.vue +91 -0
- package/src/components/ActionTable/Form/Fields/MultiKeywordField.vue +57 -0
- package/src/components/ActionTable/Form/Fields/NewPasswordField.vue +39 -0
- package/src/components/ActionTable/Form/Fields/NumberField.vue +94 -0
- package/src/components/ActionTable/Form/Fields/NumberRangeField.vue +140 -0
- package/src/components/ActionTable/Form/Fields/SelectDrawer.vue +136 -0
- package/src/components/ActionTable/Form/Fields/SelectField.vue +318 -0
- package/src/components/ActionTable/Form/Fields/SelectWithChildrenField.vue +81 -0
- package/src/components/ActionTable/Form/Fields/SingleFileField.vue +78 -0
- package/src/components/ActionTable/Form/Fields/TextField.vue +82 -0
- package/src/components/ActionTable/Form/Fields/WysiwygField.vue +46 -0
- package/src/components/ActionTable/Form/Fields/index.ts +23 -0
- package/src/components/ActionTable/Form/RenderedForm.vue +76 -0
- package/src/components/ActionTable/Form/index.ts +2 -0
- package/src/components/ActionTable/RenderComponentColumn.vue +22 -0
- package/src/components/ActionTable/TableSummaryRow.vue +95 -0
- package/src/components/ActionTable/index.ts +10 -0
- package/src/components/ActionTable/listActions.ts +362 -0
- package/src/components/ActionTable/listHelpers.ts +74 -0
- package/src/components/ActionTable/tableColumns.ts +72 -0
- package/src/components/DragAndDrop/HandleDraggable.vue +29 -29
- package/src/components/DragAndDrop/ListItemDraggable.vue +10 -10
- package/src/components/DragAndDrop/index.ts +0 -1
- package/src/components/DragAndDrop/listDragAndDrop.ts +1 -1
- package/src/components/Utility/CollapsableSidebar.vue +119 -0
- package/src/components/Utility/ContentDrawer.vue +70 -0
- package/src/components/Utility/Dialogs/ConfirmDialog.vue +132 -0
- package/src/components/Utility/Dialogs/FullScreenDialog.vue +46 -0
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +105 -0
- package/src/components/Utility/Dialogs/InfoDialog.vue +92 -0
- package/src/components/Utility/Dialogs/InputDialog.vue +35 -0
- package/src/components/Utility/ImagePreview.vue +192 -0
- package/src/components/Utility/Popover/PopoverMenu.vue +64 -0
- package/src/components/Utility/Transitions/ListTransition.vue +50 -0
- package/src/components/Utility/Transitions/SlideTransition.vue +63 -0
- package/src/components/Utility/Transitions/StaggeredListTransition.vue +97 -0
- package/src/components/Utility/index.ts +11 -0
- package/src/components/index.ts +3 -0
- package/src/helpers/FileUpload.ts +295 -0
- package/src/helpers/FlashMessages.ts +79 -0
- package/src/helpers/array.ts +37 -0
- package/src/helpers/compatibility.ts +64 -0
- package/src/helpers/date.ts +5 -0
- package/src/helpers/download.ts +200 -0
- package/src/helpers/downloadPdf.ts +92 -0
- package/src/helpers/files.ts +52 -0
- package/src/helpers/formats.ts +183 -0
- package/src/helpers/http.ts +62 -0
- package/src/helpers/index.ts +12 -1
- package/src/helpers/multiFileUpload.ts +68 -0
- package/src/helpers/singleFileUpload.ts +54 -0
- package/src/helpers/storage.ts +8 -0
- package/src/index.esm.js +3 -4
- package/src/svg/FilterIcon.svg +7 -0
- package/src/svg/ImageIcon.svg +30 -0
- package/src/svg/PdfIcon.svg +21 -0
- package/src/svg/PercentIcon.svg +13 -0
- package/src/svg/TrashIcon.svg +15 -0
- package/src/svg/XIcon.svg +18 -0
- package/src/svg/index.ts +8 -0
- package/src/vendor/tinymce-config.ts +1 -0
- package/src/vue-plugin.js +7 -4
- package/tsconfig.json +14 -13
- package/src/components/DragAndDrop/Icons/index.ts +0 -2
- /package/src/{components/DragAndDrop/Icons → svg}/DragHandleDotsIcon.svg +0 -0
- /package/src/{components/DragAndDrop/Icons → svg}/DragHandleIcon.svg +0 -0
@@ -0,0 +1,318 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
<q-select
|
4
|
+
ref="selectField"
|
5
|
+
v-bind="$props"
|
6
|
+
:model-value="selectedValue"
|
7
|
+
outlined
|
8
|
+
hide-dropdown-icon
|
9
|
+
dense
|
10
|
+
emit-value
|
11
|
+
:use-input="filterable"
|
12
|
+
:hide-selected="filterable && isShowing && !$props.multiple"
|
13
|
+
:input-debounce="100"
|
14
|
+
:options="filteredOptions"
|
15
|
+
option-label="label"
|
16
|
+
option-value="value"
|
17
|
+
placeholder=""
|
18
|
+
:input-class="{'is-hidden': !isShowing, [inputClass]: true}"
|
19
|
+
class="max-w-full"
|
20
|
+
@filter="onFilter"
|
21
|
+
@clear="onClear"
|
22
|
+
@popup-show="onShow"
|
23
|
+
@popup-hide="onHide"
|
24
|
+
@update:model-value="onUpdate"
|
25
|
+
>
|
26
|
+
<template #append>
|
27
|
+
<DropDownIcon
|
28
|
+
class="w-4 transition"
|
29
|
+
:class="isShowing ? 'rotate-180' : ''"
|
30
|
+
/>
|
31
|
+
</template>
|
32
|
+
<template #selected>
|
33
|
+
<div
|
34
|
+
v-if="$props.multiple"
|
35
|
+
class="flex gap-y-1 overflow-hidden"
|
36
|
+
:class="{'flex-nowrap gap-y-0': chipLimit === 1, [selectionClass]: true}"
|
37
|
+
>
|
38
|
+
<template v-if="chipOptions.length > 0">
|
39
|
+
<q-chip
|
40
|
+
v-for="chipOption in chipOptions"
|
41
|
+
:key="'selected-' + chipOption.label"
|
42
|
+
class="!mr-1"
|
43
|
+
>{{ chipOption.label }}
|
44
|
+
</q-chip>
|
45
|
+
<q-chip
|
46
|
+
v-if="selectedOptions.length > chipOptions.length"
|
47
|
+
class="!mr-1"
|
48
|
+
>
|
49
|
+
+{{ selectedOptions.length - chipOptions.length }}
|
50
|
+
</q-chip>
|
51
|
+
</template>
|
52
|
+
<template v-else>
|
53
|
+
{{ placeholder }}
|
54
|
+
</template>
|
55
|
+
</div>
|
56
|
+
<div
|
57
|
+
v-else
|
58
|
+
:class="selectionClass"
|
59
|
+
>{{ selectedLabel }}
|
60
|
+
</div>
|
61
|
+
</template>
|
62
|
+
</q-select>
|
63
|
+
</div>
|
64
|
+
</template>
|
65
|
+
<script setup>
|
66
|
+
import { ChevronDownIcon as DropDownIcon } from '@heroicons/vue/outline';
|
67
|
+
import { QSelect } from 'quasar';
|
68
|
+
import { computed, isRef, nextTick, ref } from 'vue';
|
69
|
+
|
70
|
+
const emit = defineEmits(['update:model-value', 'search']);
|
71
|
+
const props = defineProps({
|
72
|
+
...QSelect.props,
|
73
|
+
modelValue: {
|
74
|
+
type: [Array, String, Number, Object],
|
75
|
+
default: undefined
|
76
|
+
},
|
77
|
+
placeholder: {
|
78
|
+
type: String,
|
79
|
+
default: ''
|
80
|
+
},
|
81
|
+
selectionLabel: {
|
82
|
+
type: String,
|
83
|
+
default: null
|
84
|
+
},
|
85
|
+
chipLimit: {
|
86
|
+
type: Number,
|
87
|
+
default: 3
|
88
|
+
},
|
89
|
+
inputClass: {
|
90
|
+
type: String,
|
91
|
+
default: ''
|
92
|
+
},
|
93
|
+
selectionClass: {
|
94
|
+
type: String,
|
95
|
+
default: ''
|
96
|
+
},
|
97
|
+
options: {
|
98
|
+
type: Array,
|
99
|
+
default: () => []
|
100
|
+
},
|
101
|
+
filterable: Boolean,
|
102
|
+
filterFn: {
|
103
|
+
type: Function,
|
104
|
+
default: null
|
105
|
+
}
|
106
|
+
});
|
107
|
+
|
108
|
+
const selectField = ref(null);
|
109
|
+
|
110
|
+
// The filter applied to the dropdown list of options
|
111
|
+
const filter = ref(null);
|
112
|
+
// Is showing the dropdown list
|
113
|
+
const isShowing = ref(false);
|
114
|
+
|
115
|
+
/**
|
116
|
+
* The options formatted so each has a label, value and selectionLabel
|
117
|
+
* @type {ComputedRef<{selectionLabel: string, label: string, value: string|*}[]>}
|
118
|
+
*/
|
119
|
+
const computedOptions = computed(() => {
|
120
|
+
let options = props.options;
|
121
|
+
if (props.placeholder && !props.multiple && !props.filterable) {
|
122
|
+
options = [{ label: props.placeholder, value: null }, ...props.options];
|
123
|
+
}
|
124
|
+
options = options.map((o) => {
|
125
|
+
let opt = isRef(o) ? o.value : o;
|
126
|
+
return {
|
127
|
+
label: resolveLabel(opt),
|
128
|
+
value: resolveValue(opt),
|
129
|
+
selectionLabel: resolveSelectionLabel(opt)
|
130
|
+
};
|
131
|
+
});
|
132
|
+
return options;
|
133
|
+
});
|
134
|
+
|
135
|
+
const filteredOptions = computed(() => {
|
136
|
+
if (filter.value && !props.filterFn) {
|
137
|
+
return computedOptions.value.filter(o => o.label.toLocaleLowerCase().indexOf(filter.value.toLowerCase()) > -1);
|
138
|
+
} else {
|
139
|
+
return computedOptions.value;
|
140
|
+
}
|
141
|
+
});
|
142
|
+
|
143
|
+
/**
|
144
|
+
* The value selected. Basically a wrapper to better handle null values (see onUpdate function for more details)
|
145
|
+
* @type {ComputedRef<unknown>}
|
146
|
+
*/
|
147
|
+
const selectedValue = computed(() => {
|
148
|
+
if (props.multiple) {
|
149
|
+
const arrVal = Array.isArray(props.modelValue) ? props.modelValue : [];
|
150
|
+
return arrVal.map((v) => {
|
151
|
+
return v === null ? '__null__' : v;
|
152
|
+
}) || [];
|
153
|
+
} else {
|
154
|
+
return props.modelValue === null ? '__null__' : props.modelValue;
|
155
|
+
}
|
156
|
+
});
|
157
|
+
|
158
|
+
/**
|
159
|
+
* The selected options. The list of options from the computedOptions list that are selected as defined by the modelValue
|
160
|
+
* @type {ComputedRef<*>}
|
161
|
+
*/
|
162
|
+
const selectedOptions = computed(() => {
|
163
|
+
let values = selectedValue.value;
|
164
|
+
if (!props.multiple) {
|
165
|
+
values = (values || values === 0) ? [values] : [];
|
166
|
+
}
|
167
|
+
return computedOptions.value.filter((o) => {
|
168
|
+
return values.includes(o.value) || values.map(v => typeof v === 'object' && v.id).includes(o.value?.id);
|
169
|
+
});
|
170
|
+
});
|
171
|
+
|
172
|
+
/**
|
173
|
+
* The label to display in the input field of the dropdown for a selected state, non-selected state or an active filtering state.
|
174
|
+
* NOTE: Only applies to single select (not multiselect)
|
175
|
+
* @type {ComputedRef<unknown>}
|
176
|
+
*/
|
177
|
+
const selectedLabel = computed(() => {
|
178
|
+
if (props.filterable && isShowing.value) return '';
|
179
|
+
|
180
|
+
if (!selectedOptions.value || selectedOptions.value.length === 0) {
|
181
|
+
return props.placeholder || '(Select Option)';
|
182
|
+
}
|
183
|
+
return selectedOptions.value[0].selectionLabel;
|
184
|
+
});
|
185
|
+
|
186
|
+
/**
|
187
|
+
* The options to display as chips (limited by chipLimit prop)
|
188
|
+
* @type {ComputedRef<*>}
|
189
|
+
*/
|
190
|
+
const chipOptions = computed(() => {
|
191
|
+
return selectedOptions.value.slice(0, props.chipLimit);
|
192
|
+
});
|
193
|
+
|
194
|
+
/**
|
195
|
+
* Resolve the label of the option. This will display as the text in the dropdown list
|
196
|
+
* @param option
|
197
|
+
* @returns {*|string}
|
198
|
+
*/
|
199
|
+
function resolveLabel(option) {
|
200
|
+
if (typeof option === 'string') {
|
201
|
+
return option;
|
202
|
+
}
|
203
|
+
if (typeof props.optionLabel === 'string') {
|
204
|
+
return option[props.optionLabel];
|
205
|
+
}
|
206
|
+
if (typeof props.optionLabel === 'function') {
|
207
|
+
return props.optionLabel(option);
|
208
|
+
}
|
209
|
+
return option?.label;
|
210
|
+
}
|
211
|
+
|
212
|
+
/**
|
213
|
+
* Resolve the label to display in the input field of the dropdown when an option is selected
|
214
|
+
* NOTE: Does not apply to multiselect, single select only
|
215
|
+
* @param option
|
216
|
+
* @returns {*|{default: null, type: String | StringConstructor}|string}
|
217
|
+
*/
|
218
|
+
function resolveSelectionLabel(option) {
|
219
|
+
if (typeof option === 'string') {
|
220
|
+
return option;
|
221
|
+
}
|
222
|
+
if (typeof props.selectionLabel === 'string') {
|
223
|
+
return option[props.selectionLabel];
|
224
|
+
}
|
225
|
+
if (typeof props.selectionLabel === 'function') {
|
226
|
+
return props.selectionLabel(option);
|
227
|
+
}
|
228
|
+
return option?.selectionLabel || option?.label;
|
229
|
+
}
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Resolve the value of the option using either defaults or the provided optionValue prop
|
233
|
+
* @param option
|
234
|
+
* @returns {string|*|string}
|
235
|
+
*/
|
236
|
+
function resolveValue(option) {
|
237
|
+
if (typeof option === 'string') {
|
238
|
+
return option;
|
239
|
+
}
|
240
|
+
let value = option.value;
|
241
|
+
if (typeof props.optionValue === 'string') {
|
242
|
+
value = option[props.optionValue];
|
243
|
+
} else if (typeof props.optionValue === 'function') {
|
244
|
+
value = props.optionValue(option);
|
245
|
+
}
|
246
|
+
// Note the __null__ special case here. See the onUpdate function for more details
|
247
|
+
return value === null ? '__null__' : value;
|
248
|
+
}
|
249
|
+
|
250
|
+
/**
|
251
|
+
* Handle the update event from the QSelect
|
252
|
+
* NOTE: casts all null values as a special __null__ string as the null character is handled the same as undefined by QSelect.
|
253
|
+
* SelectField differentiates between these 2 to provide a null value selected state vs undefined is no value selected.
|
254
|
+
* @param value
|
255
|
+
*/
|
256
|
+
function onUpdate(value) {
|
257
|
+
if (Array.isArray(value)) {
|
258
|
+
value = value.map((v) => v === '__null__' ? null : v);
|
259
|
+
}
|
260
|
+
emit('update:model-value', value === '__null__' ? null : value);
|
261
|
+
}
|
262
|
+
|
263
|
+
/** XXX: This tells us when we should apply the filter. QSelect likes to trigger a new filter everytime you open the dropdown
|
264
|
+
* But most of the time when you open the dropdown it is already filtered and annoying to try and clear the previous filter
|
265
|
+
**/
|
266
|
+
const shouldFilter = ref(false);
|
267
|
+
|
268
|
+
/**
|
269
|
+
* Filter the options list. Do this asynchronously for the update so that the QSelect can update its internal state first, then update SelectField
|
270
|
+
* @param val
|
271
|
+
* @param update
|
272
|
+
*/
|
273
|
+
async function onFilter(val, update) {
|
274
|
+
if (!props.filterFn) {
|
275
|
+
filter.value = val;
|
276
|
+
await nextTick(update);
|
277
|
+
} else {
|
278
|
+
update();
|
279
|
+
if (shouldFilter.value === false) return;
|
280
|
+
if (val !== null && val !== filter.value) {
|
281
|
+
filter.value = val;
|
282
|
+
if (props.filterFn) {
|
283
|
+
await props.filterFn(val);
|
284
|
+
}
|
285
|
+
}
|
286
|
+
}
|
287
|
+
}
|
288
|
+
|
289
|
+
/**
|
290
|
+
* Clear the selected value using undefined. SelectField differentiates between null and undefined.
|
291
|
+
* See the onUpdate function for more details
|
292
|
+
*/
|
293
|
+
function onClear() {
|
294
|
+
emit('update:model-value', undefined);
|
295
|
+
}
|
296
|
+
|
297
|
+
/**
|
298
|
+
* Handle behavior when showing the dropdown
|
299
|
+
*/
|
300
|
+
function onShow() {
|
301
|
+
isShowing.value = true;
|
302
|
+
|
303
|
+
// XXX: See description on shouldFilter declaration. Only allow filtering after dropdown is ALREADY opened
|
304
|
+
shouldFilter.value = false;
|
305
|
+
nextTick(() => {
|
306
|
+
shouldFilter.value = true;
|
307
|
+
selectField.value.focus();
|
308
|
+
});
|
309
|
+
}
|
310
|
+
|
311
|
+
/**
|
312
|
+
* Handle behavior when hiding the dropdown
|
313
|
+
*/
|
314
|
+
function onHide() {
|
315
|
+
isShowing.value = false;
|
316
|
+
shouldFilter.value = false;
|
317
|
+
}
|
318
|
+
</script>
|
@@ -0,0 +1,81 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
<template v-if="!loading && !options.length">
|
4
|
+
<div class="text-gray-silver">No options available</div>
|
5
|
+
</template>
|
6
|
+
<SelectField
|
7
|
+
v-model="selectedOption"
|
8
|
+
:options="options"
|
9
|
+
:label="label"
|
10
|
+
:placeholder="placeholder"
|
11
|
+
:option-value="opt => opt"
|
12
|
+
:loading="loading"
|
13
|
+
@update:model-value="onSelectOption"
|
14
|
+
/>
|
15
|
+
<div v-if="selectedOption">
|
16
|
+
<q-checkbox
|
17
|
+
v-for="child in selectedOption.children"
|
18
|
+
:key="child.id"
|
19
|
+
:model-value="selectedChildren.includes(child.id)"
|
20
|
+
:field="child"
|
21
|
+
class="mt-3"
|
22
|
+
@update:model-value="onSelectChild(child)"
|
23
|
+
>
|
24
|
+
<div>{{ child.label }}</div>
|
25
|
+
<div class="text-xs text-gray-silver">{{ child.name }}</div>
|
26
|
+
</q-checkbox>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
</template>
|
30
|
+
<script setup>
|
31
|
+
import { remove } from '@ui/helpers/array';
|
32
|
+
import { ref, watch } from 'vue';
|
33
|
+
import SelectField from './SelectField';
|
34
|
+
|
35
|
+
const emit = defineEmits(['update:model-value']);
|
36
|
+
const props = defineProps({
|
37
|
+
modelValue: {
|
38
|
+
type: Array,
|
39
|
+
default: () => ([])
|
40
|
+
},
|
41
|
+
label: {
|
42
|
+
type: String,
|
43
|
+
default: 'Selection'
|
44
|
+
},
|
45
|
+
placeholder: {
|
46
|
+
type: String,
|
47
|
+
default: 'Select an option'
|
48
|
+
},
|
49
|
+
options: {
|
50
|
+
type: Array,
|
51
|
+
default: () => []
|
52
|
+
},
|
53
|
+
loading: Boolean
|
54
|
+
});
|
55
|
+
|
56
|
+
function resolveSelectedOption() {
|
57
|
+
if (props.modelValue?.length > 0) {
|
58
|
+
return props.options.find((option) => option.children.find(child => props.modelValue.includes(child.id)));
|
59
|
+
}
|
60
|
+
|
61
|
+
return null;
|
62
|
+
}
|
63
|
+
const selectedOption = ref(resolveSelectedOption());
|
64
|
+
const selectedChildren = ref(props.modelValue || []);
|
65
|
+
function onSelectChild(child) {
|
66
|
+
if (selectedChildren.value.includes(child.id)) {
|
67
|
+
selectedChildren.value = remove(selectedChildren.value, child.id);
|
68
|
+
} else {
|
69
|
+
selectedChildren.value.push(child.id);
|
70
|
+
}
|
71
|
+
emit('update:model-value', selectedChildren.value.length > 0 ? selectedChildren.value : undefined);
|
72
|
+
}
|
73
|
+
function onSelectOption() {
|
74
|
+
selectedChildren.value = [];
|
75
|
+
emit('update:model-value', undefined);
|
76
|
+
}
|
77
|
+
watch(() => props.modelValue, (value) => {
|
78
|
+
selectedOption.value = resolveSelectedOption();
|
79
|
+
selectedChildren.value = value || [];
|
80
|
+
});
|
81
|
+
</script>
|
@@ -0,0 +1,78 @@
|
|
1
|
+
<template>
|
2
|
+
<div
|
3
|
+
class="max-w-full relative overflow-auto"
|
4
|
+
:class="{'p-4 border rounded border-gray-medium text-center': !readonly}"
|
5
|
+
@dragover.prevent
|
6
|
+
@drop.prevent="onDrop"
|
7
|
+
>
|
8
|
+
<FieldLabel
|
9
|
+
:field="field"
|
10
|
+
:show-name="showName"
|
11
|
+
class="text-sm font-semibold"
|
12
|
+
/>
|
13
|
+
<div
|
14
|
+
v-if="!disable && !readonly"
|
15
|
+
class="text-sm mt-2"
|
16
|
+
>
|
17
|
+
<a
|
18
|
+
class="text-blue-base"
|
19
|
+
@click="$refs.file.click()"
|
20
|
+
>Upload</a>
|
21
|
+
<a
|
22
|
+
v-if="uploadedFile"
|
23
|
+
class="ml-3 text-red-dark"
|
24
|
+
@click="onClear"
|
25
|
+
>Clear</a>
|
26
|
+
<input
|
27
|
+
ref="file"
|
28
|
+
class="hidden"
|
29
|
+
type="file"
|
30
|
+
@change="onFileSelected"
|
31
|
+
/>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<ImagePreview
|
35
|
+
v-if="!readonly || uploadedFile"
|
36
|
+
class="w-32 cursor-pointer mt-2"
|
37
|
+
:class="{'border border-dashed border-blue-base': !uploadedFile, 'mx-auto': !readonly}"
|
38
|
+
:image="uploadedFile"
|
39
|
+
downloadable
|
40
|
+
@click="!disable && $refs.file.click()"
|
41
|
+
/>
|
42
|
+
<div
|
43
|
+
v-else-if="readonly"
|
44
|
+
class="py-1"
|
45
|
+
>--
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
</template>
|
49
|
+
|
50
|
+
<script setup>
|
51
|
+
import { ImagePreview } from '@ui/components';
|
52
|
+
import { useSingleFileUpload } from '@ui/helpers/singleFileUpload';
|
53
|
+
import { onMounted } from 'vue';
|
54
|
+
import FieldLabel from './FieldLabel';
|
55
|
+
|
56
|
+
const emit = defineEmits(['update:model-value']);
|
57
|
+
const props = defineProps({
|
58
|
+
modelValue: {
|
59
|
+
type: [Object, String],
|
60
|
+
default: null
|
61
|
+
},
|
62
|
+
field: {
|
63
|
+
type: Object,
|
64
|
+
required: true
|
65
|
+
},
|
66
|
+
showName: Boolean,
|
67
|
+
disable: Boolean,
|
68
|
+
readonly: Boolean
|
69
|
+
});
|
70
|
+
const { onComplete, onDrop, onFileSelected, uploadedFile, onClear } = useSingleFileUpload();
|
71
|
+
onComplete(() => emit('update:model-value', uploadedFile.value));
|
72
|
+
|
73
|
+
onMounted(() => {
|
74
|
+
if (props.modelValue) {
|
75
|
+
uploadedFile.value = props.modelValue;
|
76
|
+
}
|
77
|
+
});
|
78
|
+
</script>
|
@@ -0,0 +1,82 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
<q-input
|
4
|
+
v-if="!readonly"
|
5
|
+
:data-dusk="'text-field-' + field?.id"
|
6
|
+
:data-testid="'text-field-' + field?.id"
|
7
|
+
:placeholder="field?.placeholder"
|
8
|
+
outlined
|
9
|
+
dense
|
10
|
+
:disable="disabled"
|
11
|
+
:label-slot="!noLabel"
|
12
|
+
:input-class="inputClass"
|
13
|
+
:class="parentClass"
|
14
|
+
stack-label
|
15
|
+
:type="type"
|
16
|
+
:model-value="modelValue"
|
17
|
+
:debounce="debounce"
|
18
|
+
@keydown.enter="$emit('submit')"
|
19
|
+
@update:model-value="$emit('update:model-value', $event)"
|
20
|
+
>
|
21
|
+
<template #label>
|
22
|
+
<FieldLabel
|
23
|
+
:field="field"
|
24
|
+
:label="label"
|
25
|
+
:show-name="showName"
|
26
|
+
:class="labelClass"
|
27
|
+
/>
|
28
|
+
</template>
|
29
|
+
</q-input>
|
30
|
+
<div v-if="readonly">
|
31
|
+
<LabelValueBlock
|
32
|
+
:label="label || field.label"
|
33
|
+
:value="modelValue"
|
34
|
+
/>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</template>
|
38
|
+
|
39
|
+
<script setup>
|
40
|
+
import FieldLabel from './FieldLabel';
|
41
|
+
import LabelValueBlock from './LabelValueBlock';
|
42
|
+
|
43
|
+
defineEmits(['update:model-value', 'submit']);
|
44
|
+
defineProps({
|
45
|
+
modelValue: {
|
46
|
+
type: [String, Number],
|
47
|
+
default: ''
|
48
|
+
},
|
49
|
+
field: {
|
50
|
+
type: Object,
|
51
|
+
default: null
|
52
|
+
},
|
53
|
+
type: {
|
54
|
+
type: String,
|
55
|
+
default: 'text'
|
56
|
+
},
|
57
|
+
label: {
|
58
|
+
type: String,
|
59
|
+
default: null
|
60
|
+
},
|
61
|
+
labelClass: {
|
62
|
+
type: String,
|
63
|
+
default: 'text-sm text-gray-shadow'
|
64
|
+
},
|
65
|
+
parentClass: {
|
66
|
+
type: String,
|
67
|
+
default: ''
|
68
|
+
},
|
69
|
+
inputClass: {
|
70
|
+
type: String,
|
71
|
+
default: ''
|
72
|
+
},
|
73
|
+
noLabel: Boolean,
|
74
|
+
showName: Boolean,
|
75
|
+
disabled: Boolean,
|
76
|
+
readonly: Boolean,
|
77
|
+
debounce: {
|
78
|
+
type: [String, Number],
|
79
|
+
default: 0
|
80
|
+
}
|
81
|
+
});
|
82
|
+
</script>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
<FieldLabel
|
4
|
+
v-if="!noLabel"
|
5
|
+
:field="field"
|
6
|
+
:show-name="showName"
|
7
|
+
class="text-sm font-semibold text-gray-shadow block mb-2"
|
8
|
+
/>
|
9
|
+
<template v-if="readonly">
|
10
|
+
<div
|
11
|
+
class="border border-gray-300 rounded-md p-2 bg-gray-100"
|
12
|
+
v-html="modelValue"
|
13
|
+
/>
|
14
|
+
</template>
|
15
|
+
<TinyMceEditor
|
16
|
+
v-else
|
17
|
+
class="mt-2"
|
18
|
+
:api-key="apiKey"
|
19
|
+
:disabled="disable"
|
20
|
+
:model-value="modelValue"
|
21
|
+
@update:model-value="$emit('update:model-value', $event)"
|
22
|
+
/>
|
23
|
+
</div>
|
24
|
+
</template>
|
25
|
+
|
26
|
+
<script setup>
|
27
|
+
import { apiKey } from '@ui/vendor/tinymce-config';
|
28
|
+
import { default as TinyMceEditor } from '@tinymce/tinymce-vue';
|
29
|
+
import FieldLabel from './FieldLabel';
|
30
|
+
|
31
|
+
defineEmits(['update:model-value']);
|
32
|
+
defineProps({
|
33
|
+
modelValue: {
|
34
|
+
type: [String, Number],
|
35
|
+
default: null
|
36
|
+
},
|
37
|
+
field: {
|
38
|
+
type: Object,
|
39
|
+
required: true
|
40
|
+
},
|
41
|
+
noLabel: Boolean,
|
42
|
+
showName: Boolean,
|
43
|
+
disable: Boolean,
|
44
|
+
readonly: Boolean
|
45
|
+
});
|
46
|
+
</script>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
export { default as BooleanField } from "./BooleanField.vue";
|
2
|
+
export { default as ConfirmPasswordField } from "./ConfirmPasswordField.vue";
|
3
|
+
export { default as DateField } from "./DateField.vue";
|
4
|
+
export { default as DateRangeField } from "./DateRangeField.vue";
|
5
|
+
export { default as DateTimeField } from "./DateTimeField.vue";
|
6
|
+
export { default as DateTimePicker } from "./DateTimePicker.vue";
|
7
|
+
export { default as EditableDiv } from "./EditableDiv.vue";
|
8
|
+
export { default as FieldLabel } from "./FieldLabel.vue";
|
9
|
+
export { default as FileUploadButton } from "./FileUploadButton.vue";
|
10
|
+
export { default as InlineDateTimeField } from "./InlineDateTimeField.vue";
|
11
|
+
export { default as IntegerField } from "./IntegerField.vue";
|
12
|
+
export { default as LabeledInput } from "./LabeledInput.vue";
|
13
|
+
export { default as MultiFileField } from "./MultiFileField.vue";
|
14
|
+
export { default as MultiKeywordField } from "./MultiKeywordField.vue";
|
15
|
+
export { default as NewPasswordField } from "./NewPasswordField.vue";
|
16
|
+
export { default as NumberField } from "./NumberField.vue";
|
17
|
+
export { default as NumberRangeField } from "./NumberRangeField.vue";
|
18
|
+
export { default as SelectDrawer } from "./SelectDrawer.vue";
|
19
|
+
export { default as SelectField } from "./SelectField.vue";
|
20
|
+
export { default as SelectWithChildrenField } from "./SelectWithChildrenField.vue";
|
21
|
+
export { default as SingleFileField } from "./SingleFileField.vue";
|
22
|
+
export { default as TextField } from "./TextField.vue";
|
23
|
+
export { default as WysiwygField } from "./WysiwygField.vue";
|
@@ -0,0 +1,76 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="rendered-form">
|
3
|
+
<div
|
4
|
+
v-for="(field, index) in mappedFields"
|
5
|
+
:key="field.id"
|
6
|
+
:class="{ 'mt-4': index > 0 }"
|
7
|
+
>
|
8
|
+
<Component
|
9
|
+
:is="field.component"
|
10
|
+
v-model="fieldValues[field.name]"
|
11
|
+
:field="field"
|
12
|
+
:label="field.label || undefined"
|
13
|
+
:no-label="noLabel"
|
14
|
+
:show-name="showName"
|
15
|
+
:disable="disable"
|
16
|
+
:readonly="readonly"
|
17
|
+
@update:model-value="onInput(field.name, $event)"
|
18
|
+
/>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</template>
|
22
|
+
<script setup>
|
23
|
+
import { reactive } from 'vue';
|
24
|
+
import {
|
25
|
+
BooleanField,
|
26
|
+
DateField,
|
27
|
+
DateRangeField,
|
28
|
+
IntegerField,
|
29
|
+
MultiFileField,
|
30
|
+
NumberField,
|
31
|
+
SingleFileField,
|
32
|
+
TextField,
|
33
|
+
WysiwygField
|
34
|
+
} from './Fields';
|
35
|
+
|
36
|
+
const emit = defineEmits(['update:values']);
|
37
|
+
const props = defineProps({
|
38
|
+
values: {
|
39
|
+
type: Object,
|
40
|
+
default: null
|
41
|
+
},
|
42
|
+
fields: {
|
43
|
+
type: Array,
|
44
|
+
required: true
|
45
|
+
},
|
46
|
+
noLabel: Boolean,
|
47
|
+
showName: Boolean,
|
48
|
+
disable: Boolean,
|
49
|
+
readonly: Boolean
|
50
|
+
});
|
51
|
+
|
52
|
+
const FORM_FIELD_MAP = {
|
53
|
+
BOOLEAN: BooleanField,
|
54
|
+
DATE: DateField,
|
55
|
+
DATE_RANGE: DateRangeField,
|
56
|
+
INTEGER: IntegerField,
|
57
|
+
NUMBER: NumberField,
|
58
|
+
TEXT: TextField,
|
59
|
+
SINGLE_FILE: SingleFileField,
|
60
|
+
MULTI_FILE: MultiFileField,
|
61
|
+
WYSIWYG: WysiwygField
|
62
|
+
};
|
63
|
+
|
64
|
+
const mappedFields = props.fields.map((field) => ({
|
65
|
+
placeholder: `Enter ${field.label}`,
|
66
|
+
...field,
|
67
|
+
component: FORM_FIELD_MAP[field.type],
|
68
|
+
default: field.type === 'BOOLEAN' ? false : ''
|
69
|
+
}));
|
70
|
+
|
71
|
+
const fieldValues = reactive(props.values || {});
|
72
|
+
|
73
|
+
function onInput(key, value) {
|
74
|
+
emit('update:values', { ...fieldValues, [key]: value });
|
75
|
+
}
|
76
|
+
</script>
|