|
|
|
@ -1,19 +1,23 @@
|
|
|
|
|
<script lang="ts" setup> |
|
|
|
|
import { message } from 'ant-design-vue' |
|
|
|
|
import tinycolor from 'tinycolor2' |
|
|
|
|
import type { Select as AntSelect } from 'ant-design-vue' |
|
|
|
|
import type { SelectOptionType, SelectOptionsType } from 'nocodb-sdk' |
|
|
|
|
import { |
|
|
|
|
ActiveCellInj, |
|
|
|
|
ColumnInj, |
|
|
|
|
EditModeInj, |
|
|
|
|
IsKanbanInj, |
|
|
|
|
ReadonlyInj, |
|
|
|
|
computed, |
|
|
|
|
enumColor, |
|
|
|
|
extractSdkResponseErrorMsg, |
|
|
|
|
h, |
|
|
|
|
inject, |
|
|
|
|
onMounted, |
|
|
|
|
reactive, |
|
|
|
|
ref, |
|
|
|
|
useEventListener, |
|
|
|
|
useMetas, |
|
|
|
|
useProject, |
|
|
|
|
useSelectedCellKeyupListener, |
|
|
|
|
watch, |
|
|
|
@ -39,6 +43,8 @@ const active = inject(ActiveCellInj, ref(false))
|
|
|
|
|
|
|
|
|
|
const editable = inject(EditModeInj, ref(false)) |
|
|
|
|
|
|
|
|
|
const isPublic = inject(IsPublicInj, ref(false)) |
|
|
|
|
|
|
|
|
|
const selectedIds = ref<string[]>([]) |
|
|
|
|
|
|
|
|
|
const aselect = ref<typeof AntSelect>() |
|
|
|
@ -47,7 +53,17 @@ const isOpen = ref(false)
|
|
|
|
|
|
|
|
|
|
const isKanban = inject(IsKanbanInj, ref(false)) |
|
|
|
|
|
|
|
|
|
const options = computed<SelectOptionType[]>(() => { |
|
|
|
|
const searchVal = ref<string | null>() |
|
|
|
|
|
|
|
|
|
const { $api } = useNuxtApp() |
|
|
|
|
|
|
|
|
|
const { getMeta } = useMetas() |
|
|
|
|
|
|
|
|
|
// a variable to keep newly created options value |
|
|
|
|
// temporary until it's add the option to column meta |
|
|
|
|
const tempSelectedOptsState = reactive<string[]>([]) |
|
|
|
|
|
|
|
|
|
const options = computed<(SelectOptionType & { value?: string })[]>(() => { |
|
|
|
|
if (column?.value.colOptions) { |
|
|
|
|
const opts = column.value.colOptions |
|
|
|
|
? (column.value.colOptions as SelectOptionsType).options.filter((el: SelectOptionType) => el.title !== '') || [] |
|
|
|
@ -55,21 +71,35 @@ const options = computed<SelectOptionType[]>(() => {
|
|
|
|
|
for (const op of opts.filter((el: SelectOptionType) => el.order === null)) { |
|
|
|
|
op.title = op.title?.replace(/^'/, '').replace(/'$/, '') |
|
|
|
|
} |
|
|
|
|
return opts |
|
|
|
|
return opts.map((o: SelectOptionType) => ({ ...o, value: o.title })) |
|
|
|
|
} |
|
|
|
|
return [] |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const isOptionMissing = computed(() => { |
|
|
|
|
return (options.value ?? []).every((op) => op.title !== searchVal.value) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const vModel = computed({ |
|
|
|
|
get: () => |
|
|
|
|
selectedIds.value.reduce((acc, id) => { |
|
|
|
|
const title = options.value.find((op) => op.id === id)?.title |
|
|
|
|
get: () => { |
|
|
|
|
const selected = selectedIds.value.reduce((acc, id) => { |
|
|
|
|
const title = (options.value.find((op) => op.id === id) || options.value.find((op) => op.title === id))?.title |
|
|
|
|
|
|
|
|
|
if (title) acc.push(title) |
|
|
|
|
|
|
|
|
|
return acc |
|
|
|
|
}, [] as string[]), |
|
|
|
|
set: (val) => emit('update:modelValue', val.length === 0 ? null : val.join(',')), |
|
|
|
|
}, [] as string[]) |
|
|
|
|
|
|
|
|
|
if (tempSelectedOptsState.length) selected.push(...tempSelectedOptsState) |
|
|
|
|
|
|
|
|
|
return selected |
|
|
|
|
}, |
|
|
|
|
set: (val) => { |
|
|
|
|
if (isOptionMissing.value && val.length && val[val.length - 1] === searchVal.value) { |
|
|
|
|
return addIfMissingAndSave() |
|
|
|
|
} |
|
|
|
|
emit('update:modelValue', val.length === 0 ? null : val.join(',')) |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const selectedTitles = computed(() => |
|
|
|
@ -97,9 +127,10 @@ const handleClose = (e: MouseEvent) => {
|
|
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
|
selectedIds.value = selectedTitles.value.flatMap((el) => { |
|
|
|
|
const item = options.value.find((op) => op.title === el)?.id |
|
|
|
|
if (item) { |
|
|
|
|
return [item] |
|
|
|
|
const item = options.value.find((op) => op.title === el) |
|
|
|
|
const itemIdOrTitle = item?.id || item?.title |
|
|
|
|
if (itemIdOrTitle) { |
|
|
|
|
return [itemIdOrTitle] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return [] |
|
|
|
@ -111,10 +142,10 @@ useEventListener(document, 'click', handleClose)
|
|
|
|
|
watch( |
|
|
|
|
() => modelValue, |
|
|
|
|
() => { |
|
|
|
|
selectedIds.value = selectedIds.value = selectedTitles.value.flatMap((el) => { |
|
|
|
|
const item = options.value.find((op) => op.title === el)?.id |
|
|
|
|
if (item) { |
|
|
|
|
return [item] |
|
|
|
|
selectedIds.value = selectedTitles.value.flatMap((el) => { |
|
|
|
|
const item = options.value.find((op) => op.title === el) |
|
|
|
|
if (item && (item.id || item.title)) { |
|
|
|
|
return [(item.id || item.title)!] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return [] |
|
|
|
@ -140,8 +171,65 @@ useSelectedCellKeyupListener(active, (e) => {
|
|
|
|
|
isOpen.value = true |
|
|
|
|
} |
|
|
|
|
break |
|
|
|
|
default: |
|
|
|
|
isOpen.value = true |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const activeOptCreateInProgress = ref(0) |
|
|
|
|
|
|
|
|
|
async function addIfMissingAndSave() { |
|
|
|
|
if (!searchVal.value || isPublic.value) return false |
|
|
|
|
try { |
|
|
|
|
tempSelectedOptsState.push(searchVal.value) |
|
|
|
|
const newOptValue = searchVal?.value |
|
|
|
|
searchVal.value = '' |
|
|
|
|
activeOptCreateInProgress.value++ |
|
|
|
|
if (newOptValue && !options.value.some((o) => o.title === newOptValue)) { |
|
|
|
|
const newOptions = [...options.value] |
|
|
|
|
newOptions.push({ |
|
|
|
|
title: newOptValue, |
|
|
|
|
value: newOptValue, |
|
|
|
|
color: enumColor.light[(options.value.length + 1) % enumColor.light.length], |
|
|
|
|
}) |
|
|
|
|
column.value.colOptions = { options: newOptions.map(({ value: _, ...rest }) => rest) } |
|
|
|
|
|
|
|
|
|
await $api.dbTableColumn.update((column.value as { fk_column_id?: string })?.fk_column_id || (column.value?.id as string), { |
|
|
|
|
...column.value, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
activeOptCreateInProgress.value-- |
|
|
|
|
if (!activeOptCreateInProgress.value) { |
|
|
|
|
await getMeta(column.value.fk_model_id!, true) |
|
|
|
|
vModel.value = [...vModel.value] |
|
|
|
|
tempSelectedOptsState.splice(0, tempSelectedOptsState.length) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
activeOptCreateInProgress.value-- |
|
|
|
|
} |
|
|
|
|
} catch (e) { |
|
|
|
|
// todo: handle error |
|
|
|
|
console.log(e) |
|
|
|
|
activeOptCreateInProgress.value-- |
|
|
|
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const search = () => { |
|
|
|
|
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const onTagClick = (e: Event, onClose: Function) => { |
|
|
|
|
// check clicked element is remove icon |
|
|
|
|
if ( |
|
|
|
|
(e.target as HTMLElement)?.classList.contains('ant-tag-close-icon') || |
|
|
|
|
(e.target as HTMLElement)?.closest('.ant-tag-close-icon') |
|
|
|
|
) { |
|
|
|
|
e.stopPropagation() |
|
|
|
|
onClose() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<template> |
|
|
|
@ -152,17 +240,20 @@ useSelectedCellKeyupListener(active, (e) => {
|
|
|
|
|
mode="multiple" |
|
|
|
|
class="w-full" |
|
|
|
|
:bordered="false" |
|
|
|
|
clear-icon |
|
|
|
|
:show-arrow="!readOnly" |
|
|
|
|
:show-search="false" |
|
|
|
|
:show-search="active || editable" |
|
|
|
|
:open="isOpen && (active || editable)" |
|
|
|
|
:disabled="readOnly" |
|
|
|
|
:class="{ '!ml-[-8px]': readOnly }" |
|
|
|
|
:dropdown-class-name="`nc-dropdown-multi-select-cell ${isOpen ? 'active' : ''}`" |
|
|
|
|
@keydown.enter.stop |
|
|
|
|
@search="search" |
|
|
|
|
@keydown.stop |
|
|
|
|
@click="isOpen = (active || editable) && !isOpen" |
|
|
|
|
> |
|
|
|
|
<a-select-option |
|
|
|
|
v-for="op of options" |
|
|
|
|
:key="op.id" |
|
|
|
|
:key="op.id || op.title" |
|
|
|
|
:value="op.title" |
|
|
|
|
:data-testid="`select-option-${column.title}-${rowIndex}`" |
|
|
|
|
@click.stop |
|
|
|
@ -182,14 +273,24 @@ useSelectedCellKeyupListener(active, (e) => {
|
|
|
|
|
</a-tag> |
|
|
|
|
</a-select-option> |
|
|
|
|
|
|
|
|
|
<a-select-option v-if="searchVal && isOptionMissing && !isPublic" :key="searchVal" :value="searchVal"> |
|
|
|
|
<div class="flex gap-2 text-gray-500 items-center h-full"> |
|
|
|
|
<MdiPlusThick class="min-w-4" /> |
|
|
|
|
<div class="text-xs whitespace-normal"> |
|
|
|
|
Create new option named <strong>{{ searchVal }}</strong> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</a-select-option> |
|
|
|
|
|
|
|
|
|
<template #tagRender="{ value: val, onClose }"> |
|
|
|
|
<a-tag |
|
|
|
|
v-if="options.find((el) => el.title === val)" |
|
|
|
|
class="rounded-tag" |
|
|
|
|
class="rounded-tag nc-selected-option" |
|
|
|
|
:style="{ display: 'flex', alignItems: 'center' }" |
|
|
|
|
:color="options.find((el) => el.title === val)?.color" |
|
|
|
|
:closable="(active || editable) && (vModel.length > 1 || !column?.rqd)" |
|
|
|
|
:close-icon="h(MdiCloseCircle, { class: ['ms-close-icon'] })" |
|
|
|
|
@click="onTagClick($event, onClose)" |
|
|
|
|
@close="onClose" |
|
|
|
|
> |
|
|
|
|
<span |
|
|
|
@ -255,6 +356,3 @@ useSelectedCellKeyupListener(active, (e) => {
|
|
|
|
|
@apply "flex overflow-hidden"; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
<!-- |
|
|
|
|
|
|
|
|
|
--> |
|
|
|
|