feat(#263): DocWorkspacePage shell — Linked/Inspect/Compare switcher
Replaces the previous 3-tab strip (Ask / Inspect / Chunks) with a
top-right view switcher per the 0.6.1 mockup. The switcher renders
three buttons; Compare is intentionally disabled until 0.9.0 ships
the diff view.
DocMode union shrinks to { 'inspect', 'linked' } — only the modes
that render a tab today. The legacy 'chunks' mode value is renamed
to 'linked' to anticipate the upcoming Linked view refactor (T3);
?mode=chunks remains accepted as a backward-compat alias in parseMode
so existing bookmarks keep working.
'ask' is dropped from the workspace per ADR #259 (Ask moves out of
the doc workspace and is handled separately via the standalone
/reasoning page). The askMode feature flag is preserved in the
registry so the existing reasoning surface stays gateable; it is no
longer part of modeFlags() which now returns { inspect, linked }.
DEFAULT_MODE shifts from 'ask' to 'linked' (the mockup's default
view). MODE_PRIORITY drops to ['linked', 'inspect'].
DocWorkspaceHeader gains an `actions` named slot, used by the
workspace page to inject the switcher inside the sticky header,
matching the mockup layout.
i18n cleanup:
- adds workspace.tabs.linked, workspace.tabs.compare,
workspace.compareSoon
- removes workspace.tabs.ask, workspace.tabs.chunks, and the obsolete
workspace.{askComingSoon,inspectComingSoon,*Hint} placeholders
- renames breadcrumb.mode.chunks -> breadcrumb.mode.linked and drops
breadcrumb.mode.ask
DocAskTab.vue is kept on disk (no longer imported by the workspace)
to avoid touching reasoning-related code in this refactor — its
removal, if any, is the call of the separate Ask-handling track.
Tests:
- modes.test.ts asserts the new isDocMode/parseMode behaviour including
the chunks->linked alias
- resolveMode.test.ts targets the 2-mode priority
- router.test.ts covers both ?mode=inspect and the chunks->linked alias
- feature-flags/store.test.ts asserts modeFlags() returns { inspect, linked }
This commit is contained in:
parent
27e3323bff
commit
72669f5fa8
10 changed files with 164 additions and 103 deletions
|
|
@ -49,17 +49,17 @@ describe('router', () => {
|
|||
const route = router.resolve({
|
||||
name: ROUTES.DOC_WORKSPACE,
|
||||
params: { id: 'abc' },
|
||||
query: { mode: 'chunks' },
|
||||
query: { mode: 'inspect' },
|
||||
})
|
||||
const propsFn = route.matched[0]?.props as
|
||||
| { default?: (r: typeof route) => unknown }
|
||||
| undefined
|
||||
const computed = (propsFn?.default ?? (() => null))(route) as { id: string; mode: string }
|
||||
expect(computed.id).toBe('abc')
|
||||
expect(computed.mode).toBe('chunks')
|
||||
expect(computed.mode).toBe('inspect')
|
||||
})
|
||||
|
||||
it('falls back to ask when mode is unknown', () => {
|
||||
it('falls back to linked when mode is unknown', () => {
|
||||
const router = buildRouter()
|
||||
const route = router.resolve({
|
||||
name: ROUTES.DOC_WORKSPACE,
|
||||
|
|
@ -70,7 +70,21 @@ describe('router', () => {
|
|||
| { default?: (r: typeof route) => unknown }
|
||||
| undefined
|
||||
const computed = (propsFn?.default ?? (() => null))(route) as { mode: string }
|
||||
expect(computed.mode).toBe('ask')
|
||||
expect(computed.mode).toBe('linked')
|
||||
})
|
||||
|
||||
it('maps the legacy ?mode=chunks alias to linked', () => {
|
||||
const router = buildRouter()
|
||||
const route = router.resolve({
|
||||
name: ROUTES.DOC_WORKSPACE,
|
||||
params: { id: 'abc' },
|
||||
query: { mode: 'chunks' },
|
||||
})
|
||||
const propsFn = route.matched[0]?.props as
|
||||
| { default?: (r: typeof route) => unknown }
|
||||
| undefined
|
||||
const computed = (propsFn?.default ?? (() => null))(route) as { mode: string }
|
||||
expect(computed.mode).toBe('linked')
|
||||
})
|
||||
|
||||
it('redirects unknown paths to /', () => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
</svg>
|
||||
<h1 class="workspace-title" :title="doc.filename">{{ doc.filename }}</h1>
|
||||
<StatusBadge :state="doc.lifecycleState" />
|
||||
<div class="workspace-header-actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-header-meta">
|
||||
<div v-if="doc.stores?.length" class="workspace-stores">
|
||||
|
|
@ -79,6 +82,14 @@ defineProps<{
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.workspace-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workspace-header-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -220,16 +220,15 @@ describe('useFeatureFlagStore', () => {
|
|||
expect(store.isEnabled('askMode')).toBe(true)
|
||||
})
|
||||
|
||||
it('modeFlags() returns the three flags in a Record<DocMode, boolean>', async () => {
|
||||
it('modeFlags() returns the two workspace flags in a Record<DocMode, boolean>', async () => {
|
||||
mockApiFetch.mockResolvedValue({
|
||||
status: 'ok',
|
||||
engine: 'local',
|
||||
inspectModeEnabled: true,
|
||||
linkedModeEnabled: false,
|
||||
askModeEnabled: true,
|
||||
})
|
||||
const store = useFeatureFlagStore()
|
||||
await store.load()
|
||||
expect(store.modeFlags()).toEqual({ ask: true, chunks: false, inspect: true })
|
||||
expect(store.modeFlags()).toEqual({ inspect: true, linked: false })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -175,13 +175,10 @@ export const useFeatureFlagStore = defineStore('feature-flags', () => {
|
|||
* workspace mode flags as a `Record<DocMode, boolean>` so the routing
|
||||
* guard does not need to know about the FeatureFlag union.
|
||||
*/
|
||||
function modeFlags(): { ask: boolean; inspect: boolean; chunks: boolean } {
|
||||
// Mode key 'chunks' is kept until T3 (Linked view) renames the DocMode
|
||||
// union and route segment. The flag is already linkedModeEnabled.
|
||||
function modeFlags(): { inspect: boolean; linked: boolean } {
|
||||
return {
|
||||
ask: askModeEnabled.value,
|
||||
inspect: inspectModeEnabled.value,
|
||||
chunks: linkedModeEnabled.value,
|
||||
linked: linkedModeEnabled.value,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,40 +15,45 @@
|
|||
|
||||
<template v-else-if="doc">
|
||||
<!-- Sticky header (#218) -->
|
||||
<DocWorkspaceHeader :doc="doc" />
|
||||
|
||||
<!-- Tab strip (#216) -->
|
||||
<div class="tab-strip" role="tablist" data-e2e="tab-strip">
|
||||
<DocWorkspaceHeader :doc="doc">
|
||||
<template #actions>
|
||||
<!-- View switcher (#263) — Linked / Inspect / Compare. Compare is
|
||||
rendered as a disabled placeholder until #270 (0.9.0). -->
|
||||
<div class="view-switcher" role="tablist" data-e2e="view-switcher">
|
||||
<button
|
||||
v-for="m in ALL_MODES"
|
||||
:key="m"
|
||||
class="tab-btn"
|
||||
:class="{ active: activeMode === m, disabled: !modeEnabled(m) }"
|
||||
v-for="view in VIEWS"
|
||||
:key="view.key"
|
||||
class="view-btn"
|
||||
:class="{
|
||||
active: !view.disabled && activeMode === view.key,
|
||||
disabled: view.disabled || !isModeEnabled(view.key),
|
||||
}"
|
||||
role="tab"
|
||||
:aria-selected="activeMode === m"
|
||||
:disabled="!modeEnabled(m)"
|
||||
:title="!modeEnabled(m) ? t('workspace.modeDisabled') : undefined"
|
||||
:data-e2e="`tab-${m}`"
|
||||
@click="switchMode(m)"
|
||||
:aria-selected="!view.disabled && activeMode === view.key"
|
||||
:disabled="view.disabled || !isModeEnabled(view.key)"
|
||||
:title="viewTooltip(view)"
|
||||
:data-e2e="`view-${view.key}`"
|
||||
@click="onViewClick(view)"
|
||||
>
|
||||
{{ t(`workspace.tabs.${m}`) }}
|
||||
{{ t(`workspace.tabs.${view.key}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</DocWorkspaceHeader>
|
||||
|
||||
<!-- Tab content — lazy loaded (#216) -->
|
||||
<!-- :key on docId forces a clean remount when navigating to a different doc,
|
||||
preventing stale state (bbox, selectedPage, etc.) from leaking. -->
|
||||
<!-- View content — lazy loaded (#216). :key on docId forces a clean
|
||||
remount when navigating to a different doc, preventing stale state
|
||||
(bbox, selectedPage, etc.) from leaking. -->
|
||||
<div class="tab-content" role="tabpanel" data-e2e="tab-content">
|
||||
<Suspense>
|
||||
<DocChunksTab
|
||||
v-if="activeMode === 'chunks'"
|
||||
v-if="activeMode === 'linked'"
|
||||
:key="id"
|
||||
:doc-id="id"
|
||||
:available-stores="doc.stores ?? []"
|
||||
:store-links="doc.storeLinks"
|
||||
/>
|
||||
<DocInspectTab v-else-if="activeMode === 'inspect'" :key="id" :doc-id="id" />
|
||||
<DocAskTab v-else-if="activeMode === 'ask'" :key="id" :doc-id="id" />
|
||||
</Suspense>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -61,7 +66,7 @@ import { RouterLink, useRouter, useRoute } from 'vue-router'
|
|||
import type { Document } from '../shared/types'
|
||||
import { fetchDocument } from '../features/document/api'
|
||||
import { useFeatureFlagStore } from '../features/feature-flags/store'
|
||||
import { ALL_MODES, type DocMode } from '../shared/routing/modes'
|
||||
import { type DocMode } from '../shared/routing/modes'
|
||||
import { resolveMode } from '../shared/routing/resolveMode'
|
||||
import { useCrumbs } from '../shared/breadcrumb/store'
|
||||
import { truncate } from '../shared/breadcrumb/text'
|
||||
|
|
@ -71,7 +76,6 @@ import { ROUTES } from '../shared/routing/names'
|
|||
import DocWorkspaceHeader from '../features/document/ui/DocWorkspaceHeader.vue'
|
||||
import DocChunksTab from './DocChunksTab.vue'
|
||||
import DocInspectTab from './DocInspectTab.vue'
|
||||
import DocAskTab from './DocAskTab.vue'
|
||||
|
||||
const props = defineProps<{ id: string; mode: DocMode }>()
|
||||
|
||||
|
|
@ -86,6 +90,18 @@ const docError = ref<string | null>(null)
|
|||
|
||||
const activeMode = ref<DocMode>(props.mode)
|
||||
|
||||
// Switcher entries. Compare is intentionally disabled — the view ships
|
||||
// in 0.9.0; the button is kept visible so users can see the roadmap.
|
||||
interface ViewEntry {
|
||||
key: DocMode | 'compare'
|
||||
disabled: boolean
|
||||
}
|
||||
const VIEWS: readonly ViewEntry[] = [
|
||||
{ key: 'linked', disabled: false },
|
||||
{ key: 'inspect', disabled: false },
|
||||
{ key: 'compare', disabled: true },
|
||||
]
|
||||
|
||||
const crumbs = computed<Crumb[]>(() => [
|
||||
{ kind: 'link', label: t('breadcrumb.studio'), to: { name: ROUTES.HOME } },
|
||||
{
|
||||
|
|
@ -97,12 +113,24 @@ const crumbs = computed<Crumb[]>(() => [
|
|||
])
|
||||
useCrumbs(crumbs)
|
||||
|
||||
function modeEnabled(m: DocMode): boolean {
|
||||
return flagStore.modeFlags()[m]
|
||||
function isModeEnabled(key: DocMode | 'compare'): boolean {
|
||||
if (key === 'compare') return false
|
||||
return flagStore.modeFlags()[key]
|
||||
}
|
||||
|
||||
function viewTooltip(view: ViewEntry): string | undefined {
|
||||
if (view.disabled) return t('workspace.compareSoon')
|
||||
if (!isModeEnabled(view.key)) return t('workspace.modeDisabled')
|
||||
return undefined
|
||||
}
|
||||
|
||||
function onViewClick(view: ViewEntry): void {
|
||||
if (view.disabled || view.key === 'compare') return
|
||||
switchMode(view.key)
|
||||
}
|
||||
|
||||
function switchMode(m: DocMode): void {
|
||||
if (!modeEnabled(m)) return
|
||||
if (!isModeEnabled(m)) return
|
||||
activeMode.value = m
|
||||
router.replace({ query: { ...route.query, mode: m } })
|
||||
}
|
||||
|
|
@ -192,39 +220,42 @@ watch(
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-strip {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
.view-switcher {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-surface);
|
||||
padding: 0 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 8px 16px;
|
||||
.view-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-right: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.tab-btn:hover:not(.disabled) {
|
||||
.view-btn:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.view-btn:hover:not(.disabled) {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
.view-btn.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
background: var(--bg-active);
|
||||
}
|
||||
|
||||
.tab-btn.disabled {
|
||||
opacity: 0.35;
|
||||
.view-btn.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,9 +29,8 @@ const messages: Messages = {
|
|||
// Breadcrumb (0.6.0 doc workspace — #208)
|
||||
'breadcrumb.aria': "Fil d'Ariane",
|
||||
'breadcrumb.studio': 'Studio',
|
||||
'breadcrumb.mode.ask': 'Ask',
|
||||
'breadcrumb.mode.linked': 'Linked',
|
||||
'breadcrumb.mode.inspect': 'Inspect',
|
||||
'breadcrumb.mode.chunks': 'Chunks',
|
||||
|
||||
// Feature flags (0.6.0 — #210)
|
||||
'flags.allModesDisabled':
|
||||
|
|
@ -385,17 +384,13 @@ const messages: Messages = {
|
|||
'ask.noAnalysis': 'Aucune analyse disponible',
|
||||
'ask.noAnalysisSub': 'Analysez ce document dans le Studio avant de poser une question.',
|
||||
|
||||
// Doc workspace (#216, #218)
|
||||
'workspace.tabs.ask': 'Ask',
|
||||
// Doc workspace (#216, #218 \u2014 switcher refactored in #263)
|
||||
'workspace.tabs.linked': 'Linked',
|
||||
'workspace.tabs.inspect': 'Inspect',
|
||||
'workspace.tabs.chunks': 'Chunks',
|
||||
'workspace.tabs.compare': 'Compare',
|
||||
'workspace.backToLibrary': 'Retour \u00e0 la biblioth\u00e8que',
|
||||
'workspace.modeDisabled': 'Mode d\u00e9sactiv\u00e9 pour ce d\u00e9ploiement',
|
||||
'workspace.inspectComingSoon': 'Inspect \u2014 disponible en 0.7.0',
|
||||
'workspace.inspectComingSoonHint': 'La vue arborescence + bbox arrive prochainement.',
|
||||
'workspace.askComingSoon': 'Ask \u2014 disponible en 0.7.0',
|
||||
'workspace.askComingSoonHint':
|
||||
'Le raisonnement agentique sur le document arrive prochainement.',
|
||||
'workspace.compareSoon': 'Compare \u2014 disponible en 0.9.0',
|
||||
|
||||
// Doc tree rail (#217)
|
||||
'tree.empty': "Aucun n\u0153ud dans l'arbre.",
|
||||
|
|
@ -541,9 +536,8 @@ const messages: Messages = {
|
|||
// Breadcrumb (0.6.0 doc workspace — #208)
|
||||
'breadcrumb.aria': 'Breadcrumb',
|
||||
'breadcrumb.studio': 'Studio',
|
||||
'breadcrumb.mode.ask': 'Ask',
|
||||
'breadcrumb.mode.linked': 'Linked',
|
||||
'breadcrumb.mode.inspect': 'Inspect',
|
||||
'breadcrumb.mode.chunks': 'Chunks',
|
||||
|
||||
// Feature flags (0.6.0 — #210)
|
||||
'flags.allModesDisabled':
|
||||
|
|
@ -883,16 +877,13 @@ const messages: Messages = {
|
|||
'ask.noAnalysis': 'No analysis available',
|
||||
'ask.noAnalysisSub': 'Analyze this document in Studio before asking a question.',
|
||||
|
||||
// Doc workspace (#216, #218)
|
||||
'workspace.tabs.ask': 'Ask',
|
||||
// Doc workspace (#216, #218 — switcher refactored in #263)
|
||||
'workspace.tabs.linked': 'Linked',
|
||||
'workspace.tabs.inspect': 'Inspect',
|
||||
'workspace.tabs.chunks': 'Chunks',
|
||||
'workspace.tabs.compare': 'Compare',
|
||||
'workspace.backToLibrary': 'Back to library',
|
||||
'workspace.modeDisabled': 'Mode disabled for this deployment',
|
||||
'workspace.inspectComingSoon': 'Inspect \u2014 coming in 0.7.0',
|
||||
'workspace.inspectComingSoonHint': 'Tree + bbox view coming soon.',
|
||||
'workspace.askComingSoon': 'Ask \u2014 coming in 0.7.0',
|
||||
'workspace.askComingSoonHint': 'Agentic reasoning over the document coming soon.',
|
||||
'workspace.compareSoon': 'Compare — coming in 0.9.0',
|
||||
|
||||
// Doc tree rail (#217)
|
||||
'tree.empty': 'No nodes in tree.',
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { describe, expect, it } from 'vitest'
|
|||
import { ALL_MODES, DEFAULT_MODE, isDocMode, parseMode } from './modes'
|
||||
|
||||
describe('isDocMode', () => {
|
||||
it.each(['ask', 'inspect', 'chunks'])('accepts %s', (value) => {
|
||||
it.each(['inspect', 'linked'])('accepts %s', (value) => {
|
||||
expect(isDocMode(value)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([undefined, null, '', 'foo', 42, {}, []])('rejects %s', (value) => {
|
||||
it.each([undefined, null, '', 'foo', 'ask', 'chunks', 42, {}, []])('rejects %s', (value) => {
|
||||
expect(isDocMode(value)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -17,19 +17,26 @@ describe('parseMode', () => {
|
|||
expect(parseMode(undefined)).toBe(DEFAULT_MODE)
|
||||
expect(parseMode(null)).toBe(DEFAULT_MODE)
|
||||
expect(parseMode('garbage')).toBe(DEFAULT_MODE)
|
||||
expect(parseMode(['chunks'])).toBe(DEFAULT_MODE) // arrays not accepted
|
||||
expect(parseMode(['linked'])).toBe(DEFAULT_MODE) // arrays not accepted
|
||||
})
|
||||
|
||||
it.each(['ask', 'inspect', 'chunks'] as const)('respects %s', (mode) => {
|
||||
it.each(['linked', 'inspect'] as const)('respects %s', (mode) => {
|
||||
expect(parseMode(mode)).toBe(mode)
|
||||
})
|
||||
|
||||
it('maps the legacy ?mode=chunks alias to linked', () => {
|
||||
expect(parseMode('chunks')).toBe('linked')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ALL_MODES', () => {
|
||||
it('lists every mode exactly once', () => {
|
||||
expect(new Set(ALL_MODES).size).toBe(ALL_MODES.length)
|
||||
expect(ALL_MODES).toContain('ask')
|
||||
expect(ALL_MODES).toContain('linked')
|
||||
expect(ALL_MODES).toContain('inspect')
|
||||
expect(ALL_MODES).toContain('chunks')
|
||||
})
|
||||
|
||||
it('puts linked first (default view)', () => {
|
||||
expect(ALL_MODES[0]).toBe('linked')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,24 +1,35 @@
|
|||
/**
|
||||
* Doc workspace mode parsing.
|
||||
*
|
||||
* The doc workspace at `/docs/:id` exposes three modes via the `?mode=`
|
||||
* The doc workspace at `/docs/:id` exposes its content via the `?mode=`
|
||||
* query param. Anything missing or unknown resolves to the default,
|
||||
* `ask`, so a malformed URL never produces a broken page.
|
||||
* `linked`, so a malformed URL never produces a broken page.
|
||||
*
|
||||
* #210 layers feature-flag-aware redirection on top: if the requested
|
||||
* mode is disabled for the current tenant, the router replaces it with
|
||||
* the first enabled mode (priority `ask` > `chunks` > `inspect`).
|
||||
* the first enabled mode (priority `linked` > `inspect`).
|
||||
*
|
||||
* #263 renames the legacy `chunks` mode to `linked` and drops `ask` from
|
||||
* the workspace (Ask is handled separately via the standalone
|
||||
* `/reasoning` page). The Compare view (#263) is rendered as a disabled
|
||||
* button in the switcher — no mode value, no route segment.
|
||||
*
|
||||
* Backward compatibility: `?mode=chunks` is accepted and silently mapped
|
||||
* to `linked` so existing bookmarks keep working.
|
||||
*/
|
||||
|
||||
export type DocMode = 'ask' | 'inspect' | 'chunks'
|
||||
export type DocMode = 'inspect' | 'linked'
|
||||
|
||||
export const DEFAULT_MODE: DocMode = 'ask'
|
||||
export const ALL_MODES: readonly DocMode[] = ['ask', 'inspect', 'chunks'] as const
|
||||
export const DEFAULT_MODE: DocMode = 'linked'
|
||||
export const ALL_MODES: readonly DocMode[] = ['linked', 'inspect'] as const
|
||||
|
||||
const LEGACY_CHUNKS_ALIAS = 'chunks'
|
||||
|
||||
export function isDocMode(value: unknown): value is DocMode {
|
||||
return value === 'ask' || value === 'inspect' || value === 'chunks'
|
||||
return value === 'inspect' || value === 'linked'
|
||||
}
|
||||
|
||||
export function parseMode(raw: unknown): DocMode {
|
||||
if (raw === LEGACY_CHUNKS_ALIAS) return 'linked'
|
||||
return isDocMode(raw) ? raw : DEFAULT_MODE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,37 +3,35 @@ import { describe, expect, it } from 'vitest'
|
|||
import type { DocMode } from './modes'
|
||||
import { MODE_PRIORITY, resolveMode } from './resolveMode'
|
||||
|
||||
const allEnabled: Record<DocMode, boolean> = { ask: true, chunks: true, inspect: true }
|
||||
const allDisabled: Record<DocMode, boolean> = { ask: false, chunks: false, inspect: false }
|
||||
const allEnabled: Record<DocMode, boolean> = { linked: true, inspect: true }
|
||||
const allDisabled: Record<DocMode, boolean> = { linked: false, inspect: false }
|
||||
|
||||
describe('resolveMode', () => {
|
||||
it('returns the requested mode when it is enabled', () => {
|
||||
expect(resolveMode('ask', allEnabled)).toBe('ask')
|
||||
expect(resolveMode('chunks', allEnabled)).toBe('chunks')
|
||||
expect(resolveMode('linked', allEnabled)).toBe('linked')
|
||||
expect(resolveMode('inspect', allEnabled)).toBe('inspect')
|
||||
})
|
||||
|
||||
it('falls back to the highest-priority enabled mode when the requested one is disabled', () => {
|
||||
expect(resolveMode('chunks', { ...allEnabled, chunks: false })).toBe('ask')
|
||||
expect(resolveMode('chunks', { ask: false, chunks: false, inspect: true })).toBe('inspect')
|
||||
expect(resolveMode('linked', { linked: false, inspect: true })).toBe('inspect')
|
||||
expect(resolveMode('inspect', { linked: true, inspect: false })).toBe('linked')
|
||||
})
|
||||
|
||||
it('honours the priority order ask > chunks > inspect', () => {
|
||||
expect(resolveMode(undefined, allEnabled)).toBe('ask')
|
||||
expect(resolveMode(undefined, { ask: false, chunks: true, inspect: true })).toBe('chunks')
|
||||
expect(resolveMode(undefined, { ask: false, chunks: false, inspect: true })).toBe('inspect')
|
||||
it('honours the priority order linked > inspect', () => {
|
||||
expect(resolveMode(undefined, allEnabled)).toBe('linked')
|
||||
expect(resolveMode(undefined, { linked: false, inspect: true })).toBe('inspect')
|
||||
})
|
||||
|
||||
it('returns null when no mode is enabled', () => {
|
||||
expect(resolveMode('ask', allDisabled)).toBeNull()
|
||||
expect(resolveMode('linked', allDisabled)).toBeNull()
|
||||
expect(resolveMode(undefined, allDisabled)).toBeNull()
|
||||
})
|
||||
|
||||
it('handles missing requested gracefully', () => {
|
||||
expect(resolveMode(undefined, allEnabled)).toBe('ask')
|
||||
expect(resolveMode(undefined, allEnabled)).toBe('linked')
|
||||
})
|
||||
|
||||
it('exposes the priority in the right order', () => {
|
||||
expect(MODE_PRIORITY).toEqual(['ask', 'chunks', 'inspect'])
|
||||
expect(MODE_PRIORITY).toEqual(['linked', 'inspect'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type DocMode } from './modes'
|
||||
|
||||
/**
|
||||
* Doc workspace mode resolution under feature flags (#210).
|
||||
* Doc workspace mode resolution under feature flags (#210, updated #263).
|
||||
*
|
||||
* The router consults this when a user opens `/docs/:id?mode=<mode>`:
|
||||
*
|
||||
|
|
@ -9,8 +9,10 @@ import { type DocMode } from './modes'
|
|||
* - Otherwise, return the first enabled mode in `MODE_PRIORITY`.
|
||||
* - If no mode is enabled, return `null` (the router redirects to
|
||||
* the docs library with a flash message).
|
||||
*
|
||||
* Priority: `linked` first (the mockup's default view), then `inspect`.
|
||||
*/
|
||||
export const MODE_PRIORITY: readonly DocMode[] = ['ask', 'chunks', 'inspect'] as const
|
||||
export const MODE_PRIORITY: readonly DocMode[] = ['linked', 'inspect'] as const
|
||||
|
||||
export function resolveMode(
|
||||
requested: DocMode | undefined,
|
||||
|
|
|
|||
Loading…
Reference in a new issue