Skip to content

Commit a866537

Browse files
3mcdtefkah
andauthored
feat: begin refactoring action forms out (#1387)
Co-authored-by: Thomas F. K. Jorna <[email protected]>
1 parent e5ed0cf commit a866537

File tree

57 files changed

+1403
-1121
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1403
-1121
lines changed

core/actions/_lib/ActionField.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { PropsWithChildren } from "react";
2+
import type { ControllerProps } from "react-hook-form";
3+
import type z from "zod";
4+
5+
import { Controller } from "react-hook-form";
6+
7+
import { Field, FieldDescription, FieldError, FieldLabel } from "ui/field";
8+
import { Input } from "ui/input";
9+
import { PubFieldSelector, PubFieldSelectorHider, PubFieldSelectorProvider } from "ui/pubFields";
10+
11+
import { PubFieldSelectToggleButton } from "../../../packages/ui/src/pubFields/pubFieldSelect";
12+
import { useActionForm } from "./ActionForm";
13+
14+
type ActionFieldProps = PropsWithChildren<{
15+
name: string;
16+
label?: string;
17+
render?: ControllerProps<any>["render"];
18+
id?: string;
19+
}>;
20+
21+
export function ActionField(props: ActionFieldProps) {
22+
const { form, schema, defaultFields } = useActionForm();
23+
const fieldSchema = schema._def.innerType.shape[props.name] as z.ZodType<any>;
24+
const required = !fieldSchema.isOptional();
25+
const isDefaultField = defaultFields.includes(props.name);
26+
27+
return (
28+
<Controller
29+
name={props.name}
30+
control={form.control}
31+
render={(p) => (
32+
<PubFieldSelectorProvider field={p.field} allowedSchemas={[]} zodItem={fieldSchema}>
33+
<Field data-invalid={p.fieldState.invalid}>
34+
<div className="flex flex-row items-center justify-between space-x-2">
35+
{props.label && (
36+
<FieldLabel
37+
htmlFor={p.field.name}
38+
aria-required={required}
39+
id={props.id}
40+
>
41+
{props.label}
42+
{required && <span className="-ml-1 text-red-500">*</span>}
43+
</FieldLabel>
44+
)}
45+
<PubFieldSelectToggleButton />
46+
</div>
47+
{props.render?.(p) ?? (
48+
<Input
49+
type="text"
50+
className="bg-white"
51+
placeholder={isDefaultField ? "(use default)" : undefined}
52+
{...p.field}
53+
id={p.field.name}
54+
value={p.field.value}
55+
aria-invalid={p.fieldState.invalid}
56+
/>
57+
)}
58+
<FieldDescription>{fieldSchema.description}</FieldDescription>
59+
{p.fieldState.invalid && <FieldError errors={[p.fieldState.error]} />}
60+
</Field>
61+
<PubFieldSelectorHider>
62+
<PubFieldSelector />
63+
</PubFieldSelectorHider>
64+
</PubFieldSelectorProvider>
65+
)}
66+
/>
67+
);
68+
}

core/actions/_lib/ActionForm.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { PropsWithChildren } from "react";
2+
import type { FieldValues, UseFormReturn } from "react-hook-form";
3+
import type { ZodObject, ZodOptional } from "zod";
4+
5+
import { createContext, useCallback, useContext, useMemo } from "react";
6+
import { zodResolver } from "@hookform/resolvers/zod";
7+
import { useForm } from "react-hook-form";
8+
import { z } from "zod";
9+
10+
import { Button } from "ui/button";
11+
import { Field, FieldGroup } from "ui/field";
12+
import { Form } from "ui/form";
13+
import { FormSubmitButton } from "ui/submit-button";
14+
15+
import type { Action } from "../types";
16+
import { getDefaultValues } from "../../lib/zod";
17+
18+
export type ActionFormValues = FieldValues & {
19+
pubFields: Record<string, string[]>;
20+
};
21+
22+
type ActionFormContext = {
23+
action: Action;
24+
schema: ZodOptional<ZodObject<any>>;
25+
form: UseFormReturn<ActionFormValues>;
26+
defaultFields: string[];
27+
};
28+
29+
const ActionFormContext = createContext<ActionFormContext | undefined>(undefined);
30+
31+
type ActionFormProps = PropsWithChildren<{
32+
onSubmit(values: Record<string, unknown>, form: UseFormReturn<ActionFormValues>): Promise<void>;
33+
action: Action;
34+
defaultFields: string[];
35+
values: Record<string, unknown> | null;
36+
submitButton: {
37+
text: string;
38+
pendingText?: string;
39+
successText?: string;
40+
errorText?: string;
41+
className?: string;
42+
};
43+
secondaryButton?: {
44+
text?: string;
45+
className?: string;
46+
onClick: () => void;
47+
};
48+
}>;
49+
50+
export function ActionForm(props: ActionFormProps) {
51+
const form = useForm({
52+
resolver: zodResolver(props.action.config.schema),
53+
defaultValues: {
54+
...getDefaultValues(props.action.config.schema),
55+
...props.values,
56+
pubFields: {},
57+
},
58+
});
59+
60+
const schema = useMemo(() => {
61+
const schemaWithPartialDefaults = (props.action.config.schema as ZodObject<any>)
62+
.partial(
63+
props.defaultFields.reduce(
64+
(acc, key) => {
65+
acc[key] = true;
66+
return acc;
67+
},
68+
{} as Record<string, true>
69+
)
70+
)
71+
.extend({
72+
pubFields: z
73+
.record(z.string(), z.string().array())
74+
.optional()
75+
.describe("Mapping of pub fields to values"),
76+
})
77+
.optional();
78+
return schemaWithPartialDefaults;
79+
}, [props.action.config.schema, props.defaultFields]);
80+
81+
const onSubmit = useCallback(
82+
async (data: Record<string, unknown>) => {
83+
await props.onSubmit(data, form);
84+
},
85+
[props.onSubmit, form]
86+
);
87+
88+
return (
89+
<ActionFormContext.Provider
90+
value={{ action: props.action, schema, form, defaultFields: props.defaultFields }}
91+
>
92+
<Form {...form}>
93+
<form onSubmit={form.handleSubmit(onSubmit)}>
94+
<FieldGroup>
95+
{props.children}
96+
<Field orientation="horizontal" className="flex justify-end">
97+
{props.secondaryButton && (
98+
<Button
99+
variant="outline"
100+
type="button"
101+
className={props.secondaryButton?.className}
102+
onClick={props.secondaryButton.onClick}
103+
>
104+
{props.secondaryButton?.text}
105+
</Button>
106+
)}
107+
108+
<FormSubmitButton
109+
data-testid="action-run-button"
110+
formState={form.formState}
111+
className={props.submitButton.className}
112+
idleText={props.submitButton.text}
113+
pendingText={props.submitButton.pendingText}
114+
successText={props.submitButton.successText}
115+
errorText={props.submitButton.errorText}
116+
/>
117+
</Field>
118+
</FieldGroup>
119+
</form>
120+
</Form>
121+
</ActionFormContext.Provider>
122+
);
123+
}
124+
125+
export function useActionForm() {
126+
return useContext(ActionFormContext)!;
127+
}

core/actions/_lib/runActionInstance.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,9 @@ const _runActionInstance = async (
173173
inputPubInput.values,
174174
argsFieldOverrides
175175
);
176+
// @ts-expect-error FIXME: will be diff
176177
config = resolveWithPubfields(
177-
{ ...(args.actionInstance.config as {}), ...parsedConfig.data },
178+
{ ...args.actionInstance.config, ...parsedConfig.data },
178179
inputPubInput.values,
179180
configFieldOverrides
180181
);

core/actions/_lib/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { UseFormReturn } from "react-hook-form";
2+
3+
export type ActionFormProps = {
4+
values: Record<string, unknown> | null;
5+
onSubmit: (values: Record<string, unknown>, form: UseFormReturn<any>) => void;
6+
};

core/actions/_lib/useActionForm.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { zodResolver } from "@hookform/resolvers/zod";
2+
import { useForm } from "react-hook-form";
3+
4+
import type { Action } from "../types";
5+
6+
export const useActionForm = (action: Action) => {
7+
const form = useForm({
8+
resolver: zodResolver(action.config.schema),
9+
});
10+
return form;
11+
};

core/actions/api/index.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// shared actions between server and client
22

3-
import type { Rule } from "ajv/dist/compile/rules";
43
import type * as z from "zod";
54

65
import type { ActionInstances, Communities, Event, Rules } from "db/public";
@@ -21,15 +20,11 @@ import * as googleDriveImport from "../googleDriveImport/action";
2120
import * as http from "../http/action";
2221
import * as log from "../log/action";
2322
import * as move from "../move/action";
24-
import * as pdf from "../pdf/action";
25-
import * as pushToV6 from "../pushToV6/action";
26-
import { isSequentialRuleEvent, sequentialRuleEvents } from "../types";
23+
import { sequentialRuleEvents } from "../types";
2724

2825
export const actions = {
2926
[log.action.name]: log.action,
30-
[pdf.action.name]: pdf.action,
3127
[email.action.name]: email.action,
32-
[pushToV6.action.name]: pushToV6.action,
3328
[http.action.name]: http.action,
3429
[move.action.name]: move.action,
3530
[googleDriveImport.action.name]: googleDriveImport.action,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { FieldSet } from "ui/field";
2+
3+
import { ActionField } from "../_lib/ActionField";
4+
5+
export default function BuildJournalSiteActionForm() {
6+
return (
7+
<FieldSet>
8+
<ActionField name="siteUrl" label="Site URL" />
9+
</FieldSet>
10+
);
11+
}

core/actions/datacite/action.ts

Lines changed: 8 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,13 @@ export const action = defineAction({
1616
title: z.string(),
1717
url: z.string(),
1818
publisher: z.string(),
19-
publicationDate: z.coerce.date(),
20-
contributor: z.string(),
21-
contributorPerson: z.string(),
22-
contributorPersonName: z.string(),
19+
publicationDate: z.coerce.date().default(new Date()),
20+
contributor: z.string().default(""),
21+
contributorPerson: z.string().default(""),
22+
contributorPersonName: z.string().default(""),
2323
contributorPersonORCID: z.string().optional(),
2424
bylineContributorFlag: z.boolean().optional(),
2525
}),
26-
fieldConfig: {
27-
doi: {
28-
allowedSchemas: true,
29-
},
30-
doiSuffix: {
31-
allowedSchemas: true,
32-
},
33-
title: {
34-
allowedSchemas: true,
35-
},
36-
url: {
37-
allowedSchemas: true,
38-
},
39-
publisher: {
40-
allowedSchemas: true,
41-
},
42-
publicationDate: {
43-
allowedSchemas: true,
44-
},
45-
contributor: {
46-
allowedSchemas: true,
47-
},
48-
contributorPerson: {
49-
allowedSchemas: true,
50-
},
51-
contributorPersonName: {
52-
allowedSchemas: true,
53-
},
54-
contributorPersonORCID: {
55-
allowedSchemas: true,
56-
},
57-
bylineContributorFlag: {
58-
allowedSchemas: true,
59-
},
60-
},
6126
},
6227
params: {
6328
schema: z.object({
@@ -67,48 +32,13 @@ export const action = defineAction({
6732
title: z.string(),
6833
url: z.string(),
6934
publisher: z.string(),
70-
publicationDate: z.coerce.date(),
71-
contributor: z.string(),
72-
contributorPerson: z.string(),
73-
contributorPersonName: z.string(),
35+
publicationDate: z.coerce.date().default(new Date()),
36+
contributor: z.string().default(""),
37+
contributorPerson: z.string().default(""),
38+
contributorPersonName: z.string().default(""),
7439
contributorPersonORCID: z.string().optional(),
7540
bylineContributorFlag: z.boolean().optional(),
7641
}),
77-
fieldConfig: {
78-
doi: {
79-
allowedSchemas: true,
80-
},
81-
doiSuffix: {
82-
allowedSchemas: true,
83-
},
84-
title: {
85-
allowedSchemas: true,
86-
},
87-
url: {
88-
allowedSchemas: true,
89-
},
90-
publisher: {
91-
allowedSchemas: true,
92-
},
93-
publicationDate: {
94-
allowedSchemas: true,
95-
},
96-
contributor: {
97-
allowedSchemas: true,
98-
},
99-
contributorPerson: {
100-
allowedSchemas: true,
101-
},
102-
contributorPersonName: {
103-
allowedSchemas: true,
104-
},
105-
contributorPersonORCID: {
106-
allowedSchemas: true,
107-
},
108-
bylineContributorFlag: {
109-
allowedSchemas: true,
110-
},
111-
},
11242
},
11343
description: "Deposit a pub to DataCite",
11444
icon: Globe,

0 commit comments

Comments
 (0)