Template delete button/kira pilot (#4992)

* removed button

* ripped out delete dialog

* fixed tests

* added error message back

* redirecting after success
This commit is contained in:
Kira Pilot
2022-11-10 10:41:36 -05:00
committed by GitHub
parent 0eed533b17
commit 1c9677d37a
11 changed files with 253 additions and 190 deletions

View File

@ -5,10 +5,6 @@ import { makeStyles } from "@material-ui/core/styles"
import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
import { useMachine, useSelector } from "@xstate/react" import { useMachine, useSelector } from "@xstate/react"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
import { DeleteButton } from "components/DropdownButton/ActionCtas"
import { DropdownButton } from "components/DropdownButton/DropdownButton"
import { import {
PageHeader, PageHeader,
PageHeaderSubtitle, PageHeaderSubtitle,
@ -22,12 +18,7 @@ import {
Suspense, Suspense,
useContext, useContext,
} from "react" } from "react"
import { import { Link as RouterLink, NavLink, useParams } from "react-router-dom"
Link as RouterLink,
Navigate,
NavLink,
useParams,
} from "react-router-dom"
import { combineClasses } from "util/combineClasses" import { combineClasses } from "util/combineClasses"
import { firstLetter } from "util/firstLetter" import { firstLetter } from "util/firstLetter"
import { selectPermissions } from "xServices/auth/authSelectors" import { selectPermissions } from "xServices/auth/authSelectors"
@ -36,8 +27,8 @@ import {
TemplateContext, TemplateContext,
templateMachine, templateMachine,
} from "xServices/template/templateXService" } from "xServices/template/templateXService"
import { Margins } from "../../components/Margins/Margins" import { Margins } from "components/Margins/Margins"
import { Stack } from "../../components/Stack/Stack" import { Stack } from "components/Stack/Stack"
import { Permissions } from "xServices/auth/authXService" import { Permissions } from "xServices/auth/authXService"
import { Loader } from "components/Loader/Loader" import { Loader } from "components/Loader/Loader"
@ -76,11 +67,40 @@ export const useTemplateLayoutContext = (): TemplateLayoutContextValue => {
return context return context
} }
const TemplateSettingsButton: FC<{ templateName: string }> = ({
templateName,
}) => (
<Link
underline="none"
component={RouterLink}
to={`/templates/${templateName}/settings`}
>
<Button variant="outlined" startIcon={<SettingsOutlined />}>
{Language.settingsButton}
</Button>
</Link>
)
const CreateWorkspaceButton: FC<{
templateName: string
className?: string
}> = ({ templateName, className }) => (
<Link
underline="none"
component={RouterLink}
to={`/templates/${templateName}/workspace`}
>
<Button className={className ?? ""} startIcon={<AddCircleOutline />}>
{Language.createButton}
</Button>
</Link>
)
export const TemplateLayout: FC<PropsWithChildren> = ({ children }) => { export const TemplateLayout: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles() const styles = useStyles()
const organizationId = useOrganizationId() const organizationId = useOrganizationId()
const templateName = useTemplateName() const templateName = useTemplateName()
const [templateState, templateSend] = useMachine(templateMachine, { const [templateState, _] = useMachine(templateMachine, {
context: { context: {
templateName, templateName,
organizationId, organizationId,
@ -103,30 +123,20 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ children }) => {
!templateDAUs || !templateDAUs ||
!templatePermissions !templatePermissions
if (templateState.matches("deleted")) {
return <Navigate to="/templates" />
}
const hasIcon = template && template.icon && template.icon !== "" const hasIcon = template && template.icon && template.icon !== ""
const createWorkspaceButton = (className?: string) => ( const generatePageHeaderActions = (): JSX.Element[] => {
<Link const pageActions: JSX.Element[] = []
underline="none"
component={RouterLink}
to={`/templates/${templateName}/workspace`}
>
<Button
className={className ?? ""}
startIcon={<AddCircleOutline />}
disabled={isLoading}
>
{Language.createButton}
</Button>
</Link>
)
const handleDeleteTemplate = () => { if (!isLoading && templatePermissions.canUpdateTemplate) {
templateSend("DELETE") pageActions.push(<TemplateSettingsButton templateName={templateName} />)
}
if (!isLoading) {
pageActions.push(<CreateWorkspaceButton templateName={templateName} />)
}
return pageActions
} }
return ( return (
@ -134,36 +144,11 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ children }) => {
<Margins> <Margins>
<PageHeader <PageHeader
actions={ actions={
isLoading ? undefined : ( <>
<ChooseOne> {generatePageHeaderActions().map((action, i) => (
<Cond condition={templatePermissions.canUpdateTemplate}> <div key={i}>{action}</div>
<Link ))}
underline="none" </>
component={RouterLink}
to={`/templates/${template.name}/settings`}
>
<Button variant="outlined" startIcon={<SettingsOutlined />}>
{Language.settingsButton}
</Button>
</Link>
<DropdownButton
primaryAction={createWorkspaceButton(styles.actionButton)}
secondaryActions={[
{
action: "delete",
button: (
<DeleteButton handleAction={handleDeleteTemplate} />
),
},
]}
canCancel={false}
/>
</Cond>
<Cond>{createWorkspaceButton()}</Cond>
</ChooseOne>
)
} }
> >
<Stack direction="row" spacing={3} className={styles.pageTitle}> <Stack direction="row" spacing={3} className={styles.pageTitle}>
@ -234,31 +219,12 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ children }) => {
<Suspense fallback={<Loader />}>{children}</Suspense> <Suspense fallback={<Loader />}>{children}</Suspense>
</TemplateLayoutContext.Provider> </TemplateLayoutContext.Provider>
</Margins> </Margins>
{!isLoading && (
<DeleteDialog
isOpen={templateState.matches("confirmingDelete")}
confirmLoading={templateState.matches("deleting")}
onConfirm={() => {
templateSend("CONFIRM_DELETE")
}}
onCancel={() => {
templateSend("CANCEL_DELETE")
}}
entity="template"
name={template.name}
/>
)}
</> </>
) )
} }
export const useStyles = makeStyles((theme) => { export const useStyles = makeStyles((theme) => {
return { return {
actionButton: {
border: "none",
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
},
pageTitle: { pageTitle: {
alignItems: "center", alignItems: "center",
}, },

View File

@ -1,4 +1,13 @@
{ {
"deleteSuccess": "Template successfully deleted.", "deleteSuccess": "Template successfully deleted.",
"createdVersion": "created the version" "createdVersion": "created the version",
"templateSettings": {
"title": "Template settings",
"dangerZone": {
"dangerZoneHeader": "Danger Zone",
"deleteTemplateHeader": "Delete this template",
"deleteTemplateCaption": "Once you delete a template, there is no going back. Please be certain.",
"deleteCta": "Delete Template"
}
}
} }

View File

@ -1,4 +1,4 @@
import { fireEvent, screen } from "@testing-library/react" import { screen } from "@testing-library/react"
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout" import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"
import { rest } from "msw" import { rest } from "msw"
import { ResizeObserver } from "resize-observer" import { ResizeObserver } from "resize-observer"
@ -42,13 +42,6 @@ describe("TemplateSummaryPage", () => {
screen.getByText(MockWorkspaceResource.name) screen.getByText(MockWorkspaceResource.name)
screen.queryAllByText(`${MockTemplateVersion.name}`).length screen.queryAllByText(`${MockTemplateVersion.name}`).length
}) })
it("allows an admin to delete a template", async () => {
renderPage()
const dropdownButton = await screen.findByLabelText("open-dropdown")
fireEvent.click(dropdownButton)
const deleteButton = await screen.findByText("Delete")
expect(deleteButton).toBeDefined()
})
it("does not allow a member to delete a template", () => { it("does not allow a member to delete a template", () => {
// get member-level permissions // get member-level permissions
server.use( server.use(

View File

@ -12,7 +12,6 @@ export const TemplateSummaryPage: FC = () => {
activeTemplateVersion, activeTemplateVersion,
templateResources, templateResources,
templateVersions, templateVersions,
deleteTemplateError,
templateDAUs, templateDAUs,
} = context } = context
@ -31,7 +30,6 @@ export const TemplateSummaryPage: FC = () => {
templateResources={templateResources} templateResources={templateResources}
templateVersions={templateVersions} templateVersions={templateVersions}
templateDAUs={templateDAUs} templateDAUs={templateDAUs}
deleteTemplateError={deleteTemplateError}
/> />
</> </>
) )

View File

@ -5,7 +5,6 @@ import {
TemplateVersion, TemplateVersion,
WorkspaceResource, WorkspaceResource,
} from "api/typesGenerated" } from "api/typesGenerated"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { MemoizedMarkdown } from "components/Markdown/Markdown" import { MemoizedMarkdown } from "components/Markdown/Markdown"
import { Stack } from "components/Stack/Stack" import { Stack } from "components/Stack/Stack"
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
@ -21,7 +20,6 @@ export interface TemplateSummaryPageViewProps {
templateResources: WorkspaceResource[] templateResources: WorkspaceResource[]
templateVersions?: TemplateVersion[] templateVersions?: TemplateVersion[]
templateDAUs?: TemplateDAUsResponse templateDAUs?: TemplateDAUsResponse
deleteTemplateError: Error | unknown
} }
export const TemplateSummaryPageView: FC< export const TemplateSummaryPageView: FC<
@ -32,15 +30,10 @@ export const TemplateSummaryPageView: FC<
templateResources, templateResources,
templateVersions, templateVersions,
templateDAUs, templateDAUs,
deleteTemplateError,
}) => { }) => {
const styles = useStyles() const styles = useStyles()
const readme = frontMatter(activeTemplateVersion.readme) const readme = frontMatter(activeTemplateVersion.readme)
const deleteError = deleteTemplateError ? (
<AlertBanner severity="error" error={deleteTemplateError} dismissible />
) : null
const getStartedResources = (resources: WorkspaceResource[]) => { const getStartedResources = (resources: WorkspaceResource[]) => {
return resources.filter( return resources.filter(
(resource) => resource.workspace_transition === "start", (resource) => resource.workspace_transition === "start",
@ -49,7 +42,6 @@ export const TemplateSummaryPageView: FC<
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
{deleteError}
<TemplateStats <TemplateStats
template={template} template={template}
activeVersion={activeTemplateVersion} activeVersion={activeTemplateVersion}

View File

@ -5,7 +5,6 @@ import InputAdornment from "@material-ui/core/InputAdornment"
import Popover from "@material-ui/core/Popover" import Popover from "@material-ui/core/Popover"
import { makeStyles } from "@material-ui/core/styles" import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField" import TextField from "@material-ui/core/TextField"
import Typography from "@material-ui/core/Typography"
import { Template, UpdateTemplateMeta } from "api/typesGenerated" import { Template, UpdateTemplateMeta } from "api/typesGenerated"
import { OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { OpenDropdown } from "components/DropdownArrows/DropdownArrows"
import { FormFooter } from "components/FormFooter/FormFooter" import { FormFooter } from "components/FormFooter/FormFooter"
@ -188,9 +187,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
there are no validation errors for that field, display helper text. there are no validation errors for that field, display helper text.
We do not use the MUI helper-text prop because it overrides the validation error */} We do not use the MUI helper-text prop because it overrides the validation error */}
{form.values.default_ttl_ms && !form.errors.default_ttl_ms && ( {form.values.default_ttl_ms && !form.errors.default_ttl_ms && (
<Typography variant="subtitle2"> <span>{Language.ttlHelperText(form.values.default_ttl_ms)}</span>
{Language.ttlHelperText(form.values.default_ttl_ms)}
</Typography>
)} )}
</Stack> </Stack>

View File

@ -10,7 +10,7 @@ import {
validationSchema, validationSchema,
} from "./TemplateSettingsForm" } from "./TemplateSettingsForm"
import { TemplateSettingsPage } from "./TemplateSettingsPage" import { TemplateSettingsPage } from "./TemplateSettingsPage"
import { Language as ViewLanguage } from "./TemplateSettingsPageView" import i18next from "i18next"
const renderTemplateSettingsPage = async () => { const renderTemplateSettingsPage = async () => {
const renderResult = renderWithAuth(<TemplateSettingsPage />, { const renderResult = renderWithAuth(<TemplateSettingsPage />, {
@ -61,11 +61,25 @@ const fillAndSubmitForm = async ({
describe("TemplateSettingsPage", () => { describe("TemplateSettingsPage", () => {
it("renders", async () => { it("renders", async () => {
const { t } = i18next
const pageTitle = t("templateSettings.title", {
ns: "templatePage",
})
await renderTemplateSettingsPage() await renderTemplateSettingsPage()
const element = await screen.findByText(ViewLanguage.title) const element = await screen.findByText(pageTitle)
expect(element).toBeDefined() expect(element).toBeDefined()
}) })
it("allows an admin to delete a template", async () => {
const { t } = i18next
await renderTemplateSettingsPage()
const deleteCta = t("templateSettings.dangerZone.deleteCta", {
ns: "templatePage",
})
const deleteButton = await screen.findByText(deleteCta)
expect(deleteButton).toBeDefined()
})
it("succeeds", async () => { it("succeeds", async () => {
await renderTemplateSettingsPage() await renderTemplateSettingsPage()

View File

@ -28,6 +28,7 @@ export const TemplateSettingsPage: FC = () => {
templateSettings: template, templateSettings: template,
saveTemplateSettingsError, saveTemplateSettingsError,
getTemplateError, getTemplateError,
deleteTemplateError,
} = state.context } = state.context
return ( return (
@ -41,6 +42,7 @@ export const TemplateSettingsPage: FC = () => {
errors={{ errors={{
getTemplateError, getTemplateError,
saveTemplateSettingsError, saveTemplateSettingsError,
deleteTemplateError,
}} }}
onCancel={() => { onCancel={() => {
navigate(`/templates/${templateName}`) navigate(`/templates/${templateName}`)
@ -48,6 +50,14 @@ export const TemplateSettingsPage: FC = () => {
onSubmit={(templateSettings) => { onSubmit={(templateSettings) => {
send({ type: "SAVE", templateSettings }) send({ type: "SAVE", templateSettings })
}} }}
onDelete={() => {
send("DELETE")
}}
onConfirmDelete={() => send("CONFIRM_DELETE")}
onCancelDelete={() => send("CANCEL_DELETE")}
isConfirmingDelete={state.matches("confirmingDelete")}
isDeleting={state.matches("deleting")}
isDeleted={state.matches("deleted")}
/> />
</> </>
) )

View File

@ -4,19 +4,29 @@ import { FullPageForm } from "components/FullPageForm/FullPageForm"
import { Loader } from "components/Loader/Loader" import { Loader } from "components/Loader/Loader"
import { ComponentProps, FC } from "react" import { ComponentProps, FC } from "react"
import { TemplateSettingsForm } from "./TemplateSettingsForm" import { TemplateSettingsForm } from "./TemplateSettingsForm"
import { Stack } from "components/Stack/Stack"
export const Language = { import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
title: "Template settings", import { makeStyles } from "@material-ui/core/styles"
} import { colors } from "theme/colors"
import Button from "@material-ui/core/Button"
import { useTranslation } from "react-i18next"
import { Navigate } from "react-router-dom"
export interface TemplateSettingsPageViewProps { export interface TemplateSettingsPageViewProps {
template?: Template template?: Template
onSubmit: (data: UpdateTemplateMeta) => void onSubmit: (data: UpdateTemplateMeta) => void
onCancel: () => void onCancel: () => void
onDelete: () => void
onConfirmDelete: () => void
onCancelDelete: () => void
isConfirmingDelete: boolean
isDeleting: boolean
isDeleted: boolean
isSubmitting: boolean isSubmitting: boolean
errors?: { errors?: {
getTemplateError?: unknown getTemplateError?: unknown
saveTemplateSettingsError?: unknown saveTemplateSettingsError?: unknown
deleteTemplateError?: unknown
} }
initialTouched?: ComponentProps<typeof TemplateSettingsForm>["initialTouched"] initialTouched?: ComponentProps<typeof TemplateSettingsForm>["initialTouched"]
} }
@ -25,19 +35,39 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
template, template,
onCancel, onCancel,
onSubmit, onSubmit,
onDelete,
onConfirmDelete,
onCancelDelete,
isConfirmingDelete,
isDeleting,
isDeleted,
isSubmitting, isSubmitting,
errors = {}, errors = {},
initialTouched, initialTouched,
}) => { }) => {
const classes = useStyles()
const isLoading = !template && !errors.getTemplateError const isLoading = !template && !errors.getTemplateError
const { t } = useTranslation("templatePage")
if (isDeleted) {
return <Navigate to="/templates" />
}
return ( return (
<FullPageForm title={Language.title} onCancel={onCancel}> <FullPageForm title={t("templateSettings.title")} onCancel={onCancel}>
{Boolean(errors.getTemplateError) && ( {Boolean(errors.getTemplateError) && (
<Stack className={classes.errorContainer}>
<AlertBanner severity="error" error={errors.getTemplateError} /> <AlertBanner severity="error" error={errors.getTemplateError} />
</Stack>
)}
{Boolean(errors.deleteTemplateError) && (
<Stack className={classes.errorContainer}>
<AlertBanner severity="error" error={errors.deleteTemplateError} />
</Stack>
)} )}
{isLoading && <Loader />} {isLoading && <Loader />}
{template && ( {template && (
<>
<TemplateSettingsForm <TemplateSettingsForm
initialTouched={initialTouched} initialTouched={initialTouched}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
@ -46,7 +76,69 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
onCancel={onCancel} onCancel={onCancel}
error={errors.saveTemplateSettingsError} error={errors.saveTemplateSettingsError}
/> />
<Stack className={classes.dangerContainer}>
<div className={classes.dangerHeader}>
{t("templateSettings.dangerZone.dangerZoneHeader")}
</div>
<Stack className={classes.dangerBorder}>
<Stack spacing={0}>
<p className={classes.deleteTemplateHeader}>
{t("templateSettings.dangerZone.deleteTemplateHeader")}
</p>
<span>
{t("templateSettings.dangerZone.deleteTemplateCaption")}
</span>
</Stack>
<Button
className={classes.deleteButton}
onClick={onDelete}
aria-label={t("templateSettings.dangerZone.deleteCta")}
>
{t("templateSettings.dangerZone.deleteCta")}
</Button>
</Stack>
</Stack>
<DeleteDialog
isOpen={isConfirmingDelete}
confirmLoading={isDeleting}
onConfirm={onConfirmDelete}
onCancel={onCancelDelete}
entity="template"
name={template.name}
/>
</>
)} )}
</FullPageForm> </FullPageForm>
) )
} }
const useStyles = makeStyles((theme) => ({
errorContainer: {
marginBottom: theme.spacing(2),
},
dangerContainer: {
marginTop: theme.spacing(4),
},
dangerHeader: {
fontSize: theme.typography.h5.fontSize,
color: theme.palette.text.secondary,
},
dangerBorder: {
border: `1px solid ${colors.red[13]}`,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(2),
"& p": {
marginTop: "0px",
},
},
deleteTemplateHeader: {
fontSize: theme.typography.h6.fontSize,
fontWeight: "bold",
},
deleteButton: {
color: colors.red[8],
},
}))

View File

@ -1,22 +1,19 @@
import { displaySuccess } from "components/GlobalSnackbar/utils"
import { t } from "i18next"
import { assign, createMachine } from "xstate" import { assign, createMachine } from "xstate"
import { import {
checkAuthorization, checkAuthorization,
deleteTemplate,
getTemplateByName, getTemplateByName,
getTemplateDAUs, getTemplateDAUs,
getTemplateVersion, getTemplateVersion,
getTemplateVersionResources, getTemplateVersionResources,
getTemplateVersions, getTemplateVersions,
} from "../../api/api" } from "api/api"
import { import {
AuthorizationResponse, AuthorizationResponse,
Template, Template,
TemplateDAUsResponse, TemplateDAUsResponse,
TemplateVersion, TemplateVersion,
WorkspaceResource, WorkspaceResource,
} from "../../api/typesGenerated" } from "api/typesGenerated"
export interface TemplateContext { export interface TemplateContext {
organizationId: string organizationId: string
@ -27,15 +24,9 @@ export interface TemplateContext {
templateVersions?: TemplateVersion[] templateVersions?: TemplateVersion[]
templateDAUs?: TemplateDAUsResponse templateDAUs?: TemplateDAUsResponse
permissions?: AuthorizationResponse permissions?: AuthorizationResponse
deleteTemplateError?: Error | unknown
getTemplateError?: Error | unknown getTemplateError?: Error | unknown
} }
type TemplateEvent =
| { type: "DELETE" }
| { type: "CONFIRM_DELETE" }
| { type: "CANCEL_DELETE" }
const getPermissionsToCheck = (templateId: string) => ({ const getPermissionsToCheck = (templateId: string) => ({
canUpdateTemplate: { canUpdateTemplate: {
object: { object: {
@ -55,7 +46,6 @@ export const templateMachine =
tsTypes: {} as import("./templateXService.typegen").Typegen0, tsTypes: {} as import("./templateXService.typegen").Typegen0,
schema: { schema: {
context: {} as TemplateContext, context: {} as TemplateContext,
events: {} as TemplateEvent,
services: {} as { services: {} as {
getTemplate: { getTemplate: {
data: Template data: Template
@ -69,9 +59,6 @@ export const templateMachine =
getTemplateVersions: { getTemplateVersions: {
data: TemplateVersion[] data: TemplateVersion[]
} }
deleteTemplate: {
data: Template
}
getTemplateDAUs: { getTemplateDAUs: {
data: TemplateDAUsResponse data: TemplateDAUsResponse
} }
@ -201,11 +188,6 @@ export const templateMachine =
}, },
}, },
loaded: { loaded: {
on: {
DELETE: {
target: "confirmingDelete",
},
},
initial: "waiting", initial: "waiting",
states: { states: {
refreshingTemplate: { refreshingTemplate: {
@ -222,38 +204,6 @@ export const templateMachine =
}, },
}, },
}, },
confirmingDelete: {
on: {
CONFIRM_DELETE: {
target: "deleting",
},
CANCEL_DELETE: {
target: "loaded",
},
},
},
deleting: {
entry: "clearDeleteTemplateError",
invoke: {
src: "deleteTemplate",
id: "deleteTemplate",
onDone: [
{
target: "deleted",
actions: "displayDeleteSuccess",
},
],
onError: [
{
actions: "assignDeleteTemplateError",
target: "loaded",
},
],
},
},
deleted: {
type: "final",
},
error: { error: {
type: "final", type: "final",
}, },
@ -284,12 +234,6 @@ export const templateMachine =
return getTemplateVersions(ctx.template.id) return getTemplateVersions(ctx.template.id)
}, },
deleteTemplate: (ctx) => {
if (!ctx.template) {
throw new Error("Template not loaded")
}
return deleteTemplate(ctx.template.id)
},
getTemplateDAUs: (ctx) => { getTemplateDAUs: (ctx) => {
if (!ctx.template) { if (!ctx.template) {
throw new Error("Template not loaded") throw new Error("Template not loaded")
@ -327,14 +271,6 @@ export const templateMachine =
assignPermissions: assign({ assignPermissions: assign({
permissions: (_, event) => event.data, permissions: (_, event) => event.data,
}), }),
assignDeleteTemplateError: assign({
deleteTemplateError: (_, event) => event.data,
}),
clearDeleteTemplateError: assign({
deleteTemplateError: (_) => undefined,
}),
displayDeleteSuccess: () =>
displaySuccess(t("deleteSuccess", { ns: "templatePage" })),
}, },
}, },
) )

View File

@ -1,7 +1,9 @@
import { getTemplateByName, updateTemplateMeta } from "api/api" import { getTemplateByName, updateTemplateMeta, deleteTemplate } from "api/api"
import { Template, UpdateTemplateMeta } from "api/typesGenerated" import { Template, UpdateTemplateMeta } from "api/typesGenerated"
import { createMachine } from "xstate" import { createMachine } from "xstate"
import { assign } from "xstate/lib/actions" import { assign } from "xstate/lib/actions"
import { displaySuccess } from "components/GlobalSnackbar/utils"
import { t } from "i18next"
export const templateSettingsMachine = export const templateSettingsMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QBcwFsAOAbAhqgymMsgJYB2UsAdFgPY4TlQDEEtZYV5AbrQNadUmXASKkK1OgyYIetAMZ4S7ANoAGALqJQGWrBKl22kAA9EANgCMVAKwB2AByWATJbWWAnABYv55w-MAGhAAT0RnAGZnKgibGw9nD3M1JJsHNQiAX0zgoWw8MEJiJmpIAyZmfABBADUAUWNdfUMyYzMESx9bSK8IhwdncwcbF2dgsIRejypzX2dXf09zCLUbbNz0fNFiiSpYHG4Ktg4uMl4BKjyRQrESvYOZOUUW9S0kECbyo3f25KoHOxeDwODx9EYeNTzcYWGxqKgeGw+SJ2cx+VZebI5EBkWgQODGK4FIriSg0eiMCiNPRfVo-RBeMahRAQqhqNnuWERFFeOyedYgQnbEmlRgkqnNZS00DtSwRcz-EY2PzeeYOLk2aEIWJ2WwOIEBNIeBG8-mCm47Un7Q6U96fFptenWRJDIZy+y9AFBJkIEbRbxeWUBRwIyx2U2ba7Eu5WyDimkOhAI2yxSx+foMpWWTV2RLJgIrPoA4bh4RE24SOP2ukdHXOgJq8zuvoozWWBys9nzSydNt2NQYzFAA */ /** @xstate-layout N4IgpgJg5mDOIC5QBcwFsAOAbAhqgymMsgJYB2UsAdFgPY4TlQDEEtZYV5AbrQNadUmXASKkK1OgyYIetAMZ4S7ANoAGALqJQGWrBKl22kAA9EANgCMVAKwB2AByWATJbWWAnABYv55w-MAGhAAT0RnAGZnKgibGw9nD3M1JJsHNQiAX0zgoWw8MEJiJmpIAyZmfABBADUAUWNdfUMyYzMESx9bSK8IhwdncwcbF2dgsIRejypzX2dXf09zCLUbbNz0fNFiiSpYHG4Ktg4uMl4BKjyRQrESvYOZOUUW9S0kECbyo3f25KoHOxeDwODx9EYeNTzcYWGxqKgeGw+SJ2cx+VZebI5EBkWgQODGK4FIriSg0eiMCiNPRfVo-RBeMahRAQqhqNnuWERFFeOyedYgQnbEmlRgkqnNZS00DtSwRcz-EY2PzeeYOLk2aEIWJ2WwOIEBNIeBG8-mCm47Un7Q6U96fFptenWRJDIZy+y9AFBJkIEbRbxeWUBRwIyx2U2ba7Eu5WyDimkOhAI2yxSx+foMpWWTV2RLJgIrPoA4bh4RE24SOP2ukdHXOgJq8zuvoozWWBys9nzSydNt2NQYzFAA */
@ -17,6 +19,7 @@ export const templateSettingsMachine =
templateSettings?: Template templateSettings?: Template
getTemplateError?: unknown getTemplateError?: unknown
saveTemplateSettingsError?: unknown saveTemplateSettingsError?: unknown
deleteTemplateError?: Error | unknown
} }
services: { services: {
getTemplateSettings: { getTemplateSettings: {
@ -26,7 +29,11 @@ export const templateSettingsMachine =
data: Template data: Template
} }
} }
events: { type: "SAVE"; templateSettings: UpdateTemplateMeta } events:
| { type: "SAVE"; templateSettings: UpdateTemplateMeta }
| { type: "DELETE" }
| { type: "CONFIRM_DELETE" }
| { type: "CANCEL_DELETE" }
}, },
initial: "loading", initial: "loading",
states: { states: {
@ -50,8 +57,43 @@ export const templateSettingsMachine =
SAVE: { SAVE: {
target: "saving", target: "saving",
}, },
DELETE: {
target: "confirmingDelete",
}, },
}, },
},
confirmingDelete: {
on: {
CONFIRM_DELETE: {
target: "deleting",
},
CANCEL_DELETE: {
target: "editing",
},
},
},
deleting: {
entry: "clearDeleteTemplateError",
invoke: {
src: "deleteTemplate",
id: "deleteTemplate",
onDone: [
{
target: "deleted",
actions: "displayDeleteSuccess",
},
],
onError: [
{
actions: "assignDeleteTemplateError",
target: "editing",
},
],
},
},
deleted: {
type: "final",
},
saving: { saving: {
invoke: { invoke: {
src: "saveTemplateSettings", src: "saveTemplateSettings",
@ -94,6 +136,12 @@ export const templateSettingsMachine =
return updateTemplateMeta(templateSettings.id, newTemplateSettings) return updateTemplateMeta(templateSettings.id, newTemplateSettings)
}, },
deleteTemplate: (ctx) => {
if (!ctx.templateSettings) {
throw new Error("Template not loaded")
}
return deleteTemplate(ctx.templateSettings.id)
},
}, },
actions: { actions: {
assignTemplateSettings: assign({ assignTemplateSettings: assign({
@ -105,6 +153,14 @@ export const templateSettingsMachine =
assignSaveTemplateSettingsError: assign({ assignSaveTemplateSettingsError: assign({
saveTemplateSettingsError: (_, { data }) => data, saveTemplateSettingsError: (_, { data }) => data,
}), }),
assignDeleteTemplateError: assign({
deleteTemplateError: (_, event) => event.data,
}),
clearDeleteTemplateError: assign({
deleteTemplateError: (_) => undefined,
}),
displayDeleteSuccess: () =>
displaySuccess(t("deleteSuccess", { ns: "templatePage" })),
}, },
}, },
) )