Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 104 additions & 30 deletions apps/frontend/src/components/project-review/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
<template>
<ClientOnly>
<DefineReviewConversation>
<section class="flex min-h-0 min-w-0 flex-1 flex-col [&_.cm-editor]:text-xs">
<div class="flex shrink-0 flex-wrap items-center gap-2 pb-2.5">
<Tabs v-model:value="activeReviewTab" wrap :tabs="rightPanelTabs" />
<div class="ml-auto flex items-center gap-1">
<Tooltip v-if="!pending && project" text="Reset issues">
<Button
type="quiet"
size="sm"
icon-only
:disabled="resetting"
aria-label="Reset issues"
@click="resetIssuesModal?.show()"
>
<RotateCounterClockwiseIcon aria-hidden="true" />
</Button>
</Tooltip>
<IssuePicker ref="issuePicker" @selected="activeReviewTab = 'issues'" />
</div>
</div>
<IssueList v-show="activeReviewTab === 'issues'" />
<IssueList v-show="activeReviewTab === 're-review'" re-review />
<MessageThread v-show="activeReviewTab === 'thread'" ref="messageThread" />
</section>
<ReviewOutcomeButtons />
</DefineReviewConversation>
<NewModal
ref="resetIssuesModal"
header="Reset all issues"
Expand Down Expand Up @@ -42,38 +68,23 @@
</div>
</template>
</NewModal>
<ProjectReviewLayout :tabs="visibleTabs" :reset-key="selection" :ready="layoutReady">
<ProjectReviewLayout
:class="{ invisible: openingProject }"
:inert="openingProject"
:aria-hidden="openingProject || undefined"
:tabs="visibleTabs"
:reset-key="selection"
:ready="layoutReady"
>
<template #left><ProjectInfo /></template>
<template #right>
<div
v-if="!techReviewMode"
:key="projectId"
class="flex h-full min-h-0 min-w-0 flex-col gap-2.5 [&_.cm-editor]:text-xs"
>
<ProjectWideChecks :inert="pending" />
<section class="flex min-h-0 min-w-0 flex-1 flex-col">
<div class="flex shrink-0 flex-wrap items-center gap-2 pb-2.5">
<Tabs v-model:value="activeReviewTab" wrap :tabs="rightPanelTabs" />
<div class="ml-auto flex items-center gap-1">
<Tooltip v-if="!pending && project" text="Reset issues">
<Button
type="quiet"
size="sm"
icon-only
:disabled="resetting"
aria-label="Reset issues"
@click="resetIssuesModal?.show()"
>
<RotateCounterClockwiseIcon aria-hidden="true" />
</Button>
</Tooltip>
<IssuePicker ref="issuePicker" @selected="activeReviewTab = 'issues'" />
</div>
</div>
<IssueList v-show="activeReviewTab === 'issues'" />
<IssueList v-show="activeReviewTab === 're-review'" re-review />
<MessageThread v-show="activeReviewTab === 'thread'" ref="messageThread" />
</section>
<ReviewOutcomeButtons />
<ReviewConversation />
</div>
</template>
<template #footer><QueueBar /></template>
Expand All @@ -82,7 +93,15 @@
<template #disclosures><Disclosures :key="projectId" /></template>
<template #permissions><Permissions :key="projectId" /></template>
<template #versions><Versions :key="projectId" /></template>
<template #tech-review><TechReview :key="projectId" /></template>
<template #tech-review>
<TechReview
:key="projectId"
@add-code="addCodeToMessage"
@ready="techReviewReadyProject = $event"
>
<template v-if="techReviewMode" #conversation><ReviewConversation /></template>
</TechReview>
</template>
<template #slicer><Slicer :key="projectId" /></template>
</ProjectReviewLayout>
<template #fallback>
Expand All @@ -94,7 +113,8 @@
<script setup lang="ts">
import { RotateCounterClockwiseIcon, TrashIcon, XIcon } from '@modrinth/assets'
import { moderationSettings } from '@modrinth/moderation'
import { Button, injectLoadingState, NewModal, Tabs, Tooltip } from '@modrinth/ui'
import { Button, formatCodeBlock, injectLoadingState, NewModal, Tabs, Tooltip } from '@modrinth/ui'
import { createReusableTemplate } from '@vueuse/core'
import { computed, onScopeDispose, ref, watch } from 'vue'

