Align create people style with the update people page

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-18 18:09:59 +01:00
parent db5cefffe0
commit f347e60085
5 changed files with 217 additions and 88 deletions

View File

@@ -22,6 +22,9 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import type { CreatePeoplePageQuery as CreatePeoplePageQueryType } from "./__generated__/CreatePeoplePageQuery.graphql"; import type { CreatePeoplePageQuery as CreatePeoplePageQueryType } from "./__generated__/CreatePeoplePageQuery.graphql";
import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils";
const createPeoplePageQuery = graphql` const createPeoplePageQuery = graphql`
query CreatePeoplePageQuery { query CreatePeoplePageQuery {
@@ -40,11 +43,46 @@ const createPeopleMutation = graphql`
id id
fullName fullName
primaryEmailAddress primaryEmailAddress
additionalEmailAddresses
kind kind
} }
} }
`; `;
function EditableField({
label,
value,
onChange,
type = "text",
helpText,
required,
}: {
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
helpText?: string;
required?: boolean;
}) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">{label}</Label>
</div>
<div className="space-y-2">
<Input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
required={required}
/>
{helpText && <p className="text-sm text-gray-500">{helpText}</p>}
</div>
</div>
);
}
function CreatePeoplePageContent({ function CreatePeoplePageContent({
queryRef, queryRef,
}: { }: {
@@ -53,115 +91,187 @@ function CreatePeoplePageContent({
const navigate = useNavigate(); const navigate = useNavigate();
const environment = useRelayEnvironment(); const environment = useRelayEnvironment();
const data = usePreloadedQuery(createPeoplePageQuery, queryRef); const data = usePreloadedQuery(createPeoplePageQuery, queryRef);
const [createPeople, isCreatingPeople] = useMutation(createPeopleMutation); const [commit] = useMutation(createPeopleMutation);
const [kind, setKind] = useState<"EMPLOYEE" | "CONTRACTOR" | "VENDOR">( const { toast } = useToast();
"EMPLOYEE", const [formData, setFormData] = useState({
); fullName: '',
primaryEmailAddress: '',
additionalEmailAddresses: [] as string[],
kind: 'EMPLOYEE' as 'EMPLOYEE' | 'CONTRACTOR',
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleFieldChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({
...prev,
[field]: value,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.currentTarget); commit({
createPeople({
variables: { variables: {
input: { input: {
organizationId: data.node.id, organizationId: data.node.id,
fullName: formData.get("fullName") as string, fullName: formData.fullName,
primaryEmailAddress: formData.get("primaryEmailAddress") as string, primaryEmailAddress: formData.primaryEmailAddress,
kind, additionalEmailAddresses: formData.additionalEmailAddresses,
kind: formData.kind,
}, },
}, },
onCompleted() { onCompleted: (response) => {
// Invalidate the peoples list query
environment.commitUpdate((store) => { environment.commitUpdate((store) => {
const organization = store.get(data.node.id); const organization = store.get(data.node.id);
if (organization) { if (organization) {
organization.invalidateRecord(); organization.invalidateRecord();
} }
})
toast({
title: "Success",
description: "Person created successfully",
variant: "default",
});
navigate(`/peoples/${(response as any).createPeople.id}`);
},
onError: (error) => {
toast({
title: "Error",
description: error.message || "Failed to create person",
variant: "destructive",
}); });
navigate("/peoples");
}, },
}); });
}; };
return ( return (
<div className="p-6 max-w-2xl mx-auto"> <>
<Helmet> <Helmet>
<title>Create People - Probo Console</title> <title>Create Person - Probo Console</title>
</Helmet> </Helmet>
<div className="space-y-6"> <form onSubmit={handleSubmit}>
<div> <div className="space-y-6 p-4 md:p-6 lg:p-8">
<h1 className="text-2xl font-semibold tracking-tight"> <div className="mx-auto max-w-4xl space-y-6">
Create People <EditableField
</h1> label="Full Name"
<p className="text-sm text-muted-foreground"> value={formData.fullName}
Add a new person to your organization. onChange={(value) => handleFieldChange('fullName', value)}
</p>
</div>
<Card>
<CardHeader>
<CardTitle>People Information</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="kind">Kind</Label>
<Select
value={kind}
onValueChange={(value) => setKind(value as typeof kind)}
>
<SelectTrigger>
<SelectValue placeholder="Select a kind" />
</SelectTrigger>
<SelectContent>
<SelectItem value="EMPLOYEE">Employee</SelectItem>
<SelectItem value="CONTRACTOR">Contractor</SelectItem>
<SelectItem value="VENDOR">Vendor</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="fullName">Full Name</Label>
<Input
id="fullName"
name="fullName"
placeholder="John Doe"
required required
/> />
</div>
<div className="space-y-2"> <EditableField
<Label htmlFor="primaryEmailAddress">Email Address</Label> label="Primary Email"
<Input value={formData.primaryEmailAddress}
id="primaryEmailAddress"
name="primaryEmailAddress"
type="email" type="email"
placeholder="john@example.com" onChange={(value) => handleFieldChange('primaryEmailAddress', value)}
required required
/> />
</div>
<div className="flex justify-end gap-4"> <div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Additional Email Addresses</Label>
</div>
<div className="space-y-2">
{formData.additionalEmailAddresses.map((email, index) => (
<div key={index} className="flex gap-2">
<Input
type="email"
value={email}
onChange={(e) => {
const newEmails = [...formData.additionalEmailAddresses];
newEmails[index] = e.target.value;
handleFieldChange('additionalEmailAddresses', newEmails);
}}
/>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => navigate("/peoples")} onClick={() => {
const newEmails = formData.additionalEmailAddresses.filter((_, i) => i !== index);
handleFieldChange('additionalEmailAddresses', newEmails);
}}
> >
Cancel Remove
</Button>
<Button type="submit" disabled={isCreatingPeople}>
{isCreatingPeople ? "Creating..." : "Create People"}
</Button> </Button>
</div> </div>
</form> ))}
</CardContent> <Button
type="button"
variant="outline"
onClick={() => {
handleFieldChange('additionalEmailAddresses', [...formData.additionalEmailAddresses, '']);
}}
>
Add Email
</Button>
</div>
</div>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">Additional Information</h2>
<p className="text-sm text-gray-500">
Additional details about the person
</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Kind</Label>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleFieldChange('kind', 'EMPLOYEE')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === 'EMPLOYEE'
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Employee
</button>
<button
type="button"
onClick={() => handleFieldChange('kind', 'CONTRACTOR')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === 'CONTRACTOR'
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Contractor
</button>
</div>
</div>
</div>
</div>
</Card> </Card>
</div> </div>
</div> </div>
<div className="fixed bottom-6 right-6 flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate(-1)}
>
Cancel
</Button>
<Button
type="submit"
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Create Person
</Button>
</div>
</form>
</>
); );
} }

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<26992373ce2e1751c6d070a6bbf6c8db>> * @generated SignedSource<<ea3c61409629d685dbb651ca60a8d0b7>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,6 +11,7 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE"; export type PeopleKind = "CONTRACTOR" | "EMPLOYEE";
export type CreatePeopleInput = { export type CreatePeopleInput = {
additionalEmailAddresses?: ReadonlyArray<string> | null | undefined;
fullName: string; fullName: string;
kind: PeopleKind; kind: PeopleKind;
organizationId: string; organizationId: string;
@@ -21,6 +22,7 @@ export type CreatePeoplePageCreatePeopleMutation$variables = {
}; };
export type CreatePeoplePageCreatePeopleMutation$data = { export type CreatePeoplePageCreatePeopleMutation$data = {
readonly createPeople: { readonly createPeople: {
readonly additionalEmailAddresses: ReadonlyArray<string>;
readonly fullName: string; readonly fullName: string;
readonly id: string; readonly id: string;
readonly kind: PeopleKind; readonly kind: PeopleKind;
@@ -76,6 +78,13 @@ v1 = [
"name": "primaryEmailAddress", "name": "primaryEmailAddress",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "additionalEmailAddresses",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -105,16 +114,16 @@ return {
"selections": (v1/*: any*/) "selections": (v1/*: any*/)
}, },
"params": { "params": {
"cacheID": "6438f79d9a2e3d3e5cfcc98c4107e512", "cacheID": "6674304ecdd07af45a837b942faed8f3",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "CreatePeoplePageCreatePeopleMutation", "name": "CreatePeoplePageCreatePeopleMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation CreatePeoplePageCreatePeopleMutation(\n $input: CreatePeopleInput!\n) {\n createPeople(input: $input) {\n id\n fullName\n primaryEmailAddress\n kind\n }\n}\n" "text": "mutation CreatePeoplePageCreatePeopleMutation(\n $input: CreatePeopleInput!\n) {\n createPeople(input: $input) {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "55f9379fac4d33ca3b2c21acf80094de"; (node as any).hash = "c2962b44f158d7899d7e5b4d071e9238";
export default node; export default node;

View File

@@ -340,6 +340,7 @@ input CreatePeopleInput {
organizationId: ID! organizationId: ID!
fullName: String! fullName: String!
primaryEmailAddress: String! primaryEmailAddress: String!
additionalEmailAddresses: [String!]
kind: PeopleKind! kind: PeopleKind!
} }

View File

@@ -1732,6 +1732,7 @@ input CreatePeopleInput {
organizationId: ID! organizationId: ID!
fullName: String! fullName: String!
primaryEmailAddress: String! primaryEmailAddress: String!
additionalEmailAddresses: [String!]
kind: PeopleKind! kind: PeopleKind!
} }
@@ -9619,7 +9620,7 @@ func (ec *executionContext) unmarshalInputCreatePeopleInput(ctx context.Context,
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"organizationId", "fullName", "primaryEmailAddress", "kind"} fieldsInOrder := [...]string{"organizationId", "fullName", "primaryEmailAddress", "additionalEmailAddresses", "kind"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -9647,6 +9648,13 @@ func (ec *executionContext) unmarshalInputCreatePeopleInput(ctx context.Context,
return it, err return it, err
} }
it.PrimaryEmailAddress = data it.PrimaryEmailAddress = data
case "additionalEmailAddresses":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("additionalEmailAddresses"))
data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
it.AdditionalEmailAddresses = data
case "kind": case "kind":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind"))
data, err := ec.unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx, v) data, err := ec.unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐPeopleKind(ctx, v)

View File

@@ -63,6 +63,7 @@ type CreatePeopleInput struct {
OrganizationID gid.GID `json:"organizationId"` OrganizationID gid.GID `json:"organizationId"`
FullName string `json:"fullName"` FullName string `json:"fullName"`
PrimaryEmailAddress string `json:"primaryEmailAddress"` PrimaryEmailAddress string `json:"primaryEmailAddress"`
AdditionalEmailAddresses []string `json:"additionalEmailAddresses,omitempty"`
Kind coredata.PeopleKind `json:"kind"` Kind coredata.PeopleKind `json:"kind"`
} }