mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
update archiving, move general project settings to v3 (start redesign there), use project id lookups for stable cache keys, advanced in server search
This commit is contained in:
@@ -108,7 +108,15 @@ import { getSeverityIcon } from '../../utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: 'info' | 'warning' | 'critical' | 'success' | 'moderation' | 'circle-warning' | 'neutral'
|
||||
type?:
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'critical'
|
||||
| 'success'
|
||||
| 'moderation'
|
||||
| 'circle-warning'
|
||||
| 'neutral'
|
||||
| 'grey'
|
||||
header?: string
|
||||
body?: string
|
||||
inlineActions?: boolean
|
||||
@@ -174,6 +182,7 @@ const typeClasses = {
|
||||
success: 'border-brand-green bg-bg-green',
|
||||
moderation: 'border-brand-orange bg-bg-orange',
|
||||
neutral: 'border-surface-4 bg-surface-3',
|
||||
grey: 'border-gray-500 bg-gray-950',
|
||||
}
|
||||
|
||||
const iconClasses = {
|
||||
@@ -184,6 +193,7 @@ const iconClasses = {
|
||||
success: 'text-brand-green',
|
||||
moderation: 'text-brand-orange',
|
||||
neutral: 'text-secondary',
|
||||
grey: 'text-secondary',
|
||||
}
|
||||
|
||||
const buttonColors = {
|
||||
@@ -194,6 +204,7 @@ const buttonColors = {
|
||||
success: 'green',
|
||||
moderation: 'orange',
|
||||
neutral: 'standard',
|
||||
grey: 'standard',
|
||||
} as const
|
||||
|
||||
const progressTrackClasses = {
|
||||
@@ -204,6 +215,7 @@ const progressTrackClasses = {
|
||||
success: 'bg-brand-green/20',
|
||||
moderation: 'bg-brand-orange/20',
|
||||
neutral: 'bg-surface-4',
|
||||
grey: 'bg-surface-4',
|
||||
}
|
||||
|
||||
const progressFillClasses = {
|
||||
@@ -216,6 +228,7 @@ const progressFillClasses = {
|
||||
green: 'bg-brand-green',
|
||||
red: 'bg-brand-red',
|
||||
neutral: 'bg-surface-5',
|
||||
grey: 'bg-surface-5',
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
icon?: Component
|
||||
description?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid w-full grid-cols-[1fr_auto] items-center gap-6 p-4 rounded-2xl border border-solid border-surface-4 bg-surface-3"
|
||||
>
|
||||
<div>
|
||||
<h3 class="mb-1 mt-0 flex items-center gap-2 text-base font-semibold text-contrast">
|
||||
<component :is="icon" v-if="icon" class="size-5 shrink-0 text-primary" aria-hidden="true" />
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="description || $slots.default"
|
||||
class="flex flex-col gap-2 text-sm leading-normal [&>p]:m-0"
|
||||
>
|
||||
<p v-if="description">{{ description }}</p>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,7 +5,8 @@
|
||||
v-bind="$attrs"
|
||||
class="smart-clickable__contents"
|
||||
:class="{
|
||||
'pointer-events-none': !!$slots.clickable,
|
||||
'smart-clickable__contents--disabled': disabled,
|
||||
'pointer-events-none': !!$slots.clickable && !disabled,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
@@ -14,6 +15,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
@@ -77,7 +82,7 @@ defineOptions({
|
||||
}
|
||||
|
||||
// When clickable is being clicked, give contents an effect
|
||||
:first-child:active + .smart-clickable__contents {
|
||||
:first-child:active + .smart-clickable__contents:not(.smart-clickable__contents--disabled) {
|
||||
scale: 0.98;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ function toggle() {
|
||||
<template>
|
||||
<SmartClickable
|
||||
class="flex w-full flex-col overflow-clip rounded-2xl border border-solid border-surface-4 bg-surface-3"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #clickable>
|
||||
<button
|
||||
|
||||
@@ -109,6 +109,7 @@ export { default as ScrollablePanel } from './ScrollablePanel.vue'
|
||||
export { default as ServerNotice } from './ServerNotice.vue'
|
||||
export { default as SettingsFormGroup } from './SettingsFormGroup.vue'
|
||||
export { default as SettingsLabel } from './SettingsLabel.vue'
|
||||
export { default as SettingsOptionCard } from './SettingsOptionCard.vue'
|
||||
export { default as SettingsToggleCard } from './SettingsToggleCard.vue'
|
||||
export { default as SimpleBadge } from './SimpleBadge.vue'
|
||||
export { default as Slider } from './Slider.vue'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<NewModal ref="modal" :noblur="noblur" :danger="danger" :on-hide="onHide" max-width="550px">
|
||||
<template #title>
|
||||
<slot name="title">
|
||||
<span class="font-extrabold text-contrast text-lg">{{ title }}</span>
|
||||
</slot>
|
||||
</template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:noblur="noblur"
|
||||
:danger="danger"
|
||||
:on-hide="onHide"
|
||||
max-width="800px"
|
||||
:header="title"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<template v-if="description">
|
||||
<div
|
||||
@@ -20,7 +22,7 @@
|
||||
<label v-if="hasToType" for="confirmation">
|
||||
<span>
|
||||
To confirm you want to proceed, type
|
||||
<span class="italic font-bold">{{ confirmationText }}</span> below:
|
||||
<span class="font-semibold text-contrast">{{ confirmationText }}</span> below:
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
|
||||
@@ -66,7 +66,7 @@ const messages = defineMessages({
|
||||
},
|
||||
body3: {
|
||||
id: 'search.photosensitivity-warning-modal.body.3',
|
||||
defaultMessage: 'Using any content on Modrinth is at your own risk, please stay safe! 💚',
|
||||
defaultMessage: 'Using any content on Modrinth is at your own risk. Please stay safe! 💚',
|
||||
},
|
||||
dontShowAgain: {
|
||||
id: 'search.photosensitivity-warning-modal.dont-show-again',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { ArchiveIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
reason?: string | null
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const trimmedReason = computed(() => props.reason?.trim() || null)
|
||||
|
||||
const messages = defineMessages({
|
||||
header: {
|
||||
id: 'project.status.archived.header',
|
||||
defaultMessage: '{title} has been archived',
|
||||
},
|
||||
body: {
|
||||
id: 'project.status.archived.body',
|
||||
defaultMessage:
|
||||
'{title} will not receive any further updates unless the author decides to unarchive the project.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Admonition type="grey" :header="formatMessage(messages.header, { title })">
|
||||
<template #icon="{ iconClass }">
|
||||
<ArchiveIcon :class="iconClass" />
|
||||
</template>
|
||||
<span>{{ trimmedReason ?? formatMessage(messages.body, { title }) }}</span>
|
||||
</Admonition>
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<template v-if="showStatusBadge" #badges>
|
||||
<ProjectStatusBadge :status="project.status" />
|
||||
<ProjectStatusBadge :status="projectV3.status" />
|
||||
</template>
|
||||
|
||||
<template #metadata>
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './server'
|
||||
export * from './settings'
|
||||
|
||||
// Other
|
||||
export { default as ArchivedProjectBanner } from './ArchivedProjectBanner.vue'
|
||||
export { default as ProjectCard } from './card/ProjectCard.vue'
|
||||
export { default as ProjectBackgroundGradient } from './ProjectBackgroundGradient.vue'
|
||||
export { default as ProjectCardList } from './ProjectCardList.vue'
|
||||
|
||||
@@ -36,7 +36,10 @@ function isPhotosensitivityExclusionSelected(filters: { type: string; option: st
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isPhotosensitivityExclusionSelected(ctx.currentFilters.value),
|
||||
() =>
|
||||
isPhotosensitivityExclusionSelected(
|
||||
ctx.isServerType.value ? ctx.serverCurrentFilters.value : ctx.currentFilters.value,
|
||||
),
|
||||
(isSelected, wasSelected) => {
|
||||
if (isSelected && !wasSelected && !ctx.dismissedPhotosensitivityFilterWarning?.value) {
|
||||
nextTick(() => photosensitivityWarningModal.value?.show())
|
||||
@@ -204,6 +207,8 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
:content-class="contentClass"
|
||||
:inner-panel-class="innerPanelClass"
|
||||
:open-by-default="getFilterOpenByDefault(filterType.id)"
|
||||
@on-open="() => filterType.id === 'advanced' && setAdvancedFiltersCollapsed(false)"
|
||||
@on-close="() => filterType.id === 'advanced' && setAdvancedFiltersCollapsed(true)"
|
||||
>
|
||||
<template #header>
|
||||
<h3 :class="isApp ? 'text-base m-0' : 'm-0 text-base font-semibold'">
|
||||
|
||||
@@ -3776,6 +3776,12 @@
|
||||
"project.stats.followers-label": {
|
||||
"defaultMessage": "{count, plural, one {follower} other {followers}}"
|
||||
},
|
||||
"project.status.archived.body": {
|
||||
"defaultMessage": "{title} will not receive any further updates unless the author decides to unarchive the project."
|
||||
},
|
||||
"project.status.archived.header": {
|
||||
"defaultMessage": "{title} has been archived"
|
||||
},
|
||||
"project.versions.channel.alpha.symbol": {
|
||||
"defaultMessage": "A"
|
||||
},
|
||||
@@ -3960,7 +3966,7 @@
|
||||
"defaultMessage": "Content labels for photosensitivity triggers are self-assigned by the creators who upload their content to Modrinth. These projects have not gone through any safety testing."
|
||||
},
|
||||
"search.photosensitivity-warning-modal.body.3": {
|
||||
"defaultMessage": "Using any content on Modrinth is at your own risk, please stay safe! 💚"
|
||||
"defaultMessage": "Using any content on Modrinth is at your own risk. Please stay safe! 💚"
|
||||
},
|
||||
"search.photosensitivity-warning-modal.dont-show-again": {
|
||||
"defaultMessage": "Don't show this again"
|
||||
|
||||
@@ -146,7 +146,7 @@ const DISCLOSURE_TYPE_FILTERS = [
|
||||
'archived',
|
||||
] as const
|
||||
|
||||
type DisclosureTypeFilter = (typeof DISCLOSURE_TYPE_FILTERS)[number]
|
||||
export type DisclosureTypeFilter = (typeof DISCLOSURE_TYPE_FILTERS)[number]
|
||||
|
||||
const DISCLOSURE_TYPE_SUPPORTED_PROJECT_TYPES: Record<
|
||||
DisclosureTypeFilter,
|
||||
@@ -177,6 +177,92 @@ function isProjectTypeExclusionOption(optionId: string): optionId is ProjectType
|
||||
return (ALL_PROJECT_TYPES as string[]).includes(optionId)
|
||||
}
|
||||
|
||||
type FormatMessage = (
|
||||
descriptor: { id: string; defaultMessage: string },
|
||||
values?: Record<string, unknown>,
|
||||
) => string
|
||||
|
||||
export function formatDisclosureTypeLabel(
|
||||
formatMessage: FormatMessage,
|
||||
disclosureType: DisclosureTypeFilter,
|
||||
): string {
|
||||
switch (disclosureType) {
|
||||
case 'ai_content':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.ai_content',
|
||||
defaultMessage: 'AI-generated content',
|
||||
}),
|
||||
)
|
||||
case 'advertisements':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.advertisements',
|
||||
defaultMessage: 'Advertisements',
|
||||
}),
|
||||
)
|
||||
case 'epilepsy_triggers':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.epilepsy_triggers',
|
||||
defaultMessage: 'Photosensitivity triggers',
|
||||
}),
|
||||
)
|
||||
case 'system_interactions':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.system_interactions',
|
||||
defaultMessage: 'External system interactions',
|
||||
}),
|
||||
)
|
||||
case 'telemetry':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.telemetry',
|
||||
defaultMessage: 'Telemetry',
|
||||
}),
|
||||
)
|
||||
case 'derivative_work':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.derivative_work',
|
||||
defaultMessage: 'Derivative content',
|
||||
}),
|
||||
)
|
||||
case 'paid_features':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.paid_features',
|
||||
defaultMessage: 'Paid features',
|
||||
}),
|
||||
)
|
||||
case 'archived':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.archived',
|
||||
defaultMessage: 'Archived',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function createDisclosureFilterOptions(
|
||||
formatMessage: FormatMessage,
|
||||
projectTypes: readonly ProjectType[],
|
||||
): FilterOption[] {
|
||||
return DISCLOSURE_TYPE_FILTERS.filter((disclosureType) =>
|
||||
DISCLOSURE_TYPE_SUPPORTED_PROJECT_TYPES[disclosureType].some((projectType) =>
|
||||
projectTypes.includes(projectType),
|
||||
),
|
||||
).map((disclosureType) => ({
|
||||
id: disclosureType,
|
||||
formatted_name: formatDisclosureTypeLabel(formatMessage, disclosureType),
|
||||
icon: DISCLOSURE_TYPE_ICONS[disclosureType],
|
||||
method: 'or' as const,
|
||||
value: `disclosure_types:${disclosureType}`,
|
||||
}))
|
||||
}
|
||||
|
||||
export function useSearch(
|
||||
projectTypes: Ref<ProjectType[]>,
|
||||
tags: Ref<Tags>,
|
||||
@@ -208,67 +294,6 @@ export function useSearch(
|
||||
return formatCategory(formatMessage, categoryName)
|
||||
}
|
||||
|
||||
const formatDisclosureTypeLabel = (disclosureType: DisclosureTypeFilter): string => {
|
||||
switch (disclosureType) {
|
||||
case 'ai_content':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.ai_content',
|
||||
defaultMessage: 'AI-generated content',
|
||||
}),
|
||||
)
|
||||
case 'advertisements':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.advertisements',
|
||||
defaultMessage: 'Advertisements',
|
||||
}),
|
||||
)
|
||||
case 'epilepsy_triggers':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.epilepsy_triggers',
|
||||
defaultMessage: 'Photosensitivity triggers',
|
||||
}),
|
||||
)
|
||||
case 'system_interactions':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.system_interactions',
|
||||
defaultMessage: 'External system interactions',
|
||||
}),
|
||||
)
|
||||
case 'telemetry':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.telemetry',
|
||||
defaultMessage: 'Telemetry',
|
||||
}),
|
||||
)
|
||||
case 'derivative_work':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.derivative_work',
|
||||
defaultMessage: 'Derivative content',
|
||||
}),
|
||||
)
|
||||
case 'paid_features':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.paid_features',
|
||||
defaultMessage: 'Paid features',
|
||||
}),
|
||||
)
|
||||
case 'archived':
|
||||
return formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced.disclosure.archived',
|
||||
defaultMessage: 'Archived',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const filters = computed(() => {
|
||||
const categoryFilters: Record<string, FilterType> = {}
|
||||
for (const category of sortedCategories(tags.value, formatCategoryName, locale.value)) {
|
||||
@@ -579,17 +604,7 @@ export function useSearch(
|
||||
searchable: false,
|
||||
ordering: -1000,
|
||||
options: [
|
||||
...DISCLOSURE_TYPE_FILTERS.filter((disclosureType) =>
|
||||
DISCLOSURE_TYPE_SUPPORTED_PROJECT_TYPES[disclosureType].some((projectType) =>
|
||||
projectTypes.value.includes(projectType),
|
||||
),
|
||||
).map((disclosureType) => ({
|
||||
id: disclosureType,
|
||||
formatted_name: formatDisclosureTypeLabel(disclosureType),
|
||||
icon: DISCLOSURE_TYPE_ICONS[disclosureType],
|
||||
method: 'or' as const,
|
||||
value: `disclosure_types:${disclosureType}`,
|
||||
})),
|
||||
...createDisclosureFilterOptions(formatMessage, projectTypes.value),
|
||||
...excludeableProjectTypes.map((target) => ({
|
||||
id: target,
|
||||
formatted_name: formatMessage(getProjectTypeCategoryMessage(target)),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import { defineMessage, LOCALES, useVIntl } from '../composables/i18n'
|
||||
import type { FilterType, FilterValue, SortType, Tags } from './search'
|
||||
import { formatSearchFilterValue } from './search'
|
||||
import { createDisclosureFilterOptions, formatSearchFilterValue } from './search'
|
||||
import { formatCategory, formatCategoryHeader } from './tag-messages'
|
||||
|
||||
export const SERVER_REGIONS = {
|
||||
@@ -339,6 +339,22 @@ export function useServerSearch(opts: {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
formatted_name: formatMessage(
|
||||
defineMessage({
|
||||
id: 'search.filter_type.advanced',
|
||||
defaultMessage: 'Advanced exclusions',
|
||||
}),
|
||||
),
|
||||
supported_project_types: ['server'],
|
||||
display: 'all',
|
||||
query_param: 'a',
|
||||
supports: ['exclude'],
|
||||
searchable: false,
|
||||
ordering: -1000,
|
||||
options: createDisclosureFilterOptions(formatMessage, ['server']),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -346,11 +362,28 @@ export function useServerSearch(opts: {
|
||||
const parts = ['project_types = minecraft_java_server']
|
||||
|
||||
for (const filterType of serverFilterTypes.value) {
|
||||
const field = getFilterField(filterType.id)
|
||||
if (!field) continue
|
||||
const matched = serverCurrentFilters.value.filter((f) => f.type === filterType.id)
|
||||
if (matched.length === 0) continue
|
||||
|
||||
if (filterType.id === 'advanced') {
|
||||
const disclosureValues = matched
|
||||
.map((filterValue) => {
|
||||
const option = filterType.options.find((option) => option.id === filterValue.option)
|
||||
if (!option || !('value' in option)) return null
|
||||
const [, val] = option.value.split(':')
|
||||
return val
|
||||
})
|
||||
.filter((val): val is string => !!val)
|
||||
if (disclosureValues.length > 0) {
|
||||
const quoted = disclosureValues.map(formatSearchFilterValue).join(', ')
|
||||
parts.push(`disclosure_types NOT IN [${quoted}]`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const field = getFilterField(filterType.id)
|
||||
if (!field) continue
|
||||
|
||||
if (filterType.id === 'server_status') {
|
||||
const selected = matched[0]?.option
|
||||
if (selected === 'online') {
|
||||
|
||||
Reference in New Issue
Block a user