import {
Expand Down Expand Up @@ -144,6 +164,8 @@ import Slicer from './slicer/index.vue'
import TechReview from './tech-review/index.vue'
import Versions from './versions/index.vue'

const [DefineReviewConversation, ReviewConversation] = createReusableTemplate()

const shortcuts = provideReviewShortcuts(createReviewShortcuts())
function useShortcut(...args: Parameters<typeof shortcuts.register>) {
onScopeDispose(shortcuts.register(...args))
Expand All @@ -164,6 +186,8 @@ const {
permissions,
selection,
isLoading,
initialLoading,
error,
navigation,
} = injectProjectReviewPageContext()
const layoutReady = computed(() => !!project.value && !isLoading.value && !navigation.busy.value)
Expand Down Expand Up @@ -231,9 +255,50 @@ const issueCount = computed(() => {
const reviewContext = provideReviewContext(
createReviewContext(reviewProjectId, (target) => !!panels.resolve(target), shortcuts),
)
const { pending, loadingAction } = provideReviewSubmission(
const { draft, pending, loadingAction, techReviewMode } = provideReviewSubmission(
createReviewSubmission(messages, panels, session, previousIssues),
)
const techReviewReadyProject = ref<string>()
const loadedSelection = ref<string>()
const openingProject = computed(
() => !!selection.value && loadedSelection.value !== selection.value,
)
watch(
selection,
() => {
loadedSelection.value = undefined
},
{ flush: 'sync' },
)
watch(
projectId,
() => {
techReviewReadyProject.value = undefined
},
{ flush: 'sync' },
)
watch(
[
selection,
initialLoading,
layoutReady,
error,
navigation.busy,
techReviewMode,
techReviewReadyProject,
],
() => {
if (initialLoading.value || navigation.busy.value) return
if (
!error.value &&
(!layoutReady.value ||
(techReviewMode.value && techReviewReadyProject.value !== projectId.value))
)
return
loadedSelection.value = selection.value
},
{ immediate: true, flush: 'post' },
)
const { resetting, resetIssues } = useReReviewIssues(
{ project, threadQuery },
session,
Expand All @@ -243,6 +308,7 @@ const { resetting, resetIssues } = useReReviewIssues(
const loadingState = injectLoadingState()
watch(
() =>
openingProject.value ||
isLoading.value ||
navigation.busy.value ||
(loadingAction.value !== undefined &&
Expand Down Expand Up @@ -273,6 +339,14 @@ const messageThread = ref<InstanceType<typeof MessageThread>>()
const auth = useAuthState()
const keybinds = useModerationKeybinds()

async function addCodeToMessage(code: string) {
if (pending.value) return
const separator = draft.value.length ? (draft.value.endsWith('\n') ? '\n' : '\n\n') : ''
draft.value += separator + formatCodeBlock(code, 'java')
activeReviewTab.value = 'thread'
await messageThread.value?.openEditor(true)
}

function resetAllIssues() {
resetIssuesModal.value?.hide()
void resetIssues()
Expand Down Expand Up @@ -315,7 +389,7 @@ watch(projectId, () => {
useShortcut('cycle-conversation', () => {
const tabs = ['issues', 'thread', ...(hasPreviousStructuredIssues.value ? ['re-review'] : [])]
activeReviewTab.value = tabs[(tabs.indexOf(activeReviewTab.value) + 1) % tabs.length]
shortcuts.run('reveal-right')
shortcuts.run('reveal-conversation')
})
useShortcut(
'new-issue',
Expand All @@ -333,7 +407,7 @@ useShortcut(
're-review',
() => {
void reviewContext.revealPanel('re-review')
shortcuts.run('reveal-right')
shortcuts.run('reveal-conversation')
},
() =>
hasPreviousStructuredIssues.value || !!panels.resolve({ kind: 'panel', key: 're-review-misc' }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { injectReviewShortcuts } from '../shortcuts'
const emit = defineEmits<{ selected: [] }>()
const panels = injectReviewPanels()
const reviewMessages = injectReviewMessages()
const { pending } = injectReviewSubmission()
const { pending, techReviewMode } = injectReviewSubmission()

const selected = computed(() => panels.activeIssues.value.map(({ id }) => id))
const picker = ref<{ close: () => void; open: () => Promise<void> }>()
Expand All @@ -64,7 +64,9 @@ const options = computed<MultiSelectItem<string>[]>(() => {
categories.set(issue.category, group)
}
const orderedCategories = [...categories].sort(
([a], [b]) => Number(b === 'Project wide') - Number(a === 'Project wide'),
([a], [b]) =>
(techReviewMode.value ? Number(b === 'Tech review') - Number(a === 'Tech review') : 0) ||
Number(b === 'Project wide') - Number(a === 'Project wide'),
)
for (const [category, issues] of orderedCategories) {
items.push({
Expand Down Expand Up @@ -129,7 +131,7 @@ watch(selected, (value) => {
})
async function openPicker() {
if (pending.value) return
shortcuts.run('reveal-right')
shortcuts.run('reveal-conversation')
await picker.value?.open()
}
defineExpose({ openPicker })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<div v-if="params.group.id === cornerGroupId" class="flex h-full items-center">
<div
v-if="params.group.id === cornerGroupId && (side !== 'right' || rightEnabled)"
class="flex h-full items-center"
>
<IconButton
type="quiet"
size="sm"
Expand Down Expand Up @@ -33,7 +36,7 @@ const props = defineProps<{
params: IDockviewHeaderActionsProps
}>()

const { leftVisible, rightVisible, topLeftGroupId, topRightGroupId, toggleSidebar } =
const { leftVisible, rightVisible, rightEnabled, topLeftGroupId, topRightGroupId, toggleSidebar } =
injectProjectReviewContext()
const visible = computed(() => (props.side === 'left' ? leftVisible.value : rightVisible.value))
const icon = computed(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'dockview-vue/dist/styles/dockview.css'
import { onScopeDispose, watch, watchEffect } from 'vue'

import { useModerationKeybinds } from '~/composables/moderation'
import { injectReviewSubmission } from '~/providers/project-review/review-submission'

import { injectReviewContext } from '../review-panel/context'
import { reviewShortcutBlocked, useReviewEventListener, useReviewShortcut } from '../shortcuts'
Expand Down Expand Up @@ -80,8 +81,20 @@ watch(
provideProjectReviewContext({ ...layout, slots })

useReviewShortcut('toggle-left', () => layout.toggleSidebar('left'))
useReviewShortcut('toggle-right', () => layout.toggleSidebar('right'))
useReviewShortcut('reveal-right', () => layout.revealSlot('right'))
useReviewShortcut(
'toggle-right',
() => layout.toggleSidebar('right'),
() => layout.rightEnabled.value,
)
useReviewShortcut(
'reveal-right',
() => layout.revealSlot('right'),
() => layout.rightEnabled.value,
)
const { techReviewMode } = injectReviewSubmission()
useReviewShortcut('reveal-conversation', () =>
layout.revealSlot(techReviewMode.value ? 'tech-review' : 'right'),
)

const keybinds = useModerationKeybinds()
useReviewEventListener('keydown', (event) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,50 @@ export const workspacePanelSizes = {
center: { minimum: 20 },
} as const

export interface SavedWorkspaceLayout {
export interface SavedWorkspacePanels {
leftWidth: number
rightWidth: number
leftVisible: boolean
rightVisible: boolean
}

export interface SavedWorkspaceLayout extends SavedWorkspacePanels {
tabs: SerializedDockview
tabLayouts?: Record<string, SerializedDockview>
}

export function readWorkspacePanels(): SavedWorkspacePanels | undefined {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null')
if (
saved &&
[saved.leftWidth, saved.rightWidth].every(
(size) => typeof size === 'number' && Number.isFinite(size) && size > 0,
) &&
typeof saved.leftVisible === 'boolean' &&
typeof saved.rightVisible === 'boolean'
) {
return {
leftWidth: saved.leftWidth,
rightWidth: saved.rightWidth,
leftVisible: saved.leftVisible,
rightVisible: saved.rightVisible,
}
}
} catch {
return
}
}

export function saveWorkspacePanels(panels: SavedWorkspacePanels) {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null')
localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...saved, ...panels }))
} catch {
// no-op
}
}

export function workspaceTabLayoutKey(tabs: readonly ProjectReviewTab[]) {
return projectReviewTabs.filter((tab) => tabs.includes(tab)).join(',')
}
Expand Down
Loading
Loading