Browse Source

feat(nc-gui): add form select type field option limit

pull/7729/head
Ramesh Mane 10 months ago
parent
commit
081e57370b
  1. 14
      packages/nc-gui/assets/nc-icons/eye-off.svg
  2. 8
      packages/nc-gui/assets/nc-icons/eye.svg
  3. 63
      packages/nc-gui/components/cell/User.vue
  4. 51
      packages/nc-gui/components/smartsheet/Form.vue
  5. 267
      packages/nc-gui/components/smartsheet/form/LimitOptions.vue
  6. 3
      packages/nc-gui/utils/iconUtils.ts

14
packages/nc-gui/assets/nc-icons/eye-off.svg

@ -0,0 +1,14 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_453_14719)">
<path
d="M6.59984 2.82676C7.05873 2.71935 7.52855 2.66566 7.99984 2.66676C12.6665 2.66676 15.3332 8.0001 15.3332 8.0001C14.9285 8.75717 14.4459 9.46992 13.8932 10.1268M9.41317 9.41343C9.23007 9.60993 9.00927 9.76754 8.76394 9.87685C8.51861 9.98616 8.25377 10.0449 7.98523 10.0497C7.71669 10.0544 7.44995 10.005 7.20091 9.90443C6.95188 9.80384 6.72565 9.65412 6.53573 9.4642C6.34582 9.27428 6.1961 9.04806 6.09551 8.79902C5.99492 8.54999 5.94552 8.28325 5.95026 8.0147C5.955 7.74616 6.01378 7.48133 6.12309 7.236C6.2324 6.99067 6.39001 6.76986 6.5865 6.58677M11.9598 11.9601C10.8202 12.8288 9.43258 13.31 7.99984 13.3334C3.33317 13.3334 0.666504 8.0001 0.666504 8.0001C1.49576 6.4547 2.64593 5.1045 4.03984 4.0401L11.9598 11.9601Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path d="M0.666504 0.666748L15.3332 15.3334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_453_14719">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

8
packages/nc-gui/assets/nc-icons/eye.svg

@ -1,4 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.666656 7.99996C0.666656 7.99996 3.33332 2.66663 7.99999 2.66663C12.6667 2.66663 15.3333 7.99996 15.3333 7.99996C15.3333 7.99996 12.6667 13.3333 7.99999 13.3333C3.33332 13.3333 0.666656 7.99996 0.666656 7.99996Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> d="M0.666504 8.00008C0.666504 8.00008 3.33317 2.66675 7.99984 2.66675C12.6665 2.66675 15.3332 8.00008 15.3332 8.00008C15.3332 8.00008 12.6665 13.3334 7.99984 13.3334C3.33317 13.3334 0.666504 8.00008 0.666504 8.00008Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 675 B

63
packages/nc-gui/components/cell/User.vue

