diff --git a/client/src/coop-ui/DateInput.tsx b/client/src/coop-ui/DateInput.tsx index 0e46441..9856bbf 100644 --- a/client/src/coop-ui/DateInput.tsx +++ b/client/src/coop-ui/DateInput.tsx @@ -71,24 +71,25 @@ const DateInput: React.FC = ({ value, onChange }) => { const initialDate = useRef(date); - const handleBlur = - (field: keyof DateParts) => - (e: React.FocusEvent): void => { - if (!e.target.value) { - setDate(initialDate.current); - return; - } + const handleBlur = ( + field: keyof DateParts, + e: React.FocusEvent, + ): void => { + if (!e.target.value) { + setDate(initialDate.current); + return; + } - const newValue = Number(e.target.value); - const isValid = validateDate(field, newValue); + const newValue = Number(e.target.value); + const isValid = validateDate(field, newValue); - if (!isValid) { - setDate(initialDate.current); - } else { - // If the new value is valid, update the initial value - initialDate.current = { ...date, [field]: newValue }; - } - }; + if (!isValid) { + setDate(initialDate.current); + } else { + // If the new value is valid, update the initial value + initialDate.current = { ...date, [field]: newValue }; + } + }; const handleKeyDown = (field: keyof DateParts) => (e: React.KeyboardEvent) => { @@ -213,7 +214,7 @@ const DateInput: React.FC = ({ value, onChange }) => { e.target.select(); } }} - onBlur={handleBlur('month')} + onBlur={(e) => handleBlur('month', e)} className="p-0 outline-none w-6 border-none text-center" placeholder="M" /> @@ -231,7 +232,7 @@ const DateInput: React.FC = ({ value, onChange }) => { e.target.select(); } }} - onBlur={handleBlur('day')} + onBlur={(e) => handleBlur('day', e)} className="p-0 outline-none w-7 border-none text-center" placeholder="D" /> @@ -249,7 +250,7 @@ const DateInput: React.FC = ({ value, onChange }) => { e.target.select(); } }} - onBlur={handleBlur('year')} + onBlur={(e) => handleBlur('year', e)} className="p-0 outline-none w-12 border-none text-center" placeholder="YYYY" /> diff --git a/client/src/models/signal.ts b/client/src/models/signal.ts index 31711a5..ec9f57f 100644 --- a/client/src/models/signal.ts +++ b/client/src/models/signal.ts @@ -1,42 +1,12 @@ import { GQLIntegration, GQLScalarType, - GQLSignal, GQLSignalOutputType, GQLSignalType, GQLValueComparator, } from '../graphql/generated'; import { assertUnreachable } from '../utils/misc'; -/** - * Legacy-ish type for the core set of keys that signal keys that much of the - * code currently assumes will be present on fetched signals. - * @deprecated - */ -export type CoreSignal = Pick< - GQLSignal, - | 'id' - | 'type' - | 'name' - | 'description' - | 'disabledInfo' - | 'shouldPromptForMatchingValues' - | 'outputType' - | 'eligibleSubcategories' - | 'eligibleInputs' - | 'subcategory' - | 'integration' - | 'integrationTitle' - | 'integrationLogoUrl' - | 'integrationLogoWithBackgroundUrl' - | 'pricingStructure' - | 'docsUrl' - | 'recommendedThresholds' - | 'supportedLanguages' - | 'args' - | 'allowedInAutomatedRules' ->; - /** Signal type is string to support plugin signal types (e.g. RANDOM_SIGNAL_SELECTION). */ export function receivesRegexInput(type: string) { return ( diff --git a/client/src/utils/time.ts b/client/src/utils/time.ts index 3ce495e..8be1516 100644 --- a/client/src/utils/time.ts +++ b/client/src/utils/time.ts @@ -29,7 +29,7 @@ export function formatDate(date = new Date()): string { } function toDate(date: string | DateString | Date): Date { - return date instanceof Date ? date : new Date(date as string); + return date instanceof Date ? date : new Date(date); } export function parseDatetimeToReadableStringInUTC( @@ -100,7 +100,7 @@ export function getDateRange(start: Date, end: Date, interval: 'HOUR' | 'DAY') { while (isBefore(currentDate, end)) { datesArray.push({ ds: format(currentDate, formatStr), - } as { [key: string]: any }); + }); currentDate = advanceFn(currentDate, 1); } diff --git a/client/src/utils/tree.ts b/client/src/utils/tree.ts index 5e573ce..c8593c6 100644 --- a/client/src/utils/tree.ts +++ b/client/src/utils/tree.ts @@ -248,7 +248,7 @@ export function multilevelListFromFlatList< // Initialize map with all nodes nodeList.forEach((node) => { - map.set(node.id, { ...node } as T & { children?: T[] }); + map.set(node.id, { ...node }); }); // Build the tree diff --git a/client/src/webpages/dashboard/integrations/integrationConfigs.ts b/client/src/webpages/dashboard/integrations/integrationConfigs.ts index 0a25a52..f9781f7 100644 --- a/client/src/webpages/dashboard/integrations/integrationConfigs.ts +++ b/client/src/webpages/dashboard/integrations/integrationConfigs.ts @@ -21,7 +21,7 @@ export type IntegrationConfig = { export const INTEGRATION_CONFIGS: IntegrationConfig[] = [ { - name: 'GOOGLE_CONTENT_SAFETY_API' as GQLIntegration, + name: 'GOOGLE_CONTENT_SAFETY_API', title: 'Google Content Safety API', logo: GoogleLogo, logoWithBackground: GoogleLogoWithBackground, @@ -29,7 +29,7 @@ export const INTEGRATION_CONFIGS: IntegrationConfig[] = [ requiresInfo: true, }, { - name: 'OPEN_AI' as GQLIntegration, + name: 'OPEN_AI', title: 'OpenAI Moderation API', logo: OpenAILogo, logoWithBackground: OpenAILogoWithBackground, @@ -37,7 +37,7 @@ export const INTEGRATION_CONFIGS: IntegrationConfig[] = [ requiresInfo: true, }, { - name: 'ZENTROPI' as GQLIntegration, + name: 'ZENTROPI', title: 'Zentropi', logo: ZentropiLogo, logoWithBackground: ZentropiLogo, diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx index 0582111..954849b 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx @@ -3,12 +3,9 @@ import { Switch } from '@/coop-ui/Switch'; import { useGQLPersonalSafetySettingsQuery } from '@/graphql/generated'; import { LoaderCircle } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; -import * as React from 'react'; import FullScreenLoading from '@/components/common/FullScreenLoading'; -import { type BlurStrength } from './v2/ncmec/NCMECMediaViewer'; - export default function IframeContentDisplayComponent(props: { contentUrl: string; }) { @@ -37,10 +34,8 @@ export default function IframeContentDisplayComponent(props: { const { blur, grayscale, shouldTranslate } = state; const { loading, data } = useGQLPersonalSafetySettingsQuery(); - const { - moderatorSafetyBlurLevel = 2 as BlurStrength, - moderatorSafetyGrayscale = true, - } = data?.me?.interfacePreferences ?? {}; + const { moderatorSafetyBlurLevel = 2, moderatorSafetyGrayscale = true } = + data?.me?.interfacePreferences ?? {}; useEffect(() => { setState({ 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 2a13188..7f528b2 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx @@ -1027,7 +1027,7 @@ function ManualReviewJobReviewImpl(props: { (a) => !('type' in a) && a.id === actionId, ); if (!found || 'type' in found || !('parameters' in found)) return []; - return (found.parameters ?? []) as ReadonlyArray; + return found.parameters ?? []; }; // Adds an action to `selectedPrimaryActions` while preserving the existing @@ -1039,7 +1039,7 @@ function ManualReviewJobReviewImpl(props: { customMrtApiParamDecisionPayload?: Record, ) => { const newAction: ManualReviewJobEnqueuedPrimaryActionData = { - action: action as ManualReviewJobEnqueuedPrimaryActionData['action'], + action, target: reportedItemTarget(), policies: selectedPrimaryPolicies, ...(customMrtApiParamDecisionPayload @@ -1291,9 +1291,7 @@ function ManualReviewJobReviewImpl(props: { actionName: action.name, parameters, initialValues: - (current?.customMrtApiParamDecisionPayload as - | ActionParameterValues - | undefined) ?? {}, + current?.customMrtApiParamDecisionPayload ?? {}, }); }} > diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx index 0639f9f..314a6fc 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx @@ -250,9 +250,14 @@ function TableRowComponent(props: { return ; } - // Extract matched banks if available - const matchedBanks = (value as any)?.matchedBanks; - const hasMatches = Array.isArray(matchedBanks) && matchedBanks.length > 0; + // Extract matched banks if available, normalizing to a string[] so the + // render path below always has a concrete array to map over. + const rawMatchedBanks = (value as { matchedBanks?: string[] }) + .matchedBanks; + const matchedBanks = Array.isArray(rawMatchedBanks) + ? rawMatchedBanks + : []; + const hasMatches = matchedBanks.length > 0; return (
@@ -275,7 +280,7 @@ function TableRowComponent(props: { {label ?
{label}
: null} {hasMatches && (
- {matchedBanks.map((bankName: string) => ( + {matchedBanks.map((bankName) => ( ({ value: mapValue[key], diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/useEnqueueActionGate.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/useEnqueueActionGate.tsx index 6bbe706..da916c2 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/useEnqueueActionGate.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/useEnqueueActionGate.tsx @@ -1,12 +1,11 @@ +import { type GQLActionParameter } from '@/graphql/generated'; +import groupBy from 'lodash/groupBy'; +import { useCallback, useState, type ReactNode } from 'react'; + +import { type ActionParameterValues } from '@/components/ActionParameterInputs'; import ActionParametersModal, { defaultValuesForParameters, } from '@/components/ActionParametersModal'; -import { - type ActionParameterValues, -} from '@/components/ActionParameterInputs'; -import { type GQLActionParameter } from '@/graphql/generated'; -import groupBy from 'lodash/groupBy'; -import { type ReactNode, useCallback, useState } from 'react'; import { type ManualReviewJobEnqueuedActionData } from '../ManualReviewJobReview'; @@ -117,9 +116,8 @@ export function useEnqueueActionGate(args: { const parameters = actionMeta?.parameters ?? []; if (parameters.length === 0) return; const initialValues = - (entry.customMrtApiParamDecisionPayload as - | ActionParameterValues - | undefined) ?? defaultValuesForParameters(parameters); + entry.customMrtApiParamDecisionPayload ?? + defaultValuesForParameters(parameters); const match = (it: ManualReviewJobEnqueuedActionData) => it.action.id === entry.action.id && it.target.identifier.itemId === entry.target.identifier.itemId && diff --git a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRule.tsx b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRule.tsx index 0c57abd..2f6a71e 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRule.tsx +++ b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRule.tsx @@ -10,7 +10,7 @@ import { Input, Tooltip } from 'antd'; import React, { useState } from 'react'; import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; -import { CoreSignal } from '../../../../models/signal'; +import { GQLSignal } from '../../../../graphql/generated'; import { getInvalidRegexesInCondition, isConditionComplete, @@ -100,7 +100,7 @@ export default function ManualReviewQueueRoutingRule(props: { isEditing: boolean; setRuleEditingState: (isEditing: boolean) => void; itemTypes: readonly RoutingRuleItemType[]; - signals: readonly CoreSignal[]; + signals: readonly GQLSignal[]; queues: readonly RoutingRuleQueue[]; isReordering: boolean; isLoading: boolean; diff --git a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRuleForm.tsx b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRuleForm.tsx index ffa1778..d81b98d 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRuleForm.tsx +++ b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRuleForm.tsx @@ -4,8 +4,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { selectFilterByLabelOption } from '../../components/antDesignUtils'; -import { GQLConditionConjunction } from '../../../../graphql/generated'; -import { CoreSignal } from '../../../../models/signal'; +import { GQLConditionConjunction, GQLSignal } from '../../../../graphql/generated'; import { hasNestedConditionSets, removeConditionSet, @@ -33,7 +32,7 @@ const { Option } = Select; export default function ManualReviewQueueRoutingRuleForm(props: { rule: EditableRoutingRule; itemTypes: readonly RoutingRuleItemType[]; - signals: readonly CoreSignal[]; + signals: readonly GQLSignal[]; queues: readonly RoutingRuleQueue[]; editing: boolean; addSelectedItemTypeId: (itemTypeId: string) => void; @@ -152,7 +151,7 @@ export default function ManualReviewQueueRoutingRuleForm(props: { const renderConditionSet = (opts: { conditionSet: RuleFormConditionSet; conditionSetIndex: number; - signals: readonly CoreSignal[]; + signals: readonly GQLSignal[]; parentConditionSet?: RuleFormConditionSet; }) => { const { conditionSet, conditionSetIndex, signals, parentConditionSet } = opts; diff --git a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx index 657a630..673f192 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx +++ b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx @@ -1,8 +1,7 @@ import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons'; import { Button, Select, Tooltip } from 'antd'; -import { GQLConditionConjunction } from '../../../../graphql/generated'; -import { CoreSignal } from '../../../../models/signal'; +import { GQLConditionConjunction, GQLSignal } from '../../../../graphql/generated'; import { hasNestedConditionSets, removeCondition, @@ -88,7 +87,7 @@ export default function ManualReviewQueueRuleFormCondition(props: { parentConditionSet: RuleFormConditionSet; eligibleInputs: Map; selectedItemTypes: RoutingRuleItemType[]; - allSignals: readonly CoreSignal[]; + allSignals: readonly GQLSignal[]; editing: boolean; onUpdateConditionSet: (conditionSet: RuleFormConditionSet) => void; }) { @@ -183,7 +182,7 @@ export default function ManualReviewQueueRuleFormCondition(props: { condition={condition} location={location} editing={editing} - onUpdateSignal={(signal: CoreSignal, subcategory?: string) => { + onUpdateSignal={(signal: GQLSignal, subcategory?: string) => { // Do this manually instead of having a helper function to avoid // setting the state multiple times in the case of the signal having a subcategory let newConditionSet = updateConditionComponent( diff --git a/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx b/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx index 0b1d51c..69a73b4 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx +++ b/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx @@ -2,7 +2,7 @@ import { DownOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import { useState } from 'react'; -import { CoreSignal } from '../../../../../../models/signal'; +import { GQLSignal } from '@/graphql/generated'; import RuleFormSignalModal from '../../../../rules/rule_form/signal_modal/RuleFormSignalModal'; import { ConditionLocation, @@ -15,13 +15,13 @@ export default function ManualReviewQueueRuleConditionSignal(props: { condition: RuleFormLeafCondition; location: ConditionLocation; editing: boolean; - onUpdateSignal: (signal: CoreSignal, subcategory?: string) => void; + onUpdateSignal: (signal: GQLSignal, subcategory?: string) => void; }) { const { condition, location, editing, onUpdateSignal } = props; const eligibleSignals = condition.eligibleSignals; const [modalInfo, setModalInfo] = useState<{ visible: boolean; - initialSelectedSignal: CoreSignal | undefined; + initialSelectedSignal: GQLSignal | undefined; }>({ visible: false, initialSelectedSignal: undefined, diff --git a/client/src/webpages/dashboard/mrt/queue_routing/types.ts b/client/src/webpages/dashboard/mrt/queue_routing/types.ts index 1533bdf..f7cc430 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/types.ts +++ b/client/src/webpages/dashboard/mrt/queue_routing/types.ts @@ -1,8 +1,7 @@ import { - GQLConditionSetFieldsFragment, GQLManualReviewQueueRoutingRulesQuery, + GQLSignal, } from '../../../../graphql/generated'; -import { CoreSignal } from '../../../../models/signal'; import { getTypedConditionSetFromGQL } from '../../rules/rule_form/RuleFormUtils'; import { RuleFormConditionSet } from '../../rules/types'; @@ -32,14 +31,14 @@ export function editableRoutingRuleFromRoutingRule( rule: RoutingRule, index: number, selectedItemTypeIds: string[], - allSignals: readonly CoreSignal[], + allSignals: readonly GQLSignal[], ) { return { id: rule.id, name: rule.name, destinationQueue: rule.destinationQueue, conditionSet: getTypedConditionSetFromGQL( - rule.conditionSet as GQLConditionSetFieldsFragment, + rule.conditionSet, rule.itemTypes.filter((it) => selectedItemTypeIds.includes(it.id)), allSignals, ), diff --git a/client/src/webpages/dashboard/mrt/queue_routing/utils.test.ts b/client/src/webpages/dashboard/mrt/queue_routing/utils.test.ts new file mode 100644 index 0000000..19927ed --- /dev/null +++ b/client/src/webpages/dashboard/mrt/queue_routing/utils.test.ts @@ -0,0 +1,126 @@ +import { vi } from 'vitest'; + +import { + GQLConditionConjunction, + GQLScalarType, + GQLSignal, + GQLSignalPricingStructureType, + GQLSignalType, +} from '../../../../graphql/generated'; +import { + getConditionInputScalarType, + getEligibleSignalsForInput, + SimplifiedConditionInput, +} from '../../rules/rule_form/RuleFormUtils'; +import { RuleFormConditionSet, RuleFormLeafCondition } from '../../rules/types'; +import { updateConditionInput } from './utils'; + +vi.mock('../../rules/rule_form/RuleFormUtils', async () => { + const origin = await vi.importActual< + typeof import('../../rules/rule_form/RuleFormUtils') + >('../../rules/rule_form/RuleFormUtils'); + return { + ...origin, + getConditionInputScalarType: vi.fn(), + getEligibleSignalsForInput: vi.fn(), + }; +}); + +const makeSignal = (id: string, type: GQLSignalType): GQLSignal => ({ + __typename: 'Signal', + id, + type, + shouldPromptForMatchingValues: false, + eligibleSubcategories: [], + eligibleInputs: [], + name: `signal-${id}`, + description: 'Some description', + disabledInfo: { __typename: 'DisabledInfo', disabled: false }, + outputType: { + __typename: 'ScalarSignalOutputType', + scalarType: GQLScalarType.Number, + }, + pricingStructure: { + __typename: 'SignalPricingStructure', + type: GQLSignalPricingStructureType.Free, + }, + supportedLanguages: { __typename: 'AllLanguages', _: true }, + allowedInAutomatedRules: true, +}); + +describe('updateConditionInput signal eligibility', () => { + const oldInput: SimplifiedConditionInput = { + type: 'CONTENT_FIELD', + name: 'old_field', + contentTypeId: '12345', + }; + const newInput: SimplifiedConditionInput = { + type: 'CONTENT_FIELD', + name: 'new_field', + contentTypeId: '12345', + }; + const location = { conditionIndex: 0, conditionSetIndex: 0 }; + + beforeEach(() => { + vi.clearAllMocks(); + // Keep the input scalar type stable so the input-type-change reset path + // (which clears the whole condition) is not what's being exercised here. + vi.mocked(getConditionInputScalarType).mockReturnValue( + GQLScalarType.Number, + ); + }); + + it('clears a stale custom signal that is no longer eligible, even when other custom signals remain', () => { + // All custom signals share GQLSignalType.Custom, so a type-based check would + // incorrectly keep custom1 selected. The ID-based check must clear it. + const custom1 = makeSignal('custom-1', GQLSignalType.Custom); + const custom2 = makeSignal('custom-2', GQLSignalType.Custom); + const custom3 = makeSignal('custom-3', GQLSignalType.Custom); + + vi.mocked(getEligibleSignalsForInput).mockReturnValue([custom2, custom3]); + + const conditionSet: RuleFormConditionSet = { + conjunction: GQLConditionConjunction.And, + conditions: [ + { input: oldInput, signal: custom1, eligibleSignals: [custom1] }, + ], + }; + + const result = updateConditionInput({ + currentConditionSet: conditionSet, + location, + input: newInput, + selectedItemTypes: [], + allSignals: [custom1, custom2, custom3], + }); + + const updatedLeaf = result.conditions[0] as RuleFormLeafCondition; + expect(updatedLeaf.signal).toBeUndefined(); + expect(updatedLeaf.input).toEqual(newInput); + }); + + it('keeps the selected signal when it is still eligible for the new input', () => { + const custom2 = makeSignal('custom-2', GQLSignalType.Custom); + const custom3 = makeSignal('custom-3', GQLSignalType.Custom); + + vi.mocked(getEligibleSignalsForInput).mockReturnValue([custom2, custom3]); + + const conditionSet: RuleFormConditionSet = { + conjunction: GQLConditionConjunction.And, + conditions: [ + { input: oldInput, signal: custom2, eligibleSignals: [custom2] }, + ], + }; + + const result = updateConditionInput({ + currentConditionSet: conditionSet, + location, + input: newInput, + selectedItemTypes: [], + allSignals: [custom2, custom3], + }); + + const updatedLeaf = result.conditions[0] as RuleFormLeafCondition; + expect(updatedLeaf.signal).toEqual(custom2); + }); +}); diff --git a/client/src/webpages/dashboard/mrt/queue_routing/utils.ts b/client/src/webpages/dashboard/mrt/queue_routing/utils.ts index a46f752..6c91be0 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/utils.ts +++ b/client/src/webpages/dashboard/mrt/queue_routing/utils.ts @@ -5,10 +5,10 @@ import uniqBy from 'lodash/uniqBy'; import { GQLConditionConjunction, GQLScalarType, + GQLSignal, GQLSignalType, GQLValueComparator, } from '../../../../graphql/generated'; -import { CoreSignal } from '../../../../models/signal'; import { getDerivedFieldOutputType } from '../../rules/rule_form/condition/input/derivedField'; import { getConditionInputScalarType, @@ -37,7 +37,7 @@ import { RoutingRuleItemType } from './types'; */ export function getNewEligibleInputs( selectedItemTypes: readonly RoutingRuleItemType[], - allSignals: readonly CoreSignal[], + allSignals: readonly GQLSignal[], ) { const allBaseFields = selectedItemTypes.flatMap((it) => it.baseFields); const allDerivedFields = selectedItemTypes.flatMap((it) => it.derivedFields); @@ -155,7 +155,7 @@ export function updateConditionInput(params: { location: ConditionLocation; input: SimplifiedConditionInput; selectedItemTypes: RoutingRuleItemType[]; - allSignals: readonly CoreSignal[]; + allSignals: readonly GQLSignal[]; }) { const { currentConditionSet, @@ -225,12 +225,11 @@ export function updateConditionInput(params: { // If the previously selected signal on this condition is no // longer compatible with the newly selected input, clear it out, // and clear out all subsequent fields in the condition + const currentSignal = newConditions[conditionIndex].signal; if ( - newConditions[conditionIndex].signal != null && - // Need to compare IDs instead of objects - !allNewSignals - .map((s) => s.type) - .includes(newConditions[conditionIndex].signal!.type) + currentSignal != null && + // Compare by ID: type-based comparison fails for custom signals, which all share GQLSignalType.Custom + !allNewSignals.some((s) => s.id === currentSignal.id) ) { // Clear out all other fields on the Condition newConditions[conditionIndex] = { diff --git a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx index 6d8d935..c8c3fcd 100644 --- a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx +++ b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx @@ -719,7 +719,8 @@ export default function ManualReviewDashboardInsightsChart(props: { ...allDatesArray, ]; - const groupedData = formattedDataWithAllDates.reduce((result, item) => { + type ChartRow = Record; + const groupedData = formattedDataWithAllDates.reduce>((result, item) => { const ds = item.ds; if (!(ds in result)) { diff --git a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsFilterBy.tsx b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsFilterBy.tsx index d56cee2..dd1acd6 100644 --- a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsFilterBy.tsx +++ b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsFilterBy.tsx @@ -90,7 +90,7 @@ export function groupByColumnToFilterByColumns( groupBy: ManualReviewDashboardInsightsGroupByColumns[], ): FilterByColumnName[] { return groupBy.flatMap((groupByColumn) => { - return (() => { + return ((): FilterByColumnName[] => { switch (groupByColumn) { case GQLDecisionCountGroupByColumns.PolicyId: case GQLJobCreationGroupByColumns.PolicyId: @@ -106,8 +106,10 @@ export function groupByColumnToFilterByColumns( return ['itemTypeIds']; case GQLJobCreationGroupByColumns.Source: return ['sources', 'ruleIds']; + default: + return []; } - })() as FilterByColumnName[]; + })(); }); } @@ -231,8 +233,8 @@ export default function ManualReviewDashboardInsightsFilterBy(props: { id === 'IGNORE' ? 'IGNORE' : id === 'SUBMIT_NCMEC_REPORT' - ? 'SUBMIT_NCMEC_REPORT' - : undefined, + ? 'SUBMIT_NCMEC_REPORT' + : undefined, ), ), actionIds: filterNullOrUndefined( @@ -240,8 +242,8 @@ export default function ManualReviewDashboardInsightsFilterBy(props: { id === 'IGNORE' ? undefined : id === 'SUBMIT_NCMEC_REPORT' - ? undefined - : id, + ? undefined + : id, ), ), }); @@ -360,17 +362,19 @@ export default function ManualReviewDashboardInsightsFilterBy(props: { 'sources' in unsavedFilterValues ? unsavedFilterValues[column as GQLJobCreationFilterByColumnName] : 'actionIds' in unsavedFilterValues - ? [ - ...(unsavedFilterValues[ - column as GQLDecisionCountFilterByColumnName - ] ?? []), - ...(unsavedFilterValues.type ?? []), - ] - : metric === 'REVIEWED_JOBS' && 'reviewerIds' in unsavedFilterValues - ? unsavedFilterValues[column as GQLJobCountFilterByColumnName] - : metric === 'SKIPPED_JOBS' && 'reviewerIds' in unsavedFilterValues - ? unsavedFilterValues[column as GQLSkippedJobCountFilterByColumnName] - : []; + ? [ + ...(unsavedFilterValues[ + column as GQLDecisionCountFilterByColumnName + ] ?? []), + ...(unsavedFilterValues.type ?? []), + ] + : metric === 'REVIEWED_JOBS' && 'reviewerIds' in unsavedFilterValues + ? unsavedFilterValues[column as GQLJobCountFilterByColumnName] + : metric === 'SKIPPED_JOBS' && 'reviewerIds' in unsavedFilterValues + ? unsavedFilterValues[ + column as GQLSkippedJobCountFilterByColumnName + ] + : []; return (