refactor: api select value (#69)
* feat: add file * fix: wrong type * feat: select customer component * fixup! feat: select customer component * fix: char case * refactor: fn alias * chore: add space * feat: accept fetch parameter * refactor: naming * feat: add emit event create * fix: add suffix to add text * feat: add before options slot for select input comp * fix: error when label not found * fix: value type * feat: add required param * fix: wording * refactor: fix customer * feat: use new select component * chore: add note * feat: add decoration for select with creatable * feat: emit event * feat: close popup on click * feat: adjust alignment * feat: add readonly params * feat: add select branch option * feat: use new select component * feat: add disabled params * feat: adjust internal search and select * refactor: props type * feat: use new select component * feat: add lib for select component * refactor: use factory function instead * refactor: merge two lines of code * refactor: move watch inside * refactor: fix value not in list check * chore: cleanup * fix: remove test page size * chore: remove unused * feat: use new select component * fix: typo * fix: error * refactor: extract type * refactor: change ref var to normal var * refactor: force overwrite params to prevent error on render * feat: add clearable parameter * feat: make clearable
This commit is contained in:
parent
a227744131
commit
d414685fe7
13 changed files with 500 additions and 319 deletions
108
src/components/shared/select/SelectBranch.vue
Normal file
108
src/components/shared/select/SelectBranch.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
|
||||
import { Branch } from 'src/stores/branch/types';
|
||||
|
||||
import useStore from 'src/stores/branch';
|
||||
|
||||
type SelectOption = Branch;
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const { fetchList: getList, fetchById: getById } = useStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
selectFirstValue?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
includeCustomer: true,
|
||||
...props.params,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
if (props.selectFirstValue) {
|
||||
setDefaultValue();
|
||||
} else await getSelectedOption();
|
||||
});
|
||||
|
||||
function setDefaultValue() {
|
||||
setFirstValue();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
incremental
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="
|
||||
selectOptions.map((v) => {
|
||||
const ret = {
|
||||
label: (
|
||||
{
|
||||
['eng']: [v.nameEN, `(${v.code})`].join(' '),
|
||||
['tha']: [v.name, `(${v.code})`].join(' '),
|
||||
} as const
|
||||
)[$i18n.locale],
|
||||
value: v.id,
|
||||
};
|
||||
return ret;
|
||||
})
|
||||
"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="[(v: string) => !!v || $t('form.error.required')]"
|
||||
@filter="filter"
|
||||
>
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
128
src/components/shared/select/SelectCustomer.vue
Normal file
128
src/components/shared/select/SelectCustomer.vue
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
import SelectCustomerItem from './SelectCustomerItem.vue';
|
||||
|
||||
import { Customer, CustomerBranch } from 'src/stores/customer/types';
|
||||
|
||||
import useStore from 'src/stores/customer';
|
||||
|
||||
type SelectOption = CustomerBranch & { customer: Customer };
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const {
|
||||
fetchListCustomerBranch: getList,
|
||||
fetchListCustomerBranchById: getById,
|
||||
} = useStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
simple?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
...props.params,
|
||||
includeCustomer: true,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
await getSelectedOption();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
option-value="id"
|
||||
incremental
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="selectOptions"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="[(v: string) => !!v || $t('form.error.required')]"
|
||||
@filter="filter"
|
||||
>
|
||||
<template #selected-item="{ opt }">
|
||||
<SelectCustomerItem
|
||||
v-if="typeof opt === 'object'"
|
||||
:data="opt"
|
||||
:simple
|
||||
single-line
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #before-options v-if="creatable">
|
||||
<q-item clickable v-close-popup @click.stop="$emit('create')">
|
||||
<q-item-section>
|
||||
<b class="row items-center">
|
||||
<q-icon
|
||||
name="mdi-plus-circle-outline"
|
||||
class="q-mr-sm"
|
||||
style="color: hsl(var(--positive-bg))"
|
||||
/>
|
||||
{{ $t('general.add', { text: $t('quotation.newCustomer') }) }}
|
||||
</b>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator class="q-mx-sm" />
|
||||
</template>
|
||||
|
||||
<template #option="{ opt, scope }">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<SelectCustomerItem :data="opt" :simple />
|
||||
</q-item>
|
||||
|
||||
<q-separator class="q-mx-sm" />
|
||||
</template>
|
||||
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
100
src/components/shared/select/SelectCustomerItem.vue
Normal file
100
src/components/shared/select/SelectCustomerItem.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts" setup>
|
||||
import { Customer, CustomerBranch } from 'src/stores/customer/types';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { formatAddress } from 'src/utils/address';
|
||||
|
||||
defineProps<{
|
||||
data?: CustomerBranch & { customer: Customer };
|
||||
simple?: boolean;
|
||||
singleLine?: boolean;
|
||||
}>();
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="data">
|
||||
<div v-if="simple" class="row items-center">
|
||||
{{
|
||||
{
|
||||
['CORP']: {
|
||||
['eng']: data.registerNameEN,
|
||||
['tha']: data.registerName,
|
||||
}[$i18n.locale],
|
||||
['PERS']:
|
||||
{
|
||||
['eng']: `${optionStore.mapOption(data.namePrefix)} ${data.firstNameEN} ${data.lastNameEN}`,
|
||||
['tha']: `${optionStore.mapOption(data.namePrefix)} ${data.firstName} ${data.lastName}`,
|
||||
}[$i18n.locale] || '-',
|
||||
}[data.customer.customerType]
|
||||
}}
|
||||
({{ data.code }})
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{
|
||||
['row']: singleLine,
|
||||
['items-center']: singleLine,
|
||||
}"
|
||||
>
|
||||
<div class="q-mr-sm">
|
||||
<span style="font-weight: 600">
|
||||
{{
|
||||
data.customer.customerType === 'CORP'
|
||||
? $t('customer.form.registerName')
|
||||
: $t('customer.form.ownerName')
|
||||
}}:
|
||||
</span>
|
||||
{{
|
||||
{
|
||||
['CORP']: {
|
||||
['eng']: data.registerNameEN,
|
||||
['tha']: data.registerName,
|
||||
}[$i18n.locale],
|
||||
['PERS']:
|
||||
{
|
||||
['eng']: `${optionStore.mapOption(data.namePrefix)} ${data.firstNameEN} ${data.lastNameEN}`,
|
||||
['tha']: `${optionStore.mapOption(data.namePrefix)} ${data.firstName} ${data.lastName}`,
|
||||
}[$i18n.locale] || '-',
|
||||
}[data.customer.customerType]
|
||||
}}
|
||||
({{ data.code }})
|
||||
</div>
|
||||
<div
|
||||
class="text-caption app-text-muted-2"
|
||||
v-if="data.customer && data.province"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`branch.form.title.${data.code.endsWith('-00') ? 'branchHQLabel' : 'branchLabel'}`,
|
||||
)
|
||||
}}
|
||||
|
||||
{{ !data.code.endsWith('-00') ? +data.code.split('-')[1] : '' }}
|
||||
</div>
|
||||
<div
|
||||
class="text-caption app-text-muted-2"
|
||||
v-if="data.customer && data.province"
|
||||
>
|
||||
{{ $t('general.address') }}
|
||||
{{
|
||||
{
|
||||
['eng']: formatAddress({ ...data, en: true }),
|
||||
['tha']: formatAddress({ ...data, en: false }),
|
||||
}[$i18n.locale]
|
||||
}}
|
||||
<q-tooltip v-if="data.customer && data.province">
|
||||
{{ $t('customerBranch.form.title') }}:
|
||||
|
||||
{{ $t('general.address') }}
|
||||
{{
|
||||
{
|
||||
['eng']: formatAddress({ ...data, en: true }),
|
||||
['tha']: formatAddress({ ...data, en: false }),
|
||||
}[$i18n.locale]
|
||||
}}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
0
src/components/shared/select/SelectEmployee.vue
Normal file
0
src/components/shared/select/SelectEmployee.vue
Normal file
94
src/components/shared/select/select.ts
Normal file
94
src/components/shared/select/select.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { QSelect } from 'quasar';
|
||||
import { Ref, watch } from 'vue';
|
||||
|
||||
export type SelectProps<T extends (...args: any[]) => any> = {
|
||||
params?: Parameters<T>[0];
|
||||
creatable?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
clearable?: boolean;
|
||||
autoSelectOnSingle?: boolean;
|
||||
};
|
||||
|
||||
export const createSelect = <T extends Record<string, any>>(
|
||||
state: {
|
||||
value: Ref<string | null | undefined>;
|
||||
valueOption: Ref<T | undefined>;
|
||||
selectOptions: Ref<T[]>;
|
||||
getByValue: (id: string) => Promise<T | void> | T | void;
|
||||
getList: (query?: string) => Promise<T[] | void> | T[] | void;
|
||||
},
|
||||
opts?: {
|
||||
valueField?: keyof T;
|
||||
},
|
||||
) => {
|
||||
const { value, valueOption, selectOptions, getList, getByValue } = state;
|
||||
|
||||
const valueField = opts?.valueField || 'value';
|
||||
|
||||
let cache: T[];
|
||||
let previousSearch = '';
|
||||
|
||||
watch(value, (v) => {
|
||||
if (!v || (cache && cache.find((opt) => opt[valueField] === v))) return;
|
||||
getSelectedOption();
|
||||
});
|
||||
|
||||
async function getOptions(query?: string) {
|
||||
if (cache && selectOptions.value.length > 0 && previousSearch === query) {
|
||||
selectOptions.value = JSON.parse(JSON.stringify(cache));
|
||||
return;
|
||||
}
|
||||
const ret = await getList(query);
|
||||
if (ret) {
|
||||
cache = ret;
|
||||
selectOptions.value = JSON.parse(JSON.stringify(cache));
|
||||
previousSearch = query || previousSearch;
|
||||
}
|
||||
}
|
||||
|
||||
async function setFirstValue() {
|
||||
if (value.value) return;
|
||||
const first = selectOptions.value.at(0);
|
||||
if (first) value.value = first[valueField];
|
||||
}
|
||||
|
||||
async function getSelectedOption() {
|
||||
const currentValue = value.value;
|
||||
|
||||
if (!currentValue) return;
|
||||
if (selectOptions.value.find((v) => v[valueField] === currentValue)) return;
|
||||
if (valueOption.value && valueOption.value[valueField] === currentValue) {
|
||||
return selectOptions.value.unshift(valueOption.value);
|
||||
}
|
||||
|
||||
const ret = await getByValue(currentValue);
|
||||
|
||||
if (ret) {
|
||||
selectOptions.value.unshift(ret);
|
||||
valueOption.value = ret;
|
||||
}
|
||||
}
|
||||
|
||||
type QuasarSelectUpdate = (
|
||||
callback: () => void,
|
||||
afterFn?: ((ref: QSelect) => void) | undefined,
|
||||
) => void;
|
||||
|
||||
function filter(value: string, update: QuasarSelectUpdate) {
|
||||
update(
|
||||
() => getOptions(value),
|
||||
(ref) => {
|
||||
if (!!value && ref.options && ref.options.length > 0) {
|
||||
ref.setOptionIndex(-1);
|
||||
ref.moveOptionSelection(1, true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return { getOptions, setFirstValue, getSelectedOption, filter };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue