diff --git a/client/src/graphql/generated.ts b/client/src/graphql/generated.ts index 1e468e28..36472d22 100644 --- a/client/src/graphql/generated.ts +++ b/client/src/graphql/generated.ts @@ -4890,6 +4890,7 @@ export const GQLUserPermission = { EditMrtQueues: 'EDIT_MRT_QUEUES', ManageOrg: 'MANAGE_ORG', ManagePolicies: 'MANAGE_POLICIES', + ManageRoles: 'MANAGE_ROLES', ManuallyActionContent: 'MANUALLY_ACTION_CONTENT', MutateLiveRules: 'MUTATE_LIVE_RULES', MutateNonLiveRules: 'MUTATE_NON_LIVE_RULES', @@ -9468,7 +9469,7 @@ export type GQLAppealSettingsQuery = { } | null; readonly me?: { readonly __typename: 'User'; - readonly role?: GQLUserRole | null; + readonly permissions: ReadonlyArray; } | null; }; @@ -12267,7 +12268,7 @@ export type GQLManualReviewJobInfoQuery = { readonly me?: { readonly __typename: 'User'; readonly id: string; - readonly role?: GQLUserRole | null; + readonly permissions: ReadonlyArray; readonly reviewableQueues: ReadonlyArray<{ readonly __typename: 'ManualReviewQueue'; readonly id: string; @@ -31524,7 +31525,7 @@ export const GQLAppealSettingsDocument = gql` appealsCallbackBody } me { - role + permissions } } `; @@ -33762,7 +33763,7 @@ export const GQLManualReviewJobInfoDocument = gql` ...JobFields } } - role + permissions } } ${GQLItemTypeFragmentFragmentDoc} diff --git a/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx b/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx index 39bb37bb..9fa74296 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx @@ -1,3 +1,4 @@ +import { userHasPermissions } from '@/routing/permissions'; import { gql } from '@apollo/client'; import { Input, notification } from 'antd'; import Link from 'antd/lib/typography/Link'; @@ -10,6 +11,7 @@ import FormHeader from '../components/FormHeader'; import FormSectionHeader from '../components/FormSectionHeader'; import { + GQLUserPermission, useGQLAppealSettingsQuery, useGQLUpdateAppealSettingsMutation, } from '../../../graphql/generated'; @@ -23,7 +25,7 @@ gql` appealsCallbackBody } me { - role + permissions } } @@ -87,6 +89,10 @@ export default function ManualReviewAppealSettings() { return ; } + const canManageOrg = userHasPermissions(data?.me?.permissions, [ + GQLUserPermission.ManageOrg, + ]); + const onUpdateAppealSettings = async () => updateAppealSettings({ variables: { @@ -169,15 +175,15 @@ export default function ManualReviewAppealSettings() { { - if (data?.me?.role !== 'ADMIN') { - return "To edit these settings, ask your organization's admin to upgrade your role to Admin."; + if (!canManageOrg) { + return 'You do not have permission to edit appeal settings. Ask your organization administrator for access.'; } if (!appealsCallbackUrl) { return 'The Callback URL is required.'; diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx index 50957421..9960da8a 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx @@ -1,28 +1,25 @@ import Sidebar1 from '@/icons/lni/Design/sidebar-1.svg?react'; import AngleDoubleRight from '@/icons/lni/Direction/angle-double-right.svg?react'; +import { userHasPermissions } from '@/routing/permissions'; import { __throw } from '@/utils/misc'; import { isNonEmptyString } from '@/utils/string'; import { multilevelListFromFlatList } from '@/utils/tree'; -import { - DownOutlined, - EditOutlined, - LoadingOutlined, -} from '@ant-design/icons'; +import { DownOutlined, EditOutlined, LoadingOutlined } from '@ant-design/icons'; import { gql } from '@apollo/client'; import { Button, Dropdown, Input, Select, Tooltip } from 'antd'; import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { useNavigate, useParams } from 'react-router-dom'; -import ActionParametersModal, { - defaultValuesForParameters, -} from '@/components/ActionParametersModal'; -import { type ActionParameterValues } from '@/components/ActionParameterInputs'; import ComponentLoading from '../../../../components/common/ComponentLoading'; import CopyTextComponent from '../../../../components/common/CopyTextComponent'; import CoopModal from '../../components/CoopModal'; import { CoopModalFooterButtonProps } from '../../components/CoopModalFooter'; import PolicyDropdown from '../../components/PolicyDropdown'; +import { type ActionParameterValues } from '@/components/ActionParameterInputs'; +import ActionParametersModal, { + defaultValuesForParameters, +} from '@/components/ActionParametersModal'; import Drawer from '@/components/common/Drawer'; import { @@ -38,6 +35,7 @@ import { GQLThreadAppealManualReviewJobPayload, GQLUserManualReviewJobPayload, GQLUserPenaltySeverity, + GQLUserPermission, useGQLDequeueManualReviewJobMutation, useGQLLogSkipMutation, useGQLManualReviewJobInfoQuery, @@ -51,8 +49,8 @@ import { filterNullOrUndefined } from '../../../../utils/collections'; import { getFieldValueForRole } from '../../../../utils/itemUtils'; import { recomputeSelectedRelatedActions } from '../../../../utils/manualReviewTool'; import HTMLRenderer from '../../policies/HTMLRenderer'; -import { JOB_FRAGMENT } from './jobFragment'; import { ITEM_TYPE_FRAGMENT } from '../../rules/rule_form/RuleForm'; +import { JOB_FRAGMENT } from './jobFragment'; import ManualReviewJobDequeueErrorComponent from './ManualReviewJobDequeueErrorComponent'; import MergedReportsComponent from './MergedReportsComponent'; import ReportInfoComponent from './ReportInfoComponent'; @@ -66,8 +64,8 @@ import { } from './v2/ManualReviewJobRelatedActionsStore'; import NCMECReviewUser from './v2/ncmec/NCMECReviewUser'; import ManualReviewJobEnqueuedRelatedActions from './v2/related_actions/ManualReviewJobEnqueuedRelatedActions'; -import { useEnqueueActionGate } from './v2/useEnqueueActionGate'; import ManualReviewJobListOfThreadsComponent from './v2/threads/ManualReviewJobListOfThreadsComponent'; +import { useEnqueueActionGate } from './v2/useEnqueueActionGate'; import ManualReviewJobPrimaryUserComponent from './v2/user/ManualReviewJobPrimaryUserComponent'; const { Option } = Select; @@ -76,7 +74,9 @@ const { TextArea } = Input; // Narrows the GraphQL union of action types to "this one declares parameter // inputs". Only `CustomAction` carries `parameters`; everything else is // treated as no-parameter. -function actionHasParameters(action: { __typename?: string } | undefined): boolean { +function actionHasParameters( + action: { __typename?: string } | undefined, +): boolean { if (!action || !('parameters' in action)) return false; const params = (action as { parameters?: readonly unknown[] | null }) .parameters; @@ -161,7 +161,7 @@ gql` ...JobFields } } - role + permissions } } @@ -630,16 +630,16 @@ function ManualReviewJobReviewImpl(props: { const job = closedJob ? closedJob : jobData - ? jobData.dequeueManualReviewJob?.job - : data?.me?.reviewableQueues - .find((queue) => queue.id === queueId) - ?.jobs.find((job) => job.id === jobId); + ? jobData.dequeueManualReviewJob?.job + : data?.me?.reviewableQueues + .find((queue) => queue.id === queueId) + ?.jobs.find((job) => job.id === jobId); const pendingJobCount = jobData?.dequeueManualReviewJob ? jobData.dequeueManualReviewJob.numPendingJobs : data?.me?.reviewableQueues - ? data?.me?.reviewableQueues.find((queue) => queue.id === queueId) - ?.pendingJobCount - : undefined; + ? data?.me?.reviewableQueues.find((queue) => queue.id === queueId) + ?.pendingJobCount + : undefined; const [logSkip] = useGQLLogSkipMutation({ // This is safe because we check it before calling logSkip @@ -712,14 +712,17 @@ function ManualReviewJobReviewImpl(props: { if (!closedJob && !queue) { throw Error(`Queue not found for ID ${queueId}`); } - const userIsAdmin = data.me?.role === 'ADMIN'; + const userCanBypassSkipRestriction = userHasPermissions( + data.me?.permissions, + [GQLUserPermission.ManageOrg], + ); const filteredActions = org.actions.filter( ({ id }) => !queue?.hiddenActionIds?.includes(id), ); const { payload, policyIds } = job; const reportHistory = - 'reportHistory' in job.payload ? job.payload.reportHistory ?? [] : []; + 'reportHistory' in job.payload ? (job.payload.reportHistory ?? []) : []; const modal = ( ) ?? [] + ? ((payload.itemThreadContentItems as ReadonlyArray) ?? + []) : []; const policiesFromIds = (policyIds: readonly string[]) => policyIds.map((policyId) => { @@ -860,17 +864,18 @@ function ManualReviewJobReviewImpl(props: { // Builds the standard target descriptor for the reported item. Hoisted so // both the direct-add path and the modal Save handler use the same shape. - const reportedItemTarget = (): ManualReviewJobEnqueuedPrimaryActionData['target'] => ({ - identifier: { - itemId: reportedItem.id, - itemTypeId: reportedItem.type.id, - }, - displayName: - getFieldValueForRole( - reportedItem, - 'displayName', - ) ?? reportedItem.id, - }); + const reportedItemTarget = + (): ManualReviewJobEnqueuedPrimaryActionData['target'] => ({ + identifier: { + itemId: reportedItem.id, + itemTypeId: reportedItem.type.id, + }, + displayName: + getFieldValueForRole( + reportedItem, + 'displayName', + ) ?? reportedItem.id, + }); // Returns the parameter spec for a CustomAction by id, or `[]` for // built-ins / actions without a spec. Looks up against `decisionActions` @@ -939,7 +944,8 @@ function ManualReviewJobReviewImpl(props: { className="sticky flex flex-col border border-gray-200 border-solid rounded-md shrink-0" data-testid="manual-review-decision-action-list" > - {decisionActions.map((action) => { + {decisionActions + .map((action) => { const { key, selected, label } = (() => { if ('type' in action) { return { @@ -1201,7 +1207,8 @@ function ManualReviewJobReviewImpl(props: { ); const skipToNextJobButton = - org.hideSkipButtonForNonAdmins && !userIsAdmin ? undefined : ( + org.hideSkipButtonForNonAdmins && + !userCanBypassSkipRestriction ? undefined : (