quasar-ui-danx 0.4.27 → 0.4.29

Sign up to get free protection for your applications and to get access to all the features.
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>