mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
feat: Allow for adjustable alignment of main content on certain pages for content moderators (#7034)
* Add type safety and validation for moderation settings * Add ability for moderators to adjust page layout for discover, project, moderation pages Primarily for changes to have more tooling present without overlapping important info
This commit is contained in:
@@ -111,6 +111,18 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.align-x {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.align-l {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.align-r {
|
||||||
|
margin: 0 auto 0 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.normal-page__sidebar {
|
.normal-page__sidebar {
|
||||||
@@ -140,6 +152,18 @@
|
|||||||
'sidebar'
|
'sidebar'
|
||||||
/ 100%;
|
/ 100%;
|
||||||
|
|
||||||
|
&.align-x {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.align-l {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.align-r {
|
||||||
|
margin: 0 auto 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
&.sidebar {
|
&.sidebar {
|
||||||
grid-template:
|
grid-template:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<ProjectBackgroundGradient :project="project" />
|
<ProjectBackgroundGradient :project="project" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<template v-if="isSettings">
|
<template v-if="isSettings">
|
||||||
<div v-if="canAccessSettings" class="normal-page no-sidebar">
|
<div v-if="canAccessSettings" class="normal-page no-sidebar" :class="`align-${marginTarget}`">
|
||||||
<div class="normal-page__header">
|
<div class="normal-page__header">
|
||||||
<div
|
<div
|
||||||
class="mb-4 flex flex-wrap items-center gap-x-2 gap-y-3 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold"
|
class="mb-4 flex flex-wrap items-center gap-x-2 gap-y-3 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold"
|
||||||
@@ -94,17 +94,20 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="new-page sidebar"
|
class="new-page sidebar"
|
||||||
:class="{
|
:class="[
|
||||||
'alt-layout': cosmetics.leftContentLayout,
|
{
|
||||||
'checklist-open':
|
'alt-layout': cosmetics.leftContentLayout,
|
||||||
showModerationChecklist &&
|
'checklist-open':
|
||||||
!collapsedModerationChecklist &&
|
showModerationChecklist &&
|
||||||
!flags.alwaysShowChecklistAsPopup,
|
!collapsedModerationChecklist &&
|
||||||
'checklist-collapsed':
|
!flags.alwaysShowChecklistAsPopup,
|
||||||
showModerationChecklist &&
|
'checklist-collapsed':
|
||||||
collapsedModerationChecklist &&
|
showModerationChecklist &&
|
||||||
!flags.alwaysShowChecklistAsPopup,
|
collapsedModerationChecklist &&
|
||||||
}"
|
!flags.alwaysShowChecklistAsPopup,
|
||||||
|
},
|
||||||
|
`align-${marginTarget}`,
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="normal-page__header relative mb-4"
|
class="normal-page__header relative mb-4"
|
||||||
@@ -511,7 +514,7 @@ import {
|
|||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { moderationSettings } from '@modrinth/moderation'
|
import { getMarginTarget, moderationSettings } from '@modrinth/moderation'
|
||||||
import {
|
import {
|
||||||
Admonition,
|
Admonition,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -583,6 +586,7 @@ const config = useRuntimeConfig()
|
|||||||
const moderationQueue = useModerationQueue()
|
const moderationQueue = useModerationQueue()
|
||||||
const keybinds = useModerationKeybinds()
|
const keybinds = useModerationKeybinds()
|
||||||
const modSettings = useModerationSettings()
|
const modSettings = useModerationSettings()
|
||||||
|
const marginTarget = computed(() => getMarginTarget(modSettings.value))
|
||||||
const notifications = injectNotificationManager()
|
const notifications = injectNotificationManager()
|
||||||
const { addNotification } = notifications
|
const { addNotification } = notifications
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getMarginTarget } from '@modrinth/moderation'
|
||||||
import { commonProjectTypeCategoryMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
import { commonProjectTypeCategoryMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const modSettings = useModerationSettings()
|
||||||
|
|
||||||
const allowTabChanging = computed(() => !route.query.sid)
|
const allowTabChanging = computed(() => !route.query.sid)
|
||||||
|
const marginTarget = computed(() => getMarginTarget(modSettings.value))
|
||||||
|
|
||||||
const selectableProjectTypes = [
|
const selectableProjectTypes = [
|
||||||
{
|
{
|
||||||
@@ -47,7 +50,10 @@ const selectableProjectTypes = [
|
|||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="mx-auto box-border flex w-full max-w-[1280px] flex-col gap-4 px-6 pb-6">
|
<div
|
||||||
|
class="box-border flex w-full max-w-[1280px] flex-col gap-4 px-6 pb-6"
|
||||||
|
:class="`m${marginTarget}-auto`"
|
||||||
|
>
|
||||||
<NavTabs
|
<NavTabs
|
||||||
v-if="!flags.projectTypesPrimaryNav && allowTabChanging"
|
v-if="!flags.projectTypesPrimaryNav && allowTabChanging"
|
||||||
:links="selectableProjectTypes"
|
:links="selectableProjectTypes"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6">
|
<div
|
||||||
|
class="relativemb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
|
||||||
|
:class="`m${marginTarget}-auto`"
|
||||||
|
>
|
||||||
<h1>Moderation</h1>
|
<h1>Moderation</h1>
|
||||||
<NavTabs :links="moderationLinks" class="mb-4 hidden sm:flex" />
|
<NavTabs :links="moderationLinks" class="mb-4 hidden sm:flex" />
|
||||||
<div class="mb-4 sm:hidden">
|
<div class="mb-4 sm:hidden">
|
||||||
@@ -16,6 +19,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
||||||
|
import { getMarginTarget } from '@modrinth/moderation'
|
||||||
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -30,6 +34,9 @@ const { formatMessage } = useVIntl()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const modSettings = useModerationSettings()
|
||||||
|
const marginTarget = computed(() => getMarginTarget(modSettings.value))
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
projectsTitle: {
|
projectsTitle: {
|
||||||
id: 'moderation.page.projects',
|
id: 'moderation.page.projects',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { EnumSettingDefinition, ToggleSettingDefinition } from '../types/settings.ts'
|
import { setting } from '../types/settings.ts'
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
General: {
|
General: {
|
||||||
ChecklistPosition: {
|
ChecklistPosition: setting.asEnum({
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
id: 'checklist-position',
|
id: 'checklist-position',
|
||||||
title: 'Checklist Position',
|
title: 'Checklist Position',
|
||||||
@@ -12,21 +12,34 @@ const settings = {
|
|||||||
{ value: 'right', label: 'Right' },
|
{ value: 'right', label: 'Right' },
|
||||||
],
|
],
|
||||||
default: 'right',
|
default: 'right',
|
||||||
} as EnumSettingDefinition,
|
}),
|
||||||
ProjectKeybinds: {
|
ProjectKeybinds: setting.asToggle({
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
id: 'project-keybinds',
|
id: 'project-keybinds',
|
||||||
title: 'Enable Project Keybinds',
|
title: 'Enable Project Keybinds',
|
||||||
description: 'Weather certain keybinds should work without the checklist visible.',
|
description: 'Weather certain keybinds should work without the checklist visible.',
|
||||||
default: false,
|
default: false,
|
||||||
} as ToggleSettingDefinition,
|
}),
|
||||||
PrivateMessageHighlight: {
|
PrivateMessageHighlight: setting.asToggle({
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
id: 'private-message-highlight',
|
id: 'private-message-highlight',
|
||||||
title: 'Highlight Private Messages',
|
title: 'Highlight Private Messages',
|
||||||
description: 'Whether private messages should be highlighted in the chat.',
|
description: 'Whether private messages should be highlighted in the chat.',
|
||||||
default: true,
|
default: true,
|
||||||
} as ToggleSettingDefinition,
|
}),
|
||||||
|
AdjustPageAlignment: setting.asEnum({
|
||||||
|
type: 'enum',
|
||||||
|
id: 'adjust-page-alignment',
|
||||||
|
title: 'Adjust Page Alignment',
|
||||||
|
description:
|
||||||
|
'Whether the main page elements should be centered or offset opposite to the Checklist Position.',
|
||||||
|
entries: [
|
||||||
|
{ value: 'never', label: 'Never' },
|
||||||
|
{ value: 'checklist-present', label: 'Checklist Needed' },
|
||||||
|
{ value: 'always', label: 'Always' },
|
||||||
|
],
|
||||||
|
default: 'never',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { SettingDefinitionBase } from '../types/settings.ts'
|
import { isValidFor, type SettingDefinitionBase } from '../types/settings.ts'
|
||||||
|
import { moderationSettings } from '../index.ts'
|
||||||
|
|
||||||
export class Settings {
|
export class Settings {
|
||||||
private readonly settings: { [id: string]: any }
|
private readonly settings: { [id: string]: any }
|
||||||
@@ -13,7 +14,9 @@ export class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get<T>(definition: SettingDefinitionBase<T>): T {
|
get<T>(definition: SettingDefinitionBase<T>): T {
|
||||||
return this.settings[definition.id] ?? definition.default
|
const value = this.settings[definition.id]
|
||||||
|
|
||||||
|
return (isValidFor(definition, value) ? value : undefined) ?? definition.default
|
||||||
}
|
}
|
||||||
|
|
||||||
set<T>(definition: SettingDefinitionBase<T>, value?: T): void {
|
set<T>(definition: SettingDefinitionBase<T>, value?: T): void {
|
||||||
@@ -23,3 +26,11 @@ export class Settings {
|
|||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMarginTarget(settings: Settings): string {
|
||||||
|
return settings.get(moderationSettings.General.AdjustPageAlignment) == 'always'
|
||||||
|
? settings.get(moderationSettings.General.ChecklistPosition) == 'right'
|
||||||
|
? 'r'
|
||||||
|
: 'l'
|
||||||
|
: 'x'
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface SettingDefinitionBase<T> {
|
export interface SettingDefinitionBase<T> {
|
||||||
type: string
|
type: SettingDefinitionTypes
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
@@ -11,16 +11,53 @@ export interface ToggleSettingDefinition extends SettingDefinitionBase<boolean>
|
|||||||
type: 'toggle'
|
type: 'toggle'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnumSettingDefinition extends SettingDefinitionBase<string> {
|
export interface EnumSettingDefinition<T extends string = string> extends SettingDefinitionBase<T> {
|
||||||
type: 'enum'
|
type: 'enum'
|
||||||
entries: {
|
entries: {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: T
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StringSettingDefinition extends SettingDefinitionBase<string> {
|
export interface StringSettingDefinition extends SettingDefinitionBase<string> {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
|
regex?: RegExp
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SettingDefinitions = ReturnType<(typeof setting)[keyof typeof setting]>
|
||||||
|
export type SettingDefinitionTypes = SettingDefinitions['type']
|
||||||
|
|
||||||
|
export const setting = {
|
||||||
|
asEnum: <const E extends ReadonlyArray<{ label: string; value: string }>>(
|
||||||
|
data: EnumSettingDefinition<E[number]['value']> & { entries: E },
|
||||||
|
): EnumSettingDefinition<E[number]['value']> => {
|
||||||
|
return data as EnumSettingDefinition<E[number]['value']>
|
||||||
|
},
|
||||||
|
asToggle: (data: ToggleSettingDefinition) => data,
|
||||||
|
asString: (data: StringSettingDefinition) => data,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidFor(definitionBase: SettingDefinitionBase<any>, value: any): boolean {
|
||||||
|
if (value != null) {
|
||||||
|
// Tried my best with type safety but sadly having the `SettingDefinitions` as the type leads to issues handling types else where...
|
||||||
|
const definition = definitionBase as SettingDefinitions
|
||||||
|
if (
|
||||||
|
definition.type == 'enum' &&
|
||||||
|
definition.entries.map((entry) => entry.value).includes(value)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
} else if (definition.type == 'toggle' && typeof value === 'boolean') {
|
||||||
|
return true
|
||||||
|
} else if (
|
||||||
|
definition.type == 'string' &&
|
||||||
|
typeof value === 'string' &&
|
||||||
|
(!definition.regex || value.match(definition.regex))
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SettingDefinition =
|
export type SettingDefinition =
|
||||||
|
|||||||
Reference in New Issue
Block a user