Moderation Checklist Fixes (#3986)

* fix: DEV-164

* fix: dev-163

* feat: DEV-162
This commit is contained in:
IMB11
2025-07-13 19:08:55 +01:00
committed by GitHub
parent 6fb125cf0f
commit 058185c7fd
4 changed files with 104 additions and 19 deletions

View File

@@ -116,8 +116,10 @@
<span class="label__title">{{ action.label }}</span> <span class="label__title">{{ action.label }}</span>
</label> </label>
<DropdownSelect <DropdownSelect
:max-visible-options="3"
render-up
:name="`dropdown-${getActionId(action)}`" :name="`dropdown-${getActionId(action)}`"
:options="action.options" :options="getVisibleDropdownOptions(action)"
:model-value="getDropdownValue(action)" :model-value="getDropdownValue(action)"
:placeholder="'Select an option'" :placeholder="'Select an option'"
:disabled="false" :disabled="false"
@@ -138,7 +140,7 @@
<div class="mb-2 font-semibold">{{ action.label }}</div> <div class="mb-2 font-semibold">{{ action.label }}</div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<ButtonStyled <ButtonStyled
v-for="(option, optIndex) in action.options" v-for="(option, optIndex) in getVisibleMultiSelectOptions(action)"
:key="`${getActionId(action)}-chip-${optIndex}`" :key="`${getActionId(action)}-chip-${optIndex}`"
:color="isChipSelected(action, optIndex) ? 'brand' : 'standard'" :color="isChipSelected(action, optIndex) ? 'brand' : 'standard'"
@click="toggleChip(action, optIndex)" @click="toggleChip(action, optIndex)"
@@ -559,14 +561,20 @@ function handleKeybinds(event: KeyboardEvent) {
}, },
trySelectDropdownOption: (actionIndex: number, optionIndex: number) => { trySelectDropdownOption: (actionIndex: number, optionIndex: number) => {
const action = visibleActions.value[actionIndex] as DropdownAction; const action = visibleActions.value[actionIndex] as DropdownAction;
if (action && action.type === "dropdown" && action.options[optionIndex]) { if (action && action.type === "dropdown") {
selectDropdownOption(action, action.options[optionIndex]); const visibleOptions = getVisibleDropdownOptions(action);
if (optionIndex < visibleOptions.length) {
selectDropdownOption(action, visibleOptions[optionIndex]);
}
} }
}, },
tryToggleChip: (actionIndex: number, chipIndex: number) => { tryToggleChip: (actionIndex: number, chipIndex: number) => {
const action = visibleActions.value[actionIndex] as MultiSelectChipsAction; const action = visibleActions.value[actionIndex] as MultiSelectChipsAction;
if (action && action.type === "multi-select-chips") { 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) { function getDropdownValue(action: DropdownAction) {
const actionId = getActionId(action); const actionId = getActionId(action);
const visibleOptions = getVisibleDropdownOptions(action);
const currentValue = actionStates.value[actionId]?.value ?? action.defaultOption ?? 0; const currentValue = actionStates.value[actionId]?.value ?? action.defaultOption ?? 0;
if (action.options && action.options[currentValue]) { const allOptions = action.options;
return action.options[currentValue]; 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 { function isActionSelected(action: Action): boolean {
@@ -775,20 +787,31 @@ function selectDropdownOption(action: DropdownAction, selected: any) {
function isChipSelected(action: MultiSelectChipsAction, optionIndex: number): boolean { function isChipSelected(action: MultiSelectChipsAction, optionIndex: number): boolean {
const actionId = getActionId(action); const actionId = getActionId(action);
const selectedSet = actionStates.value[actionId]?.value as Set<number> | undefined; const selectedSet = actionStates.value[actionId]?.value as Set<number> | 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) { function toggleChip(action: MultiSelectChipsAction, optionIndex: number) {
const actionId = getActionId(action); const actionId = getActionId(action);
const state = actionStates.value[actionId]; const state = actionStates.value[actionId];
if (state && state.value instanceof Set) { if (state && state.value instanceof Set) {
if (state.value.has(optionIndex)) { const visibleOptions = getVisibleMultiSelectOptions(action);
state.value.delete(optionIndex); const visibleOption = visibleOptions[optionIndex];
} else { const originalIndex = action.options.findIndex((opt) => opt === visibleOption);
state.value.add(optionIndex);
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, stageIndex: number,
messageParts: MessagePart[], 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") { if (action.type === "button" || action.type === "toggle") {
const buttonAction = action as ButtonAction | ToggleAction; const buttonAction = action as ButtonAction | ToggleAction;
const message = await getActionMessage(buttonAction, selectedActionIds); const message = await getActionMessage(buttonAction, selectedActionIds, allValidActionIds);
if (message) { if (message) {
messageParts.push({ messageParts.push({
weight: buttonAction.weight, weight: buttonAction.weight,
@@ -885,6 +922,7 @@ async function processAction(
const matchingVariant = findMatchingVariant( const matchingVariant = findMatchingVariant(
conditionalAction.messageVariants, conditionalAction.messageVariants,
selectedActionIds, selectedActionIds,
allValidActionIds,
); );
if (matchingVariant) { if (matchingVariant) {
const message = (await matchingVariant.message()) as string; const message = (await matchingVariant.message()) as string;
@@ -944,6 +982,24 @@ function shouldShowAction(action: Action): boolean {
return true; 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 { function shouldShowStageIndex(stageIndex: number): boolean {
return shouldShowStage(checklist[stageIndex]); return shouldShowStage(checklist[stageIndex]);
} }

View File

@@ -153,6 +153,13 @@ export interface DropdownActionOption extends WeightedMessage {
* The label of the option, which is displayed to the moderator. * The label of the option, which is displayed to the moderator.
*/ */
label: string 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 { 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. * The label of the chip, which is displayed to the moderator.
*/ */
label: string 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 { export interface MultiSelectChipsAction extends BaseAction {

View File

@@ -125,13 +125,19 @@ export function processMessage(
export function findMatchingVariant( export function findMatchingVariant(
variants: ConditionalMessage[], variants: ConditionalMessage[],
selectedActionIds: string[], selectedActionIds: string[],
allValidActionIds?: string[],
): ConditionalMessage | null { ): ConditionalMessage | null {
for (const variant of variants) { for (const variant of variants) {
const conditions = variant.conditions const conditions = variant.conditions
const meetsRequired = const meetsRequired =
!conditions.requiredActions || !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 = const meetsExcluded =
!conditions.excludedActions || !conditions.excludedActions ||
@@ -148,9 +154,14 @@ export function findMatchingVariant(
export async function getActionMessage( export async function getActionMessage(
action: ButtonAction | ToggleAction, action: ButtonAction | ToggleAction,
selectedActionIds: string[], selectedActionIds: string[],
allValidActionIds?: string[],
): Promise<string> { ): Promise<string> {
if (action.conditionalMessages && action.conditionalMessages.length > 0) { if (action.conditionalMessages && action.conditionalMessages.length > 0) {
const matchingConditional = findMatchingVariant(action.conditionalMessages, selectedActionIds) const matchingConditional = findMatchingVariant(
action.conditionalMessages,
selectedActionIds,
allValidActionIds,
)
if (matchingConditional) { if (matchingConditional) {
return (await matchingConditional.message()) as string return (await matchingConditional.message()) as string
} }

View File

@@ -103,6 +103,10 @@ const props = defineProps({
type: Function, type: Function,
default: undefined, default: undefined,
}, },
maxVisibleOptions: {
type: Number,
default: undefined,
},
}) })
function getOptionLabel(option) { function getOptionLabel(option) {
@@ -263,7 +267,7 @@ const isChildOfDropdown = (element) => {
.options { .options {
z-index: 10; z-index: 10;
max-height: 18.75rem; max-height: v-bind('maxVisibleOptions ? `calc(${maxVisibleOptions} * 3rem)` : "18.75rem"');
overflow-y: auto; overflow-y: auto;
box-shadow: box-shadow:
var(--shadow-inset-sm), var(--shadow-inset-sm),