From 058185c7fde55b05f6935e905b9ae2c1e6696660 Mon Sep 17 00:00:00 2001 From: IMB11 Date: Sun, 13 Jul 2025 19:08:55 +0100 Subject: [PATCH 01/13] 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), From c1b95ede077fec79059a69c6a0da58a9625a0be1 Mon Sep 17 00:00:00 2001 From: IMB11 Date: Sun, 13 Jul 2025 21:23:06 +0100 Subject: [PATCH 02/13] fix: checklist conditional message issues + MD formatting (#3989) --- apps/frontend/package.json | 5 +- .../src/assets/styles/components.scss | 14 ++-- apps/frontend/src/assets/styles/global.scss | 24 +++--- .../ui/moderation/NewModerationChecklist.vue | 42 ++++++++-- .../ui/servers/ContentVersionEditModal.vue | 6 +- .../src/composables/servers/servers-fetch.ts | 3 +- apps/frontend/src/pages/[type]/[id].vue | 81 +++++++++---------- apps/frontend/src/pages/app.vue | 3 +- .../src/pages/dashboard/revenue/withdraw.vue | 4 +- apps/frontend/src/pages/plus.vue | 3 +- .../src/pages/servers/manage/[id].vue | 5 +- .../src/pages/servers/manage/[id]/files.vue | 2 +- packages/moderation/types/actions.ts | 15 ++-- packages/moderation/utils.ts | 20 ++++- pnpm-lock.yaml | 38 +++++---- 15 files changed, 160 insertions(+), 105 deletions(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 21cb1752e..7cded8314 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -38,10 +38,10 @@ "@intercom/messenger-js-sdk": "^0.0.14", "@ltd/j-toml": "^1.38.0", "@modrinth/assets": "workspace:*", - "@modrinth/ui": "workspace:*", - "@modrinth/utils": "workspace:*", "@modrinth/blog": "workspace:*", "@modrinth/moderation": "workspace:*", + "@modrinth/ui": "workspace:*", + "@modrinth/utils": "workspace:*", "@pinia/nuxt": "^0.5.1", "@types/three": "^0.172.0", "@vintl/vintl": "^4.4.1", @@ -59,6 +59,7 @@ "markdown-it": "14.1.0", "pathe": "^1.1.2", "pinia": "^2.1.7", + "prettier": "^3.6.2", "qrcode.vue": "^3.4.0", "semver": "^7.5.4", "three": "^0.172.0", diff --git a/apps/frontend/src/assets/styles/components.scss b/apps/frontend/src/assets/styles/components.scss index a1ea3a1e9..f647e7e25 100644 --- a/apps/frontend/src/assets/styles/components.scss +++ b/apps/frontend/src/assets/styles/components.scss @@ -197,13 +197,13 @@ } > :where( - input + *, - .input-group + *, - .textarea-wrapper + *, - .chips + *, - .resizable-textarea-wrapper + *, - .input-div + * - ) { + input + *, + .input-group + *, + .textarea-wrapper + *, + .chips + *, + .resizable-textarea-wrapper + *, + .input-div + * + ) { &:not(:empty) { margin-block-start: var(--spacing-card-md); } diff --git a/apps/frontend/src/assets/styles/global.scss b/apps/frontend/src/assets/styles/global.scss index b52b738ed..cefb9460c 100644 --- a/apps/frontend/src/assets/styles/global.scss +++ b/apps/frontend/src/assets/styles/global.scss @@ -115,10 +115,12 @@ html { --shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 11%, 0.15); --shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2); - --shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15), + --shadow-raised: + 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15), 1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12), 4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09); - --shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, + --shadow-floating: + hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px; --shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px; @@ -150,8 +152,8 @@ html { rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0.2695) 100% ); - --landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16), - inset 2px 2px 64px rgba(255, 255, 255, 0.45); + --landing-blob-shadow: + 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(255, 255, 255, 0.45); --landing-card-bg: rgba(255, 255, 255, 0.8); --landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16); @@ -251,13 +253,15 @@ html { --shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2); --shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1); - --shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, + --shadow-floating: + hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px; --shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px; --landing-maze-bg: url("https://cdn.modrinth.com/landing-new/landing.webp"); - --landing-maze-gradient-bg: linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%), + --landing-maze-gradient-bg: + linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%), url("https://cdn.modrinth.com/landing-new/landing-lower.webp"); --landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%); @@ -284,7 +288,8 @@ html { rgba(44, 48, 79, 0.35) 0%, rgba(32, 35, 50, 0.2695) 100% ); - --landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45); + --landing-blob-shadow: + 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45); --landing-card-bg: rgba(59, 63, 85, 0.15); --landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16); @@ -360,8 +365,9 @@ body { // Defaults background-color: var(--color-bg); color: var(--color-text); - --font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, - Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + --font-standard: + Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, + Fira Sans, Droid Sans, Helvetica Neue, sans-serif; --mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; font-family: var(--font-standard); font-size: 16px; diff --git a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue index d0e81c20c..1b1d0fb6d 100644 --- a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue @@ -382,6 +382,7 @@ import type { import ModpackPermissionsFlow from "./ModpackPermissionsFlow.vue"; import KeybindsModal from "./ChecklistKeybindsModal.vue"; import { finalPermissionMessages } from "@modrinth/moderation/data/modpack-permissions-stage"; +import prettier from "prettier"; const keybindsModal = ref>(); @@ -923,16 +924,28 @@ async function processAction( conditionalAction.messageVariants, selectedActionIds, allValidActionIds, + stageIndex, ); + + let message: string; + let weight: number; + if (matchingVariant) { - const message = (await matchingVariant.message()) as string; - messageParts.push({ - weight: matchingVariant.weight, - content: processMessage(message, action, stageIndex, textInputValues.value), - actionId, - stageIndex, - }); + message = (await matchingVariant.message()) as string; + weight = matchingVariant.weight; + } else if (conditionalAction.fallbackMessage) { + message = (await conditionalAction.fallbackMessage()) as string; + weight = conditionalAction.fallbackWeight ?? 0; + } else { + return; } + + messageParts.push({ + weight, + content: processMessage(message, action, stageIndex, textInputValues.value), + actionId, + stageIndex, + }); } else if (action.type === "dropdown") { const dropdownAction = action as DropdownAction; const selectedIndex = state.value ?? 0; @@ -1077,7 +1090,20 @@ async function generateMessage() { } } - message.value = fullMessage; + try { + const formattedMessage = await prettier.format(fullMessage, { + parser: "markdown", + printWidth: 80, + proseWrap: "always", + tabWidth: 2, + useTabs: false, + }); + message.value = formattedMessage; + } catch (formattingError) { + console.warn("Failed to format markdown, using original:", formattingError); + message.value = fullMessage; + } + generatedMessage.value = true; } catch (error) { console.error("Error generating message:", error); diff --git a/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue b/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue index b142bb814..d204e7aee 100644 --- a/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue +++ b/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue @@ -31,9 +31,9 @@ class="flex cursor-pointer items-center gap-1 bg-transparent p-0" @click=" versionFilter && - (unlockFilterAccordion.isOpen - ? unlockFilterAccordion.close() - : unlockFilterAccordion.open()) + (unlockFilterAccordion.isOpen + ? unlockFilterAccordion.close() + : unlockFilterAccordion.open()) " > ( const response = await $fetch(fullUrl, { method, headers, - body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined, + body: + body && contentType === "application/json" ? JSON.stringify(body) : (body ?? undefined), timeout: 10000, }); diff --git a/apps/frontend/src/pages/[type]/[id].vue b/apps/frontend/src/pages/[type]/[id].vue index 0b51f73bb..7167610ee 100644 --- a/apps/frontend/src/pages/[type]/[id].vue +++ b/apps/frontend/src/pages/[type]/[id].vue @@ -29,12 +29,11 @@ class="settings-header__icon" />
-

- {{ project.title }} -

+

{{ project.title }}

+

Project settings

+
+
@@ -716,25 +719,14 @@ :dropdown-id="`${baseId}-more-options`" >
+

{{ formatMessage(detailsMessages.title) }}

+
+
+
+
+
+
-
- -
+
+