quasar-ui-danx 0.4.37 → 0.4.38

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.37",
3
+ "version": "0.4.38",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -6,9 +6,8 @@
6
6
  @drop.prevent="onDrop"
7
7
  >
8
8
  <FieldLabel
9
- :field="field"
9
+ v-if="label"
10
10
  :label="label"
11
- :show-name="showName"
12
11
  />
13
12
 
14
13
  <input
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <div class="group flex items-center flex-nowrap gap-x-1 relative">
3
+ <QInnerLoading
4
+ v-if="loading"
5
+ showing
6
+ class="bg-sky-900 opacity-50 z-10 rounded"
7
+ color="teal"
8
+ />
9
+ <ShowHideButton
10
+ v-if="selectable"
11
+ v-model="isSelecting"
12
+ :disable="disable"
13
+ :label="selectText"
14
+ :class="selectClass"
15
+ :show-icon="selectIcon || DefaultSelectIcon"
16
+ class="mr-2"
17
+ >
18
+ <template #label>
19
+ <QMenu
20
+ :model-value="isSelecting"
21
+ @before-hide="isSelecting = false"
22
+ @hide="isSelecting = false"
23
+ >
24
+ <div>
25
+ <div
26
+ v-for="option in options"
27
+ :key="option.id"
28
+ v-ripple
29
+ class="cursor-pointer flex items-center relative"
30
+ :class="{'bg-sky-900 hover:bg-sky-800': selected?.id === option.id, 'hover:bg-slate-600': selected?.id !== option.id}"
31
+ @click="selected = option"
32
+ >
33
+ <div class="flex-grow px-4 py-2">
34
+ {{ option.name }}
35
+ </div>
36
+ <ActionButton
37
+ v-if="deletable"
38
+ type="trash"
39
+ class="ml-4 mr-2"
40
+ @click.stop.prevent="$emit('delete', option)"
41
+ />
42
+ </div>
43
+ <QSeparator class="bg-slate-400 my-2" />
44
+ <div class="px-4 mb-2">
45
+ <QBtn
46
+ :class="createClass"
47
+ :loading="loading"
48
+ @click="$emit('create')"
49
+ >
50
+ <CreateIcon
51
+ class="w-3"
52
+ :class="createText ? 'mr-2' : ''"
53
+ />
54
+ {{ createText }}
55
+ </QBtn>
56
+ </div>
57
+ </div>
58
+ </QMenu>
59
+ </template>
60
+ </ShowHideButton>
61
+
62
+ <div :class="labelClass">
63
+ <template v-if="selected">
64
+ <EditableDiv
65
+ v-if="nameEditable"
66
+ :model-value="selected.name"
67
+ color="slate-600"
68
+ @update:model-value="name => $emit('update', {name})"
69
+ />
70
+ <template v-else>
71
+ {{ selected.name }}
72
+ </template>
73
+ </template>
74
+ <template v-else>
75
+ <slot name="no-selection">
76
+ (No selection)
77
+ </slot>
78
+ </template>
79
+ </div>
80
+
81
+
82
+ <ShowHideButton
83
+ v-if="editable && selected"
84
+ v-model="editing"
85
+ :label="editText"
86
+ :class="editClass"
87
+ class="ml-1"
88
+ :show-icon="EditIcon"
89
+ :hide-icon="DoneEditingIcon"
90
+ />
91
+
92
+ <QBtn
93
+ v-if="clearable && selected"
94
+ :label="clearText"
95
+ :class="clearClass"
96
+ class="opacity-0 group-hover:opacity-100 transition-all"
97
+ @click="selected = null"
98
+ >
99
+ <ClearIcon class="w-4" />
100
+ </QBtn>
101
+ </div>
102
+ </template>
103
+ <script setup lang="ts">
104
+ import {
105
+ FaSolidCheck as DoneEditingIcon,
106
+ FaSolidCircleXmark as ClearIcon,
107
+ FaSolidListCheck as DefaultSelectIcon,
108
+ FaSolidPencil as EditIcon,
109
+ FaSolidPlus as CreateIcon
110
+ } from "danx-icon";
111
+ import { ref } from "vue";
112
+ import { ActionTargetItem } from "../../../../types";
113
+ import { ShowHideButton } from "../../../Utility/Buttons";
114
+ import ActionButton from "../../../Utility/Buttons/ActionButton";
115
+ import EditableDiv from "./EditableDiv";
116
+
117
+ defineEmits(["create", "update", "delete"]);
118
+ const selected = defineModel<ActionTargetItem | null>("selected");
119
+ const editing = defineModel<boolean>("editing");
120
+ withDefaults(defineProps<{
121
+ options: ActionTargetItem[];
122
+ showEdit?: boolean;
123
+ loading?: boolean;
124
+ selectText?: string;
125
+ createText?: string;
126
+ editText?: string;
127
+ clearText?: string;
128
+ selectClass?: string;
129
+ createClass?: string;
130
+ editClass?: string;
131
+ clearClass?: string;
132
+ labelClass?: string;
133
+ selectIcon?: object | string;
134
+ selectable?: boolean;
135
+ creatable?: boolean;
136
+ editable?: boolean;
137
+ deletable?: boolean;
138
+ nameEditable?: boolean;
139
+ clearable?: boolean;
140
+ disable?: boolean;
141
+ }>(), {
142
+ selectText: "",
143
+ createText: "",
144
+ editText: "",
145
+ clearText: "",
146
+ selectClass: "bg-sky-800",
147
+ createClass: "bg-green-900",
148
+ editClass: "",
149
+ clearClass: "rounded-full",
150
+ labelClass: "text-slate-600",
151
+ selectIcon: null
152
+ });
153
+
154
+ const isSelecting = ref(false);
155
+ </script>
@@ -30,6 +30,7 @@
30
30
  :model-value="modelValue"
31
31
  :maxlength="allowOverMax ? undefined : maxLength"
32
32
  :debounce="debounce"
33
+ :loading="loading"
33
34
  @keydown.enter="$emit('submit')"
34
35
  @update:model-value="$emit('update:model-value', $event)"
35
36
  >
@@ -19,6 +19,7 @@ export { default as NumberField } from "./NumberField.vue";
19
19
  export { default as NumberRangeField } from "./NumberRangeField.vue";
20
20
  export { default as SelectDrawer } from "./SelectDrawer.vue";
21
21
  export { default as SelectField } from "./SelectField.vue";
22
+ export { default as SelectionMenuField } from "./SelectionMenuField.vue";
22
23
  export { default as SelectOrCreateField } from "./SelectOrCreateField.vue";
23
24
  export { default as SelectWithChildrenField } from "./SelectWithChildrenField.vue";
24
25
  export { default as SingleFileField } from "./SingleFileField.vue";
@@ -0,0 +1,163 @@
1
+ <template>
2
+ <QBtn
3
+ :loading="isSaving"
4
+ class="shadow-none"
5
+ :class="colorClass"
6
+ @click="onAction"
7
+ >
8
+ <div class="flex items-center flex-nowrap">
9
+ <component
10
+ :is="icon || typeOptions.icon"
11
+ class="transition-all"
12
+ :class="iconClass + ' ' + typeOptions.iconClass"
13
+ />
14
+ <slot>
15
+ <div
16
+ v-if="label"
17
+ class="ml-2"
18
+ >
19
+ {{ label }}
20
+ </div>
21
+ </slot>
22
+ </div>
23
+ <QTooltip
24
+ v-if="tooltip"
25
+ class="whitespace-nowrap"
26
+ >
27
+ {{ tooltip }}
28
+ </QTooltip>
29
+ </QBtn>
30
+ </template>
31
+ <script setup lang="ts">
32
+ import {
33
+ FaSolidArrowsRotate as RefreshIcon,
34
+ FaSolidPause as PauseIcon,
35
+ FaSolidPencil as EditIcon,
36
+ FaSolidPlay as PlayIcon,
37
+ FaSolidPlus as CreateIcon,
38
+ FaSolidStop as StopIcon,
39
+ FaSolidTrash as TrashIcon
40
+ } from "danx-icon";
41
+ import { computed } from "vue";
42
+ import { ActionTarget, ResourceAction } from "../../../types";
43
+
44
+ export interface ActionButtonProps {
45
+ type?: "trash" | "trash-red" | "create" | "edit" | "play" | "stop" | "pause" | "refresh";
46
+ color?: "red" | "blue" | "sky" | "green" | "green-invert" | "lime" | "white" | "gray";
47
+ icon?: object | string;
48
+ iconClass?: string;
49
+ label?: string;
50
+ tooltip?: string;
51
+ saving?: boolean;
52
+ action?: ResourceAction;
53
+ target?: ActionTarget;
54
+ input?: object;
55
+ }
56
+
57
+ const emit = defineEmits(["success", "error", "always"]);
58
+ const props = withDefaults(defineProps<ActionButtonProps>(), {
59
+ type: null,
60
+ color: null,
61
+ icon: null,
62
+ iconClass: "",
63
+ label: "",
64
+ tooltip: "",
65
+ action: null,
66
+ target: null,
67
+ input: null
68
+ });
69
+
70
+ const colorClass = computed(() => {
71
+ switch (props.color) {
72
+ case "red":
73
+ return "text-red-900 bg-red-300 hover:bg-red-400";
74
+ case "lime":
75
+ return "text-lime-900 bg-lime-300 hover:bg-lime-400";
76
+ case "green":
77
+ return "text-green-900 bg-green-300 hover:bg-green-400";
78
+ case "green-invert":
79
+ return "text-lime-800 bg-green-200 hover:bg-lime-800 hover:text-green-200";
80
+ case "blue":
81
+ return "text-blue-900 bg-blue-300 hover:bg-blue-400";
82
+ case "sky":
83
+ return "text-sky-900 bg-sky-300 hover:bg-sky-400";
84
+ case "white":
85
+ return "text-white bg-gray-800 hover:bg-gray-200";
86
+ case "gray":
87
+ return "text-slate-200 bg-slate-800 hover:bg-slate-900";
88
+ default:
89
+ return "text-slate-200 hover:bg-slate-800";
90
+ }
91
+ });
92
+ const typeOptions = computed(() => {
93
+ switch (props.type) {
94
+ case "trash":
95
+ return {
96
+ icon: TrashIcon,
97
+ iconClass: "w-3"
98
+ };
99
+ case "create":
100
+ return {
101
+ icon: CreateIcon,
102
+ iconClass: "w-3"
103
+ };
104
+ case "edit":
105
+ return {
106
+ icon: EditIcon,
107
+ iconClass: "w-3"
108
+ };
109
+ case "play":
110
+ return {
111
+ icon: PlayIcon,
112
+ iconClass: "w-3"
113
+ };
114
+ case "stop":
115
+ return {
116
+ icon: StopIcon,
117
+ iconClass: "w-3"
118
+ };
119
+ case "pause":
120
+ return {
121
+ icon: PauseIcon,
122
+ iconClass: "w-3"
123
+ };
124
+ case "refresh":
125
+ return {
126
+ icon: RefreshIcon,
127
+ iconClass: "w-4"
128
+ };
129
+ default:
130
+ return {
131
+ icon: EditIcon,
132
+ iconClass: "w-3"
133
+ };
134
+ }
135
+ });
136
+
137
+ const isSaving = computed(() => {
138
+ if (props.saving) return true;
139
+ if (props.target) {
140
+ if (Array.isArray(props.target)) {
141
+ return props.target.some((t) => t.isSaving);
142
+ }
143
+ return props.target.isSaving;
144
+ }
145
+ if (props.action) {
146
+ return props.action.isApplying;
147
+ }
148
+ return false;
149
+ });
150
+
151
+ function onAction() {
152
+ if (props.action) {
153
+ props.action.trigger(props.target, props.input).then(async (response) => {
154
+ emit("success", typeof response.json === "function" ? await response.json() : response);
155
+ }).catch((e) => {
156
+ console.error(`Action emitted an error: ${props.action.name}`, e, props.target);
157
+ emit("error", e);
158
+ }).finally(() => {
159
+ emit("always");
160
+ });
161
+ }
162
+ }
163
+ </script>
@@ -10,6 +10,11 @@
10
10
  :is="isShowing ? (hideIcon || DefaultHideIcon) : (showIcon || DefaultShowIcon)"
