From e8e74678df3399fec859f6f2a13134a83c70fad6 Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Fri, 25 Jul 2025 15:12:10 +0100 Subject: [PATCH 01/22] feat: Moderation Dashboard Overhaul --- .../moderation/ModerationDelphiReportCard.vue | 179 +++++++ .../ui/moderation/ModerationQueueCard.vue | 195 ++++++++ .../ui/moderation/ModerationReportCard.vue | 267 +++++++++++ .../checklist/ChecklistKeybindsModal.vue | 116 +++++ .../ModerationChecklist.vue} | 147 +++--- .../ModpackPermissionsFlow.vue | 0 apps/frontend/src/pages/[type]/[id].vue | 31 +- apps/frontend/src/pages/moderation.vue | 97 +++- apps/frontend/src/pages/moderation/index.vue | 335 +++++++++++-- .../src/pages/moderation/report/[id].vue | 17 - .../frontend/src/pages/moderation/reports.vue | 444 +++++++++++++++++- apps/frontend/src/pages/moderation/review.vue | 304 ------------ .../moderation/technical-review-mockup.vue | 386 +++++++++++++++ .../src/pages/moderation/technical-review.vue | 3 + apps/frontend/src/store/moderation.ts | 98 ++++ .../moderation/data/report-quick-replies.ts | 3 + packages/moderation/index.ts | 4 +- packages/moderation/types/reports.ts | 27 ++ packages/utils/types.ts | 89 ++++ 19 files changed, 2263 insertions(+), 479 deletions(-) create mode 100644 apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue create mode 100644 apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue create mode 100644 apps/frontend/src/components/ui/moderation/ModerationReportCard.vue create mode 100644 apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue rename apps/frontend/src/components/ui/moderation/{NewModerationChecklist.vue => checklist/ModerationChecklist.vue} (93%) rename apps/frontend/src/components/ui/moderation/{ => checklist}/ModpackPermissionsFlow.vue (100%) delete mode 100644 apps/frontend/src/pages/moderation/report/[id].vue delete mode 100644 apps/frontend/src/pages/moderation/review.vue create mode 100644 apps/frontend/src/pages/moderation/technical-review-mockup.vue create mode 100644 apps/frontend/src/pages/moderation/technical-review.vue create mode 100644 apps/frontend/src/store/moderation.ts create mode 100644 packages/moderation/data/report-quick-replies.ts create mode 100644 packages/moderation/types/reports.ts diff --git a/apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue new file mode 100644 index 0000000000..961f340a34 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/ModerationDelphiReportCard.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue b/apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue new file mode 100644 index 0000000000..8dcb0f8f5d --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/ModerationQueueCard.vue @@ -0,0 +1,195 @@ + + + diff --git a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue new file mode 100644 index 0000000000..0e006db50e --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue @@ -0,0 +1,267 @@ + + + + diff --git a/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue b/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue new file mode 100644 index 0000000000..787474bc51 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue similarity index 93% rename from apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue rename to apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index de6d5443bf..44f928dbdf 100644 --- a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -42,9 +42,9 @@

