quasar-ui-danx 0.4.31 → 0.4.33

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.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