mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 05:35:41 +00:00
999 lines
34 KiB
Vue
999 lines
34 KiB
Vue
<script lang="ts" setup>
|
||
import type { ColumnType, GalleryType, KanbanType, LookupType } from 'nocodb-sdk'
|
||
import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
|
||
import Draggable from 'vuedraggable'
|
||
|
||
import type { SelectProps } from 'ant-design-vue'
|
||
|
||
const activeView = inject(ActiveViewInj, ref())
|
||
|
||
const meta = inject(MetaInj, ref())
|
||
|
||
const reloadViewDataHook = inject(ReloadViewDataHookInj, undefined)!
|
||
|
||
const { isMobileMode } = useGlobal()
|
||
|
||
const { isUIAllowed } = useRoles()
|
||
|
||
const isLocked = inject(IsLockedInj, ref(false))
|
||
|
||
const isPublic = inject(IsPublicInj, ref(false))
|
||
|
||
const readOnly = inject(ReadonlyInj, ref(false))
|
||
|
||
const isToolbarIconMode = inject(
|
||
IsToolbarIconMode,
|
||
computed(() => false),
|
||
)
|
||
|
||
const { $e } = useNuxtApp()
|
||
|
||
const { t } = useI18n()
|
||
|
||
const { metas, getMeta, getMetaByKey } = useMetas()
|
||
|
||
const {
|
||
showSystemFields,
|
||
fields,
|
||
filteredFieldList,
|
||
hasViewFieldDataEditPermission,
|
||
searchBasisIdMap,
|
||
numberOfHiddenFields,
|
||
filterQuery,
|
||
showAll,
|
||
hideAll,
|
||
saveOrUpdate,
|
||
metaColumnById,
|
||
loadViewColumns,
|
||
toggleFieldStyles,
|
||
toggleFieldVisibility,
|
||
isLocalMode,
|
||
} = useViewColumnsOrThrow()
|
||
|
||
const { eventBus, isDefaultView, isSqlView, isViewOperationsAllowed } = useSmartsheetStoreOrThrow()
|
||
|
||
const isFieldsMenuReadOnly = computed(() => {
|
||
return isLocked.value || !isViewOperationsAllowed.value || (isLocalMode.value && hasViewFieldDataEditPermission.value)
|
||
})
|
||
|
||
const isAddingColumnAllowed = computed(() => !readOnly.value && isUIAllowed('fieldAdd') && !isSqlView.value)
|
||
|
||
const { addUndo, defineViewScope } = useUndoRedo()
|
||
|
||
const viewStore = useViewsStore()
|
||
|
||
const { updateViewMeta } = viewStore
|
||
|
||
const eventBusHandler = async (event: SmartsheetStoreEvents, payload?: any) => {
|
||
if (event === SmartsheetStoreEvents.FIELD_RELOAD) {
|
||
try {
|
||
await loadViewColumns()
|
||
} finally {
|
||
payload?.callback?.()
|
||
}
|
||
} else if (event === SmartsheetStoreEvents.MAPPED_BY_COLUMN_CHANGE) {
|
||
loadViewColumns()
|
||
}
|
||
}
|
||
|
||
eventBus.on(eventBusHandler)
|
||
|
||
onBeforeUnmount(() => {
|
||
eventBus.off(eventBusHandler)
|
||
})
|
||
|
||
const gridDisplayValueField = computed(() => {
|
||
if (activeView.value?.type !== ViewTypes.GRID && activeView.value?.type !== ViewTypes.CALENDAR) return null
|
||
|
||
const pvCol = Object.values(metaColumnById.value)?.find((col) => col?.pv)
|
||
|
||
return filteredFieldList.value?.find((field) => field.fk_column_id === pvCol?.id)
|
||
})
|
||
|
||
const localFilteredFieldList = computed(() => {
|
||
return filteredFieldList.value.filter((el) =>
|
||
activeView.value?.type !== ViewTypes.CALENDAR ? el !== gridDisplayValueField.value : true,
|
||
)
|
||
})
|
||
|
||
const onMove = async (_event: { moved: { newIndex: number; oldIndex: number } }, undo = false) => {
|
||
try {
|
||
// todo : sync with server
|
||
if (!fields.value) return
|
||
|
||
if (!undo) {
|
||
addUndo({
|
||
undo: {
|
||
fn: () => {
|
||
if (!fields.value) return
|
||
const temp = fields.value[_event.moved.newIndex]
|
||
fields.value[_event.moved.newIndex] = fields.value[_event.moved.oldIndex]
|
||
fields.value[_event.moved.oldIndex] = temp
|
||
onMove(
|
||
{
|
||
moved: {
|
||
newIndex: _event.moved.oldIndex,
|
||
oldIndex: _event.moved.newIndex,
|
||
},
|
||
},
|
||
true,
|
||
)
|
||
},
|
||
args: [],
|
||
},
|
||
redo: {
|
||
fn: () => {
|
||
if (!fields.value) return
|
||
const temp = fields.value[_event.moved.oldIndex]
|
||
fields.value[_event.moved.oldIndex] = fields.value[_event.moved.newIndex]
|
||
fields.value[_event.moved.newIndex] = temp
|
||
onMove(_event, true)
|
||
},
|
||
args: [],
|
||
},
|
||
scope: defineViewScope({ view: activeView.value }),
|
||
})
|
||
}
|
||
|
||
if (fields.value.length < 2) return
|
||
|
||
const movedField = fields.value[_event.moved.newIndex]
|
||
if (!movedField) return
|
||
let newOrder
|
||
|
||
if (_event.moved.newIndex === 0) {
|
||
// Moving to first position
|
||
const nextField = fields.value[1]
|
||
newOrder = nextField.order / 2 // Half of next field's order
|
||
} else if (_event.moved.newIndex === fields.value.length - 1) {
|
||
// Moving to last position
|
||
const prevField = fields.value[fields.value.length - 2]
|
||
newOrder = prevField.order + 1000 // Add buffer to previous field's order
|
||
} else {
|
||
// Moving somewhere in the middle
|
||
const prevField = fields.value[_event.moved.newIndex - 1]
|
||
const nextField = fields.value[_event.moved.newIndex + 1]
|
||
newOrder = (prevField.order + nextField.order) / 2 // Average between neighbors
|
||
}
|
||
|
||
// Update only the moved field
|
||
movedField.order = newOrder
|
||
await saveOrUpdate(movedField, _event.moved.newIndex, true, !!isDefaultView.value)
|
||
|
||
await loadViewColumns()
|
||
reloadViewDataHook.trigger()
|
||
$e('a:fields:reorder')
|
||
} catch (e) {
|
||
message.error(await extractSdkResponseErrorMsg(e))
|
||
}
|
||
}
|
||
|
||
const coverOptions = ref<SelectProps['options']>([])
|
||
|
||
const updateCoverImage = async (val?: string | null) => {
|
||
if (
|
||
(activeView.value?.type === ViewTypes.GALLERY ||
|
||
activeView.value?.type === ViewTypes.KANBAN ||
|
||
activeView.value?.type === ViewTypes.CALENDAR) &&
|
||
activeView.value?.id &&
|
||
activeView.value?.view
|
||
) {
|
||
await updateViewMeta(activeView.value?.id, activeView.value?.type, {
|
||
fk_cover_image_col_id: val,
|
||
})
|
||
|
||
// Load data only if the view column is hidden to fetch cover image column data in records.
|
||
if (val && !fields.value?.find((f) => f.fk_column_id === val)?.show) {
|
||
await reloadViewDataHook?.trigger({
|
||
shouldShowLoading: false,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
const coverImageColumnId = computed({
|
||
get: () => {
|
||
const fk_cover_image_col_id =
|
||
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) && activeView.value?.view
|
||
? (activeView.value?.view as GalleryType | KanbanType).fk_cover_image_col_id
|
||
: undefined
|
||
|
||
// check if `fk_cover_image_col_id` is in `coverOptions`
|
||
// e.g. in share view, users may not share the cover image column
|
||
if (coverOptions.value?.find((o) => o.value === fk_cover_image_col_id)) return fk_cover_image_col_id
|
||
// set to `No Image` if fk_cover_image_col_id is null else undefiend (This will help to change value to no image for user)
|
||
return fk_cover_image_col_id === null ? null : undefined
|
||
},
|
||
set: async (val) => {
|
||
if (val !== coverImageColumnId.value) {
|
||
addUndo({
|
||
undo: {
|
||
fn: await updateCoverImage,
|
||
args: [coverImageColumnId.value],
|
||
},
|
||
redo: {
|
||
fn: await updateCoverImage,
|
||
args: [val],
|
||
},
|
||
scope: defineViewScope({ view: activeView.value }),
|
||
})
|
||
|
||
await updateCoverImage(val)
|
||
}
|
||
},
|
||
})
|
||
|
||
const updateCoverImageObjectFit = async (val: string) => {
|
||
if (
|
||
![ViewTypes.GALLERY, ViewTypes.KANBAN].includes(activeView.value?.type as ViewTypes) ||
|
||
!activeView.value?.id ||
|
||
!activeView.value?.view
|
||
) {
|
||
return
|
||
}
|
||
|
||
const payload = {
|
||
...parseProp((activeView.value?.view as GalleryType | KanbanType)?.meta),
|
||
fk_cover_image_object_fit: val,
|
||
}
|
||
|
||
await updateViewMeta(activeView.value?.id, activeView.value?.type, {
|
||
meta: payload,
|
||
})
|
||
}
|
||
|
||
const coverImageObjectFitOptions = [
|
||
{ value: CoverImageObjectFit.FIT, label: t('labels.fitImage') },
|
||
{ value: CoverImageObjectFit.COVER, label: t('labels.coverImageArea') },
|
||
]
|
||
|
||
const coverImageObjectFitDropdown = ref<{
|
||
isOpen: boolean
|
||
isSaving: keyof typeof CoverImageObjectFit | null
|
||
}>({
|
||
isOpen: false,
|
||
isSaving: null,
|
||
})
|
||
|
||
const coverImageObjectFit = computed({
|
||
get: () => {
|
||
return [ViewTypes.GALLERY, ViewTypes.KANBAN].includes(activeView.value?.type as ViewTypes) && activeView.value?.view
|
||
? parseProp(activeView.value?.view?.meta)?.fk_cover_image_object_fit || CoverImageObjectFit.FIT
|
||
: undefined
|
||
},
|
||
set: async (val) => {
|
||
if (val !== coverImageObjectFit.value) {
|
||
coverImageObjectFitDropdown.value.isSaving = val
|
||
|
||
addUndo({
|
||
undo: {
|
||
fn: updateCoverImageObjectFit,
|
||
args: [coverImageObjectFit.value],
|
||
},
|
||
redo: {
|
||
fn: updateCoverImageObjectFit,
|
||
args: [val],
|
||
},
|
||
scope: defineViewScope({ view: activeView.value }),
|
||
})
|
||
|
||
await updateCoverImageObjectFit(val)
|
||
}
|
||
coverImageObjectFitDropdown.value.isSaving = null
|
||
coverImageObjectFitDropdown.value.isOpen = false
|
||
},
|
||
})
|
||
|
||
const onShowAll = async () => {
|
||
addUndo({
|
||
undo: {
|
||
fn: async () => {
|
||
await hideAll()
|
||
},
|
||
args: [],
|
||
},
|
||
redo: {
|
||
fn: async () => {
|
||
await showAll()
|
||
},
|
||
args: [],
|
||
},
|
||
scope: defineViewScope({ view: activeView.value }),
|
||
})
|
||
await showAll()
|
||
}
|
||
|
||
const onHideAll = async () => {
|
||
addUndo({
|
||
undo: {
|
||
fn: async () => {
|
||
await showAll()
|
||
},
|
||
args: [],
|
||
},
|
||
redo: {
|
||
fn: async () => {
|
||
await hideAll()
|
||
},
|
||
args: [],
|
||
},
|
||
scope: defineViewScope({ view: activeView.value }),
|
||
})
|
||
await hideAll()
|
||
}
|
||
|
||
const visibleFields = computed(
|
||
() =>
|
||
fields.value?.filter((field: Field) => {
|
||
if (!field.initialShow && isLocalMode.value && !hasViewFieldDataEditPermission.value) {
|
||
return false
|
||
}
|
||
|
||
if (metaColumnById?.value?.[field.fk_column_id!]?.pv) {
|
||
return false
|
||
}
|
||
|
||
// hide system columns if not enabled
|
||
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}) || [],
|
||
)
|
||
|
||
const isLoadingShowAllColumns = ref(false)
|
||
|
||
const isDisabledShowAllColumns = computed(() => {
|
||
return (
|
||
!searchCompare(
|
||
fields.value?.map((f) => f.title),
|
||
filterQuery.value,
|
||
) || isFieldsMenuReadOnly.value
|
||
)
|
||
})
|
||
|
||
const showAllColumns = computed({
|
||
get: () => {
|
||
return visibleFields.value?.every((field) => field?.show)
|
||
},
|
||
set: async (val) => {
|
||
isLoadingShowAllColumns.value = true
|
||
try {
|
||
if (val) {
|
||
await onShowAll()
|
||
} else {
|
||
await onHideAll()
|
||
}
|
||
} finally {
|
||
isLoadingShowAllColumns.value = false
|
||
}
|
||
},
|
||
})
|
||
|
||
const open = ref(false)
|
||
|
||
const showSystemField = computed({
|
||
get: () => {
|
||
return showSystemFields.value
|
||
},
|
||
set: (val) => {
|
||
addUndo({
|
||
undo: {
|
||
fn: (v: boolean) => {
|
||
showSystemFields.value = !v
|
||
},
|
||
args: [val],
|
||
},
|
||
redo: {
|
||
fn: (v: boolean) => {
|
||
showSystemFields.value = v
|
||
},
|
||
args: [val],
|
||
},
|
||
scope: defineViewScope({ view: activeView.value }),
|
||
})
|
||
showSystemFields.value = val
|
||
},
|
||
})
|
||
|
||
const isDragging = ref<boolean>(false)
|
||
|
||
const fieldsMenuSearchRef = ref<HTMLInputElement>()
|
||
|
||
watch(open, (value) => {
|
||
if (!value) return
|
||
|
||
filterQuery.value = ''
|
||
setTimeout(() => {
|
||
fieldsMenuSearchRef.value?.focus()
|
||
}, 100)
|
||
})
|
||
|
||
watch(
|
||
fields,
|
||
async (newValue) => {
|
||
if (!newValue || isPublic.value || ![ViewTypes.GALLERY, ViewTypes.KANBAN].includes(activeView.value?.type as ViewTypes))
|
||
return
|
||
|
||
const filterFields =
|
||
newValue
|
||
.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id]?.uidt === UITypes.Attachment)
|
||
.map((field) => {
|
||
return {
|
||
value: field.fk_column_id,
|
||
label: field.title,
|
||
}
|
||
}) ?? []
|
||
|
||
coverOptions.value = [{ value: null, label: t('labels.noImage') }, ...filterFields]
|
||
|
||
const lookupColumns = newValue
|
||
.filter((f) => f.fk_column_id && metaColumnById.value[f.fk_column_id]?.uidt === UITypes.Lookup)
|
||
.map((f) => metaColumnById.value[f.fk_column_id!])
|
||
|
||
const attLookupColumnIds: Set<string> = new Set()
|
||
|
||
const loadLookupMeta = async (originalCol: ColumnType, column: ColumnType, metaId?: string): Promise<void> => {
|
||
const relationColumn =
|
||
metaId || meta.value?.id
|
||
? getMetaByKey(meta.value?.base_id, metaId || meta.value?.id)?.columns?.find(
|
||
(c: ColumnType) => c.id === (column?.colOptions as LookupType)?.fk_relation_column_id,
|
||
)
|
||
: undefined
|
||
|
||
if (relationColumn?.colOptions?.fk_related_model_id) {
|
||
const relatedBaseId = (relationColumn.colOptions as any)?.fk_related_base_id || meta.value?.base_id
|
||
await getMeta(relatedBaseId as string, relationColumn.colOptions.fk_related_model_id!)
|
||
|
||
const lookupColumn = getMetaByKey(relatedBaseId, relationColumn.colOptions.fk_related_model_id)?.columns?.find(
|
||
(c: any) => c.id === (column?.colOptions as LookupType)?.fk_lookup_column_id,
|
||
) as ColumnType | undefined
|
||
|
||
if (lookupColumn && isAttachment(lookupColumn)) {
|
||
attLookupColumnIds.add(originalCol.id)
|
||
} else if (lookupColumn && lookupColumn?.uidt === UITypes.Lookup) {
|
||
await loadLookupMeta(originalCol, lookupColumn, relationColumn.colOptions.fk_related_model_id)
|
||
}
|
||
}
|
||
}
|
||
|
||
await Promise.allSettled(lookupColumns.map((col) => loadLookupMeta(col, col)))
|
||
|
||
const lookupAttColumns = lookupColumns
|
||
.filter((column) => attLookupColumnIds.has(column?.id))
|
||
.map((c) => {
|
||
return {
|
||
value: c.id,
|
||
label: c.title,
|
||
}
|
||
})
|
||
|
||
coverOptions.value = [...coverOptions.value, ...lookupAttColumns]
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
)
|
||
|
||
const addColumnDropdown = ref(false)
|
||
|
||
const openSubmenusCount = ref(0)
|
||
const lookupDropdownsTickle = ref(0)
|
||
|
||
function scrollToLatestField() {
|
||
setTimeout(() => {
|
||
document.querySelector('.nc-fields-menu-item:last-child')?.scrollIntoView({ behavior: 'smooth' })
|
||
}, 500)
|
||
}
|
||
|
||
const showAddLookupDropdown = (field: Field) => {
|
||
if (!field.fk_column_id) return false
|
||
|
||
return !!(isAddingColumnAllowed.value && !isLocalMode.value && isLinksOrLTAR(meta.value?.columnsById?.[field.fk_column_id]))
|
||
}
|
||
|
||
function conditionalToggleFieldVisibility(field: Field) {
|
||
if (showAddLookupDropdown(field) || isFieldsMenuReadOnly.value) {
|
||
return
|
||
}
|
||
|
||
// For editor role we just have to show hidden field without giving access to change field visibility
|
||
if (!field.initialShow && isLocalMode.value && hasViewFieldDataEditPermission.value) {
|
||
return
|
||
}
|
||
|
||
field.show = !field.show
|
||
toggleFieldVisibility(field.show, field)
|
||
}
|
||
|
||
function handleFieldVisibilityClick(field: Field) {
|
||
if (isLinksOrLTAR(meta.value?.columnsById?.[field.fk_column_id!])) {
|
||
field.show = !field.show
|
||
toggleFieldVisibility(field.show, field)
|
||
}
|
||
}
|
||
|
||
function onColumnSubmitted() {
|
||
message.success(t('msg.toast.createField'))
|
||
addColumnDropdown.value = false
|
||
scrollToLatestField()
|
||
}
|
||
|
||
const editOrAddProviderRef = ref()
|
||
|
||
const onFieldsMenuDropdownVisibilityChange = (value: boolean) => {
|
||
if (!value && addColumnDropdown.value) {
|
||
open.value = true
|
||
}
|
||
}
|
||
|
||
const onAddColumnDropdownVisibilityChange = () => {
|
||
addColumnDropdown.value = true
|
||
|
||
if (editOrAddProviderRef.value && !editOrAddProviderRef.value?.shouldKeepModalOpen?.()) {
|
||
addColumnDropdown.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<NcDropdown
|
||
v-model:visible="open"
|
||
:trigger="['click']"
|
||
class="!xs:hidden"
|
||
overlay-class-name="nc-dropdown-fields-menu nc-toolbar-dropdown overflow-hidden"
|
||
:auto-close="openSubmenusCount === 0"
|
||
@visible-change="onFieldsMenuDropdownVisibilityChange"
|
||
>
|
||
<NcTooltip :disabled="!isMobileMode && !isToolbarIconMode" :class="{ 'nc-active-btn': numberOfHiddenFields }">
|
||
<template #title>
|
||
{{
|
||
activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY
|
||
? $t('title.editCards')
|
||
: $t('objects.fields')
|
||
}}
|
||
</template>
|
||
|
||
<NcButton
|
||
v-e="['c:fields']"
|
||
class="nc-fields-menu-btn nc-toolbar-btn !h-7 !border-0"
|
||
size="small"
|
||
type="secondary"
|
||
:show-as-disabled="isFieldsMenuReadOnly"
|
||
>
|
||
<div class="flex items-center gap-1">
|
||
<div class="flex items-center gap-2 min-h-5">
|
||
<GeneralIcon
|
||
v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY"
|
||
class="h-4 w-4"
|
||
icon="creditCard"
|
||
/>
|
||
<component :is="iconMap.fields" v-else class="h-4 w-4" />
|
||
|
||
<!-- Fields -->
|
||
<span v-if="!isMobileMode && !isToolbarIconMode" class="text-capitalize !text-small1 font-medium">
|
||
<template v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY">
|
||
{{ $t('title.editCards') }}
|
||
</template>
|
||
<template v-else>
|
||
{{ $t('objects.fields') }}
|
||
</template>
|
||
</span>
|
||
</div>
|
||
<span v-if="numberOfHiddenFields" class="bg-nc-bg-brand text-nc-content-brand nc-toolbar-btn-chip">
|
||
{{ numberOfHiddenFields }}
|
||
</span>
|
||
</div>
|
||
</NcButton>
|
||
</NcTooltip>
|
||
|
||
<template #overlay>
|
||
<div class="w-[320px] rounded-lg nc-table-toolbar-menu" data-testid="nc-fields-menu" @click.stop>
|
||
<div
|
||
v-if="!isPublic && (activeView?.type === ViewTypes.GALLERY || activeView?.type === ViewTypes.KANBAN)"
|
||
class="flex items-center gap-2 p-2 w-80 border-b-1 border-nc-border-gray-light"
|
||
>
|
||
<div class="pl-2 flex text-sm select-none text-nc-content-gray-subtle2">{{ $t('labels.coverImageField') }}</div>
|
||
|
||
<div
|
||
class="flex-1 nc-dropdown-cover-image-wrapper flex items-stretch border-1 border-nc-border-gray-medium rounded-lg transition-all duration-0.3s max-w-[206px]"
|
||
:class="{
|
||
'nc-disabled': isFieldsMenuReadOnly,
|
||
}"
|
||
>
|
||
<a-select
|
||
v-model:value="coverImageColumnId"
|
||
class="flex-1 max-w-[calc(100%_-_33px)]"
|
||
dropdown-class-name="nc-dropdown-cover-image !rounded-lg"
|
||
:bordered="false"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
@click.stop
|
||
>
|
||
<template #suffixIcon><GeneralIcon class="text-nc-content-gray-subtle" icon="arrowDown" /></template>
|
||
|
||
<a-select-option v-for="option of coverOptions" :key="option.value" :value="option.value">
|
||
<div class="w-full flex gap-2 items-center justify-between max-w-[400px]">
|
||
<div
|
||
class="flex-1 flex items-center gap-1"
|
||
:class="{
|
||
'max-w-[calc(100%_-_20px)]': coverImageColumnId === option.value,
|
||
'max-w-full': coverImageColumnId !== option.value,
|
||
}"
|
||
>
|
||
<SmartsheetHeaderIcon
|
||
v-if="option.value && metaColumnById[option.value]"
|
||
:column="metaColumnById[option.value]"
|
||
class="!w-3.5 !h-3.5 !ml-0"
|
||
color="text-nc-content-gray-subtle"
|
||
/>
|
||
|
||
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
|
||
<template #title>
|
||
{{ option.label }}
|
||
</template>
|
||
<template #default>{{ option.label }}</template>
|
||
</NcTooltip>
|
||
</div>
|
||
<GeneralIcon
|
||
v-if="coverImageColumnId === option.value"
|
||
id="nc-selected-item-icon"
|
||
icon="check"
|
||
class="flex-none text-nc-content-brand w-4 h-4"
|
||
/>
|
||
</div>
|
||
</a-select-option>
|
||
</a-select>
|
||
<NcDropdown
|
||
v-if="coverImageObjectFit"
|
||
v-model:visible="coverImageObjectFitDropdown.isOpen"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
placement="bottomRight"
|
||
>
|
||
<button
|
||
class="flex items-center px-2 border-l-1 border-nc-border-gray-medium disabled:(cursor-not-allowed opacity-80)"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
>
|
||
<GeneralIcon
|
||
icon="settings"
|
||
class="h-4 w-4"
|
||
:class="{
|
||
'!text-nc-content-brand': coverImageObjectFitDropdown.isOpen,
|
||
}"
|
||
/>
|
||
</button>
|
||
<template #overlay>
|
||
<NcMenu class="nc-cover-image-object-fit-dropdown-menu min-w-[168px]">
|
||
<NcMenuItem
|
||
v-for="option in coverImageObjectFitOptions"
|
||
:key="option.value"
|
||
class="!children:w-full"
|
||
@click.stop="coverImageObjectFit = option.value"
|
||
>
|
||
<span>
|
||
{{ option.label }}
|
||
</span>
|
||
|
||
<GeneralLoader
|
||
v-if="option.value === coverImageObjectFitDropdown.isSaving"
|
||
size="regular"
|
||
class="flex-none"
|
||
/>
|
||
|
||
<GeneralIcon
|
||
v-else-if="option.value === coverImageObjectFit"
|
||
icon="check"
|
||
class="flex-none text-nc-content-brand w-4 h-4"
|
||
/>
|
||
</NcMenuItem>
|
||
</NcMenu>
|
||
</template>
|
||
</NcDropdown>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="py-2" @click.stop>
|
||
<a-input
|
||
ref="fieldsMenuSearchRef"
|
||
v-model:value="filterQuery"
|
||
:placeholder="$t('placeholder.searchFields')"
|
||
class="nc-toolbar-dropdown-search-field-input !border-none !shadow-none !h-8"
|
||
>
|
||
<template #prefix> <GeneralIcon icon="search" class="nc-search-icon h-3.5 w-3.5 mr-1 ml-2" /> </template>
|
||
<template #suffix>
|
||
<div class="pl-2 flex items-center gap-2">
|
||
<NcSwitch
|
||
v-model:checked="showAllColumns"
|
||
:disabled="isDisabledShowAllColumns"
|
||
:loading="isLoadingShowAllColumns"
|
||
size="xsmall"
|
||
class="!mr-1 nc-fields-toggle-show-all-fields"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</a-input>
|
||
</div>
|
||
|
||
<div
|
||
class="flex flex-col nc-scrollbar-thin max-h-[315px] min-h-[240px] p-2 overflow-y-auto border-t-1 border-nc-border-gray-medium"
|
||
style="scrollbar-gutter: stable !important"
|
||
>
|
||
<div class="nc-fields-list">
|
||
<div
|
||
v-if="!localFilteredFieldList.length"
|
||
class="px-2 py-6 text-nc-content-gray-muted flex flex-col items-center gap-6 text-center"
|
||
>
|
||
<img
|
||
src="~assets/img/placeholder/no-search-result-found.png"
|
||
class="!w-[164px] flex-none"
|
||
alt="No search results found"
|
||
/>
|
||
|
||
{{ $t('title.noResultsMatchedYourSearch') }}
|
||
</div>
|
||
<Draggable
|
||
v-bind="getDraggableAutoScrollOptions({ scrollSensitivity: 40 })"
|
||
v-model="fields"
|
||
item-key="id"
|
||
ghost-class="nc-fields-menu-items-ghost"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
@change="onMove($event)"
|
||
@start="isDragging = true"
|
||
@end="isDragging = false"
|
||
>
|
||
<template #item="{ element: field }">
|
||
<div
|
||
v-if="localFilteredFieldList.includes(field)"
|
||
:key="field.id"
|
||
:data-testid="`nc-fields-menu-${field.title}`"
|
||
class="nc-fields-menu-item pl-2 flex flex-row items-center rounded-md"
|
||
:class="{
|
||
'hover:bg-nc-bg-gray-light': !isFieldsMenuReadOnly,
|
||
}"
|
||
@click.stop
|
||
>
|
||
<component
|
||
:is="iconMap.drag"
|
||
class="!h-3.75 text-nc-content-gray-subtle2 mr-1"
|
||
:class="{
|
||
'cursor-not-allowed': isFieldsMenuReadOnly,
|
||
'cursor-move': !isFieldsMenuReadOnly,
|
||
}"
|
||
/>
|
||
<SmartsheetToolbarAddLookupsDropdown
|
||
v-if="metas"
|
||
:key="lookupDropdownsTickle"
|
||
:column="meta?.columnsById?.[field.fk_column_id!]!"
|
||
:disabled="!showAddLookupDropdown(field)"
|
||
@created="lookupDropdownsTickle++"
|
||
@update:is-opened="openSubmenusCount += $event === true ? 1 : -1"
|
||
>
|
||
<template #default="{ isOpened }">
|
||
<div
|
||
v-e="['a:fields:show-hide']"
|
||
class="flex flex-row items-center w-full truncate ml-1 py-[5px] pr-2"
|
||
:class="{
|
||
'cursor-pointer': !isFieldsMenuReadOnly,
|
||
'is-opened-add-lookup': isOpened,
|
||
}"
|
||
@click="conditionalToggleFieldVisibility(field)"
|
||
>
|
||
<SmartsheetHeaderIcon
|
||
v-if="field.fk_column_id && metaColumnById[field.fk_column_id]"
|
||
:column="metaColumnById[field.fk_column_id]"
|
||
class="!w-3.5 !h-3.5"
|
||
color="text-nc-content-gray-subtle2"
|
||
@click.stop
|
||
/>
|
||
|
||
<NcTooltip
|
||
class="pl-1 truncate"
|
||
:class="{
|
||
'mr-3 flex-1': !showAddLookupDropdown(field) && !searchBasisIdMap[field.fk_column_id!],
|
||
}"
|
||
show-on-truncate-only
|
||
:disabled="isDragging"
|
||
>
|
||
<template #title>
|
||
{{ field.title }}
|
||
</template>
|
||
<template #default>
|
||
{{ field.title }}
|
||
</template>
|
||
</NcTooltip>
|
||
<div v-if="searchBasisIdMap[field.fk_column_id!]" class="flex-1 flex ml-1 mr-3">
|
||
<NcTooltip :title="searchBasisIdMap[field.fk_column_id!]" class="flex cursor-help">
|
||
<GeneralIcon icon="info" class="h-3.5 w-3.5 opacity-80 text-nc-content-gray-muted" />
|
||
</NcTooltip>
|
||
</div>
|
||
<div v-if="showAddLookupDropdown(field)" class="flex-1 flex mr-3">
|
||
<NcTooltip :disabled="isOpened">
|
||
<template #title>
|
||
{{ $t('tooltip.addLookupFields') }}
|
||
</template>
|
||
|
||
<div class="px-1 text-nc-content-gray-subtle2">
|
||
<GeneralIcon icon="chevronRight" class="flex-none !w-3.5 !h-3.5" />
|
||
</div>
|
||
</NcTooltip>
|
||
</div>
|
||
|
||
<div v-if="activeView.type === ViewTypes.CALENDAR" class="flex mr-2">
|
||
<NcButton
|
||
:class="{
|
||
'!text-nc-content-brand !bg-nc-bg-brand hover:!bg-nc-brand-100 active:!bg-nc-brand-200': field.bold,
|
||
'!rounded-r-none': field.italic,
|
||
}"
|
||
class="!w-5 !h-5 hover:!bg-nc-bg-gray-medium active:!bg-nc-bg-gray-dark relative"
|
||
size="xsmall"
|
||
type="text"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
@click.stop="toggleFieldStyles(field, 'bold', !field.bold)"
|
||
>
|
||
<component :is="iconMap.bold" class="!w-3.5 !h-3.5" />
|
||
<div
|
||
v-if="field.bold"
|
||
class="bg-primary w-1.25 h-1.25 rounded-full absolute top-0.25 right-0.5 border-1 border-base-white"
|
||
/>
|
||
</NcButton>
|
||
<NcButton
|
||
:class="{
|
||
'!text-nc-content-brand !bg-nc-bg-brand hover:!bg-nc-brand-100 active:!bg-nc-brand-200':
|
||
field.italic,
|
||
'!rounded-l-none': field.bold,
|
||
'!rounded-r-none': field.underline,
|
||
}"
|
||
class="!w-5 !h-5 hover:!bg-nc-bg-gray-medium active:!bg-nc-bg-gray-dark relative"
|
||
size="xsmall"
|
||
type="text"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
@click.stop="toggleFieldStyles(field, 'italic', !field.italic)"
|
||
>
|
||
<component :is="iconMap.italic" class="!w-3.5 !h-3.5" />
|
||
<div
|
||
v-if="field.italic"
|
||
class="bg-primary w-1.25 h-1.25 rounded-full absolute top-0.25 right-0.5 border-1 border-base-white"
|
||
/>
|
||
</NcButton>
|
||
<NcButton
|
||
:class="{
|
||
'!text-nc-content-brand !bg-nc-bg-brand hover:!bg-nc-brand-100 active:!bg-nc-brand-200':
|
||
field.underline,
|
||
'!rounded-l-none': field.italic,
|
||
}"
|
||
class="!w-5 !h-5 hover:!bg-nc-bg-gray-medium active:!bg-nc-bg-gray-dark relative"
|
||
size="xsmall"
|
||
type="text"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
@click.stop="toggleFieldStyles(field, 'underline', !field.underline)"
|
||
>
|
||
<component :is="iconMap.underline" class="!w-3.5 !h-3.5" />
|
||
<div
|
||
v-if="field.underline"
|
||
class="bg-primary w-1.25 h-1.25 rounded-full absolute top-0.25 right-0.5 border-1 border-base-white"
|
||
/>
|
||
</NcButton>
|
||
</div>
|
||
|
||
<span class="flex children:flex-none" @click.stop="conditionalToggleFieldVisibility(field)">
|
||
<NcSwitch
|
||
:checked="field.show"
|
||
:disabled="field.isViewEssentialField || isFieldsMenuReadOnly || isLoadingShowAllColumns"
|
||
size="xxsmall"
|
||
@change="$e('a:fields:show-hide')"
|
||
@click="handleFieldVisibilityClick(field)"
|
||
/>
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</SmartsheetToolbarAddLookupsDropdown>
|
||
|
||
<div class="flex-1" />
|
||
</div>
|
||
</template>
|
||
</Draggable>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="!isLocalMode && !filterQuery"
|
||
class="flex px-2 gap-1 py-2 border-t-1 justify-between border-nc-border-gray-medium"
|
||
>
|
||
<NcButton
|
||
class="nc-fields-show-system-fields !px-2 !font-semibold"
|
||
size="small"
|
||
type="text"
|
||
:disabled="isFieldsMenuReadOnly"
|
||
@click="showSystemField = !showSystemField"
|
||
>
|
||
<GeneralIcon :icon="showSystemField ? 'eyeSlash' : 'eye'" class="!w-4 !h-4 mr-2" />
|
||
<span> {{ $t('title.systemFields') }} </span>
|
||
</NcButton>
|
||
<NcDropdown
|
||
v-if="isAddingColumnAllowed"
|
||
v-model:visible="addColumnDropdown"
|
||
:trigger="['click']"
|
||
overlay-class-name="nc-dropdown-add-column !bg-transparent !border-none !shadow-none !rounded-2xl"
|
||
placement="right"
|
||
:align="{
|
||
offset: [9, -15],
|
||
}"
|
||
@visible-change="onAddColumnDropdownVisibilityChange"
|
||
>
|
||
<NcButton text-color="primary" class="nc-fields-add-new-field !font-semibold !px-2" size="small" type="text">
|
||
<GeneralIcon icon="ncPlus" class="!w-4 !h-4 mr-1" />
|
||
<span>{{ t('general.new') }} {{ t('objects.field') }}</span>
|
||
</NcButton>
|
||
<template #overlay>
|
||
<div class="nc-edit-or-add-provider-wrapper">
|
||
<LazySmartsheetColumnEditOrAddProvider
|
||
v-if="addColumnDropdown"
|
||
ref="editOrAddProviderRef"
|
||
@submit="onColumnSubmitted()"
|
||
@cancel="addColumnDropdown = false"
|
||
@click.stop
|
||
@keydown.stop
|
||
/>
|
||
</div>
|
||
</template>
|
||
</NcDropdown>
|
||
</div>
|
||
|
||
<GeneralLockedViewFooter
|
||
v-if="isFieldsMenuReadOnly"
|
||
:show-icon="isLocked"
|
||
:show-unlock-button="isLocked"
|
||
@on-open="open = false"
|
||
>
|
||
<template v-if="!isLocked" #title> You don’t have permission to edit this view. </template>
|
||
</GeneralLockedViewFooter>
|
||
</div>
|
||
</template>
|
||
</NcDropdown>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
:deep(.nc-toolbar-dropdown-search-field-input .ant-input::placeholder) {
|
||
@apply text-nc-content-gray-muted;
|
||
}
|
||
:deep(.xxsmall) {
|
||
@apply !min-w-0;
|
||
}
|
||
|
||
.nc-fields-menu-item {
|
||
&:has(.is-opened-add-lookup) {
|
||
@apply bg-nc-bg-gray-light;
|
||
}
|
||
}
|
||
|
||
.nc-fields-menu-items-ghost {
|
||
@apply bg-nc-bg-gray-extralight;
|
||
}
|
||
|
||
.nc-cover-image-object-fit-dropdown-menu {
|
||
:deep(.nc-menu-item-inner) {
|
||
@apply !w-full flex items-center justify-between;
|
||
}
|
||
}
|
||
.nc-dropdown-cover-image-wrapper {
|
||
@apply h-8;
|
||
|
||
&:not(.nc-disabled):not(:focus-within) {
|
||
@apply shadow-default hover:shadow-hover;
|
||
}
|
||
&:not(.nc-disabled):focus-within {
|
||
@apply shadow-selected border-nc-border-brand;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-input-affix-wrapper) {
|
||
&:not(.ant-input-affix-wrapper-disabled):not(.ant-input-affix-wrapper-focused):not(:focus) {
|
||
@apply shadow-default hover:(shadow-hover border-nc-border-gray-medium);
|
||
}
|
||
&.ant-input-affix-wrapper-focused,
|
||
&:focus {
|
||
@apply border-nc-border-brand shadow-selected;
|
||
}
|
||
}
|
||
</style>
|