feat(web): refine snippet siderbar

This commit is contained in:
JzoNg 2026-06-17 16:23:41 +08:00
parent 3e606ff0dc
commit 16a0cfc40c
8 changed files with 241 additions and 82 deletions

View File

@ -0,0 +1,107 @@
'use client'
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { formatForDisplay } from '@tanstack/react-hotkeys'
import { useTranslation } from 'react-i18next'
import SidebarLeftArrowIcon from '@/app/components/base/icons/src/vender/SidebarLeftArrowIcon'
import { useSetGotoAnythingOpen } from '@/app/components/goto-anything/atoms'
import Link from '@/next/link'
import { useRouter } from '@/next/navigation'
import ToggleButton from '../../app-sidebar/toggle-button'
type SnippetDetailTopProps = {
expand?: boolean
onToggle?: () => void
}
const SEARCH_SHORTCUT = ['Mod', 'K']
const SnippetDetailTop = ({
expand = true,
onToggle,
}: SnippetDetailTopProps) => {
const { t } = useTranslation()
const router = useRouter()
const setGotoAnythingOpen = useSetGotoAnythingOpen()
if (!expand) {
return (
<div className="flex w-full items-center justify-center px-3 pt-2 pb-1">
{onToggle && (
<ToggleButton
expand={expand}
handleToggle={onToggle}
icon={<SidebarLeftArrowIcon aria-hidden className="size-4" />}
className="size-8 rounded-[10px] border-0 bg-transparent px-0 text-text-tertiary shadow-none hover:border-0 hover:bg-state-base-hover hover:text-text-secondary"
/>
)}
</div>
)
}
return (
<div className="flex items-center py-2 pr-2 pl-1">
<div className="flex min-w-0 flex-1 items-center gap-px">
<div className="flex shrink-0 items-center rounded-lg py-2 pr-1.5 pl-0.5 transition-colors hover:bg-background-default-hover">
<button
type="button"
aria-label={t('operation.back', { ns: 'common' })}
className="flex size-4 items-center justify-center text-text-tertiary hover:text-text-secondary"
onClick={() => router.back()}
>
<span aria-hidden className="i-ri-arrow-left-s-line size-4" />
</button>
<Link
href="/"
aria-label={t('mainNav.home', { ns: 'common' })}
className="flex size-4 items-center justify-center text-text-tertiary hover:text-text-secondary"
>
<span aria-hidden className="i-custom-vender-main-nav-app-home size-4" />
</Link>
</div>
<span className="shrink-0 system-md-regular text-text-quaternary">
/
</span>
<Link
href="/snippets"
className="shrink-0 truncate rounded-lg px-1.5 py-2 system-sm-semibold-uppercase text-text-secondary transition-colors hover:bg-background-default-hover hover:text-text-primary"
>
{t('tabs.snippets', { ns: 'workflow' })}
</Link>
</div>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={t('gotoAnything.searchTitle', { ns: 'app' })}
className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary"
onClick={() => setGotoAnythingOpen(true)}
>
<span aria-hidden className="i-custom-vender-main-nav-quick-search size-4" />
</button>
)}
/>
<TooltipContent placement="bottom" className="flex items-center gap-1 rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg p-1.5 system-xs-medium text-text-secondary shadow-lg backdrop-blur-[5px]">
<span className="px-0.5">{t('gotoAnything.quickAction', { ns: 'app' })}</span>
<KbdGroup>
{SEARCH_SHORTCUT.map(key => (
<Kbd key={key}>{formatForDisplay(key)}</Kbd>
))}
</KbdGroup>
</TooltipContent>
</Tooltip>
{onToggle && (
<ToggleButton
expand={expand}
handleToggle={onToggle}
icon={<SidebarLeftArrowIcon aria-hidden className="size-4" />}
className="size-8 rounded-[10px] border-0 bg-transparent px-0 text-text-tertiary shadow-none hover:border-0 hover:bg-state-base-hover hover:text-text-secondary"
/>
)}
</div>
)
}
export default SnippetDetailTop

View File

