From 65d3267ab8bb2b222c1f2e4499bb69866321b02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 31 Dec 2025 18:49:19 +0100 Subject: [PATCH] Fix metting pages permissions handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- ...eateMeetingDialogCreateMutation.graphql.ts | 22 ++++-- ...eetingDetailPageMeetingFragment.graphql.ts | 32 ++++++++- .../core/MeetingGraphListQuery.graphql.ts | 54 ++++++++++++--- .../core/MeetingGraphNodeQuery.graphql.ts | 32 ++++++++- .../core/MeetingsListQuery.graphql.ts | 19 +++++- .../core/MeetingsPageRowFragment.graphql.ts | 18 ++++- apps/console/src/hooks/graph/MeetingGraph.ts | 3 + .../meetings/MeetingDetailPage.tsx | 67 ++----------------- .../organizations/meetings/MeetingsPage.tsx | 11 ++- .../meetings/dialogs/CreateMeetingDialog.tsx | 1 + 10 files changed, 168 insertions(+), 91 deletions(-) diff --git a/apps/console/src/__generated__/core/CreateMeetingDialogCreateMutation.graphql.ts b/apps/console/src/__generated__/core/CreateMeetingDialogCreateMutation.graphql.ts index e94cd8ade..68f08a2a7 100644 --- a/apps/console/src/__generated__/core/CreateMeetingDialogCreateMutation.graphql.ts +++ b/apps/console/src/__generated__/core/CreateMeetingDialogCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<6ed3e434ef2a7f39c1ccddb4c3ddd8b2>> * @lightSyntaxTransform * @nogrep */ @@ -28,6 +28,7 @@ export type CreateMeetingDialogCreateMutation$data = { readonly fullName: string; readonly id: string; }>; + readonly canDelete: boolean; readonly date: any; readonly id: string; readonly minutes: string | null | undefined; @@ -122,6 +123,19 @@ v4 = { } ], "storageKey": null + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:delete\")" } ], "storageKey": null @@ -195,16 +209,16 @@ return { ] }, "params": { - "cacheID": "cf37e45c7e8cffd8b44cdc85488209e8", + "cacheID": "6b9dce0e1a88fdf272b516cf59bb4137", "id": null, "metadata": {}, "name": "CreateMeetingDialogCreateMutation", "operationKind": "mutation", - "text": "mutation CreateMeetingDialogCreateMutation(\n $input: CreateMeetingInput!\n) {\n createMeeting(input: $input) {\n meetingEdge {\n node {\n id\n name\n date\n minutes\n attendees {\n id\n fullName\n }\n }\n }\n }\n}\n" + "text": "mutation CreateMeetingDialogCreateMutation(\n $input: CreateMeetingInput!\n) {\n createMeeting(input: $input) {\n meetingEdge {\n node {\n id\n name\n date\n minutes\n attendees {\n id\n fullName\n }\n canDelete: permission(action: \"core:meeting:delete\")\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "5ba1bb7a0344e0ea19b383af6d75534f"; +(node as any).hash = "b69925d4ec72b17467f8f50158d02f6f"; export default node; diff --git a/apps/console/src/__generated__/core/MeetingDetailPageMeetingFragment.graphql.ts b/apps/console/src/__generated__/core/MeetingDetailPageMeetingFragment.graphql.ts index f7cedcd75..ef25cf3d0 100644 --- a/apps/console/src/__generated__/core/MeetingDetailPageMeetingFragment.graphql.ts +++ b/apps/console/src/__generated__/core/MeetingDetailPageMeetingFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<3684f6d6c3a9746dcfcb8bb0156d2201>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -15,6 +15,8 @@ export type MeetingDetailPageMeetingFragment$data = { readonly fullName: string; readonly id: string; }>; + readonly canDelete: boolean; + readonly canUpdate: boolean; readonly date: any; readonly id: string; readonly minutes: string | null | undefined; @@ -62,6 +64,32 @@ return { "name": "minutes", "storageKey": null }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:update\")" + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:delete\")" + }, { "alias": null, "args": null, @@ -87,6 +115,6 @@ return { }; })(); -(node as any).hash = "ae1564a2e5115bfffe20359c5288d4eb"; +(node as any).hash = "16fcd4d8cc7dbe80bc7fd37ac5e824e8"; export default node; diff --git a/apps/console/src/__generated__/core/MeetingGraphListQuery.graphql.ts b/apps/console/src/__generated__/core/MeetingGraphListQuery.graphql.ts index 70710e787..aec7822b7 100644 --- a/apps/console/src/__generated__/core/MeetingGraphListQuery.graphql.ts +++ b/apps/console/src/__generated__/core/MeetingGraphListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<7ac900885cf356d87efb7496bd37bbad>> * @lightSyntaxTransform * @nogrep */ @@ -15,6 +15,7 @@ export type MeetingGraphListQuery$variables = { }; export type MeetingGraphListQuery$data = { readonly organization: { + readonly canCreateMeeting?: boolean; readonly id: string; readonly " $fragmentSpreads": FragmentRefs<"MeetingsPageListFragment">; }; @@ -47,13 +48,26 @@ v2 = { "storageKey": null }, v3 = { + "alias": "canCreateMeeting", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:create" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:create\")" +}, +v4 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v4 = [ +v5 = [ { "kind": "Literal", "name": "first", @@ -84,6 +98,14 @@ return { "plural": false, "selections": [ (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/) + ], + "type": "Organization", + "abstractKey": null + }, { "args": null, "kind": "FragmentSpread", @@ -110,11 +132,12 @@ return { "name": "node", "plural": false, "selections": [ - (v3/*: any*/), + (v4/*: any*/), (v2/*: any*/), { "kind": "InlineFragment", "selections": [ + (v3/*: any*/), { "alias": null, "args": null, @@ -135,7 +158,7 @@ return { }, { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "concreteType": "MeetingConnection", "kind": "LinkedField", "name": "meetings", @@ -191,7 +214,20 @@ return { ], "storageKey": null }, - (v3/*: any*/) + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:delete\")" + }, + (v4/*: any*/) ], "storageKey": null }, @@ -261,7 +297,7 @@ return { }, { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "filters": [ "orderBy" ], @@ -280,16 +316,16 @@ return { ] }, "params": { - "cacheID": "e320d73109abe57bb9fa89ade5fa0892", + "cacheID": "68a020e412baf691d3ce96eff5817190", "id": null, "metadata": {}, "name": "MeetingGraphListQuery", "operationKind": "query", - "text": "query MeetingGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...MeetingsPageListFragment\n }\n}\n\nfragment MeetingsPageListFragment on Organization {\n id\n context {\n summary\n }\n meetings(first: 50, orderBy: {field: DATE, direction: DESC}) {\n edges {\n node {\n id\n ...MeetingsPageRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeetingsPageRowFragment on Meeting {\n id\n name\n date\n attendees {\n id\n fullName\n }\n}\n" + "text": "query MeetingGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n canCreateMeeting: permission(action: \"core:meeting:create\")\n }\n ...MeetingsPageListFragment\n }\n}\n\nfragment MeetingsPageListFragment on Organization {\n id\n context {\n summary\n }\n meetings(first: 50, orderBy: {field: DATE, direction: DESC}) {\n edges {\n node {\n id\n ...MeetingsPageRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeetingsPageRowFragment on Meeting {\n id\n name\n date\n attendees {\n id\n fullName\n }\n canDelete: permission(action: \"core:meeting:delete\")\n}\n" } }; })(); -(node as any).hash = "be13ec2953f71c4955a5f46b93f0131f"; +(node as any).hash = "3ec4cedaa00b3109b79df0425db7f094"; export default node; diff --git a/apps/console/src/__generated__/core/MeetingGraphNodeQuery.graphql.ts b/apps/console/src/__generated__/core/MeetingGraphNodeQuery.graphql.ts index c2afd886f..f04a59156 100644 --- a/apps/console/src/__generated__/core/MeetingGraphNodeQuery.graphql.ts +++ b/apps/console/src/__generated__/core/MeetingGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<75a92b14ee7a1d73c9dde3d9d8d76de6>> * @lightSyntaxTransform * @nogrep */ @@ -118,6 +118,32 @@ return { "name": "minutes", "storageKey": null }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:update\")" + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:delete\")" + }, { "alias": null, "args": null, @@ -147,12 +173,12 @@ return { ] }, "params": { - "cacheID": "a4df3c8c55dc1bf379bba19af16448d7", + "cacheID": "efaa301fdd6d05385d7858d182d4b2ec", "id": null, "metadata": {}, "name": "MeetingGraphNodeQuery", "operationKind": "query", - "text": "query MeetingGraphNodeQuery(\n $meetingId: ID!\n) {\n node(id: $meetingId) {\n __typename\n ...MeetingDetailPageMeetingFragment\n id\n }\n}\n\nfragment MeetingDetailPageMeetingFragment on Meeting {\n id\n name\n date\n minutes\n attendees {\n id\n fullName\n }\n}\n" + "text": "query MeetingGraphNodeQuery(\n $meetingId: ID!\n) {\n node(id: $meetingId) {\n __typename\n ...MeetingDetailPageMeetingFragment\n id\n }\n}\n\nfragment MeetingDetailPageMeetingFragment on Meeting {\n id\n name\n date\n minutes\n canUpdate: permission(action: \"core:meeting:update\")\n canDelete: permission(action: \"core:meeting:delete\")\n attendees {\n id\n fullName\n }\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/core/MeetingsListQuery.graphql.ts b/apps/console/src/__generated__/core/MeetingsListQuery.graphql.ts index bd5930773..442e6f1cb 100644 --- a/apps/console/src/__generated__/core/MeetingsListQuery.graphql.ts +++ b/apps/console/src/__generated__/core/MeetingsListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<829d26690dc2a6f09541f02ef8981395>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -266,6 +266,19 @@ return { ], "storageKey": null }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:delete\")" + }, (v11/*: any*/) ], "storageKey": null @@ -355,12 +368,12 @@ return { ] }, "params": { - "cacheID": "e5e74555d8a277638d2278c5b71b930b", + "cacheID": "f4135654e654bbaa31192269ff567752", "id": null, "metadata": {}, "name": "MeetingsListQuery", "operationKind": "query", - "text": "query MeetingsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: MeetingOrder = {field: DATE, direction: DESC}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MeetingsPageListFragment_16fISc\n id\n }\n}\n\nfragment MeetingsPageListFragment_16fISc on Organization {\n id\n context {\n summary\n }\n meetings(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...MeetingsPageRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeetingsPageRowFragment on Meeting {\n id\n name\n date\n attendees {\n id\n fullName\n }\n}\n" + "text": "query MeetingsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: MeetingOrder = {field: DATE, direction: DESC}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MeetingsPageListFragment_16fISc\n id\n }\n}\n\nfragment MeetingsPageListFragment_16fISc on Organization {\n id\n context {\n summary\n }\n meetings(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...MeetingsPageRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeetingsPageRowFragment on Meeting {\n id\n name\n date\n attendees {\n id\n fullName\n }\n canDelete: permission(action: \"core:meeting:delete\")\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/core/MeetingsPageRowFragment.graphql.ts b/apps/console/src/__generated__/core/MeetingsPageRowFragment.graphql.ts index d1cd7b08e..bc090ebdf 100644 --- a/apps/console/src/__generated__/core/MeetingsPageRowFragment.graphql.ts +++ b/apps/console/src/__generated__/core/MeetingsPageRowFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7efb971ec8052cecbd96f86ea6d3af4c>> + * @generated SignedSource<<2ca6c35e54eafa06ad04d91bfc555eed>> * @lightSyntaxTransform * @nogrep */ @@ -15,6 +15,7 @@ export type MeetingsPageRowFragment$data = { readonly fullName: string; readonly id: string; }>; + readonly canDelete: boolean; readonly date: any; readonly id: string; readonly name: string; @@ -72,6 +73,19 @@ return { } ], "storageKey": null + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:meeting:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:meeting:delete\")" } ], "type": "Meeting", @@ -79,6 +93,6 @@ return { }; })(); -(node as any).hash = "91ba43abb559bed5acdda60b8f61bda9"; +(node as any).hash = "22a8c7132716408a17d39ea5ff36a38d"; export default node; diff --git a/apps/console/src/hooks/graph/MeetingGraph.ts b/apps/console/src/hooks/graph/MeetingGraph.ts index 6c92bf1dc..f18631dd1 100644 --- a/apps/console/src/hooks/graph/MeetingGraph.ts +++ b/apps/console/src/hooks/graph/MeetingGraph.ts @@ -8,6 +8,9 @@ export const meetingsQuery = graphql` query MeetingGraphListQuery($organizationId: ID!) { organization: node(id: $organizationId) { id + ... on Organization { + canCreateMeeting: permission(action: "core:meeting:create") + } ...MeetingsPageListFragment } } diff --git a/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx b/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx index 49cda50ce..156f8bc54 100644 --- a/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx +++ b/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx @@ -21,12 +21,11 @@ import { UpdateMeetingMinutesDialog, type UpdateMeetingMinutesDialogRef, } from "./dialogs/UpdateMeetingMinutesDialog"; -import { useRef, useState, useEffect, use } from "react"; +import { useRef } from "react"; import { meetingNodeQuery, useDeleteMeetingMutation, } from "/hooks/graph/MeetingGraph"; -import { PermissionsContext } from "/providers/PermissionsContext"; const meetingFragment = graphql` fragment MeetingDetailPageMeetingFragment on Meeting { @@ -34,6 +33,8 @@ const meetingFragment = graphql` name date minutes + canUpdate: permission(action: "core:meeting:update") + canDelete: permission(action: "core:meeting:delete") attendees { id fullName @@ -54,70 +55,14 @@ export default function MeetingDetailPage(props: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); const navigate = useNavigate(); - const { isAuthorized } = use(PermissionsContext); const [deleteMeeting, isDeleting] = useDeleteMeetingMutation(); const confirm = useConfirm(); const updateMinutesDialogRef = useRef(null); - const [canUpdate, setCanUpdate] = useState(false); - const [canDelete, setCanDelete] = useState(false); - - useEffect(() => { - if (!organizationId) { - setCanUpdate(false); - setCanDelete(false); - return; - } - - try { - const updateAuth = isAuthorized("Meeting", "updateMeeting"); - setCanUpdate(updateAuth); - } catch (promise) { - if (promise instanceof Promise) { - promise - .then(() => { - try { - const updateAuth = isAuthorized("Meeting", "updateMeeting"); - setCanUpdate(updateAuth); - } catch { - setCanUpdate(false); - } - }) - .catch(() => { - setCanUpdate(false); - }); - } else { - setCanUpdate(false); - } - } - - try { - const deleteAuth = isAuthorized("Meeting", "deleteMeeting"); - setCanDelete(deleteAuth); - } catch (promise) { - if (promise instanceof Promise) { - promise - .then(() => { - try { - const deleteAuth = isAuthorized("Meeting", "deleteMeeting"); - setCanDelete(deleteAuth); - } catch { - setCanDelete(false); - } - }) - .catch(() => { - setCanDelete(false); - }); - } else { - setCanDelete(false); - } - } - }, [organizationId, isAuthorized]); - usePageTitle(meeting.name); - const hasAnyAction = canUpdate || canDelete; + const hasAnyAction = meeting.canUpdate || meeting.canDelete; const handleDelete = () => { confirm( @@ -162,7 +107,7 @@ export default function MeetingDetailPage(props: Props) { /> {hasAnyAction && ( - {canUpdate && ( + {meeting.canUpdate && ( updateMinutesDialogRef.current?.open()} icon={IconPencil} @@ -170,7 +115,7 @@ export default function MeetingDetailPage(props: Props) { {__("Edit minutes")} )} - {canDelete && ( + {meeting.canDelete && ( {__("Summary")} - {isAuthorized("Meeting", "updateMeeting") && ( + {organization.canCreateMeeting && ( @@ -304,6 +301,7 @@ const rowFragment = graphql` id fullName } + canDelete: permission(action: "core:meeting:delete") } `; @@ -321,7 +319,6 @@ function MeetingRow({ const { __ } = useTranslate(); const [deleteMeeting] = useDeleteMeetingMutation(); const confirm = useConfirm(); - const { isAuthorized } = use(PermissionsContext); const handleDelete = () => { confirm( () => @@ -371,7 +368,7 @@ function MeetingRow({ )} - {isAuthorized("Meeting", "deleteMeeting") && ( + {meeting.canDelete && (