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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.30",
3
+ "version": "0.4.32",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -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: string;
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="w-32 h-32 m-2 cursor-pointer bg-gray-200"
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 w-32 h-32 m-2 rounded-2xl flex flex-col flex-nowrap items-center overflow-hidden cursor-pointer"
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 class="w-10 m-auto" />
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="sm"
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="sm"
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
 
@@ -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
- const result = await onConfirmAction(action, target, { ...input, ...confirmInput });
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
- return FlashMessages.error(`Failed to refresh ${object.__type} (${object.id}) status: ` + object.name);
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
- setTimeout(() => autoRefreshObject(object, condition, callback), interval);
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
  }
@@ -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
@@ -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
  */
@@ -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;
@@ -20,7 +20,7 @@ export interface FilterGroup {
20
20
  }
21
21
 
22
22
  export interface ListControlsRoutes<T = ActionTargetItem> {
23
- list(pager?: ListControlsPagination): Promise<T[]>;
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;
@@ -31,5 +31,6 @@ export interface RequestOptions {
31
31
 
32
32
  export interface RequestCallOptions extends RequestInit {
33
33
  abortOn?: string;
34
+ params?: AnyObject;
34
35
  }
35
36