quasar-ui-danx 0.4.30 → 0.4.32

Sign up to get free protection for your applications and to get access to all the features.
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