sprintify-ui 0.8.70 → 0.9.0
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/dist/sprintify-ui.es.js +1967 -1856
- package/dist/types/components/BaseHasMany.vue.d.ts +170 -80
- package/dist/types/components/BaseHasManyFetch.vue.d.ts +398 -0
- package/dist/types/components/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/BaseHasMany.stories.js +8 -73
- package/src/components/BaseHasMany.vue +29 -116
- package/src/components/BaseHasManyFetch.stories.js +271 -0
- package/src/components/BaseHasManyFetch.vue +222 -0
- package/src/components/BaseTooltip.vue +0 -1
- package/src/components/index.ts +2 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<BaseTagAutocomplete
|
|
3
3
|
ref="tagAutocompleteFetch"
|
|
4
4
|
:model-value="models"
|
|
5
|
-
:
|
|
5
|
+
:options="options"
|
|
6
6
|
:disabled="disabled"
|
|
7
|
+
:name="name"
|
|
7
8
|
:placeholder="placeholder"
|
|
8
9
|
:required="required"
|
|
9
10
|
:value-key="primaryKey"
|
|
10
11
|
:label-key="field"
|
|
12
|
+
:size="size"
|
|
11
13
|
:has-error="hasError"
|
|
12
|
-
:query-key="queryKey"
|
|
13
14
|
:max="max"
|
|
14
15
|
@update:model-value="onUpdate"
|
|
15
16
|
>
|
|
@@ -40,30 +41,24 @@
|
|
|
40
41
|
v-bind="footerProps"
|
|
41
42
|
/>
|
|
42
43
|
</template>
|
|
43
|
-
</
|
|
44
|
+
</BaseTagAutocomplete>
|
|
44
45
|
</template>
|
|
45
46
|
|
|
46
47
|
<script lang="ts" setup>
|
|
47
|
-
import { debounce } from 'lodash';
|
|
48
48
|
import { RawOption } from '@/types';
|
|
49
|
-
import { config } from '@/index';
|
|
50
49
|
import { PropType } from 'vue';
|
|
51
50
|
import BaseTagAutocompleteFetch from './BaseTagAutocompleteFetch.vue';
|
|
52
|
-
import
|
|
53
|
-
import {
|
|
51
|
+
import BaseTagAutocomplete from './BaseTagAutocomplete.vue';
|
|
52
|
+
import { Size } from '@/utils/sizes';
|
|
54
53
|
|
|
55
54
|
const props = defineProps({
|
|
56
55
|
modelValue: {
|
|
57
56
|
default: undefined,
|
|
58
57
|
type: [Array, String, Number, null, undefined] as PropType<string[] | string | number | null | undefined>,
|
|
59
58
|
},
|
|
60
|
-
|
|
59
|
+
options: {
|
|
61
60
|
required: true,
|
|
62
|
-
type:
|
|
63
|
-
},
|
|
64
|
-
showRouteUrl: {
|
|
65
|
-
default: undefined,
|
|
66
|
-
type: Function as PropType<((ids: (string | number)[]) => string) | undefined>,
|
|
61
|
+
type: Array as PropType<RawOption[]>,
|
|
67
62
|
},
|
|
68
63
|
primaryKey: {
|
|
69
64
|
default: 'id',
|
|
@@ -81,57 +76,47 @@ const props = defineProps({
|
|
|
81
76
|
default: false,
|
|
82
77
|
type: Boolean,
|
|
83
78
|
},
|
|
79
|
+
name: {
|
|
80
|
+
default: undefined,
|
|
81
|
+
type: String,
|
|
82
|
+
},
|
|
84
83
|
placeholder: {
|
|
85
84
|
default: undefined,
|
|
86
85
|
type: String,
|
|
87
86
|
},
|
|
87
|
+
size: {
|
|
88
|
+
default: undefined,
|
|
89
|
+
type: String as PropType<Size>,
|
|
90
|
+
},
|
|
88
91
|
max: {
|
|
89
92
|
default: undefined,
|
|
90
93
|
type: Number,
|
|
91
94
|
},
|
|
92
|
-
queryKey: {
|
|
93
|
-
default: 'search',
|
|
94
|
-
type: String,
|
|
95
|
-
},
|
|
96
|
-
currentModels: {
|
|
97
|
-
default() {
|
|
98
|
-
return undefined;
|
|
99
|
-
},
|
|
100
|
-
type: Array as PropType<RawOption[] | undefined>,
|
|
101
|
-
},
|
|
102
95
|
hasError: {
|
|
103
96
|
default: false,
|
|
104
97
|
type: Boolean,
|
|
105
98
|
},
|
|
106
99
|
});
|
|
107
100
|
|
|
108
|
-
const http = config.http;
|
|
109
|
-
|
|
110
101
|
const emit = defineEmits(['update:modelValue']);
|
|
111
102
|
|
|
112
103
|
const tagAutocompleteFetch = ref<InstanceType<
|
|
113
104
|
typeof BaseTagAutocompleteFetch
|
|
114
105
|
> | null>(null);
|
|
115
106
|
|
|
116
|
-
const models =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
() => props.currentModels,
|
|
121
|
-
ensureModelIsFilledDebounced,
|
|
122
|
-
{ deep: true }
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
watch(
|
|
126
|
-
() => props.modelValue,
|
|
127
|
-
ensureModelIsFilledDebounced,
|
|
128
|
-
{ deep: true }
|
|
129
|
-
);
|
|
107
|
+
const models = computed(() => {
|
|
108
|
+
if (!props.modelValue || !Array.isArray(props.modelValue)) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
130
111
|
|
|
131
|
-
|
|
112
|
+
return props.modelValue
|
|
113
|
+
.map((id) => {
|
|
114
|
+
return props.options.find((m) => m[props.primaryKey] === id);
|
|
115
|
+
})
|
|
116
|
+
.filter((m) => m) as RawOption[];
|
|
117
|
+
});
|
|
132
118
|
|
|
133
119
|
function onUpdate(newModels: RawOption[]) {
|
|
134
|
-
models.value = newModels;
|
|
135
120
|
emit(
|
|
136
121
|
'update:modelValue',
|
|
137
122
|
newModels.map((m) => m[props.primaryKey]),
|
|
@@ -139,84 +124,12 @@ function onUpdate(newModels: RawOption[]) {
|
|
|
139
124
|
);
|
|
140
125
|
}
|
|
141
126
|
|
|
142
|
-
function ensureModelsAreFilled() {
|
|
143
|
-
|
|
144
|
-
let modelValueInternal = props.modelValue;
|
|
145
|
-
|
|
146
|
-
if (typeof modelValueInternal == 'string' || typeof modelValueInternal == 'number') {
|
|
147
|
-
modelValueInternal = [modelValueInternal + ''];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!Array.isArray(modelValueInternal)) {
|
|
151
|
-
models.value = [];
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (modelValueInternal.length == 0) {
|
|
156
|
-
models.value = [];
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Remove incorrect models
|
|
161
|
-
|
|
162
|
-
const ids = modelValueInternal.map((id: number | string) => id.toString());
|
|
163
|
-
|
|
164
|
-
models.value = models.value.filter((m) => ids.includes(m[props.primaryKey] + ''));
|
|
165
|
-
|
|
166
|
-
const localModelIds = models.value.map((m) => m[props.primaryKey] + '');
|
|
167
|
-
|
|
168
|
-
let missingIds = ids.filter((id) => !localModelIds.includes(id));
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// Current models are fully set
|
|
172
|
-
if (missingIds.length == 0) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Try with current models
|
|
177
|
-
|
|
178
|
-
if (Array.isArray(props.currentModels)) {
|
|
179
|
-
missingIds.forEach((id) => {
|
|
180
|
-
const model = props.currentModels?.find((m) => m[props.primaryKey as never] == id);
|
|
181
|
-
|
|
182
|
-
if (model) {
|
|
183
|
-
models.value.push(model);
|
|
184
|
-
missingIds = missingIds.filter((i) => i != id);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Current models are fully set
|
|
190
|
-
if (missingIds.length == 0) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Try with show route
|
|
195
|
-
|
|
196
|
-
if (props.showRouteUrl == null) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
http
|
|
201
|
-
.get(props.showRouteUrl(missingIds))
|
|
202
|
-
.then((response: AxiosResponse) => {
|
|
203
|
-
|
|
204
|
-
const items = getItems(response.data);
|
|
205
|
-
|
|
206
|
-
models.value = items.filter((i: Record<string, any>) => {
|
|
207
|
-
// convert primary keys to string for comparison
|
|
208
|
-
return ids.includes(i[props.primaryKey] + '');
|
|
209
|
-
});
|
|
210
|
-
})
|
|
211
|
-
.catch((e: Error) => e);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
127
|
defineExpose({
|
|
215
128
|
focus: () => tagAutocompleteFetch.value?.focus(),
|
|
216
129
|
blur: () => tagAutocompleteFetch.value?.blur(),
|
|
217
130
|
open: () => tagAutocompleteFetch.value?.open(),
|
|
218
131
|
close: () => tagAutocompleteFetch.value?.close(),
|
|
219
|
-
setKeywords: (input: string) =>
|
|
220
|
-
tagAutocompleteFetch.value?.setKeywords(input),
|
|
132
|
+
setKeywords: (input: string) => tagAutocompleteFetch.value?.setKeywords(input),
|
|
221
133
|
});
|
|
134
|
+
|
|
222
135
|
</script>
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import BaseHasManyFetch from "./BaseHasManyFetch.vue";
|
|
2
|
+
import ShowValue from "@/../.storybook/components/ShowValue.vue";
|
|
3
|
+
import { createFieldStory, options, sizes } from "../../.storybook/utils";
|
|
4
|
+
import BaseAppSnackbars from "./BaseAppSnackbars.vue";
|
|
5
|
+
import QueryString from "qs";
|
|
6
|
+
import { random } from "lodash";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: "Form/BaseHasManyFetch",
|
|
10
|
+
component: BaseHasManyFetch,
|
|
11
|
+
argTypes: {
|
|
12
|
+
size: {
|
|
13
|
+
control: { type: "select" },
|
|
14
|
+
options: sizes,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
url: "https://faker.witify.io/api/todos",
|
|
19
|
+
field: "name",
|
|
20
|
+
primaryKey: "id",
|
|
21
|
+
showRouteUrl: (ids) => {
|
|
22
|
+
const params = QueryString.stringify({ filter: { id: ids } });
|
|
23
|
+
return `https://faker.witify.io/api/todos?${params}`;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
decorators: [() => ({ template: '<div class="mb-36"><story/></div>' })],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const Template = (args) => {
|
|
30
|
+
return {
|
|
31
|
+
components: { BaseHasManyFetch, ShowValue, BaseAppSnackbars },
|
|
32
|
+
setup() {
|
|
33
|
+
const value = ref(4);
|
|
34
|
+
const currentModels = ref([
|
|
35
|
+
{ id: 4, name: "Todo 4 (local)" },
|
|
36
|
+
{ id: 6, name: "Todo 6 (local)" },
|
|
37
|
+
]);
|
|
38
|
+
return { args, value, currentModels };
|
|
39
|
+
},
|
|
40
|
+
template: `
|
|
41
|
+
<BaseHasManyFetch
|
|
42
|
+
v-model="value"
|
|
43
|
+
v-bind="args"
|
|
44
|
+
:current-models="currentModels"
|
|
45
|
+
></BaseHasManyFetch>
|
|
46
|
+
<ShowValue :value="value" />
|
|
47
|
+
<BaseAppSnackbars />
|
|
48
|
+
`,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Demo = Template.bind({});
|
|
53
|
+
Demo.args = {};
|
|
54
|
+
|
|
55
|
+
export const Disabled = (args) => {
|
|
56
|
+
return {
|
|
57
|
+
components: { BaseHasManyFetch, ShowValue },
|
|
58
|
+
setup() {
|
|
59
|
+
// current model is incorrect, to test component's resilience
|
|
60
|
+
const currentModel = options[1];
|
|
61
|
+
const value = ref([7]);
|
|
62
|
+
return { args, value, currentModel };
|
|
63
|
+
},
|
|
64
|
+
template: `<BaseHasManyFetch
|
|
65
|
+
v-bind="args"
|
|
66
|
+
v-model="value"
|
|
67
|
+
:current-models="[currentModel]"
|
|
68
|
+
:disabled="true"
|
|
69
|
+
></BaseHasManyFetch>
|
|
70
|
+
<ShowValue :value="value" />`,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Maximum = Template.bind({});
|
|
75
|
+
Maximum.args = {
|
|
76
|
+
max: 3,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const ShowRouteUrl = Template.bind({});
|
|
80
|
+
ShowRouteUrl.args = {
|
|
81
|
+
max: 3,
|
|
82
|
+
showRouteUrl: (ids) => {
|
|
83
|
+
const params = QueryString.stringify({ filter: { id: ids } });
|
|
84
|
+
return `https://faker.witify.io/api/todos?${params}`;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Sizes = (args) => ({
|
|
89
|
+
components: { BaseHasManyFetch },
|
|
90
|
+
setup() {
|
|
91
|
+
const value = ref([]);
|
|
92
|
+
return { args, sizes, value };
|
|
93
|
+
},
|
|
94
|
+
template: `
|
|
95
|
+
<div v-for="size in sizes" class="mb-1">
|
|
96
|
+
<p class="text-xs text-slate-600 leading-tight">{{ size }}</p>
|
|
97
|
+
<BaseHasManyFetch v-model="value" v-bind="args" :size="size"></BaseHasManyFetch>
|
|
98
|
+
</div>
|
|
99
|
+
`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const SlotOption = (args) => {
|
|
103
|
+
return {
|
|
104
|
+
components: { BaseHasManyFetch },
|
|
105
|
+
setup() {
|
|
106
|
+
const value = ref([]);
|
|
107
|
+
return { args, value };
|
|
108
|
+
},
|
|
109
|
+
template: `
|
|
110
|
+
<div class="mb-20">
|
|
111
|
+
<BaseHasManyFetch
|
|
112
|
+
v-model="value"
|
|
113
|
+
v-bind="args"
|
|
114
|
+
>
|
|
115
|
+
<template #option="{ option, active, selected }">
|
|
116
|
+
<div
|
|
117
|
+
class="rounded px-2 py-1"
|
|
118
|
+
:class="{
|
|
119
|
+
'hover:bg-slate-100': !active && !selected,
|
|
120
|
+
'bg-slate-200 hover:bg-slate-300': active && !selected,
|
|
121
|
+
'bg-blue-500 text-white hover:bg-blue-600': !active && selected,
|
|
122
|
+
'bg-blue-600 text-white hover:bg-blue-700': active && selected,
|
|
123
|
+
}"
|
|
124
|
+
>
|
|
125
|
+
<p class="text-sm font-medium">{{ option.title }}</p>
|
|
126
|
+
<p class="opacity-60 text-xs">{{ option.owner?.name }}</p>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
</BaseHasManyFetch>
|
|
130
|
+
</div>
|
|
131
|
+
`,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const SlotItem = (args) => {
|
|
136
|
+
return {
|
|
137
|
+
components: { BaseHasManyFetch },
|
|
138
|
+
setup() {
|
|
139
|
+
const value = ref(null);
|
|
140
|
+
return { args, value };
|
|
141
|
+
},
|
|
142
|
+
template: `
|
|
143
|
+
<BaseHasManyFetch
|
|
144
|
+
v-model="value"
|
|
145
|
+
v-bind="args"
|
|
146
|
+
>
|
|
147
|
+
<template #items="{items, removeOption}">
|
|
148
|
+
<div
|
|
149
|
+
v-for="item in items"
|
|
150
|
+
:key="item"
|
|
151
|
+
class="p-0.5"
|
|
152
|
+
>
|
|
153
|
+
<div class="flex items-center rounded border pl-2 py-1 bg-white">
|
|
154
|
+
<BaseIcon icon="heroicons:tag" class="mr-2 text-slate-500" />
|
|
155
|
+
<div>
|
|
156
|
+
{{ item.label }}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
class="flex shrink-0 appearance-none items-center justify-center border-0 bg-transparent pl-1 pr-3 text-xs outline-none"
|
|
162
|
+
@click=removeOption(item)
|
|
163
|
+
>
|
|
164
|
+
✕
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</template>
|
|
169
|
+
</BaseHasManyFetch>
|
|
170
|
+
`,
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const SlotFooter = (args) => {
|
|
175
|
+
return {
|
|
176
|
+
components: { BaseHasManyFetch },
|
|
177
|
+
setup() {
|
|
178
|
+
const value = ref([]);
|
|
179
|
+
function onClick() {
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
alert(1);
|
|
182
|
+
}, 300);
|
|
183
|
+
}
|
|
184
|
+
return { args, value, onClick };
|
|
185
|
+
},
|
|
186
|
+
template: `
|
|
187
|
+
<BaseHasManyFetch
|
|
188
|
+
v-model="value"
|
|
189
|
+
v-bind="args"
|
|
190
|
+
>
|
|
191
|
+
<template #footer>
|
|
192
|
+
<div class="text-center p-2 border-t">
|
|
193
|
+
<button type="button" @click=onClick class="btn btn-sm w-full btn-slate-200-outline">This is the footer 💯</button>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
</BaseHasManyFetch>
|
|
197
|
+
`,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const SlotEmpty = (args) => {
|
|
202
|
+
return {
|
|
203
|
+
components: { BaseHasManyFetch },
|
|
204
|
+
setup() {
|
|
205
|
+
const value = ref([]);
|
|
206
|
+
return { args, value };
|
|
207
|
+
},
|
|
208
|
+
template: `
|
|
209
|
+
<BaseHasManyFetch
|
|
210
|
+
v-model="value"
|
|
211
|
+
v-bind="args"
|
|
212
|
+
>
|
|
213
|
+
<template #empty="props">
|
|
214
|
+
<div>
|
|
215
|
+
<div v-if="props.firstSearch" class="text-center py-10 p-6">🤓🤓🤓</div>
|
|
216
|
+
<div v-else class="text-center px-6 py-20">Start your search... 🔎</div>
|
|
217
|
+
</div>
|
|
218
|
+
</template>
|
|
219
|
+
</BaseHasManyFetch>
|
|
220
|
+
`,
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const RaceConditionTemplate = (args) => {
|
|
225
|
+
return {
|
|
226
|
+
components: { BaseHasManyFetch, ShowValue, BaseAppSnackbars },
|
|
227
|
+
setup() {
|
|
228
|
+
const value = ref(["4", "6"]);
|
|
229
|
+
|
|
230
|
+
const valueExternal = ref([]);
|
|
231
|
+
|
|
232
|
+
const intervalId = setInterval(() => {
|
|
233
|
+
const newValue = [random(1, 10)];
|
|
234
|
+
value.value = newValue;
|
|
235
|
+
valueExternal.value = newValue;
|
|
236
|
+
}, 300);
|
|
237
|
+
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
clearInterval(intervalId);
|
|
240
|
+
}, 1000);
|
|
241
|
+
|
|
242
|
+
return { args, value, valueExternal };
|
|
243
|
+
},
|
|
244
|
+
template: `
|
|
245
|
+
<BaseHasManyFetch
|
|
246
|
+
v-model="value"
|
|
247
|
+
v-bind="args"
|
|
248
|
+
></BaseHasManyFetch>
|
|
249
|
+
|
|
250
|
+
<br>
|
|
251
|
+
|
|
252
|
+
<p class="-mb-4">Value from data</p>
|
|
253
|
+
<ShowValue :value="valueExternal" />
|
|
254
|
+
|
|
255
|
+
<br>
|
|
256
|
+
|
|
257
|
+
<p class="-mb-4">Value from component</p>
|
|
258
|
+
<ShowValue :value="value" />
|
|
259
|
+
<BaseAppSnackbars />
|
|
260
|
+
`,
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const RaceCondition = RaceConditionTemplate.bind({});
|
|
265
|
+
RaceCondition.args = {};
|
|
266
|
+
|
|
267
|
+
export const Field = createFieldStory({
|
|
268
|
+
component: BaseHasManyFetch,
|
|
269
|
+
componentName: "BaseHasManyFetch",
|
|
270
|
+
label: "Name",
|
|
271
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BaseTagAutocompleteFetch
|
|
3
|
+
ref="tagAutocompleteFetch"
|
|
4
|
+
:model-value="models"
|
|
5
|
+
:url="url"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
:placeholder="placeholder"
|
|
8
|
+
:required="required"
|
|
9
|
+
:value-key="primaryKey"
|
|
10
|
+
:label-key="field"
|
|
11
|
+
:has-error="hasError"
|
|
12
|
+
:query-key="queryKey"
|
|
13
|
+
:max="max"
|
|
14
|
+
@update:model-value="onUpdate"
|
|
15
|
+
>
|
|
16
|
+
<template #items="itemProps">
|
|
17
|
+
<slot
|
|
18
|
+
name="items"
|
|
19
|
+
v-bind="itemProps"
|
|
20
|
+
/>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template #option="optionProps">
|
|
24
|
+
<slot
|
|
25
|
+
name="option"
|
|
26
|
+
v-bind="optionProps"
|
|
27
|
+
/>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<template #empty="emptyProps">
|
|
31
|
+
<slot
|
|
32
|
+
name="empty"
|
|
33
|
+
v-bind="emptyProps"
|
|
34
|
+
/>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<template #footer="footerProps">
|
|
38
|
+
<slot
|
|
39
|
+
name="footer"
|
|
40
|
+
v-bind="footerProps"
|
|
41
|
+
/>
|
|
42
|
+
</template>
|
|
43
|
+
</BaseTagAutocompleteFetch>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script lang="ts" setup>
|
|
47
|
+
import { debounce } from 'lodash';
|
|
48
|
+
import { RawOption } from '@/types';
|
|
49
|
+
import { config } from '@/index';
|
|
50
|
+
import { PropType } from 'vue';
|
|
51
|
+
import BaseTagAutocompleteFetch from './BaseTagAutocompleteFetch.vue';
|
|
52
|
+
import { AxiosResponse } from 'axios';
|
|
53
|
+
import { getItems } from '@/utils/getApiData';
|
|
54
|
+
|
|
55
|
+
const props = defineProps({
|
|
56
|
+
modelValue: {
|
|
57
|
+
default: undefined,
|
|
58
|
+
type: [Array, String, Number, null, undefined] as PropType<string[] | string | number | null | undefined>,
|
|
59
|
+
},
|
|
60
|
+
url: {
|
|
61
|
+
required: true,
|
|
62
|
+
type: String,
|
|
63
|
+
},
|
|
64
|
+
showRouteUrl: {
|
|
65
|
+
default: undefined,
|
|
66
|
+
type: Function as PropType<((ids: (string | number)[]) => string) | undefined>,
|
|
67
|
+
},
|
|
68
|
+
primaryKey: {
|
|
69
|
+
default: 'id',
|
|
70
|
+
type: String,
|
|
71
|
+
},
|
|
72
|
+
field: {
|
|
73
|
+
required: true,
|
|
74
|
+
type: String,
|
|
75
|
+
},
|
|
76
|
+
required: {
|
|
77
|
+
default: false,
|
|
78
|
+
type: Boolean,
|
|
79
|
+
},
|
|
80
|
+
disabled: {
|
|
81
|
+
default: false,
|
|
82
|
+
type: Boolean,
|
|
83
|
+
},
|
|
84
|
+
placeholder: {
|
|
85
|
+
default: undefined,
|
|
86
|
+
type: String,
|
|
87
|
+
},
|
|
88
|
+
max: {
|
|
89
|
+
default: undefined,
|
|
90
|
+
type: Number,
|
|
91
|
+
},
|
|
92
|
+
queryKey: {
|
|
93
|
+
default: 'search',
|
|
94
|
+
type: String,
|
|
95
|
+
},
|
|
96
|
+
currentModels: {
|
|
97
|
+
default() {
|
|
98
|
+
return undefined;
|
|
99
|
+
},
|
|
100
|
+
type: Array as PropType<RawOption[] | undefined>,
|
|
101
|
+
},
|
|
102
|
+
hasError: {
|
|
103
|
+
default: false,
|
|
104
|
+
type: Boolean,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const http = config.http;
|
|
109
|
+
|
|
110
|
+
const emit = defineEmits(['update:modelValue']);
|
|
111
|
+
|
|
112
|
+
const tagAutocompleteFetch = ref<InstanceType<
|
|
113
|
+
typeof BaseTagAutocompleteFetch
|
|
114
|
+
> | null>(null);
|
|
115
|
+
|
|
116
|
+
const models = ref<RawOption[]>([]);
|
|
117
|
+
const ensureModelIsFilledDebounced = debounce(() => ensureModelsAreFilled(), 100);
|
|
118
|
+
|
|
119
|
+
watch(
|
|
120
|
+
() => props.currentModels,
|
|
121
|
+
ensureModelIsFilledDebounced,
|
|
122
|
+
{ deep: true }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
watch(
|
|
126
|
+
() => props.modelValue,
|
|
127
|
+
ensureModelIsFilledDebounced,
|
|
128
|
+
{ deep: true }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
ensureModelIsFilledDebounced();
|
|
132
|
+
|
|
133
|
+
function onUpdate(newModels: RawOption[]) {
|
|
134
|
+
models.value = newModels;
|
|
135
|
+
emit(
|
|
136
|
+
'update:modelValue',
|
|
137
|
+
newModels.map((m) => m[props.primaryKey]),
|
|
138
|
+
newModels,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ensureModelsAreFilled() {
|
|
143
|
+
|
|
144
|
+
let modelValueInternal = props.modelValue;
|
|
145
|
+
|
|
146
|
+
if (typeof modelValueInternal == 'string' || typeof modelValueInternal == 'number') {
|
|
147
|
+
modelValueInternal = [modelValueInternal + ''];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!Array.isArray(modelValueInternal)) {
|
|
151
|
+
models.value = [];
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (modelValueInternal.length == 0) {
|
|
156
|
+
models.value = [];
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Remove incorrect models
|
|
161
|
+
|
|
162
|
+
const ids = modelValueInternal.map((id: number | string) => id.toString());
|
|
163
|
+
|
|
164
|
+
models.value = models.value.filter((m) => ids.includes(m[props.primaryKey] + ''));
|
|
165
|
+
|
|
166
|
+
const localModelIds = models.value.map((m) => m[props.primaryKey] + '');
|
|
167
|
+
|
|
168
|
+
let missingIds = ids.filter((id) => !localModelIds.includes(id));
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
// Current models are fully set
|
|
172
|
+
if (missingIds.length == 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Try with current models
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(props.currentModels)) {
|
|
179
|
+
missingIds.forEach((id) => {
|
|
180
|
+
const model = props.currentModels?.find((m) => m[props.primaryKey as never] == id);
|
|
181
|
+
|
|
182
|
+
if (model) {
|
|
183
|
+
models.value.push(model);
|
|
184
|
+
missingIds = missingIds.filter((i) => i != id);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Current models are fully set
|
|
190
|
+
if (missingIds.length == 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Try with show route
|
|
195
|
+
|
|
196
|
+
if (props.showRouteUrl == null) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
http
|
|
201
|
+
.get(props.showRouteUrl(missingIds))
|
|
202
|
+
.then((response: AxiosResponse) => {
|
|
203
|
+
|
|
204
|
+
const items = getItems(response.data);
|
|
205
|
+
|
|
206
|
+
models.value = items.filter((i: Record<string, any>) => {
|
|
207
|
+
// convert primary keys to string for comparison
|
|
208
|
+
return ids.includes(i[props.primaryKey] + '');
|
|
209
|
+
});
|
|
210
|
+
})
|
|
211
|
+
.catch((e: Error) => e);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
defineExpose({
|
|
215
|
+
focus: () => tagAutocompleteFetch.value?.focus(),
|
|
216
|
+
blur: () => tagAutocompleteFetch.value?.blur(),
|
|
217
|
+
open: () => tagAutocompleteFetch.value?.open(),
|
|
218
|
+
close: () => tagAutocompleteFetch.value?.close(),
|
|
219
|
+
setKeywords: (input: string) =>
|
|
220
|
+
tagAutocompleteFetch.value?.setKeywords(input),
|
|
221
|
+
});
|
|
222
|
+
</script>
|