mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 06:15:46 +00:00
1387 lines
40 KiB
TypeScript
1387 lines
40 KiB
TypeScript
import type { CalendarType, FilterType, GalleryType, KanbanType, MapType, RowColoringInfo, SortType, ViewType } from 'nocodb-sdk'
|
|
import {
|
|
ProjectRoles,
|
|
ViewSettingOverrideOptions,
|
|
ViewTypes,
|
|
WorkspaceUserRoles,
|
|
ViewTypes as _ViewTypes,
|
|
getFirstNonPersonalView,
|
|
} from 'nocodb-sdk'
|
|
import { acceptHMRUpdate, defineStore } from 'pinia'
|
|
import { useTitle } from '@vueuse/core'
|
|
import type { ViewPageType } from '~/lib/types'
|
|
import { getFormattedViewTabTitle } from '~/helpers/parsers/parserHelpers'
|
|
import { DlgViewCopyViewConfigFromAnotherView, DlgViewCreate } from '#components'
|
|
import { userLocalStorageInfoManager } from '#imports'
|
|
|
|
// Types and Interfaces
|
|
interface RecentView {
|
|
viewName: string
|
|
viewId: string | undefined
|
|
viewType: ViewTypes
|
|
tableID: string
|
|
isDefault: boolean
|
|
baseName: string
|
|
tableName: string
|
|
workspaceId: string
|
|
baseId: string
|
|
}
|
|
|
|
export const useViewsStore = defineStore('viewsStore', () => {
|
|
const { $api, $e, $eventBus } = useNuxtApp()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const { ncNavigateTo, user } = useGlobal()
|
|
|
|
const router = useRouter()
|
|
|
|
const { meta: metaKey, control } = useMagicKeys()
|
|
|
|
const bases = useBases()
|
|
|
|
const { getMeta } = useMetas()
|
|
|
|
const tablesStore = useTablesStore()
|
|
|
|
const workspaceStore = useWorkspace()
|
|
|
|
const { refreshCommandPalette } = useCommandPalette()
|
|
|
|
const { isUIAllowed } = useRoles()
|
|
|
|
const { sharedView } = useSharedView()
|
|
|
|
const { openedProject, activeProjectId } = storeToRefs(bases)
|
|
|
|
const { activeWorkspaceId } = storeToRefs(workspaceStore)
|
|
|
|
const { activeTable } = storeToRefs(tablesStore)
|
|
|
|
const { isFeatureEnabled } = useBetaFeatureToggle()
|
|
|
|
const { blockCardFieldHeaderVisibility } = useEeConfig()
|
|
|
|
const route = router.currentRoute
|
|
|
|
const allRecentViews = ref<RecentView[]>([])
|
|
|
|
// Helper function to create composite key: baseId:tableId
|
|
const getViewsKey = (baseId: string, tableId: string) => `${baseId}:${tableId}`
|
|
|
|
const viewsByTable = ref<Map<string, ViewType[]>>(new Map())
|
|
|
|
const activeSorts = ref<SortType[]>([])
|
|
|
|
const activeNestedFilters = ref<FilterType[]>([])
|
|
|
|
const isViewsLoading = ref(true)
|
|
|
|
const isViewDataLoading = ref(true)
|
|
|
|
const isPaginationLoading = ref(false)
|
|
|
|
const lastOpenedViewId = ref<string | undefined>(undefined)
|
|
|
|
const preFillFormSearchParams = ref('')
|
|
|
|
const activeViewRowColorInfo = ref<RowColoringInfo>(defaultRowColorInfo)
|
|
|
|
// Computed properties
|
|
const isPublic = computed(() => route.value.meta?.public)
|
|
|
|
const recentViews = computed<RecentView[]>(() =>
|
|
allRecentViews.value.filter((f) => f.workspaceId === activeWorkspaceId.value).splice(0, 10),
|
|
)
|
|
|
|
const views = computed({
|
|
get: () => {
|
|
if (!tablesStore.activeTableId) return []
|
|
|
|
// Try to get the base_id from activeTable
|
|
if (activeTable.value?.base_id) {
|
|
const key = getViewsKey(activeTable.value.base_id, tablesStore.activeTableId)
|
|
return viewsByTable.value.get(key) ?? []
|
|
}
|
|
|
|
// Fallback: Search through all keys to find a match for this tableId
|
|
// This handles the case when activeTable.base_id is not yet available (e.g., direct URL load)
|
|
for (const [k, views] of viewsByTable.value) {
|
|
if (k.endsWith(`:${tablesStore.activeTableId}`)) {
|
|
return views
|
|
}
|
|
}
|
|
|
|
return []
|
|
},
|
|
set: (value) => {
|
|
if (!tablesStore.activeTableId) return
|
|
|
|
// For setting, we need the base_id
|
|
if (!activeTable.value?.base_id) {
|
|
console.warn('Cannot set views without base_id')
|
|
return
|
|
}
|
|
|
|
const key = getViewsKey(activeTable.value.base_id, tablesStore.activeTableId)
|
|
if (!value) return viewsByTable.value.delete(key)
|
|
|
|
viewsByTable.value.set(key, value)
|
|
},
|
|
})
|
|
|
|
const activeViewTitleOrId = computed(() => {
|
|
if (!route.value.params.viewTitle?.length) {
|
|
// find the default view and navigate to it, if not found navigate to the first one
|
|
const defaultView = getFirstNonPersonalView(views.value)
|
|
|
|
return defaultView?.id
|
|
}
|
|
|
|
return route.value.params.viewTitle
|
|
})
|
|
|
|
const openedViewsTab = computed<ViewPageType>(() => {
|
|
// For types in ViewPageType type
|
|
if (!route.value.params?.slugs || route.value.params.slugs?.length <= 1) return 'view'
|
|
|
|
if (['field', 'permissions', 'relation', 'api', 'webhook'].includes(route.value.params.slugs[1] as ViewPageType)) {
|
|
return route.value.params.slugs[1] as ViewPageType
|
|
}
|
|
|
|
return 'view'
|
|
})
|
|
|
|
const activeView = computed<ViewType | undefined>({
|
|
get() {
|
|
if (sharedView.value) return sharedView.value
|
|
|
|
if (!activeTable.value) return undefined
|
|
|
|
if (!activeViewTitleOrId.value) return undefined
|
|
|
|
return (
|
|
views.value.find((v) => v.id === activeViewTitleOrId.value) ??
|
|
views.value.find((v) => v.title === activeViewTitleOrId.value)
|
|
)
|
|
},
|
|
set(_view: ViewType | undefined) {
|
|
if (sharedView.value) {
|
|
sharedView.value = _view
|
|
return
|
|
}
|
|
|
|
if (!activeTable.value) return
|
|
if (!_view) return
|
|
|
|
const viewIndex =
|
|
views.value.findIndex((v) => v.id === activeViewTitleOrId.value) ??
|
|
views.value.findIndex((v) => v.title === activeViewTitleOrId.value)
|
|
if (viewIndex === -1) return
|
|
|
|
views.value[viewIndex] = _view
|
|
},
|
|
})
|
|
|
|
const activeViewUrlSlug = computed(() => {
|
|
return route.value.params.slugs?.[0] || ''
|
|
})
|
|
|
|
const activeViewReadableUrlSlug = computed(() => {
|
|
if (sharedView.value || !activeView.value) return ''
|
|
|
|
return getViewReadableUrlSlug({ tableTitle: activeTable.value?.title, viewOrViewTitle: activeView.value })
|
|
})
|
|
|
|
const isActiveViewLocked = computed(() => activeView.value?.lock_type === 'locked')
|
|
const isLockedView = computed(() => activeView.value?.lock_type === 'locked')
|
|
|
|
const isActiveViewFieldHeaderVisible = computed(() => {
|
|
// If card field header visibility is not enabled or blocked, return true to show header by default
|
|
if (blockCardFieldHeaderVisibility.value || !isEeUI) return true
|
|
|
|
return parseProp((activeView.value?.view as GalleryType | KanbanType)?.meta)?.is_field_header_visible ?? true
|
|
})
|
|
|
|
const isShowEveryonePersonalViewsEnabled = computed({
|
|
get: () => {
|
|
if (!isEeUI || !isFeatureEnabled(FEATURE_FLAG.SHOW_EVERYONES_PERSONAL_VIEWS)) {
|
|
return true
|
|
}
|
|
|
|
return !!userLocalStorageInfoManager.get(user.value?.id, activeWorkspaceId.value, 'showOtherUserPersonalViews', true)
|
|
},
|
|
set: (value: boolean) => {
|
|
if (!isEeUI || !isFeatureEnabled(FEATURE_FLAG.SHOW_EVERYONES_PERSONAL_VIEWS)) {
|
|
return
|
|
}
|
|
|
|
userLocalStorageInfoManager.set(user.value?.id, activeWorkspaceId.value, 'showOtherUserPersonalViews', value)
|
|
},
|
|
})
|
|
|
|
const refreshViewTabTitle = createEventHook<void>()
|
|
|
|
const loadViews = async ({
|
|
tableId,
|
|
baseId,
|
|
ignoreLoading,
|
|
force,
|
|
}: { tableId?: string; baseId?: string; ignoreLoading?: boolean; force?: boolean } = {}) => {
|
|
const effectiveBaseId = baseId || activeProjectId.value
|
|
|
|
if (!effectiveBaseId) {
|
|
console.error('[loadViews] baseId is required but was not provided')
|
|
return
|
|
}
|
|
|
|
tableId = tableId ?? tablesStore.activeTableId
|
|
|
|
let response
|
|
if (tableId) {
|
|
// Wait for tables to be loaded if they're not available yet
|
|
await until(() => tablesStore.baseTables.get(effectiveBaseId)?.length).toBeTruthy({ timeout: 10000 })
|
|
|
|
const table = tablesStore.baseTables.get(effectiveBaseId)?.find((t) => t.id === tableId)
|
|
if (!table) {
|
|
console.warn('Could not find table:', tableId, 'in base:', effectiveBaseId)
|
|
return
|
|
}
|
|
|
|
// Use the table's actual base_id (handles cross-base scenarios)
|
|
const tableBaseId = table.base_id || effectiveBaseId
|
|
|
|
const key = getViewsKey(tableBaseId, tableId)
|
|
|
|
if (!force && viewsByTable.value.get(key)) {
|
|
viewsByTable.value.set(
|
|
key,
|
|
(viewsByTable.value.get(key) ?? []).sort((a, b) => a.order! - b.order!),
|
|
)
|
|
isViewsLoading.value = false
|
|
return viewsByTable.value.get(key)
|
|
}
|
|
if (!ignoreLoading) isViewsLoading.value = true
|
|
|
|
response = (
|
|
await $api.internal.getOperation(activeWorkspaceId.value!, tableBaseId, {
|
|
operation: 'viewList',
|
|
tableId,
|
|
})
|
|
).list as ViewType[]
|
|
if (response) {
|
|
viewsByTable.value.set(
|
|
key,
|
|
response.sort((a, b) => a.order! - b.order!),
|
|
)
|
|
}
|
|
|
|
if (!ignoreLoading) isViewsLoading.value = false
|
|
}
|
|
|
|
return response
|
|
}
|
|
|
|
const navigateToView = async ({
|
|
view,
|
|
baseId,
|
|
tableId,
|
|
tableTitle,
|
|
hardReload,
|
|
doNotSwitchTab,
|
|
}: {
|
|
view: ViewType
|
|
baseId: string
|
|
tableId: string
|
|
tableTitle?: string
|
|
hardReload?: boolean
|
|
doNotSwitchTab?: boolean
|
|
}) => {
|
|
const cmdOrCtrl = isMac() ? metaKey.value : control.value
|
|
|
|
const routeName = 'index-typeOrId-baseId-index-index-viewId-viewTitle-slugs'
|
|
|
|
let baseIdOrBaseId = baseId
|
|
|
|
if (['base'].includes(route.value.params.typeOrId as string)) {
|
|
baseIdOrBaseId = route.value.params.baseId as string
|
|
}
|
|
|
|
const slugs = doNotSwitchTab ? router.currentRoute.value.params.slugs || [] : []
|
|
|
|
if (ncIsArray(slugs)) {
|
|
;(slugs as string[])[0] = getViewReadableUrlSlug({ tableTitle, viewOrViewTitle: view })
|
|
}
|
|
|
|
if (
|
|
router.currentRoute.value.query &&
|
|
router.currentRoute.value.query.page &&
|
|
router.currentRoute.value.query.page === 'fields'
|
|
) {
|
|
if (cmdOrCtrl) {
|
|
await navigateTo(
|
|
router.resolve({
|
|
name: routeName,
|
|
params: {
|
|
viewTitle: view.id || '',
|
|
viewId: tableId,
|
|
baseId: baseIdOrBaseId,
|
|
slugs,
|
|
},
|
|
query: router.currentRoute.value.query,
|
|
}).href,
|
|
{
|
|
open: navigateToBlankTargetOpenOption,
|
|
},
|
|
)
|
|
} else {
|
|
await router.push({
|
|
name: routeName,
|
|
params: {
|
|
viewTitle: view.id || '',
|
|
viewId: tableId,
|
|
baseId: baseIdOrBaseId,
|
|
slugs,
|
|
},
|
|
query: router.currentRoute.value.query,
|
|
})
|
|
}
|
|
} else {
|
|
if (cmdOrCtrl) {
|
|
await navigateTo(
|
|
router.resolve({
|
|
name: routeName,
|
|
params: {
|
|
viewTitle: view.id || '',
|
|
viewId: tableId,
|
|
baseId: baseIdOrBaseId,
|
|
slugs,
|
|
},
|
|
}).href,
|
|
{
|
|
open: navigateToBlankTargetOpenOption,
|
|
},
|
|
)
|
|
} else {
|
|
await router.push({
|
|
name: routeName,
|
|
params: {
|
|
viewTitle: view.id || '',
|
|
viewId: tableId,
|
|
baseId: baseIdOrBaseId,
|
|
slugs,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
if (!cmdOrCtrl && hardReload) {
|
|
await router
|
|
.replace({
|
|
name: routeName,
|
|
query: { reload: 'true' },
|
|
params: {
|
|
viewId: tableId,
|
|
baseId: baseIdOrBaseId,
|
|
viewTitle: view.id || '',
|
|
slugs,
|
|
},
|
|
})
|
|
.then(() => {
|
|
router.replace({
|
|
name: routeName,
|
|
query: {},
|
|
params: {
|
|
viewId: tableId,
|
|
viewTitle: view.id || '',
|
|
baseId: baseIdOrBaseId,
|
|
slugs,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
const createView = async (tableId: string, form: CreateViewForm): Promise<ViewType | null> => {
|
|
if (!tableId) return null
|
|
|
|
try {
|
|
let data: ViewType | null = null
|
|
|
|
switch (form.type) {
|
|
case ViewTypes.GRID:
|
|
data = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'gridViewCreate',
|
|
tableId,
|
|
},
|
|
form,
|
|
)
|
|
break
|
|
case ViewTypes.GALLERY:
|
|
data = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'galleryViewCreate',
|
|
tableId,
|
|
},
|
|
form,
|
|
)
|
|
break
|
|
case ViewTypes.FORM:
|
|
data = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'formViewCreate',
|
|
tableId,
|
|
},
|
|
{
|
|
...form,
|
|
...getDefaultViewMetas(ViewTypes.FORM),
|
|
},
|
|
)
|
|
break
|
|
case ViewTypes.KANBAN:
|
|
data = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'kanbanViewCreate',
|
|
tableId,
|
|
},
|
|
form,
|
|
)
|
|
break
|
|
case ViewTypes.MAP:
|
|
data = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'mapViewCreate',
|
|
tableId,
|
|
},
|
|
form,
|
|
)
|
|
break
|
|
case ViewTypes.CALENDAR:
|
|
data = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'calendarViewCreate',
|
|
tableId,
|
|
},
|
|
{
|
|
...form,
|
|
calendar_range: form.calendar_range.map((range) => ({
|
|
fk_from_column_id: range.fk_from_column_id,
|
|
fk_to_column_id: range.fk_to_column_id,
|
|
})),
|
|
},
|
|
)
|
|
break
|
|
}
|
|
|
|
if (data) {
|
|
// Get the base_id for the table
|
|
const table = tablesStore.baseTables.get(activeProjectId.value!)?.find((t) => t.id === tableId)
|
|
if (!table?.base_id) {
|
|
console.warn('Could not find base_id for table:', tableId)
|
|
return null
|
|
}
|
|
|
|
const key = getViewsKey(table.base_id, tableId)
|
|
|
|
// Add the new view to the store
|
|
const tableViews = viewsByTable.value.get(key) || []
|
|
|
|
// Get the first collaborative grid view before
|
|
const oldFirstCollabGridView = getFirstNonPersonalView(tableViews, {
|
|
includeViewType: ViewTypes.GRID,
|
|
})
|
|
|
|
viewsByTable.value.set(key, [...tableViews, data])
|
|
|
|
// Get the new first collaborative grid view after adding
|
|
const newFirstCollabGridView = getFirstNonPersonalView([...tableViews, data], {
|
|
includeViewType: ViewTypes.GRID,
|
|
})
|
|
|
|
// If the first collaborative grid view changed, trigger getMeta
|
|
if (newFirstCollabGridView?.id !== oldFirstCollabGridView?.id && tableId) {
|
|
await getMeta(table.base_id, tableId, true)
|
|
}
|
|
|
|
// Refresh command palette
|
|
refreshCommandPalette()
|
|
|
|
// Telemetry event
|
|
$e(form.copy_from_id ? 'a:view:duplicate' : 'a:view:create', { view: data.type })
|
|
|
|
return data
|
|
}
|
|
|
|
return null
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
throw e
|
|
}
|
|
}
|
|
|
|
const duplicateView = async (view: ViewType): Promise<ViewType | null> => {
|
|
if (!view?.id || !view?.base_id) return null
|
|
|
|
const key = getViewsKey(view.base_id, view.fk_model_id)
|
|
const views = viewsByTable.value.get(key) || []
|
|
const uniqueTitle = generateUniqueTitle(`${view.title} copy`, views, 'title', '_', true)
|
|
|
|
const getViewSpecificProps = (sourceView: ViewType) => {
|
|
const baseProps = {
|
|
fk_grp_col_id: null,
|
|
fk_geo_data_col_id: null,
|
|
fk_cover_image_col_id: null,
|
|
calendar_range: [] as Array<{
|
|
fk_from_column_id: string
|
|
fk_to_column_id: string | null
|
|
}>,
|
|
}
|
|
|
|
switch (sourceView.type) {
|
|
case ViewTypes.GALLERY:
|
|
return {
|
|
...baseProps,
|
|
fk_cover_image_col_id: (sourceView.view as GalleryType)?.fk_cover_image_col_id || null,
|
|
}
|
|
case ViewTypes.KANBAN:
|
|
return {
|
|
...baseProps,
|
|
fk_cover_image_col_id: (sourceView.view as KanbanType)?.fk_cover_image_col_id || null,
|
|
fk_grp_col_id: (sourceView.view as KanbanType)?.fk_grp_col_id || null,
|
|
}
|
|
case ViewTypes.MAP:
|
|
return {
|
|
...baseProps,
|
|
fk_geo_data_col_id: (sourceView.view as MapType)?.fk_geo_data_col_id || null,
|
|
}
|
|
case ViewTypes.CALENDAR:
|
|
return {
|
|
...baseProps,
|
|
calendar_range:
|
|
(sourceView.view as CalendarType)?.calendar_range?.map((range) => ({
|
|
fk_from_column_id: range.fk_from_column_id as string,
|
|
fk_to_column_id: range.fk_to_column_id as string,
|
|
})) || [],
|
|
}
|
|
default:
|
|
return baseProps
|
|
}
|
|
}
|
|
|
|
const viewSpecificProps = getViewSpecificProps(view)
|
|
|
|
const duplicateForm: CreateViewForm = {
|
|
title: uniqueTitle,
|
|
type: view.type,
|
|
description: view.description || '',
|
|
copy_from_id: view.id!,
|
|
row_coloring_mode: view.row_coloring_mode!,
|
|
meta: parseProp(view.meta)?.rowColoringInfo ? { rowColoringInfo: parseProp(view.meta).rowColoringInfo } : undefined,
|
|
...viewSpecificProps,
|
|
}
|
|
|
|
return await createView(view.fk_model_id, duplicateForm)
|
|
}
|
|
|
|
const deleteView = async (view: ViewType) => {
|
|
if (!view?.id || !view?.base_id) return
|
|
|
|
const activeViewId = activeView.value?.id
|
|
|
|
try {
|
|
await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'viewDelete',
|
|
viewId: view.id,
|
|
},
|
|
{},
|
|
)
|
|
|
|
const key = getViewsKey(view.base_id, view.fk_model_id)
|
|
|
|
// Remove view from the viewsByTable map
|
|
const tableViews = viewsByTable.value.get(key) || []
|
|
|
|
// Get the first collaborative grid view before delete
|
|
const oldFirstCollabGridView = getFirstNonPersonalView(tableViews, {
|
|
includeViewType: ViewTypes.GRID,
|
|
})
|
|
|
|
const updatedViews = tableViews.filter((v) => v.id !== view.id)
|
|
viewsByTable.value.set(key, updatedViews)
|
|
|
|
// Get the new first collaborative grid view after delete
|
|
const newFirstCollabGridView = getFirstNonPersonalView(updatedViews, {
|
|
includeViewType: ViewTypes.GRID,
|
|
})
|
|
|
|
// If the first collaborative grid view changed after deletion, trigger getMeta
|
|
if (newFirstCollabGridView?.id !== oldFirstCollabGridView?.id && view.fk_model_id) {
|
|
await getMeta(view.base_id, view.fk_model_id, true)
|
|
}
|
|
|
|
// Remove from recent views
|
|
removeFromRecentViews({
|
|
viewId: view.id,
|
|
tableId: view.fk_model_id,
|
|
baseId: view.base_id,
|
|
})
|
|
|
|
// Refresh command palette
|
|
refreshCommandPalette()
|
|
|
|
// Telemetry event
|
|
$e('a:view:delete', { view: view.type })
|
|
|
|
// If we deleted the active view, navigate to default or first view
|
|
if (activeViewId === view.id) {
|
|
const key = getViewsKey(view.base_id, view.fk_model_id)
|
|
const remainingViews = viewsByTable.value.get(key) || []
|
|
const defaultView = remainingViews[0]
|
|
|
|
if (defaultView && activeTable.value) {
|
|
await navigateToView({
|
|
view: defaultView,
|
|
baseId: activeTable.value.base_id!,
|
|
tableId: view.fk_model_id,
|
|
})
|
|
} else {
|
|
ncNavigateTo({
|
|
workspaceId: activeWorkspaceId.value,
|
|
baseId: view.base_id,
|
|
})
|
|
}
|
|
}
|
|
|
|
return true
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
throw e
|
|
}
|
|
}
|
|
|
|
const updateView = async (
|
|
viewId: string,
|
|
updates: Partial<ViewType>,
|
|
extra?: {
|
|
is_default_view?: boolean
|
|
},
|
|
): Promise<ViewType | null> => {
|
|
try {
|
|
const updatedView = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
openedProject.value!.id!,
|
|
{
|
|
operation: 'viewUpdate',
|
|
viewId,
|
|
},
|
|
updates,
|
|
)
|
|
|
|
// Find the table and update the view in the store
|
|
const tableId = activeView.value?.fk_model_id
|
|
const baseId = activeView.value?.base_id
|
|
if (tableId && baseId) {
|
|
const key = getViewsKey(baseId, tableId)
|
|
const tableViews = viewsByTable.value.get(key) || []
|
|
const viewIndex = tableViews.findIndex((v) => v.id === viewId)
|
|
|
|
if (viewIndex !== -1) {
|
|
if (extra?.is_default_view && tableId) {
|
|
await getMeta(baseId, tableId, true)
|
|
}
|
|
|
|
// Replace with the response from API
|
|
tableViews[viewIndex] = updatedView
|
|
viewsByTable.value.set(key, [...tableViews])
|
|
|
|
// Update recent views if title changed
|
|
if (updatedView.title) {
|
|
allRecentViews.value = allRecentViews.value.map((rv) => {
|
|
if (rv.viewId === viewId && rv.tableID === tableId) {
|
|
rv.viewName = updatedView.title
|
|
}
|
|
return rv
|
|
})
|
|
}
|
|
|
|
refreshCommandPalette()
|
|
|
|
return updatedView
|
|
}
|
|
}
|
|
|
|
return updatedView
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
const updateViewMeta = async (
|
|
viewId: string,
|
|
viewType: ViewTypes,
|
|
updates: Record<string, any>,
|
|
args?: {
|
|
skipNetworkCall?: boolean
|
|
},
|
|
): Promise<ViewType | null> => {
|
|
try {
|
|
let updatedView
|
|
|
|
if (!args?.skipNetworkCall) {
|
|
switch (viewType) {
|
|
case ViewTypes.GRID:
|
|
updatedView = await $api.internal.postOperation(
|
|
activeView.value!.fk_workspace_id!,
|
|
activeView.value!.base_id!,
|
|
{ operation: 'gridViewUpdate', viewId },
|
|
updates,
|
|
)
|
|
break
|
|
case ViewTypes.GALLERY:
|
|
updatedView = await $api.internal.postOperation(
|
|
activeView.value!.fk_workspace_id!,
|
|
activeView.value!.base_id!,
|
|
{ operation: 'galleryViewUpdate', viewId },
|
|
updates,
|
|
)
|
|
break
|
|
case ViewTypes.KANBAN:
|
|
updatedView = await $api.internal.postOperation(
|
|
activeView.value!.fk_workspace_id!,
|
|
activeView.value!.base_id!,
|
|
{ operation: 'kanbanViewUpdate', viewId },
|
|
updates,
|
|
)
|
|
break
|
|
case ViewTypes.MAP:
|
|
updatedView = await $api.internal.postOperation(
|
|
activeView.value!.fk_workspace_id!,
|
|
activeView.value!.base_id!,
|
|
{ operation: 'mapViewUpdate', viewId },
|
|
updates,
|
|
)
|
|
break
|
|
case ViewTypes.CALENDAR:
|
|
updatedView = await $api.internal.postOperation(
|
|
activeView.value!.fk_workspace_id!,
|
|
activeView.value!.base_id!,
|
|
{ operation: 'calendarViewUpdate', viewId },
|
|
updates,
|
|
)
|
|
break
|
|
case ViewTypes.FORM:
|
|
updatedView = await $api.internal.postOperation(
|
|
activeView.value!.fk_workspace_id!,
|
|
activeView.value!.base_id!,
|
|
{ operation: 'formViewUpdate', viewId },
|
|
updates,
|
|
)
|
|
break
|
|
default:
|
|
throw new Error(`Unsupported view type for meta update: ${viewType}`)
|
|
}
|
|
} else {
|
|
updatedView = {
|
|
...activeView.value,
|
|
view: {
|
|
...(activeView.value || {}).view,
|
|
...updates,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Find the table and update the view in the store
|
|
const tableId = activeView.value?.fk_model_id
|
|
const baseId = activeView.value?.base_id
|
|
if (tableId && baseId) {
|
|
const key = getViewsKey(baseId, tableId)
|
|
const tableViews = viewsByTable.value.get(key) || []
|
|
const viewIndex = tableViews.findIndex((v) => v.id === viewId)
|
|
|
|
if (viewIndex !== -1) {
|
|
tableViews[viewIndex] = updatedView
|
|
viewsByTable.value.set(key, [...tableViews])
|
|
}
|
|
}
|
|
|
|
if (isPublic.value) {
|
|
sharedView.value = {
|
|
...sharedView.value,
|
|
view: {
|
|
...(sharedView.value?.view || {}),
|
|
...updates,
|
|
},
|
|
}
|
|
}
|
|
|
|
refreshCommandPalette()
|
|
|
|
return updatedView
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
const onViewsTabChange = (page: ViewPageType) => {
|
|
router.push({
|
|
name: 'index-typeOrId-baseId-index-index-viewId-viewTitle-slugs',
|
|
params: {
|
|
typeOrId: route.value.params.typeOrId,
|
|
baseId: route.value.params.baseId,
|
|
viewId: route.value.params.viewId,
|
|
viewTitle: activeViewTitleOrId.value,
|
|
slugs: [activeViewReadableUrlSlug.value, ...(page !== 'view' ? [page] : [])],
|
|
},
|
|
})
|
|
}
|
|
|
|
const changeView = async ({ viewId, tableId, baseId }: { viewId: string | null; tableId: string; baseId: string }) => {
|
|
const routeName = 'index-typeOrId-baseId-index-index-viewId-viewTitle'
|
|
await router.push({ name: routeName, params: { viewTitle: viewId || '', viewId: tableId, baseId } })
|
|
}
|
|
|
|
function removeFromRecentViews({ viewId, tableId, baseId }: { viewId?: string | undefined; tableId: string; baseId?: string }) {
|
|
if (baseId && !viewId && !tableId) {
|
|
allRecentViews.value = allRecentViews.value.filter((f) => f.baseId !== baseId)
|
|
} else if (baseId && tableId && !viewId) {
|
|
allRecentViews.value = allRecentViews.value.filter((f) => f.baseId !== baseId || f.tableID !== tableId)
|
|
} else if (tableId && viewId) {
|
|
allRecentViews.value = allRecentViews.value.filter((f) => f.viewId !== viewId || f.tableID !== tableId)
|
|
}
|
|
}
|
|
|
|
const updateTabTitle = () => {
|
|
if (!activeView.value || !activeView.value.base_id) {
|
|
if (openedProject.value?.title) {
|
|
useTitle(openedProject.value?.title)
|
|
}
|
|
return
|
|
}
|
|
|
|
const tableName = tablesStore.baseTables
|
|
.get(activeView.value.base_id)
|
|
?.find((t) => t.id === activeView.value.fk_model_id)?.title
|
|
|
|
const baseName = bases.basesList.find((p) => p.id === activeView.value.base_id)?.title
|
|
|
|
useTitle(
|
|
getFormattedViewTabTitle({
|
|
viewName: activeView.value.title,
|
|
tableName: tableName || '',
|
|
baseName: baseName || '',
|
|
isSharedView: !!sharedView.value?.id,
|
|
}),
|
|
)
|
|
}
|
|
|
|
const updateViewCoverImageColumnId = ({
|
|
columnIds,
|
|
metaId,
|
|
baseId,
|
|
}: {
|
|
columnIds: Set<string>
|
|
metaId: string
|
|
baseId: string
|
|
}) => {
|
|
const key = getViewsKey(baseId, metaId)
|
|
if (!viewsByTable.value.get(key)) return
|
|
|
|
let isColumnUsedAsCoverImage = false
|
|
|
|
for (const view of viewsByTable.value.get(key) || []) {
|
|
if (
|
|
[_ViewTypes.GALLERY, _ViewTypes.KANBAN].includes(view.type) &&
|
|
view.view?.fk_cover_image_col_id &&
|
|
columnIds.has(view.view?.fk_cover_image_col_id)
|
|
) {
|
|
isColumnUsedAsCoverImage = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!isColumnUsedAsCoverImage) return
|
|
|
|
viewsByTable.value.set(
|
|
key,
|
|
(viewsByTable.value.get(key) || [])
|
|
.map((view) => {
|
|
if (
|
|
[_ViewTypes.GALLERY, _ViewTypes.KANBAN].includes(view.type) &&
|
|
view.view?.fk_cover_image_col_id &&
|
|
columnIds.has(view.view?.fk_cover_image_col_id)
|
|
) {
|
|
view.view.fk_cover_image_col_id = null
|
|
}
|
|
return view
|
|
})
|
|
.sort((a, b) => a.order! - b.order!),
|
|
)
|
|
}
|
|
|
|
const setCurrentViewExpandedFormMode = async (viewId: string, mode: 'field' | 'attachment', columnId?: string) => {
|
|
/**
|
|
* Update value only if it is EeUI and active view
|
|
*/
|
|
if (!isEeUI || !viewId || activeView.value?.id !== viewId) return
|
|
|
|
try {
|
|
if (isUIAllowed('viewCreateOrEdit')) {
|
|
await updateView(viewId, {
|
|
expanded_record_mode: mode,
|
|
attachment_mode_column_id: columnId,
|
|
})
|
|
}
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
}
|
|
}
|
|
|
|
const setCurrentViewExpandedFormAttachmentColumn = async (viewId: string, columnId: string) => {
|
|
/**
|
|
* Update value only if it is EeUI and active view
|
|
*/
|
|
if (!isEeUI || !viewId || activeView.value?.id !== viewId) return
|
|
|
|
try {
|
|
if (isUIAllowed('viewCreateOrEdit')) {
|
|
await updateView(viewId, {
|
|
attachment_mode_column_id: columnId,
|
|
})
|
|
}
|
|
|
|
Object.assign(activeView.value, { attachment_mode_column_id: columnId })
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
}
|
|
}
|
|
|
|
const onOpenViewCreateModal = ({
|
|
title = '',
|
|
type,
|
|
copyViewId,
|
|
groupingFieldColumnId,
|
|
calendarRange,
|
|
coverImageColumnId,
|
|
baseId,
|
|
tableId,
|
|
sourceId,
|
|
}: {
|
|
title?: string
|
|
type: ViewTypes | 'AI'
|
|
copyViewId?: string
|
|
groupingFieldColumnId?: string
|
|
calendarRange?: Array<{
|
|
fk_from_column_id: string
|
|
fk_to_column_id: string | null
|
|
}>
|
|
coverImageColumnId?: string
|
|
baseId: string
|
|
tableId: string
|
|
sourceId?: string
|
|
}) => {
|
|
if (!baseId || !tableId) return
|
|
|
|
const isDlgOpen = ref(true)
|
|
|
|
const { close } = useDialog(DlgViewCreate, {
|
|
'modelValue': isDlgOpen,
|
|
title,
|
|
type,
|
|
'tableId': tableId,
|
|
'selectedViewId': copyViewId,
|
|
calendarRange,
|
|
groupingFieldColumnId,
|
|
coverImageColumnId,
|
|
'onUpdate:modelValue': closeDialog,
|
|
'baseId': baseId,
|
|
'sourceId': sourceId,
|
|
'onCreated': async (view?: ViewType) => {
|
|
closeDialog()
|
|
|
|
refreshCommandPalette()
|
|
|
|
if (view) {
|
|
navigateToView({
|
|
view,
|
|
tableId,
|
|
baseId,
|
|
doNotSwitchTab: true,
|
|
})
|
|
}
|
|
|
|
$e('a:view:create', { view: view?.type || type })
|
|
},
|
|
})
|
|
|
|
function closeDialog() {
|
|
isDlgOpen.value = false
|
|
|
|
close(1000)
|
|
}
|
|
}
|
|
|
|
const isUserViewOwner = (view?: ViewType, _user: User | null = user.value) => {
|
|
if (!view || !_user) return false
|
|
|
|
return (
|
|
view?.owned_by === _user?.id ||
|
|
!!(!view?.owned_by && (_user?.base_roles?.[ProjectRoles.OWNER] || _user?.workspace_roles?.[WorkspaceUserRoles.OWNER]))
|
|
)
|
|
}
|
|
|
|
const getCopyViewConfigBtnAccessStatus = (view: ViewType, from: 'view-action-menu' | 'toolbar' = 'view-action-menu') => {
|
|
const result = {
|
|
isDisabled: false,
|
|
tooltip: '',
|
|
isVisible: isEeUI && isUIAllowed('viewCreateOrEdit'),
|
|
}
|
|
|
|
if (!view) return result
|
|
|
|
if (view?.lock_type === LockType.Personal && !isUserViewOwner(view)) {
|
|
result.isDisabled = true
|
|
result.tooltip = t('tooltip.onlyViewOwnerCanCopyViewConfig')
|
|
|
|
if (from === 'toolbar') {
|
|
result.isVisible = false
|
|
}
|
|
} else if (view?.lock_type === LockType.Locked) {
|
|
result.isDisabled = true
|
|
result.tooltip = t('title.thisViewIsLockType', {
|
|
type: t(viewLockIcons[view?.lock_type]?.title).toLowerCase(),
|
|
})
|
|
|
|
if (from === 'toolbar') {
|
|
result.isVisible = false
|
|
}
|
|
} else if (view.base_id) {
|
|
const key = getViewsKey(view.base_id, view.fk_model_id)
|
|
if ((viewsByTable.value.get(key) || []).length < 2) {
|
|
result.isDisabled = true
|
|
result.tooltip = t('tooltip.youNeedAtLeastOneExistingViewToCopyConfigurations')
|
|
|
|
if (from === 'toolbar') {
|
|
result.isVisible = false
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
const onOpenCopyViewConfigFromAnotherViewModal = ({
|
|
defaultOptions,
|
|
destView = activeView.value,
|
|
onCopy = (_: ViewSettingOverrideOptions[]) => undefined,
|
|
}: {
|
|
defaultOptions?: ViewSettingOverrideOptions[]
|
|
destView?: ViewType
|
|
onCopy?: (selectedCopyViewConfigTypes: ViewSettingOverrideOptions[]) => void
|
|
} = {}) => {
|
|
if (!destView || !isEeUI || !isUIAllowed('viewCreateOrEdit')) return
|
|
|
|
// If destination view is locked or if personal and user is not the owner or if table has only one view then return
|
|
if (getCopyViewConfigBtnAccessStatus(destView).isDisabled) {
|
|
return
|
|
}
|
|
|
|
const isOpen = ref(true)
|
|
|
|
const { close } = useDialog(DlgViewCopyViewConfigFromAnotherView, {
|
|
'modelValue': isOpen,
|
|
'onUpdate:modelValue': closeDialog,
|
|
'destView': destView,
|
|
'defaultSelectedCopyViewConfigTypes': defaultOptions,
|
|
'onCopy': onCopy,
|
|
})
|
|
|
|
function closeDialog() {
|
|
isOpen.value = false
|
|
close(1000)
|
|
}
|
|
}
|
|
|
|
const copyViewConfigurationFromAnotherView = async (
|
|
destView: ViewType,
|
|
sourceViewId: string,
|
|
settingToOverride: ViewSettingOverrideOptions[],
|
|
) => {
|
|
if (!sourceViewId || settingToOverride.length === 0 || !destView) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const res = await $api.internal.postOperation(
|
|
activeWorkspaceId.value!,
|
|
destView.base_id!,
|
|
{
|
|
operation: 'viewSettingOverride',
|
|
},
|
|
{
|
|
destinationViewId: destView.id!,
|
|
sourceViewId,
|
|
settingToOverride,
|
|
},
|
|
)
|
|
|
|
const defaultView = getFirstNonPersonalView(views.value, {
|
|
includeViewType: ViewTypes.GRID,
|
|
})
|
|
|
|
if (
|
|
defaultView?.id === destView.id &&
|
|
[ViewSettingOverrideOptions.FIELD_ORDER, ViewSettingOverrideOptions.FIELD_VISIBILITY].some((type) =>
|
|
settingToOverride.includes(type),
|
|
)
|
|
) {
|
|
// default view col order and visibility is stored in column meta so we have to load it again
|
|
await getMeta(destView.base_id!, destView.fk_model_id!, true)
|
|
}
|
|
|
|
if (res?.view && destView.fk_model_id && destView.base_id) {
|
|
const key = getViewsKey(destView.base_id, destView.fk_model_id)
|
|
const tableViews = viewsByTable.value.get(key) || []
|
|
const viewIndex = tableViews.findIndex((v) => v.id === destView.id)
|
|
|
|
if (viewIndex !== -1) {
|
|
// Replace with the response from API
|
|
tableViews[viewIndex] = res.view
|
|
viewsByTable.value.set(key, [...tableViews])
|
|
|
|
refreshCommandPalette()
|
|
}
|
|
}
|
|
|
|
// Reload view meta as well as data if the destination view is the active view
|
|
if (destView.id === activeView.value?.id) {
|
|
$eventBus.smartsheetStoreEventBus.emit(SmartsheetStoreEvents.COPIED_VIEW_CONFIG, {
|
|
viewId: destView.id,
|
|
copiedOptions: settingToOverride,
|
|
})
|
|
|
|
$eventBus.smartsheetStoreEventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD, {
|
|
callback: () => {
|
|
// Load data after fields reload
|
|
forcedNextTick(() => {
|
|
$eventBus.smartsheetStoreEventBus.emit(SmartsheetStoreEvents.DATA_RELOAD)
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
message.toast(t('objects.copyViewConfig.viewConfigurationCopied'))
|
|
|
|
return true
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
const errorInfo = await extractSdkResponseErrorMsgv2(e)
|
|
|
|
if (errorInfo.error === NcErrorType.ERR_FEATURE_NOT_SUPPORTED) {
|
|
message.error(errorInfo.message)
|
|
} else {
|
|
message.error(t('objects.copyViewConfig.errorOccuredWhileCopyingViewConfiguration'), undefined, {
|
|
copyText: errorInfo.message,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
function getViewReadableUrlSlug({ tableTitle, viewOrViewTitle }: { tableTitle?: string; viewOrViewTitle: ViewType | string }) {
|
|
const viewTitle = ncIsObject(viewOrViewTitle) ? viewOrViewTitle.title : viewOrViewTitle
|
|
|
|
return toReadableUrlSlug([tableTitle, viewTitle])
|
|
}
|
|
|
|
async function hasOnlyOneGridViewInTable(tableId: string) {
|
|
await loadViews({
|
|
tableId,
|
|
})
|
|
|
|
// Find the key in viewsByTable that matches this tableId
|
|
let key: string | undefined
|
|
for (const [k] of viewsByTable.value) {
|
|
if (k.endsWith(`:${tableId}`)) {
|
|
key = k
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!key) return false
|
|
|
|
const grids = viewsByTable.value.get(key)?.filter((v) => v.type === ViewTypes.GRID && v.lock_type !== LockType.Personal)
|
|
return grids?.length === 1
|
|
}
|
|
|
|
watch(
|
|
() => tablesStore.activeTableId,
|
|
async (newId, oldId) => {
|
|
if (newId === oldId) return
|
|
if (isPublic.value) {
|
|
isViewsLoading.value = false
|
|
return
|
|
}
|
|
|
|
isViewDataLoading.value = true
|
|
|
|
try {
|
|
if (tablesStore.activeTable) tablesStore.activeTable.isViewsLoading = true
|
|
|
|
await loadViews()
|
|
} catch (e) {
|
|
console.error(e)
|
|
} finally {
|
|
if (tablesStore.activeTable) tablesStore.activeTable.isViewsLoading = false
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(activeView, (view) => {
|
|
if (!view) return
|
|
if (!view.base_id) return
|
|
|
|
const tableName = tablesStore.baseTables.get(view.base_id)?.find((t) => t.id === view.fk_model_id)?.title
|
|
|
|
const base = bases.basesList.find((p) => p.id === view.base_id)
|
|
allRecentViews.value = [
|
|
{
|
|
viewId: view.id,
|
|
baseId: view.base_id as string,
|
|
tableID: view.fk_model_id,
|
|
viewName: view.title,
|
|
viewType: view.type,
|
|
workspaceId: activeWorkspaceId.value,
|
|
tableName: tableName as string,
|
|
baseName: base?.title as string,
|
|
managed_app_master: base?.managed_app_master,
|
|
managed_app_id: base?.managed_app_id,
|
|
iconColor: parseProp(base?.meta).iconColor,
|
|
},
|
|
...allRecentViews.value.filter((f) => f.viewId !== view.id || f.tableID !== view.fk_model_id),
|
|
]
|
|
})
|
|
|
|
watch(
|
|
() => [activeView.value?.title, activeView.value?.id],
|
|
() => {
|
|
updateTabTitle()
|
|
},
|
|
{
|
|
flush: 'post',
|
|
},
|
|
)
|
|
|
|
/**
|
|
* Keeps the browser URL slug in sync with the view's readable slug.
|
|
* Triggers only when:
|
|
* - The current browser URL slug is missing, OR
|
|
* - The browser URL slug does not match the view's readable slug.
|
|
*/
|
|
watch(
|
|
[activeViewReadableUrlSlug, activeViewUrlSlug],
|
|
([newactiveViewReadableUrlSlug, newActiveViewUrlSlug]) => {
|
|
if (!newactiveViewReadableUrlSlug || newActiveViewUrlSlug === newactiveViewReadableUrlSlug) return
|
|
|
|
const slugs = (route.value.params.slugs as string[]) || []
|
|
|
|
const newSlug = [newactiveViewReadableUrlSlug]
|
|
|
|
if (slugs.length > 1) {
|
|
newSlug.push(...slugs.slice(1))
|
|
}
|
|
|
|
router.replace({
|
|
name: 'index-typeOrId-baseId-index-index-viewId-viewTitle-slugs',
|
|
params: {
|
|
...route.value.params,
|
|
viewTitle: route.value.params.viewTitle || activeView.value?.id,
|
|
slugs: newSlug,
|
|
},
|
|
query: route.value.query,
|
|
force: true,
|
|
})
|
|
},
|
|
{
|
|
immediate: true,
|
|
flush: 'post',
|
|
},
|
|
)
|
|
|
|
refreshViewTabTitle.on(() => {
|
|
updateTabTitle()
|
|
})
|
|
|
|
return {
|
|
// State
|
|
isLockedView,
|
|
isViewsLoading,
|
|
isViewDataLoading,
|
|
isPaginationLoading,
|
|
recentViews,
|
|
allRecentViews,
|
|
views,
|
|
activeView,
|
|
openedViewsTab,
|
|
viewsByTable,
|
|
activeViewTitleOrId,
|
|
activeSorts,
|
|
activeNestedFilters,
|
|
isActiveViewLocked,
|
|
preFillFormSearchParams,
|
|
lastOpenedViewId,
|
|
activeViewRowColorInfo,
|
|
sharedView,
|
|
isActiveViewFieldHeaderVisible,
|
|
|
|
// Methods
|
|
createView,
|
|
updateView,
|
|
updateViewMeta,
|
|
deleteView,
|
|
loadViews,
|
|
onViewsTabChange,
|
|
navigateToView,
|
|
changeView,
|
|
hasOnlyOneGridViewInTable,
|
|
removeFromRecentViews,
|
|
refreshViewTabTitle: refreshViewTabTitle.trigger,
|
|
updateViewCoverImageColumnId,
|
|
duplicateView,
|
|
setCurrentViewExpandedFormMode,
|
|
setCurrentViewExpandedFormAttachmentColumn,
|
|
onOpenViewCreateModal,
|
|
getViewReadableUrlSlug,
|
|
onOpenCopyViewConfigFromAnotherViewModal,
|
|
copyViewConfigurationFromAnotherView,
|
|
isUserViewOwner,
|
|
getCopyViewConfigBtnAccessStatus,
|
|
isShowEveryonePersonalViewsEnabled,
|
|
}
|
|
})
|
|
|
|
if (import.meta.hot) {
|
|
import.meta.hot.accept(acceptHMRUpdate(useViewsStore as any, import.meta.hot))
|
|
}
|