diff --git a/packages/nc-gui/assets/nc-icons/eye-off.svg b/packages/nc-gui/assets/nc-icons/eye-off.svg new file mode 100644 index 0000000000..196a4ed3a0 --- /dev/null +++ b/packages/nc-gui/assets/nc-icons/eye-off.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nc-gui/assets/nc-icons/eye.svg b/packages/nc-gui/assets/nc-icons/eye.svg index 3e19595fa4..597b5e291f 100644 --- a/packages/nc-gui/assets/nc-icons/eye.svg +++ b/packages/nc-gui/assets/nc-icons/eye.svg @@ -1,4 +1,8 @@ - - - + + + \ No newline at end of file diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss index f3330b1469..8ea1d047ca 100644 --- a/packages/nc-gui/assets/style.scss +++ b/packages/nc-gui/assets/style.scss @@ -749,3 +749,39 @@ svg.nc-cell-icon, svg.nc-virtual-cell-icon { @apply w-1em h-1em flex-none; font-size: 1rem; } + + +// For select type field list layout +.nc-field-layout-list { + @apply !flex !flex-col !items-start w-full !space-y-0.5 !max-w-full; + + .ant-checkbox-wrapper { + @apply !m-0 !h-9 !mr-0 !flex !items-center w-full !max-w-full pl-2 rounded-lg hover:bg-gray-100; + + &:hover { + .ant-checkbox-checked:after { + @apply !rounded; + } + } + .ant-checkbox { + @apply !top-0; + + & + span { + @apply !flex !pl-4 max-w-[calc(100%_-_16px)]; + } + .ant-checkbox-checked:after, + .ant-checkbox-inner { + @apply !rounded; + } + } + } + .ant-radio-wrapper { + @apply !m-0 !h-9 !mr-0 !flex !items-center w-full !max-w-full pl-2 rounded-lg hover:bg-gray-100; + .ant-radio { + @apply !top-0; + } + .ant-radio + span { + @apply !flex !pl-4 max-w-[calc(100%_-_16px)]; + } + } +} \ No newline at end of file diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index 34080cd190..5c5a205f2a 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -3,6 +3,7 @@ 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 type { FormFieldsLimitOptionsType } from '~/lib' import { ActiveCellInj, ColumnInj, @@ -95,7 +96,43 @@ const options = computed<(SelectOptionType & { value?: string })[]>(() => { for (const op of opts.filter((el: SelectOptionType) => el.order === null)) { op.title = op.title?.replace(/^'/, '').replace(/'$/, '') } - return opts.map((o: SelectOptionType) => ({ ...o, value: o.title })) + let order = 1 + const limitOptionsById = + ((parseProp(column.value.meta)?.limitOptions || []).reduce( + (o: Record, f: FormFieldsLimitOptionsType) => { + if (order < (f?.order ?? 0)) { + order = f.order + } + return { + ...o, + [f.id]: f, + } + }, + {}, + ) as Record) ?? {} + + if ( + !isEditColumn.value && + isForm.value && + parseProp(column.value.meta)?.isLimitOption && + (parseProp(column.value.meta)?.limitOptions || []).length + ) { + return opts + .filter((o: SelectOptionType) => { + if (limitOptionsById[o.id!]?.show !== undefined) { + return limitOptionsById[o.id!]?.show + } + return false + }) + .map((o) => ({ + ...o, + value: o.title, + order: o.id && limitOptionsById[o.id] ? limitOptionsById[o.id]?.order : order++, + })) + .sort((a, b) => a.order - b.order) + } else { + return opts.map((o: SelectOptionType) => ({ ...o, value: o.title })) + } } return [] }) @@ -353,154 +390,194 @@ const onFocus = () => { + + + -
- -
- {{ $t('msg.selectOption.createNewOptionNamed') }} {{ searchVal }} -
-
- - - + - + + + + + {{ op.title }} + + + + + + + +
+ +
+ {{ $t('msg.selectOption.createNewOptionNamed') }} {{ searchVal }} +
+
+
+ + -
+ + {{ val }} + + + + + diff --git a/packages/nc-gui/components/cell/SingleSelect.vue b/packages/nc-gui/components/cell/SingleSelect.vue index 1ca601c92f..57ccd9ee38 100644 --- a/packages/nc-gui/components/cell/SingleSelect.vue +++ b/packages/nc-gui/components/cell/SingleSelect.vue @@ -3,6 +3,7 @@ import { message } from 'ant-design-vue' import tinycolor from 'tinycolor2' import type { Select as AntSelect } from 'ant-design-vue' import type { SelectOptionType } from 'nocodb-sdk' +import type { FormFieldsLimitOptionsType } from '~/lib' import { ActiveCellInj, ColumnInj, @@ -88,7 +89,44 @@ const options = computed<(SelectOptionType & { value: string })[]>(() => { for (const op of opts.filter((el: any) => el.order === null)) { op.title = op.title.replace(/^'/, '').replace(/'$/, '') } - return opts.map((o: any) => ({ ...o, value: o.title })) + + let order = 1 + const limitOptionsById = + ((parseProp(column.value.meta)?.limitOptions || []).reduce( + (o: Record, f: FormFieldsLimitOptionsType) => { + if (order < (f?.order ?? 0)) { + order = f.order + } + return { + ...o, + [f.id]: f, + } + }, + {}, + ) as Record) ?? {} + + if ( + !isEditColumn.value && + isForm.value && + parseProp(column.value.meta)?.isLimitOption && + (parseProp(column.value.meta)?.limitOptions || []).length + ) { + return opts + .filter((o: SelectOptionType & { value: string }) => { + if (limitOptionsById[o.id]?.show !== undefined) { + return limitOptionsById[o.id]?.show + } + return false + }) + .map((o: any) => ({ + ...o, + value: o.title, + order: o.id && limitOptionsById[o.id] ? limitOptionsById[o.id]?.order : order++, + })) + .sort((a, b) => a.order - b.order) + } else { + return opts.map((o: any) => ({ ...o, value: o.title })) + } } return [] }) @@ -272,82 +310,70 @@ const onFocus = () => { diff --git a/packages/nc-gui/components/cell/User.vue b/packages/nc-gui/components/cell/User.vue index 2b89d774b5..3331110136 100644 --- a/packages/nc-gui/components/cell/User.vue +++ b/packages/nc-gui/components/cell/User.vue @@ -1,8 +1,10 @@ + + + + diff --git a/packages/nc-gui/composables/useSharedFormViewStore.ts b/packages/nc-gui/composables/useSharedFormViewStore.ts index e60f089657..4be41faebc 100644 --- a/packages/nc-gui/composables/useSharedFormViewStore.ts +++ b/packages/nc-gui/composables/useSharedFormViewStore.ts @@ -109,6 +109,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share columns.value = viewMeta.model?.columns?.map((c) => ({ ...c, + meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) }, description: fieldById[c.id].description, })) diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 2b7a35c5b7..853f28a53e 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -297,7 +297,7 @@ export function useViewData( fk_column_id: c.id, fk_view_id: viewMeta.value?.id, ...(fieldById[c.id!] ? fieldById[c.id!] : {}), - meta: { ...parseProp(c.meta), ...parseProp(fieldById[c.id!]?.meta) }, // TODO: discuss with @pranav + meta: { ...parseProp(fieldById[c.id!]?.meta), ...parseProp(c.meta) }, // TODO: discuss with @pranav order: (fieldById[c.id!] && fieldById[c.id!].order) || order++, id: fieldById[c.id!] && fieldById[c.id!].id, })) diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index 8d8dacb99b..1f56a52179 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -195,7 +195,9 @@ "restore": "Restore", "replace": "Replace", "banner": "Banner", - "logo": "Logo" + "logo": "Logo", + "dropdown":"Dropdown", + "list":"List" }, "objects": { "day": "Day", @@ -425,7 +427,9 @@ "setNull": "Set NULL", "setDefault": "Set Default" }, - "selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here" + "selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here", + "noOptionsFound": "No options found" + }, "labels": { "selectYear": "Select Year", @@ -692,7 +696,10 @@ "backgroundColor":"Background Color", "hideNocodbBranding":"Hide NocoDB Branding", "showOnConditions": "Show on condtions", - "showFieldOnConditionsMet":"Shows field only when conditions are met" + "showFieldOnConditionsMet":"Shows field only when conditions are met", + "limitOptions": "Limit options", + "limitOptionsSubtext": "Limit options visible to users by selecting available options", + "clearSelection": "Clear selection" }, "activity": { "noRange": "Calendar view requires a date range", @@ -1002,7 +1009,8 @@ "noTokenCreated": "No API Tokens created", "noTokenCreatedLabel": "Looks like you haven’t generated any API tokens yet.", "inviteYourTeam": "Invite your team", - "inviteYourTeamLabel": "Fast track your projects by collaborating on them with your team!" + "inviteYourTeamLabel": "Fast track your projects by collaborating on them with your team!", + "searchOptions": "Search options" }, "msg": { "clickToCopyFieldId": "Click to copy Field Id", diff --git a/packages/nc-gui/lib/types.ts b/packages/nc-gui/lib/types.ts index 2c4f5c5222..01b9e168b6 100644 --- a/packages/nc-gui/lib/types.ts +++ b/packages/nc-gui/lib/types.ts @@ -198,6 +198,12 @@ interface UsersSortType { type CommandPaletteType = 'cmd-k' | 'cmd-j' | 'cmd-l' +interface FormFieldsLimitOptionsType { + id: string + order: number + show: boolean +} + export type { User, ProjectMetaInfo, @@ -226,4 +232,5 @@ export type { UsersSortType, CommandPaletteType, CalendarRangeType, + FormFieldsLimitOptionsType, } diff --git a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue index 13a91b6536..32f8ba8ba7 100644 --- a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue +++ b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue @@ -66,8 +66,16 @@ p { } .nc-form-view { + .nc-data-cell { + @apply !border-none rounded-none; + + &:focus-within { + @apply !border-none; + } + } + .nc-cell { - @apply bg-white dark:bg-slate-500; + @apply bg-white dark:bg-slate-500 appearance-none; &.nc-cell-checkbox { @apply color-transition !border-0; @@ -89,7 +97,18 @@ p { @apply bg-white dark:bg-slate-500; &.nc-input { - @apply w-full rounded p-2 min-h-[40px] flex items-center; + @apply w-full; + + &:not(.layout-list) { + @apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500; + + & > div { + @apply !bg-transparent; + } + } + &.layout-list { + @apply h-auto !pl-0 !py-1 !bg-transparent !dark:bg-none; + } .duration-cell-wrapper { @apply w-full; @@ -105,8 +124,7 @@ p { input, textarea, - &.nc-virtual-cell, - > div { + &.nc-virtual-cell { @apply bg-white dark:(bg-slate-500 text-white); .ant-btn { @@ -117,6 +135,18 @@ p { @apply dark:(bg-slate-700 text-white); } } + &:not(.layout-list) > div { + @apply bg-white dark:(bg-slate-500 text-white); + } + &.layout-list > div { + .ant-btn { + @apply dark:(bg-slate-300); + } + + .chip { + @apply dark:(bg-slate-700 text-white); + } + } &.nc-cell-longtext { @apply p-0 h-auto; @@ -132,7 +162,7 @@ p { } } &:not(.nc-cell-longtext) { - @apply px-2 py-2; + @apply p-2; } textarea { diff --git a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue index 57f239400d..6b36f969d1 100644 --- a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue +++ b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue @@ -176,7 +176,10 @@ const onDecode = async (scannedCodeValue: string) => { v-model="formState[field.title]" class="nc-input truncate" :data-testid="`nc-form-input-cell-${field.label || field.title}`" - :class="`nc-form-input-${field.title?.replaceAll(' ', '')}`" + :class="[ + `nc-form-input-${field.title?.replaceAll(' ', '')}`, + { 'layout-list': parseProp(field?.meta)?.isList }, + ]" :column="field" edit-enabled /> diff --git a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue index cc3e0f27a5..f749801aa9 100644 --- a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue +++ b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue @@ -292,6 +292,7 @@ onMounted(() => { v-else v-model="formState[field.title]" class="nc-input h-auto" + :class="parseProp(field?.meta)?.isList ? 'layout-list' : ''" :data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`" :column="field" edit-enabled @@ -481,33 +482,6 @@ onMounted(() => { @apply flex items-center justify-center gap-2 text-left; } - .nc-input { - @apply appearance-none w-full !bg-white !rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500; - &.nc-cell-rating, - &.nc-cell-geodata { - @apply !py-1; - } - - :deep(input) { - @apply !px-1; - } - &.nc-cell-longtext { - @apply p-0 h-auto overflow-hidden; - } - &:not(.nc-cell-longtext) { - @apply px-2 py-2; - :deep(textarea) { - @apply !p-2; - } - } - - &.nc-cell-checkbox { - > * { - @apply justify-center flex items-center; - } - } - } - .nc-form-data-cell.nc-data-cell { @apply !border-none rounded-none; diff --git a/packages/nc-gui/utils/iconUtils.ts b/packages/nc-gui/utils/iconUtils.ts index 695e5877b6..9a53f2fe17 100644 --- a/packages/nc-gui/utils/iconUtils.ts +++ b/packages/nc-gui/utils/iconUtils.ts @@ -92,6 +92,7 @@ import Sort from '~icons/nc-icons/sort' // NocoDB Icons import NcEye from '~icons/nc-icons/eye' +import NcEyeOff from '~icons/nc-icons/eye-off' import NcStar from '~icons/nc-icons/star' import NcUnStar from '~icons/nc-icons/star-remove' import NcSearch from '~icons/nc-icons/search' @@ -417,7 +418,7 @@ export const iconMap = { calculator: h('span', { class: 'material-symbols' }, 'calculate'), rollup: h('span', { class: 'material-symbols' }, 'group_work'), eye: NcEye, - eyeSlash: h('span', { class: 'material-symbols' }, 'visibility_off'), + eyeSlash: NcEyeOff, expand: h('span', { class: 'material-symbols' }, 'open_in_full'), shrink: h('span', { class: 'material-symbols' }, 'close_fullscreen'), check: NcCheck, diff --git a/packages/nc-gui/utils/parseUtils.ts b/packages/nc-gui/utils/parseUtils.ts index ae567ab3b9..7e89c6dd24 100644 --- a/packages/nc-gui/utils/parseUtils.ts +++ b/packages/nc-gui/utils/parseUtils.ts @@ -1,7 +1,7 @@ export function parseProp(v: any): any { if (!v) return {} try { - return typeof v === 'string' ? JSON.parse(v) : v + return typeof v === 'string' ? JSON.parse(v) ?? {} : v } catch { return {} } diff --git a/packages/nocodb-sdk/src/lib/UITypes.ts b/packages/nocodb-sdk/src/lib/UITypes.ts index d5fa23214c..f7895f0cac 100644 --- a/packages/nocodb-sdk/src/lib/UITypes.ts +++ b/packages/nocodb-sdk/src/lib/UITypes.ts @@ -202,4 +202,11 @@ export const getEquivalentUIType = ({ } }; +export const isSelectTypeCol = ( + colOrUidt: ColumnType | { uidt: UITypes | string } | UITypes | string +) => { + return [UITypes.SingleSelect, UITypes.MultiSelect, UITypes.User].includes( + (typeof colOrUidt === 'object' ? colOrUidt?.uidt : colOrUidt) + ); +}; export default UITypes; diff --git a/packages/nocodb-sdk/src/lib/index.ts b/packages/nocodb-sdk/src/lib/index.ts index e06f405e5d..2a38f678de 100644 --- a/packages/nocodb-sdk/src/lib/index.ts +++ b/packages/nocodb-sdk/src/lib/index.ts @@ -18,6 +18,7 @@ export { isCreatedOrLastModifiedByCol, isHiddenCol, getEquivalentUIType, + isSelectTypeCol, } from '~/lib/UITypes'; export { default as CustomAPI, FileType } from '~/lib/CustomAPI'; export { default as TemplateGenerator } from '~/lib/TemplateGenerator';