You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
@@ -1,340 +1,340 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col justify-between gap-3 lg:flex-row">
|
||||
<div class="iconified-input flex-1 lg:max-w-md">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-[40px]"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
@input="goToPage(1)"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => (query = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col justify-between gap-3 lg:flex-row">
|
||||
<div class="iconified-input flex-1 lg:max-w-md">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-[40px]"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
@input="goToPage(1)"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => (query = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="hidden flex-1 justify-center lg:flex">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
<ConfettiExplosion v-if="visible" />
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="hidden flex-1 justify-center lg:flex">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
<ConfettiExplosion v-if="visible" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-end gap-2 sm:flex-row lg:flex-shrink-0">
|
||||
<div class="flex flex-col gap-2 sm:flex-row">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentFilterType"
|
||||
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
|
||||
:name="formatMessage(messages.filterBy)"
|
||||
:options="filterTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<FilterIcon class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }} ({{ filteredProjects.length }})</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
<div class="flex flex-col justify-end gap-2 sm:flex-row lg:flex-shrink-0">
|
||||
<div class="flex flex-col gap-2 sm:flex-row">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentFilterType"
|
||||
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
|
||||
:name="formatMessage(messages.filterBy)"
|
||||
:options="filterTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<FilterIcon class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }} ({{ filteredProjects.length }})</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
|
||||
:name="formatMessage(messages.sortBy)"
|
||||
:options="sortTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<SortAscIcon v-if="selected === 'Oldest'" class="size-4 flex-shrink-0" />
|
||||
<SortDescIcon v-else class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }}</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
|
||||
:name="formatMessage(messages.sortBy)"
|
||||
:options="sortTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<SortAscIcon v-if="selected === 'Oldest'" class="size-4 flex-shrink-0" />
|
||||
<SortDescIcon v-else class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }}</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
|
||||
<ButtonStyled color="orange" class="w-full sm:w-auto">
|
||||
<button
|
||||
class="flex !h-[40px] w-full items-center justify-center gap-2 sm:w-auto"
|
||||
@click="moderateAllInFilter()"
|
||||
>
|
||||
<ScaleIcon class="size-4 flex-shrink-0" />
|
||||
<span class="hidden sm:inline">{{ formatMessage(messages.moderate) }}</span>
|
||||
<span class="sm:hidden">Moderate</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled color="orange" class="w-full sm:w-auto">
|
||||
<button
|
||||
class="flex !h-[40px] w-full items-center justify-center gap-2 sm:w-auto"
|
||||
@click="moderateAllInFilter()"
|
||||
>
|
||||
<ScaleIcon class="size-4 flex-shrink-0" />
|
||||
<span class="hidden sm:inline">{{ formatMessage(messages.moderate) }}</span>
|
||||
<span class="sm:hidden">Moderate</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="flex justify-center lg:hidden">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
<ConfettiExplosion v-if="visible" />
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="flex justify-center lg:hidden">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
<ConfettiExplosion v-if="visible" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<div v-if="paginatedProjects.length === 0" class="universal-card h-24 animate-pulse"></div>
|
||||
<ModerationQueueCard
|
||||
v-for="item in paginatedProjects"
|
||||
v-else
|
||||
:key="item.project.id"
|
||||
:queue-entry="item"
|
||||
:owner="item.owner"
|
||||
:org="item.org"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<div v-if="paginatedProjects.length === 0" class="universal-card h-24 animate-pulse"></div>
|
||||
<ModerationQueueCard
|
||||
v-for="item in paginatedProjects"
|
||||
v-else
|
||||
:key="item.project.id"
|
||||
:queue-entry="item"
|
||||
:owner="item.owner"
|
||||
:org="item.org"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="mt-4 flex justify-center">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="mt-4 flex justify-center">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { DropdownSelect, Button, ButtonStyled, Pagination } from "@modrinth/ui";
|
||||
import {
|
||||
XIcon,
|
||||
SearchIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
FilterIcon,
|
||||
ScaleIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||
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.ts";
|
||||
FilterIcon,
|
||||
ScaleIcon,
|
||||
SearchIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, ButtonStyled, DropdownSelect, Pagination } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import Fuse from 'fuse.js'
|
||||
import ConfettiExplosion from 'vue-confetti-explosion'
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const moderationStore = useModerationStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
import ModerationQueueCard from '~/components/ui/moderation/ModerationQueueCard.vue'
|
||||
import { enrichProjectBatch, type ModerationProject } from '~/helpers/moderation.ts'
|
||||
import { useModerationStore } from '~/store/moderation.ts'
|
||||
|
||||
const visible = ref(false);
|
||||
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) {
|
||||
setTimeout(async () => {
|
||||
history.state.confetti = false;
|
||||
visible.value = true;
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
visible.value = false;
|
||||
}, 5000);
|
||||
}, 1000);
|
||||
setTimeout(async () => {
|
||||
history.state.confetti = false
|
||||
visible.value = true
|
||||
await nextTick()
|
||||
setTimeout(() => {
|
||||
visible.value = false
|
||||
}, 5000)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
searchPlaceholder: {
|
||||
id: "moderation.search.placeholder",
|
||||
defaultMessage: "Search...",
|
||||
},
|
||||
filterBy: {
|
||||
id: "moderation.filter.by",
|
||||
defaultMessage: "Filter by",
|
||||
},
|
||||
sortBy: {
|
||||
id: "moderation.sort.by",
|
||||
defaultMessage: "Sort by",
|
||||
},
|
||||
moderate: {
|
||||
id: "moderation.moderate",
|
||||
defaultMessage: "Moderate",
|
||||
},
|
||||
});
|
||||
searchPlaceholder: {
|
||||
id: 'moderation.search.placeholder',
|
||||
defaultMessage: 'Search...',
|
||||
},
|
||||
filterBy: {
|
||||
id: 'moderation.filter.by',
|
||||
defaultMessage: 'Filter by',
|
||||
},
|
||||
sortBy: {
|
||||
id: 'moderation.sort.by',
|
||||
defaultMessage: 'Sort by',
|
||||
},
|
||||
moderate: {
|
||||
id: 'moderation.moderate',
|
||||
defaultMessage: 'Moderate',
|
||||
},
|
||||
})
|
||||
|
||||
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 useLazyAsyncData('moderation-projects', async () => {
|
||||
const startTime = performance.now()
|
||||
let currentOffset = 0
|
||||
const PROJECT_ENDPOINT_COUNT = 350
|
||||
const allProjects: ModerationProject[] = []
|
||||
|
||||
const enrichmentPromises: Promise<ModerationProject[]>[] = [];
|
||||
const enrichmentPromises: Promise<ModerationProject[]>[] = []
|
||||
|
||||
while (true) {
|
||||
const projects = (await useBaseFetch(
|
||||
`moderation/projects?count=${PROJECT_ENDPOINT_COUNT}&offset=${currentOffset}`,
|
||||
{ internal: true },
|
||||
)) as any[];
|
||||
let projects: any[] = []
|
||||
do {
|
||||
projects = (await useBaseFetch(
|
||||
`moderation/projects?count=${PROJECT_ENDPOINT_COUNT}&offset=${currentOffset}`,
|
||||
{ internal: true },
|
||||
)) as any[]
|
||||
|
||||
if (projects.length === 0) break;
|
||||
if (projects.length === 0) break
|
||||
|
||||
const enrichmentPromise = enrichProjectBatch(projects);
|
||||
enrichmentPromises.push(enrichmentPromise);
|
||||
const enrichmentPromise = enrichProjectBatch(projects)
|
||||
enrichmentPromises.push(enrichmentPromise)
|
||||
|
||||
currentOffset += projects.length;
|
||||
currentOffset += projects.length
|
||||
|
||||
if (enrichmentPromises.length >= 3) {
|
||||
const completed = await Promise.all(enrichmentPromises.splice(0, 2));
|
||||
allProjects.push(...completed.flat());
|
||||
}
|
||||
if (enrichmentPromises.length >= 3) {
|
||||
const completed = await Promise.all(enrichmentPromises.splice(0, 2))
|
||||
allProjects.push(...completed.flat())
|
||||
}
|
||||
} while (projects.length === PROJECT_ENDPOINT_COUNT)
|
||||
|
||||
if (projects.length < PROJECT_ENDPOINT_COUNT) break;
|
||||
}
|
||||
const remainingBatches = await Promise.all(enrichmentPromises)
|
||||
allProjects.push(...remainingBatches.flat())
|
||||
|
||||
const remainingBatches = await Promise.all(enrichmentPromises);
|
||||
allProjects.push(...remainingBatches.flat());
|
||||
const endTime = performance.now()
|
||||
const duration = endTime - startTime
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
console.debug(
|
||||
`Projects fetched and processed in ${duration.toFixed(2)}ms (${(duration / 1000).toFixed(2)}s)`,
|
||||
)
|
||||
|
||||
console.debug(
|
||||
`Projects fetched and processed in ${duration.toFixed(2)}ms (${(duration / 1000).toFixed(2)}s)`,
|
||||
);
|
||||
return allProjects
|
||||
})
|
||||
|
||||
return allProjects;
|
||||
});
|
||||
|
||||
const query = ref(route.query.q?.toString() || "");
|
||||
const query = ref(route.query.q?.toString() || '')
|
||||
|
||||
watch(
|
||||
query,
|
||||
(newQuery) => {
|
||||
const currentQuery = { ...route.query };
|
||||
if (newQuery) {
|
||||
currentQuery.q = newQuery;
|
||||
} else {
|
||||
delete currentQuery.q;
|
||||
}
|
||||
query,
|
||||
(newQuery) => {
|
||||
const currentQuery = { ...route.query }
|
||||
if (newQuery) {
|
||||
currentQuery.q = newQuery
|
||||
} else {
|
||||
delete currentQuery.q
|
||||
}
|
||||
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: currentQuery,
|
||||
});
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
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;
|
||||
}
|
||||
},
|
||||
);
|
||||
() => route.query.q,
|
||||
(newQueryParam) => {
|
||||
const newValue = newQueryParam?.toString() || ''
|
||||
if (query.value !== newValue) {
|
||||
query.value = newValue
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const currentFilterType = ref("All projects");
|
||||
const currentFilterType = ref('All projects')
|
||||
const filterTypes: readonly string[] = readonly([
|
||||
"All projects",
|
||||
"Modpacks",
|
||||
"Mods",
|
||||
"Resource Packs",
|
||||
"Data Packs",
|
||||
"Plugins",
|
||||
"Shaders",
|
||||
]);
|
||||
'All projects',
|
||||
'Modpacks',
|
||||
'Mods',
|
||||
'Resource Packs',
|
||||
'Data Packs',
|
||||
'Plugins',
|
||||
'Shaders',
|
||||
])
|
||||
|
||||
const currentSortType = ref("Oldest");
|
||||
const sortTypes: readonly string[] = readonly(["Oldest", "Newest"]);
|
||||
const currentSortType = ref('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));
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 15
|
||||
const totalPages = computed(() => Math.ceil((filteredProjects.value?.length || 0) / itemsPerPage))
|
||||
|
||||
const fuse = computed(() => {
|
||||
if (!allProjects.value || allProjects.value.length === 0) return null;
|
||||
return new Fuse(allProjects.value, {
|
||||
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,
|
||||
});
|
||||
});
|
||||
if (!allProjects.value || allProjects.value.length === 0) return null
|
||||
return new Fuse(allProjects.value, {
|
||||
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 searchResults = computed(() => {
|
||||
if (!query.value || !fuse.value) return null;
|
||||
return fuse.value.search(query.value).map((result) => result.item);
|
||||
});
|
||||
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];
|
||||
});
|
||||
if (!allProjects.value) return []
|
||||
return query.value && searchResults.value ? searchResults.value : [...allProjects.value]
|
||||
})
|
||||
|
||||
const typeFiltered = computed(() => {
|
||||
if (currentFilterType.value === "All projects") return baseFiltered.value;
|
||||
if (currentFilterType.value === 'All projects') return baseFiltered.value
|
||||
|
||||
const filterMap: Record<string, string> = {
|
||||
Modpacks: "modpack",
|
||||
Mods: "mod",
|
||||
"Resource Packs": "resourcepack",
|
||||
"Data Packs": "datapack",
|
||||
Plugins: "plugin",
|
||||
Shaders: "shader",
|
||||
};
|
||||
const filterMap: Record<string, string> = {
|
||||
Modpacks: 'modpack',
|
||||
Mods: 'mod',
|
||||
'Resource Packs': 'resourcepack',
|
||||
'Data Packs': 'datapack',
|
||||
Plugins: 'plugin',
|
||||
Shaders: 'shader',
|
||||
}
|
||||
|
||||
const projectType = filterMap[currentFilterType.value];
|
||||
if (!projectType) return baseFiltered.value;
|
||||
const projectType = filterMap[currentFilterType.value]
|
||||
if (!projectType) return baseFiltered.value
|
||||
|
||||
return baseFiltered.value.filter(
|
||||
(queueItem) =>
|
||||
queueItem.project.project_types.length > 0 &&
|
||||
queueItem.project.project_types[0] === projectType,
|
||||
);
|
||||
});
|
||||
return baseFiltered.value.filter(
|
||||
(queueItem) =>
|
||||
queueItem.project.project_types.length > 0 &&
|
||||
queueItem.project.project_types[0] === projectType,
|
||||
)
|
||||
})
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
const filtered = [...typeFiltered.value];
|
||||
const filtered = [...typeFiltered.value]
|
||||
|
||||
if (currentSortType.value === "Oldest") {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.project.queued || a.project.published || 0).getTime();
|
||||
const dateB = new Date(b.project.queued || b.project.published || 0).getTime();
|
||||
return dateA - dateB;
|
||||
});
|
||||
} else {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.project.queued || a.project.published || 0).getTime();
|
||||
const dateB = new Date(b.project.queued || b.project.published || 0).getTime();
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
if (currentSortType.value === 'Oldest') {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.project.queued || a.project.published || 0).getTime()
|
||||
const dateB = new Date(b.project.queued || b.project.published || 0).getTime()
|
||||
return dateA - dateB
|
||||
})
|
||||
} else {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.project.queued || a.project.published || 0).getTime()
|
||||
const dateB = new Date(b.project.queued || b.project.published || 0).getTime()
|
||||
return dateB - dateA
|
||||
})
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
return filtered
|
||||
})
|
||||
|
||||
const paginatedProjects = computed(() => {
|
||||
if (!filteredProjects.value) return [];
|
||||
const start = (currentPage.value - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
return filteredProjects.value.slice(start, end);
|
||||
});
|
||||
if (!filteredProjects.value) return []
|
||||
const start = (currentPage.value - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
return filteredProjects.value.slice(start, end)
|
||||
})
|
||||
|
||||
function goToPage(page: number) {
|
||||
currentPage.value = page;
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
function moderateAllInFilter() {
|
||||
moderationStore.setQueue(filteredProjects.value.map((queueItem) => queueItem.project.id));
|
||||
navigateTo({
|
||||
name: "type-id",
|
||||
params: {
|
||||
type: "project",
|
||||
id: moderationStore.getCurrentProjectId(),
|
||||
},
|
||||
state: {
|
||||
showChecklist: true,
|
||||
},
|
||||
});
|
||||
moderationStore.setQueue(filteredProjects.value.map((queueItem) => queueItem.project.id))
|
||||
navigateTo({
|
||||
name: 'type-id',
|
||||
params: {
|
||||
type: 'project',
|
||||
id: moderationStore.getCurrentProjectId(),
|
||||
},
|
||||
state: {
|
||||
showChecklist: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { Report } from "@modrinth/utils";
|
||||
import { enrichReportBatch } from "~/helpers/moderation.ts";
|
||||
import ModerationReportCard from "~/components/ui/moderation/ModerationReportCard.vue";
|
||||
import type { Report } from '@modrinth/utils'
|
||||
|
||||
const { params } = useRoute();
|
||||
const reportId = params.id as string;
|
||||
import ModerationReportCard from '~/components/ui/moderation/ModerationReportCard.vue'
|
||||
import { enrichReportBatch } from '~/helpers/moderation.ts'
|
||||
|
||||
const { params } = useRoute()
|
||||
const reportId = params.id as string
|
||||
|
||||
const { data: report } = await useAsyncData(`moderation-report-${reportId}`, async () => {
|
||||
try {
|
||||
const report = (await useBaseFetch(`report/${reportId}`, { apiVersion: 3 })) as Report;
|
||||
const enrichedReport = (await enrichReportBatch([report]))[0];
|
||||
return enrichedReport;
|
||||
} catch (error) {
|
||||
console.error("Error fetching report:", error);
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Report not found",
|
||||
});
|
||||
}
|
||||
});
|
||||
try {
|
||||
const report = (await useBaseFetch(`report/${reportId}`, { apiVersion: 3 })) as Report
|
||||
const enrichedReport = (await enrichReportBatch([report]))[0]
|
||||
return enrichedReport
|
||||
} catch (error) {
|
||||
console.error('Error fetching report:', error)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Report not found',
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<ModerationReportCard v-if="report" :report="report" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<ModerationReportCard v-if="report" :report="report" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,289 +1,288 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col justify-between gap-3 lg:flex-row">
|
||||
<div class="iconified-input flex-1 lg:max-w-md">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-[40px]"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
@input="goToPage(1)"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => (query = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col justify-between gap-3 lg:flex-row">
|
||||
<div class="iconified-input flex-1 lg:max-w-md">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-[40px]"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
@input="goToPage(1)"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => (query = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="hidden flex-1 justify-center lg:flex">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="hidden flex-1 justify-center lg:flex">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-end gap-2 sm:flex-row lg:flex-shrink-0">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentFilterType"
|
||||
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
|
||||
:name="formatMessage(messages.filterBy)"
|
||||
:options="filterTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<FilterIcon class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }} ({{ filteredReports.length }})</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
<div class="flex flex-col justify-end gap-2 sm:flex-row lg:flex-shrink-0">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentFilterType"
|
||||
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
|
||||
:name="formatMessage(messages.filterBy)"
|
||||
:options="filterTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<FilterIcon class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }} ({{ filteredReports.length }})</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
|
||||
:name="formatMessage(messages.sortBy)"
|
||||
:options="sortTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<SortAscIcon v-if="selected === 'Oldest'" class="size-4 flex-shrink-0" />
|
||||
<SortDescIcon v-else class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }}</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
|
||||
:name="formatMessage(messages.sortBy)"
|
||||
:options="sortTypes as unknown[]"
|
||||
@change="goToPage(1)"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<SortAscIcon v-if="selected === 'Oldest'" class="size-4 flex-shrink-0" />
|
||||
<SortDescIcon v-else class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }}</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="flex justify-center lg:hidden">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="flex justify-center lg:hidden">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<div v-if="paginatedReports.length === 0" class="universal-card h-24 animate-pulse"></div>
|
||||
<ReportCard v-for="report in paginatedReports" v-else :key="report.id" :report="report" />
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<div v-if="paginatedReports.length === 0" class="universal-card h-24 animate-pulse"></div>
|
||||
<ReportCard v-for="report in paginatedReports" v-else :key="report.id" :report="report" />
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="mt-4 flex justify-center">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="mt-4 flex justify-center">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DropdownSelect, Button, Pagination } from "@modrinth/ui";
|
||||
import { XIcon, SearchIcon, SortAscIcon, SortDescIcon, FilterIcon } from "@modrinth/assets";
|
||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||
import type { Report } from "@modrinth/utils";
|
||||
import Fuse from "fuse.js";
|
||||
import type { ExtendedReport } from "@modrinth/moderation";
|
||||
import ReportCard from "~/components/ui/moderation/ModerationReportCard.vue";
|
||||
import { enrichReportBatch } from "~/helpers/moderation.ts";
|
||||
import { FilterIcon, SearchIcon, SortAscIcon, SortDescIcon, XIcon } from '@modrinth/assets'
|
||||
import type { ExtendedReport } from '@modrinth/moderation'
|
||||
import { Button, DropdownSelect, Pagination } from '@modrinth/ui'
|
||||
import type { Report } from '@modrinth/utils'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
import ReportCard from '~/components/ui/moderation/ModerationReportCard.vue'
|
||||
import { enrichReportBatch } from '~/helpers/moderation.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const messages = defineMessages({
|
||||
searchPlaceholder: {
|
||||
id: "moderation.search.placeholder",
|
||||
defaultMessage: "Search...",
|
||||
},
|
||||
filterBy: {
|
||||
id: "moderation.filter.by",
|
||||
defaultMessage: "Filter by",
|
||||
},
|
||||
sortBy: {
|
||||
id: "moderation.sort.by",
|
||||
defaultMessage: "Sort by",
|
||||
},
|
||||
});
|
||||
searchPlaceholder: {
|
||||
id: 'moderation.search.placeholder',
|
||||
defaultMessage: 'Search...',
|
||||
},
|
||||
filterBy: {
|
||||
id: 'moderation.filter.by',
|
||||
defaultMessage: 'Filter by',
|
||||
},
|
||||
sortBy: {
|
||||
id: 'moderation.sort.by',
|
||||
defaultMessage: 'Sort by',
|
||||
},
|
||||
})
|
||||
|
||||
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 { data: allReports } = await useLazyAsyncData('new-moderation-reports', async () => {
|
||||
const startTime = performance.now()
|
||||
let currentOffset = 0
|
||||
const REPORT_ENDPOINT_COUNT = 350
|
||||
const allReports: ExtendedReport[] = []
|
||||
|
||||
const enrichmentPromises: Promise<ExtendedReport[]>[] = [];
|
||||
const enrichmentPromises: Promise<ExtendedReport[]>[] = []
|
||||
|
||||
while (true) {
|
||||
const reports = (await useBaseFetch(
|
||||
`report?count=${REPORT_ENDPOINT_COUNT}&offset=${currentOffset}`,
|
||||
{ apiVersion: 3 },
|
||||
)) as Report[];
|
||||
let reports: Report[]
|
||||
do {
|
||||
reports = (await useBaseFetch(`report?count=${REPORT_ENDPOINT_COUNT}&offset=${currentOffset}`, {
|
||||
apiVersion: 3,
|
||||
})) as Report[]
|
||||
|
||||
if (reports.length === 0) break;
|
||||
if (reports.length === 0) break
|
||||
|
||||
const enrichmentPromise = enrichReportBatch(reports);
|
||||
enrichmentPromises.push(enrichmentPromise);
|
||||
const enrichmentPromise = enrichReportBatch(reports)
|
||||
enrichmentPromises.push(enrichmentPromise)
|
||||
|
||||
currentOffset += reports.length;
|
||||
currentOffset += reports.length
|
||||
|
||||
if (enrichmentPromises.length >= 3) {
|
||||
const completed = await Promise.all(enrichmentPromises.splice(0, 2));
|
||||
allReports.push(...completed.flat());
|
||||
}
|
||||
if (enrichmentPromises.length >= 3) {
|
||||
const completed = await Promise.all(enrichmentPromises.splice(0, 2))
|
||||
allReports.push(...completed.flat())
|
||||
}
|
||||
} while (reports.length === REPORT_ENDPOINT_COUNT)
|
||||
|
||||
if (reports.length < REPORT_ENDPOINT_COUNT) break;
|
||||
}
|
||||
const remainingBatches = await Promise.all(enrichmentPromises)
|
||||
allReports.push(...remainingBatches.flat())
|
||||
|
||||
const remainingBatches = await Promise.all(enrichmentPromises);
|
||||
allReports.push(...remainingBatches.flat());
|
||||
const endTime = performance.now()
|
||||
const duration = endTime - startTime
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
console.debug(
|
||||
`Reports fetched and processed in ${duration.toFixed(2)}ms (${(duration / 1000).toFixed(2)}s)`,
|
||||
)
|
||||
|
||||
console.debug(
|
||||
`Reports fetched and processed in ${duration.toFixed(2)}ms (${(duration / 1000).toFixed(2)}s)`,
|
||||
);
|
||||
return allReports
|
||||
})
|
||||
|
||||
return allReports;
|
||||
});
|
||||
|
||||
const query = ref(route.query.q?.toString() || "");
|
||||
const query = ref(route.query.q?.toString() || '')
|
||||
|
||||
watch(
|
||||
query,
|
||||
(newQuery) => {
|
||||
const currentQuery = { ...route.query };
|
||||
if (newQuery) {
|
||||
currentQuery.q = newQuery;
|
||||
} else {
|
||||
delete currentQuery.q;
|
||||
}
|
||||
query,
|
||||
(newQuery) => {
|
||||
const currentQuery = { ...route.query }
|
||||
if (newQuery) {
|
||||
currentQuery.q = newQuery
|
||||
} else {
|
||||
delete currentQuery.q
|
||||
}
|
||||
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: currentQuery,
|
||||
});
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
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;
|
||||
}
|
||||
},
|
||||
);
|
||||
() => route.query.q,
|
||||
(newQueryParam) => {
|
||||
const newValue = newQueryParam?.toString() || ''
|
||||
if (query.value !== newValue) {
|
||||
query.value = newValue
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const currentFilterType = ref("All");
|
||||
const filterTypes: readonly string[] = readonly(["All", "Unread", "Read"]);
|
||||
const currentFilterType = ref('All')
|
||||
const filterTypes: readonly string[] = readonly(['All', 'Unread', 'Read'])
|
||||
|
||||
const currentSortType = ref("Oldest");
|
||||
const sortTypes: readonly string[] = readonly(["Oldest", "Newest"]);
|
||||
const currentSortType = ref('Oldest')
|
||||
const sortTypes: readonly string[] = readonly(['Oldest', 'Newest'])
|
||||
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = 15;
|
||||
const totalPages = computed(() => Math.ceil((filteredReports.value?.length || 0) / itemsPerPage));
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 15
|
||||
const totalPages = computed(() => Math.ceil((filteredReports.value?.length || 0) / itemsPerPage))
|
||||
|
||||
const fuse = computed(() => {
|
||||
if (!allReports.value || allReports.value.length === 0) return null;
|
||||
return new Fuse(allReports.value, {
|
||||
keys: [
|
||||
{
|
||||
name: "id",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "body",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "report_type",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "item_id",
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
name: "reporter_user.username",
|
||||
weight: 2,
|
||||
},
|
||||
"project.name",
|
||||
"project.slug",
|
||||
"user.username",
|
||||
"version.name",
|
||||
"target.name",
|
||||
"target.slug",
|
||||
],
|
||||
includeScore: true,
|
||||
threshold: 0.4,
|
||||
});
|
||||
});
|
||||
if (!allReports.value || allReports.value.length === 0) return null
|
||||
return new Fuse(allReports.value, {
|
||||
keys: [
|
||||
{
|
||||
name: 'id',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: 'body',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: 'report_type',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: 'item_id',
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
name: 'reporter_user.username',
|
||||
weight: 2,
|
||||
},
|
||||
'project.name',
|
||||
'project.slug',
|
||||
'user.username',
|
||||
'version.name',
|
||||
'target.name',
|
||||
'target.slug',
|
||||
],
|
||||
includeScore: true,
|
||||
threshold: 0.4,
|
||||
})
|
||||
})
|
||||
|
||||
const memberRoleMap = computed(() => {
|
||||
if (!allReports.value?.length) return new Map();
|
||||
if (!allReports.value?.length) return new Map()
|
||||
|
||||
const map = new Map();
|
||||
for (const report of allReports.value) {
|
||||
if (report.thread?.members?.length) {
|
||||
const roleMap = new Map();
|
||||
for (const member of report.thread.members) {
|
||||
roleMap.set(member.id, member.role);
|
||||
}
|
||||
map.set(report.id, roleMap);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
});
|
||||
const map = new Map()
|
||||
for (const report of allReports.value) {
|
||||
if (report.thread?.members?.length) {
|
||||
const roleMap = new Map()
|
||||
for (const member of report.thread.members) {
|
||||
roleMap.set(member.id, member.role)
|
||||
}
|
||||
map.set(report.id, roleMap)
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const searchResults = computed(() => {
|
||||
if (!query.value || !fuse.value) return null;
|
||||
return fuse.value.search(query.value).map((result) => result.item);
|
||||
});
|
||||
if (!query.value || !fuse.value) return null
|
||||
return fuse.value.search(query.value).map((result) => result.item)
|
||||
})
|
||||
|
||||
const baseFiltered = computed(() => {
|
||||
if (!allReports.value) return [];
|
||||
return query.value && searchResults.value ? searchResults.value : [...allReports.value];
|
||||
});
|
||||
if (!allReports.value) return []
|
||||
return query.value && searchResults.value ? searchResults.value : [...allReports.value]
|
||||
})
|
||||
|
||||
const typeFiltered = computed(() => {
|
||||
if (currentFilterType.value === "All") return baseFiltered.value;
|
||||
if (currentFilterType.value === 'All') return baseFiltered.value
|
||||
|
||||
return baseFiltered.value.filter((report) => {
|
||||
const messages = report.thread?.messages || [];
|
||||
return baseFiltered.value.filter((report) => {
|
||||
const messages = report.thread?.messages || []
|
||||
|
||||
if (messages.length === 0) {
|
||||
return currentFilterType.value === "Unread";
|
||||
}
|
||||
if (messages.length === 0) {
|
||||
return currentFilterType.value === 'Unread'
|
||||
}
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (!lastMessage.author_id) return false;
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
if (!lastMessage.author_id) return false
|
||||
|
||||
const roleMap = memberRoleMap.value.get(report.id);
|
||||
if (!roleMap) return false;
|
||||
const roleMap = memberRoleMap.value.get(report.id)
|
||||
if (!roleMap) return false
|
||||
|
||||
const authorRole = roleMap.get(lastMessage.author_id);
|
||||
const isModeratorMessage = authorRole === "moderator" || authorRole === "admin";
|
||||
const authorRole = roleMap.get(lastMessage.author_id)
|
||||
const isModeratorMessage = authorRole === 'moderator' || authorRole === 'admin'
|
||||
|
||||
return currentFilterType.value === "Read" ? isModeratorMessage : !isModeratorMessage;
|
||||
});
|
||||
});
|
||||
return currentFilterType.value === 'Read' ? isModeratorMessage : !isModeratorMessage
|
||||
})
|
||||
})
|
||||
|
||||
const filteredReports = computed(() => {
|
||||
const filtered = [...typeFiltered.value];
|
||||
const filtered = [...typeFiltered.value]
|
||||
|
||||
if (currentSortType.value === "Oldest") {
|
||||
filtered.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
|
||||
} else {
|
||||
filtered.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
||||
}
|
||||
if (currentSortType.value === 'Oldest') {
|
||||
filtered.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime())
|
||||
} else {
|
||||
filtered.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime())
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
return filtered
|
||||
})
|
||||
|
||||
const paginatedReports = computed(() => {
|
||||
if (!filteredReports.value) return [];
|
||||
const start = (currentPage.value - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
return filteredReports.value.slice(start, end);
|
||||
});
|
||||
if (!filteredReports.value) return []
|
||||
const start = (currentPage.value - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
return filteredReports.value.slice(start, end)
|
||||
})
|
||||
|
||||
function goToPage(page: number) {
|
||||
currentPage.value = page;
|
||||
currentPage.value = page
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,386 +1,387 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col justify-between gap-3 lg:flex-row">
|
||||
<div class="iconified-input flex-1 lg:max-w-md">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-[40px]"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
@input="updateSearchResults()"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => (query = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col justify-between gap-3 lg:flex-row">
|
||||
<div class="iconified-input flex-1 lg:max-w-md">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-[40px]"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
@input="updateSearchResults()"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => (query = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="hidden flex-1 justify-center lg:flex">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="hidden flex-1 justify-center lg:flex">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-end gap-2 sm:flex-row lg:flex-shrink-0">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentFilterType"
|
||||
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
|
||||
:name="formatMessage(messages.filterBy)"
|
||||
:options="filterTypes as unknown[]"
|
||||
@change="updateSearchResults()"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<FilterIcon class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }} ({{ filteredReports.length }})</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
<div class="flex flex-col justify-end gap-2 sm:flex-row lg:flex-shrink-0">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentFilterType"
|
||||
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
|
||||
:name="formatMessage(messages.filterBy)"
|
||||
:options="filterTypes as unknown[]"
|
||||
@change="updateSearchResults()"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<FilterIcon class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }} ({{ filteredReports.length }})</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
|
||||
:name="formatMessage(messages.sortBy)"
|
||||
:options="sortTypes as unknown[]"
|
||||
@change="updateSearchResults()"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<SortAscIcon v-if="selected === 'Oldest'" class="size-4 flex-shrink-0" />
|
||||
<SortDescIcon v-else class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }}</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
|
||||
:name="formatMessage(messages.sortBy)"
|
||||
:options="sortTypes as unknown[]"
|
||||
@change="updateSearchResults()"
|
||||
>
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold text-secondary">
|
||||
<SortAscIcon v-if="selected === 'Oldest'" class="size-4 flex-shrink-0" />
|
||||
<SortDescIcon v-else class="size-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ selected }}</span>
|
||||
</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="flex justify-center lg:hidden">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="flex justify-center lg:hidden">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<DelphiReportCard
|
||||
v-for="report in paginatedReports"
|
||||
:key="report.version.id"
|
||||
:report="report"
|
||||
/>
|
||||
<div
|
||||
v-if="!paginatedReports || paginatedReports.length === 0"
|
||||
class="universal-card h-24 animate-pulse"
|
||||
></div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<DelphiReportCard
|
||||
v-for="report in paginatedReports"
|
||||
:key="report.version.id"
|
||||
:report="report"
|
||||
/>
|
||||
<div
|
||||
v-if="!paginatedReports || paginatedReports.length === 0"
|
||||
class="universal-card h-24 animate-pulse"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="mt-4 flex justify-center">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="mt-4 flex justify-center">
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { TeamMember, Organization, DelphiReport, Project, Version } from "@modrinth/utils";
|
||||
import Fuse from "fuse.js";
|
||||
import type { OwnershipTarget, ExtendedDelphiReport } from "@modrinth/moderation";
|
||||
import DelphiReportCard from "~/components/ui/moderation/ModerationDelphiReportCard.vue";
|
||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||
import { FilterIcon, SearchIcon, SortAscIcon, SortDescIcon, XIcon } from '@modrinth/assets'
|
||||
import type { ExtendedDelphiReport, OwnershipTarget } from '@modrinth/moderation'
|
||||
import { Button, DropdownSelect, Pagination } from '@modrinth/ui'
|
||||
import type { DelphiReport, Organization, Project, TeamMember, Version } from '@modrinth/utils'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
import DelphiReportCard from '~/components/ui/moderation/ModerationDelphiReportCard.vue'
|
||||
import { asEncodedJsonArray, fetchSegmented } from '~/utils/fetch-helpers.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const messages = defineMessages({
|
||||
searchPlaceholder: {
|
||||
id: "moderation.technical.search.placeholder",
|
||||
defaultMessage: "Search tech reviews...",
|
||||
},
|
||||
filterBy: {
|
||||
id: "moderation.filter.by",
|
||||
defaultMessage: "Filter by",
|
||||
},
|
||||
sortBy: {
|
||||
id: "moderation.sort.by",
|
||||
defaultMessage: "Sort by",
|
||||
},
|
||||
});
|
||||
searchPlaceholder: {
|
||||
id: 'moderation.technical.search.placeholder',
|
||||
defaultMessage: 'Search tech reviews...',
|
||||
},
|
||||
filterBy: {
|
||||
id: 'moderation.filter.by',
|
||||
defaultMessage: 'Filter by',
|
||||
},
|
||||
sortBy: {
|
||||
id: 'moderation.sort.by',
|
||||
defaultMessage: 'Sort by',
|
||||
},
|
||||
})
|
||||
|
||||
async function getProjectQuicklyForMock(projectId: string): Promise<Project> {
|
||||
return (await useBaseFetch(`project/${projectId}`)) as Project;
|
||||
return (await useBaseFetch(`project/${projectId}`)) as Project
|
||||
}
|
||||
|
||||
async function getVersionQuicklyForMock(versionId: string): Promise<Version> {
|
||||
return (await useBaseFetch(`version/${versionId}`)) as Version;
|
||||
return (await useBaseFetch(`version/${versionId}`)) as Version
|
||||
}
|
||||
|
||||
const mockDelphiReports: DelphiReport[] = [
|
||||
{
|
||||
project: await getProjectQuicklyForMock("7MoE34WK"),
|
||||
version: await getVersionQuicklyForMock("cTkKLWgA"),
|
||||
trace_type: "url_usage",
|
||||
file_path: "me/decce/gnetum/ASMEventHandlerHelper.java",
|
||||
priority_score: 29,
|
||||
status: "pending",
|
||||
detected_at: "2025-04-01T12:00:00Z",
|
||||
} as DelphiReport,
|
||||
{
|
||||
project: await getProjectQuicklyForMock("7MoE34WK"),
|
||||
version: await getVersionQuicklyForMock("cTkKLWgA"),
|
||||
trace_type: "url_usage",
|
||||
file_path: "me/decce/gnetum/SomeOtherFile.java",
|
||||
priority_score: 48,
|
||||
status: "rejected",
|
||||
detected_at: "2025-03-02T12:00:00Z",
|
||||
} as DelphiReport,
|
||||
{
|
||||
project: await getProjectQuicklyForMock("7MoE34WK"),
|
||||
version: await getVersionQuicklyForMock("cTkKLWgA"),
|
||||
trace_type: "url_usage",
|
||||
file_path: "me/decce/gnetum/YetAnotherFile.java",
|
||||
priority_score: 15,
|
||||
status: "approved",
|
||||
detected_at: "2025-02-03T12:00:00Z",
|
||||
} as DelphiReport,
|
||||
];
|
||||
{
|
||||
project: await getProjectQuicklyForMock('7MoE34WK'),
|
||||
version: await getVersionQuicklyForMock('cTkKLWgA'),
|
||||
trace_type: 'url_usage',
|
||||
file_path: 'me/decce/gnetum/ASMEventHandlerHelper.java',
|
||||
priority_score: 29,
|
||||
status: 'pending',
|
||||
detected_at: '2025-04-01T12:00:00Z',
|
||||
} as DelphiReport,
|
||||
{
|
||||
project: await getProjectQuicklyForMock('7MoE34WK'),
|
||||
version: await getVersionQuicklyForMock('cTkKLWgA'),
|
||||
trace_type: 'url_usage',
|
||||
file_path: 'me/decce/gnetum/SomeOtherFile.java',
|
||||
priority_score: 48,
|
||||
status: 'rejected',
|
||||
detected_at: '2025-03-02T12:00:00Z',
|
||||
} as DelphiReport,
|
||||
{
|
||||
project: await getProjectQuicklyForMock('7MoE34WK'),
|
||||
version: await getVersionQuicklyForMock('cTkKLWgA'),
|
||||
trace_type: 'url_usage',
|
||||
file_path: 'me/decce/gnetum/YetAnotherFile.java',
|
||||
priority_score: 15,
|
||||
status: 'approved',
|
||||
detected_at: '2025-02-03T12:00:00Z',
|
||||
} as DelphiReport,
|
||||
]
|
||||
|
||||
const { data: allReports } = await useAsyncData("moderation-tech-reviews", async () => {
|
||||
// TODO: replace with actual API call
|
||||
const delphiReports = mockDelphiReports;
|
||||
const { data: allReports } = await useAsyncData('moderation-tech-reviews', async () => {
|
||||
// TODO: replace with actual API call
|
||||
const delphiReports = mockDelphiReports
|
||||
|
||||
if (delphiReports.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (delphiReports.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const teamIds = [...new Set(delphiReports.map((report) => report.project.team).filter(Boolean))];
|
||||
const orgIds = [
|
||||
...new Set(delphiReports.map((report) => report.project.organization).filter(Boolean)),
|
||||
];
|
||||
const teamIds = [...new Set(delphiReports.map((report) => report.project.team).filter(Boolean))]
|
||||
const orgIds = [
|
||||
...new Set(delphiReports.map((report) => report.project.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 [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 orgTeamIds = orgsData.map((org) => org.team_id).filter(Boolean)
|
||||
const orgTeamsData: TeamMember[][] =
|
||||
orgTeamIds.length > 0
|
||||
? await fetchSegmented(orgTeamIds, (ids) => `teams?ids=${asEncodedJsonArray(ids)}`)
|
||||
: []
|
||||
|
||||
const teamMap = new Map<string, TeamMember[]>();
|
||||
const orgMap = new Map<string, Organization>();
|
||||
const teamMap = new Map<string, TeamMember[]>()
|
||||
const orgMap = new Map<string, Organization>()
|
||||
|
||||
teamsData.forEach((team) => {
|
||||
let teamId = null;
|
||||
for (const member of team) {
|
||||
teamId = member.team_id;
|
||||
if (!teamMap.has(teamId)) {
|
||||
teamMap.set(teamId, team);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
teamsData.forEach((team) => {
|
||||
let teamId = null
|
||||
for (const member of team) {
|
||||
teamId = member.team_id
|
||||
if (!teamMap.has(teamId)) {
|
||||
teamMap.set(teamId, team)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
orgTeamsData.forEach((team) => {
|
||||
let teamId = null;
|
||||
for (const member of team) {
|
||||
teamId = member.team_id;
|
||||
if (!teamMap.has(teamId)) {
|
||||
teamMap.set(teamId, team);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
orgTeamsData.forEach((team) => {
|
||||
let teamId = null
|
||||
for (const member of team) {
|
||||
teamId = member.team_id
|
||||
if (!teamMap.has(teamId)) {
|
||||
teamMap.set(teamId, team)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
orgsData.forEach((org: Organization) => {
|
||||
orgMap.set(org.id, org);
|
||||
});
|
||||
orgsData.forEach((org: Organization) => {
|
||||
orgMap.set(org.id, org)
|
||||
})
|
||||
|
||||
const extendedReports: ExtendedDelphiReport[] = delphiReports.map((report) => {
|
||||
let target: OwnershipTarget | undefined;
|
||||
const project = report.project;
|
||||
const extendedReports: ExtendedDelphiReport[] = delphiReports.map((report) => {
|
||||
let target: OwnershipTarget | undefined
|
||||
const project = report.project
|
||||
|
||||
if (project) {
|
||||
let owner: TeamMember | null = null;
|
||||
let org: Organization | null = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (project.team) {
|
||||
const teamMembers = teamMap.get(project.team)
|
||||
if (teamMembers) {
|
||||
owner = teamMembers.find((member) => member.role === 'Owner') || null
|
||||
}
|
||||
}
|
||||
|
||||
if (project.organization) {
|
||||
org = orgMap.get(project.organization) || null;
|
||||
}
|
||||
if (project.organization) {
|
||||
org = orgMap.get(project.organization) || null
|
||||
}
|
||||
|
||||
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 (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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...report,
|
||||
target,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...report,
|
||||
target,
|
||||
}
|
||||
})
|
||||
|
||||
extendedReports.sort((a, b) => b.priority_score - a.priority_score);
|
||||
extendedReports.sort((a, b) => b.priority_score - a.priority_score)
|
||||
|
||||
return extendedReports;
|
||||
});
|
||||
return extendedReports
|
||||
})
|
||||
|
||||
const query = ref(route.query.q?.toString() || "");
|
||||
const query = ref(route.query.q?.toString() || '')
|
||||
watch(
|
||||
query,
|
||||
(newQuery) => {
|
||||
const currentQuery = { ...route.query };
|
||||
if (newQuery) {
|
||||
currentQuery.q = newQuery;
|
||||
} else {
|
||||
delete currentQuery.q;
|
||||
}
|
||||
query,
|
||||
(newQuery) => {
|
||||
const currentQuery = { ...route.query }
|
||||
if (newQuery) {
|
||||
currentQuery.q = newQuery
|
||||
} else {
|
||||
delete currentQuery.q
|
||||
}
|
||||
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: currentQuery,
|
||||
});
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
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;
|
||||
}
|
||||
},
|
||||
);
|
||||
() => route.query.q,
|
||||
(newQueryParam) => {
|
||||
const newValue = newQueryParam?.toString() || ''
|
||||
if (query.value !== newValue) {
|
||||
query.value = newValue
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const currentFilterType = useLocalStorage("moderation-tech-reviews-filter-type", () => "Pending");
|
||||
const filterTypes: readonly string[] = readonly(["All", "Pending", "Approved", "Rejected"]);
|
||||
const currentFilterType = useLocalStorage('moderation-tech-reviews-filter-type', () => 'Pending')
|
||||
const filterTypes: readonly string[] = readonly(['All', 'Pending', 'Approved', 'Rejected'])
|
||||
|
||||
const currentSortType = useLocalStorage("moderation-tech-reviews-sort-type", () => "Priority");
|
||||
const sortTypes: readonly string[] = readonly(["Priority", "Oldest", "Newest"]);
|
||||
const currentSortType = useLocalStorage('moderation-tech-reviews-sort-type', () => 'Priority')
|
||||
const sortTypes: readonly string[] = readonly(['Priority', 'Oldest', 'Newest'])
|
||||
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = 15;
|
||||
const totalPages = computed(() => Math.ceil((filteredReports.value?.length || 0) / itemsPerPage));
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 15
|
||||
const totalPages = computed(() => Math.ceil((filteredReports.value?.length || 0) / itemsPerPage))
|
||||
|
||||
const fuse = computed(() => {
|
||||
if (!allReports.value || allReports.value.length === 0) return null;
|
||||
return new Fuse(allReports.value, {
|
||||
keys: [
|
||||
{
|
||||
name: "version.id",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "version.version_number",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "project.title",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "project.slug",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "version.files.filename",
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
name: "trace_type",
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
weight: 0.5,
|
||||
},
|
||||
"file_path",
|
||||
"project.id",
|
||||
"target.name",
|
||||
"target.slug",
|
||||
],
|
||||
includeScore: true,
|
||||
threshold: 0.4,
|
||||
});
|
||||
});
|
||||
if (!allReports.value || allReports.value.length === 0) return null
|
||||
return new Fuse(allReports.value, {
|
||||
keys: [
|
||||
{
|
||||
name: 'version.id',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: 'version.version_number',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: 'project.title',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: 'project.slug',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: 'version.files.filename',
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
name: 'trace_type',
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
weight: 0.5,
|
||||
},
|
||||
'file_path',
|
||||
'project.id',
|
||||
'target.name',
|
||||
'target.slug',
|
||||
],
|
||||
includeScore: true,
|
||||
threshold: 0.4,
|
||||
})
|
||||
})
|
||||
|
||||
const filteredReports = computed(() => {
|
||||
if (!allReports.value) return [];
|
||||
if (!allReports.value) return []
|
||||
|
||||
let filtered;
|
||||
let filtered
|
||||
|
||||
if (query.value && fuse.value) {
|
||||
const results = fuse.value.search(query.value);
|
||||
filtered = results.map((result) => result.item);
|
||||
} else {
|
||||
filtered = [...allReports.value];
|
||||
}
|
||||
if (query.value && fuse.value) {
|
||||
const results = fuse.value.search(query.value)
|
||||
filtered = results.map((result) => result.item)
|
||||
} else {
|
||||
filtered = [...allReports.value]
|
||||
}
|
||||
|
||||
if (currentFilterType.value === "Pending") {
|
||||
filtered = filtered.filter((report) => report.status === "pending");
|
||||
} else if (currentFilterType.value === "Approved") {
|
||||
filtered = filtered.filter((report) => report.status === "approved");
|
||||
} else if (currentFilterType.value === "Rejected") {
|
||||
filtered = filtered.filter((report) => report.status === "rejected");
|
||||
}
|
||||
if (currentFilterType.value === 'Pending') {
|
||||
filtered = filtered.filter((report) => report.status === 'pending')
|
||||
} else if (currentFilterType.value === 'Approved') {
|
||||
filtered = filtered.filter((report) => report.status === 'approved')
|
||||
} else if (currentFilterType.value === 'Rejected') {
|
||||
filtered = filtered.filter((report) => report.status === 'rejected')
|
||||
}
|
||||
|
||||
if (currentSortType.value === "Priority") {
|
||||
filtered.sort((a, b) => b.priority_score - a.priority_score);
|
||||
} else if (currentSortType.value === "Oldest") {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.detected_at).getTime();
|
||||
const dateB = new Date(b.detected_at).getTime();
|
||||
return dateA - dateB;
|
||||
});
|
||||
} else {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.detected_at).getTime();
|
||||
const dateB = new Date(b.detected_at).getTime();
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
if (currentSortType.value === 'Priority') {
|
||||
filtered.sort((a, b) => b.priority_score - a.priority_score)
|
||||
} else if (currentSortType.value === 'Oldest') {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.detected_at).getTime()
|
||||
const dateB = new Date(b.detected_at).getTime()
|
||||
return dateA - dateB
|
||||
})
|
||||
} else {
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.detected_at).getTime()
|
||||
const dateB = new Date(b.detected_at).getTime()
|
||||
return dateB - dateA
|
||||
})
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
return filtered
|
||||
})
|
||||
|
||||
const paginatedReports = computed(() => {
|
||||
if (!filteredReports.value) return [];
|
||||
const start = (currentPage.value - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
return filteredReports.value.slice(start, end);
|
||||
});
|
||||
if (!filteredReports.value) return []
|
||||
const start = (currentPage.value - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
return filteredReports.value.slice(start, end)
|
||||
})
|
||||
|
||||
function updateSearchResults() {
|
||||
currentPage.value = 1;
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
currentPage.value = page;
|
||||
currentPage.value = page
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<template>
|
||||
<p>Not yet implemented.</p>
|
||||
<p>Not yet implemented.</p>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user