@ -26,6 +26,12 @@ import {
} from '#imports' } from '#imports'
import MdiCloseCircle from '~icons/mdi/close-circle' import MdiCloseCircle from '~icons/mdi/close-circle'
interface LimitOptionsType {
id: string
order: number
show: boolean
}
interface Props { interface Props {
modelValue?: UserFieldRecordType[] | UserFieldRecordType | string | null modelValue?: UserFieldRecordType[] | UserFieldRecordType | string | null
rowIndex?: number rowIndex?: number
@ -78,16 +84,57 @@ const searchVal = ref<string | null>()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const options = computed<UserFieldRecordType[]>(() => { const options = computed<UserFieldRecordType[]>(() => {
let order = 1
const limitOptionsById =
((parseProp(column.value.meta)?.limitOptions || []).reduce((o: Record<string, LimitOptionsType>, f: LimitOptionsType) => {
if (f?.order !== undefined && order < f.order) {
order = f.order
}
return {
...o,
[f.id]: f,
}
}, {}) as Record<string, LimitOptionsType>) ?? {}
const collaborators: UserFieldRecordType[] = [] const collaborators: UserFieldRecordType[] = []
collaborators.push( if (
...(baseUsers.value?.map((user: any) => ({ !isEditColumn.value &&
id: user.id, isForm.value &&
email: user.email, parseProp(column.value.meta)?.isLimitOption &&
display_name: user.display_name, (parseProp(column.value.meta)?.limitOptions || []).length
deleted: user.deleted, ) {
})) || []), collaborators.push(
) ...(
baseUsers.value
.filter((user) => {
if (limitOptionsById[user.id]?.show !== undefined) {
return limitOptionsById[user.id]?.show
}
return false
})
?.map((user: any) => ({
id: user.id,
email: user.email,
display_name: user.display_name,
deleted: user.deleted,
order: user.id && limitOptionsById[user.id] ? limitOptionsById[user.id]?.order ?? user.order : order++,
})) || []
).sort((a, b) => a.order - b.order),
)
} else {
collaborators.push(
...(
baseUsers.value?.map((user: any) => ({
id: user.id,
email: user.email,
display_name: user.display_name,
deleted: user.deleted,
order: order++,
})) || []
).sort((a, b) => a.order - b.order),
)
}
return collaborators return collaborators
}) })

51
packages/nc-gui/components/smartsheet/Form.vue

@ -160,9 +160,7 @@ const imageCropperData = ref<{
cropFor: 'banner', cropFor: 'banner',
}) })
const focusLabel: VNodeRef = (el) => { const focusLabel = ref<HTMLTextAreaElement>()
return (el as HTMLInputElement)?.focus()
}
const searchQuery = ref('') const searchQuery = ref('')
@ -514,7 +512,15 @@ const handleOnUploadImage = (data: Record<string, any> = {}) => {
updateView() updateView()
} }
onClickOutside(draggableRef, () => { onClickOutside(draggableRef, (e) => {
if (
(e.target as HTMLElement)?.closest(
'.nc-dropdown-single-select-cell, .nc-dropdown-multi-select-cell, .nc-dropdown-user-select-cell',
)
) {
return
}
activeRow.value = '' activeRow.value = ''
isTabPressed.value = false isTabPressed.value = false
}) })
@ -570,6 +576,12 @@ watch(activeRow, (newValue) => {
} }
}) })
watch([focusLabel, activeRow], () => {
if (activeRow && focusLabel.value) {
focusLabel.value?.focus()
}
})
useEventListener( useEventListener(
formRef, formRef,
'focusout', 'focusout',
@ -991,7 +1003,7 @@ useEventListener(
<template v-if="activeRow === element.title"> <template v-if="activeRow === element.title">
<a-form-item class="my-0 !mb-2"> <a-form-item class="my-0 !mb-2">
<a-textarea <a-textarea
:ref="focusLabel" ref="focusLabel"
v-model:value="element.label" v-model:value="element.label"
:rows="1" :rows="1"
auto-size auto-size
@ -1118,20 +1130,25 @@ useEventListener(
</div> </div>
</div> </div>
<!-- Limit options --> <!-- Limit options -->
<div <div v-if="isSelectTypeCol(element.uidt)" class="px-3 py-2 border-1 border-gray-200 rounded-lg">
v-if="isSelectTypeCol(element.uidt)" <div class="flex items-start gap-3">
class="flex items-start gap-3 px-3 py-2 border-1 border-gray-200 rounded-lg" <a-switch
> v-model:checked="element.meta.isLimitOption"
<a-switch v-e="['a:form-view:field:limit-options']"
v-model:checked="element.meta.isLimitOption" size="small"
v-e="['a:form-view:field:limit-options']" @change="updateColMeta(element)"
size="small" />
@change="updateColMeta(element)"
/>
<div>
<div class="font-medium text-gray-800">{{ $t('labels.limitOptions') }}</div> <div class="font-medium text-gray-800">{{ $t('labels.limitOptions') }}</div>
</div>
<div class="pl-8 flex-1 max-w-[calc(100%_-_40px)]">
<div class="text-gray-500">{{ $t('labels.limitOptionsSubtext') }}.</div> <div class="text-gray-500">{{ $t('labels.limitOptionsSubtext') }}.</div>
<div v-if="element.meta.isLimitOption" class="mt-5"></div> <div v-if="element.meta.isLimitOption" class="mt-5 max-w-[80%]">
<LazySmartsheetFormLimitOptions
v-model:modelValue="element.meta.limitOptions"
:column="element"
@update:modelValue="updateColMeta(element)"
></LazySmartsheetFormLimitOptions>
</div>
</div> </div>
</div> </div>
</div> </div>

267
packages/nc-gui/components/smartsheet/form/LimitOptions.vue

@ -0,0 +1,267 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import tinycolor from 'tinycolor2'
import { UITypes, type ColumnType, type SelectOptionType, type SelectOptionsType, type UserFieldRecordType } from 'nocodb-sdk'
import { iconMap, MetaInj } from '#imports'
interface LimitOptionsType {
id: string
order: number
show: boolean
}
const props = defineProps<{
modelValue: LimitOptionsType[]
column: ColumnType
}>()
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj)!
const column = toRef(props, 'column')
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
const baseUsers = computed(() => (meta.value.base_id ? basesUser.value.get(meta.value.base_id) || [] : []))
const searchQuery = ref('')
const drag = ref(false)
const { t } = useI18n()
const vModel = computed({
get: () => {
let order = 1
const limitOptionsById =
(props.modelValue || []).reduce((o: Record<string, LimitOptionsType>, f: LimitOptionsType) => {
if (f?.order !== undefined && order < f.order) {
order = f.order
}
return {
...o,
[f.id]: f,
}
}, {} as Record<string, LimitOptionsType>) ?? {}
if (UITypes.User === column.value.uidt) {
const collaborators = ((baseUsers.value || []) as UserFieldRecordType[])
?.filter((user) => !user?.deleted)
?.map((user: any) => ({
id: user.id,
email: user.email,
display_name: user.display_name,
order: user.id && limitOptionsById[user.id] ? limitOptionsById[user.id]?.order ?? user.order : order++,
show:
user.id && limitOptionsById[user.id]
? limitOptionsById[user.id]?.show
: !(props.modelValue || []).length
? true
: false,
}))
if ((props.modelValue || []).length !== collaborators.length) {
emit(
'update:modelValue',
collaborators.map((o) => ({ id: o.id, order: o.order, show: o.show })),
)
}
return collaborators
} else if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(column.value.uidt as UITypes)) {
const updateModelValue = ((column.value.colOptions as SelectOptionsType)?.options || []).map((c) => {
return {
...c,
order: c.id && limitOptionsById[c.id] ? limitOptionsById[c.id]?.order ?? c.order : order++,
show: c.id && limitOptionsById[c.id] ? limitOptionsById[c.id]?.show : !(props.modelValue || []).length ? true : false,
} as SelectOptionType & { show?: boolean }
})
if ((props.modelValue || []).length !== ((column.value.colOptions as SelectOptionsType)?.options || []).length) {
emit(
'update:modelValue',
updateModelValue.map((o) => ({ id: o.id, order: o.order, show: o.show })),
)
}
return updateModelValue
}
return []
},
set: (val) => {
emit(
'update:modelValue',
val.map((o) => ({ id: o.id, order: o.order, show: o.show })),
)
},
})
const syncOptions = () => {
// set initial colOptions if not set
}
</script>
<template>
<div class="w-full h-full nc-col-option-select-option nc-form-scrollbar">
<div v-if="vModel.length > 12">
<a-input
v-model:value="searchQuery"
class="!h-9 !px-3 !py-1 !rounded-lg mb-2"
:placeholder="`Search option...`"
name="nc-form-field-limit-option-search-input"
data-testid="nc-form-field-limit-option-search-input"
>
<template #prefix>
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" />
</template>
<template #suffix>
<GeneralIcon
v-if="searchQuery.length > 0"
icon="close"
class="ml-2 h-4 w-4 text-gray-500 group-hover:text-black"
data-testid="nc-form-field-clear-search"
@click="searchQuery = ''"
/>
</template>
</a-input>
</div>
<Draggable
v-if="vModel.length"
:list="vModel"
item-key="id"
handle=".nc-child-draggable-icon"
class="rounded-lg border-1 border-gray-200 !max-h-[224px] overflow-y-auto nc-form-scrollbar"
@change="syncOptions"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element }">
<div
v-if="
column.uidt === UITypes.User
? (element?.display_name?.trim() || element?.email)?.toLowerCase().includes(searchQuery.toLowerCase())
: element.title?.toLowerCase().includes(searchQuery.toLowerCase())
"
:key="element.id"
class="w-full h-10 px-2 py-1.5 flex flex-row items-center gap-3 border-b-1 last:border-none border-gray-200"
:class="[
`nc-form-field-${column.title?.replaceAll(' ', '')}-limit-option-${element.title?.replaceAll(' ', '')}`,
`${element.show ? 'hover:bg-gray-50' : 'bg-gray-100'}`,
]"
:data-testid="`nc-form-field-${column.title?.replaceAll(' ', '')}-limit-option-${element.title?.replaceAll(' ', '')}`"
>
<component :is="iconMap.drag" class="flex-none cursor-move !h-4 !w-4 text-gray-600" />
<div
@click="
() => {
element.show = !element.show
vModel = vModel
}
"
>
<component
:is="element.show ? iconMap.eye : iconMap.eyeSlash"
class="flex-none cursor-pointer !h-4 !w-4 text-gray-600"
/>
</div>
<a-tag v-if="column.uidt === UITypes.User" class="rounded-tag max-w-[calc(100%_-_70px)] !pl-0" color="'#ccc'">
<span
:style="{
'color': tinycolor.isReadable('#ccc' || '#ccc', '#fff', { level: 'AA', size: 'large' })
? '#fff'
: tinycolor.mostReadable('#ccc' || '#ccc', ['#0b1d05', '#fff']).toHex8String(),
'font-size': '13px',
}"
class="flex items-stretch gap-2"
>
<div>
<GeneralUserIcon
size="auto"
:name="element.display_name?.trim() ? element.display_name?.trim() : ''"
:email="element.email"
class="!text-[0.65rem]"
/>
</div>
<NcTooltip class="truncate max-w-full" show-on-truncate-only>
<template #title>
{{ element.display_name?.trim() || element?.email }}
</template>
<span
class="text-ellipsis overflow-hidden"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ element.display_name?.trim() || element?.email }}
</span>
</NcTooltip>
</span>
</a-tag>
<a-tag v-else class="rounded-tag max-w-[calc(100%_-_70px)]" :color="element.color">
<span
:style="{
'color': tinycolor.isReadable(element.color || '#ccc', '#fff', { level: 'AA', size: 'large' })
? '#fff'
: tinycolor.mostReadable(element.color || '#ccc', ['#0b1d05', '#fff']).toHex8String(),
'font-size': '13px',
}"
>
<NcTooltip class="truncate max-w-full" show-on-truncate-only>
<template #title>
{{ element.title }}
</template>
<span
class="text-ellipsis overflow-hidden"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ element.title }}
</span>
</NcTooltip>
</span>
</a-tag>
</div>
</template>
<template v-if="!vModel.length" #footer><div class="px-0.5 py-2 text-gray-500 text-center">No options found</div></template>
<template
v-else-if="
vModel.length &&
searchQuery &&
!vModel?.filter((el) => {
return column.uidt === UITypes.User
? (element?.display_name?.trim() || element?.email)?.toLowerCase().includes(searchQuery.toLowerCase())
: element.title?.toLowerCase().includes(searchQuery.toLowerCase())
})?.length
"
#footer
>
<div class="px-0.5 py-2 text-gray-500 text-center">No options found with title `{{ searchQuery }}`</div>
</template>
</Draggable>
</div>
</template>
<style scoped>
.nc-form-scrollbar {
@apply scrollbar scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent;
&::-webkit-scrollbar-thumb:hover {
@apply !scrollbar-thumb-gray-300;
}
}
.rounded-tag {
@apply py-0 px-[12px] rounded-[12px];
}
:deep(.ant-tag) {
@apply rounded-tag my-[2px];
}
</style>

