From b94b58e2def3fac405dfd047fd206733ff87a27f Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sat, 31 May 2025 18:04:54 +0200 Subject: [PATCH] Add people pages Signed-off-by: Bryan Frimin Signed-off-by: Sacha Al Himdani --- .../src/components/form/EmailsField.tsx | 49 +++ apps/console2/src/hooks/graph/PeopleGraph.ts | 156 +++++++- .../PeopleGraphDeleteMutation.graphql.ts | 132 +++++++ .../PeopleGraphNodeQuery.graphql.ts | 165 +++++++++ .../PeopleGraphPaginatedFragment.graphql.ts | 252 +++++++++++++ .../PeopleGraphPaginatedQuery.graphql.ts | 271 ++++++++++++++ .../PeopleGraphUpdateMutation.graphql.ts | 142 ++++++++ .../__generated__/PeopleListQuery.graphql.ts | 344 ++++++++++++++++++ .../organizations/measures/MeasuresPage.tsx | 47 ++- .../organizations/people/PeopleDetailPage.tsx | 89 +++++ .../organizations/people/PeopleListPage.tsx | 127 +++++++ .../people/dialogs/CreatePeopleDialog.tsx | 127 +++++++ .../CreatePeopleDialogMutation.graphql.ts | 195 ++++++++++ .../people/tabs/PeopleProfileTab.tsx | 84 +++++ .../people/tabs/PeopleRoleTab.tsx | 112 ++++++ .../people/tabs/PeopleTasksTab.tsx | 7 + .../organizations/policies/PoliciesPage.tsx | 51 +-- .../organizations/policies/PolicyPage.tsx | 62 ++-- .../organizations/risks/RiskDetailPage.tsx | 56 ++- .../pages/organizations/risks/RisksPage.tsx | 53 ++- .../organizations/vendors/VendorsPage.tsx | 65 ++-- .../console2/src/providers/RelayProviders.tsx | 14 +- apps/console2/src/routes.tsx | 7 +- apps/console2/src/routes/peopleRoutes.ts | 48 +++ packages/helpers/src/index.ts | 1 + packages/helpers/src/people.ts | 37 ++ packages/ui/src/Layouts/AuthLayout.tsx | 4 +- packages/ui/src/Layouts/Layout.tsx | 2 + .../ui/src/Molecules/Dialog/ConfirmDialog.tsx | 98 +++-- packages/ui/src/index.ts | 5 +- 30 files changed, 2589 insertions(+), 213 deletions(-) create mode 100644 apps/console2/src/components/form/EmailsField.tsx create mode 100644 apps/console2/src/hooks/graph/__generated__/PeopleGraphDeleteMutation.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedFragment.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts create mode 100644 apps/console2/src/pages/organizations/people/PeopleDetailPage.tsx create mode 100644 apps/console2/src/pages/organizations/people/PeopleListPage.tsx create mode 100644 apps/console2/src/pages/organizations/people/dialogs/CreatePeopleDialog.tsx create mode 100644 apps/console2/src/pages/organizations/people/dialogs/__generated__/CreatePeopleDialogMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/people/tabs/PeopleProfileTab.tsx create mode 100644 apps/console2/src/pages/organizations/people/tabs/PeopleRoleTab.tsx create mode 100644 apps/console2/src/pages/organizations/people/tabs/PeopleTasksTab.tsx create mode 100644 apps/console2/src/routes/peopleRoutes.ts create mode 100644 packages/helpers/src/people.ts diff --git a/apps/console2/src/components/form/EmailsField.tsx b/apps/console2/src/components/form/EmailsField.tsx new file mode 100644 index 000000000..808be8313 --- /dev/null +++ b/apps/console2/src/components/form/EmailsField.tsx @@ -0,0 +1,49 @@ +import { Button, IconPlusLarge, IconTrashCan, Input, Label } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { useFieldArray } from "react-hook-form"; +import type { Control } from "react-hook-form"; +import type { UseFormRegister } from "react-hook-form"; + +type Props = { + control: Control; + register: UseFormRegister; +}; + +/** + * A field to handle multiple emails + */ +export function EmailsField({ control, register }: Props) { + const { __ } = useTranslate(); + const { fields, append, remove } = useFieldArray({ + name: "additionalEmailAddresses", + control, + }); + + return ( +
+ {fields.length > 0 && } + {fields.map((field, index) => ( +
+ +
+ ))} + +
+ ); +} diff --git a/apps/console2/src/hooks/graph/PeopleGraph.ts b/apps/console2/src/hooks/graph/PeopleGraph.ts index 0f7ae0bc7..08f3173d4 100644 --- a/apps/console2/src/hooks/graph/PeopleGraph.ts +++ b/apps/console2/src/hooks/graph/PeopleGraph.ts @@ -1,7 +1,20 @@ import { graphql } from "relay-runtime"; import type { PeopleGraphQuery } from "./__generated__/PeopleGraphQuery.graphql"; -import { useLazyLoadQuery } from "react-relay"; -import { useMemo } from "react"; +import { + useLazyLoadQuery, + useMutation, + usePreloadedQuery, + useRefetchableFragment, + type PreloadedQuery, +} from "react-relay"; +import { useMemo, useTransition } from "react"; +import type { PeopleGraphPaginatedQuery } from "./__generated__/PeopleGraphPaginatedQuery.graphql"; +import type { PeopleGraphPaginatedFragment$key } from "./__generated__/PeopleGraphPaginatedFragment.graphql"; +import { useConfirm } from "@probo/ui"; +import type { PeopleGraphDeleteMutation } from "./__generated__/PeopleGraphDeleteMutation.graphql"; +import { sprintf } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import type { PeopleGraphNodeQuery$data } from "./__generated__/PeopleGraphNodeQuery.graphql"; const peopleQuery = graphql` query PeopleGraphQuery($organizationId: ID!) { @@ -21,6 +34,9 @@ const peopleQuery = graphql` } `; +/** + * Return a list of people (used for people selectors) + */ export function usePeople(organizationId: string) { const data = useLazyLoadQuery(peopleQuery, { organizationId: organizationId, @@ -29,3 +45,139 @@ export function usePeople(organizationId: string) { return data.organization?.peoples?.edges.map((edge) => edge.node) ?? []; }, [data]); } + +export const paginatedPeopleQuery = graphql` + query PeopleGraphPaginatedQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + ... on Organization { + id + ...PeopleGraphPaginatedFragment + } + } + } +`; + +const paginatedPeopleFragment = graphql` + fragment PeopleGraphPaginatedFragment on Organization + @refetchable(queryName: "PeopleListQuery") + @argumentDefinitions( + first: { type: "Int", defaultValue: 50 } + order: { type: "PeopleOrder", defaultValue: null } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + peoples( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) @connection(key: "PeopleGraphPaginatedQuery_peoples") { + __id + edges { + node { + id + fullName + primaryEmailAddress + kind + additionalEmailAddresses + } + } + } + } +`; + +export function usePeopleQuery( + queryRef: PreloadedQuery +) { + const data = usePreloadedQuery(paginatedPeopleQuery, queryRef); + const [dataFragment, refetch] = useRefetchableFragment( + paginatedPeopleFragment, + data.organization as PeopleGraphPaginatedFragment$key + ); + const people = dataFragment?.peoples?.edges.map((edge) => edge.node); + return { + people, + refetch, + connectionId: dataFragment.peoples.__id, + }; +} + +export const deletePeopleMutation = graphql` + mutation PeopleGraphDeleteMutation( + $input: DeletePeopleInput! + $connections: [ID!]! + ) { + deletePeople(input: $input) { + deletedPeopleId @deleteEdge(connections: $connections) + } + } +`; + +export const PeopleConnectionKey = "PeopleGraphPaginatedQuery_peoples"; + +export const useDeletePeople = ( + people: { id?: string; fullName?: string }, + connectionId: string +) => { + const [mutate] = useMutation(deletePeopleMutation); + const confirm = useConfirm(); + const { __ } = useTranslate(); + + return () => { + if (!people.id || !people.fullName) { + return alert(__("Failed to delete people: missing id or fullName")); + } + confirm( + () => + new Promise((resolve) => { + mutate({ + variables: { + input: { + peopleId: people.id!, + }, + connections: [connectionId], + }, + onCompleted: () => resolve(), + }); + }), + { + message: sprintf( + __( + 'This will permanently delete "%s". This action cannot be undone.' + ), + people.fullName + ), + } + ); + }; +}; + +export const peopleNodeQuery = graphql` + query PeopleGraphNodeQuery($peopleId: ID!) { + node(id: $peopleId) { + ... on People { + id + fullName + primaryEmailAddress + kind + additionalEmailAddresses + } + } + } +`; + +export const updatePeopleMutation = graphql` + mutation PeopleGraphUpdateMutation($input: UpdatePeopleInput!) { + updatePeople(input: $input) { + people { + id + fullName + primaryEmailAddress + kind + additionalEmailAddresses + } + } + } +`; diff --git a/apps/console2/src/hooks/graph/__generated__/PeopleGraphDeleteMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/PeopleGraphDeleteMutation.graphql.ts new file mode 100644 index 000000000..db7f4db8d --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/PeopleGraphDeleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<242c546027e5b7c8410fca920a4dfa3c>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeletePeopleInput = { + peopleId: string; +}; +export type PeopleGraphDeleteMutation$variables = { + connections: ReadonlyArray; + input: DeletePeopleInput; +}; +export type PeopleGraphDeleteMutation$data = { + readonly deletePeople: { + readonly deletedPeopleId: string; + }; +}; +export type PeopleGraphDeleteMutation = { + response: PeopleGraphDeleteMutation$data; + variables: PeopleGraphDeleteMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedPeopleId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "PeopleGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeletePeoplePayload", + "kind": "LinkedField", + "name": "deletePeople", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "PeopleGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeletePeoplePayload", + "kind": "LinkedField", + "name": "deletePeople", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedPeopleId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9e0f5fd398468199333312505f258bb2", + "id": null, + "metadata": {}, + "name": "PeopleGraphDeleteMutation", + "operationKind": "mutation", + "text": "mutation PeopleGraphDeleteMutation(\n $input: DeletePeopleInput!\n) {\n deletePeople(input: $input) {\n deletedPeopleId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "611aa91bbb039c4e5800838f5db7985c"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql.ts new file mode 100644 index 000000000..242700454 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql.ts @@ -0,0 +1,165 @@ +/** + * @generated SignedSource<<006f8f3dc3c4d41a23cc71a870aa571c>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +export type PeopleGraphNodeQuery$variables = { + peopleId: string; +}; +export type PeopleGraphNodeQuery$data = { + readonly node: { + readonly additionalEmailAddresses?: ReadonlyArray; + readonly fullName?: string; + readonly id?: string; + readonly kind?: PeopleKind; + readonly primaryEmailAddress?: string; + }; +}; +export type PeopleGraphNodeQuery = { + response: PeopleGraphNodeQuery$data; + variables: PeopleGraphNodeQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "peopleId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "peopleId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "PeopleGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/) + ], + "type": "People", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "PeopleGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/) + ], + "type": "People", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "319f5f4ff11db061920a25bbfda43244", + "id": null, + "metadata": {}, + "name": "PeopleGraphNodeQuery", + "operationKind": "query", + "text": "query PeopleGraphNodeQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n kind\n additionalEmailAddresses\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "b4e6fe403e30fbc27ae638d464ad7543"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedFragment.graphql.ts b/apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedFragment.graphql.ts new file mode 100644 index 000000000..ddc45ef56 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedFragment.graphql.ts @@ -0,0 +1,252 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +import { FragmentRefs } from "relay-runtime"; +export type PeopleGraphPaginatedFragment$data = { + readonly id: string; + readonly peoples: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly additionalEmailAddresses: ReadonlyArray; + readonly fullName: string; + readonly id: string; + readonly kind: PeopleKind; + readonly primaryEmailAddress: string; + }; + }>; + }; + readonly " $fragmentType": "PeopleGraphPaginatedFragment"; +}; +export type PeopleGraphPaginatedFragment$key = { + readonly " $data"?: PeopleGraphPaginatedFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"PeopleGraphPaginatedFragment">; +}; + +import PeopleListQuery_graphql from './PeopleListQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "peoples" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": 50, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": PeopleListQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "PeopleGraphPaginatedFragment", + "selections": [ + { + "alias": "peoples", + "args": [ + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "__PeopleGraphPaginatedQuery_peoples_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + (v1/*: any*/) + ], + "type": "Organization", + "abstractKey": null +}; +})(); + +(node as any).hash = "c163b7909a337efd22088852b9908045"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql.ts new file mode 100644 index 000000000..38fece81c --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql.ts @@ -0,0 +1,271 @@ +/** + * @generated SignedSource<<43878b22fe8a689f41bcd506711ddb5f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type PeopleGraphPaginatedQuery$variables = { + organizationId: string; +}; +export type PeopleGraphPaginatedQuery$data = { + readonly organization: { + readonly id?: string; + readonly " $fragmentSpreads": FragmentRefs<"PeopleGraphPaginatedFragment">; + }; +}; +export type PeopleGraphPaginatedQuery = { + response: PeopleGraphPaginatedQuery$data; + variables: PeopleGraphPaginatedQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "PeopleGraphPaginatedQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "PeopleGraphPaginatedFragment" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "PeopleGraphPaginatedQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v4/*: any*/), + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "peoples", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": "peoples(first:50)" + }, + { + "alias": null, + "args": (v4/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "PeopleGraphPaginatedQuery_peoples", + "kind": "LinkedHandle", + "name": "peoples" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "b154e94ac7e783a492bc5aed7d44229d", + "id": null, + "metadata": {}, + "name": "PeopleGraphPaginatedQuery", + "operationKind": "query", + "text": "query PeopleGraphPaginatedQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...PeopleGraphPaginatedFragment\n }\n id\n }\n}\n\nfragment PeopleGraphPaginatedFragment on Organization {\n peoples(first: 50) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n additionalEmailAddresses\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "2d5f8ae64618a57eeb623b0ade941486"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql.ts new file mode 100644 index 000000000..887b32770 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql.ts @@ -0,0 +1,142 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +export type UpdatePeopleInput = { + additionalEmailAddresses?: ReadonlyArray | null | undefined; + fullName?: string | null | undefined; + id: string; + kind?: PeopleKind | null | undefined; + primaryEmailAddress?: string | null | undefined; +}; +export type PeopleGraphUpdateMutation$variables = { + input: UpdatePeopleInput; +}; +export type PeopleGraphUpdateMutation$data = { + readonly updatePeople: { + readonly people: { + readonly additionalEmailAddresses: ReadonlyArray; + readonly fullName: string; + readonly id: string; + readonly kind: PeopleKind; + readonly primaryEmailAddress: string; + }; + }; +}; +export type PeopleGraphUpdateMutation = { + response: PeopleGraphUpdateMutation$data; + variables: PeopleGraphUpdateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdatePeoplePayload", + "kind": "LinkedField", + "name": "updatePeople", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "people", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "PeopleGraphUpdateMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "PeopleGraphUpdateMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "c61ca0a6b20067ce5e0c708fc55509d8", + "id": null, + "metadata": {}, + "name": "PeopleGraphUpdateMutation", + "operationKind": "mutation", + "text": "mutation PeopleGraphUpdateMutation(\n $input: UpdatePeopleInput!\n) {\n updatePeople(input: $input) {\n people {\n id\n fullName\n primaryEmailAddress\n kind\n additionalEmailAddresses\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "48c2618963704a8cd3b1f0a68817cdad"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts new file mode 100644 index 000000000..e720c10dc --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts @@ -0,0 +1,344 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrderDirection = "ASC" | "DESC"; +export type PeopleOrderField = "CREATED_AT" | "FULL_NAME"; +export type PeopleOrder = { + direction: OrderDirection; + field: PeopleOrderField; +}; +export type PeopleListQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: PeopleOrder | null | undefined; +}; +export type PeopleListQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"PeopleGraphPaginatedFragment">; + }; +}; +export type PeopleListQuery = { + response: PeopleListQuery$data; + variables: PeopleListQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": 50, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v5 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" +}, +v6 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v7 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v8 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v9 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v10 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v13 = [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "PeopleListQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "PeopleGraphPaginatedFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v3/*: any*/) + ], + "kind": "Operation", + "name": "PeopleListQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "peoples", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v12/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + }, + (v11/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v13/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "PeopleGraphPaginatedQuery_peoples", + "kind": "LinkedHandle", + "name": "peoples" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "a17ff68897dbcf3daa33b073bc83f1bb", + "id": null, + "metadata": {}, + "name": "PeopleListQuery", + "operationKind": "query", + "text": "query PeopleListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: PeopleOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleGraphPaginatedFragment_16fISc\n id\n }\n}\n\nfragment PeopleGraphPaginatedFragment_16fISc on Organization {\n peoples(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n additionalEmailAddresses\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "c163b7909a337efd22088852b9908045"; + +export default node; diff --git a/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx b/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx index 760398d8e..dee87e1fd 100644 --- a/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx +++ b/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx @@ -9,7 +9,6 @@ import { ActionDropdown, Button, Card, - ConfirmDialog, DropdownItem, FileButton, IconChevronDown, @@ -27,7 +26,7 @@ import { Th, Thead, Tr, - useConfirmDialogRef, + useConfirm, } from "@probo/ui"; import { measuresQuery, @@ -245,33 +244,33 @@ type MeasureRowProps = { function MeasureRow(props: MeasureRowProps) { const { __ } = useTranslate(); const [deleteMeasure, isDeleting] = useDeleteMeasureMutation(); + const confirm = useConfirm(); const onDelete = () => { - return new Promise((resolve) => { - deleteMeasure({ - variables: { - input: { measureId: props.measure.id }, - connections: [props.connectionId], - }, - onCompleted: () => resolve(), - }); - }); - }; - - const confirmRef = useConfirmDialogRef(); - - return ( - <> - + new Promise((resolve) => { + deleteMeasure({ + variables: { + input: { measureId: props.measure.id }, + connections: [props.connectionId], + }, + onCompleted: () => resolve(), + }); + }), + { + message: sprintf( __( 'This will permanently delete the measure "%s". This action cannot be undone.' ), props.measure.name - )} - onConfirm={onDelete} - ref={confirmRef} - /> + ), + } + ); + }; + + return ( + <> {props.measure.name} @@ -281,7 +280,7 @@ function MeasureRow(props: MeasureRowProps) { {__("Edit")} confirmRef.current?.open()} + onClick={onDelete} disabled={isDeleting} variant="danger" icon={IconTrashCan} diff --git a/apps/console2/src/pages/organizations/people/PeopleDetailPage.tsx b/apps/console2/src/pages/organizations/people/PeopleDetailPage.tsx new file mode 100644 index 000000000..59b9a194e --- /dev/null +++ b/apps/console2/src/pages/organizations/people/PeopleDetailPage.tsx @@ -0,0 +1,89 @@ +import { + ConnectionHandler, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import type { PeopleGraphNodeQuery } from "/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql"; +import { + PeopleConnectionKey, + peopleNodeQuery, + useDeletePeople, +} from "/hooks/graph/PeopleGraph"; +import { + ActionDropdown, + Avatar, + Breadcrumb, + DropdownItem, + IconTrashCan, + TabLink, + Tabs, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { Outlet } from "react-router"; + +type Props = { + queryRef: PreloadedQuery; +}; + +export default function PeopleDetailPage(props: Props) { + const data = usePreloadedQuery(peopleNodeQuery, props.queryRef); + const people = data.node; + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + const deletePeople = useDeletePeople( + people, + ConnectionHandler.getConnectionID(organizationId, PeopleConnectionKey) + ); + + return ( +
+ +
+
+ +
{people.fullName}
+
+ + + {__("Delete")} + + +
+ + + + {__("Tasks")} + + + {__("Role & access")} + + + {__("General information")} + + + + +
+ ); +} diff --git a/apps/console2/src/pages/organizations/people/PeopleListPage.tsx b/apps/console2/src/pages/organizations/people/PeopleListPage.tsx new file mode 100644 index 000000000..ee94eebeb --- /dev/null +++ b/apps/console2/src/pages/organizations/people/PeopleListPage.tsx @@ -0,0 +1,127 @@ +import { + Button, + IconPlusLarge, + PageHeader, + Thead, + Tbody, + Tr, + Th, + Td, + Avatar, + ActionDropdown, + DropdownItem, + IconPencil, + IconTrashCan, + useConfirm, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import type { PeopleGraphPaginatedQuery } from "/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql"; +import { useMutation, type PreloadedQuery } from "react-relay"; +import { + deletePeopleMutation, + useDeletePeople, + usePeopleQuery, +} from "/hooks/graph/PeopleGraph"; +import { SortableTable, SortableTh } from "/components/SortableTable"; +import type { PeopleGraphPaginatedFragment$data } from "/hooks/graph/__generated__/PeopleGraphPaginatedFragment.graphql"; +import type { NodeOf } from "/types"; +import { usePageTitle } from "@probo/hooks"; +import { getRole, sprintf } from "@probo/helpers"; +import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog"; +import type { PeopleGraphDeleteMutation } from "/hooks/graph/__generated__/PeopleGraphDeleteMutation.graphql"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { Link } from "react-router"; + +type People = NodeOf; + +export default function PeopleListPage({ + queryRef, +}: { + queryRef: PreloadedQuery; +}) { + const { __ } = useTranslate(); + const { people, refetch, connectionId } = usePeopleQuery(queryRef); + + usePageTitle(__("Members")); + + return ( +
+ + + + + + + + + {__("Name")} + {__("Role")} + {__("Actions")} + + + + {people.map((person) => ( + + ))} + + +
+ ); +} + +function PeopleRow({ + people, + connectionId, +}: { + people: People; + connectionId: string; +}) { + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + const deletePeople = useDeletePeople(people, connectionId); + + return ( + + +
+ +
+
{people.fullName}
+
+ {people.primaryEmailAddress} +
+
+
+ + {getRole(__, people.kind)} + + + + + + {__("Edit")} + + + + {__("Delete")} + + + + + ); +} diff --git a/apps/console2/src/pages/organizations/people/dialogs/CreatePeopleDialog.tsx b/apps/console2/src/pages/organizations/people/dialogs/CreatePeopleDialog.tsx new file mode 100644 index 000000000..e6453462b --- /dev/null +++ b/apps/console2/src/pages/organizations/people/dialogs/CreatePeopleDialog.tsx @@ -0,0 +1,127 @@ +import { useTranslate } from "@probo/i18n"; +import { + Breadcrumb, + Button, + Dialog, + DialogContent, + DialogFooter, + Field, + Option, + useDialogRef, +} from "@probo/ui"; +import type { ReactNode } from "react"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import z from "zod"; +import { getRoles, peopleRoles } from "@probo/helpers"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { graphql } from "relay-runtime"; +import { ControlledField } from "/components/form/ControlledField"; +import { EmailsField } from "/components/form/EmailsField"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; + +type Props = { + children: ReactNode; + connectionId: string; +}; + +const schema = z.object({ + fullName: z.string().min(1), + primaryEmailAddress: z.string().email(), + additionalEmailAddresses: z.preprocess( + // Empty additional emails are skipped + (v) => (v as string[]).filter((v) => !!v), + z.array(z.string().email()) + ), + kind: z.enum(peopleRoles), +}); + +export const createPeopleMutation = graphql` + mutation CreatePeopleDialogMutation( + $input: CreatePeopleInput! + $connections: [ID!]! + ) { + createPeople(input: $input) { + peopleEdge @prependEdge(connections: $connections) { + node { + id + fullName + primaryEmailAddress + kind + additionalEmailAddresses + } + } + } + } +`; + +export function CreatePeopleDialog({ children, connectionId }: Props) { + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + const { control, handleSubmit, register } = useFormWithSchema(schema, { + defaultValues: { + additionalEmailAddresses: [], + }, + }); + const ref = useDialogRef(); + + const [mutate, isMutating] = useMutationWithToasts(createPeopleMutation, { + successMessage: __("Person created successfully."), + errorMessage: __("Failed to create person. Please try again."), + }); + + const onSubmit = handleSubmit((data) => { + mutate({ + variables: { + input: { + ...data, + organizationId, + }, + connections: [connectionId], + }, + onSuccess: () => { + ref.current?.close(); + }, + }); + }); + + return ( + } + > +
+ + + + + {getRoles(__).map((role) => ( + + ))} + + + + + + +
+
+ ); +} diff --git a/apps/console2/src/pages/organizations/people/dialogs/__generated__/CreatePeopleDialogMutation.graphql.ts b/apps/console2/src/pages/organizations/people/dialogs/__generated__/CreatePeopleDialogMutation.graphql.ts new file mode 100644 index 000000000..607a89b2b --- /dev/null +++ b/apps/console2/src/pages/organizations/people/dialogs/__generated__/CreatePeopleDialogMutation.graphql.ts @@ -0,0 +1,195 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +export type CreatePeopleInput = { + additionalEmailAddresses?: ReadonlyArray | null | undefined; + fullName: string; + kind: PeopleKind; + organizationId: string; + primaryEmailAddress: string; +}; +export type CreatePeopleDialogMutation$variables = { + connections: ReadonlyArray; + input: CreatePeopleInput; +}; +export type CreatePeopleDialogMutation$data = { + readonly createPeople: { + readonly peopleEdge: { + readonly node: { + readonly additionalEmailAddresses: ReadonlyArray; + readonly fullName: string; + readonly id: string; + readonly kind: PeopleKind; + readonly primaryEmailAddress: string; + }; + }; + }; +}; +export type CreatePeopleDialogMutation = { + response: CreatePeopleDialogMutation$data; + variables: CreatePeopleDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "peopleEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "CreatePeopleDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreatePeoplePayload", + "kind": "LinkedField", + "name": "createPeople", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "CreatePeopleDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreatePeoplePayload", + "kind": "LinkedField", + "name": "createPeople", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "peopleEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "634c2845825a02a0343c3211b41c90a0", + "id": null, + "metadata": {}, + "name": "CreatePeopleDialogMutation", + "operationKind": "mutation", + "text": "mutation CreatePeopleDialogMutation(\n $input: CreatePeopleInput!\n) {\n createPeople(input: $input) {\n peopleEdge {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n additionalEmailAddresses\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "867aad1276c26bd787ddf80c6f166abe"; + +export default node; diff --git a/apps/console2/src/pages/organizations/people/tabs/PeopleProfileTab.tsx b/apps/console2/src/pages/organizations/people/tabs/PeopleProfileTab.tsx new file mode 100644 index 000000000..279475441 --- /dev/null +++ b/apps/console2/src/pages/organizations/people/tabs/PeopleProfileTab.tsx @@ -0,0 +1,84 @@ +import z from "zod"; +import { peopleRoles } from "@probo/helpers"; +import { useOutletContext } from "react-router"; +import type { PeopleGraphNodeQuery$data } from "/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql"; +import { useTranslate } from "@probo/i18n"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql"; +import { updatePeopleMutation } from "/hooks/graph/PeopleGraph"; +import { Button, Card, Field } from "@probo/ui"; +import { EmailsField } from "/components/form/EmailsField"; + +const schema = z.object({ + fullName: z.string().min(1), + primaryEmailAddress: z.string().email(), + additionalEmailAddresses: z.preprocess( + // Empty additional emails are skipped + (v) => (v as string[]).filter((v) => !!v), + z.array(z.string().email()) + ), + kind: z.enum(peopleRoles), +}); + +export default function PeopleProfileTab() { + const { people } = useOutletContext<{ + people: PeopleGraphNodeQuery$data["node"]; + }>(); + const { __ } = useTranslate(); + const { control, formState, handleSubmit, register, reset } = + useFormWithSchema(schema, { + defaultValues: { + kind: people.kind, + fullName: people.fullName, + primaryEmailAddress: people.primaryEmailAddress, + additionalEmailAddresses: [...(people.additionalEmailAddresses ?? [])], + }, + }); + const [mutate, isMutating] = useMutationWithToasts( + updatePeopleMutation, + { + successMessage: __("Member updated successfully."), + errorMessage: __("Failed to update member. Please try again."), + } + ); + + const onSubmit = handleSubmit((data) => { + mutate({ + variables: { + input: { + id: people.id!, + fullName: data.fullName, + primaryEmailAddress: data.primaryEmailAddress, + additionalEmailAddresses: data.additionalEmailAddresses, + // TODO : make these field optional in the query (server side) + kind: people.kind, + }, + }, + onCompleted: () => { + reset(data); + }, + }); + }); + + return ( +
+ + + + + +
+ {formState.isDirty && ( + + )} +
+
+ ); +} diff --git a/apps/console2/src/pages/organizations/people/tabs/PeopleRoleTab.tsx b/apps/console2/src/pages/organizations/people/tabs/PeopleRoleTab.tsx new file mode 100644 index 000000000..6f25bad5f --- /dev/null +++ b/apps/console2/src/pages/organizations/people/tabs/PeopleRoleTab.tsx @@ -0,0 +1,112 @@ +import { useTranslate } from "@probo/i18n"; +import { Button, Card, IconCheckmark1 } from "@probo/ui"; +import type { PropsWithChildren } from "react"; +import z from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { ControlledField } from "/components/form/ControlledField"; +import { Option } from "@probo/ui"; +import { getRoles, peopleRoles } from "@probo/helpers"; +import type { PeopleGraphNodeQuery$data } from "/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql"; +import { useOutletContext } from "react-router"; +import { updatePeopleMutation } from "/hooks/graph/PeopleGraph"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql"; + +const schema = z.object({ + kind: z.enum(peopleRoles), +}); + +export default function PeopleRoleTab() { + const { people } = useOutletContext<{ + people: PeopleGraphNodeQuery$data["node"]; + }>(); + const { __ } = useTranslate(); + const { control, formState, handleSubmit, reset } = useFormWithSchema( + schema, + { + defaultValues: { + kind: people.kind, + }, + } + ); + const [mutate, isMutating] = useMutationWithToasts( + updatePeopleMutation, + { + successMessage: __("Member updated successfully."), + errorMessage: __("Failed to update member. Please try again."), + } + ); + + const onSubmit = handleSubmit((data) => { + mutate({ + variables: { + input: { + id: people.id!, + kind: data.kind, + // TODO : make these field optional in the query (server side) + fullName: people.fullName!, + primaryEmailAddress: people.primaryEmailAddress!, + additionalEmailAddresses: people.additionalEmailAddresses ?? [], + }, + }, + onCompleted: () => { + reset(data); + }, + }); + }); + + return ( +
+ + + {getRoles(__).map((role) => ( + + ))} + +
+
{__("Permissions")}
+
    + + {__("Access dashboard & reports relevant to their team")} + + + {__("Create and manage own tasks, tickets, or projects")} + + + {__("Comment on shared documents or projects")} + + + {__("Receive notifications and system alerts")} + + + {__("Join and participate in team chats or threads")} + +
+
+
+
+ {formState.isDirty && ( + + )} +
+
+ ); +} + +function AccessItem({ children }: PropsWithChildren) { + return ( +
  • + + {children} +
  • + ); +} diff --git a/apps/console2/src/pages/organizations/people/tabs/PeopleTasksTab.tsx b/apps/console2/src/pages/organizations/people/tabs/PeopleTasksTab.tsx new file mode 100644 index 000000000..fa0ad540f --- /dev/null +++ b/apps/console2/src/pages/organizations/people/tabs/PeopleTasksTab.tsx @@ -0,0 +1,7 @@ +export default function PeopleTasksTab() { + return ( +
    + Not available yet +
    + ); +} diff --git a/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx b/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx index be2adb955..85bb1ecd7 100644 --- a/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx +++ b/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx @@ -9,10 +9,10 @@ import { Td, Avatar, Badge, - ConfirmDialog, IconTrashCan, Button, IconPlusLarge, + useConfirm, } from "@probo/ui"; import { useFragment, @@ -151,18 +151,30 @@ function PolicyRow({ const signedCount = signatures.filter( (signature) => signature.state === "SIGNED" ).length; - const [deletePolicy] = useDeletePolicyMutation(); + const [deletePolicy, isDeleting] = useDeletePolicyMutation(); + const confirm = useConfirm(); const handleDelete = () => { - return new Promise((resolve) => { - deletePolicy({ - variables: { - input: { policyId: policy.id }, - connections: [connectionId], - }, - onCompleted: () => resolve(), - }); - }); + confirm( + () => + new Promise((resolve) => { + deletePolicy({ + variables: { + input: { policyId: policy.id }, + connections: [connectionId], + }, + onCompleted: () => resolve(), + }); + }), + { + message: sprintf( + __( + 'This will permanently delete the policy "%s". This action cannot be undone.' + ), + policy.title + ), + } + ); }; return ( @@ -203,17 +215,12 @@ function PolicyRow({ {signedCount}/{signatures.length} - -