quasar-ui-danx 0.4.27 → 0.4.29

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.
Files changed (51) hide show
  1. package/README.md +35 -35
  2. package/dist/danx.es.js +24686 -24119
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +109 -109
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +1 -1
  8. package/src/components/ActionTable/ActionTable.vue +29 -7
  9. package/src/components/ActionTable/Filters/FilterableField.vue +14 -2
  10. package/src/components/ActionTable/Form/ActionForm.vue +17 -12
  11. package/src/components/ActionTable/Form/Fields/DateField.vue +24 -20
  12. package/src/components/ActionTable/Form/Fields/DateRangeField.vue +57 -53
  13. package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +9 -2
  14. package/src/components/ActionTable/Form/Fields/EditableDiv.vue +51 -21
  15. package/src/components/ActionTable/Form/Fields/FieldLabel.vue +1 -1
  16. package/src/components/ActionTable/Form/Fields/SelectField.vue +27 -6
  17. package/src/components/ActionTable/Form/Fields/SelectOrCreateField.vue +56 -0
  18. package/src/components/ActionTable/Form/Fields/TextField.vue +2 -0
  19. package/src/components/ActionTable/Form/Fields/index.ts +1 -0
  20. package/src/components/ActionTable/Form/RenderedForm.vue +7 -20
  21. package/src/components/ActionTable/Form/Utilities/MaxLengthCounter.vue +1 -1
  22. package/src/components/ActionTable/Form/Utilities/SaveStateIndicator.vue +37 -0
  23. package/src/components/ActionTable/Form/Utilities/index.ts +1 -0
  24. package/src/components/ActionTable/Layouts/ActionTableLayout.vue +20 -23
  25. package/src/components/ActionTable/Toolbars/ActionToolbar.vue +44 -36
  26. package/src/components/ActionTable/{listControls.ts → controls.ts} +13 -9
  27. package/src/components/ActionTable/index.ts +1 -1
  28. package/src/components/DragAndDrop/ListItemDraggable.vue +45 -31
  29. package/src/components/DragAndDrop/dragAndDrop.ts +221 -220
  30. package/src/components/DragAndDrop/listDragAndDrop.ts +269 -227
  31. package/src/components/PanelsDrawer/PanelsDrawer.vue +7 -7
  32. package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +3 -3
  33. package/src/components/Utility/Buttons/ShowHideButton.vue +86 -0
  34. package/src/components/Utility/Buttons/index.ts +1 -0
  35. package/src/components/Utility/Dialogs/ActionFormDialog.vue +30 -0
  36. package/src/components/Utility/Dialogs/CreateNewWithNameDialog.vue +26 -0
  37. package/src/components/Utility/Dialogs/RenderedFormDialog.vue +50 -0
  38. package/src/components/Utility/Dialogs/index.ts +3 -0
  39. package/src/helpers/FileUpload.ts +4 -4
  40. package/src/helpers/actions.ts +84 -20
  41. package/src/helpers/files.ts +56 -43
  42. package/src/helpers/formats.ts +23 -20
  43. package/src/helpers/objectStore.ts +24 -12
  44. package/src/types/actions.d.ts +50 -26
  45. package/src/types/controls.d.ts +23 -25
  46. package/src/types/fields.d.ts +1 -0
  47. package/src/types/files.d.ts +2 -2
  48. package/src/types/index.d.ts +5 -0
  49. package/src/types/shared.d.ts +9 -0
  50. package/src/types/tables.d.ts +3 -3
  51. package/types/vue-shims.d.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -3,7 +3,6 @@
3
3
  class="dx-action-table overflow-hidden"
4
4
  :class="{'dx-no-data': !hasData, 'dx-is-loading': loadingList || loadingSummary, 'dx-is-loading-list': loadingList}"
5
5
  >
6
- <ActionVnode />
7
6
  <QTable
8
7
  ref="actionTable"
9
8
  :selected="selectedRows"
@@ -67,8 +66,7 @@
67
66
  import { QTable } from "quasar";
68
67
  import { computed, ref } from "vue";
69
68
  import { getItem, setItem } from "../../helpers";