3
packages/nc-gui/utils/iconUtils.ts

@ -92,6 +92,7 @@ import Sort from '~icons/nc-icons/sort'
// NocoDB Icons // NocoDB Icons
import NcEye from '~icons/nc-icons/eye' import NcEye from '~icons/nc-icons/eye'
import NcEyeOff from '~icons/nc-icons/eye-off'
import NcStar from '~icons/nc-icons/star' import NcStar from '~icons/nc-icons/star'
import NcUnStar from '~icons/nc-icons/star-remove' import NcUnStar from '~icons/nc-icons/star-remove'
import NcSearch from '~icons/nc-icons/search' import NcSearch from '~icons/nc-icons/search'
@ -417,7 +418,7 @@ export const iconMap = {
calculator: h('span', { class: 'material-symbols' }, 'calculate'), calculator: h('span', { class: 'material-symbols' }, 'calculate'),
rollup: h('span', { class: 'material-symbols' }, 'group_work'), rollup: h('span', { class: 'material-symbols' }, 'group_work'),
eye: NcEye, eye: NcEye,
eyeSlash: h('span', { class: 'material-symbols' }, 'visibility_off'), eyeSlash: NcEyeOff,
expand: h('span', { class: 'material-symbols' }, 'open_in_full'), expand: h('span', { class: 'material-symbols' }, 'open_in_full'),
shrink: h('span', { class: 'material-symbols' }, 'close_fullscreen'), shrink: h('span', { class: 'material-symbols' }, 'close_fullscreen'),
check: NcCheck, check: NcCheck,

Loading…
Cancel
Save