quasar-ui-danx 0.4.30 → 0.4.32
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/dist/danx.es.js +4138 -4056
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +70 -70
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActionTable/Form/Fields/EditableDiv.vue +8 -5
- package/src/components/ActionTable/Form/Fields/MultiFileField.vue +34 -6
- package/src/components/ActionTable/controls.ts +18 -6
- package/src/components/Utility/Files/FilePreview.vue +7 -5
- package/src/helpers/actions.ts +17 -1
- package/src/helpers/objectStore.ts +43 -2
- package/src/helpers/request.ts +12 -0
- package/src/helpers/utils.ts +7 -0
- package/src/types/actions.d.ts +1 -0
- package/src/types/controls.d.ts +7 -2
- package/src/types/requests.d.ts +1 -0
package/package.json
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
<div class="inline-block relative">
|
3
3
|
<div
|
4
4
|
contenteditable
|
5
|
-
class="relative inline-block transition duration-300 outline-none outline-offset-0 border-none focus:outline-4 hover:outline-4 rounded-sm z-10"
|
5
|
+
class="relative inline-block transition duration-300 outline-none outline-offset-0 border-none focus:outline-4 hover:outline-4 rounded-sm z-10 min-w-10 min-h-10"
|
6
6
|
:style="{minWidth, minHeight}"
|
7
7
|
:class="contentClass"
|
8
8
|
@input="onInput"
|
@@ -12,9 +12,9 @@
|
|
12
12
|
{{ text }}
|
13
13
|
</div>
|
14
14
|
<div
|
15
|
-
v-if="!text && placeholder"
|
15
|
+
v-if="!text && placeholder && !hasFocus"
|
16
16
|
ref="placeholderDiv"
|
17
|
-
class="text-gray-600 absolute-top-left whitespace-nowrap z-1"
|
17
|
+
class="text-gray-600 absolute-top-left whitespace-nowrap z-1 pointer-events-none"
|
18
18
|
>
|
19
19
|
{{ placeholder }}
|
20
20
|
</div>
|
@@ -27,11 +27,13 @@ import { computed, onMounted, ref, watch } from "vue";
|
|
27
27
|
|
28
28
|
const emit = defineEmits(["update:model-value", "change"]);
|
29
29
|
const props = withDefaults(defineProps<{
|
30
|
-
modelValue
|
30
|
+
modelValue?: string;
|
31
31
|
color?: string;
|
32
|
+
textColor?: string;
|
32
33
|
debounceDelay?: number;
|
33
34
|
placeholder?: string;
|
34
35
|
}>(), {
|
36
|
+
modelValue: "",
|
35
37
|
// NOTE: You must safe-list required colors in tailwind.config.js
|
36
38
|
// Add text-blue-900, hover:bg-blue-200, hover:outline-blue-200, focus:outline-blue-200 and focus:bg-blue-200 for the following config
|
37
39
|
color: "blue-200",
|
@@ -72,6 +74,7 @@ function onInput(e) {
|
|
72
74
|
const contentClass = computed(() => [
|
73
75
|
`hover:bg-${props.color} focus:bg-${props.color}`,
|
74
76
|
`hover:text-${props.textColor} focus:text-${props.textColor}`,
|
75
|
-
`hover:outline-${props.color} focus:outline-${props.color}
|
77
|
+
`hover:outline-${props.color} focus:outline-${props.color}`,
|
78
|
+
text.value ? "" : "!bg-none"
|
76
79
|
]);
|
77
80
|
</script>
|
@@ -24,7 +24,9 @@
|
|
24
24
|
<FilePreview
|
25
25
|
v-for="file in uploadedFiles"
|
26
26
|
:key="'file-upload-' + file.id"
|
27
|
-
class="
|
27
|
+
class="m-2 cursor-pointer bg-gray-200"
|
28
|
+
:class="filePreviewClass"
|
29
|
+
:style="styleSize"
|
28
30
|
:file="file"
|
29
31
|
:related-files="file.transcodes || uploadedFiles"
|
30
32
|
downloadable
|
@@ -33,14 +35,19 @@
|
|
33
35
|
/>
|
34
36
|
<div
|
35
37
|
v-if="!disable && !readonly"
|
36
|
-
class="dx-add-remove-files
|
38
|
+
class="dx-add-remove-files m-2 flex flex-col flex-nowrap items-center overflow-hidden cursor-pointer"
|
39
|
+
:class="filePreviewClass"
|
40
|
+
:style="styleSize"
|
37
41
|
>
|
38
42
|
<div
|
39
43
|
class="dx-add-file flex-grow p-1 pt-3 flex justify-center items-center bg-green-200 text-green-700 w-full hover:bg-green-100"
|
40
44
|
@click="$refs.file.click()"
|
41
45
|
>
|
42
46
|
<div>
|
43
|
-
<AddFileIcon
|
47
|
+
<AddFileIcon
|
48
|
+
class="m-auto"
|
49
|
+
:class="addIconClass"
|
50
|
+
/>
|
44
51
|
<div class="mt-1 text-center">
|
45
52
|
Add
|
46
53
|
</div>
|
@@ -66,7 +73,7 @@
|
|
66
73
|
</template>
|
67
74
|
|
68
75
|
<script setup lang="ts">
|
69
|
-
import { onMounted } from "vue";
|
76
|
+
import { computed, onMounted } from "vue";
|
70
77
|
import { useMultiFileUpload } from "../../../../helpers";
|
71
78
|
import { ImageIcon as AddFileIcon, TrashIcon as RemoveFileIcon } from "../../../../svg";
|
72
79
|
import { FormField, UploadedFile } from "../../../../types";
|
@@ -74,14 +81,28 @@ import { FilePreview } from "../../../Utility";
|
|
74
81
|
import FieldLabel from "./FieldLabel";
|
75
82
|
|
76
83
|
const emit = defineEmits(["update:model-value"]);
|
77
|
-
const props = defineProps<{
|
84
|
+
const props = withDefaults(defineProps<{
|
78
85
|
modelValue?: UploadedFile[];
|
79
86
|
field?: FormField;
|
80
87
|
label?: string;
|
81
88
|
showName?: boolean;
|
82
89
|
disable?: boolean;
|
83
90
|
readonly?: boolean;
|
84
|
-
|
91
|
+
width?: number | string;
|
92
|
+
height?: number | string;
|
93
|
+
addIconClass?: string;
|
94
|
+
filePreviewClass?: string;
|
95
|
+
filePreviewBtnSize?: string;
|
96
|
+
}>(), {
|
97
|
+
modelValue: null,
|
98
|
+
field: null,
|
99
|
+
label: "",
|
100
|
+
width: 128,
|
101
|
+
height: 128,
|
102
|
+
addIconClass: "w-10",
|
103
|
+
filePreviewClass: "rounded-2xl",
|
104
|
+
filePreviewBtnSize: "sm"
|
105
|
+
});
|
85
106
|
|
86
107
|
const { onComplete, onDrop, onFilesSelected, uploadedFiles, clearUploadedFiles, onRemove } = useMultiFileUpload();
|
87
108
|
onMounted(() => {
|
@@ -90,4 +111,11 @@ onMounted(() => {
|
|
90
111
|
}
|
91
112
|
});
|
92
113
|
onComplete(() => emit("update:model-value", uploadedFiles.value));
|
114
|
+
|
115
|
+
const styleSize = computed(() => {
|
116
|
+
return {
|
117
|
+
width: typeof props.width === "number" ? `${props.width}px` : props.width,
|
118
|
+
height: typeof props.height === "number" ? `${props.height}px` : props.height
|
119
|
+
};
|
120
|
+
});
|
93
121
|
</script>
|
@@ -72,7 +72,7 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
|
|
72
72
|
}
|
73
73
|
|
74
74
|
async function loadList() {
|
75
|
-
if (!isInitialized) return;
|
75
|
+
if (!isInitialized || options.isListEnabled === false) return;
|
76
76
|
// isLoadingList.value = true;
|
77
77
|
try {
|
78
78
|
setPagedItems(await options.routes.list(pager.value));
|
@@ -84,7 +84,7 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
|
|
84
84
|
}
|
85
85
|
|
86
86
|
async function loadSummary() {
|
87
|
-
if (!options.routes.summary || !isInitialized) return;
|
87
|
+
if (!options.routes.summary || !isInitialized || options.isSummaryEnabled === false) return;
|
88
88
|
|
89
89
|
isLoadingSummary.value = true;
|
90
90
|
const summaryFilter: ListControlsFilter = { id: null, ...activeFilter.value, ...globalFilter.value };
|
@@ -115,7 +115,8 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
|
|
115
115
|
* Loads the filter field options for the current filter.
|
116
116
|
*/
|
117
117
|
async function loadFieldOptions() {
|
118
|
-
if (!options.routes.fieldOptions) return;
|
118
|
+
if (!options.routes.fieldOptions || options.isFieldOptionsEnabled === false) return;
|
119
|
+
|
119
120
|
isLoadingFilters.value = true;
|
120
121
|
try {
|
121
122
|
fieldOptions.value = await options.routes.fieldOptions(activeFilter.value) || {};
|
@@ -207,7 +208,7 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
|
|
207
208
|
* Loads more items into the list.
|
208
209
|
*/
|
209
210
|
async function loadMore(index: number, perPage: number | undefined = undefined) {
|
210
|
-
if (!options.routes.more) return false;
|
211
|
+
if (!options.routes.more || options.isListEnabled === false) return false;
|
211
212
|
|
212
213
|
try {
|
213
214
|
const newItems = await options.routes.more({
|
@@ -296,7 +297,7 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
|
|
296
297
|
async function getActiveItemDetails() {
|
297
298
|
try {
|
298
299
|
const latestResult = latestCallOnly("active-item", async () => {
|
299
|
-
if (!activeItem.value || !options.routes.details) return undefined;
|
300
|
+
if (!activeItem.value || !options.routes.details || options.isDetailsEnabled === false) return undefined;
|
300
301
|
return await options.routes.details(activeItem.value);
|
301
302
|
});
|
302
303
|
|
@@ -414,12 +415,22 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
|
|
414
415
|
options.routes.export && await options.routes.export(filter);
|
415
416
|
}
|
416
417
|
|
418
|
+
function setOptions(newOptions: Partial<ListControlsOptions>) {
|
419
|
+
options = { ...options, ...newOptions };
|
420
|
+
}
|
421
|
+
|
417
422
|
// Initialize the list actions and load settings, lists, summaries, filter fields, etc.
|
418
|
-
function initialize() {
|
423
|
+
function initialize(updateOptions?: Partial<ListControlsOptions>) {
|
419
424
|
const vueRouter = getVueRouter();
|
420
425
|
isInitialized = true;
|
426
|
+
|
427
|
+
if (updateOptions) {
|
428
|
+
options = { ...options, ...updateOptions };
|
429
|
+
}
|
430
|
+
|
421
431
|
loadSettings();
|
422
432
|
|
433
|
+
|
423
434
|
/**
|
424
435
|
* Watch the id params in the route and set the active item to the item with the given id.
|
425
436
|
*/
|
@@ -496,6 +507,7 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
|
|
496
507
|
|
497
508
|
// List controls
|
498
509
|
initialize,
|
510
|
+
setOptions,
|
499
511
|
resetPaging,
|
500
512
|
setPagination,
|
501
513
|
setSelectedRows,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<template>
|
2
2
|
<div
|
3
|
-
class="relative flex justify-center bg-gray-100 overflow-hidden"
|
3
|
+
class="group relative flex justify-center bg-gray-100 overflow-hidden"
|
4
4
|
:class="{'rounded-2xl': !square}"
|
5
5
|
>
|
6
6
|
<template v-if="computedImage">
|
@@ -91,10 +91,10 @@
|
|
91
91
|
</slot>
|
92
92
|
</template>
|
93
93
|
|
94
|
-
<div class="absolute top-1 right-1 flex items-center justify-between space-x-1">
|
94
|
+
<div class="absolute top-1 right-1 flex items-center flex-nowrap justify-between space-x-1 transition-all opacity-0 group-hover:opacity-100">
|
95
95
|
<QBtn
|
96
96
|
v-if="downloadable && computedImage?.url"
|
97
|
-
size="
|
97
|
+
:size="btnSize"
|
98
98
|
class="dx-file-preview-download py-1 px-2 opacity-70 hover:opacity-100"
|
99
99
|
:class="downloadButtonClass"
|
100
100
|
@click.stop="download(computedImage.url)"
|
@@ -104,7 +104,7 @@
|
|
104
104
|
|
105
105
|
<QBtn
|
106
106
|
v-if="removable"
|
107
|
-
size="
|
107
|
+
:size="btnSize"
|
108
108
|
class="dx-file-preview-remove bg-red-900 text-white opacity-50 hover:opacity-100 py-1 px-2"
|
109
109
|
@click.stop="onRemove"
|
110
110
|
>
|
@@ -158,6 +158,7 @@ export interface FilePreviewProps {
|
|
158
158
|
removable?: boolean;
|
159
159
|
disabled?: boolean;
|
160
160
|
square?: boolean;
|
161
|
+
btnSize?: "xs" | "sm" | "md" | "lg";
|
161
162
|
}
|
162
163
|
|
163
164
|
const emit = defineEmits(["remove"]);
|
@@ -172,7 +173,8 @@ const props = withDefaults(defineProps<FilePreviewProps>(), {
|
|
172
173
|
downloadable: false,
|
173
174
|
removable: false,
|
174
175
|
disabled: false,
|
175
|
-
square: false
|
176
|
+
square: false,
|
177
|
+
btnSize: "sm"
|
176
178
|
});
|
177
179
|
|
178
180
|
|
package/src/helpers/actions.ts
CHANGED
@@ -133,7 +133,19 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionGlobal
|
|
133
133
|
activeActionVnode.value = {
|
134
134
|
vnode,
|
135
135
|
confirm: async (confirmInput: any) => {
|
136
|
-
|
136
|
+
|
137
|
+
// Resolve the input based on the useInputFromConfirm option
|
138
|
+
// Not setting useInputFromConfirm will merge the input from the confirm dialog with the input from the action
|
139
|
+
let resolvedInput;
|
140
|
+
if (action.useInputFromConfirm === false) {
|
141
|
+
resolvedInput = input;
|
142
|
+
} else if (action.useInputFromConfirm === true) {
|
143
|
+
resolvedInput = confirmInput;
|
144
|
+
} else {
|
145
|
+
resolvedInput = { ...input, ...confirmInput };
|
146
|
+
}
|
147
|
+
|
148
|
+
const result = await onConfirmAction(action, target, resolvedInput);
|
137
149
|
|
138
150
|
// Only resolve when we have a non-error response, so we can show the error message w/o
|
139
151
|
// hiding the dialog / vnode
|
@@ -166,6 +178,10 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionGlobal
|
|
166
178
|
result.item = storeObject(result.item);
|
167
179
|
}
|
168
180
|
|
181
|
+
if (result?.result?.__type) {
|
182
|
+
result.result = storeObject(result.result);
|
183
|
+
}
|
184
|
+
|
169
185
|
return result;
|
170
186
|
}
|
171
187
|
|
@@ -19,6 +19,10 @@ export function storeObjects<T extends TypedObject>(newObjects: T[]) {
|
|
19
19
|
* Returns the stored object that should be used instead of the passed object as the returned object is shared across the system
|
20
20
|
*/
|
21
21
|
export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredObjects: AnyObject = {}): ShallowReactive<T> {
|
22
|
+
if (typeof newObject !== "object") {
|
23
|
+
return newObject;
|
24
|
+
}
|
25
|
+
|
22
26
|
const id = newObject?.id || newObject?.name;
|
23
27
|
const type = newObject?.__type;
|
24
28
|
if (!id || !type) return shallowReactive(newObject);
|
@@ -76,9 +80,37 @@ export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredO
|
|
76
80
|
store.set(objectKey, reactiveObject);
|
77
81
|
}
|
78
82
|
|
83
|
+
if (reactiveObject.__deleted_at) {
|
84
|
+
removeObjectFromLists(reactiveObject);
|
85
|
+
}
|
86
|
+
|
79
87
|
return reactiveObject;
|
80
88
|
}
|
81
89
|
|
90
|
+
/**
|
91
|
+
* Remove an object from all lists in the store
|
92
|
+
*/
|
93
|
+
function removeObjectFromLists<T extends TypedObject>(object: T) {
|
94
|
+
for (const storedObject of store.values()) {
|
95
|
+
for (const key of Object.keys(storedObject)) {
|
96
|
+
const value = storedObject[key];
|
97
|
+
if (Array.isArray(value) && value.length > 0) {
|
98
|
+
const index = value.findIndex(v => v.__id === object.__id && v.__type === object.__type);
|
99
|
+
if (index !== -1) {
|
100
|
+
value.splice(index, 1);
|
101
|
+
storedObject[key] = [...value];
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Auto refresh an object based on a condition and a callback. Returns the timeout ID for the auto-refresh.
|
110
|
+
* NOTE: Use the timeout ID to clear the auto-refresh when the object is no longer needed (eg: when the component is unmounted)
|
111
|
+
*/
|
112
|
+
const registeredAutoRefreshes: AnyObject = {};
|
113
|
+
|
82
114
|
export async function autoRefreshObject<T extends TypedObject>(object: T, condition: (object: T) => boolean, callback: (object: T) => Promise<T>, interval = 3000) {
|
83
115
|
if (!object?.id || !object?.__type) {
|
84
116
|
throw new Error("Invalid stored object. Cannot auto-refresh");
|
@@ -88,11 +120,20 @@ export async function autoRefreshObject<T extends TypedObject>(object: T, condit
|
|
88
120
|
const refreshedObject = await callback(object);
|
89
121
|
|
90
122
|
if (!refreshedObject.id) {
|
91
|
-
|
123
|
+
FlashMessages.error(`Failed to refresh ${object.__type} (${object.id}) status: ` + object.name);
|
124
|
+
return null;
|
92
125
|
}
|
93
126
|
|
94
127
|
storeObject(refreshedObject);
|
95
128
|
}
|
96
129
|
|
97
|
-
|
130
|
+
// Save the timeoutId to the object so it can be cleared when the object refresh is no longer needed
|
131
|
+
const timeoutId = setTimeout(() => autoRefreshObject(object, condition, callback, interval), interval);
|
132
|
+
registeredAutoRefreshes[object.__type + ":" + object.id] = timeoutId;
|
133
|
+
}
|
134
|
+
|
135
|
+
export async function stopAutoRefreshObject<T extends TypedObject>(object: T) {
|
136
|
+
const timeoutId = registeredAutoRefreshes[object.__type + ":" + object.id];
|
137
|
+
|
138
|
+
timeoutId && clearTimeout(timeoutId);
|
98
139
|
}
|
package/src/helpers/request.ts
CHANGED
@@ -35,6 +35,18 @@ export const request: RequestApi = {
|
|
35
35
|
options.signal = abort.signal;
|
36
36
|
}
|
37
37
|
|
38
|
+
if (options.params) {
|
39
|
+
// Transform object values in params to JSON strings
|
40
|
+
for (const [key, value] of Object.entries(options.params)) {
|
41
|
+
if (typeof value === "object" && value !== null) {
|
42
|
+
options.params[key] = JSON.stringify(value);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
url += (url.match(/\?/) ? "&" : "?") + new URLSearchParams(options.params).toString();
|
47
|
+
delete options.params;
|
48
|
+
}
|
49
|
+
|
38
50
|
const response = await fetch(request.url(url), options);
|
39
51
|
|
40
52
|
// Verify the app version of the client and server are matching
|
package/src/helpers/utils.ts
CHANGED
@@ -12,6 +12,13 @@ export function sleep(delay: number) {
|
|
12
12
|
return new Promise((resolve) => setTimeout(resolve, delay));
|
13
13
|
}
|
14
14
|
|
15
|
+
/**
|
16
|
+
* Deep clone an object
|
17
|
+
*/
|
18
|
+
export function cloneDeep(obj: any) {
|
19
|
+
return JSON.parse(JSON.stringify(obj));
|
20
|
+
}
|
21
|
+
|
15
22
|
/**
|
16
23
|
* Poll a callback function until the result is true
|
17
24
|
*/
|
package/src/types/actions.d.ts
CHANGED
@@ -32,6 +32,7 @@ export interface ActionOptions<T = ActionTargetItem> {
|
|
32
32
|
category?: string;
|
33
33
|
class?: string;
|
34
34
|
debounce?: number;
|
35
|
+
useInputFromConfirm?: boolean;
|
35
36
|
optimistic?: boolean | ((action: ActionOptions<T>, target: T | null, input: any) => void);
|
36
37
|
vnode?: (target: ActionTarget<T>, data: any) => VNode | any;
|
37
38
|
enabled?: (target: ActionTarget<T>) => boolean;
|
package/src/types/controls.d.ts
CHANGED
@@ -20,7 +20,7 @@ export interface FilterGroup {
|
|
20
20
|
}
|
21
21
|
|
22
22
|
export interface ListControlsRoutes<T = ActionTargetItem> {
|
23
|
-
list(pager?: ListControlsPagination): Promise<
|
23
|
+
list(pager?: ListControlsPagination): Promise<PagedItems>;
|
24
24
|
|
25
25
|
summary?(filter?: ListControlsFilter): Promise<AnyObject>;
|
26
26
|
|
@@ -47,6 +47,10 @@ export interface ListControlsOptions {
|
|
47
47
|
urlPattern?: RegExp | null;
|
48
48
|
filterDefaults?: Record<string, object>;
|
49
49
|
refreshFilters?: boolean;
|
50
|
+
isListEnabled?: boolean;
|
51
|
+
isSummaryEnabled?: boolean;
|
52
|
+
isDetailsEnabled?: boolean;
|
53
|
+
isFieldOptionsEnabled?: boolean;
|
50
54
|
}
|
51
55
|
|
52
56
|
export interface ListControlsPagination {
|
@@ -92,7 +96,8 @@ export interface ListController<T = ActionTargetItem> {
|
|
92
96
|
activePanel: Ref<string | null>;
|
93
97
|
|
94
98
|
// List Controls
|
95
|
-
initialize: () => void;
|
99
|
+
initialize: (updateOptions?: Partial<ListControlsOptions>) => void;
|
100
|
+
setOptions: (updateOptions: Partial<ListControlsOptions>) => void;
|
96
101
|
resetPaging: () => void;
|
97
102
|
setPagination: (updated: Partial<ListControlsPagination>) => void;
|
98
103
|
setSelectedRows: (selection: T[]) => void;
|