70
- import { ActionTargetItem, ListControlsPagination, TableColumn } from "../../types";
71
- import { ActionVnode } from "../Utility";
69
+ import { ActionTargetItem, ListControlsPagination, ResourceAction, TableColumn } from "../../types";
72
70
  import { ActionTableColumn, ActionTableHeaderColumn } from "./Columns";
73
71
  import EmptyTableState from "./EmptyTableState.vue";
74
72
  import { mapSortBy, registerStickyScrolling } from "./listHelpers";
@@ -86,6 +84,7 @@ export interface Props {
86
84
  loadingSummary?: boolean;
87
85
  pagedItems?: any;
88
86
  summary: any;
87
+ menuActions?: ResourceAction[];
89
88
  columns: TableColumn[];
90
89
  rowsPerPageOptions?: number[];
91
90
  summaryColSpan?: number;
@@ -105,10 +104,33 @@ const props = withDefaults(defineProps<Props>(), {
105
104
  const actionTable = ref(null);
106
105
  registerStickyScrolling(actionTable);
107
106
 
108
- const tableColumns = computed<TableColumn[]>(() => props.columns.map((column) => ({
109
- ...column,
110
- field: column.field || column.name
111
- })));
107
+ const tableColumns = computed<TableColumn[]>(() => {
108
+ const columns = [...props.columns].map((column: TableColumn) => ({
109
+ ...column,
110
+ field: column.field || column.name
111
+ }));
112
+
113
+ // Inject the Action Menu column if there are menu actions
114
+ if (props.menuActions?.length) {
115
+ const menuColumn = columns.find((column) => column.name === "menu");
116
+ const menuColumnOptions: TableColumn = {
117
+ name: "menu",
118
+ label: "",
119
+ required: true,
120
+ hideContent: true,
121
+ shrink: true,
122
+ actionMenu: props.menuActions
123
+ };
124
+
125
+ if (menuColumn) {
126
+ Object.assign(menuColumn, menuColumnOptions);
127
+ } else {
128
+ columns.unshift(menuColumnOptions);
129
+ }
130
+ }
131
+
132
+ return columns;
133
+ });
112
134
  const hasData = computed(() => props.pagedItems?.data?.length);
113
135
  const COLUMN_SETTINGS_KEY = `column-settings-${props.name}`;
114
136
  const columnSettings = ref(getItem(COLUMN_SETTINGS_KEY) || {});
@@ -1,6 +1,15 @@
1
1
  <template>
2
2
  <div>
3
- <template v-if="field.type === 'multi-select'">
3
+ <template v-if="field.type === 'text'">
4
+ <TextField
5
+ :model-value="modelValue"
6
+ :label="field.label"
7
+ :placeholder="field.placeholder"
8
+ :debounce="1000"
9
+ @update:model-value="onUpdate"
10
+ />
11
+ </template>
12
+ <template v-else-if="field.type === 'multi-select'">
4
13
  <SelectField
5
14
  v-if="field.options?.length > 0 || loading"
6
15
  :model-value="modelValue"
@@ -41,6 +50,7 @@
41
50
  v-else-if="field.type === 'date'"
42
51
  :model-value="modelValue"
43
52
  :label="field.label"
53
+ :clearable="field.clearable === undefined ? true : field.clearable"
44
54
  class="mt-2"
45
55
  @update:model-value="onUpdate"
46
56
  />
@@ -49,6 +59,7 @@
49
59
  :model-value="modelValue"
50
60
  :label="field.label"
51
61
  :inline="!!field.inline"
62
+ :clearable="field.clearable === undefined ? true : field.clearable"
52
63
  with-time
53
64
  class="mt-2 reactive"
54
65
  @update:model-value="onUpdate"
@@ -117,7 +128,8 @@ import {
117
128
  MultiKeywordField,
118
129
  NumberRangeField,
119
130
  SelectField,
120
- SelectWithChildrenField
131
+ SelectWithChildrenField,
132
+ TextField
121
133
  } from "../Form/Fields";
122
134
 
123
135
  const emit = defineEmits(["update:model-value"]);
@@ -1,16 +1,14 @@
1
1
  <template>
2
- <div>
3
- <RenderedForm
4
- v-bind="renderedFormProps"
5
- v-model:values="input"
6
- empty-value=""
7
- :saved-at="target.updated_at"
8
- :saving="action.isApplying"
9
- @update:values="action.trigger(target, input)"
10
- >
11
- <slot />
12
- </RenderedForm>
13
- </div>
2
+ <RenderedForm
3
+ v-bind="renderedFormProps"
4
+ v-model:values="input"
5
+ empty-value=""
6
+ :saved-at="hideSavedAt ? undefined : target.updated_at"
7
+ :saving="action.isApplying"
8
+ @update:values="onUpdate"
9
+ >
10
+ <slot />
11
+ </RenderedForm>
14
12
  </template>
15
13
  <script setup lang="ts">
16
14
  import { Ref, ref, watch } from "vue";
@@ -28,8 +26,10 @@ interface ActionFormProps {
28
26
  clearable?: boolean;
29
27
  fieldClass?: string;
30
28
  savingClass?: string;
29
+ hideSavedAt?: boolean;
31
30
  }
32
31
 
32
+ const emit = defineEmits(["saved"]);
33
33
  const props = withDefaults(defineProps<ActionFormProps>(), {
34
34
  fieldClass: "",
35
35
  savingClass: undefined
@@ -58,4 +58,9 @@ watch(() => props.target, (target: ActionTargetItem) => {
58
58
  }
59
59
  }
60
60
  });
61
+
62
+ async function onUpdate() {
63
+ await props.action.trigger(props.target, input.value);
64
+ emit("saved");
65
+ }
61
66
  </script>
@@ -7,8 +7,8 @@
7
7
  {{ label }}
8
8
  </div>
9
9
  <div class="flex items-center cursor-pointer">
10
- <DateIcon class="w-5 text-blue-600" />
11
- <div class="font-bold ml-3 hover:text-blue-600">
10
+ <DateIcon class="w-5" />
11
+ <div class="flex-grow font-bold ml-3 hover:opacity-70">
12
12
  <template v-if="date">
13
13
  {{ formattedDate }}
14
14
  </template>
@@ -16,6 +16,15 @@
16
16
  -&nbsp;-
17
17
  </template>
18
18
  </div>
19
+ <div v-if="clearable && date">
20
+ <QBtn
21
+ icon="close"
22
+ size="sm"
23
+ round
24
+ flat
25
+ @click.stop.prevent="(date = null) || onSave()"
26
+ />
27
+ </div>
19
28
  </div>
20
29
  <QPopupProxy>
21
30
  <QDate
@@ -26,34 +35,29 @@
26
35
  </div>
27
36
  </template>
28
37
 
29
- <script setup>
38
+ <script setup lang="ts">
30
39
  import { CalendarIcon as DateIcon } from "@heroicons/vue/outline";
31
40
  import { computed, ref, watch } from "vue";
32
- import { fDate, parseQDate } from "../../../../helpers";
41
+ import { fDate, parseDateTime } from "../../../../helpers";
33
42
 
34
43
  const emit = defineEmits(["update:model-value"]);
35
- const props = defineProps({
36
- modelValue: {
37
- type: [String, Object],
38
- default: null
39
- },
40
- label: {
41
- type: String,
42
- default: null
43
- }
44
- });
44
+ const props = defineProps<{
45
+ modelValue?: string | null;
46
+ label: string | null;
47
+ clearable: boolean;
48
+ }>();
45
49
 
46
50
  const formattedDate = computed(() => {
47
- if (props.modelValue) {
48
- return fDate(parseQDate(props.modelValue || "0000-00-00"));
49
- }
50
- return null;
51
+ if (props.modelValue) {
52
+ return fDate(parseDateTime(props.modelValue || "0000-00-00"));
53
+ }
54
+ return "- -";
51
55
  });
52
56
 
53
- const date = ref(props.modelValue);
57
+ const date = ref(parseDateTime(props.modelValue));
54
58
  watch(() => props.modelValue, val => date.value = val);
55
59
 
56
60
  function onSave() {
57
- emit("update:model-value", date.value);
61
+ emit("update:model-value", date.value);
58
62
  }
59
63
  </script>
@@ -13,8 +13,8 @@
13
13
  </template>
14
14
  <template v-else>
15
15
  <div class="flex items-center cursor-pointer">
16
- <DateIcon class="w-5 text-blue-600" />
17
- <div class="font-bold ml-3 hover:text-blue-600">
16
+ <DateIcon class="w-5 py-2" />
17
+ <div class="flex-grow font-bold ml-3 hover:opacity-70">
18
18
  <template v-if="dateRangeValue">
19
19
  {{ formattedDates.from }} - {{ formattedDates.to }}
20
20
  </template>
@@ -22,6 +22,15 @@
22
22
  -&nbsp;-
23
23
  </template>
24
24
  </div>
25
+ <div v-if="clearable && dateRange">
26
+ <QBtn
27
+ icon="close"
28
+ size="sm"
29
+ round
30
+ flat
31
+ @click.stop.prevent="(dateRange = null) || onSave()"
32
+ />
33
+ </div>
25
34
  </div>
26
35
  <QPopupProxy>
27
36
  <QDate
@@ -34,80 +43,75 @@
34
43
  </div>
35
44
  </template>
36
45
 
37
- <script setup>
46
+ <script setup lang="ts">
38
47
  import { CalendarIcon as DateIcon } from "@heroicons/vue/outline";
39
48
  import { computed, ref, watch } from "vue";
40
49
  import { fDate, parseQDate, parseQDateTime } from "../../../../helpers";
41
50
  import FieldLabel from "./FieldLabel";
42
51
 
43
52
  const emit = defineEmits(["update:model-value"]);
44
- const props = defineProps({
45
- modelValue: {
46
- type: Object,
47
- default: null
48
- },
49
- label: {
50
- type: String,
51
- default: null
52
- },
53
- inline: Boolean,
54
- withTime: Boolean
55
- });
53
+ const props = defineProps<{
54
+ modelValue?: { from: string; to: string } | null;
55
+ label: string | null;
56
+ inline: boolean;
57
+ clearable: boolean;
58
+ withTime: boolean;
59
+ }>();
56
60
 
57
61
  const formattedDates = computed(() => {
58
- if (dateRangeValue.value) {
59
- if (props.withTime) {
60
- return {
61
- from: fDate(parseQDateTime(dateRangeValue.value.from || "0000-00-00")),
62
- to: fDate(parseQDateTime(dateRangeValue.value.to || "9999-12-31"))
63
- };
64
- }
62
+ if (dateRangeValue.value) {
63
+ if (props.withTime) {
64
+ return {
65
+ from: fDate(parseQDateTime(dateRangeValue.value.from || "0000-00-00")),
66
+ to: fDate(parseQDateTime(dateRangeValue.value.to || "9999-12-31"))
67
+ };
68
+ }
65
69
 
66
- return {
67
- from: fDate(parseQDate(dateRangeValue.value.from || "0000-00-00")),
68
- to: fDate(parseQDate(dateRangeValue.value.to || "9999-12-31"))
69
- };
70
- }
71
- return {
72
- from: null,
73
- to: null
74
- };
70
+ return {
71
+ from: fDate(parseQDate(dateRangeValue.value.from || "0000-00-00")),
72
+ to: fDate(parseQDate(dateRangeValue.value.to || "9999-12-31"))
73
+ };
74
+ }
75
+ return {
76
+ from: null,
77
+ to: null
78
+ };
75
79
  });
76
80
 
77
81
  const dateRange = ref(toQDateValue(props.modelValue));
78
82
  watch(() => props.modelValue, val => dateRange.value = toQDateValue(val));
79
83
 
80
84
  function toQDateValue(val) {
81
- if (val?.from && val?.to) {
82
- return fDate(val.from) === fDate(val.to) ? val.from : val;
83
- }
84
- return null;
85
+ if (val?.from && val?.to) {
86
+ return fDate(val.from) === fDate(val.to) ? val.from : val;
87
+ }
88
+ return null;
85
89
  }
86
90
 
87
91
  const dateRangeValue = computed(() => {
88
- let range;
92
+ let range;
89
93
 
90
- if (typeof dateRange.value === "string") {
91
- range = {
92
- from: dateRange.value,
93
- to: dateRange.value
94
- };
95
- } else if (dateRange.value) {
96
- range = {
97
- from: dateRange.value.from,
98
- to: dateRange.value.to
99
- };
100
- }
94
+ if (typeof dateRange.value === "string") {
95
+ range = {
96
+ from: dateRange.value,
97
+ to: dateRange.value
98
+ };
99
+ } else if (dateRange.value) {
100
+ range = {
101
+ from: dateRange.value.from,
102
+ to: dateRange.value.to
103
+ };
104
+ }
101
105
 
102
- if (range?.from && range?.to && props.withTime && !range.from.includes("00:00:00")) {
103
- range.from += " 00:00:00";
104
- range.to += " 23:59:59";
105
- }
106
+ if (range?.from && range?.to && props.withTime && !range.from.includes("00:00:00")) {
107
+ range.from += " 00:00:00";
108
+ range.to += " 23:59:59";
109
+ }
106
110
 
107
- return range;
111
+ return range;
108
112
  });
109
113
 
110
114
  function onSave() {
111
- emit("update:model-value", dateRangeValue.value);
115
+ emit("update:model-value", dateRangeValue.value);
112
116
  }
113
117
  </script>
@@ -10,9 +10,9 @@
10
10
  :contenteditable="!readonly && isEditing"
11
11
  class="flex-grow p-2 rounded outline-none border-none"
12
12
  :class="{[editingClass]: isEditing, [inputClass]: true}"
13
- @input="text = $event.target.innerText"
13
+ @input="onUpdate($event.target.innerText)"
14
14
  >
15
- {{ text }}
15
+ {{ isEditing ? editingText : text }}
16
16
  </div>
17
17
  <div v-if="!readonly">
18
18
  <QBtn
@@ -43,6 +43,7 @@ export interface EditOnClickTextFieldProps {
43
43
 
44
44
  const editableBox = ref<HTMLElement | null>(null);
45
45
  const text = defineModel({ type: String });
46
+ const editingText = ref(text.value);
46
47
  const props = withDefaults(defineProps<EditOnClickTextFieldProps>(), {
47
48
  class: "hover:bg-slate-300",
48
49
  editingClass: "bg-slate-500",
@@ -52,11 +53,17 @@ const isEditing = ref(false);
52
53
  function onEdit() {
53
54
  if (props.readonly) return;
54
55
  isEditing.value = true;
56
+ editingText.value = text.value;
55
57
  nextTick(() => {
56
58
  editableBox.value?.focus();
57
59
  });
58
60
  }
59
61
 
62
+ function onUpdate(newText: string) {
63
+ editingText.value = newText;
64
+ text.value = newText;
65
+ }
66
+
60
67
  </script>
61
68
 
62
69
  <style lang="scss" scoped>
@@ -1,39 +1,69 @@
1
1
  <template>
2
- <div
3
- contenteditable
4
- class="inline-block hover:bg-blue-200 focus:bg-blue-200 transition duration-300 outline-none"
5
- @input="onInput"
6
- >
7
- {{ text }}
2
+ <div class="inline-block relative">
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"
6
+ :style="{minWidth, minHeight}"
7
+ :class="contentClass"
8
+ @input="onInput"
9
+ >
10
+ {{ text }}
11
+ </div>
12
+ <div
13
+ v-if="!text && placeholder"
14
+ ref="placeholderDiv"
15
+ class="text-gray-600 absolute-top-left whitespace-nowrap z-1"
16
+ >
17
+ {{ placeholder }}
18
+ </div>
8
19
  </div>
9
20
  </template>
10
21
 
11
- <script setup>
22
+ <script setup lang="ts">
12
23
  import { useDebounceFn } from "@vueuse/core";
13
- import { ref } from "vue";
24
+ import { computed, onMounted, ref } from "vue";
14
25
 
15
26
  const emit = defineEmits(["update:model-value", "change"]);
16
- const props = defineProps({
17
- modelValue: {
18
- type: String,
19
- required: true
20
- },
21
- debounceDelay: {
22
- type: Number,
23
- default: 500
24
- }
27
+ const props = withDefaults(defineProps<{
28
+ modelValue: string;
29
+ color?: string;
30
+ debounceDelay?: number;
31
+ placeholder?: string;
32
+ }>(), {
33
+ // NOTE: You must safe-list required colors in tailwind.config.js
34
+ // 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
35
+ color: "blue-200",
36
+ textColor: "blue-900",
37
+ debounceDelay: 1000,
38
+ placeholder: "Enter Text..."
25
39
  });
26
40
 
27
41
  const text = ref(props.modelValue);
42
+ const placeholderDiv = ref(null);
43
+ const minWidth = ref(0);
44
+ const minHeight = ref(0);
45
+
46
+ onMounted(() => {
47
+ // Set the min-width to the width of the placeholder
48
+ if (placeholderDiv.value) {
49
+ minWidth.value = placeholderDiv.value.offsetWidth + "px";
50
+ minHeight.value = placeholderDiv.value.offsetHeight + "px";
51
+ }
52
+ });
28
53
 
29
54
  const debouncedChange = useDebounceFn(() => {
30
- emit("update:model-value", text.value);
31
- emit("change", text.value);
55
+ emit("update:model-value", text.value);
56
+ emit("change", text.value);
32
57
  }, props.debounceDelay);
33
58
 
34
59
  function onInput(e) {
35
- text.value = e.target.innerText;
36
- debouncedChange();
60
+ text.value = e.target.innerText;
61
+ debouncedChange();
37
62
  }
38
63
 
64
+ const contentClass = computed(() => [
65
+ `hover:bg-${props.color} focus:bg-${props.color}`,
66
+ `hover:text-${props.textColor} focus:text-${props.textColor}`,
67
+ `hover:outline-${props.color} focus:outline-${props.color}`
68
+ ]);
39
69
  </script>
@@ -22,7 +22,7 @@
22
22
 
23
23
  <script setup lang="ts">
24
24
  defineProps<{
25
- label?: string;
25
+ label?: string | null;
26
26
  name?: string;
27
27
  required?: boolean;
28
28
  requiredLabel?: string;
@@ -86,6 +86,8 @@ export interface Props extends QSelectProps {
86
86
  options?: unknown[];
87
87
  filterable?: boolean;
88
88
  filterFn?: (val: string) => void;
89
+ selectByObject?: boolean;
90
+ optionLabel?: string | ((option) => string);
89
91
  }
90
92
 
91
93
  const emit = defineEmits(["update:model-value", "search", "update"]);
@@ -97,7 +99,8 @@ const props = withDefaults(defineProps<Props>(), {
97
99
  inputClass: "",
98
100
  selectionClass: "",
99
101
  options: () => [],
100
- filterFn: null
102
+ filterFn: null,
103
+ optionLabel: "label"
101
104
  });
102
105
 
103
106
  const selectField = ref(null);
@@ -146,7 +149,11 @@ const selectedValue = computed(() => {
146
149
  return v === null ? "__null__" : v;
147
150
  }) || [];
148
151
  } else {
149
- return props.modelValue === null ? "__null__" : props.modelValue;
152
+ if (props.modelValue === null) return "__null__";
153
+
154
+ if (props.selectByObject) return props.modelValue.value || props.modelValue.id;
155
+
156
+ return props.modelValue;
150
157
  }
151
158
  });
152
159
 
@@ -159,8 +166,14 @@ const selectedOptions = computed(() => {
159
166
  if (!props.multiple) {
160
167
  values = (values || values === 0) ? [values] : [];
161
168
  }
169
+
170
+ const comparableValues = values.map((v) => {
171
+ if (v === "__null__") return null;
172
+ if (typeof v === "object") return v.value || v.id;
173
+ return v;
174
+ });
162
175
  return computedOptions.value.filter((o) => {
163
- return values.includes(o.value) || values.map(v => typeof v === "object" && v.id).includes(o.value?.id);
176
+ return comparableValues.includes(o.value);
164
177
  });
165
178
  });
166
179
 
@@ -175,6 +188,7 @@ const selectedLabel = computed(() => {
175
188
  if (!selectedOptions.value || selectedOptions.value.length === 0) {
176
189
  return props.placeholder || "(Select Option)";
177
190
  }
191
+
178
192
  return selectedOptions.value[0].selectionLabel;
179
193
  });
180
194
 
@@ -220,7 +234,7 @@ function resolveSelectionLabel(option) {
220
234
  if (typeof props.selectionLabel === "function") {
221
235
  return props.selectionLabel(option);
222
236
  }
223
- return option?.selectionLabel || option?.label;
237
+ return option?.selectionLabel || option?.label || (option && option[props.optionLabel]);
224
238
  }
225
239
 
226
240
  /**
@@ -232,7 +246,7 @@ function resolveValue(option) {
232
246
  if (!option || typeof option === "string") {
233
247
  return option;
234
248
  }
235
- let value = option.value;
249
+ let value = option.value || option.id;
236
250
  if (typeof props.optionValue === "string") {
237
251
  value = option[props.optionValue];
238
252
  } else if (typeof props.optionValue === "function") {
@@ -255,6 +269,13 @@ function onUpdate(value) {
255
269
 
256
270
  value = value === "__null__" ? null : value;
257
271
 
272
+ if (props.selectByObject && value !== null && value !== undefined && typeof value !== "object") {
273
+ if (props.multiple) {
274
+ value = props.options.filter((o) => value.includes(resolveValue(o)));
275
+ } else {
276
+ value = props.options.find((o) => resolveValue(o) === value);
277
+ }
278
+ }
258
279
  emit("update:model-value", value);
259
280
  emit("update", value);
260
281
  }
@@ -275,7 +296,7 @@ async function onFilter(val, update) {
275
296
  await nextTick(update);
276
297
  } else {
277
298
  update();
278
- if (shouldFilter.value === false) return;
299
+ if (!shouldFilter.value) return;
279
300
  if (val !== null && val !== filter.value) {
280
301
  filter.value = val;
281
302
  if (props.filterFn) {
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <div class="flex items-stretch flex-nowrap gap-x-4">
3
+ <QBtn
4
+ class="bg-green-900 px-4"
5
+ :loading="loading"
6
+ @click="$emit('create')"
7
+ >
8
+ <CreateIcon
9
+ class="w-4"
10
+ :class="createText ? 'mr-2' : ''"
11
+ />
12
+ {{ createText }}
13
+ </QBtn>
14
+ <SelectField
15
+ v-model="selected"
16
+ class="flex-grow"
17
+ :options="options"
18
+ :clearable="clearable"
19
+ :select-by-object="selectByObject"
20
+ :option-label="optionLabel"
21
+ />
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
+ </div>
30
+ </template>
31
+ <script setup lang="ts">
32
+ import { FaSolidPlus as CreateIcon } from "danx-icon";
33
+ import { QSelectOption } from "quasar";
34
+ import { ActionTargetItem } from "../../../../types";
35
+ import { ShowHideButton } from "../../../Utility/Buttons";
36
+ import SelectField from "./SelectField";
37
+
38
+ defineEmits(["create"]);
39
+ const selected = defineModel<string | number | object | null>("selected");
40
+ const editing = defineModel<boolean>("editing");
41
+ withDefaults(defineProps<{
42
+ options: QSelectOption[] | ActionTargetItem[];
43
+ showEdit?: boolean;
44
+ canEdit?: boolean;
45
+ loading?: boolean;
46
+ selectByObject?: boolean;
47
+ optionLabel?: string;
48
+ createText?: string;
49
+ editText?: string;
50
+ clearable?: boolean;
51
+ }>(), {
52
+ optionLabel: "label",
53
+ createText: "Create",
54
+ editText: "Edit"
55
+ });
56
+ </script>