@ -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 v a r i a b l e t o k e e p n e w l y c r e a t e d o p t i o n s v a l u e
/ / t e m p o r a r y u n t i l i t ' s a d d t h e o p t i o n t o c o l u m n m e t a
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 = selected Titles . 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 ) {
/ / t o d o : h a n d l e e r r o r
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 ) => {
/ / c h e c k c l i c k e d e l e m e n t i s r e m o v e i c o n
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 - t a g >
< / a - s e l e c t - o p t i o n >
< 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 - s e l e c t - o p t i o n >
< 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 >
<!--
-- >