mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
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:
@ -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",
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -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],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
@ -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" })),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -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" })),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user