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.
- package/README.md +35 -35
- package/dist/danx.es.js +24686 -24119
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +109 -109
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActionTable/ActionTable.vue +29 -7
- package/src/components/ActionTable/Filters/FilterableField.vue +14 -2
- package/src/components/ActionTable/Form/ActionForm.vue +17 -12
- package/src/components/ActionTable/Form/Fields/DateField.vue +24 -20
- package/src/components/ActionTable/Form/Fields/DateRangeField.vue +57 -53
- package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +9 -2
- package/src/components/ActionTable/Form/Fields/EditableDiv.vue +51 -21
- package/src/components/ActionTable/Form/Fields/FieldLabel.vue +1 -1
- package/src/components/ActionTable/Form/Fields/SelectField.vue +27 -6
- package/src/components/ActionTable/Form/Fields/SelectOrCreateField.vue +56 -0
- package/src/components/ActionTable/Form/Fields/TextField.vue +2 -0
- package/src/components/ActionTable/Form/Fields/index.ts +1 -0
- package/src/components/ActionTable/Form/RenderedForm.vue +7 -20
- package/src/components/ActionTable/Form/Utilities/MaxLengthCounter.vue +1 -1
- package/src/components/ActionTable/Form/Utilities/SaveStateIndicator.vue +37 -0
- package/src/components/ActionTable/Form/Utilities/index.ts +1 -0
- package/src/components/ActionTable/Layouts/ActionTableLayout.vue +20 -23
- package/src/components/ActionTable/Toolbars/ActionToolbar.vue +44 -36
- package/src/components/ActionTable/{listControls.ts → controls.ts} +13 -9
- package/src/components/ActionTable/index.ts +1 -1
- package/src/components/DragAndDrop/ListItemDraggable.vue +45 -31
- package/src/components/DragAndDrop/dragAndDrop.ts +221 -220
- package/src/components/DragAndDrop/listDragAndDrop.ts +269 -227
- package/src/components/PanelsDrawer/PanelsDrawer.vue +7 -7
- package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +3 -3
- package/src/components/Utility/Buttons/ShowHideButton.vue +86 -0
- package/src/components/Utility/Buttons/index.ts +1 -0
- package/src/components/Utility/Dialogs/ActionFormDialog.vue +30 -0
- package/src/components/Utility/Dialogs/CreateNewWithNameDialog.vue +26 -0
- package/src/components/Utility/Dialogs/RenderedFormDialog.vue +50 -0
- package/src/components/Utility/Dialogs/index.ts +3 -0
- package/src/helpers/FileUpload.ts +4 -4
- package/src/helpers/actions.ts +84 -20
- package/src/helpers/files.ts +56 -43
- package/src/helpers/formats.ts +23 -20
- package/src/helpers/objectStore.ts +24 -12
- package/src/types/actions.d.ts +50 -26
- package/src/types/controls.d.ts +23 -25
- package/src/types/fields.d.ts +1 -0
- package/src/types/files.d.ts +2 -2
- package/src/types/index.d.ts +5 -0
- package/src/types/shared.d.ts +9 -0
- package/src/types/tables.d.ts +3 -3
- package/types/vue-shims.d.ts +3 -2
package/package.json
CHANGED
@@ -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[]>(() =>
|
109
|
-
...column
|
110
|
-
|
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 === '
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
11
|
-
<div class="font-bold ml-3 hover:
|
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
|
- -
|
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,
|
41
|
+
import { fDate, parseDateTime } from "../../../../helpers";
|
33
42
|
|
34
43
|
const emit = defineEmits(["update:model-value"]);
|
35
|
-
const props = defineProps
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
17
|
-
<div class="font-bold ml-3 hover:
|
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
|
- -
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
92
|
+
let range;
|
89
93
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
111
|
+
return range;
|
108
112
|
});
|
109
113
|
|
110
114
|
function onSave() {
|
111
|
-
|
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="
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
31
|
-
|
55
|
+
emit("update:model-value", text.value);
|
56
|
+
emit("change", text.value);
|
32
57
|
}, props.debounceDelay);
|
33
58
|
|
34
59
|
function onInput(e) {
|
35
|
-
|
36
|
-
|
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>
|
@@ -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
|
-
|
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
|
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
|
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>
|