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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 104 additions and 19 deletions

View File

@ -116,8 +116,10 @@
<span class="label__title">{{ action.label }}</span>
</label>
<DropdownSelect
:max-visible-options="3"
render-up
:name="`dropdown-${getActionId(action)}`"
:options="action.options"
:options="getVisibleDropdownOptions(action)"
:model-value="getDropdownValue(action)"
:placeholder="'Select an option'"
:disabled="false"
@ -138,7 +140,7 @@
<div class="mb-2 font-semibold">{{ action.label }}</div>
<div class="flex flex-wrap gap-2">
<ButtonStyled
v-for="(option, optIndex) in action.options"
v-for="(option, optIndex) in getVisibleMultiSelectOptions(action)"
:key="`${getActionId(action)}-chip-${optIndex}`"
:color="isChipSelected(action, optIndex) ? 'brand' : 'standard'"
@click="toggleChip(action, optIndex)"
@ -559,14 +561,20 @@ function handleKeybinds(event: KeyboardEvent) {
},
trySelectDropdownOption: (actionIndex: number, optionIndex: number) => {
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<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) {
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]);
}

View File

@ -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 {

View File

@ -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<string> {
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
}

View File

@ -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),