Add fields to organization

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-19 16:05:07 +02:00
parent b39dd79728
commit 104330d3fd
17 changed files with 971 additions and 96 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<6f19d3ea0d16b3f4418e85261e3e4915>>
* @generated SignedSource<<148c6dc2b419fa0611529004e0a3535a>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -54,14 +54,21 @@ v3 = {
"name": "name",
"storageKey": null
},
v4 = [
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
v5 = [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
v5 = {
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -139,7 +146,29 @@ return {
},
{
"alias": null,
"args": (v4/*: any*/),
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
},
{
"alias": null,
"args": (v5/*: any*/),
"concreteType": "UserConnection",
"kind": "LinkedField",
"name": "users",
@@ -169,14 +198,8 @@ return {
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
(v5/*: any*/)
(v4/*: any*/),
(v6/*: any*/)
],
"storageKey": null
}
@@ -188,7 +211,7 @@ return {
},
{
"alias": null,
"args": (v4/*: any*/),
"args": (v5/*: any*/),
"concreteType": "ConnectorConnection",
"kind": "LinkedField",
"name": "connectors",
@@ -219,7 +242,7 @@ return {
"name": "type",
"storageKey": null
},
(v5/*: any*/)
(v6/*: any*/)
],
"storageKey": null
}
@@ -239,12 +262,12 @@ return {
]
},
"params": {
"cacheID": "3062be33ff59b816b10d098c083db370",
"cacheID": "c19271fc733cfb1de051ccdd98ae9bef",
"id": null,
"metadata": {},
"name": "OrganizationGraph_ViewQuery",
"operationKind": "query",
"text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n"
"text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n description\n websiteUrl\n email\n headquarterAddress\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n"
}
};
})();

View File

@@ -180,6 +180,10 @@ const TRUST_CENTER_QUERY = `
id
name
logoUrl
description
websiteUrl
email
headquarterAddress
}
documents(first: 100) {
edges {

View File

@@ -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">

View File

@@ -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;

View File

@@ -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;