mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 17:14:50 +00:00
feat: moderation settings (#6895)
* feat: moderation settings * chore: move moderation specific feature flag to settings * feat: force open collapsed regions if opened in new tab * chore: remove unused field * chore: run prepr
This commit is contained in:
@@ -300,6 +300,7 @@ type SharedInstanceVersionDependency = Labrinth.Versions.v2.Dependency & {
|
||||
|
||||
const props = defineProps<{
|
||||
report: ExtendedReport
|
||||
collapsed: boolean
|
||||
sharedInstanceDetailsLoader?: () => Promise<SharedInstanceReportDetails>
|
||||
sharedInstanceVersionContentLoader?: (
|
||||
instanceId: string,
|
||||
@@ -311,7 +312,7 @@ const reportThread = ref<{
|
||||
setReplyContent: (content: string) => void
|
||||
sendReply: (privateMessage?: boolean) => Promise<void>
|
||||
} | null>(null)
|
||||
const isThreadCollapsed = ref(true)
|
||||
const isThreadCollapsed = ref(props.collapsed)
|
||||
const sharedInstanceDetails = ref<SharedInstanceReportDetails | null>(null)
|
||||
const sharedInstanceLoading = ref(false)
|
||||
const sharedInstanceError = ref<string | null>(null)
|
||||
|
||||
@@ -94,6 +94,7 @@ const props = defineProps<{
|
||||
focusedDetailId?: string | null
|
||||
loadingIssues: Set<string>
|
||||
decompiledSources: Map<string, string>
|
||||
collapsed: boolean
|
||||
}>()
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -173,7 +174,7 @@ type Tab = 'Thread' | 'Files' | 'File'
|
||||
const tabs: readonly ('Thread' | 'Files')[] = ['Thread', 'Files']
|
||||
const currentTab = ref<Tab>('Thread')
|
||||
|
||||
const isThreadCollapsed = ref(true)
|
||||
const isThreadCollapsed = ref(props.collapsed)
|
||||
|
||||
const remainingMessageCount = computed(() => {
|
||||
if (!props.item.thread?.messages) return 0
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<kbd
|
||||
v-for="(definition, index) in definitions"
|
||||
:key="`keybind-${index}`"
|
||||
ref="keybinding"
|
||||
class="cursor-pointer border-2 !text-lg font-bold"
|
||||
:class="{
|
||||
editing: editing === index,
|
||||
}"
|
||||
@click="startEditing(index)"
|
||||
>
|
||||
{{ toDisplay(definition) }}
|
||||
</kbd>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type KeybindDefinition, toKeybindDefinition } from '@modrinth/moderation'
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
definitions: KeybindDefinition[]
|
||||
onChange: (definitions: KeybindDefinition[]) => void
|
||||
}>()
|
||||
|
||||
const keybinding = useTemplateRef('keybinding')
|
||||
const definitions = ref(JSON.parse(JSON.stringify(props.definitions)))
|
||||
const editing = ref(-1)
|
||||
|
||||
function startEditing(index: number) {
|
||||
if (editing.value === index) {
|
||||
stopEditing()
|
||||
} else {
|
||||
editing.value = index
|
||||
window.addEventListener('keyup', handleKeybinds)
|
||||
window.addEventListener('click', handleMouse)
|
||||
}
|
||||
}
|
||||
|
||||
function stopEditing() {
|
||||
console.log('stop editing')
|
||||
|
||||
editing.value = -1
|
||||
window.removeEventListener('keyup', handleKeybinds)
|
||||
window.removeEventListener('click', handleMouse)
|
||||
}
|
||||
|
||||
function handleMouse(event: MouseEvent) {
|
||||
if (keybinding.value && event.target && editing.value != -1) {
|
||||
const editingRef = keybinding.value[editing.value]
|
||||
if (editingRef === event.target || editingRef.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stopEditing()
|
||||
}
|
||||
|
||||
function handleKeybinds(event: KeyboardEvent) {
|
||||
definitions.value[editing.value] = toKeybindDefinition(event)
|
||||
props.onChange(definitions.value)
|
||||
stopEditing()
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function toDisplay(definition: KeybindDefinition): string {
|
||||
const keys = []
|
||||
|
||||
if (definition.ctrl || definition.meta) {
|
||||
keys.push(isMac() ? 'CMD' : 'CTRL')
|
||||
}
|
||||
if (definition.shift) keys.push('SHIFT')
|
||||
if (definition.alt) keys.push('ALT')
|
||||
|
||||
const mainKey = definition.key
|
||||
.toUpperCase()
|
||||
.replace('ARROWLEFT', '←')
|
||||
.replace('ARROWRIGHT', '→')
|
||||
.replace('ARROWUP', '↑')
|
||||
.replace('ARROWDOWN', '↓')
|
||||
.replace('ENTER', '↵')
|
||||
.replace('ESCAPE', 'ESC')
|
||||
|
||||
keys.push(mainKey)
|
||||
|
||||
return keys.join(' + ')
|
||||
}
|
||||
|
||||
function isMac() {
|
||||
return navigator.platform.toUpperCase().includes('MAC')
|
||||
}
|
||||
|
||||
onUnmounted(stopEditing)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editing {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
border-color: var(--color-red);
|
||||
box-shadow: 0 0 10px 1px var(--color-red);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Moderation shortcuts" :closable="true">
|
||||
<div id="moderation-checklist-keybinds-modal">
|
||||
<div class="keybinds-sections">
|
||||
<div class="grid grid-cols-2 gap-x-12 gap-y-3">
|
||||
<div
|
||||
v-for="[id, keybind] in Object.entries(keybinds)"
|
||||
:key="id"
|
||||
class="keybind-item flex flex-wrap items-center justify-between gap-4"
|
||||
:class="{
|
||||
'col-span-2':
|
||||
Object.keys(keybinds).length % 2 === 1 &&
|
||||
Object.keys(keybinds)[Object.keys(keybinds).length - 1] === id,
|
||||
}"
|
||||
>
|
||||
<span class="text-sm text-secondary">{{ keybind.description }}</span>
|
||||
<ChecklistKeybind
|
||||
:definitions="
|
||||
(!Array.isArray(keybind.keybind) ? [keybind.keybind] : keybind.keybind).map(
|
||||
normalizeKeybind,
|
||||
)
|
||||
"
|
||||
:on-change="
|
||||
(definitions) => {
|
||||
keybinds[id].keybind = definitions
|
||||
saveModerationKeybinds()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeKeybind } from '@modrinth/moderation'
|
||||
import NewModal from '@modrinth/ui/src/components/modal/NewModal.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { saveModerationKeybinds } from '#imports'
|
||||
import ChecklistKeybind from '~/components/ui/moderation/checklist/ChecklistKeybind.vue'
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const keybinds = useModerationKeybinds()
|
||||
|
||||
function show(event?: MouseEvent) {
|
||||
modal.value?.show(event)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@media (max-width: 768px) {
|
||||
.keybinds-sections {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<KeybindsModal ref="keybindsModal" />
|
||||
<ConfirmModal
|
||||
v-if="isLockedByOther"
|
||||
ref="takeOverModal"
|
||||
@@ -13,7 +12,12 @@
|
||||
<div
|
||||
tabindex="0"
|
||||
class="moderation-checklist flex max-h-[calc(100vh-2rem)] w-[600px] max-w-full flex-col overflow-hidden rounded-2xl border-[1px] border-solid border-orange bg-bg-raised p-4 transition-all delay-200 duration-200 ease-in-out"
|
||||
:class="{ '!w-fit': collapsed, locked: isLockedByOther }"
|
||||
:class="{
|
||||
'!w-fit': collapsed,
|
||||
locked: isLockedByOther,
|
||||
'right-4': settings.get(moderationSettings.General.ChecklistPosition) === 'right',
|
||||
'left-4': settings.get(moderationSettings.General.ChecklistPosition) === 'left',
|
||||
}"
|
||||
>
|
||||
<div class="flex grow-0 flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -52,11 +56,6 @@
|
||||
{{ checklistTitleText }}
|
||||
</button>
|
||||
</h1>
|
||||
<ButtonStyled circular>
|
||||
<button v-tooltip="`Keyboard shortcuts`" @click="keybindsModal?.show($event)">
|
||||
<KeyboardIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="!isPseudoStage && currentStageObj._guidanceUrl" circular>
|
||||
<a v-tooltip="`Stage guidance`" target="_blank" :href="currentStageObj._guidanceUrl">
|
||||
<FileTextIcon />
|
||||
@@ -370,7 +369,6 @@ import {
|
||||
CheckIcon,
|
||||
DropdownIcon,
|
||||
FileTextIcon,
|
||||
KeyboardIcon,
|
||||
LeftArrowIcon,
|
||||
LinkIcon,
|
||||
ListBulletedIcon,
|
||||
@@ -383,12 +381,13 @@ import {
|
||||
UndoIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type {
|
||||
IdentifiedNodeBuilder,
|
||||
NodeState,
|
||||
Priority,
|
||||
StageNodeBuilder,
|
||||
ValueNodeBuilder,
|
||||
import {
|
||||
type IdentifiedNodeBuilder,
|
||||
moderationSettings,
|
||||
type NodeState,
|
||||
type Priority,
|
||||
type StageNodeBuilder,
|
||||
type ValueNodeBuilder,
|
||||
} from '@modrinth/moderation'
|
||||
import {
|
||||
createTrackedPatch,
|
||||
@@ -396,7 +395,6 @@ import {
|
||||
expandVariables,
|
||||
getBooleanChildState,
|
||||
GLOBAL_STATE_KEY,
|
||||
handleKeybind,
|
||||
isNodeActive,
|
||||
kebabToTitleCase,
|
||||
NodeBuilder,
|
||||
@@ -438,15 +436,14 @@ import type { LockAcquireResponse } from '~/services/moderation-queue.ts'
|
||||
import { useModerationQueue } from '~/services/moderation-queue.ts'
|
||||
|
||||
import { type ActiveAction, type LiveNode, NODE_META_KEY, STATE_KEY } from './checklist-context'
|
||||
import KeybindsModal from './ChecklistKeybindsModal.vue'
|
||||
import NodeRenderer from './NodeRenderer.vue'
|
||||
|
||||
const notifications = injectNotificationManager()
|
||||
const { addNotification } = notifications
|
||||
const debug = useDebugLogger('ModerationChecklist')
|
||||
const keybinds = useModerationKeybinds()
|
||||
const settings = useModerationSettings()
|
||||
|
||||
const keybindsModal = ref<InstanceType<typeof KeybindsModal>>()
|
||||
const takeOverModal = ref<InstanceType<typeof ConfirmModal>>()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -1223,83 +1220,43 @@ interface MessagePart {
|
||||
content: string
|
||||
}
|
||||
|
||||
function ignoreLegacyActionKeybind() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function handleKeybinds(event: KeyboardEvent) {
|
||||
handleKeybind(
|
||||
event,
|
||||
{
|
||||
project: projectV2.value,
|
||||
state: {
|
||||
currentStage: currentStage.value,
|
||||
totalStages: resolvedStages.value.length,
|
||||
currentStageId: currentStageObj.value.id,
|
||||
currentStageTitle: currentStageObj.value.label,
|
||||
keybinds.value.handle(event, {
|
||||
project: projectV2.value,
|
||||
scope: 'checklist',
|
||||
state: {
|
||||
currentStage: currentStage.value,
|
||||
totalStages: resolvedStages.value.length,
|
||||
currentStageId: currentStageObj.value.id,
|
||||
currentStageTitle: currentStageObj.value.label,
|
||||
|
||||
isCollapsed: props.collapsed,
|
||||
isDone: done.value,
|
||||
hasGeneratedMessage: generatedMessage.value,
|
||||
isLoadingMessage: loadingMessage.value,
|
||||
isCollapsed: props.collapsed,
|
||||
isDone: done.value,
|
||||
hasGeneratedMessage: generatedMessage.value,
|
||||
isLoadingMessage: loadingMessage.value,
|
||||
|
||||
futureProjectCount: moderationQueue.queueLength,
|
||||
visibleActionsCount: resolveChildren(
|
||||
currentStageObj.value,
|
||||
nodeStates.value[currentStageObj.value.id!] ?? {},
|
||||
).filter((c) => c instanceof NodeBuilder).length,
|
||||
|
||||
focusedActionIndex: null,
|
||||
focusedActionType: null,
|
||||
},
|
||||
actions: {
|
||||
tryGoNext: nextStage,
|
||||
tryGoBack: previousStage,
|
||||
tryGenerateMessage: generateMessage,
|
||||
trySkipProject: skipCurrentProject,
|
||||
|
||||
tryToggleCollapse: () => emit('toggleCollapsed'),
|
||||
tryResetProgress: resetProgress,
|
||||
tryExitModeration: handleExit,
|
||||
|
||||
tryApprove: () => sendMessage(approveSendStatus.value),
|
||||
tryReject: () => sendMessage('rejected'),
|
||||
tryWithhold: () => sendMessage('withheld'),
|
||||
tryEditMessage: previousStage,
|
||||
|
||||
tryCopyLink: async (permalink: boolean, relative: boolean, page: boolean) => {
|
||||
let url = ``
|
||||
if (relative) {
|
||||
url += `${globalThis.location.origin}`
|
||||
} else {
|
||||
url += `https://modrinth.com`
|
||||
}
|
||||
|
||||
if (permalink) {
|
||||
url += `/project/${projectV2.value.id}`
|
||||
} else {
|
||||
url += `/${projectV2.value.project_type}/${projectV2.value.slug}`
|
||||
}
|
||||
|
||||
if (page) {
|
||||
url += `/${globalThis.location.pathname.split('/').slice(3).join('/')}`
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(url)
|
||||
},
|
||||
|
||||
tryCopyId: async () => await navigator.clipboard.writeText(projectV2.value.id),
|
||||
|
||||
tryToggleAction: ignoreLegacyActionKeybind,
|
||||
trySelectDropdownOption: ignoreLegacyActionKeybind,
|
||||
tryToggleChip: ignoreLegacyActionKeybind,
|
||||
tryFocusNextAction: ignoreLegacyActionKeybind,
|
||||
tryFocusPreviousAction: ignoreLegacyActionKeybind,
|
||||
tryActivateFocusedAction: ignoreLegacyActionKeybind,
|
||||
},
|
||||
futureProjectCount: moderationQueue.queueLength,
|
||||
visibleActionsCount: resolveChildren(
|
||||
currentStageObj.value,
|
||||
nodeStates.value[currentStageObj.value.id!] ?? {},
|
||||
).filter((c) => c instanceof NodeBuilder).length,
|
||||
},
|
||||
Object.values(keybinds.value),
|
||||
)
|
||||
actions: {
|
||||
tryGoNext: nextStage,
|
||||
tryGoBack: previousStage,
|
||||
tryGenerateMessage: generateMessage,
|
||||
trySkipProject: skipCurrentProject,
|
||||
|
||||
tryToggleCollapse: () => emit('toggleCollapsed'),
|
||||
tryResetProgress: resetProgress,
|
||||
tryExitModeration: handleExit,
|
||||
|
||||
tryApprove: () => sendMessage(approveSendStatus.value),
|
||||
tryReject: () => sendMessage('rejected'),
|
||||
tryWithhold: () => sendMessage('withheld'),
|
||||
tryEditMessage: previousStage,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Trigger debounced prefetch when user progresses through stages
|
||||
@@ -1315,7 +1272,9 @@ onMounted(async () => {
|
||||
window.addEventListener('keydown', handleKeybinds)
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
notifications.setNotificationLocation('left')
|
||||
if (settings.value.get(moderationSettings.General.ChecklistPosition) === 'right') {
|
||||
notifications.setNotificationLocation('left')
|
||||
}
|
||||
|
||||
const finishedId = localStorage.getItem('moderation-checklist-finished')
|
||||
if (finishedId === projectV2.value.id) {
|
||||
@@ -1870,6 +1829,12 @@ const stageOptions = computed<StageOption[]>(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.moderation-checklist {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
overflow-y: auto;
|
||||
z-index: 50;
|
||||
transition: bottom 0.25s ease-in-out;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="flex flex-row items-center gap-2 text-sm text-secondary">
|
||||
<GlobeIcon
|
||||
v-if="props.global"
|
||||
v-tooltip="'Can be used without the checklist open if setting enabled.'"
|
||||
/>
|
||||
{{ props.title }}
|
||||
<ButtonStyled size="small" circular type="transparent">
|
||||
<Button :disabled="!hasChanged" @click="resetToDefault">
|
||||
<RotateCounterClockwiseIcon />
|
||||
</Button>
|
||||
</ButtonStyled>
|
||||
</span>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<kbd
|
||||
v-if="definitions.length === 0"
|
||||
ref="keybinding"
|
||||
class="cursor-pointer border-2 !text-lg font-bold text-secondary"
|
||||
:class="{ editing: editing === 0 }"
|
||||
@click="startEditing(0)"
|
||||
>
|
||||
Not Bound
|
||||
</kbd>
|
||||
<kbd
|
||||
v-for="(definition, index) in definitions"
|
||||
v-else
|
||||
:key="`keybind-${index}`"
|
||||
ref="keybinding"
|
||||
class="cursor-pointer border-2 !text-lg font-bold"
|
||||
:class="{
|
||||
editing: editing === index,
|
||||
}"
|
||||
@click="startEditing(index)"
|
||||
>
|
||||
{{ toDisplay(definition) }}
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GlobeIcon, RotateCounterClockwiseIcon } from '@modrinth/assets'
|
||||
import { type KeybindDefinition, toKeybindDefinition } from '@modrinth/moderation'
|
||||
import { Button, ButtonStyled } from '@modrinth/ui'
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
global: boolean
|
||||
definitions: KeybindDefinition[]
|
||||
default: KeybindDefinition[]
|
||||
onChange: (definitions: KeybindDefinition[]) => void
|
||||
}>()
|
||||
|
||||
const keybinding = useTemplateRef('keybinding')
|
||||
const definitions = ref(JSON.parse(JSON.stringify(props.definitions)))
|
||||
const editing = ref(-1)
|
||||
const hasChanged = computed(
|
||||
() => JSON.stringify(definitions.value) !== JSON.stringify(props.default),
|
||||
)
|
||||
const isMac = ref(false)
|
||||
|
||||
function startEditing(index: number) {
|
||||
if (editing.value === index) {
|
||||
stopEditing()
|
||||
} else {
|
||||
editing.value = index
|
||||
window.addEventListener('keyup', handleKeybinds)
|
||||
window.addEventListener('click', handleMouse)
|
||||
}
|
||||
}
|
||||
|
||||
function stopEditing() {
|
||||
editing.value = -1
|
||||
window.removeEventListener('keyup', handleKeybinds)
|
||||
window.removeEventListener('click', handleMouse)
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
definitions.value = JSON.parse(JSON.stringify(props.default))
|
||||
props.onChange(definitions.value)
|
||||
}
|
||||
|
||||
function handleMouse(event: MouseEvent) {
|
||||
if (keybinding.value && event.target && event.target instanceof Node && editing.value != -1) {
|
||||
const editingRef = Array.isArray(keybinding.value)
|
||||
? keybinding.value[editing.value]
|
||||
: keybinding.value
|
||||
if (editingRef === event.target || editingRef.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stopEditing()
|
||||
}
|
||||
|
||||
function handleKeybinds(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
definitions.value.splice(editing.value, 1)
|
||||
} else if (definitions.value && definitions.value.length > 0) {
|
||||
definitions.value[editing.value] = toKeybindDefinition(event)
|
||||
} else {
|
||||
definitions.value.push(toKeybindDefinition(event))
|
||||
}
|
||||
props.onChange(definitions.value)
|
||||
stopEditing()
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function toDisplay(definition: KeybindDefinition): string {
|
||||
const keys = []
|
||||
|
||||
if (definition.ctrl || definition.meta) {
|
||||
keys.push(isMac.value ? 'CMD' : 'CTRL')
|
||||
}
|
||||
if (definition.shift) keys.push('SHIFT')
|
||||
if (definition.alt) keys.push('ALT')
|
||||
|
||||
const mainKey = definition.key
|
||||
.toUpperCase()
|
||||
.replace('ARROWLEFT', '←')
|
||||
.replace('ARROWRIGHT', '→')
|
||||
.replace('ARROWUP', '↑')
|
||||
.replace('ARROWDOWN', '↓')
|
||||
.replace('ENTER', '↵')
|
||||
|
||||
keys.push(mainKey)
|
||||
|
||||
return keys.join(' + ')
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopEditing()
|
||||
isMac.value = navigator.platform.toUpperCase().includes('MAC')
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
setDefinitions(newDefinitions: KeybindDefinition[]) {
|
||||
definitions.value = JSON.parse(JSON.stringify(newDefinitions))
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editing {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
border-color: var(--color-red);
|
||||
box-shadow: 0 0 10px 1px var(--color-red);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import ModerationKeybind from '~/components/ui/moderation/settings/ModerationKeybind.vue'
|
||||
|
||||
const keybinds = useModerationKeybinds()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="universal-card">
|
||||
<h2 class="text-2xl">Keybinds</h2>
|
||||
<div class="grid grid-cols-2 gap-x-12 gap-y-4">
|
||||
<ModerationKeybind
|
||||
v-for="[id, keybind] in keybinds"
|
||||
:key="id"
|
||||
:title="keybind.description"
|
||||
:global="keybind.scope === 'project'"
|
||||
:definitions="keybind.keybind"
|
||||
:default="keybind.defaultKeybind"
|
||||
:on-change="
|
||||
(definitions) => {
|
||||
keybinds.set(id, definitions)
|
||||
saveModerationOptions()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { moderationSettings, type SettingDefinition } from '@modrinth/moderation'
|
||||
import { Combobox, Toggle } from '@modrinth/ui'
|
||||
|
||||
const flattenedSettings = Object.entries(moderationSettings).reduce(
|
||||
(acc, [group, settings]) => {
|
||||
acc[group] = Object.values(settings)
|
||||
return acc
|
||||
},
|
||||
{} as { [name: string]: SettingDefinition[] },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
const merged: { [name: string]: SettingDefinition[] } = {}
|
||||
const addMergedSettings = (settings: { [name: string]: SettingDefinition[] }) => {
|
||||
for (const [groupId, groupSettings] of Object.entries(settings)) {
|
||||
const group = (merged[groupId] = merged[groupId] || [])
|
||||
group.push(...groupSettings)
|
||||
}
|
||||
}
|
||||
|
||||
addMergedSettings(flattenedSettings)
|
||||
const event = new CustomEvent('request-moderation-settings', {
|
||||
detail: {
|
||||
addSettings: addMergedSettings,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
|
||||
displayedSettings.value = merged
|
||||
})
|
||||
|
||||
const configuredSettings = useModerationSettings()
|
||||
const displayedSettings = ref<{ [name: string]: SettingDefinition[] }>(flattenedSettings)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="[name, page] of Object.entries(displayedSettings)" :key="name" class="universal-card">
|
||||
<h2 class="text-2xl">{{ name }}</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="setting in page"
|
||||
:key="setting.id"
|
||||
class="flex flex-row flex-wrap items-center justify-between gap-2"
|
||||
>
|
||||
<label class="flex-1">
|
||||
<span class="block font-semibold text-contrast">{{ setting.title }}</span>
|
||||
<span class="block text-secondary">{{ setting.description }}</span>
|
||||
<span class="block text-secondary">
|
||||
Default:
|
||||
<span
|
||||
class="font-semibold"
|
||||
:class="{
|
||||
'text-red': setting.type === 'toggle' && !setting.default,
|
||||
'text-green': setting.type === 'toggle' && setting.default,
|
||||
}"
|
||||
>{{ setting.default }}</span
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Toggle
|
||||
v-if="setting.type === 'toggle'"
|
||||
:model-value="configuredSettings.get(setting)"
|
||||
class="shrink-0"
|
||||
@update:model-value="(value) => configuredSettings.set(setting, value)"
|
||||
/>
|
||||
<Combobox
|
||||
v-if="setting.type === 'enum'"
|
||||
:model-value="configuredSettings.get(setting)"
|
||||
:options="setting.entries.map((entry) => ({ value: entry.value, label: entry.label }))"
|
||||
class="!w-1/4"
|
||||
@update:model-value="(value) => configuredSettings.set(setting, value)"
|
||||
/>
|
||||
<input
|
||||
v-if="setting.type === 'string'"
|
||||
type="text"
|
||||
:value="configuredSettings.get(setting)"
|
||||
class="input !w-1/4"
|
||||
@input="
|
||||
(event) => configuredSettings.set(setting, (event.target as HTMLInputElement).value)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,7 +5,7 @@
|
||||
'has-body': message.body.type === 'text' && !forceCompact,
|
||||
'no-actions': noLinks,
|
||||
private: isPrivateMessage,
|
||||
'show-private-bg': flags.showModeratorPrivateMessageHighlight,
|
||||
'show-private-bg': settings.get(moderationSettings.General.PrivateMessageHighlight),
|
||||
}"
|
||||
>
|
||||
<template v-if="members[message.author_id]">
|
||||
@@ -149,6 +149,7 @@ import {
|
||||
ScaleIcon,
|
||||
TrashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { moderationSettings } from '@modrinth/moderation'
|
||||
import {
|
||||
AutoLink,
|
||||
Avatar,
|
||||
@@ -194,7 +195,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update-thread'])
|
||||
const flags = useFeatureFlags()
|
||||
const settings = useModerationSettings()
|
||||
|
||||
const formattedMessage = computed(() => {
|
||||
const body = renderString(props.message.body.body)
|
||||
|
||||
Reference in New Issue
Block a user