11
11
  :class="iconClass"
12
12
  />
13
+ </slot>
14
+ <slot
15
+ name="label"
16
+ :is-showing="isShowing"
17
+ >
13
18
  <div
14
19
  v-if="label"
15
20
  :class="labelClass"
@@ -4,6 +4,7 @@
4
4
  v-bind="$props"
5
5
  @close="onClose"
6
6
  >
7
+ <slot />
7
8
  <template
8
9
  v-if="$slots.title"
9
10
  #title
@@ -308,6 +308,10 @@ async function onConfirmAction(action: ActionOptions, target: ActionTarget, inpu
308
308
 
309
309
  export function withDefaultActions(label: string, listController?: ListController): ActionOptions[] {
310
310
  return [
311
+ {
312
+ name: "quick-create",
313
+ alias: "create"
314
+ },
311
315
  {
312
316
  name: "create",
313
317
  label: "Create " + label,
@@ -180,16 +180,27 @@ export function fSecondsToTime(second: number) {
180
180
  return (hours ? hours + ":" : "") + time.toFormat("mm:ss");
181
181
  }
182
182
 
183
- export function fElapsedTime(start: string, end?: string) {
183
+ /**
184
+ * Formats a number of seconds into a duration string in 00h 00m 00s format
185
+ */
186
+ export function fSecondsToDuration(seconds: number) {
187
+ const hours = Math.floor(seconds / 3600);
188
+ const minutes = Math.floor((seconds % 3600) / 60);
189
+ const secs = Math.floor(seconds % 60);
190
+ return `${hours ? hours + "h " : ""}${minutes ? minutes + "m " : ""}${secs}s`;
191
+ }
192
+
193
+ /**
194
+ * Formats a duration between two date strings in 00h 00m 00s format
195
+ */
196
+ export function fDuration(start: string, end?: string) {
184
197
  const endDateTime = end ? parseDateTime(end) : DateTime.now();
185
198
  const diff = endDateTime?.diff(parseDateTime(start) || DateTime.now(), ["hours", "minutes", "seconds"]);
186
199
  if (!diff?.isValid) {
187
200
  return "-";
188
201
  }
189
- const hours = Math.floor(diff.hours);
190
- const minutes = Math.floor(diff.minutes);
191
- const seconds = Math.floor(diff.seconds);
192
- return `${hours ? hours + "h " : ""}${minutes ? minutes + "m " : ""}${seconds}s`;
202
+ const totalSeconds = diff.as("seconds");
203
+ return fSecondsToDuration(totalSeconds);
193
204
  }
194
205
 
195
206
  /**
@@ -5,27 +5,31 @@ import { request } from "./request";
5
5
 
6
6
  export function useActionRoutes(baseUrl: string, extend?: object): ListControlsRoutes {
7
7
  return {
8
- list(pager?) {
9
- return request.post(`${baseUrl}/list`, pager);
8
+ list(pager?, options?) {
9
+ return request.post(`${baseUrl}/list`, pager, options);
10
10
  },
11
- summary(filter) {
12
- return request.post(`${baseUrl}/summary`, { filter });
11
+ summary(filter, options?) {
12
+ return request.post(`${baseUrl}/summary`, { filter }, options);
13
13
  },
14
- details(target, fields) {
15
- return request.get(`${baseUrl}/${target.id}/details`, { params: { fields } });
14
+ details(target, fields, options?) {
15
+ options = options || {};
16
+ fields && (options.params = { fields });
17
+ return request.get(`${baseUrl}/${target.id}/details`, options);
16
18
  },
17
- async detailsAndStore(target, fields) {
18
- const item = await request.get(`${baseUrl}/${target.id}/details`, { params: { fields } });
19
+ async detailsAndStore(target, fields, options?) {
20
+ options = options || {};
21
+ fields && (options.params = { fields });
22
+ const item = await request.get(`${baseUrl}/${target.id}/details`, options);
19
23
  return storeObject(item);
20
24
  },
21
- fieldOptions() {
22
- return request.get(`${baseUrl}/field-options`);
25
+ fieldOptions(options?) {
26
+ return request.get(`${baseUrl}/field-options`, options);
23
27
  },
24
- applyAction(action, target, data) {
25
- return request.post(`${baseUrl}/${target ? target.id : "new"}/apply-action`, { action, data });
28
+ applyAction(action, target, data, options?) {
29
+ return request.post(`${baseUrl}/${target ? target.id : "new"}/apply-action`, { action, data }, options);
26
30
  },
27
- batchAction(action, targets, data) {
28
- return request.post(`${baseUrl}/batch-action`, { action, filter: { id: targets.map(r => r.id) }, data });
31
+ batchAction(action, targets, data, options?) {
32
+ return request.post(`${baseUrl}/batch-action`, { action, filter: { id: targets.map(r => r.id) }, data }, options);
29
33
  },
30
34
  export(filter, name) {
31
35
  return downloadFile(`${baseUrl}/export`, name || "export.csv", { filter });
@@ -1,3 +1,4 @@
1
+ import { RequestCallOptions } from "src/types/requests";
1
2
  import { ActionOptions, ActionTargetItem, ResourceAction } from "./actions";
2
3
  import { AnyObject, ComputedRef, LabelValueItem, Ref } from "./shared";
3
4
 
@@ -20,21 +21,21 @@ export interface FilterGroup {
20
21
  }
21
22
 
22
23
  export interface ListControlsRoutes<T = ActionTargetItem> {
23
- list(pager?: ListControlsPagination): Promise<PagedItems>;
24
+ list(pager?: ListControlsPagination, options?: RequestCallOptions): Promise<PagedItems>;
24
25
 
25
- summary?(filter?: ListControlsFilter): Promise<AnyObject>;
26
+ summary?(filter?: ListControlsFilter, options?: RequestCallOptions): Promise<AnyObject>;
26
27
 
27
- details?(target: T, fields: ControlsFieldsList): Promise<T>;
28
+ details?(target: T, fields?: ControlsFieldsList, options?: RequestCallOptions): Promise<T>;
28
29
 
29
- detailsAndStore?(target: T, fields: ControlsFieldsList): Promise<T>;
30
+ detailsAndStore?(target: T, fields?: ControlsFieldsList, options?: RequestCallOptions): Promise<T>;
30
31
 
31
- more?(pager: ListControlsPagination): Promise<T[]>;
32
+ more?(pager: ListControlsPagination, options?: RequestCallOptions): Promise<T[]>;
32
33
 
33
- fieldOptions?(filter?: AnyObject): Promise<AnyObject>;
34
+ fieldOptions?(filter?: AnyObject, options?: RequestCallOptions): Promise<AnyObject>;
34
35
 
35
- applyAction?(action: string | ResourceAction | ActionOptions, target: T | null, data?: object): Promise<AnyObject>;
36
+ applyAction?(action: string | ResourceAction | ActionOptions, target: T | null, data?: object, options?: RequestCallOptions): Promise<AnyObject>;
36
37
 
37
- batchAction?(action: string | ResourceAction | ActionOptions, targets: T[], data: object): Promise<AnyObject>;
38
+ batchAction?(action: string | ResourceAction | ActionOptions, targets: T[], data: object, options?: RequestCallOptions): Promise<AnyObject>;
38
39
 
39
40
  export?(filter?: ListControlsFilter, name?: string): Promise<void>;
40
41
  }
@@ -19,6 +19,7 @@ export interface TextFieldProps {
19
19
  disabled?: boolean;
20
20
  readonly?: boolean;
21
21
  debounce?: string | number;
22
+ loading?: boolean;
22
23
  }
23
24
 
24
25
  export interface NumberFieldProps extends TextFieldProps {