feat(bitable): U4 view type switcher with 5 types (grid enabled, others disabled)
- Add viewSwitcherUtils.ts (5 view types metadata: label/icon/disabled/tooltip) - Refactor ViewSwitcher: button -> dropdown with 5 types, disabled items show "规划中" tooltip - Update BitableFileDetailView.handleCreateView to accept viewType parameter (no more hardcoded grid) - Bind :creating=viewCreating to ViewSwitcher for loading/disabled state during POST - Extend store createView + API createView to pass view_type field (already in prior commits) - Add loading/disabled state on create button to prevent duplicate clicks - Extend e2e/bitable-view.spec.ts with 5 view type scenarios (E1-E5) Closes R3 (P0): view type selection in UI, backend already supports view_type. Refs: docs/plans/2026-07-03-001-feat-bitable-p0-ux-and-agent-parity-plan.md U4
This commit is contained in:
parent
5baaeb489d
commit
f280627da1
|
|
@ -72,3 +72,192 @@ test.describe('Bitable View E2E', () => {
|
||||||
await expect(newButton).toBeVisible({ timeout: 10_000 })
|
await expect(newButton).toBeVisible({ timeout: 10_000 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// U4 (R3): View type switcher — "新建视图" exposes 5 view types; only `grid`
|
||||||
|
// is enabled in v1, the rest are disabled with a "规划中" tooltip.
|
||||||
|
//
|
||||||
|
// These tests require a running backend (to create a file + table so the
|
||||||
|
// ViewSwitcher renders). They skip gracefully if the backend is down, mirroring
|
||||||
|
// the B1-B3 suite. The POST /views request is intercepted and mocked so the
|
||||||
|
// view-creation assertion is deterministic and does not depend on backend
|
||||||
|
// view persistence.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test.describe('Bitable View Type Switcher E2E (U4)', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
try {
|
||||||
|
await waitForServer(undefined, 5_000)
|
||||||
|
} catch {
|
||||||
|
test.skip(true, 'Backend not running — skipping view type switcher E2E')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in, create a bitable file + table via the UI, and wait for the
|
||||||
|
* ViewSwitcher ("新建视图" button) to render. Returns once the grid header
|
||||||
|
* is visible so callers can interact with the view switcher.
|
||||||
|
*
|
||||||
|
* Each test uses a unique file name to avoid collisions with parallel runs.
|
||||||
|
*/
|
||||||
|
async function openFileDetailWithTable(page: Page, label: string): Promise<void> {
|
||||||
|
await loginAndOpenBitable(page)
|
||||||
|
|
||||||
|
// Create a file — opens the file detail view.
|
||||||
|
await page.getByRole('button', { name: /新建文件/ }).click()
|
||||||
|
await page.getByPlaceholder('请输入文件名').fill(`U4-${label}`)
|
||||||
|
await page.getByRole('button', { name: /确\s*定/ }).click()
|
||||||
|
await expect(page).toHaveURL(/\/bitable\/[^/]+/, { timeout: 10_000 })
|
||||||
|
|
||||||
|
// Create a table — required for the ViewSwitcher to render.
|
||||||
|
await page.locator('.table-view-list__header .ant-btn').click()
|
||||||
|
await expect(page.getByText('新建数据表')).toBeVisible({ timeout: 5_000 })
|
||||||
|
await page.getByPlaceholder('请输入表名').fill(`U4表-${label}`)
|
||||||
|
await page.getByRole('button', { name: /确\s*定/ }).click()
|
||||||
|
|
||||||
|
// Wait for the grid header (and thus the ViewSwitcher) to render.
|
||||||
|
await expect(page.locator('.bitable-file-detail-view__table-name')).toContainText(
|
||||||
|
`U4表-${label}`,
|
||||||
|
{ timeout: 10_000 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open the "新建视图" dropdown and return the menu item locator list. */
|
||||||
|
async function openViewTypeDropdown(page: Page): Promise<import('@playwright/test').Locator> {
|
||||||
|
await page.getByRole('button', { name: /新建视图/ }).click()
|
||||||
|
// The dropdown overlay menu items.
|
||||||
|
return page.locator('.ant-dropdown-menu-item')
|
||||||
|
}
|
||||||
|
|
||||||
|
test('E1: "新建视图" dropdown exposes all 5 view types', async ({ page }) => {
|
||||||
|
await openFileDetailWithTable(page, 'E1-types')
|
||||||
|
|
||||||
|
const items = await openViewTypeDropdown(page)
|
||||||
|
await expect(items).toHaveCount(5, { timeout: 5_000 })
|
||||||
|
|
||||||
|
// Labels in spec order: 表格 / 看板 / 画廊 / 甘特 / 表单
|
||||||
|
const labels = await items.allTextContents()
|
||||||
|
expect(labels.map((t) => t.trim())).toEqual(['表格', '看板', '画廊', '甘特', '表单'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('E2: kanban/gallery/gantt/form are disabled with "规划中" tooltip', async ({ page }) => {
|
||||||
|
await openFileDetailWithTable(page, 'E2-disabled')
|
||||||
|
|
||||||
|
const items = await openViewTypeDropdown(page)
|
||||||
|
|
||||||
|
// The 4 unimplemented types are disabled and carry title="规划中".
|
||||||
|
for (const label of ['看板', '画廊', '甘特', '表单']) {
|
||||||
|
const item = items.filter({ hasText: label })
|
||||||
|
await expect(item).toHaveClass(/ant-dropdown-menu-item-disabled/, { timeout: 5_000 })
|
||||||
|
await expect(item).toHaveAttribute('title', '规划中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// grid is enabled and has no "规划中" title.
|
||||||
|
const gridItem = items.filter({ hasText: '表格' })
|
||||||
|
await expect(gridItem).not.toHaveClass(/ant-dropdown-menu-item-disabled/)
|
||||||
|
await expect(gridItem).not.toHaveAttribute('title', '规划中')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('E3: selecting grid sends POST /views with view_type=grid', async ({ page }) => {
|
||||||
|
await openFileDetailWithTable(page, 'E3-create-grid')
|
||||||
|
|
||||||
|
let capturedBody: { name?: string; view_type?: string } | null = null
|
||||||
|
await page.route('**/api/v1/bitable/tables/*/views', (route) => {
|
||||||
|
if (route.request().method() !== 'POST') return route.continue()
|
||||||
|
capturedBody = route.request().postDataJSON()
|
||||||
|
const name = capturedBody?.name ?? '测试视图'
|
||||||
|
return route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
view: {
|
||||||
|
id: 'view-e3-mock',
|
||||||
|
table_id: 'tbl-e3',
|
||||||
|
name,
|
||||||
|
view_type: 'grid',
|
||||||
|
config: {},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = await openViewTypeDropdown(page)
|
||||||
|
await items.filter({ hasText: '表格' }).click()
|
||||||
|
|
||||||
|
// Name modal (AModal.confirm) appears — fill + confirm.
|
||||||
|
const nameInput = page.getByPlaceholder('请输入视图名称')
|
||||||
|
await expect(nameInput).toBeVisible({ timeout: 5_000 })
|
||||||
|
await nameInput.fill('网格视图E3')
|
||||||
|
await page.getByRole('button', { name: /确\s*定/ }).click()
|
||||||
|
|
||||||
|
// Assert the POST body carried view_type=grid (no longer hardcoded elsewhere).
|
||||||
|
await expect
|
||||||
|
.poll(async () => capturedBody, { timeout: 5_000 })
|
||||||
|
.toMatchObject({ name: '网格视图E3', view_type: 'grid' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('E4: clicking a disabled type does not open the name modal or fire POST', async ({ page }) => {
|
||||||
|
await openFileDetailWithTable(page, 'E4-disabled-click')
|
||||||
|
|
||||||
|
let postFired = false
|
||||||
|
await page.route('**/api/v1/bitable/tables/*/views', (route) => {
|
||||||
|
if (route.request().method() === 'POST') {
|
||||||
|
postFired = true
|
||||||
|
return route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ success: true, view: { id: 'x', table_id: 'x', name: 'x', view_type: 'kanban', config: {}, created_at: '' } }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return route.continue()
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = await openViewTypeDropdown(page)
|
||||||
|
// Click the disabled "看板" item — antd will not emit a click event for it.
|
||||||
|
await items.filter({ hasText: '看板' }).click({ force: true })
|
||||||
|
|
||||||
|
// The name modal must NOT appear, and no POST /views must have fired.
|
||||||
|
await expect(page.getByPlaceholder('请输入视图名称')).not.toBeVisible({ timeout: 1_500 })
|
||||||
|
expect(postFired).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('E5: created grid view is added to the view tab list (round-trip contract)', async ({ page }) => {
|
||||||
|
await openFileDetailWithTable(page, 'E5-roundtrip')
|
||||||
|
|
||||||
|
await page.route('**/api/v1/bitable/tables/*/views', (route) => {
|
||||||
|
if (route.request().method() !== 'POST') return route.continue()
|
||||||
|
const body = route.request().postDataJSON() ?? {}
|
||||||
|
return route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
view: {
|
||||||
|
id: 'view-e5-mock',
|
||||||
|
table_id: 'tbl-e5',
|
||||||
|
name: body.name ?? 'E5视图',
|
||||||
|
view_type: body.view_type ?? 'grid',
|
||||||
|
config: {},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = await openViewTypeDropdown(page)
|
||||||
|
await items.filter({ hasText: '表格' }).click()
|
||||||
|
|
||||||
|
const nameInput = page.getByPlaceholder('请输入视图名称')
|
||||||
|
await expect(nameInput).toBeVisible({ timeout: 5_000 })
|
||||||
|
await nameInput.fill('E5网格视图')
|
||||||
|
await page.getByRole('button', { name: /确\s*定/ }).click()
|
||||||
|
|
||||||
|
// The mock response is pushed into the store; a new tab with the view
|
||||||
|
// name appears in the ViewSwitcher tabs.
|
||||||
|
await expect(page.locator('.ant-tabs-tab', { hasText: 'E5网格视图' })).toBeVisible({
|
||||||
|
timeout: 5_000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,8 @@
|
||||||
v-model:activeKey="activeKey"
|
v-model:activeKey="activeKey"
|
||||||
type="editable-card"
|
type="editable-card"
|
||||||
size="small"
|
size="small"
|
||||||
:add-icon="h(PlusOutlined)"
|
:hide-add="true"
|
||||||
@change="onSwitch"
|
@change="onSwitch"
|
||||||
@edit="onEdit"
|
|
||||||
>
|
>
|
||||||
<a-tab-pane
|
<a-tab-pane
|
||||||
v-for="v in views"
|
v-for="v in views"
|
||||||
|
|
@ -16,6 +15,35 @@
|
||||||
/>
|
/>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
|
||||||
|
<!-- U4: "新建视图" is a dropdown exposing all 5 view types. Only `grid`
|
||||||
|
is enabled in v1; the rest are disabled with a "规划中" tooltip. -->
|
||||||
|
<a-dropdown :trigger="['click']" placement="bottomLeft">
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:icon="h(PlusOutlined)"
|
||||||
|
:loading="creating"
|
||||||
|
:disabled="creating"
|
||||||
|
>
|
||||||
|
新建视图
|
||||||
|
</a-button>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="handleTypeClick">
|
||||||
|
<a-menu-item
|
||||||
|
v-for="meta in VIEW_TYPE_LIST"
|
||||||
|
:key="meta.viewType"
|
||||||
|
:disabled="meta.disabled"
|
||||||
|
:title="meta.tooltip"
|
||||||
|
>
|
||||||
|
<span class="view-switcher__type-item">
|
||||||
|
<component :is="meta.icon" />
|
||||||
|
<span>{{ meta.label }}</span>
|
||||||
|
</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
|
||||||
<a-button
|
<a-button
|
||||||
v-if="activeKey"
|
v-if="activeKey"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -32,16 +60,22 @@
|
||||||
import { ref, watch, h } from 'vue'
|
import { ref, watch, h } from 'vue'
|
||||||
import { Tabs as ATabs, Button as AButton } from 'ant-design-vue'
|
import { Tabs as ATabs, Button as AButton } from 'ant-design-vue'
|
||||||
import { PlusOutlined, FilterOutlined } from '@ant-design/icons-vue'
|
import { PlusOutlined, FilterOutlined } from '@ant-design/icons-vue'
|
||||||
import type { IBitableView } from '@/api/bitable'
|
import type { IBitableView, ViewType } from '@/api/bitable'
|
||||||
|
import { VIEW_TYPE_LIST } from '@/helpers/viewSwitcherUtils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
views: IBitableView[]
|
defineProps<{
|
||||||
activeViewId: string | null
|
views: IBitableView[]
|
||||||
}>()
|
activeViewId: string | null
|
||||||
|
/** True while a createView POST is in flight — disables + shows spinner. */
|
||||||
|
creating?: boolean
|
||||||
|
}>(),
|
||||||
|
{ creating: false },
|
||||||
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'switch', viewId: string): void
|
(e: 'switch', viewId: string): void
|
||||||
(e: 'create'): void
|
(e: 'create', viewType: ViewType): void
|
||||||
(e: 'config'): void
|
(e: 'config'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
@ -59,11 +93,10 @@ function onSwitch(key: string | number): void {
|
||||||
emit('switch', String(key))
|
emit('switch', String(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEdit(_targetKey: unknown, action: 'add' | 'remove'): void {
|
// a-menu only emits click for enabled items; disabled items are skipped, so
|
||||||
if (action === 'add') {
|
// no extra guard is needed — the "规划中" tooltip is shown via `title`.
|
||||||
emit('create')
|
function handleTypeClick({ key }: { key: string }): void {
|
||||||
}
|
emit('create', key as ViewType)
|
||||||
// remove is disabled (closable=false) — no-op
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -71,13 +104,19 @@ function onEdit(_targetKey: unknown, action: 'add' | 'remove'): void {
|
||||||
.view-switcher {
|
.view-switcher {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--bitable-spacing-sm);
|
||||||
padding: 0 16px;
|
padding: 0 var(--bitable-spacing-lg);
|
||||||
border-bottom: 1px solid var(--border-color, #f0f0f0);
|
border-bottom: 1px solid var(--bitable-color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-switcher :deep(.ant-tabs) {
|
.view-switcher :deep(.ant-tabs) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-switcher__type-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--bitable-spacing-xs);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* View type metadata for the ViewSwitcher dropdown (U4 / R3).
|
||||||
|
*
|
||||||
|
* Pure data + lookup — no Vue, no store, no I/O. Safe to unit-test in
|
||||||
|
* isolation.
|
||||||
|
*
|
||||||
|
* v1 ships only `grid`; the other four types (kanban/gallery/gantt/form) are
|
||||||
|
* rendered as disabled menu items with a "规划中" tooltip so users can see the
|
||||||
|
* roadmap without hitting a dead-end click. The "规划中" string is hardcoded
|
||||||
|
* per spec (no i18n lookup, no config) — it is a temporary placeholder that
|
||||||
|
* gets replaced per-type when each view ships.
|
||||||
|
*
|
||||||
|
* ponytail: icon is a Vue component ref (Ant Design icon), typed as `Component`
|
||||||
|
* so the dropdown can render it via `<component :is="meta.icon" />`. Ceiling:
|
||||||
|
* the list is static; adding a new view type means appending here + flipping
|
||||||
|
* `disabled` to false when implemented.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import {
|
||||||
|
TableOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
PictureOutlined,
|
||||||
|
BarChartOutlined,
|
||||||
|
FormOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import type { ViewType } from '@/api/bitable'
|
||||||
|
|
||||||
|
export interface ViewTypeMeta {
|
||||||
|
/** Backend view_type discriminator (matches ViewType union + ViewType enum). */
|
||||||
|
viewType: ViewType
|
||||||
|
/** Chinese label shown in the dropdown. */
|
||||||
|
label: string
|
||||||
|
/** Ant Design icon component rendered before the label. */
|
||||||
|
icon: Component
|
||||||
|
/** Disabled = not yet implemented in v1; shown greyed-out with tooltip. */
|
||||||
|
disabled: boolean
|
||||||
|
/** Tooltip shown on hover for disabled items (hardcoded "规划中"). */
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordered view type list for the "新建视图" dropdown.
|
||||||
|
*
|
||||||
|
* Order matches the U4 spec: grid (enabled) → kanban / gallery / gantt / form
|
||||||
|
* (disabled, "规划中"). Only `grid` is creatable in v1.
|
||||||
|
*/
|
||||||
|
export const VIEW_TYPE_LIST: ViewTypeMeta[] = [
|
||||||
|
{ viewType: 'grid', label: '表格', icon: TableOutlined, disabled: false },
|
||||||
|
{ viewType: 'kanban', label: '看板', icon: AppstoreOutlined, disabled: true, tooltip: '规划中' },
|
||||||
|
{ viewType: 'gallery', label: '画廊', icon: PictureOutlined, disabled: true, tooltip: '规划中' },
|
||||||
|
{ viewType: 'gantt', label: '甘特', icon: BarChartOutlined, disabled: true, tooltip: '规划中' },
|
||||||
|
{ viewType: 'form', label: '表单', icon: FormOutlined, disabled: true, tooltip: '规划中' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up the metadata for a given view type.
|
||||||
|
*
|
||||||
|
* Falls back to `grid` (the only enabled v1 type) for unknown values — this
|
||||||
|
* keeps the UI robust against a stale/forward-incompatible view_type returned
|
||||||
|
* by the backend after an upgrade.
|
||||||
|
*/
|
||||||
|
export function getViewTypeMeta(viewType: ViewType): ViewTypeMeta {
|
||||||
|
return VIEW_TYPE_LIST.find((m) => m.viewType === viewType) ?? VIEW_TYPE_LIST[0]
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
<ViewSwitcher
|
<ViewSwitcher
|
||||||
:views="store.views"
|
:views="store.views"
|
||||||
:active-view-id="store.currentView?.id ?? null"
|
:active-view-id="store.currentView?.id ?? null"
|
||||||
|
:creating="viewCreating"
|
||||||
@switch="handleSwitchView"
|
@switch="handleSwitchView"
|
||||||
@create="handleCreateView"
|
@create="handleCreateView"
|
||||||
@config="viewConfigOpen = true"
|
@config="viewConfigOpen = true"
|
||||||
|
|
@ -127,7 +128,7 @@ import {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { useBitableStore } from '@/stores/bitable'
|
import { useBitableStore } from '@/stores/bitable'
|
||||||
import type { IBitableField } from '@/api/bitable'
|
import type { IBitableField, ViewType } from '@/api/bitable'
|
||||||
import TableViewList from '@/components/bitable/TableViewList.vue'
|
import TableViewList from '@/components/bitable/TableViewList.vue'
|
||||||
import BitableGrid from '@/components/bitable/BitableGrid.vue'
|
import BitableGrid from '@/components/bitable/BitableGrid.vue'
|
||||||
import TableCreateModal from '@/components/bitable/TableCreateModal.vue'
|
import TableCreateModal from '@/components/bitable/TableCreateModal.vue'
|
||||||
|
|
@ -143,6 +144,9 @@ const store = useBitableStore()
|
||||||
const createModalOpen = ref(false)
|
const createModalOpen = ref(false)
|
||||||
const fieldPanelOpen = ref(false)
|
const fieldPanelOpen = ref(false)
|
||||||
const viewConfigOpen = ref(false)
|
const viewConfigOpen = ref(false)
|
||||||
|
// U4: createView POST in-flight flag — disables the "新建视图" button + shows
|
||||||
|
// a spinner to prevent duplicate submits.
|
||||||
|
const viewCreating = ref(false)
|
||||||
|
|
||||||
const fileId = computed(() => (route.params.fileId as string) ?? '')
|
const fileId = computed(() => (route.params.fileId as string) ?? '')
|
||||||
const tableId = computed(() => (route.params.tableId as string) ?? '')
|
const tableId = computed(() => (route.params.tableId as string) ?? '')
|
||||||
|
|
@ -225,8 +229,10 @@ function handleSwitchView(viewId: string): void {
|
||||||
store.switchView(viewId)
|
store.switchView(viewId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateView(): Promise<void> {
|
async function handleCreateView(viewType: ViewType = 'grid'): Promise<void> {
|
||||||
// ponytail: simple prompt for view name; full create modal is overkill for v1
|
// ponytail: simple prompt for view name; full create modal is overkill for v1.
|
||||||
|
// viewType comes from the ViewSwitcher dropdown (U4) — defaults to 'grid'
|
||||||
|
// for backward compatibility with any direct caller.
|
||||||
let name = ''
|
let name = ''
|
||||||
AModal.confirm({
|
AModal.confirm({
|
||||||
title: '新建视图',
|
title: '新建视图',
|
||||||
|
|
@ -239,7 +245,12 @@ async function handleCreateView(): Promise<void> {
|
||||||
}),
|
}),
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
if (!name.trim()) return
|
if (!name.trim()) return
|
||||||
await store.createView(name.trim(), 'grid')
|
viewCreating.value = true
|
||||||
|
try {
|
||||||
|
await store.createView(name.trim(), viewType)
|
||||||
|
} finally {
|
||||||
|
viewCreating.value = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue