refactor: Update create workspace flow to allow creation from the workspaces page (#1684)

This commit is contained in:
Bruno Quaresma
2022-05-24 08:37:44 -05:00
committed by GitHub
parent 5f8d0e5dad
commit fcd610ee7b
11 changed files with 368 additions and 262 deletions

View File

@ -15,6 +15,7 @@
"drpcserver", "drpcserver",
"Dsts", "Dsts",
"fatih", "fatih",
"Formik",
"goarch", "goarch",
"gographviz", "gographviz",
"goleak", "goleak",
@ -22,6 +23,7 @@
"gsyslog", "gsyslog",
"hashicorp", "hashicorp",
"hclsyntax", "hclsyntax",
"httpapi",
"httpmw", "httpmw",
"idtoken", "idtoken",
"Iflag", "Iflag",
@ -63,6 +65,7 @@
"tfjson", "tfjson",
"tfstate", "tfstate",
"trimprefix", "trimprefix",
"typegen",
"unconvert", "unconvert",
"Untar", "Untar",
"VMID", "VMID",

View File

@ -56,6 +56,16 @@ export const AppRouter: React.FC = () => (
</AuthAndFrame> </AuthAndFrame>
} }
/> />
<Route
path="new"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
<Route path=":workspace"> <Route path=":workspace">
<Route <Route
index index
@ -85,17 +95,6 @@ export const AppRouter: React.FC = () => (
</AuthAndFrame> </AuthAndFrame>
} }
/> />
<Route path=":template">
<Route
path="new"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
</Route>
</Route> </Route>
<Route path="users"> <Route path="users">

View File

@ -35,7 +35,7 @@ export const FormFooter: React.FC<FormFooterProps> = ({
const styles = useStyles() const styles = useStyles()
return ( return (
<div className={styles.footer}> <div className={styles.footer}>
<Button className={styles.button} onClick={onCancel} variant="outlined"> <Button type="button" className={styles.button} onClick={onCancel} variant="outlined">
{Language.cancelLabel} {Language.cancelLabel}
</Button> </Button>
<LoadingButton loading={isLoading} className={styles.button} variant="contained" color="primary" type="submit"> <LoadingButton loading={isLoading} className={styles.button} variant="contained" color="primary" type="submit">

View File

@ -5,10 +5,17 @@ import { reach, StringSchema } from "yup"
import * as API from "../../api/api" import * as API from "../../api/api"
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter" import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities" import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
import { history, render } from "../../testHelpers/renderHelpers" import { renderWithAuth } from "../../testHelpers/renderHelpers"
import CreateWorkspacePage from "./CreateWorkspacePage" import CreateWorkspacePage from "./CreateWorkspacePage"
import { Language, validationSchema } from "./CreateWorkspacePageView" import { Language, validationSchema } from "./CreateWorkspacePageView"
const renderCreateWorkspacePage = () => {
return renderWithAuth(<CreateWorkspacePage />, {
route: "/workspaces/new?template=" + MockTemplate.name,
path: "/workspaces/new",
})
}
const fillForm = async ({ name = "example" }: { name?: string }) => { const fillForm = async ({ name = "example" }: { name?: string }) => {
const nameField = await screen.findByLabelText(Language.nameLabel) const nameField = await screen.findByLabelText(Language.nameLabel)
await userEvent.type(nameField, name) await userEvent.type(nameField, name)
@ -19,25 +26,21 @@ const fillForm = async ({ name = "example" }: { name?: string }) => {
const nameSchema = reach(validationSchema, "name") as StringSchema const nameSchema = reach(validationSchema, "name") as StringSchema
describe("CreateWorkspacePage", () => { describe("CreateWorkspacePage", () => {
beforeEach(() => {
history.replace("/templates/" + MockTemplate.name + "/new")
})
it("renders", async () => { it("renders", async () => {
render(<CreateWorkspacePage />) renderCreateWorkspacePage()
const element = await screen.findByText("Create workspace") const element = await screen.findByText("Create workspace")
expect(element).toBeDefined() expect(element).toBeDefined()
}) })
it("shows validation error message", async () => { it("shows validation error message", async () => {
render(<CreateWorkspacePage />) renderCreateWorkspacePage()
await fillForm({ name: "$$$" }) await fillForm({ name: "$$$" })
const errorMessage = await screen.findByText(Language.nameMatches) const errorMessage = await screen.findByText(Language.nameMatches)
expect(errorMessage).toBeDefined() expect(errorMessage).toBeDefined()
}) })
it("succeeds", async () => { it("succeeds", async () => {
render(<CreateWorkspacePage />) renderCreateWorkspacePage()
// You have to spy the method before it is used. // You have to spy the method before it is used.
jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace) jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace)
await fillForm({ name: "test" }) await fillForm({ name: "test" })

View File

@ -1,36 +1,59 @@
import { useMachine } from "@xstate/react" import { useActor, useMachine } from "@xstate/react"
import React from "react" import React, { useContext } from "react"
import { useNavigate } from "react-router" import { useNavigate, useSearchParams } from "react-router-dom"
import { useParams } from "react-router-dom" import { Template } from "../../api/typesGenerated"
import { createWorkspace } from "../../api/api" import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
import { templateMachine } from "../../xServices/template/templateXService" import { XServiceContext } from "../../xServices/StateContext"
import { CreateWorkspacePageView } from "./CreateWorkspacePageView" import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
const useOrganizationId = () => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const organizationId = authState.context.me?.organization_ids[0]
if (!organizationId) {
throw new Error("No organization ID found")
}
return organizationId
}
const CreateWorkspacePage: React.FC = () => { const CreateWorkspacePage: React.FC = () => {
const { template } = useParams() const organizationId = useOrganizationId()
const [templateState] = useMachine(templateMachine, { const [searchParams] = useSearchParams()
context: { const preSelectedTemplateName = searchParams.get("template")
name: template, const navigate = useNavigate()
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
context: { organizationId, preSelectedTemplateName },
actions: {
onCreateWorkspace: (_, event) => {
navigate("/workspaces/" + event.data.id)
},
}, },
}) })
const navigate = useNavigate()
const loading = templateState.hasTag("loading")
if (!templateState.context.template || !templateState.context.templateSchema) {
return null
}
return ( return (
<CreateWorkspacePageView <CreateWorkspacePageView
template={templateState.context.template} loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
templateSchema={templateState.context.templateSchema} loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
loading={loading} creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
onCancel={() => navigate("/templates")} templates={createWorkspaceState.context.templates}
onSubmit={async (req) => { selectedTemplate={createWorkspaceState.context.selectedTemplate}
if (!templateState.context.template) { templateSchema={createWorkspaceState.context.templateSchema}
throw new Error("template isn't valid") onCancel={() => {
} navigate(preSelectedTemplateName ? "/templates" : "/workspaces")
const workspace = await createWorkspace(templateState.context.template.organization_id, req) }}
navigate("/workspaces/" + workspace.id) onSubmit={(request) => {
send({
type: "CREATE_WORKSPACE",
request,
})
}}
onSelectTemplate={(template: Template) => {
send({
type: "SELECT_TEMPLATE",
template,
})
}} }}
/> />
) )

View File

@ -1,9 +1,32 @@
import { ComponentMeta, Story } from "@storybook/react" import { ComponentMeta, Story } from "@storybook/react"
import React from "react" import React from "react"
import { createParameterSchema } from "../../components/ParameterInput/ParameterInput.stories" import { ParameterSchema } from "../../api/typesGenerated"
import { MockTemplate } from "../../testHelpers/entities" import { MockTemplate } from "../../testHelpers/entities"
import { CreateWorkspacePageView, CreateWorkspacePageViewProps } from "./CreateWorkspacePageView" import { CreateWorkspacePageView, CreateWorkspacePageViewProps } from "./CreateWorkspacePageView"
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
return {
id: "000000",
job_id: "000000",
allow_override_destination: false,
allow_override_source: true,
created_at: "",
default_destination_scheme: "none",
default_refresh: "",
default_source_scheme: "data",
default_source_value: "default-value",
name: "parameter name",
description: "Some description!",
redisplay_value: false,
validation_condition: "",
validation_contains: [],
validation_error: "",
validation_type_system: "",
validation_value_type: "",
...partial,
}
}
export default { export default {
title: "pages/CreateWorkspacePageView", title: "pages/CreateWorkspacePageView",
component: CreateWorkspacePageView, component: CreateWorkspacePageView,
@ -13,13 +36,15 @@ const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspace
export const NoParameters = Template.bind({}) export const NoParameters = Template.bind({})
NoParameters.args = { NoParameters.args = {
template: MockTemplate, templates: [MockTemplate],
selectedTemplate: MockTemplate,
templateSchema: [], templateSchema: [],
} }
export const Parameters = Template.bind({}) export const Parameters = Template.bind({})
Parameters.args = { Parameters.args = {
template: MockTemplate, templates: [MockTemplate],
selectedTemplate: MockTemplate,
templateSchema: [ templateSchema: [
createParameterSchema({ createParameterSchema({
name: "region", name: "region",

View File

@ -1,16 +1,19 @@
import { makeStyles } from "@material-ui/core/styles" import MenuItem from "@material-ui/core/MenuItem"
import TextField from "@material-ui/core/TextField" import TextField, { TextFieldProps } from "@material-ui/core/TextField"
import { FormikContextType, useFormik } from "formik" import { FormikContextType, useFormik } from "formik"
import React from "react" import React from "react"
import * as Yup from "yup" import * as Yup from "yup"
import * as TypesGen from "../../api/typesGenerated" import * as TypesGen from "../../api/typesGenerated"
import { FormFooter } from "../../components/FormFooter/FormFooter" import { FormFooter } from "../../components/FormFooter/FormFooter"
import { FullPageForm } from "../../components/FullPageForm/FullPageForm" import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
import { Loader } from "../../components/Loader/Loader"
import { Margins } from "../../components/Margins/Margins" import { Margins } from "../../components/Margins/Margins"
import { ParameterInput } from "../../components/ParameterInput/ParameterInput" import { ParameterInput } from "../../components/ParameterInput/ParameterInput"
import { Stack } from "../../components/Stack/Stack"
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
export const Language = { export const Language = {
templateLabel: "Template",
nameLabel: "Name", nameLabel: "Name",
nameRequired: "Please enter a name.", nameRequired: "Please enter a name.",
nameMatches: "Name must start with a-Z or 0-9 and can contain a-Z, 0-9 or -", nameMatches: "Name must start with a-Z or 0-9 and can contain a-Z, 0-9 or -",
@ -24,12 +27,15 @@ const maxLenName = 32
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/ const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
export interface CreateWorkspacePageViewProps { export interface CreateWorkspacePageViewProps {
loading?: boolean loadingTemplates: boolean
template: TypesGen.Template loadingTemplateSchema: boolean
templateSchema: TypesGen.ParameterSchema[] creatingWorkspace: boolean
templates?: TypesGen.Template[]
selectedTemplate?: TypesGen.Template
templateSchema?: TypesGen.ParameterSchema[]
onCancel: () => void onCancel: () => void
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => Promise<void> onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
onSelectTemplate: (template: TypesGen.Template) => void
} }
export const validationSchema = Yup.object({ export const validationSchema = Yup.object({
@ -40,15 +46,19 @@ export const validationSchema = Yup.object({
}) })
export const CreateWorkspacePageView: React.FC<CreateWorkspacePageViewProps> = (props) => { export const CreateWorkspacePageView: React.FC<CreateWorkspacePageViewProps> = (props) => {
const styles = useStyles()
const [parameterValues, setParameterValues] = React.useState<Record<string, string>>({}) const [parameterValues, setParameterValues] = React.useState<Record<string, string>>({})
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({ const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({
initialValues: { initialValues: {
name: "", name: "",
template_id: props.template.id, template_id: props.selectedTemplate ? props.selectedTemplate.id : "",
}, },
enableReinitialize: true,
validationSchema, validationSchema,
onSubmit: (request) => { onSubmit: (request) => {
if (!props.templateSchema) {
throw new Error("No template schema loaded")
}
const createRequests: TypesGen.CreateParameterRequest[] = [] const createRequests: TypesGen.CreateParameterRequest[] = []
props.templateSchema.forEach((schema) => { props.templateSchema.forEach((schema) => {
let value = schema.default_source_value let value = schema.default_source_value
@ -70,49 +80,84 @@ export const CreateWorkspacePageView: React.FC<CreateWorkspacePageViewProps> = (
}) })
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form) const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)
const handleTemplateChange: TextFieldProps["onChange"] = (event) => {
if (!props.templates) {
throw new Error("Templates are not loaded")
}
const templateId = event.target.value
const selectedTemplate = props.templates.find((template) => template.id === templateId)
if (!selectedTemplate) {
throw new Error(`Template ${templateId} not found`)
}
form.setFieldValue("template_id", selectedTemplate.id)
props.onSelectTemplate(selectedTemplate)
}
return ( return (
<Margins> <Margins>
<FullPageForm title="Create workspace" onCancel={props.onCancel}> <FullPageForm title="Create workspace" onCancel={props.onCancel}>
<form onSubmit={form.handleSubmit}> <form onSubmit={form.handleSubmit}>
<TextField {props.loadingTemplates && <Loader />}
{...getFieldHelpers("name")}
disabled={form.isSubmitting}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label={Language.nameLabel}
variant="outlined"
/>
{props.templateSchema.length > 0 && (
<div className={styles.parameters}>
{props.templateSchema.map((schema) => (
<ParameterInput
disabled={form.isSubmitting}
key={schema.id}
onChange={(value) => {
setParameterValues({
...parameterValues,
[schema.name]: value,
})
}}
schema={schema}
/>
))}
</div>
)}
<FormFooter onCancel={props.onCancel} isLoading={props.loading || form.isSubmitting} /> <Stack>
{props.templates && (
<TextField
{...getFieldHelpers("template_id")}
disabled={form.isSubmitting}
onChange={handleTemplateChange}
autoFocus
fullWidth
label={Language.templateLabel}
variant="outlined"
select
>
{props.templates.map((template) => (
<MenuItem key={template.id} value={template.id}>
{template.name}
</MenuItem>
))}
</TextField>
)}
{props.selectedTemplate && props.templateSchema && (
<>
<TextField
{...getFieldHelpers("name")}
disabled={form.isSubmitting}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label={Language.nameLabel}
variant="outlined"
/>
{props.templateSchema.length > 0 && (
<Stack>
{props.templateSchema.map((schema) => (
<ParameterInput
disabled={form.isSubmitting}
key={schema.id}
onChange={(value) => {
setParameterValues({
...parameterValues,
[schema.name]: value,
})
}}
schema={schema}
/>
))}
</Stack>
)}
<FormFooter onCancel={props.onCancel} isLoading={props.creatingWorkspace} />
</>
)}
</Stack>
</form> </form>
</FullPageForm> </FullPageForm>
</Margins> </Margins>
) )
} }
const useStyles = makeStyles((theme) => ({
parameters: {
paddingTop: theme.spacing(4),
"& > *": {
marginBottom: theme.spacing(4),
},
},
}))

View File

@ -83,7 +83,11 @@ export const TemplatesPageView: React.FC<TemplatesPageViewProps> = (props) => {
<Avatar variant="square" className={styles.templateAvatar}> <Avatar variant="square" className={styles.templateAvatar}>
{firstLetter(template.name)} {firstLetter(template.name)}
</Avatar> </Avatar>
<Link component={RouterLink} to={`/templates/${template.name}/new`} className={styles.templateLink}> <Link
component={RouterLink}
to={`/workspaces/new?template=${template.name}`}
className={styles.templateLink}
>
<b>{template.name}</b> <b>{template.name}</b>
<span>{template.description}</span> <span>{template.description}</span>
</Link> </Link>

View File

@ -39,7 +39,7 @@ export const WorkspacesPageView: React.FC<WorkspacesPageViewProps> = (props) =>
<Stack spacing={4}> <Stack spacing={4}>
<Margins> <Margins>
<div className={styles.actions}> <div className={styles.actions}>
<Link component={RouterLink} to="/templates"> <Link underline="none" component={RouterLink} to="/workspaces/new">
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button> <Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
</Link> </Link>
</div> </div>

View File

@ -0,0 +1,171 @@
import { assign, createMachine } from "xstate"
import { createWorkspace, getTemplates, getTemplateVersionSchema } from "../../api/api"
import { CreateWorkspaceRequest, ParameterSchema, Template, Workspace } from "../../api/typesGenerated"
type CreateWorkspaceContext = {
organizationId: string
templates?: Template[]
selectedTemplate?: Template
templateSchema?: ParameterSchema[]
createWorkspaceRequest?: CreateWorkspaceRequest
createdWorkspace?: Workspace
// This is useful when the user wants to create a workspace from the template
// page having it pre selected. It is string or null because of the
// useSearchQuery
preSelectedTemplateName: string | null
}
type CreateWorkspaceEvent =
| {
type: "SELECT_TEMPLATE"
template: Template
}
| {
type: "CREATE_WORKSPACE"
request: CreateWorkspaceRequest
}
export const createWorkspaceMachine = createMachine(
{
id: "createWorkspaceState",
initial: "gettingTemplates",
schema: {
context: {} as CreateWorkspaceContext,
events: {} as CreateWorkspaceEvent,
services: {} as {
getTemplates: {
data: Template[]
}
getTemplateSchema: {
data: ParameterSchema[]
}
createWorkspace: {
data: Workspace
}
},
},
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
states: {
gettingTemplates: {
invoke: {
src: "getTemplates",
onDone: [
{
actions: ["assignTemplates", "assignPreSelectedTemplate"],
target: "gettingTemplateSchema",
cond: "hasValidPreSelectedTemplate",
},
{
actions: ["assignTemplates"],
target: "selectingTemplate",
},
],
onError: {
target: "error",
},
},
},
selectingTemplate: {
on: {
SELECT_TEMPLATE: {
actions: ["assignSelectedTemplate"],
target: "gettingTemplateSchema",
},
},
},
gettingTemplateSchema: {
invoke: {
src: "getTemplateSchema",
onDone: {
actions: ["assignTemplateSchema"],
target: "fillingParams",
},
onError: {
target: "error",
},
},
},
fillingParams: {
on: {
CREATE_WORKSPACE: {
actions: ["assignCreateWorkspaceRequest"],
target: "creatingWorkspace",
},
},
},
creatingWorkspace: {
invoke: {
src: "createWorkspace",
onDone: {
actions: ["onCreateWorkspace"],
target: "created",
},
onError: {
target: "error",
},
},
},
created: {
type: "final",
},
error: {},
},
},
{
services: {
getTemplates: (context) => getTemplates(context.organizationId),
getTemplateSchema: (context) => {
const { selectedTemplate } = context
if (!selectedTemplate) {
throw new Error("No selected template")
}
return getTemplateVersionSchema(selectedTemplate.active_version_id)
},
createWorkspace: (context) => {
const { createWorkspaceRequest, organizationId } = context
if (!createWorkspaceRequest) {
throw new Error("No create workspace request")
}
return createWorkspace(organizationId, createWorkspaceRequest)
},
},
guards: {
hasValidPreSelectedTemplate: (ctx, event) => {
if (!ctx.preSelectedTemplateName) {
return false
}
const template = event.data.find((template) => template.name === ctx.preSelectedTemplateName)
return !!template
},
},
actions: {
assignTemplates: assign({
templates: (_, event) => event.data,
}),
assignSelectedTemplate: assign({
selectedTemplate: (_, event) => event.template,
}),
assignTemplateSchema: assign({
templateSchema: (_, event) => event.data,
}),
assignCreateWorkspaceRequest: assign({
createWorkspaceRequest: (_, event) => event.request,
}),
assignPreSelectedTemplate: assign({
selectedTemplate: (ctx, event) => {
const selectedTemplate = event.data.find((template) => template.name === ctx.preSelectedTemplateName)
// The proper validation happens on hasValidPreSelectedTemplate
if (!selectedTemplate) {
throw new Error("Invalid template selected")
}
return selectedTemplate
},
}),
},
},
)

View File

@ -1,167 +0,0 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import * as TypesGen from "../../api/typesGenerated"
interface TemplateContext {
name: string
organizations?: TypesGen.Organization[]
organizationsError?: Error | unknown
template?: TypesGen.Template
templateError?: Error | unknown
templateVersion?: TypesGen.TemplateVersion
templateVersionError?: Error | unknown
templateSchema?: TypesGen.ParameterSchema[]
templateSchemaError?: Error | unknown
}
export const templateMachine = createMachine(
{
tsTypes: {} as import("./templateXService.typegen").Typegen0,
schema: {
context: {} as TemplateContext,
services: {} as {
getOrganizations: {
data: TypesGen.Organization[]
}
getTemplate: {
data: TypesGen.Template
}
getTemplateVersion: {
data: TypesGen.TemplateVersion
}
getTemplateSchema: {
data: TypesGen.ParameterSchema[]
}
},
},
id: "templateState",
initial: "gettingOrganizations",
states: {
gettingOrganizations: {
entry: "clearOrganizationsError",
invoke: {
src: "getOrganizations",
id: "getOrganizations",
onDone: [
{
actions: ["assignOrganizations", "clearOrganizationsError"],
target: "gettingTemplate",
},
],
onError: [
{
actions: "assignOrganizationsError",
target: "error",
},
],
},
tags: "loading",
},
gettingTemplate: {
entry: "clearTemplateError",
invoke: {
src: "getTemplate",
id: "getTemplate",
onDone: {
target: "gettingTemplateVersion",
actions: ["assignTemplate", "clearTemplateError"],
},
onError: {
target: "error",
actions: "assignTemplateError",
},
},
tags: "loading",
},
gettingTemplateVersion: {
entry: "clearTemplateVersionError",
invoke: {
src: "getTemplateVersion",
id: "getTemplateVersion",
onDone: {
target: "gettingTemplateSchema",
actions: ["assignTemplateVersion", "clearTemplateVersionError"],
},
onError: {
target: "error",
actions: "assignTemplateVersionError",
},
},
},
gettingTemplateSchema: {
entry: "clearTemplateSchemaError",
invoke: {
src: "getTemplateSchema",
id: "getTemplateSchema",
onDone: {
target: "done",
actions: ["assignTemplateSchema", "clearTemplateSchemaError"],
},
onError: {
target: "error",
actions: "assignTemplateSchemaError",
},
},
},
done: {},
error: {},
},
},
{
actions: {
assignOrganizations: assign({
organizations: (_, event) => event.data,
}),
assignOrganizationsError: assign({
organizationsError: (_, event) => event.data,
}),
clearOrganizationsError: assign((context) => ({
...context,
organizationsError: undefined,
})),
assignTemplate: assign({
template: (_, event) => event.data,
}),
assignTemplateError: assign({
templateError: (_, event) => event.data,
}),
clearTemplateError: (context) => assign({ ...context, templateError: undefined }),
assignTemplateVersion: assign({
templateVersion: (_, event) => event.data,
}),
assignTemplateVersionError: assign({
templateVersionError: (_, event) => event.data,
}),
clearTemplateVersionError: (context) => assign({ ...context, templateVersionError: undefined }),
assignTemplateSchema: assign({
templateSchema: (_, event) => event.data,
}),
assignTemplateSchemaError: assign({
templateSchemaError: (_, event) => event.data,
}),
clearTemplateSchemaError: (context) => assign({ ...context, templateSchemaError: undefined }),
},
services: {
getOrganizations: API.getOrganizations,
getTemplate: async (context) => {
if (!context.organizations || context.organizations.length === 0) {
throw new Error("no organizations")
}
return API.getTemplateByName(context.organizations[0].id, context.name)
},
getTemplateVersion: async (context) => {
if (!context.template) {
throw new Error("no template")
}
return API.getTemplateVersion(context.template.active_version_id)
},
getTemplateSchema: async (context) => {
if (!context.templateVersion) {
throw new Error("no template version")
}
return API.getTemplateVersionSchema(context.templateVersion.id)
},
},
},
)