mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Feat: delete template button (#3781)
* Add api call * Extract DropDownButton * Start adding DropdownButton to Template page * Move stories to dropdown button * Format * Update xservice to delete * Deletion flow * Format * Move ErrorSummary for consistency * RBAC (unfinished) and style tweak * Format * Test rbac * Format * Move ErrorSummary under PageHeader in workspace and template * Format * Replace hook with onBlur * Make style arg optional * Format
This commit is contained in:
@@ -153,6 +153,11 @@ export const updateTemplateMeta = async (
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => {
|
||||
const response = await axios.delete<TypesGen.Template>(`/api/v2/templates/${templateId}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getWorkspace = async (
|
||||
workspaceId: string,
|
||||
params?: TypesGen.WorkspaceOptions,
|
||||
|
||||
@@ -21,10 +21,15 @@ interface ArrowProps {
|
||||
|
||||
export const OpenDropdown: FC<ArrowProps> = ({ margin = true }) => {
|
||||
const styles = useStyles({ margin })
|
||||
return <KeyboardArrowDown className={styles.arrowIcon} />
|
||||
return <KeyboardArrowDown aria-label="open-dropdown" className={styles.arrowIcon} />
|
||||
}
|
||||
|
||||
export const CloseDropdown: FC<ArrowProps> = ({ margin = true }) => {
|
||||
const styles = useStyles({ margin })
|
||||
return <KeyboardArrowUp className={`${styles.arrowIcon} ${styles.arrowIconUp}`} />
|
||||
return (
|
||||
<KeyboardArrowUp
|
||||
aria-label="close-dropdown"
|
||||
className={`${styles.arrowIcon} ${styles.arrowIconUp}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { WorkspaceStateEnum } from "util/workspace"
|
||||
import { DeleteButton, DisabledButton, StartButton, UpdateButton } from "./ActionCtas"
|
||||
import { DropdownButton, DropdownButtonProps } from "./DropdownButton"
|
||||
|
||||
export default {
|
||||
title: "Components/DropdownButton",
|
||||
component: DropdownButton,
|
||||
}
|
||||
|
||||
const Template: Story<DropdownButtonProps> = (args) => <DropdownButton {...args} />
|
||||
|
||||
export const WithDropdown = Template.bind({})
|
||||
WithDropdown.args = {
|
||||
primaryAction: <StartButton handleAction={action("start")} />,
|
||||
secondaryActions: [
|
||||
{ action: "update", button: <UpdateButton handleAction={action("update")} /> },
|
||||
{ action: "delete", button: <DeleteButton handleAction={action("delete")} /> },
|
||||
],
|
||||
canCancel: false,
|
||||
}
|
||||
|
||||
export const WithCancel = Template.bind({})
|
||||
WithCancel.args = {
|
||||
primaryAction: <DisabledButton workspaceState={WorkspaceStateEnum.deleting} />,
|
||||
secondaryActions: [],
|
||||
canCancel: true,
|
||||
handleCancel: action("cancel"),
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Popover from "@material-ui/core/Popover"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
|
||||
import { DropdownContent } from "components/DropdownButton/DropdownContent/DropdownContent"
|
||||
import { FC, ReactNode, useRef, useState } from "react"
|
||||
import { CancelButton } from "./ActionCtas"
|
||||
|
||||
export interface DropdownButtonProps {
|
||||
primaryAction: ReactNode
|
||||
secondaryActions: Array<{ action: string; button: ReactNode }>
|
||||
canCancel: boolean
|
||||
handleCancel?: () => void
|
||||
}
|
||||
|
||||
export const DropdownButton: FC<DropdownButtonProps> = ({
|
||||
primaryAction,
|
||||
secondaryActions,
|
||||
canCancel,
|
||||
handleCancel,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const id = isOpen ? "action-popover" : undefined
|
||||
|
||||
return (
|
||||
<span className={styles.buttonContainer}>
|
||||
{/* primary workspace CTA */}
|
||||
<span data-testid="primary-cta" className={styles.primaryCta}>
|
||||
{primaryAction}
|
||||
</span>
|
||||
{canCancel && handleCancel ? (
|
||||
<CancelButton handleAction={handleCancel} />
|
||||
) : (
|
||||
<>
|
||||
{/* popover toggle button */}
|
||||
<Button
|
||||
data-testid="workspace-actions-button"
|
||||
aria-controls="workspace-actions-menu"
|
||||
aria-haspopup="true"
|
||||
className={styles.dropdownButton}
|
||||
ref={anchorRef}
|
||||
disabled={!secondaryActions.length}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
>
|
||||
{isOpen ? <CloseDropdown /> : <OpenDropdown />}
|
||||
</Button>
|
||||
<Popover
|
||||
classes={{ paper: styles.popoverPaper }}
|
||||
id={id}
|
||||
open={isOpen}
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={() => setIsOpen(false)}
|
||||
onBlur={() => setIsOpen(false)}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
{/* secondary workspace CTAs */}
|
||||
<DropdownContent secondaryActions={secondaryActions} />
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
buttonContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
display: "inline-flex",
|
||||
},
|
||||
dropdownButton: {
|
||||
border: "none",
|
||||
borderLeft: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
|
||||
minWidth: "unset",
|
||||
width: "63px", // matching cancel button so button grouping doesn't grow in size
|
||||
"& .MuiButton-label": {
|
||||
marginRight: "8px",
|
||||
},
|
||||
},
|
||||
primaryCta: {
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
width: "100%",
|
||||
|
||||
"& > *": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
},
|
||||
popoverPaper: {
|
||||
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing(1)}px`,
|
||||
},
|
||||
}))
|
||||
+4
-7
@@ -1,24 +1,21 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { FC } from "react"
|
||||
import { ButtonMapping, ButtonTypesEnum } from "../constants"
|
||||
import { FC, ReactNode } from "react"
|
||||
|
||||
export interface DropdownContentProps {
|
||||
secondaryActions: ButtonTypesEnum[]
|
||||
buttonMapping: Partial<ButtonMapping>
|
||||
secondaryActions: Array<{ action: string; button: ReactNode }>
|
||||
}
|
||||
|
||||
/* secondary workspace CTAs */
|
||||
export const DropdownContent: FC<React.PropsWithChildren<DropdownContentProps>> = ({
|
||||
secondaryActions,
|
||||
buttonMapping,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<span data-testid="secondary-ctas">
|
||||
{secondaryActions.map((action) => (
|
||||
{secondaryActions.map(({ action, button }) => (
|
||||
<div key={action} className={styles.popoverActionButton}>
|
||||
{buttonMapping[action]}
|
||||
{button}
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
@@ -68,20 +68,19 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
const styles = useStyles()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const buildError = workspaceErrors[WorkspaceErrors.BUILD_ERROR] ? (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.BUILD_ERROR]} dismissible />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
const cancellationError = workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR] ? (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]} dismissible />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<Stack spacing={1}>
|
||||
{workspaceErrors[WorkspaceErrors.BUILD_ERROR] ? (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.BUILD_ERROR]} dismissible />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR] ? (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]} dismissible />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Stack>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Stack direction="row" spacing={1} className={styles.actions}>
|
||||
@@ -109,39 +108,37 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
<PageHeaderSubtitle>{workspace.owner_name}</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<Stack direction="row" spacing={3}>
|
||||
<Stack direction="column" className={styles.firstColumnSpacer} spacing={3}>
|
||||
<WorkspaceScheduleBanner
|
||||
isLoading={bannerProps.isLoading}
|
||||
onExtend={bannerProps.onExtend}
|
||||
<Stack direction="column" className={styles.firstColumnSpacer} spacing={2.5}>
|
||||
{buildError}
|
||||
{cancellationError}
|
||||
|
||||
<WorkspaceScheduleBanner
|
||||
isLoading={bannerProps.isLoading}
|
||||
onExtend={bannerProps.onExtend}
|
||||
workspace={workspace}
|
||||
/>
|
||||
|
||||
<WorkspaceDeletedBanner workspace={workspace} handleClick={() => navigate(`/templates`)} />
|
||||
|
||||
<WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
|
||||
|
||||
{!!resources && !!resources.length && (
|
||||
<Resources
|
||||
resources={resources}
|
||||
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
|
||||
workspace={workspace}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
buildInfo={buildInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WorkspaceDeletedBanner
|
||||
workspace={workspace}
|
||||
handleClick={() => navigate(`/templates`)}
|
||||
/>
|
||||
|
||||
<WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
|
||||
|
||||
{!!resources && !!resources.length && (
|
||||
<Resources
|
||||
resources={resources}
|
||||
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
|
||||
workspace={workspace}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
buildInfo={buildInfo}
|
||||
/>
|
||||
<WorkspaceSection title="Logs" contentsProps={{ className: styles.timelineContents }}>
|
||||
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]} />
|
||||
) : (
|
||||
<BuildsTable builds={builds} className={styles.timelineTable} />
|
||||
)}
|
||||
|
||||
<WorkspaceSection title="Logs" contentsProps={{ className: styles.timelineContents }}>
|
||||
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]} />
|
||||
) : (
|
||||
<BuildsTable builds={builds} className={styles.timelineTable} />
|
||||
)}
|
||||
</WorkspaceSection>
|
||||
</Stack>
|
||||
</WorkspaceSection>
|
||||
</Stack>
|
||||
</Margins>
|
||||
)
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { WorkspaceStateEnum } from "util/workspace"
|
||||
import { DeleteButton, StartButton, StopButton } from "../ActionCtas"
|
||||
import { ButtonMapping, ButtonTypesEnum, WorkspaceStateActions } from "../constants"
|
||||
import { DropdownContent, DropdownContentProps } from "./DropdownContent"
|
||||
|
||||
// These are the stories for the secondary actions (housed in the dropdown)
|
||||
// in WorkspaceActions.tsx
|
||||
|
||||
export default {
|
||||
title: "WorkspaceActionsDropdown",
|
||||
component: DropdownContent,
|
||||
}
|
||||
|
||||
const Template: Story<DropdownContentProps> = (args) => <DropdownContent {...args} />
|
||||
|
||||
const buttonMappingMock: Partial<ButtonMapping> = {
|
||||
[ButtonTypesEnum.delete]: <DeleteButton handleAction={() => jest.fn()} />,
|
||||
[ButtonTypesEnum.start]: <StartButton handleAction={() => jest.fn()} />,
|
||||
[ButtonTypesEnum.stop]: <StopButton handleAction={() => jest.fn()} />,
|
||||
[ButtonTypesEnum.delete]: <DeleteButton handleAction={() => jest.fn()} />,
|
||||
}
|
||||
|
||||
const defaultArgs = {
|
||||
buttonMapping: buttonMappingMock,
|
||||
}
|
||||
|
||||
export const Started = Template.bind({})
|
||||
Started.args = {
|
||||
...defaultArgs,
|
||||
secondaryActions: WorkspaceStateActions[WorkspaceStateEnum.started].secondary,
|
||||
}
|
||||
|
||||
export const Stopped = Template.bind({})
|
||||
Stopped.args = {
|
||||
...defaultArgs,
|
||||
secondaryActions: WorkspaceStateActions[WorkspaceStateEnum.stopped].secondary,
|
||||
}
|
||||
|
||||
export const Canceled = Template.bind({})
|
||||
Canceled.args = {
|
||||
...defaultArgs,
|
||||
secondaryActions: WorkspaceStateActions[WorkspaceStateEnum.canceled].secondary,
|
||||
}
|
||||
|
||||
export const Errored = Template.bind({})
|
||||
Errored.args = {
|
||||
...defaultArgs,
|
||||
secondaryActions: WorkspaceStateActions[WorkspaceStateEnum.error].secondary,
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { fireEvent, screen } from "@testing-library/react"
|
||||
import { WorkspaceStateEnum } from "util/workspace"
|
||||
import * as Mocks from "../../testHelpers/entities"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { Language } from "./ActionCtas"
|
||||
import { Language } from "../DropdownButton/ActionCtas"
|
||||
import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions"
|
||||
|
||||
const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Popover from "@material-ui/core/Popover"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { FC, ReactNode, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { DropdownButton } from "components/DropdownButton/DropdownButton"
|
||||
import { FC, ReactNode, useMemo } from "react"
|
||||
import { getWorkspaceStatus, WorkspaceStateEnum, WorkspaceStatus } from "util/workspace"
|
||||
import { Workspace } from "../../api/typesGenerated"
|
||||
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
|
||||
import {
|
||||
ActionLoadingButton,
|
||||
CancelButton,
|
||||
@@ -14,9 +11,8 @@ import {
|
||||
StartButton,
|
||||
StopButton,
|
||||
UpdateButton,
|
||||
} from "./ActionCtas"
|
||||
} from "../DropdownButton/ActionCtas"
|
||||
import { ButtonMapping, ButtonTypesEnum, WorkspaceStateActions } from "./constants"
|
||||
import { DropdownContent } from "./DropdownContent/DropdownContent"
|
||||
|
||||
/**
|
||||
* Jobs submitted while another job is in progress will be discarded,
|
||||
@@ -43,11 +39,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
handleUpdate,
|
||||
handleCancel,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const id = isOpen ? "action-popover" : undefined
|
||||
|
||||
const workspaceStatus: keyof typeof WorkspaceStateEnum = getWorkspaceStatus(
|
||||
workspace.latest_build,
|
||||
)
|
||||
@@ -70,16 +61,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
return updatedActions
|
||||
}, [canBeUpdated, workspaceState])
|
||||
|
||||
/**
|
||||
* Ensures we close the popover before calling any action handler
|
||||
*/
|
||||
useEffect(() => {
|
||||
setIsOpen(false)
|
||||
return () => {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [workspaceStatus])
|
||||
|
||||
// A mapping of button type to the corresponding React component
|
||||
const buttonMapping: ButtonMapping = {
|
||||
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
|
||||
@@ -98,80 +79,14 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.buttonContainer}>
|
||||
{/* primary workspace CTA */}
|
||||
<span data-testid="primary-cta" className={styles.primaryCta}>
|
||||
{buttonMapping[actions.primary]}
|
||||
</span>
|
||||
{actions.canCancel ? (
|
||||
// cancel CTA
|
||||
<>{buttonMapping[ButtonTypesEnum.cancel]}</>
|
||||
) : (
|
||||
<>
|
||||
{/* popover toggle button */}
|
||||
<Button
|
||||
data-testid="workspace-actions-button"
|
||||
aria-controls="workspace-actions-menu"
|
||||
aria-haspopup="true"
|
||||
className={styles.dropdownButton}
|
||||
ref={anchorRef}
|
||||
disabled={!actions.secondary.length}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
>
|
||||
{isOpen ? <CloseDropdown /> : <OpenDropdown />}
|
||||
</Button>
|
||||
<Popover
|
||||
classes={{ paper: styles.popoverPaper }}
|
||||
id={id}
|
||||
open={isOpen}
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={() => setIsOpen(false)}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
{/* secondary workspace CTAs */}
|
||||
<DropdownContent secondaryActions={actions.secondary} buttonMapping={buttonMapping} />
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<DropdownButton
|
||||
primaryAction={buttonMapping[actions.primary]}
|
||||
canCancel={actions.canCancel}
|
||||
handleCancel={handleCancel}
|
||||
secondaryActions={actions.secondary.map((action) => ({
|
||||
action,
|
||||
button: buttonMapping[action],
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
buttonContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
display: "inline-flex",
|
||||
},
|
||||
dropdownButton: {
|
||||
border: "none",
|
||||
borderLeft: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
|
||||
minWidth: "unset",
|
||||
width: "63px", // matching cancel button so button grouping doesn't grow in size
|
||||
"& .MuiButton-label": {
|
||||
marginRight: "8px",
|
||||
},
|
||||
},
|
||||
primaryCta: {
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
width: "100%",
|
||||
|
||||
"& > *": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
},
|
||||
popoverPaper: {
|
||||
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing(1)}px`,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -3,20 +3,20 @@ import { WorkspaceStateEnum } from "util/workspace"
|
||||
|
||||
// the button types we have
|
||||
export enum ButtonTypesEnum {
|
||||
start,
|
||||
starting,
|
||||
stop,
|
||||
stopping,
|
||||
delete,
|
||||
deleting,
|
||||
update,
|
||||
cancel,
|
||||
error,
|
||||
start = "start",
|
||||
starting = "starting",
|
||||
stop = "stop",
|
||||
stopping = "stopping",
|
||||
delete = "delete",
|
||||
deleting = "deleting",
|
||||
update = "update",
|
||||
cancel = "cancel",
|
||||
error = "error",
|
||||
// disabled buttons
|
||||
canceling,
|
||||
disabled,
|
||||
queued,
|
||||
loading,
|
||||
canceling = "canceling",
|
||||
disabled = "disabled",
|
||||
queued = "queued",
|
||||
loading = "loading",
|
||||
}
|
||||
|
||||
export type ButtonMapping = {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import common from "./common.json"
|
||||
import templatePage from "./templatePage.json"
|
||||
import workspacePage from "./workspacePage.json"
|
||||
|
||||
export const en = {
|
||||
common,
|
||||
workspacePage,
|
||||
templatePage,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"deleteDialog": {
|
||||
"title": "Delete template",
|
||||
"message": "Are you sure you want to delete this template?",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"deleteSuccess": "Template successfully deleted."
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import { server } from "testHelpers/server"
|
||||
import * as CreateDayString from "util/createDayString"
|
||||
import {
|
||||
MockMemberPermissions,
|
||||
MockTemplate,
|
||||
MockTemplateVersion,
|
||||
MockUser,
|
||||
MockWorkspaceResource,
|
||||
renderWithAuth,
|
||||
} from "../../testHelpers/renderHelpers"
|
||||
@@ -23,4 +27,28 @@ describe("TemplatePage", () => {
|
||||
screen.getByText(MockWorkspaceResource.name)
|
||||
screen.queryAllByText(`${MockTemplateVersion.name}`).length
|
||||
})
|
||||
it("allows an admin to delete a template", async () => {
|
||||
renderWithAuth(<TemplatePage />, {
|
||||
route: `/templates/${MockTemplate.id}`,
|
||||
path: "/templates/:template",
|
||||
})
|
||||
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", () => {
|
||||
// get member-level permissions
|
||||
server.use(
|
||||
rest.post(`/api/v2/users/${MockUser.id}/authorization`, async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(MockMemberPermissions))
|
||||
}),
|
||||
)
|
||||
renderWithAuth(<TemplatePage />, {
|
||||
route: `/templates/${MockTemplate.id}`,
|
||||
path: "/templates/:template",
|
||||
})
|
||||
const dropdownButton = screen.queryByLabelText("open-dropdown")
|
||||
expect(dropdownButton).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { FC } from "react"
|
||||
import { useMachine, useSelector } from "@xstate/react"
|
||||
import { ConfirmDialog } from "components/ConfirmDialog/ConfirmDialog"
|
||||
import { FC, useContext } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Navigate, useParams } from "react-router-dom"
|
||||
import { selectPermissions } from "xServices/auth/authSelectors"
|
||||
import { XServiceContext } from "xServices/StateContext"
|
||||
import { Loader } from "../../components/Loader/Loader"
|
||||
import { useOrganizationId } from "../../hooks/useOrganizationId"
|
||||
import { pageTitle } from "../../util/page"
|
||||
@@ -20,21 +24,37 @@ const useTemplateName = () => {
|
||||
|
||||
export const TemplatePage: FC<React.PropsWithChildren<unknown>> = () => {
|
||||
const organizationId = useOrganizationId()
|
||||
const { t } = useTranslation("templatePage")
|
||||
const templateName = useTemplateName()
|
||||
const [templateState] = useMachine(templateMachine, {
|
||||
const [templateState, templateSend] = useMachine(templateMachine, {
|
||||
context: {
|
||||
templateName,
|
||||
organizationId,
|
||||
},
|
||||
})
|
||||
const { template, activeTemplateVersion, templateResources, templateVersions } =
|
||||
templateState.context
|
||||
const isLoading = !template || !activeTemplateVersion || !templateResources
|
||||
const {
|
||||
template,
|
||||
activeTemplateVersion,
|
||||
templateResources,
|
||||
templateVersions,
|
||||
deleteTemplateError,
|
||||
} = templateState.context
|
||||
const xServices = useContext(XServiceContext)
|
||||
const permissions = useSelector(xServices.authXService, selectPermissions)
|
||||
const isLoading = !template || !activeTemplateVersion || !templateResources || !permissions
|
||||
|
||||
const handleDeleteTemplate = () => {
|
||||
templateSend("DELETE")
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
if (templateState.matches("deleted")) {
|
||||
return <Navigate to="/templates" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -45,6 +65,25 @@ export const TemplatePage: FC<React.PropsWithChildren<unknown>> = () => {
|
||||
activeTemplateVersion={activeTemplateVersion}
|
||||
templateResources={templateResources}
|
||||
templateVersions={templateVersions}
|
||||
canDeleteTemplate={permissions.deleteTemplates}
|
||||
handleDeleteTemplate={handleDeleteTemplate}
|
||||
deleteTemplateError={deleteTemplateError}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={templateState.matches("confirmingDelete")}
|
||||
confirmLoading={templateState.matches("deleting")}
|
||||
title={t("deleteDialog.title")}
|
||||
confirmText={t("deleteDialog.confirm")}
|
||||
onConfirm={() => {
|
||||
templateSend("CONFIRM_DELETE")
|
||||
}}
|
||||
onClose={() => {
|
||||
templateSend("CANCEL_DELETE")
|
||||
}}
|
||||
description={<>{t("deleteDialog.message")}</>}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,9 @@ import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
|
||||
import { DeleteButton } from "components/DropdownButton/ActionCtas"
|
||||
import { DropdownButton } from "components/DropdownButton/DropdownButton"
|
||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||
import frontMatter from "front-matter"
|
||||
import { FC } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
@@ -36,6 +39,9 @@ export interface TemplatePageViewProps {
|
||||
activeTemplateVersion: TemplateVersion
|
||||
templateResources: WorkspaceResource[]
|
||||
templateVersions?: TemplateVersion[]
|
||||
handleDeleteTemplate: (templateId: string) => void
|
||||
deleteTemplateError: Error | unknown
|
||||
canDeleteTemplate: boolean
|
||||
}
|
||||
|
||||
export const TemplatePageView: FC<React.PropsWithChildren<TemplatePageViewProps>> = ({
|
||||
@@ -43,97 +49,131 @@ export const TemplatePageView: FC<React.PropsWithChildren<TemplatePageViewProps>
|
||||
activeTemplateVersion,
|
||||
templateResources,
|
||||
templateVersions,
|
||||
handleDeleteTemplate,
|
||||
deleteTemplateError,
|
||||
canDeleteTemplate,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const readme = frontMatter(activeTemplateVersion.readme)
|
||||
const hasIcon = template.icon && template.icon !== ""
|
||||
|
||||
const deleteError = deleteTemplateError ? (
|
||||
<ErrorSummary error={deleteTemplateError} dismissible />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
const getStartedResources = (resources: WorkspaceResource[]) => {
|
||||
return resources.filter((resource) => resource.workspace_transition === "start")
|
||||
}
|
||||
|
||||
const createWorkspaceButton = (className?: string) => (
|
||||
<Link underline="none" component={RouterLink} to={`/templates/${template.name}/workspace`}>
|
||||
<Button className={className ?? ""} startIcon={<AddCircleOutline />}>
|
||||
{Language.createButton}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Link
|
||||
underline="none"
|
||||
component={RouterLink}
|
||||
to={`/templates/${template.name}/settings`}
|
||||
>
|
||||
<Button variant="outlined" startIcon={<SettingsOutlined />}>
|
||||
{Language.settingsButton}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
underline="none"
|
||||
component={RouterLink}
|
||||
to={`/templates/${template.name}/workspace`}
|
||||
>
|
||||
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack direction="row" spacing={3} className={styles.pageTitle}>
|
||||
<div>
|
||||
{hasIcon ? (
|
||||
<div className={styles.iconWrapper}>
|
||||
<img src={template.icon} alt="" />
|
||||
</div>
|
||||
) : (
|
||||
<Avatar className={styles.avatar}>{firstLetter(template.name)}</Avatar>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<PageHeaderTitle>{template.name}</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
{template.description === "" ? Language.noDescription : template.description}
|
||||
</PageHeaderSubtitle>
|
||||
</div>
|
||||
</Stack>
|
||||
</PageHeader>
|
||||
<>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Link
|
||||
underline="none"
|
||||
component={RouterLink}
|
||||
to={`/templates/${template.name}/settings`}
|
||||
>
|
||||
<Button variant="outlined" startIcon={<SettingsOutlined />}>
|
||||
{Language.settingsButton}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Stack spacing={2.5}>
|
||||
<TemplateStats template={template} activeVersion={activeTemplateVersion} />
|
||||
<WorkspaceSection
|
||||
title={Language.resourcesTitle}
|
||||
contentsProps={{ className: styles.resourcesTableContents }}
|
||||
{canDeleteTemplate ? (
|
||||
<DropdownButton
|
||||
primaryAction={createWorkspaceButton(styles.actionButton)}
|
||||
secondaryActions={[
|
||||
{
|
||||
action: "delete",
|
||||
button: (
|
||||
<DeleteButton handleAction={() => handleDeleteTemplate(template.id)} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
canCancel={false}
|
||||
/>
|
||||
) : (
|
||||
createWorkspaceButton()
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<TemplateResourcesTable resources={getStartedResources(templateResources)} />
|
||||
</WorkspaceSection>
|
||||
<WorkspaceSection
|
||||
title={Language.readmeTitle}
|
||||
contentsProps={{ className: styles.readmeContents }}
|
||||
>
|
||||
<div className={styles.markdownWrapper}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, target, children }) => (
|
||||
<Link href={href} target={target}>
|
||||
{children}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{readme.body}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</WorkspaceSection>
|
||||
<WorkspaceSection
|
||||
title={Language.versionsTitle}
|
||||
contentsProps={{ className: styles.versionsTableContents }}
|
||||
>
|
||||
<VersionsTable versions={templateVersions} />
|
||||
</WorkspaceSection>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={3} className={styles.pageTitle}>
|
||||
<div>
|
||||
{hasIcon ? (
|
||||
<div className={styles.iconWrapper}>
|
||||
<img src={template.icon} alt="" />
|
||||
</div>
|
||||
) : (
|
||||
<Avatar className={styles.avatar}>{firstLetter(template.name)}</Avatar>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<PageHeaderTitle>{template.name}</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
{template.description === "" ? Language.noDescription : template.description}
|
||||
</PageHeaderSubtitle>
|
||||
</div>
|
||||
</Stack>
|
||||
</PageHeader>
|
||||
|
||||
<Stack spacing={2.5}>
|
||||
{deleteError}
|
||||
<TemplateStats template={template} activeVersion={activeTemplateVersion} />
|
||||
<WorkspaceSection
|
||||
title={Language.resourcesTitle}
|
||||
contentsProps={{ className: styles.resourcesTableContents }}
|
||||
>
|
||||
<TemplateResourcesTable resources={getStartedResources(templateResources)} />
|
||||
</WorkspaceSection>
|
||||
<WorkspaceSection
|
||||
title={Language.readmeTitle}
|
||||
contentsProps={{ className: styles.readmeContents }}
|
||||
>
|
||||
<div className={styles.markdownWrapper}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, target, children }) => (
|
||||
<Link href={href} target={target}>
|
||||
{children}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{readme.body}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</WorkspaceSection>
|
||||
<WorkspaceSection
|
||||
title={Language.versionsTitle}
|
||||
contentsProps={{ className: styles.versionsTableContents }}
|
||||
>
|
||||
<VersionsTable versions={templateVersions} />
|
||||
</WorkspaceSection>
|
||||
</Stack>
|
||||
</>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
export const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
actionButton: {
|
||||
border: "none",
|
||||
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
|
||||
},
|
||||
readmeContents: {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import i18next from "i18next"
|
||||
import { rest } from "msw"
|
||||
import * as api from "../../api/api"
|
||||
import { Workspace } from "../../api/typesGenerated"
|
||||
import { Language } from "../../components/WorkspaceActions/ActionCtas"
|
||||
import { Language } from "../../components/DropdownButton/ActionCtas"
|
||||
import {
|
||||
MockBuilds,
|
||||
MockCanceledWorkspace,
|
||||
|
||||
@@ -40,6 +40,9 @@ export const handlers = [
|
||||
rest.get("/api/v2/templateversions/:templateVersionId/resources", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]))
|
||||
}),
|
||||
rest.delete("/api/v2/templates/:templateId", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockTemplate))
|
||||
}),
|
||||
|
||||
// users
|
||||
rest.get("/api/v2/users", async (req, res, ctx) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ export const checks = {
|
||||
updateUsers: "updateUsers",
|
||||
createUser: "createUser",
|
||||
createTemplates: "createTemplates",
|
||||
deleteTemplates: "deleteTemplates",
|
||||
viewAuditLog: "viewAuditLog",
|
||||
} as const
|
||||
|
||||
@@ -42,6 +43,12 @@ export const permissionsToCheck = {
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
[checks.deleteTemplates]: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
},
|
||||
action: "delete",
|
||||
},
|
||||
[checks.viewAuditLog]: {
|
||||
object: {
|
||||
resource_type: "audit_log",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { displaySuccess } from "components/GlobalSnackbar/utils"
|
||||
import { t } from "i18next"
|
||||
import { assign, createMachine } from "xstate"
|
||||
import {
|
||||
deleteTemplate,
|
||||
getTemplateByName,
|
||||
getTemplateVersion,
|
||||
getTemplateVersionResources,
|
||||
@@ -14,131 +17,208 @@ interface TemplateContext {
|
||||
activeTemplateVersion?: TemplateVersion
|
||||
templateResources?: WorkspaceResource[]
|
||||
templateVersions?: TemplateVersion[]
|
||||
deleteTemplateError?: Error | unknown
|
||||
}
|
||||
|
||||
export const templateMachine = createMachine(
|
||||
{
|
||||
schema: {
|
||||
context: {} as TemplateContext,
|
||||
services: {} as {
|
||||
getTemplate: {
|
||||
data: Template
|
||||
}
|
||||
getActiveTemplateVersion: {
|
||||
data: TemplateVersion
|
||||
}
|
||||
getTemplateResources: {
|
||||
data: WorkspaceResource[]
|
||||
}
|
||||
getTemplateVersions: {
|
||||
data: TemplateVersion[]
|
||||
}
|
||||
type TemplateEvent = { type: "DELETE" } | { type: "CONFIRM_DELETE" } | { type: "CANCEL_DELETE" }
|
||||
|
||||
export const templateMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOhgBdyCoAVMVABwBt1ywBiCAe0JIIDcuAazAk0WPIVIUq+WvWaswCAV0ytcPANoAGALqJQDLrFxUehkAA9EAJgDsOkgGZbO2wE5bANg8BGABZnHR0AgBoQAE9EPx0-Eg9EpIAOe28-D28ggF9siPEcAmI+fDNcdCYASXwAMy4SLCp+MDpGFjYANTAAJ1MeMjBKagBBTCaWhXawLt7NfE4eUVURMQxCqRKyiuq6hrHcZtbFTp6+-AGhuVHxo6mZs5V8QXVzfF0DJBBjU1fLGwQAgF4t4AKzeWzOTIhEEhZIRaIIZI6EEkEIhZyg2xuEF+XL5NaSYoELZVWr1NhtJQAJTgXAArt1MHALrJ5JS2DTYPTGXAFrxlqICoTSMSqNsySQKccwJzuUzYCzqLdqbSGfLHs8NNp9JZvmULJ9-kDkiRks4QR50SDkh5nH5nPCYjpXKi0SDnObbeaAniQEKiiLSmLSbspXdTnMFTIlZMlPdI3ylk9hIKCQHNsGduTYydZjwo4NWcrc2dYBq1Fq3jrPnrfobEAFQqbQt5kiCcf4go6ECD7Ca0XFMskAmksr7-RtReUQ1xEyRYOQlKsJOmp+K6rqTPr8H9ELaTclkg5wQEzRidB5u-Y0q6QgFbAEsuCMuO0xsmFx0BBIOwACIAUQAGX-Gh-03H45ksBFrycGE7zcW1bD8e0In+Pw3G8EggT8M0-Fbc8PGSV8Vw2TAeBqXBulQahfzAJhBg4ABhAB5AA5AAxSoqQAWQAfQA4DQPA7ddwQZCMVRUF7Bw3svXsEFuz8MFMNtC8bRtHQzWHYj1mKMjako6i5Fo+i2HYRjhlYxigP4oCQLAmstzrUA0OcaSSHsZwbSUjwAl83zwiiGJGw8LCwTtU8vGQnI8j9N9im-UzqDnAUSEShjizAYTnOsRB7BHEglLwoqskCWxFJ8ewPJ0bx8tsC1IQhZwdOFNK6MGZKem6LhuhIY46iotrTImdkssciCDRcvcbVRJrwr8fLXHsRTYRIOCau8WqYRxIjfXwLhv3gT4J2KaM5Ey7LIPrAFyqChBLVvEJPGk2w21PFrVyDacsz2G4c2mCN+jOqBrgOEbpXjSavicq6prEhar1tR7IXSaSfVik7AxJH7GjBzLIfOWA6UweUjqMGGof+TzbEKsEMiRdwYUhbtkhw5HnHvXx0g8D7Jy+9d6lxw5-oJy7Kb3B07vsJDkekjwcSyWxeaJfmZ0lf7ZTVZlgcyzWeTJ6GJp3a7kOWu7YjcR6wQCEFAQfbxlaxzMJTDFUuS1hUiZJuADdrWHcoQVtMJxdtWfvaL0hWm2rfcRXHxBR2M2+l2NdVfWxeNuHbW7Xz+xCWJkm8ZE3LbRO1zV12S0jRVzpFwH8F9inM4D03u2Ux6sWvQj2wTjH4qd5PQzrvMG-nYnSYz0TQRNG3gnUyE+zNNv-EevDrUya9mr7kiVexlPRoJxujdE7O7r8gIO+dXtUkyMvVazSfrt8bt7xpgcFttC1nXsROPy-SBH5w1iO6MKwRghAkbH2BSUtHBrTRPeC8rhxKJ30hRKiNF2psEAS3ZCNMkR4R8MOWqfloEIntCvDmnoRzyUIvLRO6VWTYP+F4TCNt0g22REhV6pCYgEJIPVDENsPBpA9GkehmCAHjREtdVmmFPCKy2u6RwcJzaQicMOb0wQ3ALVxNvXSRAmExBUWQvOA5baqXlrbXIuQgA */
|
||||
createMachine(
|
||||
{
|
||||
tsTypes: {} as import("./templateXService.typegen").Typegen0,
|
||||
schema: {
|
||||
context: {} as TemplateContext,
|
||||
events: {} as TemplateEvent,
|
||||
services: {} as {
|
||||
getTemplate: {
|
||||
data: Template
|
||||
}
|
||||
getActiveTemplateVersion: {
|
||||
data: TemplateVersion
|
||||
}
|
||||
getTemplateResources: {
|
||||
data: WorkspaceResource[]
|
||||
}
|
||||
getTemplateVersions: {
|
||||
data: TemplateVersion[]
|
||||
}
|
||||
deleteTemplate: {
|
||||
data: Template
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import("./templateXService.typegen").Typegen0,
|
||||
initial: "gettingTemplate",
|
||||
states: {
|
||||
gettingTemplate: {
|
||||
invoke: {
|
||||
src: "getTemplate",
|
||||
id: "(machine)",
|
||||
initial: "gettingTemplate",
|
||||
states: {
|
||||
gettingTemplate: {
|
||||
invoke: {
|
||||
src: "getTemplate",
|
||||
onDone: [
|
||||
{
|
||||
actions: "assignTemplate",
|
||||
target: "initialInfo",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
initialInfo: {
|
||||
type: "parallel",
|
||||
states: {
|
||||
activeTemplateVersion: {
|
||||
initial: "gettingActiveTemplateVersion",
|
||||
states: {
|
||||
gettingActiveTemplateVersion: {
|
||||
invoke: {
|
||||
src: "getActiveTemplateVersion",
|
||||
onDone: [
|
||||
{
|
||||
actions: "assignActiveTemplateVersion",
|
||||
target: "success",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
success: {
|
||||
type: "final",
|
||||
},
|
||||
},
|
||||
},
|
||||
templateResources: {
|
||||
initial: "gettingTemplateResources",
|
||||
states: {
|
||||
gettingTemplateResources: {
|
||||
invoke: {
|
||||
src: "getTemplateResources",
|
||||
onDone: [
|
||||
{
|
||||
actions: "assignTemplateResources",
|
||||
target: "success",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
success: {
|
||||
type: "final",
|
||||
},
|
||||
},
|
||||
},
|
||||
templateVersions: {
|
||||
initial: "gettingTemplateVersions",
|
||||
states: {
|
||||
gettingTemplateVersions: {
|
||||
invoke: {
|
||||
src: "getTemplateVersions",
|
||||
onDone: [
|
||||
{
|
||||
actions: "assignTemplateVersions",
|
||||
target: "success",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
success: {
|
||||
type: "final",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
onDone: {
|
||||
actions: ["assignTemplate"],
|
||||
target: "initialInfo",
|
||||
target: "loaded",
|
||||
},
|
||||
},
|
||||
},
|
||||
initialInfo: {
|
||||
type: "parallel",
|
||||
onDone: "loaded",
|
||||
states: {
|
||||
activeTemplateVersion: {
|
||||
initial: "gettingActiveTemplateVersion",
|
||||
states: {
|
||||
gettingActiveTemplateVersion: {
|
||||
invoke: {
|
||||
src: "getActiveTemplateVersion",
|
||||
onDone: {
|
||||
actions: ["assignActiveTemplateVersion"],
|
||||
target: "success",
|
||||
},
|
||||
},
|
||||
},
|
||||
success: { type: "final" },
|
||||
},
|
||||
},
|
||||
templateResources: {
|
||||
initial: "gettingTemplateResources",
|
||||
states: {
|
||||
gettingTemplateResources: {
|
||||
invoke: {
|
||||
src: "getTemplateResources",
|
||||
onDone: {
|
||||
actions: ["assignTemplateResources"],
|
||||
target: "success",
|
||||
},
|
||||
},
|
||||
},
|
||||
success: { type: "final" },
|
||||
},
|
||||
},
|
||||
templateVersions: {
|
||||
initial: "gettingTemplateVersions",
|
||||
states: {
|
||||
gettingTemplateVersions: {
|
||||
invoke: {
|
||||
src: "getTemplateVersions",
|
||||
onDone: {
|
||||
actions: ["assignTemplateVersions"],
|
||||
target: "success",
|
||||
},
|
||||
},
|
||||
},
|
||||
success: { type: "final" },
|
||||
loaded: {
|
||||
on: {
|
||||
DELETE: {
|
||||
target: "confirmingDelete",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
loaded: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
services: {
|
||||
getTemplate: (ctx) => getTemplateByName(ctx.organizationId, ctx.templateName),
|
||||
getActiveTemplateVersion: (ctx) => {
|
||||
if (!ctx.template) {
|
||||
throw new Error("Template not loaded")
|
||||
}
|
||||
|
||||
return getTemplateVersion(ctx.template.active_version_id)
|
||||
},
|
||||
getTemplateResources: (ctx) => {
|
||||
if (!ctx.template) {
|
||||
throw new Error("Template not loaded")
|
||||
}
|
||||
|
||||
return getTemplateVersionResources(ctx.template.active_version_id)
|
||||
},
|
||||
getTemplateVersions: (ctx) => {
|
||||
if (!ctx.template) {
|
||||
throw new Error("Template not loaded")
|
||||
}
|
||||
|
||||
return getTemplateVersions(ctx.template.id)
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
assignTemplate: assign({
|
||||
template: (_, event) => event.data,
|
||||
}),
|
||||
assignActiveTemplateVersion: assign({
|
||||
activeTemplateVersion: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateResources: assign({
|
||||
templateResources: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateVersions: assign({
|
||||
templateVersions: (_, event) => event.data,
|
||||
}),
|
||||
{
|
||||
services: {
|
||||
getTemplate: (ctx) => getTemplateByName(ctx.organizationId, ctx.templateName),
|
||||
getActiveTemplateVersion: (ctx) => {
|
||||
if (!ctx.template) {
|
||||
throw new Error("Template not loaded")
|
||||
}
|
||||
|
||||
return getTemplateVersion(ctx.template.active_version_id)
|
||||
},
|
||||
getTemplateResources: (ctx) => {
|
||||
if (!ctx.template) {
|
||||
throw new Error("Template not loaded")
|
||||
}
|
||||
|
||||
return getTemplateVersionResources(ctx.template.active_version_id)
|
||||
},
|
||||
getTemplateVersions: (ctx) => {
|
||||
if (!ctx.template) {
|
||||
throw new Error("Template not loaded")
|
||||
}
|
||||
|
||||
return getTemplateVersions(ctx.template.id)
|
||||
},
|
||||
deleteTemplate: (ctx) => {
|
||||
if (!ctx.template) {
|
||||
throw new Error("Template not loaded")
|
||||
}
|
||||
return deleteTemplate(ctx.template.id)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
assignTemplate: assign({
|
||||
template: (_, event) => event.data,
|
||||
}),
|
||||
assignActiveTemplateVersion: assign({
|
||||
activeTemplateVersion: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateResources: assign({
|
||||
templateResources: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateVersions: assign({
|
||||
templateVersions: (_, event) => event.data,
|
||||
}),
|
||||
assignDeleteTemplateError: assign({
|
||||
deleteTemplateError: (_, event) => event.data,
|
||||
}),
|
||||
clearDeleteTemplateError: assign({
|
||||
deleteTemplateError: (_) => undefined,
|
||||
}),
|
||||
displayDeleteSuccess: () => displaySuccess(t("deleteSuccess", { ns: "templatePage" })),
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user