mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
10 KiB
318 lines
10 KiB
<script lang="ts" setup> |
|
import { type ViewType, ViewTypes } from 'nocodb-sdk' |
|
|
|
const { isMobileMode } = useGlobal() |
|
|
|
const { t } = useI18n() |
|
|
|
const { $e } = useNuxtApp() |
|
|
|
const { isUIAllowed } = useRoles() |
|
|
|
const { base } = storeToRefs(useBase()) |
|
|
|
const { activeTable } = storeToRefs(useTablesStore()) |
|
|
|
const viewsStore = useViewsStore() |
|
|
|
const { activeView, views } = storeToRefs(viewsStore) |
|
|
|
const { loadViews, navigateToView } = viewsStore |
|
|
|
const { refreshCommandPalette } = useCommandPalette() |
|
|
|
const { isFeatureEnabled } = useBetaFeatureToggle() |
|
|
|
const isOpen = ref<boolean>(false) |
|
|
|
const activeSource = computed(() => { |
|
return base.value.sources?.find((s) => s.id === activeView.value?.source_id) |
|
}) |
|
|
|
/** |
|
* Handles navigation to a selected view. |
|
* |
|
* @param view - The view to navigate to. |
|
* @returns A Promise that resolves when the navigation is complete. |
|
* |
|
* @remarks |
|
* This function is called when a user selects a view from the dropdown list. |
|
* It checks if the view has a valid ID and then navigates to the selected view. |
|
* If the view is a form and it's already active, it performs a hard reload. |
|
*/ |
|
const handleNavigateToView = async (view: ViewType) => { |
|
if (!view?.id) return |
|
|
|
await navigateToView({ |
|
view, |
|
tableId: activeTable.value.id!, |
|
baseId: base.value.id!, |
|
hardReload: view.type === ViewTypes.FORM && activeView.value?.id === view.id, |
|
doNotSwitchTab: true, |
|
}) |
|
} |
|
|
|
/** |
|
* Filters the view options based on the input string. |
|
* |
|
* @param input - The search input string. |
|
* @param view - The view object to be filtered. |
|
* @returns True if the view matches the filter criteria, false otherwise. |
|
* |
|
* @remarks |
|
* This function is used to filter the list of views in the dropdown. |
|
* It checks if the input string matches either the default view title (translated) or the view's title. |
|
* The matching is case-insensitive. |
|
*/ |
|
const filterOption = (input: string = '', view: ViewType) => { |
|
if (view.is_default && t('title.defaultView').toLowerCase().includes(input)) { |
|
return true |
|
} |
|
|
|
return view.title?.toLowerCase()?.includes(input.toLowerCase()) |
|
} |
|
|
|
/** |
|
* Opens a modal for creating or editing a view. |
|
* |
|
* @param options - The options for opening the modal. |
|
* @param options.title - The title of the modal. Default is an empty string. |
|
* @param options.type - The type of view to create or edit. |
|
* @param options.copyViewId - The ID of the view to copy, if creating a copy. |
|
* @param options.groupingFieldColumnId - The ID of the column to use for grouping, if applicable. |
|
* @param options.calendarRange - The date range for calendar views. |
|
* @param options.coverImageColumnId - The ID of the column to use for cover images, if applicable. |
|
* |
|
* @returns A Promise that resolves when the modal operation is complete. |
|
* |
|
* @remarks |
|
* This function opens a modal dialog for creating or editing a view. |
|
* It handles the dialog state, view creation, and navigation to the newly created view. |
|
* After creating a view, it refreshes the command palette and reloads the views. |
|
* |
|
* @see {@link packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue} for a similar implementation of view creation dialog. |
|
* If this function is updated, consider updating the other implementations as well. |
|
*/ |
|
async function onOpenModal({ |
|
title = '', |
|
type, |
|
copyViewId, |
|
groupingFieldColumnId, |
|
calendarRange, |
|
coverImageColumnId, |
|
}: { |
|
title?: string |
|
type: ViewTypes | 'AI' |
|
copyViewId?: string |
|
groupingFieldColumnId?: string |
|
calendarRange?: Array<{ |
|
fk_from_column_id: string |
|
fk_to_column_id: string | null |
|
}> |
|
coverImageColumnId?: string |
|
}) { |
|
isOpen.value = false |
|
|
|
const isDlgOpen = ref(true) |
|
|
|
const { close } = useDialog(resolveComponent('DlgViewCreate'), { |
|
'modelValue': isDlgOpen, |
|
title, |
|
type, |
|
'tableId': activeTable.value.id, |
|
'selectedViewId': copyViewId, |
|
calendarRange, |
|
groupingFieldColumnId, |
|
coverImageColumnId, |
|
'onUpdate:modelValue': closeDialog, |
|
'baseId': base.value.id, |
|
'onCreated': async (view?: ViewType) => { |
|
closeDialog() |
|
|
|
refreshCommandPalette() |
|
|
|
await loadViews({ |
|
tableId: activeTable.value.id!, |
|
force: true, |
|
}) |
|
|
|
activeTable.value.meta = { |
|
...(activeTable.value.meta as object), |
|
hasNonDefaultViews: true, |
|
} |
|
|
|
if (view) { |
|
navigateToView({ |
|
view, |
|
tableId: activeTable.value.id!, |
|
baseId: base.value.id!, |
|
doNotSwitchTab: true, |
|
}) |
|
} |
|
|
|
$e('a:view:create', { view: view?.type || type }) |
|
}, |
|
}) |
|
|
|
function closeDialog() { |
|
isOpen.value = false |
|
isDlgOpen.value = false |
|
|
|
close(1000) |
|
} |
|
} |
|
</script> |
|
|
|
<template> |
|
<NcDropdown v-if="activeView" v-model:visible="isOpen"> |
|
<slot name="default" :is-open="isOpen"></slot> |
|
<template #overlay> |
|
<LazyNcList |
|
v-model:open="isOpen" |
|
:value="activeView.id" |
|
:list="views" |
|
option-value-key="id" |
|
option-label-key="title" |
|
search-input-placeholder="Search views" |
|
:filter-option="filterOption" |
|
@change="handleNavigateToView" |
|
> |
|
<template #listItem="{ option }"> |
|
<div> |
|
<LazyGeneralEmojiPicker :emoji="option?.meta?.icon" readonly size="xsmall"> |
|
<template #default> |
|
<GeneralViewIcon :meta="{ type: option?.type }" class="min-w-4 text-lg flex" /> |
|
</template> |
|
</LazyGeneralEmojiPicker> |
|
</div> |
|
<NcTooltip class="truncate flex-1" show-on-truncate-only> |
|
<template #title> |
|
{{ option?.is_default ? $t('title.defaultView') : option?.title }} |
|
</template> |
|
{{ option?.is_default ? $t('title.defaultView') : option?.title }} |
|
</NcTooltip> |
|
<GeneralIcon |
|
v-if="option.id === activeView.id" |
|
id="nc-selected-item-icon" |
|
icon="check" |
|
class="flex-none text-primary w-4 h-4" |
|
/> |
|
</template> |
|
|
|
<template v-if="!isMobileMode && isUIAllowed('viewCreateOrEdit')" #listFooter> |
|
<NcDivider class="!mt-0 !mb-2" /> |
|
<div class="overflow-hidden mb-2"> |
|
<a-menu class="nc-viewlist-menu"> |
|
<a-sub-menu popup-class-name="nc-viewlist-submenu-popup "> |
|
<template #title> |
|
<div class="flex items-center justify-between gap-2 text-sm font-weight-500 !text-brand-500"> |
|
<div class="flex items-center gap-2"> |
|
<GeneralIcon icon="plus" /> |
|
<div> |
|
{{ |
|
$t('general.createEntity', { |
|
entity: $t('objects.view'), |
|
}) |
|
}} |
|
</div> |
|
</div> |
|
<GeneralIcon icon="arrowRight" class="text-base text-gray-600 group-hover:text-gray-800" /> |
|
</div> |
|
</template> |
|
|
|
<template #expandIcon> </template> |
|
|
|
<a-menu-item @click.stop="onOpenModal({ type: ViewTypes.GRID })"> |
|
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-grid"> |
|
<GeneralViewIcon :meta="{ type: ViewTypes.GRID }" /> |
|
Grid |
|
</div> |
|
</a-menu-item> |
|
|
|
<a-menu-item v-if="!activeSource?.is_schema_readonly" @click="onOpenModal({ type: ViewTypes.FORM })"> |
|
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-form"> |
|
<GeneralViewIcon :meta="{ type: ViewTypes.FORM }" /> |
|
Form |
|
</div> |
|
</a-menu-item> |
|
<a-menu-item @click="onOpenModal({ type: ViewTypes.GALLERY })"> |
|
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-gallery"> |
|
<GeneralViewIcon :meta="{ type: ViewTypes.GALLERY }" /> |
|
Gallery |
|
</div> |
|
</a-menu-item> |
|
<a-menu-item data-testid="topbar-view-create-kanban" @click="onOpenModal({ type: ViewTypes.KANBAN })"> |
|
<div class="nc-viewlist-submenu-popup-item"> |
|
<GeneralViewIcon :meta="{ type: ViewTypes.KANBAN }" /> |
|
Kanban |
|
</div> |
|
</a-menu-item> |
|
<a-menu-item data-testid="topbar-view-create-calendar" @click="onOpenModal({ type: ViewTypes.CALENDAR })"> |
|
<div class="nc-viewlist-submenu-popup-item"> |
|
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" class="!w-4 !h-4" /> |
|
{{ $t('objects.viewType.calendar') }} |
|
</div> |
|
</a-menu-item> |
|
|
|
<template v-if="isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)"> |
|
<NcDivider /> |
|
<a-menu-item data-testid="sidebar-view-create-ai" @click="onOpenModal({ type: 'AI' })"> |
|
<div class="nc-viewlist-submenu-popup-item"> |
|
<GeneralIcon icon="ncAutoAwesome" class="!w-4 !h-4 text-nc-fill-purple-dark" /> |
|
<div>{{ $t('labels.aiSuggested') }}</div> |
|
</div> |
|
</a-menu-item> |
|
</template> |
|
</a-sub-menu> |
|
</a-menu> |
|
</div> |
|
</template> |
|
</LazyNcList> |
|
</template> |
|
</NcDropdown> |
|
</template> |
|
|
|
<style lang="scss"> |
|
.nc-viewlist-menu { |
|
@apply !border-r-0; |
|
|
|
.ant-menu-submenu { |
|
@apply !mx-2; |
|
|
|
.ant-menu-submenu-title { |
|
@apply flex items-center gap-2 py-1.5 px-2 my-0 h-auto hover:bg-gray-100 cursor-pointer rounded-md; |
|
|
|
.ant-menu-title-content { |
|
@apply w-full; |
|
} |
|
} |
|
} |
|
} |
|
|
|
.nc-viewlist-submenu-popup { |
|
@apply !rounded-lg border-1 border-gray-50; |
|
|
|
.ant-menu.ant-menu-sub { |
|
@apply p-2 !rounded-lg !shadow-lg shadow-gray-200; |
|
} |
|
|
|
.ant-menu-item { |
|
@apply h-auto !my-0 text-sm !leading-5 py-2 px-2 hover:!bg-gray-100 cursor-pointer rounded-md; |
|
|
|
.ant-menu-title-content { |
|
@apply w-full px-0; |
|
} |
|
|
|
.nc-viewlist-submenu-popup-item { |
|
@apply flex items-center gap-2 !text-gray-800; |
|
} |
|
|
|
&.ant-menu-item-selected { |
|
@apply bg-transparent; |
|
} |
|
} |
|
} |
|
.nc-viewlist-submenu-popup .ant-dropdown-menu.ant-dropdown-menu-sub { |
|
@apply !rounded-lg !shadow-lg shadow-gray-200; |
|
} |
|
</style>
|
|
|