@ -14,6 +14,9 @@ import DatasetDetailTop from '@/app/components/app-sidebar/dataset-detail-top'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import DifyLogo from '@/app/components/base/logo/dify-logo' import DifyLogo from '@/app/components/base/logo/dify-logo'
import EnvNav from '@/app/components/header/env-nav' import EnvNav from '@/app/components/header/env-nav'
// import { buildIntegrationPath } from '@/app/components/integrations/routes'
// import { SnippetSidebarContent } from '@/app/components/snippets/components/snippet-sidebar'
// import { useSnippetDetailStore } from '@/app/components/snippets/store'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation' import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag' import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
@ -25,6 +28,7 @@ import AccountSection from './components/account-section'
import HelpMenu from './components/help-menu' import HelpMenu from './components/help-menu'
import MainNavLink from './components/nav-link' import MainNavLink from './components/nav-link'
import { MainNavSearchButton } from './components/search-button' import { MainNavSearchButton } from './components/search-button'
// import SnippetDetailTop from './components/snippet-detail-top'
import WebAppsSection from './components/web-apps-section' import WebAppsSection from './components/web-apps-section'
import { WorkspaceCard } from './components/workspace-card' import { WorkspaceCard } from './components/workspace-card'
import { isMainNavRouteVisible, MAIN_NAV_ROUTES } from './routes' import { isMainNavRouteVisible, MAIN_NAV_ROUTES } from './routes'
@ -111,9 +115,7 @@ const MainNav = ({
const detailNavigationTransitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null) const detailNavigationTransitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const isDetailNavigationHoverPreviewOpen = isCollapsedDetailNavigation && detailNavigationHoverPreviewOpen const isDetailNavigationHoverPreviewOpen = isCollapsedDetailNavigation && detailNavigationHoverPreviewOpen
const detailNavigationVisibleExpanded = detailNavigationExpanded || isDetailNavigationHoverPreviewOpen const detailNavigationVisibleExpanded = detailNavigationExpanded || isDetailNavigationHoverPreviewOpen
const bottomNavigationExpanded = showSnippetDetailBottomNavigation const bottomNavigationExpanded = !showDetailNavigation || detailNavigationVisibleExpanded
? false
: !showDetailNavigation || detailNavigationVisibleExpanded
const handleToggleDetailNavigation = useCallback(() => { const handleToggleDetailNavigation = useCallback(() => {
if (isDetailNavigationHoverPreviewOpen) { if (isDetailNavigationHoverPreviewOpen) {
if (detailNavigationTransitionTimerRef.current) if (detailNavigationTransitionTimerRef.current)
@ -222,9 +224,7 @@ const MainNav = ({
? detailNavigationExpanded ? detailNavigationExpanded
? 'w-[248px] bg-background-body p-1' ? 'w-[248px] bg-background-body p-1'
: 'w-16 bg-background-body p-1' : 'w-16 bg-background-body p-1'
: showSnippetDetailBottomNavigation : 'w-60 flex-col',
? 'w-16 bg-background-body p-1'
: 'w-60 flex-col',
'bg-background-body', 'bg-background-body',
className, className,
)} )}

View File

@ -1,8 +1,8 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import type { WorkflowProps } from '@/app/components/workflow' import type { WorkflowProps } from '@/app/components/workflow'
import type { SnippetDetailPayload, SnippetInputField } from '@/models/snippet' import type { SnippetDetail, SnippetDetailPayload, SnippetInputField } from '@/models/snippet'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { fireEvent, screen, waitFor } from '@testing-library/react' import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
import { PipelineInputVarType } from '@/models/pipeline' import { PipelineInputVarType } from '@/models/pipeline'
@ -13,6 +13,7 @@ const mockDoSyncWorkflowDraft = vi.fn()
const mockSyncWorkflowDraftWhenPageClose = vi.fn() const mockSyncWorkflowDraftWhenPageClose = vi.fn()
const mockReset = vi.fn() const mockReset = vi.fn()
const mockSetFields = vi.fn() const mockSetFields = vi.fn()
const mockSetNavigationState = vi.fn()
const mockPublishSnippetMutateAsync = vi.fn() const mockPublishSnippetMutateAsync = vi.fn()
const mockUseSnippetPublishedWorkflow = vi.fn() const mockUseSnippetPublishedWorkflow = vi.fn()
const mockFetchInspectVars = vi.fn() const mockFetchInspectVars = vi.fn()
@ -62,8 +63,13 @@ let capturedHooksStore: Record<string, unknown> | undefined
let capturedWorkflowNodes: WorkflowProps['nodes'] | undefined let capturedWorkflowNodes: WorkflowProps['nodes'] | undefined
let snippetDetailStoreState: { let snippetDetailStoreState: {
fields: SnippetInputField[] fields: SnippetInputField[]
onFieldsChange?: (fields: SnippetInputField[]) => void
readonly: boolean
reset: typeof mockReset reset: typeof mockReset
setFields: typeof mockSetFields setFields: typeof mockSetFields
setNavigationState: typeof mockSetNavigationState
snippet?: SnippetDetail
snippetId?: string
} }
vi.mock('@/app/components/snippets/store', () => ({ vi.mock('@/app/components/snippets/store', () => ({
@ -190,34 +196,6 @@ vi.mock('@/app/components/snippets/components/snippet-children', () => ({
), ),
})) }))
vi.mock('@/app/components/snippets/components/snippet-sidebar', () => ({
default: ({
fields,
onFieldsChange,
}: {
fields: SnippetInputField[]
onFieldsChange: (fields: SnippetInputField[]) => void
}) => (
<div>
<button type="button" onClick={() => onFieldsChange([])}>remove</button>
<button
type="button"
onClick={() => onFieldsChange([
...fields,
{
type: PipelineInputVarType.textInput,
label: 'New Field',
variable: 'new_field',
required: true,
},
])}
>
submit
</button>
</div>
),
}))
const payload: SnippetDetailPayload = { const payload: SnippetDetailPayload = {
snippet: { snippet: {
id: 'snippet-1', id: 'snippet-1',
@ -328,12 +306,20 @@ describe('SnippetMain', () => {
}, },
}) })
mockHandleCheckBeforePublish.mockResolvedValue(true) mockHandleCheckBeforePublish.mockResolvedValue(true)
mockSetNavigationState.mockImplementation((state) => {
snippetDetailStoreState = {
...snippetDetailStoreState,
...state,
}
})
capturedHooksStore = undefined capturedHooksStore = undefined
capturedWorkflowNodes = undefined capturedWorkflowNodes = undefined
snippetDetailStoreState = { snippetDetailStoreState = {
fields: [...payload.inputFields], fields: [...payload.inputFields],
readonly: true,
reset: mockReset, reset: mockReset,
setFields: mockSetFields, setFields: mockSetFields,
setNavigationState: mockSetNavigationState,
} }
mockWorkspacePermissionKeys.value = ['snippets.create_and_modify'] mockWorkspacePermissionKeys.value = ['snippets.create_and_modify']
}) })
@ -414,7 +400,12 @@ describe('SnippetMain', () => {
it('should sync draft input_fields when removing a field from the panel', async () => { it('should sync draft input_fields when removing a field from the panel', async () => {
renderSnippetMain({ currentNodes: [createDraftNode()] }) renderSnippetMain({ currentNodes: [createDraftNode()] })
fireEvent.click(screen.getByRole('button', { name: 'remove' })) await waitFor(() => {
expect(snippetDetailStoreState.onFieldsChange).toEqual(expect.any(Function))
})
act(() => {
snippetDetailStoreState.onFieldsChange?.([])
})
await waitFor(() => { await waitFor(() => {
expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([], { expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([], {
@ -426,7 +417,20 @@ describe('SnippetMain', () => {
it('should sync draft input_fields when adding a field from the sidebar', async () => { it('should sync draft input_fields when adding a field from the sidebar', async () => {
renderSnippetMain({ currentNodes: [createDraftNode()] }) renderSnippetMain({ currentNodes: [createDraftNode()] })
fireEvent.click(screen.getByRole('button', { name: 'submit' })) await waitFor(() => {
expect(snippetDetailStoreState.onFieldsChange).toEqual(expect.any(Function))
})
act(() => {
snippetDetailStoreState.onFieldsChange?.([
...payload.inputFields,
{
type: PipelineInputVarType.textInput,
label: 'New Field',
variable: 'new_field',
required: true,
},
])
})
await waitFor(() => { await waitFor(() => {
expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([ expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([

View File

@ -53,20 +53,6 @@ vi.mock('@/app/components/app/configuration/config-var/config-modal', () => ({
}, },
})) }))
vi.mock('@/next/link', () => ({
default: ({
children,
href,
className,
}: {
children: React.ReactNode
href: string
className?: string
}) => (
<a href={href} className={className}>{children}</a>
),
}))
vi.mock('@/app/components/workflow/nodes/start/components/var-list', () => ({ vi.mock('@/app/components/workflow/nodes/start/components/var-list', () => ({
default: (props: { default: (props: {
list: InputVar[] list: InputVar[]
@ -155,7 +141,11 @@ describe('SnippetSidebar', () => {
/>, />,
) )
expect(screen.getByRole('link', { name: /snippet\.management/i })).toHaveAttribute('href', '/snippets') expect(screen.queryByRole('link', { name: /snippet\.management/i })).not.toBeInTheDocument()
expect(screen.getByText(snippet.name)).toHaveAttribute('title', snippet.name)
expect(screen.getByText(snippet.name)).toHaveClass('truncate')
expect(screen.getByText(snippet.description)).toHaveAttribute('title', snippet.description)
expect(screen.getByText(snippet.description)).toHaveClass('truncate')
expect(screen.queryByRole('button', { name: /common\.operation\.add/i })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: /common\.operation\.add/i })).not.toBeInTheDocument()
expect(capturedVarListProps?.readonly).toBe(true) expect(capturedVarListProps?.readonly).toBe(true)
}) })

View File

@ -39,7 +39,6 @@ import { useSnippetInputFieldActions } from './hooks/use-snippet-input-field-act
import { useSnippetPublish } from './hooks/use-snippet-publish' import { useSnippetPublish } from './hooks/use-snippet-publish'
import SaveBeforeLeavingDialog from './save-before-leaving-dialog' import SaveBeforeLeavingDialog from './save-before-leaving-dialog'
import SnippetChildren from './snippet-children' import SnippetChildren from './snippet-children'
import SnippetSidebar from './snippet-sidebar'
type SnippetMainProps = { type SnippetMainProps = {
payload: SnippetDetailPayload payload: SnippetDetailPayload
@ -365,9 +364,11 @@ const SnippetMain = ({
const { const {
reset, reset,
setFields, setFields,
setNavigationState,
} = useSnippetDetailStore(useShallow(state => ({ } = useSnippetDetailStore(useShallow(state => ({
reset: state.reset, reset: state.reset,
setFields: state.setFields, setFields: state.setFields,
setNavigationState: state.setNavigationState,
}))) })))
const { const {
fields, fields,
@ -385,6 +386,8 @@ const SnippetMain = ({
const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl(snippetId) const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl(snippetId)
useEffect(() => { useEffect(() => {
reset() reset()
return () => reset()
}, [reset, snippetId]) }, [reset, snippetId])
useEffect(() => { useEffect(() => {
@ -447,6 +450,15 @@ const SnippetMain = ({
setHasDraftChanges(true) setHasDraftChanges(true)
}, [canCreateAndModifySnippet, handleFieldsChange, isEditing, setHasDraftChanges]) }, [canCreateAndModifySnippet, handleFieldsChange, isEditing, setHasDraftChanges])
useEffect(() => {
setNavigationState({
snippetId,
snippet,
readonly: !isEditing,
onFieldsChange: handleFieldsChangeInEditing,
})
}, [handleFieldsChangeInEditing, isEditing, setNavigationState, snippet, snippetId])
const updateLocalDraftFromSyncPayload = useCallback(( const updateLocalDraftFromSyncPayload = useCallback((
syncedDraftPayload?: Omit<SnippetDraftSyncPayload, 'hash'> | void, syncedDraftPayload?: Omit<SnippetDraftSyncPayload, 'hash'> | void,
) => { ) => {
@ -600,12 +612,6 @@ const SnippetMain = ({
return ( return (
<div className="relative flex h-full min-h-0 min-w-0"> <div className="relative flex h-full min-h-0 min-w-0">
<SnippetSidebar
snippet={snippet}
fields={fields}
readonly={!isEditing}
onFieldsChange={handleFieldsChangeInEditing}
/>
<div className="relative min-h-0 min-w-0 grow"> <div className="relative min-h-0 min-w-0 grow">
<WorkflowWithInnerContext <WorkflowWithInnerContext
key={`${snippetId}-${isEditing ? 'draft' : 'published'}`} key={`${snippetId}-${isEditing ? 'draft' : 'published'}`}

View File

@ -11,7 +11,6 @@ import SnippetInfoDropdown from '@/app/components/app-sidebar/snippet-info/dropd
import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal' import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal'
import Field from '@/app/components/workflow/nodes/_base/components/field' import Field from '@/app/components/workflow/nodes/_base/components/field'
import VarList from '@/app/components/workflow/nodes/start/components/var-list' import VarList from '@/app/components/workflow/nodes/start/components/var-list'
import Link from '@/next/link'
import { hasDuplicateStr } from '@/utils/var' import { hasDuplicateStr } from '@/utils/var'
type SnippetSidebarProps = { type SnippetSidebarProps = {
@ -21,6 +20,10 @@ type SnippetSidebarProps = {
onFieldsChange: (fields: SnippetInputField[]) => void onFieldsChange: (fields: SnippetInputField[]) => void
} }
type SnippetSidebarContentProps = SnippetSidebarProps & {
className?: string
}
const toWorkflowInputVar = (field: SnippetInputField): InputVar => ({ const toWorkflowInputVar = (field: SnippetInputField): InputVar => ({
...field, ...field,
type: field.type as unknown as InputVar['type'], type: field.type as unknown as InputVar['type'],
@ -32,12 +35,13 @@ const toSnippetInputField = (field: InputVar): SnippetInputField => ({
type: field.type as unknown as SnippetInputField['type'], type: field.type as unknown as SnippetInputField['type'],
}) })
const SnippetSidebar = ({ export const SnippetSidebarContent = ({
snippet, snippet,
fields, fields,
readonly, readonly,
onFieldsChange, onFieldsChange,
}: SnippetSidebarProps) => { className,
}: SnippetSidebarContentProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isShowAddVarModal, setIsShowAddVarModal] = useState(false) const [isShowAddVarModal, setIsShowAddVarModal] = useState(false)
const workflowInputVars = useMemo(() => fields.map(toWorkflowInputVar), [fields]) const workflowInputVars = useMemo(() => fields.map(toWorkflowInputVar), [fields])
@ -88,21 +92,13 @@ const SnippetSidebar = ({
}, [fields, onFieldsChange]) }, [fields, onFieldsChange])
return ( return (
<aside className="flex h-full w-90 shrink-0 flex-col overflow-hidden rounded-tl-2xl border-r border-divider-subtle bg-background-default"> <div className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-background-default', className)}>
<div className="shrink-0 px-6 pt-7"> <div className="shrink-0 px-3 py-2">
<Link <div className="flex items-start gap-3">
href="/snippets"
className="inline-flex items-center gap-2 system-sm-semibold-uppercase text-text-primary hover:text-text-accent"
>
<span aria-hidden className="i-ri-arrow-left-line h-4 w-4" />
{t('management', { ns: 'snippet' })}
</Link>
<div className="mt-12 flex items-start gap-3">
<div className="min-w-0 grow"> <div className="min-w-0 grow">
<div className="system-xl-semibold text-text-primary">{snippet.name}</div> <div className="truncate system-xl-semibold text-text-primary" title={snippet.name}>{snippet.name}</div>
{!!snippet.description && ( {!!snippet.description && (
<div className="mt-3 system-sm-regular text-text-tertiary"> <div className="mt-3 truncate system-sm-regular text-text-tertiary" title={snippet.description}>
{snippet.description} {snippet.description}
</div> </div>
)} )}
@ -111,9 +107,7 @@ const SnippetSidebar = ({
</div> </div>
</div> </div>
<div className="mx-6 mt-7 h-px shrink-0 bg-divider-subtle" /> <div className="flex min-h-0 grow flex-col px-3 pt-6">
<div className="flex min-h-0 grow flex-col px-6 pt-7">
<Field <Field
title={t('inputVariables', { ns: 'snippet' })} title={t('inputVariables', { ns: 'snippet' })}
operations={!readonly operations={!readonly
@ -151,6 +145,14 @@ const SnippetSidebar = ({
varKeys={fields.map(v => v.variable)} varKeys={fields.map(v => v.variable)}
/> />
)} )}
</div>
)
}
const SnippetSidebar = (props: SnippetSidebarProps) => {
return (
<aside className="flex h-full w-90 shrink-0 flex-col overflow-hidden rounded-tl-2xl border-r border-divider-subtle bg-background-default">
<SnippetSidebarContent {...props} />
</aside> </aside>
) )
} }

View File

@ -1,4 +1,4 @@
import type { SnippetInputField } from '@/models/snippet' import type { SnippetDetail, SnippetInputField } from '@/models/snippet'
import { PipelineInputVarType } from '@/models/pipeline' import { PipelineInputVarType } from '@/models/pipeline'
import { useSnippetDetailStore } from '..' import { useSnippetDetailStore } from '..'
@ -9,6 +9,15 @@ const createField = (variable: string): SnippetInputField => ({
required: true, required: true,
}) })
const snippet: SnippetDetail = {
id: 'snippet-1',
name: 'Snippet',
description: 'Description',
updatedAt: '2026-03-29 10:00',
usage: '0',
tags: [],
}
describe('useSnippetDetailStore', () => { describe('useSnippetDetailStore', () => {
beforeEach(() => { beforeEach(() => {
useSnippetDetailStore.getState().reset() useSnippetDetailStore.getState().reset()
@ -28,4 +37,32 @@ describe('useSnippetDetailStore', () => {
expect(useSnippetDetailStore.getState().fields).toEqual([]) expect(useSnippetDetailStore.getState().fields).toEqual([])
}) })
it('should store and reset snippet navigation state', () => {
const onFieldsChange = vi.fn()
useSnippetDetailStore.getState().setNavigationState({
snippetId: 'snippet-1',
snippet,
readonly: false,
onFieldsChange,
})
expect(useSnippetDetailStore.getState()).toMatchObject({
snippetId: 'snippet-1',
snippet,
readonly: false,
onFieldsChange,
})
useSnippetDetailStore.getState().reset()
expect(useSnippetDetailStore.getState()).toMatchObject({
fields: [],
readonly: true,
snippet: undefined,
snippetId: undefined,
onFieldsChange: undefined,
})
})
}) })

View File

@ -1,20 +1,33 @@
'use client' 'use client'
import type { SnippetInputField } from '@/models/snippet' import type { SnippetDetail, SnippetInputField } from '@/models/snippet'
import { create } from 'zustand' import { create } from 'zustand'
type SnippetNavigationState = {
snippet?: SnippetDetail
snippetId?: string
readonly: boolean
onFieldsChange?: (fields: SnippetInputField[]) => void
}
type SnippetDetailUIState = { type SnippetDetailUIState = {
fields: SnippetInputField[] fields: SnippetInputField[]
setFields: (fields: SnippetInputField[]) => void setFields: (fields: SnippetInputField[]) => void
setNavigationState: (state: SnippetNavigationState) => void
reset: () => void reset: () => void
} } & SnippetNavigationState
const initialState = { const initialState = {
fields: [] as SnippetInputField[], fields: [] as SnippetInputField[],
readonly: true,
snippet: undefined,
snippetId: undefined,
onFieldsChange: undefined,
} }
export const useSnippetDetailStore = create<SnippetDetailUIState>(set => ({ export const useSnippetDetailStore = create<SnippetDetailUIState>(set => ({
...initialState, ...initialState,
setFields: fields => set({ fields }), setFields: fields => set({ fields }),
setNavigationState: state => set(state),
reset: () => set(initialState), reset: () => set(initialState),
})) }))