Add fields to organization
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
Label,
|
||||
PageHeader,
|
||||
Spinner,
|
||||
Textarea,
|
||||
useConfirm,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
@@ -19,14 +20,15 @@ import type { PreloadedQuery } from "react-relay";
|
||||
import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql";
|
||||
import { useFragment, useMutation, usePreloadedQuery } from "react-relay";
|
||||
import { organizationViewQuery } from "/hooks/graph/OrganizationGraph";
|
||||
import { useDebounceCallback } from "usehooks-ts";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type {
|
||||
SettingsPageFragment$data,
|
||||
SettingsPageFragment$key,
|
||||
} from "./__generated__/SettingsPageFragment.graphql";
|
||||
import { useState, type ChangeEventHandler } from "react";
|
||||
import { useState, type ChangeEventHandler, useEffect } from "react";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { z } from "zod";
|
||||
import type { NodeOf } from "/types";
|
||||
import clsx from "clsx";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
@@ -36,6 +38,16 @@ import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
||||
import { useNavigate } from "react-router";
|
||||
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
||||
|
||||
const organizationSchema = z.object({
|
||||
name: z.string().min(1, "Organization name is required"),
|
||||
description: z.string().optional(),
|
||||
websiteUrl: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
headquarterAddress: z.string().optional(),
|
||||
});
|
||||
|
||||
type OrganizationFormData = z.infer<typeof organizationSchema>;
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<OrganizationGraph_ViewQuery>;
|
||||
};
|
||||
@@ -45,6 +57,10 @@ const organizationFragment = graphql`
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
users(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
@@ -75,6 +91,10 @@ const updateOrganizationMutation = graphql`
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,25 +112,59 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
organizationFragment,
|
||||
organizationKey
|
||||
);
|
||||
const [updateOrganization, isUpdating] = useMutation(
|
||||
updateOrganizationMutation
|
||||
);
|
||||
const [updateOrganization] = useMutation(updateOrganizationMutation);
|
||||
const [deleteOrganization, isDeleting] = useDeleteOrganizationMutation();
|
||||
const users = organization.users.edges.map((edge) => edge.node);
|
||||
|
||||
const updateOrganizationName = useDebounceCallback((name: string) => {
|
||||
if (!name) {
|
||||
return "";
|
||||
}
|
||||
const { formState, handleSubmit, register, reset } =
|
||||
useFormWithSchema(organizationSchema, {
|
||||
defaultValues: {
|
||||
name: organization.name || "",
|
||||
description: organization.description || "",
|
||||
websiteUrl: organization.websiteUrl || "",
|
||||
email: organization.email || "",
|
||||
headquarterAddress: organization.headquarterAddress || "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
name: organization.name || "",
|
||||
description: organization.description || "",
|
||||
websiteUrl: organization.websiteUrl || "",
|
||||
email: organization.email || "",
|
||||
headquarterAddress: organization.headquarterAddress || "",
|
||||
});
|
||||
}, [organization, reset]);
|
||||
|
||||
const onSubmit = handleSubmit((data: OrganizationFormData) => {
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
name,
|
||||
name: data.name,
|
||||
description: data.description || null,
|
||||
websiteUrl: data.websiteUrl || null,
|
||||
email: data.email || null,
|
||||
headquarterAddress: data.headquarterAddress || null,
|
||||
},
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Failed to update organization"),
|
||||
description: error.message || __("Please try again."),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
onCompleted() {
|
||||
toast({
|
||||
title: __("Organization updated"),
|
||||
description: __("Your organization details have been updated successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
const updateOrganizationLogo: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -156,14 +210,15 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
<PageHeader title={__("Settings")} />
|
||||
|
||||
{/* Organization settings */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-medium">
|
||||
{__("Organization details")}
|
||||
</h2>
|
||||
{isUpdating && <Spinner />}
|
||||
</div>
|
||||
<Card padded className="space-y-4">
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-medium">
|
||||
{__("Organization details")}
|
||||
</h2>
|
||||
{formState.isSubmitting && <Spinner />}
|
||||
</div>
|
||||
<Card padded className="space-y-4">
|
||||
<div>
|
||||
<Label>{__("Organization logo")}</Label>
|
||||
<div className="flex w-max items-center gap-4">
|
||||
@@ -173,7 +228,7 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
size="xl"
|
||||
/>
|
||||
<FileButton
|
||||
disabled={isUpdating}
|
||||
disabled={formState.isSubmitting}
|
||||
onChange={updateOrganizationLogo}
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
@@ -183,16 +238,62 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<Field
|
||||
readOnly={isUpdating}
|
||||
{...register("name")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="name"
|
||||
type="text"
|
||||
defaultValue={organization.name}
|
||||
label={__("Organization name")}
|
||||
placeholder={__("Organization name")}
|
||||
onChange={(e) => updateOrganizationName(e.currentTarget.value)}
|
||||
/>
|
||||
<div>
|
||||
<Label>{__("Description")}</Label>
|
||||
<Textarea
|
||||
{...register("description")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="description"
|
||||
placeholder={__("Brief description of your organization")}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Field
|
||||
{...register("websiteUrl")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="websiteUrl"
|
||||
type="url"
|
||||
label={__("Website URL")}
|
||||
placeholder={__("https://example.com")}
|
||||
/>
|
||||
<Field
|
||||
{...register("email")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="email"
|
||||
type="email"
|
||||
label={__("Email")}
|
||||
placeholder={__("contact@example.com")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{__("Headquarter Address")}</Label>
|
||||
<Textarea
|
||||
{...register("headquarterAddress")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="headquarterAddress"
|
||||
placeholder={__("123 Main St, City, Country")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{formState.isDirty && (
|
||||
<div className="flex justify-end pt-6">
|
||||
<Button type="submit" disabled={formState.isSubmitting}>
|
||||
{formState.isSubmitting ? __("Updating...") : __("Update Organization")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Integrations */}
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<e5b6c1db04175ebddb21379dfd331fc6>>
|
||||
* @generated SignedSource<<c071187d5ef9cecb128621c57f97393b>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -21,6 +21,9 @@ export type SettingsPageFragment$data = {
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly description: string | null | undefined;
|
||||
readonly email: string | null | undefined;
|
||||
readonly headquarterAddress: string | null | undefined;
|
||||
readonly id: string;
|
||||
readonly logoUrl: string | null | undefined;
|
||||
readonly name: string;
|
||||
@@ -34,6 +37,7 @@ export type SettingsPageFragment$data = {
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly websiteUrl: string | null | undefined;
|
||||
readonly " $fragmentType": "SettingsPageFragment";
|
||||
};
|
||||
export type SettingsPageFragment$key = {
|
||||
@@ -56,14 +60,21 @@ v1 = {
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v2 = [
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 100
|
||||
}
|
||||
],
|
||||
v3 = {
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
@@ -87,7 +98,29 @@ return {
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "websiteUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "headquarterAddress",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v3/*: any*/),
|
||||
"concreteType": "UserConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "users",
|
||||
@@ -117,14 +150,8 @@ return {
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
(v3/*: any*/)
|
||||
(v2/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
@@ -136,7 +163,7 @@ return {
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"args": (v3/*: any*/),
|
||||
"concreteType": "ConnectorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "connectors",
|
||||
@@ -167,7 +194,7 @@ return {
|
||||
"name": "type",
|
||||
"storageKey": null
|
||||
},
|
||||
(v3/*: any*/)
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
@@ -183,6 +210,6 @@ return {
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "b7ee53afcaaa0406dceeda0427afdfef";
|
||||
(node as any).hash = "bdd548b7b3fa4c1cfef32569dc63d45d";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<6dc30c35c9ce7367967d235b1a259179>>
|
||||
* @generated SignedSource<<892a78db951d547b53d595d219b94e75>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -10,9 +10,13 @@
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type UpdateOrganizationInput = {
|
||||
description?: string | null | undefined;
|
||||
email?: string | null | undefined;
|
||||
headquarterAddress?: string | null | undefined;
|
||||
logo?: any | null | undefined;
|
||||
name?: string | null | undefined;
|
||||
organizationId: string;
|
||||
websiteUrl?: string | null | undefined;
|
||||
};
|
||||
export type SettingsPage_UpdateMutation$variables = {
|
||||
input: UpdateOrganizationInput;
|
||||
@@ -20,9 +24,13 @@ export type SettingsPage_UpdateMutation$variables = {
|
||||
export type SettingsPage_UpdateMutation$data = {
|
||||
readonly updateOrganization: {
|
||||
readonly organization: {
|
||||
readonly description: string | null | undefined;
|
||||
readonly email: string | null | undefined;
|
||||
readonly headquarterAddress: string | null | undefined;
|
||||
readonly id: string;
|
||||
readonly logoUrl: string | null | undefined;
|
||||
readonly name: string;
|
||||
readonly websiteUrl: string | null | undefined;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -82,6 +90,34 @@ v1 = [
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "websiteUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "headquarterAddress",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -108,16 +144,16 @@ return {
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "ac8d86332fabe20dd7ea8fef4207b75e",
|
||||
"cacheID": "3526d270f311db33e0563abc99c96978",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsPage_UpdateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SettingsPage_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n }\n }\n}\n"
|
||||
"text": "mutation SettingsPage_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "85f76939223253c1d102d05cd74f7538";
|
||||
(node as any).hash = "54949158defa7f1bb8e1e578db7cd7be";
|
||||
|
||||
export default node;
|
||||
|
||||
Reference in New Issue
Block a user