quasar-ui-danx 0.4.31 → 0.4.33

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.31",
3
+ "version": "0.4.33",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div class="inline-block relative">
3
3
  <div
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 min-w-10 min-h-10"
4
+ :contenteditable="readonly ? 'false' : 'true'"
5
+ class="relative inline-block transition duration-300 outline-none outline-offset-0 border-none rounded-sm z-10 min-w-10 min-h-10"
6
6
  :style="{minWidth, minHeight}"
7
7
  :class="contentClass"
8
8
  @input="onInput"
@@ -12,7 +12,7 @@
12
12
  {{ text }}
13
13
  </div>
14
14
  <div
15
- v-if="!text && placeholder && !hasFocus"
15
+ v-if="!text && placeholder && !hasFocus && !readonly"
16
16
  ref="placeholderDiv"
17
17
  class="text-gray-600 absolute-top-left whitespace-nowrap z-1 pointer-events-none"
18
18
  >
@@ -31,6 +31,7 @@ const props = withDefaults(defineProps<{
31
31
  color?: string;
32
32
  textColor?: string;
33
33
  debounceDelay?: number;
34
+ readonly?: boolean;
34
35
  placeholder?: string;
35
36
  }>(), {
36
37
  modelValue: "",
@@ -43,16 +44,16 @@ const props = withDefaults(defineProps<{
43
44
  });
44
45
 
45
46
  const text = ref(props.modelValue);
46
- const placeholderDiv = ref(null);
47
- const minWidth = ref(0);
48
- const minHeight = ref(0);
47
+ const placeholderDiv = ref<Element | null>(null);
48
+ const minWidth = ref<string>("0");
49
+ const minHeight = ref<string>("0");
49
50
  const hasFocus = ref(false);
50
51
 
51
52
  onMounted(() => {
52
53
  // Set the min-width to the width of the placeholder
53
54
  if (placeholderDiv.value) {
54
- minWidth.value = placeholderDiv.value.offsetWidth + "px";
55
- minHeight.value = placeholderDiv.value.offsetHeight + "px";
55
+ minWidth.value = placeholderDiv.value?.offsetWidth + "px";
56
+ minHeight.value = placeholderDiv.value?.offsetHeight + "px";
56
57
  }
57
58
  });
58
59
 
@@ -72,9 +73,12 @@ function onInput(e) {
72
73
  }
73
74
 
74
75
  const contentClass = computed(() => [
75
- `hover:bg-${props.color} focus:bg-${props.color}`,
76
- `hover:text-${props.textColor} focus:text-${props.textColor}`,
77
- `hover:outline-${props.color} focus:outline-${props.color}`,
76
+ ...(props.readonly ? [] : [
77
+ `hover:bg-${props.color} focus:bg-${props.color}`,
78
+ `hover:text-${props.textColor} focus:text-${props.textColor}`,
79
+ `hover:outline-${props.color} focus:outline-${props.color}`,
80
+ "focus:outline-4 hover:outline-4"
81
+ ]),
78
82
  text.value ? "" : "!bg-none"
79
83
  ]);
80
84
  </script>
@@ -22,6 +22,7 @@
22
22
  label=""
23
23
  :input-class="{'is-hidden': !isShowing, [inputClass]: true}"
24
24
  class="max-w-full dx-select-field"
25
+ :class="selectClass"
25
26
  @filter="onFilter"
26
27
  @clear="onClear"
27
28
  @popup-show="onShow"
@@ -82,6 +83,7 @@ export interface Props extends QSelectProps {
82
83
  selectionLabel?: string | ((option) => string);
83
84
  chipLimit?: number;
84
85
  inputClass?: string;
86
+ selectClass?: string;
85
87
  selectionClass?: string;
86
88
  options?: unknown[];
87
89
  filterable?: boolean;
@@ -97,6 +99,7 @@ const props = withDefaults(defineProps<Props>(), {
97
99
  selectionLabel: null,
98
100
  chipLimit: 3,
99
101
  inputClass: "",
102
+ selectClass: "",
100
103
  selectionClass: "",
101
104
  options: () => [],
102
105
  filterFn: null,
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="flex items-stretch flex-nowrap gap-x-4">
3
3
  <QBtn
4
- class="bg-green-900 px-4"
4
+ :class="createClass"
5
5
  :loading="loading"
6
6
  @click="$emit('create')"
7
7
  >
@@ -11,6 +11,14 @@
11
11
  />
12
12
  {{ createText }}
13
13
  </QBtn>
14
+ <ShowHideButton
15
+ v-if="showEdit"
16
+ v-model="editing"
17
+ :disable="!canEdit"
18
+ :label="editText"
19
+ :class="editClass"
20
+ :show-icon="EditIcon"
21
+ />
14
22
  <SelectField
15
23
  v-model="selected"
16
24
  class="flex-grow"
@@ -19,17 +27,10 @@
19
27
  :select-by-object="selectByObject"
20
28
  :option-label="optionLabel"
21
29
  />
22
- <ShowHideButton
23
- v-if="showEdit"
24
- v-model="editing"
25
- :disable="!canEdit"
26
- :label="editText"
27
- class="bg-sky-800 w-1/5"
28
- />
29
30
  </div>
30
31
  </template>
31
32
  <script setup lang="ts">
32
- import { FaSolidPlus as CreateIcon } from "danx-icon";
33
+ import { FaSolidPencil as EditIcon, FaSolidPlus as CreateIcon } from "danx-icon";
33
34
  import { QSelectOption } from "quasar";
34
35
  import { ActionTargetItem } from "../../../../types";
35
36
  import { ShowHideButton } from "../../../Utility/Buttons";
@@ -47,10 +48,14 @@ withDefaults(defineProps<{
47
48
  optionLabel?: string;
48
49
  createText?: string;
49
50
  editText?: string;
51
+ createClass?: string;
52
+ editClass?: string;
50
53
  clearable?: boolean;
51
54
  }>(), {
52
55
  optionLabel: "label",
53
- createText: "Create",
54
- editText: "Edit"
56
+ createText: "",
57
+ editText: "",
58
+ createClass: "bg-green-900 px-4",
59
+ editClass: "bg-sky-800 px-4"
55
60
  });
56
61
  </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,19 +1,23 @@
1
1
  <template>
2
2
  <div
3
- :class="{'cursor-move': !showHandle}"
4
- draggable="true"
5
- @dragstart.stop="dragAndDrop.dragStart"
6
- @dragend="dragAndDrop.dragEnd"
3
+ :class="{'cursor-move': !showHandle && !disabled}"
4
+ :draggable="disabled ? undefined : 'true'"
5
+ @dragstart.stop="disabled ? null : dragAndDrop.dragStart"
6
+ @dragend="disabled ? null : dragAndDrop.dragEnd"
7
7
  >
8
8
  <div :class="contentClass">
9
9
  <div
10
10
  v-if="showHandle"
11
- class="cursor-move"
12
- :class="handleClass"
11
+ :class="resolvedHandleClass"
13
12
  >
13
+ <div
14
+ v-if="disabled"
15
+ :class="handleSize"
16
+ />
14
17
  <SvgImg
18
+ v-else
15
19
  :svg="DragHandleIcon"
16
- class="w-4 h-4"
20
+ :class="handleSize"
17
21
  alt="drag-handle"
18
22
  />
19
23
  </div>
@@ -24,6 +28,7 @@
24
28
  </div>
25
29
  </template>
26
30
  <script setup lang="ts">
31
+ import { computed } from "vue";
27
32
  import { DragHandleDotsIcon as DragHandleIcon } from "../../svg";
28
33
  import { SvgImg } from "../Utility";
29
34
  import { ListDragAndDrop } from "./listDragAndDrop";
@@ -37,14 +42,21 @@ const props = withDefaults(defineProps<{
37
42
  changeDropZone?: boolean;
38
43
  contentClass?: string | object;
39
44
  handleClass?: string | object;
45
+ handleSize?: string;
40
46
  listItems?: any[];
47
+ disabled?: boolean;
41
48
  }>(), {
42
49
  direction: "vertical",
50
+ handleSize: "w-4 h-4",
43
51
  handleClass: "",
44
52
  contentClass: "flex flex-nowrap items-center",
45
53
  listItems: () => []
46
54
  });
47
55
 
56
+ const resolvedHandleClass = computed(() => ({
57
+ "cursor-move": !props.disabled,
58
+ ...(typeof props.handleClass === "string" ? { [props.handleClass]: true } : props.handleClass)
59
+ }));
48
60
  const dragAndDrop = new ListDragAndDrop()
49
61
  .setDropZone(props.dropZone)
50
62
  .setOptions({ showPlaceholder: true, allowDropZoneChange: props.changeDropZone, direction: props.direction })
@@ -1,7 +1,7 @@
1
1
  import { useDebounceFn } from "@vueuse/core";
2
2
  import { FaSolidCopy as CopyIcon, FaSolidPencil as EditIcon, FaSolidTrash as DeleteIcon } from "danx-icon";
3
3
  import { uid } from "quasar";
4
- import { h, isReactive, Ref, shallowRef } from "vue";
4
+ import { h, isReactive, Ref, shallowReactive, shallowRef } from "vue";
5
5
  import { ConfirmActionDialog, CreateNewWithNameDialog } from "../components";
6
6
  import type { ActionGlobalOptions, ActionOptions, ActionTarget, ListController, ResourceAction } from "../types";
7
7
  import { FlashMessages } from "./FlashMessages";
@@ -48,35 +48,39 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionGlobal
48
48
  */
49
49
  function getAction(actionName: string, actionOptions?: Partial<ActionOptions>): ResourceAction {
50
50
  /// Resolve the action options or resource action based on the provided input
51
- const baseOptions = actions.find(a => a.name === actionName) || { name: actionName };
51
+ let resourceAction: Partial<ResourceAction> = actions.find(a => a.name === actionName) || { name: actionName };
52
52
 
53
53
  if (actionOptions) {
54
- Object.assign(baseOptions, actionOptions);
54
+ Object.assign(resourceAction, actionOptions);
55
55
  }
56
56
 
57
57
  // If the action is already reactive, return it
58
- if (isReactive(baseOptions) && "__type" in baseOptions) return baseOptions as ResourceAction;
59
-
60
- const resourceAction: ResourceAction = storeObject({
61
- onAction: globalOptions?.routes?.applyAction,
62
- onBatchAction: globalOptions?.routes?.batchAction,
63
- onBatchSuccess: globalOptions?.controls?.clearSelectedRows,
64
- ...globalOptions,
65
- ...baseOptions,
66
- trigger: (target, input) => performAction(resourceAction, target, input),
67
- isApplying: false,
68
- __type: "__Action:" + namespace
69
- });
58
+ if (!isReactive(resourceAction) || !("__type" in resourceAction)) {
59
+ resourceAction = storeObject({
60
+ onAction: globalOptions?.routes?.applyAction,
61
+ onBatchAction: globalOptions?.routes?.batchAction,
62
+ onBatchSuccess: globalOptions?.controls?.clearSelectedRows,
63
+ ...globalOptions,
64
+ ...resourceAction,
65
+ isApplying: false,
66
+ __type: "__Action:" + namespace
67
+ });
70
68
 
71
- // Assign Trigger function if it doesn't exist
72
- if (baseOptions.debounce) {
73
- resourceAction.trigger = useDebounceFn((target, input) => performAction(resourceAction, target, input), baseOptions.debounce);
69
+ // Splice the resourceAction in place of the action in the actions list
70
+ actions.splice(actions.findIndex(a => a.name === actionName), 1, resourceAction as ResourceAction);
74
71
  }
75
72
 
76
- // Splice the resourceAction in place of the action in the actions list
77
- actions.splice(actions.findIndex(a => a.name === actionName), 1, resourceAction);
73
+ // Return a clone of the action so it can be modified without affecting the original
74
+ const clonedAction = shallowReactive({ ...resourceAction }) as ResourceAction;
78
75
 
79
- return resourceAction;
76
+ // Assign Trigger function if it doesn't exist
77
+ if (clonedAction.debounce) {
78
+ clonedAction.trigger = useDebounceFn((target, input) => performAction(clonedAction, target, input), clonedAction.debounce);
79
+ } else {
80
+ clonedAction.trigger = (target, input) => performAction(clonedAction, target, input);
81
+ }
82
+
83
+ return clonedAction;
80
84
  }
81
85
 
82
86
  /**
@@ -133,7 +137,19 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionGlobal
133
137
  activeActionVnode.value = {
134
138
  vnode,
135
139
  confirm: async (confirmInput: any) => {
136
- const result = await onConfirmAction(action, target, { ...input, ...confirmInput });
140
+
141
+ // Resolve the input based on the useInputFromConfirm option
142
+ // Not setting useInputFromConfirm will merge the input from the confirm dialog with the input from the action
143
+ let resolvedInput;
144
+ if (action.useInputFromConfirm === false) {
145
+ resolvedInput = input;
146
+ } else if (action.useInputFromConfirm === true) {
147
+ resolvedInput = confirmInput;
148
+ } else {
149
+ resolvedInput = { ...input, ...confirmInput };
150
+ }
151
+
152
+ const result = await onConfirmAction(action, target, resolvedInput);
137
153
 
138
154
  // Only resolve when we have a non-error response, so we can show the error message w/o
139
155
  // hiding the dialog / vnode
@@ -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,31 @@ 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
+
82
108
  /**
83
109
  * Auto refresh an object based on a condition and a callback. Returns the timeout ID for the auto-refresh.
84
110
  * NOTE: Use the timeout ID to clear the auto-refresh when the object is no longer needed (eg: when the component is unmounted)
@@ -101,9 +127,8 @@ export async function autoRefreshObject<T extends TypedObject>(object: T, condit
101
127
  storeObject(refreshedObject);
102
128
  }
103
129
 
104
-
130
+ // Save the timeoutId to the object so it can be cleared when the object refresh is no longer needed
105
131
  const timeoutId = setTimeout(() => autoRefreshObject(object, condition, callback, interval), interval);
106
-
107
132
  registeredAutoRefreshes[object.__type + ":" + object.id] = timeoutId;
108
133
  }
109
134
 
@@ -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
@@ -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;
@@ -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