From 104330d3fdbeb490ddc8ec3b22ee06a32675a9df Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Fri, 19 Sep 2025 16:05:07 +0200 Subject: [PATCH] Add fields to organization Signed-off-by: Sacha Al Himdani --- .../OrganizationGraph_ViewQuery.graphql.ts | 55 ++- .../src/hooks/useTrustCenterQueries.ts | 4 + .../src/pages/organizations/SettingsPage.tsx | 147 +++++-- .../SettingsPageFragment.graphql.ts | 57 ++- .../SettingsPage_UpdateMutation.graphql.ts | 44 ++- pkg/coredata/migrations/20250919T121111Z.sql | 5 + pkg/coredata/organization.go | 62 ++- pkg/probo/organization_service.go | 32 +- pkg/server/api/console/v1/schema.graphql | 9 + pkg/server/api/console/v1/schema/schema.go | 372 +++++++++++++++++- .../api/console/v1/types/organization.go | 12 +- pkg/server/api/console/v1/types/types.go | 14 +- pkg/server/api/console/v1/v1_resolver.go | 8 +- pkg/server/api/trust/v1/schema.graphql | 5 + pkg/server/api/trust/v1/schema/schema.go | 223 ++++++++++- pkg/server/api/trust/v1/types/organization.go | 8 +- pkg/server/api/trust/v1/types/types.go | 10 +- 17 files changed, 971 insertions(+), 96 deletions(-) create mode 100644 pkg/coredata/migrations/20250919T121111Z.sql diff --git a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts index 31eb287b5..0c6696efe 100644 --- a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts @@ -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" } }; })(); diff --git a/apps/console/src/hooks/useTrustCenterQueries.ts b/apps/console/src/hooks/useTrustCenterQueries.ts index d835c5235..e35bd5718 100644 --- a/apps/console/src/hooks/useTrustCenterQueries.ts +++ b/apps/console/src/hooks/useTrustCenterQueries.ts @@ -180,6 +180,10 @@ const TRUST_CENTER_QUERY = ` id name logoUrl + description + websiteUrl + email + headquarterAddress } documents(first: 100) { edges { diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx index 39e99dda7..1bc6e59a1 100644 --- a/apps/console/src/pages/organizations/SettingsPage.tsx +++ b/apps/console/src/pages/organizations/SettingsPage.tsx @@ -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; + type Props = { queryRef: PreloadedQuery; }; @@ -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 = (e) => { const file = e.target.files?.[0]; @@ -156,14 +210,15 @@ export default function SettingsPage({ queryRef }: Props) { {/* Organization settings */} -
-
-

- {__("Organization details")} -

- {isUpdating && } -
- +
+
+
+

+ {__("Organization details")} +

+ {formState.isSubmitting && } +
+
@@ -173,7 +228,7 @@ export default function SettingsPage({ queryRef }: Props) { size="xl" />
updateOrganizationName(e.currentTarget.value)} /> +
+ +