You are done moderating this project! - diff --git a/apps/frontend/src/components/ui/thread/ThreadMessage.vue b/apps/frontend/src/components/ui/thread/ThreadMessage.vue index 9d962c98ed..f47b066137 100644 --- a/apps/frontend/src/components/ui/thread/ThreadMessage.vue +++ b/apps/frontend/src/components/ui/thread/ThreadMessage.vue @@ -36,7 +36,7 @@ v-tooltip="'Modrinth Team'" /> diff --git a/apps/frontend/src/layouts/default.vue b/apps/frontend/src/layouts/default.vue index 308b7e3a9b..f0c6b43572 100644 --- a/apps/frontend/src/layouts/default.vue +++ b/apps/frontend/src/layouts/default.vue @@ -295,7 +295,7 @@ { id: 'review-projects', color: 'orange', - link: '/moderation/review', + link: '/moderation/', }, { id: 'review-reports', diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts index 249a53b604..4bd1244079 100644 --- a/packages/assets/generated-icons.ts +++ b/packages/assets/generated-icons.ts @@ -38,6 +38,7 @@ import _CodeIcon from './icons/code.svg?component' import _CoffeeIcon from './icons/coffee.svg?component' import _CogIcon from './icons/cog.svg?component' import _CoinsIcon from './icons/coins.svg?component' +import _CollapseIcon from './icons/collapse.svg?component' import _CollectionIcon from './icons/collection.svg?component' import _CompassIcon from './icons/compass.svg?component' import _ContractIcon from './icons/contract.svg?component' @@ -229,6 +230,7 @@ export const CodeIcon = _CodeIcon export const CoffeeIcon = _CoffeeIcon export const CogIcon = _CogIcon export const CoinsIcon = _CoinsIcon +export const CollapseIcon = _CollapseIcon export const CollectionIcon = _CollectionIcon export const CompassIcon = _CompassIcon export const ContractIcon = _ContractIcon diff --git a/packages/assets/icons/collapse.svg b/packages/assets/icons/collapse.svg new file mode 100644 index 0000000000..49723c697a --- /dev/null +++ b/packages/assets/icons/collapse.svg @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/packages/ui/src/components/base/CollapsibleRegion.vue b/packages/ui/src/components/base/CollapsibleRegion.vue new file mode 100644 index 0000000000..80a7728799 --- /dev/null +++ b/packages/ui/src/components/base/CollapsibleRegion.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index e1217fc590..85b7f2da0b 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -10,6 +10,7 @@ export { default as Card } from './base/Card.vue' export { default as Checkbox } from './base/Checkbox.vue' export { default as Chips } from './base/Chips.vue' export { default as Collapsible } from './base/Collapsible.vue' +export { default as CollapsibleRegion } from './base/CollapsibleRegion.vue' export { default as ContentPageHeader } from './base/ContentPageHeader.vue' export { default as CopyCode } from './base/CopyCode.vue' export { default as DoubleIcon } from './base/DoubleIcon.vue' From 581485d60386f1d95c94d52a6a9a26bc66c2d908 Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Fri, 25 Jul 2025 15:43:00 +0100 Subject: [PATCH 04/22] fix: report layout --- .../ui/moderation/ModerationReportCard.vue | 3 ++- .../src/components/ui/thread/ReportThread.vue | 23 ++++++++++++++++--- packages/assets/generated-icons.ts | 2 ++ packages/assets/icons/ellipsis-vertical.svg | 1 + packages/utils/types.ts | 2 +- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 packages/assets/icons/ellipsis-vertical.svg diff --git a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue index 0e006db50e..67bbd727fc 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue @@ -121,12 +121,13 @@

-
+
diff --git a/apps/frontend/src/components/ui/thread/ReportThread.vue b/apps/frontend/src/components/ui/thread/ReportThread.vue index 4eb8e50f26..82b919d948 100644 --- a/apps/frontend/src/components/ui/thread/ReportThread.vue +++ b/apps/frontend/src/components/ui/thread/ReportThread.vue @@ -112,7 +112,7 @@ + + diff --git a/apps/frontend/src/pages/moderation/reports.vue b/apps/frontend/src/pages/moderation/reports/index.vue similarity index 53% rename from apps/frontend/src/pages/moderation/reports.vue rename to apps/frontend/src/pages/moderation/reports/index.vue index a3c161bae1..9444f75fa2 100644 --- a/apps/frontend/src/pages/moderation/reports.vue +++ b/apps/frontend/src/pages/moderation/reports/index.vue @@ -58,11 +58,8 @@
- -
+
+
@@ -76,19 +73,11 @@ import { DropdownSelect, Button, Pagination } from "@modrinth/ui"; import { XIcon, SearchIcon, SortAscIcon, SortDescIcon, FilterIcon } from "@modrinth/assets"; import { defineMessages, useVIntl } from "@vintl/vintl"; import { useLocalStorage } from "@vueuse/core"; -import type { - Project, - Report, - Thread, - User, - Version, - TeamMember, - Organization, -} from "@modrinth/utils"; +import type { Report } from "@modrinth/utils"; import Fuse from "fuse.js"; -import type { OwnershipTarget, ExtendedReport } from "@modrinth/moderation"; +import type { ExtendedReport } from "@modrinth/moderation"; import ReportCard from "~/components/ui/moderation/ModerationReportCard.vue"; -import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts"; +import { enrichReportBatch } from "~/helpers/moderation"; const { formatMessage } = useVIntl(); const route = useRoute(); @@ -109,190 +98,46 @@ const messages = defineMessages({ }, }); -const { data: allReports } = await useAsyncData("moderation-reports", async () => { - const reports = (await useBaseFetch("report?all=true&count=10000", { - apiVersion: 3, - })) as Report[]; - - const threadIDs = reports.map((report) => report.thread_id).filter(Boolean); - const threads = (await fetchSegmented( - threadIDs, - (ids) => `threads?ids=${asEncodedJsonArray(ids)}`, - )) as Thread[]; - - const userIDs = reports - .filter((report) => report.item_type === "user") - .map((report) => report.item_id); - const versionIDs = reports - .filter((report) => report.item_type === "version") - .map((report) => report.item_id); - const projectIDs = reports - .filter((report) => report.item_type === "project") - .map((report) => report.item_id); - - const versions = (await fetchSegmented( - versionIDs, - (ids) => `versions?ids=${asEncodedJsonArray(ids)}`, - )) as Version[]; - - const fullProjectIds = new Set([ - ...projectIDs, - ...versions.map((v) => v.project_id).filter(Boolean), - ]); - - const projects = (await fetchSegmented( - Array.from(fullProjectIds), - (ids) => `projects?ids=${asEncodedJsonArray(ids)}`, - )) as Project[]; - - const teamIds = [...new Set(projects.map((p) => p.team).filter(Boolean))]; - const orgIds = [...new Set(projects.map((p) => p.organization).filter(Boolean))]; - - const [teamsData, orgsData]: [TeamMember[][], Organization[]] = await Promise.all([ - teamIds.length > 0 - ? fetchSegmented(teamIds, (ids) => `teams?ids=${asEncodedJsonArray(ids)}`) - : Promise.resolve([]), - orgIds.length > 0 - ? fetchSegmented(orgIds, (ids) => `organizations?ids=${asEncodedJsonArray(ids)}`, { - apiVersion: 3, - }) - : Promise.resolve([]), - ]); - - const orgTeamIds = orgsData.map((org) => org.team_id).filter(Boolean); - const orgTeamsData: TeamMember[][] = - orgTeamIds.length > 0 - ? await fetchSegmented(orgTeamIds, (ids) => `teams?ids=${asEncodedJsonArray(ids)}`) - : []; - - const ownerUserIds = new Set(); - - teamsData.flat().forEach((member) => { - if (member.role === "Owner") { - ownerUserIds.add(member.user.id); - } - }); - - orgTeamsData.flat().forEach((member) => { - if (member.role === "Owner") { - ownerUserIds.add(member.user.id); - } - }); +const { data: allReports } = await useLazyAsyncData("new-moderation-reports", async () => { + const startTime = performance.now(); + let currentOffset = 0; + const REPORT_ENDPOINT_COUNT = 350; + const allReports: ExtendedReport[] = []; - const fullUserIds = new Set([ - ...userIDs, - ...reports.map((report) => report.reporter), - ...ownerUserIds, - ]); - - const users = (await fetchSegmented( - Array.from(fullUserIds), - (ids) => `users?ids=${asEncodedJsonArray(ids)}`, - )) as User[]; - - const teamMap = new Map(); - const orgMap = new Map(); - - teamsData.forEach((team) => { - let teamId = null; - for (const member of team) { - teamId = member.team_id; - if (!teamMap.has(teamId)) { - teamMap.set(teamId, team); - break; - } - } - }); + const enrichmentPromises: Promise[] = []; - orgTeamsData.forEach((team) => { - let teamId = null; - for (const member of team) { - teamId = member.team_id; - if (!teamMap.has(teamId)) { - teamMap.set(teamId, team); - break; - } - } - }); + while (true) { + const reports = (await useBaseFetch( + `report?count=${REPORT_ENDPOINT_COUNT}&offset=${currentOffset}`, + { apiVersion: 3 }, + )) as Report[]; - orgsData.forEach((org: Organization) => { - orgMap.set(org.id, org); - }); + if (reports.length === 0) break; - const extendedReports: ExtendedReport[] = reports.map((report) => { - const thread = threads.find((t) => t.id === report.thread_id) || ({} as Thread); - const version = - report.item_type === "version" - ? versions.find((v: { id: string }) => v.id === report.item_id) - : undefined; - - const project = - report.item_type === "project" - ? projects.find((p: { id: string }) => p.id === report.item_id) - : report.item_type === "version" && version - ? projects.find((p: { id: string }) => p.id === version.project_id) - : undefined; - - let target: OwnershipTarget | undefined; - - if (report.item_type === "user") { - const targetUser = users.find((u: { id: string }) => u.id === report.item_id); - if (targetUser) { - target = { - name: targetUser.username, - slug: targetUser.username, - avatar_url: targetUser.avatar_url, - type: "user", - }; - } - } else if (project) { - let owner: TeamMember | null = null; - let org: Organization | null = null; - - if (project.team) { - const teamMembers = teamMap.get(project.team); - if (teamMembers) { - owner = teamMembers.find((member) => member.role === "Owner") || null; - } - } + const enrichmentPromise = enrichReportBatch(reports); + enrichmentPromises.push(enrichmentPromise); - if (project.organization) { - org = orgMap.get(project.organization) || null; - } + currentOffset += reports.length; - // Prioritize organization over individual owner - if (org) { - target = { - name: org.name, - avatar_url: org.icon_url, - type: "organization", - slug: org.slug, - }; - } else if (owner) { - target = { - name: owner.user.username, - avatar_url: owner.user.avatar_url, - type: "user", - slug: owner.user.username, - }; - } + if (enrichmentPromises.length >= 3) { + const completed = await Promise.all(enrichmentPromises.splice(0, 2)); + allReports.push(...completed.flat()); } - return { - ...report, - thread, - reporter_user: users.find((user) => user.id === report.reporter) || ({} as User), - project, - user: - report.item_type === "user" - ? users.find((u: { id: string }) => u.id === report.item_id) - : undefined, - version, - target, - }; - }); + if (reports.length < REPORT_ENDPOINT_COUNT) break; + } - return extendedReports; + const remainingBatches = await Promise.all(enrichmentPromises); + allReports.push(...remainingBatches.flat()); + + const endTime = performance.now(); + const duration = endTime - startTime; + + console.debug( + `Reports fetched and processed in ${duration.toFixed(2)}ms (${(duration / 1000).toFixed(2)}s)`, + ); + + return allReports; }); const query = ref(route.query.q?.toString() || ""); @@ -384,8 +229,12 @@ const filteredReports = computed(() => { if (currentFilterType.value !== "All") { filtered = filtered.filter((report) => { - const messages = report.thread?.messages ?? []; - if (messages.length === 0) return false; + const messages = [...report.thread?.messages]; + + if (messages.length === 0) { + return currentFilterType.value === "Unread"; + } + const lastMessage = messages[messages.length - 1]; if (currentFilterType.value === "Read") { return ( From 90aceaaba3148b7f1c3331e58b0c0c774d6bf597 Mon Sep 17 00:00:00 2001 From: IMB11 Date: Sun, 27 Jul 2025 19:50:12 +0100 Subject: [PATCH 09/22] feat: memoize filtering --- .../src/pages/moderation/reports/index.vue | 103 +++++++++--------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/apps/frontend/src/pages/moderation/reports/index.vue b/apps/frontend/src/pages/moderation/reports/index.vue index 9444f75fa2..5b027fe940 100644 --- a/apps/frontend/src/pages/moderation/reports/index.vue +++ b/apps/frontend/src/pages/moderation/reports/index.vue @@ -10,7 +10,7 @@ spellcheck="false" type="text" :placeholder="formatMessage(messages.searchPlaceholder)" - @input="updateSearchResults()" + @input="goToPage(1)" />
+
-
@@ -103,15 +101,16 @@ import { } from "@modrinth/assets"; import { defineMessages, useVIntl } from "@vintl/vintl"; import { useLocalStorage } from "@vueuse/core"; -import type { Project, TeamMember, Organization } from "@modrinth/utils"; import ConfettiExplosion from "vue-confetti-explosion"; import Fuse from "fuse.js"; import ModerationQueueCard from "~/components/ui/moderation/ModerationQueueCard.vue"; -import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts"; import { useModerationStore } from "~/store/moderation.ts"; +import { enrichProjectBatch, type ModerationProject } from "~/helpers/moderation"; const { formatMessage } = useVIntl(); const moderationStore = useModerationStore(); +const route = useRoute(); +const router = useRouter(); const visible = ref(false); if (import.meta.client && history && history.state && history.state.confetti) { @@ -144,72 +143,79 @@ const messages = defineMessages({ }, }); -interface ModerationProject { - project: Project; - owner: TeamMember | null; - org: Organization | null; -} +const { data: allProjects } = await useLazyAsyncData("moderation-projects", async () => { + const startTime = performance.now(); + let currentOffset = 0; + const PROJECT_ENDPOINT_COUNT = 350; + const allProjects: ModerationProject[] = []; -const { data: allProjects } = await useAsyncData("moderation-projects", async () => { - const projects = (await useBaseFetch("moderation/projects?count=10000")) as Project[]; - - const teamIds = [...new Set(projects.map((p) => p.team).filter(Boolean))]; - const orgIds = [...new Set(projects.map((p) => p.organization).filter(Boolean))]; - - const [teamsData, orgsData]: [TeamMember[][], Organization[]] = await Promise.all([ - teamIds.length > 0 - ? fetchSegmented(teamIds, (ids) => `teams?ids=${asEncodedJsonArray(ids)}`) - : Promise.resolve([]), - orgIds.length > 0 - ? fetchSegmented(orgIds, (ids) => `organizations?ids=${asEncodedJsonArray(ids)}`, { - apiVersion: 3, - }) - : Promise.resolve([]), - ]); - - const teamMap = new Map(); - const orgMap = new Map(); - - teamsData.forEach((team) => { - let teamId = null; - for (const member of team) { - teamId = member.team_id; - if (!teamMap.has(teamId)) { - teamMap.set(teamId, team); - break; - } - } - }); + const enrichmentPromises: Promise[] = []; - orgsData.forEach((org: Organization) => { - orgMap.set(org.id, org); - }); + while (true) { + const projects = (await useBaseFetch( + `moderation/projects?count=${PROJECT_ENDPOINT_COUNT}&offset=${currentOffset}`, + { internal: true }, + )) as any[]; - return projects.map((project) => { - let owner: TeamMember | null = null; - let org: Organization | null = null; + if (projects.length === 0) break; - if (project.team) { - const teamMembers = teamMap.get(project.team); - if (teamMembers) { - owner = teamMembers.find((member) => member.role === "Owner") || null; - } - } + const enrichmentPromise = enrichProjectBatch(projects); + enrichmentPromises.push(enrichmentPromise); - if (project.organization) { - org = orgMap.get(project.organization) || null; + currentOffset += projects.length; + + if (enrichmentPromises.length >= 3) { + const completed = await Promise.all(enrichmentPromises.splice(0, 2)); + allProjects.push(...completed.flat()); } - return { - project, - owner, - org, - } as ModerationProject; - }); + if (projects.length < PROJECT_ENDPOINT_COUNT) break; + } + + const remainingBatches = await Promise.all(enrichmentPromises); + allProjects.push(...remainingBatches.flat()); + + const endTime = performance.now(); + const duration = endTime - startTime; + + console.debug( + `Projects fetched and processed in ${duration.toFixed(2)}ms (${(duration / 1000).toFixed(2)}s)`, + ); + + return allProjects; }); -const query = useLocalStorage("moderation-query", ""); -const currentFilterType = useLocalStorage("moderation-current-filter-type", () => "All"); +const query = ref(route.query.q?.toString() || ""); + +watch( + query, + (newQuery) => { + const currentQuery = { ...route.query }; + if (newQuery) { + currentQuery.q = newQuery; + } else { + delete currentQuery.q; + } + + router.replace({ + path: route.path, + query: currentQuery, + }); + }, + { immediate: false }, +); + +watch( + () => route.query.q, + (newQueryParam) => { + const newValue = newQueryParam?.toString() || ""; + if (query.value !== newValue) { + query.value = newValue; + } + }, +); + +const currentFilterType = useLocalStorage("moderation-current-filter-type", () => "All projects"); const filterTypes: readonly string[] = readonly([ "All projects", "Modpacks", @@ -222,6 +228,7 @@ const filterTypes: readonly string[] = readonly([ const currentSortType = useLocalStorage("moderation-current-sort-type", () => "Oldest"); const sortTypes: readonly string[] = readonly(["Oldest", "Newest"]); + const currentPage = ref(1); const itemsPerPage = 15; const totalPages = computed(() => Math.ceil((filteredProjects.value?.length || 0) / itemsPerPage)); @@ -229,39 +236,64 @@ const totalPages = computed(() => Math.ceil((filteredProjects.value?.length || 0 const fuse = computed(() => { if (!allProjects.value || allProjects.value.length === 0) return null; return new Fuse(allProjects.value, { - keys: ["title", "description", "project_type", "slug"], + keys: [ + { + name: "project.title", + weight: 3, + }, + { + name: "project.slug", + weight: 2, + }, + { + name: "project.description", + weight: 2, + }, + { + name: "project.project_type", + weight: 1, + }, + "owner.user.username", + "org.name", + "org.slug", + ], includeScore: true, threshold: 0.4, }); }); -const filteredProjects = computed(() => { +const searchResults = computed(() => { + if (!query.value || !fuse.value) return null; + return fuse.value.search(query.value).map((result) => result.item); +}); + +const baseFiltered = computed(() => { if (!allProjects.value) return []; + return query.value && searchResults.value ? searchResults.value : [...allProjects.value]; +}); - let filtered; +const typeFiltered = computed(() => { + if (currentFilterType.value === "All projects") return baseFiltered.value; - if (query.value && fuse.value) { - const results = fuse.value.search(query.value); - filtered = results.map((result) => result.item); - } else { - filtered = [...allProjects.value]; - } + const filterMap: Record = { + Modpacks: "modpack", + Mods: "mod", + "Resource Packs": "resourcepack", + "Data Packs": "datapack", + Plugins: "plugin", + Shaders: "shader", + }; - if (currentFilterType.value !== "All projects") { - const filterMap: Record = { - Modpacks: "modpack", - Mods: "mod", - "Resource Packs": "resourcepack", - "Data Packs": "datapack", - Plugins: "plugin", - Shaders: "shader", - }; - - const projectType = filterMap[currentFilterType.value]; - if (projectType) { - filtered = filtered.filter((queueItem) => queueItem.project.project_type === projectType); - } - } + const projectType = filterMap[currentFilterType.value]; + if (!projectType) return baseFiltered.value; + + return baseFiltered.value.filter((queueItem) => + queueItem.project.project_types.includes(projectType), + ); +}); + +const filteredProjects = computed(() => { + const filtered = [...typeFiltered.value]; if (currentSortType.value === "Oldest") { filtered.sort((a, b) => { @@ -287,8 +319,8 @@ const paginatedProjects = computed(() => { return filteredProjects.value.slice(start, end); }); -function updateSearchResults() { - currentPage.value = 1; +function goToPage(page: number) { + currentPage.value = page; } function moderateAllInFilter() { @@ -304,8 +336,4 @@ function moderateAllInFilter() { }, }); } - -function goToPage(page: number) { - currentPage.value = page; -} diff --git a/packages/ui/src/components/base/DropdownSelect.vue b/packages/ui/src/components/base/DropdownSelect.vue index c87ef340b9..9843eb152f 100644 --- a/packages/ui/src/components/base/DropdownSelect.vue +++ b/packages/ui/src/components/base/DropdownSelect.vue @@ -163,7 +163,6 @@ const onFocus = () => { } const onBlur = (event) => { - console.log(event) if (!isChildOfDropdown(event.relatedTarget)) { dropdownVisible.value = false } diff --git a/packages/utils/types.ts b/packages/utils/types.ts index 207bd79dea..61f553a207 100644 --- a/packages/utils/types.ts +++ b/packages/utils/types.ts @@ -18,7 +18,7 @@ export type DonationPlatform = | { short: 'ko-fi'; name: 'Ko-fi' } | { short: 'other'; name: 'Other' } -export type ProjectType = 'mod' | 'modpack' | 'resourcepack' | 'shader' +export type ProjectType = 'mod' | 'modpack' | 'resourcepack' | 'shader' | 'plugin' | 'datapack' export type MonetizationStatus = 'monetized' | 'demonetized' | 'force-demonetized' export type GameVersion = string @@ -65,7 +65,8 @@ export interface Project { client_side: Environment server_side: Environment - team: ModrinthId + team?: ModrinthId + team_id: ModrinthId thread_id: ModrinthId organization: ModrinthId @@ -76,6 +77,7 @@ export interface Project { donation_urls: DonationLink[] published: string + created?: string updated: string approved: string queued: string From 873dd7c69ea586cc5ac2ee7b790e37ac21cdffec Mon Sep 17 00:00:00 2001 From: IMB11 Date: Sun, 27 Jul 2025 20:40:13 +0100 Subject: [PATCH 11/22] fix: lint issues --- apps/frontend/src/pages/moderation/index.vue | 4 ++-- apps/frontend/src/pages/moderation/reports/index.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/pages/moderation/index.vue b/apps/frontend/src/pages/moderation/index.vue index 3e112ce121..5b2de54f93 100644 --- a/apps/frontend/src/pages/moderation/index.vue +++ b/apps/frontend/src/pages/moderation/index.vue @@ -75,10 +75,10 @@
diff --git a/apps/frontend/src/pages/moderation/reports/index.vue b/apps/frontend/src/pages/moderation/reports/index.vue index 5b027fe940..e98b4e0ee7 100644 --- a/apps/frontend/src/pages/moderation/reports/index.vue +++ b/apps/frontend/src/pages/moderation/reports/index.vue @@ -59,7 +59,7 @@
- +
From 21150395ef761cae12d02d4e4a6df3ed5bfc7c65 Mon Sep 17 00:00:00 2001 From: IMB11 Date: Sun, 27 Jul 2025 21:11:30 +0100 Subject: [PATCH 12/22] feat: impl quick reply functionality --- .../ui/moderation/ModerationReportCard.vue | 53 ++++--------------- .../src/components/ui/thread/ReportThread.vue | 7 +++ apps/frontend/src/pages/moderation/index.vue | 2 +- .../src/pages/moderation/reports/[id].vue | 2 +- .../src/pages/moderation/reports/index.vue | 4 +- .../data/messages/reports/antivirus.md | 3 ++ .../messages/reports/confirmed-malware.md | 3 ++ .../data/messages/reports/gameplay-issue.md | 6 +++ .../data/messages/reports/platform-issue.md | 5 ++ .../moderation/data/messages/reports/spam.md | 3 ++ .../moderation/data/messages/reports/stale.md | 3 ++ .../moderation/data/report-quick-replies.ts | 36 ++++++++++--- packages/moderation/types/reports.ts | 2 +- .../src/components/base/CollapsibleRegion.vue | 10 ++++ 14 files changed, 85 insertions(+), 54 deletions(-) create mode 100644 packages/moderation/data/messages/reports/antivirus.md create mode 100644 packages/moderation/data/messages/reports/confirmed-malware.md create mode 100644 packages/moderation/data/messages/reports/gameplay-issue.md create mode 100644 packages/moderation/data/messages/reports/platform-issue.md create mode 100644 packages/moderation/data/messages/reports/spam.md create mode 100644 packages/moderation/data/messages/reports/stale.md diff --git a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue index fb2844e8e3..499b1d381a 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue @@ -120,9 +120,10 @@
- + (); +const reportThread = ref | null>(null); +const collapsibleRegion = ref | null>(null); + const formatRelativeTime = useRelativeTime(); function updateThread(newThread: any) { @@ -217,49 +221,12 @@ const visibleQuickReplies = computed(() => { }); async function handleQuickReply(reply: ReportQuickReply) { - if (!props.report.thread) { - addNotification({ - title: "Error", - text: "No thread available for this report", - type: "error", - }); - return; - } - - try { - await useBaseFetch(`thread/${props.report.thread.id}`, { - method: "POST", - body: { - body: { - type: "text", - body: reply.message, - private: reply.private || false, - }, - }, - }); + const message = + typeof reply.message === "function" ? await reply.message(props.report) : reply.message; - const threadId = props.report.thread_id; - if (threadId) { - try { - const updatedThread = (await useBaseFetch(`thread/${threadId}`)) as any; - updateThread(updatedThread); - } catch (error) { - console.error("Failed to update thread:", error); - } - } - - addNotification({ - title: "Reply sent", - text: "Quick reply has been sent successfully", - type: "success", - }); - } catch (err: any) { - addNotification({ - title: "Error sending quick reply", - text: err.data ? err.data.description : err, - type: "error", - }); - } + collapsibleRegion.value?.setCollapsed(false); + await nextTick(); + reportThread.value?.setReplyContent(message); } const reportItemAvatarUrl = computed(() => { diff --git a/apps/frontend/src/components/ui/thread/ReportThread.vue b/apps/frontend/src/components/ui/thread/ReportThread.vue index 8d1eb82d9b..83875029d1 100644 --- a/apps/frontend/src/components/ui/thread/ReportThread.vue +++ b/apps/frontend/src/components/ui/thread/ReportThread.vue @@ -143,6 +143,13 @@ const members = computed(() => { }); const replyBody = ref(""); +function setReplyContent(content: string) { + replyBody.value = content; +} + +defineExpose({ + setReplyContent, +}); const sortedMessages = computed(() => { const messages: TypeThreadMessage[] = [ diff --git a/apps/frontend/src/pages/moderation/index.vue b/apps/frontend/src/pages/moderation/index.vue index 5b2de54f93..c344d7cbfd 100644 --- a/apps/frontend/src/pages/moderation/index.vue +++ b/apps/frontend/src/pages/moderation/index.vue @@ -105,7 +105,7 @@ import ConfettiExplosion from "vue-confetti-explosion"; import Fuse from "fuse.js"; import ModerationQueueCard from "~/components/ui/moderation/ModerationQueueCard.vue"; import { useModerationStore } from "~/store/moderation.ts"; -import { enrichProjectBatch, type ModerationProject } from "~/helpers/moderation"; +import { enrichProjectBatch, type ModerationProject } from "~/helpers/moderation.ts"; const { formatMessage } = useVIntl(); const moderationStore = useModerationStore(); diff --git a/apps/frontend/src/pages/moderation/reports/[id].vue b/apps/frontend/src/pages/moderation/reports/[id].vue index 5a5930fd03..8ec2c4d67e 100644 --- a/apps/frontend/src/pages/moderation/reports/[id].vue +++ b/apps/frontend/src/pages/moderation/reports/[id].vue @@ -1,5 +1,4 @@