quasar-ui-danx 0.4.43 → 0.4.46

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.43",
3
+ "version": "0.4.46",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -115,7 +115,7 @@ 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 || options.isFieldOptionsEnabled === false) return;
118
+ if (isLoadingFilters.value || !options.routes.fieldOptions || options.isFieldOptionsEnabled === false) return;
119
119
 
120
120
  isLoadingFilters.value = true;
121
121
  try {
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <QBtn
3
3
  :loading="isSaving"
4
- class="shadow-none"
5
- :class="disabled ? disabledClass : colorClass"
4
+ class="shadow-none py-0"
5
+ :class="buttonClass"
6
6
  :disable="disabled"
7
7
  @click="()=> onAction()"
8
8
  >
@@ -16,6 +16,7 @@
16
16
  <div
17
17
  v-if="label"
18
18
  class="ml-2"
19
+ :class="labelClass"
19
20
  >
20
21
  {{ label }}
21
22
  </div>
@@ -25,7 +26,9 @@
25
26
  v-if="tooltip"
26
27
  class="whitespace-nowrap"
27
28
  >
28
- {{ tooltip }}
29
+ <slot name="tooltip">
30
+ {{ tooltip }}
31
+ </slot>
29
32
  </QTooltip>
30
33
  <QMenu
31
34
  v-if="isConfirming"
@@ -70,9 +73,11 @@ import { ActionTarget, ResourceAction } from "../../../types";
70
73
  export interface ActionButtonProps {
71
74
  type?: "trash" | "trash-red" | "create" | "edit" | "copy" | "play" | "stop" | "pause" | "refresh" | "confirm" | "cancel";
72
75
  color?: "red" | "blue" | "sky" | "green" | "green-invert" | "lime" | "white" | "gray";
76
+ size?: "xxs" | "xs" | "sm" | "md" | "lg";
73
77
  icon?: object | string;
74
78
  iconClass?: string;
75
- label?: string;
79
+ label?: string | number;
80
+ labelClass?: string;
76
81
  tooltip?: string;
77
82
  saving?: boolean;
78
83
  action?: ResourceAction;
@@ -90,7 +95,9 @@ const props = withDefaults(defineProps<ActionButtonProps>(), {
90
95
  color: null,
91
96
  icon: null,
92
97
  iconClass: "",
98
+ size: "md",
93
99
  label: "",
100
+ labelClass: "",
94
101
  tooltip: "",
95
102
  action: null,
96
103
  target: null,
@@ -99,6 +106,36 @@ const props = withDefaults(defineProps<ActionButtonProps>(), {
99
106
  disabledClass: "text-slate-800 bg-slate-500 opacity-50"
100
107
  });
101
108
 
109
+ const mappedSizeClass = {
110
+ xxs: {
111
+ icon: "w-2",
112
+ button: "px-.5 h-5"
113
+ },
114
+ xs: {
115
+ icon: "w-3",
116
+ button: "px-1.5 h-6"
117
+ },
118
+ sm: {
119
+ icon: "w-4",
120
+ button: "px-2 h-8"
121
+ },
122
+ md: {
123
+ icon: "w-5",
124
+ button: "px-2.5 h-10"
125
+ },
126
+ lg: {
127
+ icon: "w-6",
128
+ button: "px-3 h-12"
129
+ }
130
+ };
131
+
132
+ const buttonClass = computed(() => {
133
+ return {
134
+ [props.disabled ? props.disabledClass : colorClass.value]: true,
135
+ [mappedSizeClass[props.size].button]: true
136
+ };
137
+ });
138
+
102
139
  const colorClass = computed(() => {
103
140
  switch (props.color) {
104
141
  case "red":
@@ -122,61 +159,62 @@ const colorClass = computed(() => {
122
159
  }
123
160
  });
124
161
  const typeOptions = computed(() => {
162
+ const iconClass = mappedSizeClass[props.size].icon;
125
163
  switch (props.type) {
126
164
  case "trash":
127
165
  return {
128
166
  icon: TrashIcon,
129
- iconClass: "w-3"
167
+ iconClass
130
168
  };
131
169
  case "create":
132
170
  return {
133
171
  icon: CreateIcon,
134
- iconClass: "w-3"
172
+ iconClass
135
173
  };
136
174
  case "confirm":
137
175
  return {
138
176
  icon: ConfirmIcon,
139
- iconClass: "w-3"
177
+ iconClass
140
178
  };
141
179
  case "cancel":
142
180
  return {
143
181
  icon: CancelIcon,
144
- iconClass: "w-3"
182
+ iconClass
145
183
  };
146
184
  case "edit":
147
185
  return {
148
186
  icon: EditIcon,
149
- iconClass: "w-3"
187
+ iconClass
150
188
  };
151
189
  case "copy":
152
190
  return {
153
191
  icon: CopyIcon,
154
- iconClass: "w-3"
192
+ iconClass
155
193
  };
156
194
  case "play":
157
195
  return {
158
196
  icon: PlayIcon,
159
- iconClass: "w-3"
197
+ iconClass
160
198
  };
161
199
  case "stop":
162
200
  return {
163
201
  icon: StopIcon,
164
- iconClass: "w-3"
202
+ iconClass
165
203
  };
166
204
  case "pause":
167
205
  return {
168
206
  icon: PauseIcon,
169
- iconClass: "w-3"
207
+ iconClass
170
208
  };
171
209
  case "refresh":
172
210
  return {
173
211
  icon: RefreshIcon,
174
- iconClass: "w-4"
212
+ iconClass
175
213
  };
176
214
  default:
177
215
  return {
178
216
  icon: EditIcon,
179
- iconClass: "w-3"
217
+ iconClass
180
218
  };
181
219
  }
182
220
  });
@@ -1,37 +1,21 @@
1
1
  <template>
2
- <QBtn
3
- class="py-1 px-2"
4
- :disable="disable"
2
+ <ActionButton
3
+ :icon="isShowing ? (hideIcon || DefaultHideIcon) : (showIcon || DefaultShowIcon)"
4
+ :label="(isShowing ? hideLabel : showLabel) || label"
5
+ :label-class="labelClass"
5
6
  @click="onToggle"
6
7
  >
7
- <div class="flex items-center flex-nowrap whitespace-nowrap">
8
- <slot :is-showing="isShowing">
9
- <component
10
- :is="isShowing ? (hideIcon || DefaultHideIcon) : (showIcon || DefaultShowIcon)"
11
- :class="iconClass"
12
- />
13
- </slot>
14
- <slot
15
- name="label"
16
- :is-showing="isShowing"
17
- >
18
- <div
19
- v-if="label"
20
- :class="labelClass"
21
- >
22
- {{ (isShowing ? hideLabel : showLabel) || label }}
23
- </div>
24
- </slot>
25
- </div>
26
- <QTooltip v-if="tooltip">
27
- {{ tooltip }}
28
- </QTooltip>
29
- </QBtn>
8
+ <slot />
9
+ <template #tooltip>
10
+ <slot name="tooltip" />
11
+ </template>
12
+ </ActionButton>
30
13
  </template>
31
14
  <script lang="ts" setup>
32
15
  import { FaSolidEye as DefaultShowIcon, FaSolidEyeSlash as DefaultHideIcon } from "danx-icon";
33
16
  import { nextTick } from "vue";
34
17
  import { getItem, setItem } from "../../../helpers";
18
+ import ActionButton from "./ActionButton";
35
19
 
36
20
  export interface Props {
37
21
  name?: string;
@@ -39,14 +23,11 @@ export interface Props {
39
23
  hideLabel?: string;
40
24
  showIcon?: object | string;
41
25
  hideIcon?: object | string;
42
- iconClass?: string;
43
26
  labelClass?: string;
44
- label?: string;
45
- tooltip?: string;
46
- disable?: boolean;
27
+ label?: string | number;
47
28
  }
48
29
 
49
- const emit = defineEmits(["show", "hide"]);
30
+ const emit = defineEmits<{ show: void, hide: void }>();
50
31
  const isShowing = defineModel<boolean>();
51
32
  const props = withDefaults(defineProps<Props>(), {
52
33
  name: "",
@@ -54,25 +35,24 @@ const props = withDefaults(defineProps<Props>(), {
54
35
  hideLabel: "",
55
36
  showIcon: null,
56
37
  hideIcon: null,
57
- iconClass: "w-4 h-6",
58
38
  labelClass: "ml-2",
59
- label: "",
60
- tooltip: ""
39
+ label: ""
61
40
  });
62
41
 
42
+ console.log("isShowing", isShowing.value);
63
43
  const SETTINGS_KEY = "show-hide-button";
64
44
  const settings = getItem(SETTINGS_KEY, {});
65
45
 
66
46
  if (props.name) {
67
47
  if (settings[props.name] !== undefined) {
68
48
  isShowing.value = settings[props.name];
49
+ console.log("setting is sowing to", isShowing.value);
69
50
  }
70
51
  }
71
52
 
72
53
  function onToggle() {
73
54
  isShowing.value = !isShowing.value;
74
55
 
75
-
76
56
  // NOTE: use nextTick to ensure the value is updated before saving (if the parent does not pass a value for modelValue, this can cause a desync)
77
57
  nextTick(() => {
78
58
  if (isShowing.value) {
@@ -138,7 +138,7 @@
138
138
  <script setup lang="ts">
139
139
  import { DocumentTextIcon as TextFileIcon, DownloadIcon, PlayIcon } from "@heroicons/vue/outline";
140
140
  import { computed, ComputedRef, onMounted, ref } from "vue";
141
- import { download, FileUpload } from "../../../helpers";
141
+ import { download, FileUpload, uniqueBy } from "../../../helpers";
142
142
  import { ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
143
143
  import { UploadedFile } from "../../../types";
144
144
  import { FullScreenCarouselDialog } from "../Dialogs";
@@ -202,10 +202,16 @@ const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
202
202
  });
203
203
 
204
204
  const isUploading = computed(() => !props.file || props.file?.progress !== undefined);
205
- const previewableFiles: ComputedRef<[UploadedFile | null]> = computed(() => {
206
- return props.relatedFiles?.length > 0 ? props.relatedFiles : [computedImage.value];
205
+ const previewableFiles: ComputedRef<(UploadedFile | null)[] | null> = computed(() => {
206
+ return props.relatedFiles?.length > 0 ? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl) : [computedImage.value];
207
207
  });
208
208
 
209
+ function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
210
+ return a.id === b.id ||
211
+ [b.url, b.optimized?.url, b.thumb?.url].includes(a.url) ||
212
+ [a.url, a.optimized?.url, a.thumb?.url].includes(b.url);
213
+ }
214
+
209
215
  const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
210
216
  const mimeType = computed(
211
217
  () => computedImage.value?.type || computedImage.value?.mime || ""
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <div :class="{[colorClass]: true, [sizeClass]: true, 'rounded-full': true}">
3
+ <slot>{{ label }}</slot>
4
+ </div>
5
+ </template>
6
+ <script setup lang="ts">
7
+ import { computed } from "vue";
8
+ import { LabelPillWidgetProps } from "../../../types";
9
+
10
+ const props = withDefaults(defineProps<LabelPillWidgetProps>(), {
11
+ label: "",
12
+ color: "none",
13
+ size: "md"
14
+ });
15
+
16
+ const colorClasses = {
17
+ sky: "bg-sky-950 text-sky-400",
18
+ green: "bg-green-950 text-green-400",
19
+ red: "bg-red-950 text-red-400",
20
+ amber: "bg-amber-950 text-amber-400",
21
+ yellow: "bg-yellow-950 text-yellow-400",
22
+ blue: "bg-blue-950 text-blue-400",
23
+ slate: "bg-slate-950 text-slate-400",
24
+ gray: "bg-slate-700 text-gray-300",
25
+ none: ""
26
+ };
27
+
28
+ const sizeClasses = {
29
+ xxs: "text-xs px-1 py-.5",
30
+ xs: "text-xs px-2 py-1",
31
+ sm: "text-sm px-3 py-1.5",
32
+ md: "text-base px-3 py-2",
33
+ lg: "text-lg px-4 py-2"
34
+ };
35
+
36
+ const colorClass = computed(() => {
37
+ return colorClasses[props.color];
38
+ });
39
+ const sizeClass = computed(() => sizeClasses[props.size]);
40
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <div class="flex items-stretch">
3
+ <div
4
+ class="rounded-l-lg bg-slate-400 text-slate-900 text-xs px-2 flex items-center justify-center"
5
+ :class="labelClass"
6
+ >
7
+ <slot name="label">
8
+ {{ label }}
9
+ </slot>
10
+ </div>
11
+ <div class="rounded-r-lg bg-slate-900 text-slate-400 px-2 py-1">
12
+ <slot name="value">
13
+ {{ value }}
14
+ </slot>
15
+ </div>
16
+ </div>
17
+ </template>
18
+ <script setup lang="ts">
19
+ withDefaults(defineProps<{
20
+ label: string;
21
+ value: string;
22
+ labelClass?: string;
23
+ }>(), {
24
+ labelClass: ""
25
+ });
26
+ </script>
@@ -0,0 +1,2 @@
1
+ export { default as LabelPillWidget } from "./LabelPillWidget.vue";
2
+ export { default as LabelValuePillWidget } from "./LabelValuePillWidget.vue";
@@ -8,3 +8,4 @@ export * from "./Popovers";
8
8
  export * from "./Tabs";
9
9
  export * from "./Tools";
10
10
  export * from "./Transitions";
11
+ export * from "./Widgets";
@@ -2,31 +2,31 @@
2
2
  * Replace an item in an array with a new item
3
3
  */
4
4
  export function replace(array: any[], item: any, newItem = undefined, appendIfMissing = false) {
5
- const index: any = typeof item === "function" ? array.findIndex(item) : array.indexOf(item);
6
- if (index === false || index === -1) {
7
- return appendIfMissing ? [...array, newItem] : array;
8
- }
5
+ const index: any = typeof item === "function" ? array.findIndex(item) : array.indexOf(item);
6
+ if (index === false || index === -1) {
7
+ return appendIfMissing ? [...array, newItem] : array;
8
+ }
9
9
 
10
- const newArray = [...array];
11
- newItem !== undefined
12
- ? newArray.splice(index, 1, newItem)
13
- : newArray.splice(index, 1);
14
- return newArray;
10
+ const newArray = [...array];
11
+ newItem !== undefined
12
+ ? newArray.splice(index, 1, newItem)
13
+ : newArray.splice(index, 1);
14
+ return newArray;
15
15
  }
16
16
 
17
17
  /**
18
18
  * Remove an item from an array
19
19
  */
20
20
  export function remove(array: any[], item: any) {
21
- return replace(array, item);
21
+ return replace(array, item);
22
22
  }
23
23
 
24
24
  /**
25
25
  * Remove duplicate items from an array using a callback to compare 2 elements
26
26
  */
27
- export function uniqueBy(array: any[], cb: Function) {
28
- return array.filter((a, index, self) => {
29
- // Check if the current element 'a' is the first occurrence in the array
30
- return index === self.findIndex((b) => cb(a, b));
31
- });
27
+ export function uniqueBy(array: any[], cb: (a: any, b: any) => boolean) {
28
+ return array.filter((a, index, self) => {
29
+ // Check if the current element 'a' is the first occurrence in the array
30
+ return index === self.findIndex((b) => cb(a, b));
31
+ });
32
32
  }
@@ -92,6 +92,7 @@ function storeObjectChildren<T extends TypedObject>(object: T, recentlyStoredObj
92
92
  for (const index in value) {
93
93
  if (value[index] && typeof value[index] === "object") {
94
94
  if (!applyToObject[key]) {
95
+ // @ts-expect-error this is fine... T is generic, but not sure why the matter to write to an object?
95
96
  applyToObject[key] = [];
96
97
  }
97
98
  applyToObject[key][index] = storeObject(value[index], recentlyStoredObjects);
@@ -128,7 +129,10 @@ function removeObjectFromLists<T extends TypedObject>(object: T) {
128
129
  */
129
130
  const registeredAutoRefreshes: AnyObject = {};
130
131
 
131
- export async function autoRefreshObject<T extends TypedObject>(object: T, condition: (object: T) => boolean, callback: (object: T) => Promise<T>, interval = 3000) {
132
+ export async function autoRefreshObject<T extends TypedObject>(name: string, object: T, condition: (object: T) => boolean, callback: (object: T) => Promise<T>, interval = 3000) {
133
+ // Always clear any previously registered auto refreshes before creating a new timeout
134
+ stopAutoRefreshObject(name);
135
+
132
136
  if (!object?.id || !object?.__type) {
133
137
  throw new Error("Invalid stored object. Cannot auto-refresh");
134
138
  }
@@ -144,13 +148,11 @@ export async function autoRefreshObject<T extends TypedObject>(object: T, condit
144
148
  storeObject(refreshedObject);
145
149
  }
146
150
 
147
- // Save the timeoutId to the object so it can be cleared when the object refresh is no longer needed
148
- const timeoutId = setTimeout(() => autoRefreshObject(object, condition, callback, interval), interval);
149
- registeredAutoRefreshes[object.__type + ":" + object.id] = timeoutId;
151
+ // Save the autoRefreshId for the object so it can be cleared when the object refresh is no longer needed
152
+ registeredAutoRefreshes[name] = setTimeout(() => autoRefreshObject(name, object, condition, callback, interval), interval);
150
153
  }
151
154
 
152
- export async function stopAutoRefreshObject<T extends TypedObject>(object: T) {
153
- const timeoutId = registeredAutoRefreshes[object.__type + ":" + object.id];
154
-
155
+ export function stopAutoRefreshObject(name: string) {
156
+ const timeoutId = registeredAutoRefreshes[name];
155
157
  timeoutId && clearTimeout(timeoutId);
156
158
  }
@@ -33,6 +33,9 @@ export async function pollUntil(callback: () => any, interval = 1000) {
33
33
  */
34
34
  export function waitForRef(ref: Ref, value: any) {
35
35
  return new Promise<void>((resolve) => {
36
+ if (ref.value === value) {
37
+ resolve();
38
+ }
36
39
  watch(ref, (newValue) => {
37
40
  if (newValue === value) {
38
41
  resolve();
@@ -31,7 +31,7 @@ export interface ListControlsRoutes<T = ActionTargetItem> {
31
31
 
32
32
  fieldOptions?(options?: RequestCallOptions): Promise<AnyObject>;
33
33
 
34
- applyAction?(action: string | ResourceAction | ActionOptions, target: T | null, data?: object, options?: RequestCallOptions): Promise<AnyObject>;
34
+ applyAction?(action: string | ResourceAction | ActionOptions, target: T | null, data?: object, options?: RequestCallOptions): Promise<ApplyActionResponse>;
35
35
 
36
36
  batchAction?(action: string | ResourceAction | ActionOptions, targets: T[], data: object, options?: RequestCallOptions): Promise<AnyObject>;
37
37
 
@@ -123,5 +123,7 @@ export interface ListController<T = ActionTargetItem> {
123
123
  export interface ApplyActionResponse<T = ActionTargetItem> {
124
124
  item?: T;
125
125
  result?: T | AnyObject;
126
- success: boolean;
126
+ success?: boolean;
127
+ error?: boolean;
128
+ message?: string;
127
129
  }
@@ -12,5 +12,6 @@ export * from "./forms";
12
12
  export * from "./requests";
13
13
  export * from "./shared";
14
14
  export * from "./tables";
15
+ export * from "./widgets";
15
16
 
16
17
  export type DanxController<T = ActionTargetItem> = ActionController<T> & ListController<T>;
@@ -0,0 +1,5 @@
1
+ export interface LabelPillWidgetProps {
2
+ label?: string;
3
+ size?: "xs" | "sm" | "md" | "lg";
4
+ color?: "sky" | "green" | "red" | "amber" | "yellow" | "blue" | "slate" | "gray" | "none";
5
+ }