From 058185c7fde55b05f6935e905b9ae2c1e6696660 Mon Sep 17 00:00:00 2001 From: IMB11 Date: Sun, 13 Jul 2025 19:08:55 +0100 Subject: [PATCH] Moderation Checklist Fixes (#3986) * fix: DEV-164 * fix: dev-163 * feat: DEV-162 --- .../ui/moderation/NewModerationChecklist.vue | 88 +++++++++++++++---- packages/moderation/types/actions.ts | 14 +++ packages/moderation/utils.ts | 15 +++- .../ui/src/components/base/DropdownSelect.vue | 6 +- 4 files changed, 104 insertions(+), 19 deletions(-) diff --git a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue index 955b87dd0..d0e81c20c 100644 --- a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue @@ -116,8 +116,10 @@ {{ action.label }} {{ action.label }}
{ const action = visibleActions.value[actionIndex] as DropdownAction; - if (action && action.type === "dropdown" && action.options[optionIndex]) { - selectDropdownOption(action, action.options[optionIndex]); + if (action && action.type === "dropdown") { + const visibleOptions = getVisibleDropdownOptions(action); + if (optionIndex < visibleOptions.length) { + selectDropdownOption(action, visibleOptions[optionIndex]); + } } }, tryToggleChip: (actionIndex: number, chipIndex: number) => { const action = visibleActions.value[actionIndex] as MultiSelectChipsAction; if (action && action.type === "multi-select-chips") { - toggleChip(action, chipIndex); + const visibleOptions = getVisibleMultiSelectOptions(action); + if (chipIndex < visibleOptions.length) { + toggleChip(action, chipIndex); + } } }, @@ -733,13 +741,17 @@ const multiSelectActions = computed(() => function getDropdownValue(action: DropdownAction) { const actionId = getActionId(action); + const visibleOptions = getVisibleDropdownOptions(action); const currentValue = actionStates.value[actionId]?.value ?? action.defaultOption ?? 0; - if (action.options && action.options[currentValue]) { - return action.options[currentValue]; + const allOptions = action.options; + const storedOption = allOptions[currentValue]; + + if (storedOption && visibleOptions.includes(storedOption)) { + return storedOption; } - return action.options?.[0] || null; + return visibleOptions[0] || null; } function isActionSelected(action: Action): boolean { @@ -775,20 +787,31 @@ function selectDropdownOption(action: DropdownAction, selected: any) { function isChipSelected(action: MultiSelectChipsAction, optionIndex: number): boolean { const actionId = getActionId(action); const selectedSet = actionStates.value[actionId]?.value as Set | undefined; - return selectedSet?.has(optionIndex) || false; + + const visibleOptions = getVisibleMultiSelectOptions(action); + const visibleOption = visibleOptions[optionIndex]; + const originalIndex = action.options.findIndex((opt) => opt === visibleOption); + + return selectedSet?.has(originalIndex) || false; } function toggleChip(action: MultiSelectChipsAction, optionIndex: number) { const actionId = getActionId(action); const state = actionStates.value[actionId]; if (state && state.value instanceof Set) { - if (state.value.has(optionIndex)) { - state.value.delete(optionIndex); - } else { - state.value.add(optionIndex); + const visibleOptions = getVisibleMultiSelectOptions(action); + const visibleOption = visibleOptions[optionIndex]; + const originalIndex = action.options.findIndex((opt) => opt === visibleOption); + + if (originalIndex !== -1) { + if (state.value.has(originalIndex)) { + state.value.delete(originalIndex); + } else { + state.value.add(originalIndex); + } + state.selected = state.value.size > 0; + persistState(); } - state.selected = state.value.size > 0; - persistState(); } } @@ -869,9 +892,23 @@ async function processAction( stageIndex: number, messageParts: MessagePart[], ) { + const allValidActionIds: string[] = []; + checklist.forEach((stage, stageIdx) => { + stage.actions.forEach((stageAction, actionIdx) => { + allValidActionIds.push(getActionIdForStage(stageAction, stageIdx, actionIdx)); + if (stageAction.enablesActions) { + stageAction.enablesActions.forEach((enabledAction, enabledIdx) => { + allValidActionIds.push( + getActionIdForStage(enabledAction, stageIdx, actionIdx, enabledIdx), + ); + }); + } + }); + }); + if (action.type === "button" || action.type === "toggle") { const buttonAction = action as ButtonAction | ToggleAction; - const message = await getActionMessage(buttonAction, selectedActionIds); + const message = await getActionMessage(buttonAction, selectedActionIds, allValidActionIds); if (message) { messageParts.push({ weight: buttonAction.weight, @@ -885,6 +922,7 @@ async function processAction( const matchingVariant = findMatchingVariant( conditionalAction.messageVariants, selectedActionIds, + allValidActionIds, ); if (matchingVariant) { const message = (await matchingVariant.message()) as string; @@ -944,6 +982,24 @@ function shouldShowAction(action: Action): boolean { return true; } +function getVisibleDropdownOptions(action: DropdownAction) { + return action.options.filter((option) => { + if (typeof option.shouldShow === "function") { + return option.shouldShow(props.project); + } + return true; + }); +} + +function getVisibleMultiSelectOptions(action: MultiSelectChipsAction) { + return action.options.filter((option) => { + if (typeof option.shouldShow === "function") { + return option.shouldShow(props.project); + } + return true; + }); +} + function shouldShowStageIndex(stageIndex: number): boolean { return shouldShowStage(checklist[stageIndex]); } diff --git a/packages/moderation/types/actions.ts b/packages/moderation/types/actions.ts index b92148fba..46b3dd8ce 100644 --- a/packages/moderation/types/actions.ts +++ b/packages/moderation/types/actions.ts @@ -153,6 +153,13 @@ export interface DropdownActionOption extends WeightedMessage { * The label of the option, which is displayed to the moderator. */ label: string + + /** + * A function that determines whether this option should be shown for a given project. + * + * By default, it returns `true`, meaning the option is always shown. + */ + shouldShow?: (project: Project) => boolean } export interface DropdownAction extends BaseAction { @@ -179,6 +186,13 @@ export interface MultiSelectChipsOption extends WeightedMessage { * The label of the chip, which is displayed to the moderator. */ label: string + + /** + * A function that determines whether this option should be shown for a given project. + * + * By default, it returns `true`, meaning the option is always shown. + */ + shouldShow?: (project: Project) => boolean } export interface MultiSelectChipsAction extends BaseAction { diff --git a/packages/moderation/utils.ts b/packages/moderation/utils.ts index 50f637116..2d43bb88f 100644 --- a/packages/moderation/utils.ts +++ b/packages/moderation/utils.ts @@ -125,13 +125,19 @@ export function processMessage( export function findMatchingVariant( variants: ConditionalMessage[], selectedActionIds: string[], + allValidActionIds?: string[], ): ConditionalMessage | null { for (const variant of variants) { const conditions = variant.conditions const meetsRequired = !conditions.requiredActions || - conditions.requiredActions.every((id) => selectedActionIds.includes(id)) + conditions.requiredActions.every((id) => { + if (allValidActionIds && !allValidActionIds.includes(id)) { + return false + } + return selectedActionIds.includes(id) + }) const meetsExcluded = !conditions.excludedActions || @@ -148,9 +154,14 @@ export function findMatchingVariant( export async function getActionMessage( action: ButtonAction | ToggleAction, selectedActionIds: string[], + allValidActionIds?: string[], ): Promise { if (action.conditionalMessages && action.conditionalMessages.length > 0) { - const matchingConditional = findMatchingVariant(action.conditionalMessages, selectedActionIds) + const matchingConditional = findMatchingVariant( + action.conditionalMessages, + selectedActionIds, + allValidActionIds, + ) if (matchingConditional) { return (await matchingConditional.message()) as string } diff --git a/packages/ui/src/components/base/DropdownSelect.vue b/packages/ui/src/components/base/DropdownSelect.vue index 0642c4b57..c87ef340b 100644 --- a/packages/ui/src/components/base/DropdownSelect.vue +++ b/packages/ui/src/components/base/DropdownSelect.vue @@ -103,6 +103,10 @@ const props = defineProps({ type: Function, default: undefined, }, + maxVisibleOptions: { + type: Number, + default: undefined, + }, }) function getOptionLabel(option) { @@ -263,7 +267,7 @@ const isChildOfDropdown = (element) => { .options { z-index: 10; - max-height: 18.75rem; + max-height: v-bind('maxVisibleOptions ? `calc(${maxVisibleOptions} * 3rem)` : "18.75rem"'); overflow-y: auto; box-shadow: var(--shadow-inset-sm),