mirror of
https://github.com/modrinth/code.git
synced 2026-08-02 06:05:54 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34de877c7d | ||
|
|
187cad2578 | ||
|
|
6c6bcd0b82 | ||
|
|
1957549141 | ||
|
|
8cbe4061a8 |
@@ -2,12 +2,7 @@
|
||||
<div class="shadow-card rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<NuxtLink
|
||||
:to="`/project/${queueEntry.project.slug}`"
|
||||
target="_blank"
|
||||
tabindex="-1"
|
||||
class="flex"
|
||||
>
|
||||
<NuxtLink :to="`/project/${projectRouteParam}`" target="_blank" tabindex="-1" class="flex">
|
||||
<Avatar
|
||||
:src="queueEntry.project.icon_url"
|
||||
size="4rem"
|
||||
@@ -17,7 +12,7 @@
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<NuxtLink
|
||||
:to="`/project/${queueEntry.project.slug}`"
|
||||
:to="`/project/${projectRouteParam}`"
|
||||
target="_blank"
|
||||
class="text-lg font-semibold text-contrast hover:underline"
|
||||
>
|
||||
@@ -177,7 +172,7 @@ function getDaysQueued(date: Date): number {
|
||||
const queuedDate = computed(() => {
|
||||
return dayjs(
|
||||
props.queueEntry.project.queued ||
|
||||
props.queueEntry.project.created ||
|
||||
props.queueEntry.project.published ||
|
||||
props.queueEntry.project.updated,
|
||||
)
|
||||
})
|
||||
@@ -186,10 +181,14 @@ const daysInQueue = computed(() => {
|
||||
return getDaysQueued(queuedDate.value.toDate())
|
||||
})
|
||||
|
||||
const projectRouteParam = computed(
|
||||
() => props.queueEntry.project.slug || props.queueEntry.project.id,
|
||||
)
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
const date =
|
||||
props.queueEntry.project.queued ||
|
||||
props.queueEntry.project.created ||
|
||||
props.queueEntry.project.published ||
|
||||
props.queueEntry.project.updated
|
||||
if (!date) return 'Unknown'
|
||||
|
||||
@@ -202,7 +201,7 @@ const formattedDate = computed(() => {
|
||||
|
||||
function copyLink() {
|
||||
const base = window.location.origin
|
||||
const projectUrl = `${base}/project/${props.queueEntry.project.slug}`
|
||||
const projectUrl = `${base}/project/${projectRouteParam.value}`
|
||||
navigator.clipboard.writeText(projectUrl).then(() => {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
|
||||
@@ -1949,7 +1949,10 @@ function generateModpackMessage(allFiles: {
|
||||
|
||||
const hasNextProject = ref(false)
|
||||
async function refreshModerationCaches(threadId?: string) {
|
||||
const refreshes: Promise<unknown>[] = [invalidate(), refreshNuxtData('moderation-projects')]
|
||||
const refreshes: Promise<unknown>[] = [
|
||||
invalidate(),
|
||||
queryClient.invalidateQueries({ queryKey: ['moderation-projects'] }),
|
||||
]
|
||||
|
||||
if (threadId) {
|
||||
refreshes.push(queryClient.invalidateQueries({ queryKey: ['thread', threadId] }))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { ExtendedReport, OwnershipTarget } from '@modrinth/moderation'
|
||||
import type {
|
||||
Organization,
|
||||
@@ -197,14 +198,10 @@ export interface ModerationOwnershipOrganization {
|
||||
|
||||
export type ModerationOwnership = ModerationOwnershipUser | ModerationOwnershipOrganization
|
||||
|
||||
export interface ProjectWithOwnership {
|
||||
ownership: ModerationOwnership
|
||||
external_dependencies_count: number
|
||||
[key: string]: any
|
||||
}
|
||||
export type ProjectWithOwnership = Labrinth.Moderation.Internal.QueueProject
|
||||
|
||||
export interface ModerationProject {
|
||||
project: any
|
||||
project: Omit<ProjectWithOwnership, 'ownership' | 'external_dependencies_count'>
|
||||
ownership: ModerationOwnership | null
|
||||
external_dependencies_count: number
|
||||
}
|
||||
|
||||
@@ -2672,6 +2672,9 @@
|
||||
"layout.nav.upgrade-to-modrinth-plus": {
|
||||
"message": "Upgrade to Modrinth+"
|
||||
},
|
||||
"moderation.exclude-technical-review": {
|
||||
"message": "Exclude TR"
|
||||
},
|
||||
"moderation.moderate": {
|
||||
"message": "Moderate"
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<Combobox
|
||||
v-model="currentFilterType"
|
||||
class="!w-full flex-grow sm:!w-[280px] sm:flex-grow-0 lg:!w-[280px]"
|
||||
trigger-class="!h-10"
|
||||
:options="filterTypes"
|
||||
:placeholder="formatMessage(commonMessages.filterByLabel)"
|
||||
@select="goToPage(1)"
|
||||
@@ -26,7 +27,7 @@
|
||||
<span class="flex flex-row gap-2 align-middle font-semibold">
|
||||
<ListFilterIcon class="size-5 flex-shrink-0 text-secondary" />
|
||||
<span class="truncate text-contrast"
|
||||
>{{ currentFilterType }} ({{ filteredProjects.length }})</span
|
||||
>{{ currentFilterType }} ({{ totalProjects }})</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
@@ -35,6 +36,7 @@
|
||||
<Combobox
|
||||
v-model="currentSortType"
|
||||
class="!w-full flex-grow sm:!w-[240px] sm:flex-grow-0"
|
||||
trigger-class="!h-10"
|
||||
:options="sortTypes"
|
||||
:placeholder="formatMessage(commonMessages.sortByLabel)"
|
||||
@select="goToPage(1)"
|
||||
@@ -54,6 +56,7 @@
|
||||
<Combobox
|
||||
v-model="itemsPerPage"
|
||||
class="!w-full flex-grow sm:!w-[160px] sm:flex-grow-0 lg:!w-[140px]"
|
||||
trigger-class="!h-10"
|
||||
:options="itemsPerPageOptions"
|
||||
placeholder="Items per page"
|
||||
@select="goToPage(1)"
|
||||
@@ -69,7 +72,7 @@
|
||||
<ButtonStyled color="orange">
|
||||
<button
|
||||
class="flex !h-[40px] w-full items-center justify-center gap-2 sm:w-auto"
|
||||
:disabled="paginatedProjects?.length === 0"
|
||||
:disabled="pending || paginatedProjects?.length === 0"
|
||||
@click="moderateAllInFilter()"
|
||||
>
|
||||
<ScaleIcon class="flex-shrink-0" />
|
||||
@@ -80,17 +83,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-between">
|
||||
<div>
|
||||
Showing {{ itemsPerPage * (currentPage - 1) + 1 }}–{{
|
||||
itemsPerPage * (currentPage - 1) + Math.min(itemsPerPage, paginatedProjects.length)
|
||||
}}
|
||||
of {{ filteredProjects.length }}
|
||||
{{
|
||||
currentFilterType === DEFAULT_FILTER_TYPE ? 'projects' : currentFilterType.toLowerCase()
|
||||
}}
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div v-if="totalProjects > 0">
|
||||
Showing {{ pageStart }}–{{ pageEnd }} of {{ totalProjects }}
|
||||
{{
|
||||
currentFilterType === DEFAULT_FILTER_TYPE ? 'projects' : currentFilterType.toLowerCase()
|
||||
}}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-secondary">
|
||||
<Toggle id="moderation-exclude-technical-review" v-model="excludeTechnicalReview" small />
|
||||
<label class="cursor-pointer" for="moderation-exclude-technical-review">
|
||||
{{ formatMessage(messages.excludeTechnicalReview) }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination :page="currentPage" :count="totalPages" @switch-page="goToPage" />
|
||||
<Pagination
|
||||
v-if="totalPages > 1"
|
||||
:page="currentPage"
|
||||
:count="totalPages"
|
||||
@switch-page="goToPage"
|
||||
/>
|
||||
<ConfettiExplosion v-if="visible" />
|
||||
</div>
|
||||
|
||||
@@ -124,6 +137,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ListFilterIcon, ScaleIcon, SearchIcon, SortAscIcon, SortDescIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
@@ -132,20 +146,18 @@ import {
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
EmptyState,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
Pagination,
|
||||
StyledInput,
|
||||
Toggle,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import ConfettiExplosion from 'vue-confetti-explosion'
|
||||
|
||||
import ModerationQueueCard from '~/components/ui/moderation/ModerationQueueCard.vue'
|
||||
import {
|
||||
type ModerationProject,
|
||||
type ProjectWithOwnership,
|
||||
toModerationProjects,
|
||||
} from '~/helpers/moderation.ts'
|
||||
import { type ModerationProject, toModerationProjects } from '~/helpers/moderation.ts'
|
||||
import { useModerationQueue } from '~/services/moderation-queue.ts'
|
||||
|
||||
useHead({ title: 'Projects queue - Modrinth' })
|
||||
@@ -155,6 +167,7 @@ const { addNotification } = injectNotificationManager()
|
||||
const moderationQueue = useModerationQueue()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const visible = ref(false)
|
||||
if (import.meta.client && history && history.state && history.state.confetti) {
|
||||
@@ -173,37 +186,14 @@ const messages = defineMessages({
|
||||
id: 'moderation.moderate',
|
||||
defaultMessage: 'Moderate',
|
||||
},
|
||||
})
|
||||
|
||||
const { data: allProjects, pending } = await useLazyAsyncData('moderation-projects', async () => {
|
||||
const startTime = performance.now()
|
||||
let currentOffset = 0
|
||||
const PROJECT_ENDPOINT_COUNT = 350
|
||||
const allProjects: ModerationProject[] = []
|
||||
|
||||
let projects: ProjectWithOwnership[] = []
|
||||
do {
|
||||
projects = (await useBaseFetch(
|
||||
`moderation/projects?count=${PROJECT_ENDPOINT_COUNT}&offset=${currentOffset}`,
|
||||
{ internal: true },
|
||||
)) as ProjectWithOwnership[]
|
||||
|
||||
if (projects.length === 0) break
|
||||
|
||||
allProjects.push(...toModerationProjects(projects))
|
||||
currentOffset += projects.length
|
||||
} while (projects.length === PROJECT_ENDPOINT_COUNT)
|
||||
|
||||
const duration = performance.now() - startTime
|
||||
|
||||
console.debug(
|
||||
`Projects fetched and processed in ${duration.toFixed(2)}ms (${(duration / 1000).toFixed(2)}s)`,
|
||||
)
|
||||
|
||||
return allProjects
|
||||
excludeTechnicalReview: {
|
||||
id: 'moderation.exclude-technical-review',
|
||||
defaultMessage: 'Exclude TR',
|
||||
},
|
||||
})
|
||||
|
||||
const query = ref(route.query.q?.toString() || '')
|
||||
const excludeTechnicalReview = ref(false)
|
||||
|
||||
watch(
|
||||
query,
|
||||
@@ -379,116 +369,106 @@ const itemsPerPage = computed({
|
||||
})
|
||||
|
||||
const currentPage = ref(1)
|
||||
const totalPages = computed(() =>
|
||||
Math.ceil((filteredProjects.value?.length || 0) / itemsPerPage.value),
|
||||
|
||||
function toApiProjectType(label: string): string | undefined {
|
||||
switch (label) {
|
||||
case 'Modpacks':
|
||||
return 'modpack'
|
||||
case 'Mods':
|
||||
return 'mod'
|
||||
case 'Resource Packs':
|
||||
return 'resourcepack'
|
||||
case 'Data Packs':
|
||||
return 'datapack'
|
||||
case 'Plugins':
|
||||
return 'plugin'
|
||||
case 'Shaders':
|
||||
return 'shader'
|
||||
case 'Servers':
|
||||
return 'minecraft_java_server'
|
||||
case 'Fucked up':
|
||||
return 'none'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function toApiSort(label: string): Labrinth.Moderation.Internal.ProjectsSort {
|
||||
switch (label) {
|
||||
case 'Newest':
|
||||
return 'newest'
|
||||
case 'Most external deps':
|
||||
return 'most_external_deps'
|
||||
case 'Least external deps':
|
||||
return 'least_external_deps'
|
||||
default:
|
||||
return 'oldest'
|
||||
}
|
||||
}
|
||||
|
||||
const moderationProjectsRequest = computed<Labrinth.Moderation.Internal.ProjectsRequest>(() => ({
|
||||
count: itemsPerPage.value,
|
||||
offset: (currentPage.value - 1) * itemsPerPage.value,
|
||||
exclude_technical_review: excludeTechnicalReview.value,
|
||||
query: query.value || undefined,
|
||||
project_type: toApiProjectType(currentFilterType.value),
|
||||
sort: toApiSort(currentSortType.value),
|
||||
}))
|
||||
|
||||
const moderationProjectsQueryKey = computed(
|
||||
() => ['moderation-projects', moderationProjectsRequest.value] as const,
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
'ownership.name',
|
||||
],
|
||||
includeScore: true,
|
||||
threshold: 0.4,
|
||||
})
|
||||
const {
|
||||
data: moderationProjectsResponse,
|
||||
isPending: moderationProjectsPending,
|
||||
isPlaceholderData: moderationProjectsPlaceholder,
|
||||
} = useQuery({
|
||||
queryKey: moderationProjectsQueryKey,
|
||||
queryFn: ({ queryKey }) => client.labrinth.moderation_internal.getProjects(queryKey[1]),
|
||||
placeholderData: (previousData) => previousData,
|
||||
})
|
||||
|
||||
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]
|
||||
})
|
||||
|
||||
const typeFiltered = computed(() => {
|
||||
if (currentFilterType.value === 'All projects') {
|
||||
return baseFiltered.value
|
||||
} else if (currentFilterType.value === 'Fucked up') {
|
||||
return baseFiltered.value.filter((queueItem) => queueItem.project.project_types.length === 0)
|
||||
const pending = computed(
|
||||
() => moderationProjectsPending.value || moderationProjectsPlaceholder.value,
|
||||
)
|
||||
const totalProjects = computed(() => moderationProjectsResponse.value?.total ?? 0)
|
||||
const totalPages = computed(() => Math.ceil(totalProjects.value / itemsPerPage.value))
|
||||
const filteredProjects = computed(() =>
|
||||
toModerationProjects(moderationProjectsResponse.value?.projects ?? []),
|
||||
)
|
||||
const paginatedProjects = computed(() => filteredProjects.value)
|
||||
const pageStart = computed(() =>
|
||||
totalProjects.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1,
|
||||
)
|
||||
const pageEnd = computed(() =>
|
||||
Math.min(
|
||||
(currentPage.value - 1) * itemsPerPage.value + paginatedProjects.value.length,
|
||||
totalProjects.value,
|
||||
),
|
||||
)
|
||||
const projectsById = computed(() => {
|
||||
const projects = new Map<string, ModerationProject>()
|
||||
for (const project of filteredProjects.value) {
|
||||
projects.set(project.project.id, project)
|
||||
}
|
||||
|
||||
const filterMap: Record<string, string> = {
|
||||
Modpacks: 'modpack',
|
||||
Mods: 'mod',
|
||||
'Resource Packs': 'resourcepack',
|
||||
'Data Packs': 'datapack',
|
||||
Plugins: 'plugin',
|
||||
Shaders: 'shader',
|
||||
Servers: 'minecraft_java_server',
|
||||
}
|
||||
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) ||
|
||||
(projectType === 'minecraft_java_server' &&
|
||||
queueItem.project.project_types.includes('minecraft_java_server')),
|
||||
)
|
||||
return projects
|
||||
})
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
const filtered = [...typeFiltered.value]
|
||||
|
||||
if (currentSortType.value === 'Most external deps') {
|
||||
filtered.sort((a, b) => {
|
||||
const depsDiff = b.external_dependencies_count - a.external_dependencies_count
|
||||
if (depsDiff !== 0) return depsDiff
|
||||
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 if (currentSortType.value === 'Least external deps') {
|
||||
filtered.sort((a, b) => {
|
||||
const depsDiff = a.external_dependencies_count - b.external_dependencies_count
|
||||
if (depsDiff !== 0) return depsDiff
|
||||
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 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
|
||||
})
|
||||
watch(totalPages, (pages) => {
|
||||
if (pages === 0 && currentPage.value !== 1) {
|
||||
currentPage.value = 1
|
||||
return
|
||||
}
|
||||
|
||||
return filtered
|
||||
if (pages > 0 && currentPage.value > pages) {
|
||||
currentPage.value = pages
|
||||
}
|
||||
})
|
||||
|
||||
const paginatedProjects = computed(() => {
|
||||
if (!filteredProjects.value) return []
|
||||
const start = (currentPage.value - 1) * itemsPerPage.value
|
||||
const end = start + itemsPerPage.value
|
||||
return filteredProjects.value.slice(start, end)
|
||||
watch(excludeTechnicalReview, () => {
|
||||
goToPage(1)
|
||||
})
|
||||
|
||||
const emptyStateHeading = computed(() => {
|
||||
@@ -525,21 +505,16 @@ function notifySkippedProjects(skippedCount: number) {
|
||||
})
|
||||
}
|
||||
|
||||
async function findFirstEligibleProject(): Promise<ModerationProject | null> {
|
||||
async function findFirstEligibleProject(): Promise<string | null> {
|
||||
let skippedCount = 0
|
||||
|
||||
while (moderationQueue.hasItems) {
|
||||
const currentId = moderationQueue.getCurrentProjectId()
|
||||
if (!currentId) return null
|
||||
|
||||
const project = filteredProjects.value.find((p) => p.project.id === currentId)
|
||||
if (!project) {
|
||||
await moderationQueue.completeCurrentProject(currentId, 'skipped')
|
||||
skippedCount++
|
||||
continue
|
||||
}
|
||||
const project = projectsById.value.get(currentId)
|
||||
|
||||
if (project.project.status !== 'processing') {
|
||||
if (project && project.project.status !== 'processing') {
|
||||
await moderationQueue.completeCurrentProject(currentId, 'skipped')
|
||||
skippedCount++
|
||||
continue
|
||||
@@ -550,13 +525,13 @@ async function findFirstEligibleProject(): Promise<ModerationProject | null> {
|
||||
|
||||
if (!lockStatus.locked || lockStatus.expired || lockStatus.is_own_lock) {
|
||||
notifySkippedProjects(skippedCount)
|
||||
return project
|
||||
return currentId
|
||||
}
|
||||
|
||||
await moderationQueue.completeCurrentProject(currentId, 'skipped')
|
||||
skippedCount++
|
||||
} catch {
|
||||
return project
|
||||
return currentId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,17 +540,42 @@ async function findFirstEligibleProject(): Promise<ModerationProject | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
function getProjectRouteParam(projectId: string): string {
|
||||
return projectsById.value.get(projectId)?.project.slug || projectId
|
||||
}
|
||||
|
||||
async function navigateToModerationProject(projectId: string) {
|
||||
await navigateTo({
|
||||
name: 'type-project',
|
||||
params: {
|
||||
type: 'project',
|
||||
project: getProjectRouteParam(projectId),
|
||||
},
|
||||
state: {
|
||||
showChecklist: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function getFilteredProjectIds(): Promise<string[]> {
|
||||
const response = await client.labrinth.moderation_internal.getProjectIds({
|
||||
exclude_technical_review: excludeTechnicalReview.value,
|
||||
query: query.value || undefined,
|
||||
project_type: toApiProjectType(currentFilterType.value),
|
||||
sort: toApiSort(currentSortType.value),
|
||||
})
|
||||
|
||||
return response.ids
|
||||
}
|
||||
|
||||
async function moderateAllInFilter() {
|
||||
// Start from the current page - get projects from current page onwards
|
||||
const startIndex = (currentPage.value - 1) * itemsPerPage.value
|
||||
const projectsFromCurrentPage = filteredProjects.value.slice(startIndex)
|
||||
const projectIds = projectsFromCurrentPage.map((queueItem) => queueItem.project.id)
|
||||
const projectIds = (await getFilteredProjectIds()).slice(startIndex)
|
||||
await moderationQueue.setQueue(projectIds)
|
||||
|
||||
// Find first unlocked project
|
||||
const targetProject = await findFirstEligibleProject()
|
||||
const targetProjectId = await findFirstEligibleProject()
|
||||
|
||||
if (!targetProject) {
|
||||
if (!targetProjectId) {
|
||||
addNotification({
|
||||
title: 'No projects available',
|
||||
text: 'All projects in queue are already moderated or locked by others.',
|
||||
@@ -584,34 +584,22 @@ async function moderateAllInFilter() {
|
||||
return
|
||||
}
|
||||
|
||||
navigateTo({
|
||||
name: 'type-project',
|
||||
params: {
|
||||
type: 'project',
|
||||
project: targetProject.project.slug,
|
||||
},
|
||||
state: {
|
||||
showChecklist: true,
|
||||
},
|
||||
})
|
||||
await navigateToModerationProject(targetProjectId)
|
||||
}
|
||||
|
||||
async function startFromProject(projectId: string) {
|
||||
// Find the index of the clicked project in the filtered list
|
||||
const projectIndex = filteredProjects.value.findIndex((p) => p.project.id === projectId)
|
||||
const allFilteredProjectIds = await getFilteredProjectIds()
|
||||
const projectIndex = allFilteredProjectIds.indexOf(projectId)
|
||||
if (projectIndex === -1) {
|
||||
// Project not found in filtered list, just moderate it alone
|
||||
await moderationQueue.setSingleProject(projectId)
|
||||
} else {
|
||||
// Start queue from this project onwards
|
||||
const projectsFromHere = filteredProjects.value.slice(projectIndex)
|
||||
const projectIds = projectsFromHere.map((queueItem) => queueItem.project.id)
|
||||
const projectIds = allFilteredProjectIds.slice(projectIndex)
|
||||
await moderationQueue.setQueue(projectIds)
|
||||
}
|
||||
|
||||
const targetProject = await findFirstEligibleProject()
|
||||
const targetProjectId = await findFirstEligibleProject()
|
||||
|
||||
if (!targetProject) {
|
||||
if (!targetProjectId) {
|
||||
addNotification({
|
||||
title: 'No projects available',
|
||||
text: 'All projects in queue are already moderated or locked by others.',
|
||||
@@ -620,15 +608,6 @@ async function startFromProject(projectId: string) {
|
||||
return
|
||||
}
|
||||
|
||||
navigateTo({
|
||||
name: 'type-project',
|
||||
params: {
|
||||
type: 'project',
|
||||
project: targetProject.project.slug,
|
||||
},
|
||||
state: {
|
||||
showChecklist: true,
|
||||
},
|
||||
})
|
||||
await navigateToModerationProject(targetProjectId)
|
||||
}
|
||||
</script>
|
||||
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n select id\n from mods\n where id = any($1)\n and status not in ('rejected', 'draft', 'withheld', 'withdrawn')\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd"
|
||||
}
|
||||
Generated
-31
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id,\n external_dependencies_count as \"external_dependencies_count!\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id,\n m.queued,\n (\n SELECT COUNT(*)\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE v.mod_id = m.id\n AND d.dependency_file_name IS NOT NULL\n ) external_dependencies_count\n FROM mods m\n\n /* -- Temporarily, don't exclude projects in tech rev q\n\n -- exclude projects in tech review queue\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = m.id AND didws.status = 'pending'\n */\n\n WHERE\n m.status = $1\n /* AND didws.status IS NULL */ -- Temporarily don't exclude\n\n GROUP BY m.id\n ) t\n WHERE\n ($4::boolean IS NULL OR (external_dependencies_count > 0) = $4)\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "external_dependencies_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "119a59fcf4bb2f19f89002c712a67c75d30056143c0bcabdbd74bb4c7b442082"
|
||||
}
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d"
|
||||
}
|
||||
Generated
+28
File diff suppressed because one or more lines are too long
Generated
+135
File diff suppressed because one or more lines are too long
Generated
+138
File diff suppressed because one or more lines are too long
@@ -101,6 +101,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
pats::edit_pat,
|
||||
pats::delete_pat,
|
||||
moderation::get_projects,
|
||||
moderation::get_project_ids,
|
||||
moderation::get_project_meta,
|
||||
moderation::set_project_meta,
|
||||
moderation::acquire_lock,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
pub(crate) mod moderation;
|
||||
mod notifications;
|
||||
mod openapi;
|
||||
pub(crate) mod project_creation;
|
||||
@@ -26,7 +25,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.configure(super::internal::flows::config)
|
||||
.configure(super::internal::pats::config)
|
||||
.configure(super::internal::admin::config)
|
||||
.configure(moderation::config)
|
||||
.configure(notifications::config)
|
||||
.configure(project_creation::config)
|
||||
.configure(projects::config)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
use super::ApiError;
|
||||
use crate::database::PgPool;
|
||||
use crate::models::projects::Project;
|
||||
use crate::models::v2::projects::LegacyProject;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::internal;
|
||||
use crate::{database::redis::RedisPool, routes::v2_reroute};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(web::scope("/moderation").service(get_projects));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultCount {
|
||||
#[serde(default = "default_count")]
|
||||
pub count: u16,
|
||||
}
|
||||
|
||||
fn default_count() -> u16 {
|
||||
100
|
||||
}
|
||||
|
||||
/// List projects in the moderation queue.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation",
|
||||
tag = "v2 moderation",
|
||||
get,
|
||||
operation_id = "getModerationProjects",
|
||||
params(
|
||||
("count" = Option<u16>, Query, description = "Maximum number of projects to return")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Expected response to a valid request", body = Vec<LegacyProject>),
|
||||
(
|
||||
status = 401,
|
||||
description = "Incorrect token scopes or no authorization to access the requested item(s)"
|
||||
),
|
||||
(
|
||||
status = 404,
|
||||
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
|
||||
)
|
||||
),
|
||||
security(("bearer_auth" = ["PROJECT_READ"]))
|
||||
)]
|
||||
#[get("/projects")]
|
||||
pub async fn get_projects(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
count: web::Query<ResultCount>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let response = internal::moderation::get_projects_internal(
|
||||
req,
|
||||
pool.clone(),
|
||||
redis.clone(),
|
||||
web::Query(internal::moderation::ProjectsRequestOptions {
|
||||
count: count.count,
|
||||
offset: 0,
|
||||
has_external_dependencies: None,
|
||||
}),
|
||||
session_queue,
|
||||
)
|
||||
.await
|
||||
.map(|resp| HttpResponse::Ok().json(resp))
|
||||
.or_else(v2_reroute::flatten_404_error)?;
|
||||
|
||||
// Convert to V2 projects
|
||||
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
|
||||
Ok(project) => {
|
||||
let legacy_projects =
|
||||
LegacyProject::from_many(project, &**pool, &redis).await?;
|
||||
Ok(HttpResponse::Ok().json(legacy_projects))
|
||||
}
|
||||
Err(response) => Ok(response),
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,34 @@ export class LabrinthModerationInternalModule extends AbstractModule {
|
||||
return 'labrinth_moderation_internal'
|
||||
}
|
||||
|
||||
public async getProjects(
|
||||
params: Labrinth.Moderation.Internal.ProjectsRequest = {},
|
||||
): Promise<Labrinth.Moderation.Internal.ProjectsResponse> {
|
||||
return this.client.request<Labrinth.Moderation.Internal.ProjectsResponse>(
|
||||
'/moderation/projects',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
params,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async getProjectIds(
|
||||
params: Omit<Labrinth.Moderation.Internal.ProjectsRequest, 'count' | 'offset'> = {},
|
||||
): Promise<Labrinth.Moderation.Internal.ProjectIdsResponse> {
|
||||
return this.client.request<Labrinth.Moderation.Internal.ProjectIdsResponse>(
|
||||
'/moderation/projects/ids',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
params,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async acquireLock(
|
||||
projectId: string,
|
||||
): Promise<Labrinth.Moderation.Internal.LockAcquireResponse> {
|
||||
|
||||
@@ -1914,6 +1914,57 @@ export namespace Labrinth {
|
||||
|
||||
export namespace Moderation {
|
||||
export namespace Internal {
|
||||
export type Ownership =
|
||||
| {
|
||||
kind: 'user'
|
||||
id: string
|
||||
name: string
|
||||
icon_url: string | null
|
||||
}
|
||||
| {
|
||||
kind: 'organization'
|
||||
id: string
|
||||
name: string
|
||||
icon_url: string | null
|
||||
}
|
||||
|
||||
export type ProjectsSort = 'oldest' | 'newest' | 'most_external_deps' | 'least_external_deps'
|
||||
|
||||
export type ProjectsRequest = {
|
||||
count?: number
|
||||
offset?: number
|
||||
has_external_dependencies?: boolean
|
||||
exclude_technical_review?: boolean
|
||||
query?: string
|
||||
project_type?: string
|
||||
sort?: ProjectsSort
|
||||
}
|
||||
|
||||
export type QueueProject = {
|
||||
id: string
|
||||
slug: string | null
|
||||
name: string
|
||||
summary: string
|
||||
icon_url: string | null
|
||||
status: Projects.v2.ProjectStatus
|
||||
requested_status: Projects.v2.ProjectStatus | null
|
||||
queued: string | null
|
||||
published: string
|
||||
updated: string
|
||||
project_types: string[]
|
||||
ownership: Ownership
|
||||
external_dependencies_count: number
|
||||
}
|
||||
|
||||
export type ProjectsResponse = {
|
||||
total: number
|
||||
projects: QueueProject[]
|
||||
}
|
||||
|
||||
export type ProjectIdsResponse = {
|
||||
ids: string[]
|
||||
}
|
||||
|
||||
export type LockedByUser = {
|
||||
id: string
|
||||
username: string
|
||||
|
||||
Reference in New Issue
Block a user