mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
feat(web): show snippet publish action
This commit is contained in:
parent
01957f302f
commit
b8cc01cf10
@ -7,35 +7,17 @@ import SnippetWorkflowPanel from './workflow-panel'
|
|||||||
type SnippetChildrenProps = {
|
type SnippetChildrenProps = {
|
||||||
snippetId: string
|
snippetId: string
|
||||||
fields: SnippetInputField[]
|
fields: SnippetInputField[]
|
||||||
canDiscardChanges: boolean
|
|
||||||
canEdit?: boolean
|
|
||||||
canSave: boolean
|
canSave: boolean
|
||||||
hasDraftChanges: boolean
|
|
||||||
isEditing: boolean
|
|
||||||
isPublishing: boolean
|
isPublishing: boolean
|
||||||
onCancel: () => void
|
|
||||||
onEdit: () => void
|
|
||||||
onExitEditing: () => void | Promise<void>
|
|
||||||
onExitEditingWithoutSave: () => void | Promise<void>
|
|
||||||
onPublish: () => void
|
onPublish: () => void
|
||||||
onSaveAndExitEditing: () => void | Promise<void>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SnippetChildren = ({
|
const SnippetChildren = ({
|
||||||
snippetId,
|
snippetId,
|
||||||
fields,
|
fields,
|
||||||
canDiscardChanges,
|
|
||||||
canEdit = true,
|
|
||||||
canSave,
|
canSave,
|
||||||
hasDraftChanges,
|
|
||||||
isEditing,
|
|
||||||
isPublishing,
|
isPublishing,
|
||||||
onCancel,
|
|
||||||
onEdit,
|
|
||||||
onExitEditing,
|
|
||||||
onExitEditingWithoutSave,
|
|
||||||
onPublish,
|
onPublish,
|
||||||
onSaveAndExitEditing,
|
|
||||||
}: SnippetChildrenProps) => {
|
}: SnippetChildrenProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -43,18 +25,9 @@ const SnippetChildren = ({
|
|||||||
|
|
||||||
<SnippetHeader
|
<SnippetHeader
|
||||||
snippetId={snippetId}
|
snippetId={snippetId}
|
||||||
canDiscardChanges={canDiscardChanges}
|
|
||||||
canEdit={canEdit}
|
|
||||||
canSave={canSave}
|
canSave={canSave}
|
||||||
hasDraftChanges={hasDraftChanges}
|
|
||||||
isEditing={isEditing}
|
|
||||||
isPublishing={isPublishing}
|
isPublishing={isPublishing}
|
||||||
onCancel={onCancel}
|
|
||||||
onEdit={onEdit}
|
|
||||||
onExitEditing={onExitEditing}
|
|
||||||
onExitEditingWithoutSave={onExitEditingWithoutSave}
|
|
||||||
onPublish={onPublish}
|
onPublish={onPublish}
|
||||||
onSaveAndExitEditing={onSaveAndExitEditing}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SnippetWorkflowPanel
|
<SnippetWorkflowPanel
|
||||||
|
|||||||
@ -1,28 +1,8 @@
|
|||||||
import type { ReactNode } from 'react'
|
|
||||||
import type { HeaderProps } from '@/app/components/workflow/header'
|
import type { HeaderProps } from '@/app/components/workflow/header'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { expectLoadingButton } from '@/test/button'
|
import { expectLoadingButton } from '@/test/button'
|
||||||
import SnippetHeader from '..'
|
import SnippetHeader from '..'
|
||||||
|
|
||||||
vi.mock('@langgenius/dify-ui/alert-dialog', () => ({
|
|
||||||
AlertDialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
||||||
AlertDialogActions: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
||||||
AlertDialogCancelButton: ({ children }: { children: ReactNode }) => <button type="button">{children}</button>,
|
|
||||||
AlertDialogConfirmButton: ({
|
|
||||||
children,
|
|
||||||
disabled,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
children: ReactNode
|
|
||||||
disabled?: boolean
|
|
||||||
onClick?: () => void
|
|
||||||
}) => <button type="button" disabled={disabled} onClick={onClick}>{children}</button>,
|
|
||||||
AlertDialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
||||||
AlertDialogDescription: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
||||||
AlertDialogTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
||||||
AlertDialogTrigger: ({ children, render }: { children?: ReactNode, render?: ReactNode }) => render ?? <button type="button">{children}</button>,
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/workflow/header', () => ({
|
vi.mock('@/app/components/workflow/header', () => ({
|
||||||
default: (props: HeaderProps) => {
|
default: (props: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
@ -44,242 +24,71 @@ vi.mock('@/app/components/workflow/header', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
describe('SnippetHeader', () => {
|
describe('SnippetHeader', () => {
|
||||||
const mockCancel = vi.fn()
|
|
||||||
const mockEdit = vi.fn()
|
|
||||||
const mockExitEditing = vi.fn()
|
|
||||||
const mockExitEditingWithoutSave = vi.fn()
|
|
||||||
const mockPublish = vi.fn()
|
const mockPublish = vi.fn()
|
||||||
const mockSaveAndExit = vi.fn()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Verifies the wrapper passes the expected workflow header configuration.
|
it('should configure workflow header slots and hide workflow-only controls', () => {
|
||||||
describe('Rendering', () => {
|
render(
|
||||||
it('should configure workflow header slots and hide workflow-only controls', () => {
|
<SnippetHeader
|
||||||
render(
|
snippetId="snippet-1"
|
||||||
<SnippetHeader
|
canSave
|
||||||
snippetId="snippet-1"
|
isPublishing={false}
|
||||||
canDiscardChanges
|
onPublish={mockPublish}
|
||||||
canSave
|
/>,
|
||||||
hasDraftChanges={false}
|
)
|
||||||
isEditing={false}
|
|
||||||
isPublishing={false}
|
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
const header = screen.getByTestId('workflow-header')
|
const header = screen.getByTestId('workflow-header')
|
||||||
expect(header).toHaveAttribute('data-show-env', 'false')
|
expect(header).toHaveAttribute('data-show-env', 'false')
|
||||||
expect(header).toHaveAttribute('data-show-global-variable', 'false')
|
expect(header).toHaveAttribute('data-show-global-variable', 'false')
|
||||||
expect(header).toHaveAttribute('data-history-url', '/snippets/snippet-1/workflow-runs')
|
expect(header).toHaveAttribute('data-history-url', '/snippets/snippet-1/workflow-runs')
|
||||||
expect(screen.getByText('snippet.viewOnly')).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: /snippet\.publishButton/i })).toBeInTheDocument()
|
||||||
expect(screen.getByRole('button', { name: /snippet\.edit/i })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: /snippet\.testRunButton/i })).toBeInTheDocument()
|
||||||
expect(screen.getByRole('button', { name: /snippet\.testRunButton/i })).toBeInTheDocument()
|
expect(screen.queryByText('snippet.viewOnly')).not.toBeInTheDocument()
|
||||||
})
|
expect(screen.queryByRole('button', { name: /snippet\.edit/i })).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('button', { name: /snippet\.exitEditing/i })).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Verifies forwarded callbacks still drive the snippet-specific controls.
|
it('should publish from the primary header action', () => {
|
||||||
describe('User Interactions', () => {
|
render(
|
||||||
it('should invoke the snippet callbacks when save and discard are clicked in editing mode', () => {
|
<SnippetHeader
|
||||||
render(
|
snippetId="snippet-1"
|
||||||
<SnippetHeader
|
canSave
|
||||||
snippetId="snippet-1"
|
isPublishing={false}
|
||||||
canDiscardChanges
|
onPublish={mockPublish}
|
||||||
canSave
|
/>,
|
||||||
hasDraftChanges
|
)
|
||||||
isEditing
|
|
||||||
isPublishing={false}
|
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /^snippet\.save$/i }))
|
fireEvent.click(screen.getByRole('button', { name: /snippet\.publishButton/i }))
|
||||||
fireEvent.click(screen.getByRole('button', { name: /snippet\.discardChanges/i }))
|
|
||||||
|
|
||||||
expect(mockPublish).toHaveBeenCalledTimes(1)
|
expect(mockPublish).toHaveBeenCalledTimes(1)
|
||||||
expect(mockCancel).toHaveBeenCalledTimes(1)
|
})
|
||||||
})
|
|
||||||
|
|
||||||
it('should disable save actions when the current graph has no nodes', () => {
|
it('should disable publish when the current graph has no nodes', () => {
|
||||||
render(
|
render(
|
||||||
<SnippetHeader
|
<SnippetHeader
|
||||||
snippetId="snippet-1"
|
snippetId="snippet-1"
|
||||||
canDiscardChanges
|
canSave={false}
|
||||||
canSave={false}
|
isPublishing={false}
|
||||||
hasDraftChanges
|
onPublish={mockPublish}
|
||||||
isEditing
|
/>,
|
||||||
isPublishing={false}
|
)
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /^snippet\.save$/i })).toBeDisabled()
|
expect(screen.getByRole('button', { name: /snippet\.publishButton/i })).toBeDisabled()
|
||||||
expect(screen.getByRole('button', { name: /snippet\.saveAndExit/i })).toBeDisabled()
|
})
|
||||||
expect(screen.getByRole('button', { name: /snippet\.doNotSave/i })).not.toBeDisabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should hide the discard draft action when there is no published workflow', () => {
|
it('should show publish loading state while publishing', () => {
|
||||||
render(
|
render(
|
||||||
<SnippetHeader
|
<SnippetHeader
|
||||||
snippetId="snippet-1"
|
snippetId="snippet-1"
|
||||||
canDiscardChanges={false}
|
canSave
|
||||||
canSave
|
isPublishing
|
||||||
hasDraftChanges
|
onPublish={mockPublish}
|
||||||
isEditing
|
/>,
|
||||||
isPublishing={false}
|
)
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(screen.queryByText('snippet.discardDraft')).not.toBeInTheDocument()
|
expectLoadingButton(screen.getByRole('button', { name: /snippet\.publishButton/i }))
|
||||||
expect(screen.getByText('snippet.editingDraft')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should enter editing mode from the readonly header action', () => {
|
|
||||||
render(
|
|
||||||
<SnippetHeader
|
|
||||||
snippetId="snippet-1"
|
|
||||||
canDiscardChanges
|
|
||||||
canSave
|
|
||||||
hasDraftChanges={false}
|
|
||||||
isEditing={false}
|
|
||||||
isPublishing={false}
|
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'snippet.edit' }))
|
|
||||||
|
|
||||||
expect(mockEdit).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should exit editing immediately when there are no draft changes', () => {
|
|
||||||
render(
|
|
||||||
<SnippetHeader
|
|
||||||
snippetId="snippet-1"
|
|
||||||
canDiscardChanges
|
|
||||||
canSave
|
|
||||||
hasDraftChanges={false}
|
|
||||||
isEditing
|
|
||||||
isPublishing={false}
|
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'snippet.exitEditing' }))
|
|
||||||
|
|
||||||
expect(mockExitEditing).toHaveBeenCalledTimes(1)
|
|
||||||
expect(mockExitEditingWithoutSave).not.toHaveBeenCalled()
|
|
||||||
expect(mockSaveAndExit).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should disable edit actions while publishing', () => {
|
|
||||||
render(
|
|
||||||
<SnippetHeader
|
|
||||||
snippetId="snippet-1"
|
|
||||||
canDiscardChanges
|
|
||||||
canSave
|
|
||||||
hasDraftChanges
|
|
||||||
isEditing
|
|
||||||
isPublishing
|
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: 'snippet.exitEditing' })).toBeDisabled()
|
|
||||||
expectLoadingButton(screen.getByRole('button', { name: /^snippet\.save$/i }))
|
|
||||||
expect(screen.getByRole('button', { name: 'snippet.doNotSave' })).toBeDisabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should discard changes from the exit confirmation dialog', async () => {
|
|
||||||
render(
|
|
||||||
<SnippetHeader
|
|
||||||
snippetId="snippet-1"
|
|
||||||
canDiscardChanges
|
|
||||||
canSave
|
|
||||||
hasDraftChanges
|
|
||||||
isEditing
|
|
||||||
isPublishing={false}
|
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'snippet.exitEditing' }))
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'snippet.doNotSave' }))
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockExitEditingWithoutSave).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
|
||||||
expect(mockSaveAndExit).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should save and exit from the exit confirmation dialog', async () => {
|
|
||||||
render(
|
|
||||||
<SnippetHeader
|
|
||||||
snippetId="snippet-1"
|
|
||||||
canDiscardChanges
|
|
||||||
canSave
|
|
||||||
hasDraftChanges
|
|
||||||
isEditing
|
|
||||||
isPublishing={false}
|
|
||||||
onCancel={mockCancel}
|
|
||||||
onEdit={mockEdit}
|
|
||||||
onExitEditing={mockExitEditing}
|
|
||||||
onExitEditingWithoutSave={mockExitEditingWithoutSave}
|
|
||||||
onPublish={mockPublish}
|
|
||||||
onSaveAndExitEditing={mockSaveAndExit}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'snippet.exitEditing' }))
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'snippet.saveAndExit' }))
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockSaveAndExit).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
|
||||||
expect(mockExitEditingWithoutSave).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,128 +5,42 @@ import { Button } from '@langgenius/dify-ui/button'
|
|||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Header from '@/app/components/workflow/header'
|
import Header from '@/app/components/workflow/header'
|
||||||
import SaveBeforeLeavingDialog from '../save-before-leaving-dialog'
|
|
||||||
import CancelChanges from './cancel-changes'
|
|
||||||
import RunMode from './run-mode'
|
import RunMode from './run-mode'
|
||||||
|
|
||||||
type SnippetHeaderProps = {
|
type SnippetHeaderProps = {
|
||||||
snippetId: string
|
snippetId: string
|
||||||
canDiscardChanges: boolean
|
|
||||||
canEdit?: boolean
|
|
||||||
canSave: boolean
|
canSave: boolean
|
||||||
hasDraftChanges: boolean
|
|
||||||
isEditing: boolean
|
|
||||||
isPublishing: boolean
|
isPublishing: boolean
|
||||||
onCancel: () => void
|
|
||||||
onEdit: () => void
|
|
||||||
onExitEditing: () => void | Promise<void>
|
|
||||||
onExitEditingWithoutSave: () => void | Promise<void>
|
|
||||||
onPublish: () => void
|
onPublish: () => void
|
||||||
onSaveAndExitEditing: () => void | Promise<void>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ViewOnlyBadge = () => {
|
const PublishAction = ({
|
||||||
const { t } = useTranslation('snippet')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-components-badge-status-light-normal-border-inner bg-components-badge-bg-blue-light-soft px-1.5 py-0.5 system-xs-semibold-uppercase text-text-accent">
|
|
||||||
{t('viewOnly')}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditActions = ({
|
|
||||||
canEdit = true,
|
|
||||||
canSave,
|
canSave,
|
||||||
hasDraftChanges,
|
|
||||||
isEditing,
|
|
||||||
isPublishing,
|
isPublishing,
|
||||||
onEdit,
|
|
||||||
onExitEditing,
|
|
||||||
onExitEditingWithoutSave,
|
|
||||||
onPublish,
|
onPublish,
|
||||||
onSaveAndExitEditing,
|
}: Pick<SnippetHeaderProps, 'canSave' | 'isPublishing' | 'onPublish'>) => {
|
||||||
}: Pick<SnippetHeaderProps, 'canEdit' | 'canSave' | 'hasDraftChanges' | 'isEditing' | 'isPublishing' | 'onEdit' | 'onExitEditing' | 'onExitEditingWithoutSave' | 'onPublish' | 'onSaveAndExitEditing'>) => {
|
|
||||||
const { t } = useTranslation('snippet')
|
const { t } = useTranslation('snippet')
|
||||||
const [exitConfirmOpen, setExitConfirmOpen] = useState(false)
|
|
||||||
|
|
||||||
if (!isEditing) {
|
|
||||||
if (!canEdit)
|
|
||||||
return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button variant="primary" onClick={onEdit}>
|
|
||||||
{t('edit')}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<SaveBeforeLeavingDialog
|
variant="primary"
|
||||||
open={exitConfirmOpen}
|
loading={isPublishing}
|
||||||
onOpenChange={setExitConfirmOpen}
|
disabled={isPublishing || !canSave}
|
||||||
trigger={(
|
onClick={onPublish}
|
||||||
<Button
|
>
|
||||||
disabled={isPublishing || !canEdit}
|
{t('publishButton')}
|
||||||
onClick={(event) => {
|
</Button>
|
||||||
if (!canEdit)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (!hasDraftChanges) {
|
|
||||||
event.preventDefault()
|
|
||||||
void onExitEditing()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setExitConfirmOpen(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('exitEditing')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
disabled={isPublishing || !canEdit}
|
|
||||||
saveDisabled={!canEdit || !canSave}
|
|
||||||
loading={isPublishing}
|
|
||||||
onDiscard={async () => {
|
|
||||||
await onExitEditingWithoutSave()
|
|
||||||
setExitConfirmOpen(false)
|
|
||||||
}}
|
|
||||||
onSave={async () => {
|
|
||||||
await onSaveAndExitEditing()
|
|
||||||
setExitConfirmOpen(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
loading={isPublishing}
|
|
||||||
disabled={isPublishing || !canEdit || !canSave}
|
|
||||||
onClick={onPublish}
|
|
||||||
>
|
|
||||||
{t('save')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SnippetHeader = ({
|
const SnippetHeader = ({
|
||||||
snippetId,
|
snippetId,
|
||||||
canDiscardChanges,
|
|
||||||
canEdit = true,
|
|
||||||
canSave,
|
canSave,
|
||||||
hasDraftChanges,
|
|
||||||
isEditing,
|
|
||||||
isPublishing,
|
isPublishing,
|
||||||
onCancel,
|
|
||||||
onEdit,
|
|
||||||
onExitEditing,
|
|
||||||
onExitEditingWithoutSave,
|
|
||||||
onPublish,
|
onPublish,
|
||||||
onSaveAndExitEditing,
|
|
||||||
}: SnippetHeaderProps) => {
|
}: SnippetHeaderProps) => {
|
||||||
const { t } = useTranslation('snippet')
|
const { t } = useTranslation('snippet')
|
||||||
const viewHistoryProps = useMemo(() => {
|
const viewHistoryProps = useMemo(() => {
|
||||||
@ -139,21 +53,11 @@ const SnippetHeader = ({
|
|||||||
return {
|
return {
|
||||||
normal: {
|
normal: {
|
||||||
components: {
|
components: {
|
||||||
title: isEditing
|
|
||||||
? (hasDraftChanges ? <CancelChanges canDiscardChanges={canDiscardChanges} onCancel={onCancel} /> : <></>)
|
|
||||||
: <ViewOnlyBadge />,
|
|
||||||
left: (
|
left: (
|
||||||
<EditActions
|
<PublishAction
|
||||||
canEdit={canEdit}
|
|
||||||
canSave={canSave}
|
canSave={canSave}
|
||||||
hasDraftChanges={hasDraftChanges}
|
|
||||||
isEditing={isEditing}
|
|
||||||
isPublishing={isPublishing}
|
isPublishing={isPublishing}
|
||||||
onEdit={onEdit}
|
|
||||||
onExitEditing={onExitEditing}
|
|
||||||
onExitEditingWithoutSave={onExitEditingWithoutSave}
|
|
||||||
onPublish={onPublish}
|
onPublish={onPublish}
|
||||||
onSaveAndExitEditing={onSaveAndExitEditing}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -174,7 +78,7 @@ const SnippetHeader = ({
|
|||||||
viewHistoryProps,
|
viewHistoryProps,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}, [canDiscardChanges, canEdit, canSave, hasDraftChanges, isEditing, isPublishing, onCancel, onEdit, onExitEditing, onExitEditingWithoutSave, onPublish, onSaveAndExitEditing, t, viewHistoryProps])
|
}, [canSave, isPublishing, onPublish, t, viewHistoryProps])
|
||||||
|
|
||||||
return <Header {...headerProps} />
|
return <Header {...headerProps} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,19 +93,12 @@ const hasSnippetDraftNodes = (payload?: Omit<SnippetDraftSyncPayload, 'hash'> |
|
|||||||
const SnippetMainContent = ({
|
const SnippetMainContent = ({
|
||||||
snippetId,
|
snippetId,
|
||||||
fields,
|
fields,
|
||||||
canDiscardChanges,
|
|
||||||
canEdit,
|
|
||||||
canSave,
|
canSave,
|
||||||
hasDraftChanges,
|
hasDraftChanges,
|
||||||
isEditing,
|
isEditing,
|
||||||
onBeforePublish,
|
onBeforePublish,
|
||||||
onCancel,
|
|
||||||
onDiscardRoute,
|
onDiscardRoute,
|
||||||
onEdit,
|
|
||||||
onExitEditing,
|
|
||||||
onExitEditingWithoutSave,
|
|
||||||
onSaved,
|
onSaved,
|
||||||
onSavedAndExitEditing,
|
|
||||||
}: SnippetMainContentProps) => {
|
}: SnippetMainContentProps) => {
|
||||||
const { push } = useRouter()
|
const { push } = useRouter()
|
||||||
const { t } = useTranslation('snippet')
|
const { t } = useTranslation('snippet')
|
||||||
@ -134,12 +127,6 @@ const SnippetMainContent = ({
|
|||||||
return didSave
|
return didSave
|
||||||
}, [handlePublish, onBeforePublish, onSaved, t])
|
}, [handlePublish, onBeforePublish, onSaved, t])
|
||||||
|
|
||||||
const handleSaveAndExitEditing = useCallback(async () => {
|
|
||||||
const didSave = await handlePublishSnippet()
|
|
||||||
if (didSave)
|
|
||||||
onSavedAndExitEditing()
|
|
||||||
}, [handlePublishSnippet, onSavedAndExitEditing])
|
|
||||||
|
|
||||||
const navigateToPendingHref = useCallback((href: string) => {
|
const navigateToPendingHref = useCallback((href: string) => {
|
||||||
const url = new URL(href, window.location.href)
|
const url = new URL(href, window.location.href)
|
||||||
if (url.origin === window.location.origin)
|
if (url.origin === window.location.origin)
|
||||||
@ -226,18 +213,9 @@ const SnippetMainContent = ({
|
|||||||
<SnippetChildren
|
<SnippetChildren
|
||||||
snippetId={snippetId}
|
snippetId={snippetId}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
canDiscardChanges={canDiscardChanges}
|
|
||||||
canEdit={canEdit}
|
|
||||||
canSave={canSave}
|
canSave={canSave}
|
||||||
hasDraftChanges={hasDraftChanges}
|
|
||||||
isEditing={isEditing}
|
|
||||||
isPublishing={isPublishing}
|
isPublishing={isPublishing}
|
||||||
onCancel={onCancel}
|
|
||||||
onEdit={onEdit}
|
|
||||||
onExitEditing={onExitEditing}
|
|
||||||
onExitEditingWithoutSave={onExitEditingWithoutSave}
|
|
||||||
onPublish={handlePublishSnippet}
|
onPublish={handlePublishSnippet}
|
||||||
onSaveAndExitEditing={handleSaveAndExitEditing}
|
|
||||||
/>
|
/>
|
||||||
<SaveBeforeLeavingDialog
|
<SaveBeforeLeavingDialog
|
||||||
open={!!pendingHref}
|
open={!!pendingHref}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user