Merge branch 'main' into app-updater-rework
# Conflicts: # packages/app-lib/src/state/friends.rs # packages/app-lib/src/util/fetch.rs
This commit is contained in:
commit
2774cdca76
@ -2,5 +2,8 @@
|
|||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
||||||
|
|
||||||
|
[target.x86_64-pc-windows-msvc]
|
||||||
|
linker = "rust-lld"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
|
|||||||
4
.github/workflows/theseus-build.yml
vendored
4
.github/workflows/theseus-build.yml
vendored
@ -75,7 +75,7 @@ jobs:
|
|||||||
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||||
chmod: 0755
|
chmod: 0755
|
||||||
|
|
||||||
- name: ⚙️ Set application version
|
- name: ⚙️ Set application version and environment
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||||
@ -84,6 +84,8 @@ jobs:
|
|||||||
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||||
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
||||||
|
|
||||||
|
cp packages/app-lib/.env.prod packages/app-lib/.env
|
||||||
|
|
||||||
- name: 💨 Setup Turbo cache
|
- name: 💨 Setup Turbo cache
|
||||||
uses: rharkor/caching-for-turbo@v1.8
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/turbo-ci.yml
vendored
4
.github/workflows/turbo-ci.yml
vendored
@ -74,6 +74,10 @@ jobs:
|
|||||||
cp .env.local .env
|
cp .env.local .env
|
||||||
sqlx database setup
|
sqlx database setup
|
||||||
|
|
||||||
|
- name: ⚙️ Set app environment
|
||||||
|
working-directory: packages/app-lib
|
||||||
|
run: cp .env.staging .env
|
||||||
|
|
||||||
- name: 🔍 Lint and test
|
- name: 🔍 Lint and test
|
||||||
run: pnpm run ci
|
run: pnpm run ci
|
||||||
|
|
||||||
|
|||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -8930,6 +8930,7 @@ dependencies = [
|
|||||||
"data-url",
|
"data-url",
|
||||||
"dirs",
|
"dirs",
|
||||||
"discord-rich-presence",
|
"discord-rich-presence",
|
||||||
|
"dotenvy",
|
||||||
"dunce",
|
"dunce",
|
||||||
"either",
|
"either",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
|||||||
@ -34,7 +34,7 @@ const enabledLocales: string[] = [];
|
|||||||
/**
|
/**
|
||||||
* Overrides for the categories of the certain locales.
|
* Overrides for the categories of the certain locales.
|
||||||
*/
|
*/
|
||||||
const localesCategoriesOverrides: Partial = {
|
const localesCategoriesOverrides: Partial<Record<string, "fun" | "experimental">> = {
|
||||||
"en-x-pirate": "fun",
|
"en-x-pirate": "fun",
|
||||||
"en-x-updown": "fun",
|
"en-x-updown": "fun",
|
||||||
"en-x-lolcat": "fun",
|
"en-x-lolcat": "fun",
|
||||||
@ -260,13 +260,7 @@ export default defineNuxtConfig({
|
|||||||
const omorphiaLocales: string[] = [];
|
const omorphiaLocales: string[] = [];
|
||||||
const omorphiaLocaleSets = new Map<string, { files: { from: string }[] }>();
|
const omorphiaLocaleSets = new Map<string, { files: { from: string }[] }>();
|
||||||
|
|
||||||
const externalLocales = [
|
for await (const localeDir of globIterate("node_modules/@modrinth/ui/src/locales/*", {
|
||||||
"node_modules/@modrinth/ui/src/locales/en-US",
|
|
||||||
"node_modules/@modrinth/moderation/locales/en-US",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const localePath of externalLocales) {
|
|
||||||
for await (const localeDir of globIterate(localePath, {
|
|
||||||
posix: true,
|
posix: true,
|
||||||
})) {
|
})) {
|
||||||
const tag = basename(localeDir);
|
const tag = basename(localeDir);
|
||||||
@ -283,7 +277,6 @@ export default defineNuxtConfig({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return function resolveLocaleImport(tag: string) {
|
return function resolveLocaleImport(tag: string) {
|
||||||
return omorphiaLocaleSets.get(matchLocale([tag], omorphiaLocales, "en-x-placeholder"));
|
return omorphiaLocaleSets.get(matchLocale([tag], omorphiaLocales, "en-x-placeholder"));
|
||||||
@ -308,7 +301,7 @@ export default defineNuxtConfig({
|
|||||||
format: "crowdin",
|
format: "crowdin",
|
||||||
});
|
});
|
||||||
} else if (fileName === "meta.json") {
|
} else if (fileName === "meta.json") {
|
||||||
const meta: Record = await fs
|
const meta: Record<string, { message: string }> = await fs
|
||||||
.readFile(localeFile, "utf8")
|
.readFile(localeFile, "utf8")
|
||||||
.then((date) => JSON.parse(date));
|
.then((date) => JSON.parse(date));
|
||||||
const localeMeta = (locale.meta ??= {});
|
const localeMeta = (locale.meta ??= {});
|
||||||
|
|||||||
@ -1,442 +1,510 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showInvitation" class="universal-card information invited my-4">
|
<div v-if="showInvitation" class="universal-card information invited">
|
||||||
<h2>{{ getFormattedMessage(messages.invitationTitle) }}</h2>
|
<h2>Invitation to join project</h2>
|
||||||
<p v-if="currentMember?.project_role">
|
<p>
|
||||||
{{ formatMessage(messages.invitationWithRole, { role: currentMember.project_role }) }}
|
You've been invited be a member of this project with the role of '{{ currentMember.role }}'.
|
||||||
</p>
|
</p>
|
||||||
<p v-else>{{ getFormattedMessage(messages.invitationNoRole) }}</p>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<ButtonStyled color="brand">
|
<button class="iconified-button brand-button" @click="acceptInvite()">
|
||||||
<button class="brand-button" @click="acceptInvite()">
|
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
{{ getFormattedMessage(messages.accept) }}
|
Accept
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
<button class="iconified-button danger-button" @click="declineInvite()">
|
||||||
<ButtonStyled color="red">
|
|
||||||
<button @click="declineInvite">
|
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ getFormattedMessage(messages.decline) }}
|
Decline
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
currentMember &&
|
currentMember &&
|
||||||
visibleNags.length > 0 &&
|
nags.filter((x) => x.condition).length > 0 &&
|
||||||
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
|
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
|
||||||
"
|
"
|
||||||
class="universal-card my-4"
|
class="author-actions universal-card mb-4"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-full flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="header__row">
|
||||||
<div class="flex flex-auto flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="header__title">
|
||||||
<h2 class="my-0 mr-auto">{{ getFormattedMessage(messages.publishingChecklist) }}</h2>
|
<h2>Publishing checklist</h2>
|
||||||
|
<div class="checklist">
|
||||||
|
<span class="checklist__title">Progress:</span>
|
||||||
|
<div class="checklist__items">
|
||||||
|
<div
|
||||||
|
v-for="nag in nags"
|
||||||
|
:key="`checklist-${nag.id}`"
|
||||||
|
v-tooltip="nag.title"
|
||||||
|
:aria-label="nag.title"
|
||||||
|
:class="'circle ' + (!nag.condition ? 'done' : '') + nag.status"
|
||||||
|
class="circle"
|
||||||
|
>
|
||||||
|
<CheckIcon v-if="!nag.condition" />
|
||||||
|
<AsteriskIcon v-else-if="nag.status === 'required'" />
|
||||||
|
<LightBulbIcon v-else-if="nag.status === 'suggestion'" />
|
||||||
|
<ScaleIcon v-else-if="nag.status === 'review'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<ButtonStyled circular>
|
<button
|
||||||
<button :class="!collapsed && '[&>svg]:rotate-180'" @click="toggleCollapsed()">
|
:class="{ 'not-collapsed': !collapsed }"
|
||||||
<DropdownIcon class="duration-250 transition-transform ease-in-out" />
|
class="square-button"
|
||||||
|
@click="toggleCollapsed()"
|
||||||
|
>
|
||||||
|
<DropdownIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!collapsed" class="grid-display width-16 mt-4">
|
<div v-if="!collapsed" class="grid-display width-16">
|
||||||
<div v-for="nag in visibleNags" :key="nag.id" class="grid-display__item">
|
<div
|
||||||
<span class="flex items-center gap-2 font-semibold">
|
v-for="nag in nags.filter((x) => x.condition && !x.hide)"
|
||||||
<component
|
:key="nag.id"
|
||||||
:is="nag.icon || getDefaultIcon(nag.status)"
|
class="grid-display__item"
|
||||||
v-tooltip="getStatusTooltip(nag.status)"
|
>
|
||||||
:class="[
|
<span class="label">
|
||||||
'h-4 w-4',
|
<AsteriskIcon
|
||||||
nag.status === 'required' && 'text-red',
|
v-if="nag.status === 'required'"
|
||||||
nag.status === 'warning' && 'text-orange',
|
v-tooltip="'Required'"
|
||||||
nag.status === 'suggestion' && 'text-purple',
|
:class="nag.status"
|
||||||
]"
|
aria-label="Required"
|
||||||
:aria-label="getStatusTooltip(nag.status)"
|
|
||||||
/>
|
/>
|
||||||
{{ getFormattedMessage(nag.title) }}
|
<LightBulbIcon
|
||||||
</span>
|
v-else-if="nag.status === 'suggestion'"
|
||||||
{{ getNagDescription(nag) }}
|
v-tooltip="'Suggestion'"
|
||||||
|
:class="nag.status"
|
||||||
|
aria-label="Suggestion"
|
||||||
|
/>
|
||||||
|
<ScaleIcon
|
||||||
|
v-else-if="nag.status === 'review'"
|
||||||
|
v-tooltip="'Review'"
|
||||||
|
:class="nag.status"
|
||||||
|
aria-label="Review"
|
||||||
|
/>{{ nag.title }}</span
|
||||||
|
>
|
||||||
|
{{ nag.description }}
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="nag.link && shouldShowLink(nag)"
|
v-if="nag.link"
|
||||||
|
:class="{ invisible: nag.link.hide }"
|
||||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
||||||
nag.link.path
|
nag.link.path
|
||||||
}`"
|
}`"
|
||||||
class="goto-link"
|
class="goto-link"
|
||||||
>
|
>
|
||||||
{{ getFormattedMessage(nag.link.title) }}
|
{{ nag.link.title }}
|
||||||
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
|
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<ButtonStyled
|
|
||||||
v-if="nag.status === 'special-submit-action' && nag.id === 'submit-for-review'"
|
|
||||||
color="orange"
|
|
||||||
@click="submitForReview"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
:disabled="!canSubmitForReview"
|
v-else-if="nag.action"
|
||||||
v-tooltip="
|
:disabled="nag.action.disabled()"
|
||||||
!canSubmitForReview ? getFormattedMessage(messages.submitChecklistTooltip) : undefined
|
class="btn btn-orange"
|
||||||
"
|
@click="nag.action.onClick"
|
||||||
>
|
>
|
||||||
<SendIcon />
|
<SendIcon />
|
||||||
{{ getFormattedMessage(messages.submitForReview) }}
|
{{ nag.action.title }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
AsteriskIcon,
|
AsteriskIcon,
|
||||||
LightBulbIcon,
|
LightBulbIcon,
|
||||||
TriangleAlertIcon,
|
|
||||||
DropdownIcon,
|
|
||||||
SendIcon,
|
SendIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
|
DropdownIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
|
import { formatProjectType } from "@modrinth/utils";
|
||||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||||
import { nags } from "@modrinth/moderation";
|
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
|
||||||
import { useVIntl, defineMessages, type MessageDescriptor } from "@vintl/vintl";
|
|
||||||
import type { Nag, NagContext, NagStatus } from "@modrinth/moderation";
|
|
||||||
import type { Project, User, Version } from "@modrinth/utils";
|
|
||||||
import type { Component } from "vue";
|
|
||||||
|
|
||||||
interface Tags {
|
const props = defineProps({
|
||||||
rejectedStatuses: string[];
|
project: {
|
||||||
}
|
type: Object,
|
||||||
|
required: true,
|
||||||
interface Auth {
|
},
|
||||||
user: {
|
versions: {
|
||||||
id: string;
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentMember: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
allMembers: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isSettings: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
routeName: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
setProcessing: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return () => {
|
||||||
|
addNotification({
|
||||||
|
group: "main",
|
||||||
|
title: "An error occurred",
|
||||||
|
text: "setProcessing function not found",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
interface Member {
|
|
||||||
accepted?: boolean;
|
|
||||||
project_role?: string;
|
|
||||||
user?: Partial<User>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
project: Project;
|
|
||||||
versions?: Version[];
|
|
||||||
currentMember?: Member | null;
|
|
||||||
allMembers?: Member[] | null;
|
|
||||||
isSettings?: boolean;
|
|
||||||
collapsed?: boolean;
|
|
||||||
routeName?: string;
|
|
||||||
auth: Auth;
|
|
||||||
tags: Tags;
|
|
||||||
setProcessing?: (processing: boolean) => void;
|
|
||||||
toggleCollapsed?: () => void;
|
|
||||||
updateMembers?: () => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
invitationTitle: {
|
|
||||||
id: "project-member-header.invitation-title",
|
|
||||||
defaultMessage: "Invitation to join project",
|
|
||||||
},
|
},
|
||||||
invitationWithRole: {
|
|
||||||
id: "project-member-header.invitation-with-role",
|
|
||||||
defaultMessage: "You've been invited be a member of this project with the role of '{role}'.",
|
|
||||||
},
|
},
|
||||||
invitationNoRole: {
|
toggleCollapsed: {
|
||||||
id: "project-member-header.invitation-no-role",
|
type: Function,
|
||||||
defaultMessage:
|
default() {
|
||||||
"You've been invited to join this project. Please accept or decline the invitation.",
|
return () => {
|
||||||
|
addNotification({
|
||||||
|
group: "main",
|
||||||
|
title: "An error occurred",
|
||||||
|
text: "toggleCollapsed function not found",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
};
|
||||||
},
|
},
|
||||||
accept: {
|
|
||||||
id: "project-member-header.accept",
|
|
||||||
defaultMessage: "Accept",
|
|
||||||
},
|
},
|
||||||
decline: {
|
updateMembers: {
|
||||||
id: "project-member-header.decline",
|
type: Function,
|
||||||
defaultMessage: "Decline",
|
default() {
|
||||||
|
return () => {
|
||||||
|
addNotification({
|
||||||
|
group: "main",
|
||||||
|
title: "An error occurred",
|
||||||
|
text: "updateMembers function not found",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
};
|
||||||
},
|
},
|
||||||
publishingChecklist: {
|
|
||||||
id: "project-member-header.publishing-checklist",
|
|
||||||
defaultMessage: "Publishing checklist",
|
|
||||||
},
|
|
||||||
submitForReview: {
|
|
||||||
id: "project-member-header.submit-for-review",
|
|
||||||
defaultMessage: "Submit for review",
|
|
||||||
},
|
|
||||||
submitForReviewDesc: {
|
|
||||||
id: "project-member-header.submit-for-review-desc",
|
|
||||||
defaultMessage:
|
|
||||||
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
|
|
||||||
},
|
|
||||||
resubmitForReview: {
|
|
||||||
id: "project-member-header.resubmit-for-review",
|
|
||||||
defaultMessage: "Resubmit for review",
|
|
||||||
},
|
|
||||||
resubmitForReviewDesc: {
|
|
||||||
id: "project-member-header.resubmit-for-review-desc",
|
|
||||||
defaultMessage:
|
|
||||||
"Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
|
|
||||||
},
|
|
||||||
visitModerationPage: {
|
|
||||||
id: "project-member-header.visit-moderation-page",
|
|
||||||
defaultMessage: "Visit moderation page",
|
|
||||||
},
|
|
||||||
submitChecklistTooltip: {
|
|
||||||
id: "project-member-header.submit-checklist-tooltip",
|
|
||||||
defaultMessage: "You must complete the required steps in the publishing checklist!",
|
|
||||||
},
|
|
||||||
successJoin: {
|
|
||||||
id: "project-member-header.success-join",
|
|
||||||
defaultMessage: "You have joined the project team",
|
|
||||||
},
|
|
||||||
errorJoin: {
|
|
||||||
id: "project-member-header.error-join",
|
|
||||||
defaultMessage: "Failed to accept team invitation",
|
|
||||||
},
|
|
||||||
successDecline: {
|
|
||||||
id: "project-member-header.success-decline",
|
|
||||||
defaultMessage: "You have declined the team invitation",
|
|
||||||
},
|
|
||||||
errorDecline: {
|
|
||||||
id: "project-member-header.error-decline",
|
|
||||||
defaultMessage: "Failed to decline team invitation",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
id: "project-member-header.success",
|
|
||||||
defaultMessage: "Success",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
id: "project-member-header.error",
|
|
||||||
defaultMessage: "Error",
|
|
||||||
},
|
|
||||||
required: {
|
|
||||||
id: "project-member-header.required",
|
|
||||||
defaultMessage: "Required",
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
id: "project-member-header.warning",
|
|
||||||
defaultMessage: "Warning",
|
|
||||||
},
|
|
||||||
suggestion: {
|
|
||||||
id: "project-member-header.suggestion",
|
|
||||||
defaultMessage: "Suggestion",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured));
|
||||||
|
|
||||||
function getNagDescription(nag: Nag): string {
|
const nags = computed(() => [
|
||||||
if (typeof nag.description === "function") {
|
{
|
||||||
return nag.description(nagContext.value);
|
condition: props.versions.length < 1,
|
||||||
}
|
title: "Upload a version",
|
||||||
return formatMessage(nag.description);
|
id: "upload-version",
|
||||||
}
|
description: "At least one version is required for a project to be submitted for review.",
|
||||||
|
status: "required",
|
||||||
function getFormattedMessage(message: string | MessageDescriptor): string {
|
link: {
|
||||||
if (typeof message === "string") {
|
path: "versions",
|
||||||
return message;
|
title: "Visit versions page",
|
||||||
}
|
hide: props.routeName === "type-id-versions",
|
||||||
return formatMessage(message);
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
condition:
|
||||||
versions: () => [],
|
props.project.body === "" || props.project.body.startsWith("# Placeholder description"),
|
||||||
currentMember: null,
|
title: "Add a description",
|
||||||
allMembers: null,
|
id: "add-description",
|
||||||
isSettings: false,
|
description:
|
||||||
collapsed: false,
|
"A description that clearly describes the project's purpose and function is required.",
|
||||||
routeName: "",
|
status: "required",
|
||||||
});
|
link: {
|
||||||
|
path: "settings/description",
|
||||||
const emit = defineEmits<{
|
title: "Visit description settings",
|
||||||
toggleCollapsed: [];
|
hide: props.routeName === "type-id-settings-description",
|
||||||
updateMembers: [];
|
},
|
||||||
setProcessing: [processing: boolean];
|
},
|
||||||
}>();
|
{
|
||||||
|
condition: !props.project.icon_url,
|
||||||
const nagContext = computed<NagContext>(() => ({
|
title: "Add an icon",
|
||||||
project: props.project,
|
id: "add-icon",
|
||||||
versions: props.versions,
|
description:
|
||||||
currentMember: props.currentMember as User,
|
"Your project should have a nice-looking icon to uniquely identify your project at a glance.",
|
||||||
currentRoute: props.routeName,
|
status: "suggestion",
|
||||||
tags: props.tags,
|
link: {
|
||||||
submitProject: submitForReview,
|
path: "settings",
|
||||||
}));
|
title: "Visit general settings",
|
||||||
|
hide: props.routeName === "type-id-settings",
|
||||||
const canSubmitForReview = computed(() => {
|
},
|
||||||
return (
|
},
|
||||||
applicableNags.value.filter((nag) => nag.status === "required" && !isNagComplete(nag))
|
{
|
||||||
.length === 0
|
condition: props.project.gallery.length === 0 || !featuredGalleryImage,
|
||||||
);
|
title: "Feature a gallery image",
|
||||||
});
|
id: "feature-gallery-image",
|
||||||
|
description: "Featured gallery images may be the first impression of many users.",
|
||||||
async function submitForReview() {
|
status: "suggestion",
|
||||||
if (canSubmitForReview) {
|
link: {
|
||||||
await setProcessing(true);
|
path: "gallery",
|
||||||
}
|
title: "Visit gallery page",
|
||||||
}
|
hide: props.routeName === "type-id-gallery",
|
||||||
|
},
|
||||||
const applicableNags = computed<Nag[]>(() => {
|
},
|
||||||
return nags.filter((nag) => {
|
{
|
||||||
return nag.shouldShow(nagContext.value);
|
hide: props.project.versions.length === 0,
|
||||||
});
|
condition: props.project.categories.length < 1,
|
||||||
});
|
title: "Select tags",
|
||||||
|
id: "select-tags",
|
||||||
function isNagComplete(nag: Nag): boolean {
|
description: "Select all tags that apply to your project.",
|
||||||
const context = nagContext.value;
|
status: "suggestion",
|
||||||
return !nag.shouldShow(context);
|
link: {
|
||||||
}
|
path: "settings/tags",
|
||||||
|
title: "Visit tag settings",
|
||||||
const visibleNags = computed<Nag[]>(() => {
|
hide: props.routeName === "type-id-settings-tags",
|
||||||
const finalNags = applicableNags.value.filter((nag) => !isNagComplete(nag));
|
},
|
||||||
|
},
|
||||||
if (props.project.status === "draft") {
|
{
|
||||||
finalNags.push({
|
condition: !(
|
||||||
|
props.project.issues_url ||
|
||||||
|
props.project.source_url ||
|
||||||
|
props.project.wiki_url ||
|
||||||
|
props.project.discord_url ||
|
||||||
|
props.project.donation_urls.length > 0
|
||||||
|
),
|
||||||
|
title: "Add external links",
|
||||||
|
id: "add-links",
|
||||||
|
description:
|
||||||
|
"Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.",
|
||||||
|
status: "suggestion",
|
||||||
|
link: {
|
||||||
|
path: "settings/links",
|
||||||
|
title: "Visit links settings",
|
||||||
|
hide: props.routeName === "type-id-settings-links",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hide:
|
||||||
|
props.project.versions.length === 0 ||
|
||||||
|
props.project.project_type === "resourcepack" ||
|
||||||
|
props.project.project_type === "plugin" ||
|
||||||
|
props.project.project_type === "shader" ||
|
||||||
|
props.project.project_type === "datapack",
|
||||||
|
condition:
|
||||||
|
props.project.client_side === "unknown" ||
|
||||||
|
props.project.server_side === "unknown" ||
|
||||||
|
(props.project.client_side === "unsupported" && props.project.server_side === "unsupported"),
|
||||||
|
title: "Select supported environments",
|
||||||
|
id: "select-environments",
|
||||||
|
description: `Select if the ${formatProjectType(
|
||||||
|
props.project.project_type,
|
||||||
|
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||||
|
status: "required",
|
||||||
|
link: {
|
||||||
|
path: "settings",
|
||||||
|
title: "Visit general settings",
|
||||||
|
hide: props.routeName === "type-id-settings",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: props.project.license.id === "LicenseRef-Unknown",
|
||||||
|
title: "Select license",
|
||||||
|
id: "select-license",
|
||||||
|
description: `Select the license your ${formatProjectType(
|
||||||
|
props.project.project_type,
|
||||||
|
).toLowerCase()} is distributed under.`,
|
||||||
|
status: "required",
|
||||||
|
link: {
|
||||||
|
path: "settings/license",
|
||||||
|
title: "Visit license settings",
|
||||||
|
hide: props.routeName === "type-id-settings-license",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: props.project.status === "draft",
|
||||||
|
title: "Submit for review",
|
||||||
id: "submit-for-review",
|
id: "submit-for-review",
|
||||||
title: messages.submitForReview,
|
description:
|
||||||
description: () => formatMessage(messages.submitForReviewDesc),
|
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
|
||||||
status: "special-submit-action",
|
status: "review",
|
||||||
shouldShow: (ctx) => ctx.project.status === "draft",
|
link: null,
|
||||||
});
|
action: {
|
||||||
}
|
onClick: submitForReview,
|
||||||
|
title: "Submit for review",
|
||||||
if (props.tags.rejectedStatuses.includes(props.project.status)) {
|
disabled: () => nags.value.filter((x) => x.condition && x.status === "required").length > 0,
|
||||||
finalNags.push({
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hide: props.project.stats === "draft",
|
||||||
|
condition: props.tags.rejectedStatuses.includes(props.project.status),
|
||||||
|
title: "Resubmit for review",
|
||||||
id: "resubmit-for-review",
|
id: "resubmit-for-review",
|
||||||
title: messages.resubmitForReview,
|
description: `Your project has been ${props.project.status} by
|
||||||
description: (ctx) =>
|
Modrinth's staff. In most cases, you can resubmit for review after
|
||||||
formatMessage(messages.resubmitForReviewDesc, { status: ctx.project.status }),
|
addressing the staff's message.`,
|
||||||
status: "special-submit-action",
|
status: "review",
|
||||||
shouldShow: (ctx) => ctx.tags.rejectedStatuses.includes(ctx.project.status),
|
|
||||||
link: {
|
link: {
|
||||||
path: "moderation",
|
path: "moderation",
|
||||||
title: messages.visitModerationPage,
|
title: "Visit moderation page",
|
||||||
shouldShow: () => props.routeName !== "type-id-moderation",
|
hide: props.routeName === "type-id-moderation",
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
]);
|
||||||
|
|
||||||
return finalNags;
|
const showInvitation = computed(() => {
|
||||||
});
|
|
||||||
|
|
||||||
function shouldShowLink(nag: Nag): boolean {
|
|
||||||
return nag.link?.shouldShow ? nag.link.shouldShow(nagContext.value) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultIcon(status: NagStatus): Component {
|
|
||||||
switch (status) {
|
|
||||||
case "required":
|
|
||||||
return AsteriskIcon;
|
|
||||||
case "warning":
|
|
||||||
return TriangleAlertIcon;
|
|
||||||
case "suggestion":
|
|
||||||
return LightBulbIcon;
|
|
||||||
case "special-submit-action":
|
|
||||||
return ScaleIcon;
|
|
||||||
default:
|
|
||||||
return AsteriskIcon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusTooltip(status: NagStatus): string {
|
|
||||||
switch (status) {
|
|
||||||
case "required":
|
|
||||||
return formatMessage(messages.required);
|
|
||||||
case "warning":
|
|
||||||
return formatMessage(messages.warning);
|
|
||||||
case "suggestion":
|
|
||||||
return formatMessage(messages.suggestion);
|
|
||||||
default:
|
|
||||||
return formatMessage(messages.required);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showInvitation = computed<boolean>(() => {
|
|
||||||
if (props.allMembers && props.auth) {
|
if (props.allMembers && props.auth) {
|
||||||
const member = props.allMembers.find((x) => x?.user?.id === props.auth.user.id);
|
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id);
|
||||||
return !!member && !member.accepted;
|
return member && !member.accepted;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleCollapsed(): void {
|
const acceptInvite = () => {
|
||||||
if (props.toggleCollapsed) {
|
acceptTeamInvite(props.project.team);
|
||||||
props.toggleCollapsed();
|
props.updateMembers();
|
||||||
} else {
|
};
|
||||||
emit("toggleCollapsed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateMembers(): Promise<void> {
|
const declineInvite = () => {
|
||||||
if (props.updateMembers) {
|
removeTeamMember(props.project.team, props.auth.user.id);
|
||||||
await props.updateMembers();
|
props.updateMembers();
|
||||||
} else {
|
};
|
||||||
emit("updateMembers");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProcessing(processing: boolean): void {
|
const submitForReview = async () => {
|
||||||
if (props.setProcessing) {
|
if (
|
||||||
props.setProcessing(processing);
|
!props.acknowledgedMessage ||
|
||||||
} else {
|
nags.value.filter((x) => x.condition && x.status === "required").length === 0
|
||||||
emit("setProcessing", processing);
|
) {
|
||||||
}
|
await props.setProcessing();
|
||||||
}
|
|
||||||
|
|
||||||
async function acceptInvite(): Promise<void> {
|
|
||||||
try {
|
|
||||||
setProcessing(true);
|
|
||||||
await acceptTeamInvite(props.project.team);
|
|
||||||
await updateMembers();
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.success),
|
|
||||||
text: formatMessage(messages.successJoin),
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.error),
|
|
||||||
text: formatMessage(messages.errorJoin),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function declineInvite(): Promise<void> {
|
|
||||||
try {
|
|
||||||
setProcessing(true);
|
|
||||||
await removeTeamMember(props.project.team, props.auth.user.id);
|
|
||||||
await updateMembers();
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.success),
|
|
||||||
text: formatMessage(messages.successDecline),
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
addNotification({
|
|
||||||
group: "main",
|
|
||||||
title: formatMessage(messages.error),
|
|
||||||
text: formatMessage(messages.errorDecline),
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.duration-250 {
|
.invited {
|
||||||
transition-duration: 250ms;
|
}
|
||||||
|
|
||||||
|
.author-actions {
|
||||||
|
margin-top: var(--spacing-card-md);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__row {
|
||||||
|
align-items: center;
|
||||||
|
column-gap: var(--spacing-card-lg);
|
||||||
|
row-gap: var(--spacing-card-md);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.header__title {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: var(--spacing-card-lg);
|
||||||
|
row-gap: var(--spacing-card-md);
|
||||||
|
flex-basis: min-content;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 auto 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
svg {
|
||||||
|
transition: transform 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.not-collapsed svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-display__item .label {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-card-xs);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion {
|
||||||
|
color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review {
|
||||||
|
color: var(--color-orange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-card-xs);
|
||||||
|
width: fit-content;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.checklist__title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: var(--spacing-card-xs);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist__items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-card-xs);
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
--circle-size: 2rem;
|
||||||
|
--background-color: var(--color-bg);
|
||||||
|
--content-color: var(--color-gray);
|
||||||
|
width: var(--circle-size);
|
||||||
|
height: var(--circle-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--content-color);
|
||||||
|
width: calc(var(--circle-size) / 2);
|
||||||
|
height: calc(var(--circle-size) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.required {
|
||||||
|
--content-color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.suggestion {
|
||||||
|
--content-color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.review {
|
||||||
|
--content-color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.done {
|
||||||
|
--background-color: var(--color-green);
|
||||||
|
--content-color: var(--color-brand-inverted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -533,69 +533,6 @@
|
|||||||
"profile.user-id": {
|
"profile.user-id": {
|
||||||
"message": "User ID: {id}"
|
"message": "User ID: {id}"
|
||||||
},
|
},
|
||||||
"project-member-header.accept": {
|
|
||||||
"message": "Accept"
|
|
||||||
},
|
|
||||||
"project-member-header.decline": {
|
|
||||||
"message": "Decline"
|
|
||||||
},
|
|
||||||
"project-member-header.error": {
|
|
||||||
"message": "Error"
|
|
||||||
},
|
|
||||||
"project-member-header.error-decline": {
|
|
||||||
"message": "Failed to decline team invitation"
|
|
||||||
},
|
|
||||||
"project-member-header.error-join": {
|
|
||||||
"message": "Failed to accept team invitation"
|
|
||||||
},
|
|
||||||
"project-member-header.invitation-no-role": {
|
|
||||||
"message": "You've been invited to join this project. Please accept or decline the invitation."
|
|
||||||
},
|
|
||||||
"project-member-header.invitation-title": {
|
|
||||||
"message": "Invitation to join project"
|
|
||||||
},
|
|
||||||
"project-member-header.invitation-with-role": {
|
|
||||||
"message": "You've been invited be a member of this project with the role of '{role}'."
|
|
||||||
},
|
|
||||||
"project-member-header.publishing-checklist": {
|
|
||||||
"message": "Publishing checklist"
|
|
||||||
},
|
|
||||||
"project-member-header.required": {
|
|
||||||
"message": "Required"
|
|
||||||
},
|
|
||||||
"project-member-header.resubmit-for-review": {
|
|
||||||
"message": "Resubmit for review"
|
|
||||||
},
|
|
||||||
"project-member-header.resubmit-for-review-desc": {
|
|
||||||
"message": "Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message."
|
|
||||||
},
|
|
||||||
"project-member-header.submit-checklist-tooltip": {
|
|
||||||
"message": "You must complete the required steps in the publishing checklist!"
|
|
||||||
},
|
|
||||||
"project-member-header.submit-for-review": {
|
|
||||||
"message": "Submit for review"
|
|
||||||
},
|
|
||||||
"project-member-header.submit-for-review-desc": {
|
|
||||||
"message": "Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published."
|
|
||||||
},
|
|
||||||
"project-member-header.success": {
|
|
||||||
"message": "Success"
|
|
||||||
},
|
|
||||||
"project-member-header.success-decline": {
|
|
||||||
"message": "You have declined the team invitation"
|
|
||||||
},
|
|
||||||
"project-member-header.success-join": {
|
|
||||||
"message": "You have joined the project team"
|
|
||||||
},
|
|
||||||
"project-member-header.suggestion": {
|
|
||||||
"message": "Suggestion"
|
|
||||||
},
|
|
||||||
"project-member-header.visit-moderation-page": {
|
|
||||||
"message": "Visit moderation page"
|
|
||||||
},
|
|
||||||
"project-member-header.warning": {
|
|
||||||
"message": "Warning"
|
|
||||||
},
|
|
||||||
"project-type.collection.plural": {
|
"project-type.collection.plural": {
|
||||||
"message": "Collections"
|
"message": "Collections"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -22,10 +22,6 @@
|
|||||||
"
|
"
|
||||||
:on-image-upload="onUploadHandler"
|
:on-image-upload="onUploadHandler"
|
||||||
/>
|
/>
|
||||||
<div v-if="descriptionWarning" class="flex items-center gap-1.5 text-orange">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ descriptionWarning }}
|
|
||||||
</div>
|
|
||||||
<div class="input-group markdown-disclaimer">
|
<div class="input-group markdown-disclaimer">
|
||||||
<button
|
<button
|
||||||
:disabled="!hasChanges"
|
:disabled="!hasChanges"
|
||||||
@ -42,8 +38,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
|
import { SaveIcon } from "@modrinth/assets";
|
||||||
import { MIN_DESCRIPTION_CHARS } from "@modrinth/moderation";
|
|
||||||
import { MarkdownEditor } from "@modrinth/ui";
|
import { MarkdownEditor } from "@modrinth/ui";
|
||||||
import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils";
|
import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
@ -58,17 +53,6 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const description = ref(props.project.body);
|
const description = ref(props.project.body);
|
||||||
|
|
||||||
const descriptionWarning = computed(() => {
|
|
||||||
const text = description.value?.trim() || "";
|
|
||||||
const charCount = text.length;
|
|
||||||
|
|
||||||
if (charCount < MIN_DESCRIPTION_CHARS) {
|
|
||||||
return `It's recommended to have a description with at least ${MIN_DESCRIPTION_CHARS} characters. (${charCount}/${MIN_DESCRIPTION_CHARS})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchRequestPayload = computed(() => {
|
const patchRequestPayload = computed(() => {
|
||||||
const payload: {
|
const payload: {
|
||||||
body?: string;
|
body?: string;
|
||||||
|
|||||||
@ -82,10 +82,6 @@
|
|||||||
<label for="project-summary">
|
<label for="project-summary">
|
||||||
<span class="label__title">Summary</span>
|
<span class="label__title">Summary</span>
|
||||||
</label>
|
</label>
|
||||||
<div v-if="summaryWarning" class="my-2 flex items-center gap-1.5 text-orange">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ summaryWarning }}
|
|
||||||
</div>
|
|
||||||
<div class="textarea-wrapper summary-input">
|
<div class="textarea-wrapper summary-input">
|
||||||
<textarea
|
<textarea
|
||||||
id="project-summary"
|
id="project-summary"
|
||||||
@ -244,18 +240,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
|
import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
|
||||||
import {
|
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
||||||
UploadIcon,
|
|
||||||
SaveIcon,
|
|
||||||
TrashIcon,
|
|
||||||
XIcon,
|
|
||||||
IssuesIcon,
|
|
||||||
CheckIcon,
|
|
||||||
TriangleAlertIcon,
|
|
||||||
} from "@modrinth/assets";
|
|
||||||
import { Multiselect } from "vue-multiselect";
|
import { Multiselect } from "vue-multiselect";
|
||||||
import { ConfirmModal, Avatar } from "@modrinth/ui";
|
import { ConfirmModal, Avatar } from "@modrinth/ui";
|
||||||
import { MIN_SUMMARY_CHARS } from "@modrinth/moderation";
|
|
||||||
import FileInput from "~/components/ui/FileInput.vue";
|
import FileInput from "~/components/ui/FileInput.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -313,17 +300,6 @@ const hasDeletePermission = computed(() => {
|
|||||||
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT;
|
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT;
|
||||||
});
|
});
|
||||||
|
|
||||||
const summaryWarning = computed(() => {
|
|
||||||
const text = summary.value?.trim() || "";
|
|
||||||
const charCount = text.length;
|
|
||||||
|
|
||||||
if (charCount < MIN_SUMMARY_CHARS) {
|
|
||||||
return `It's recommended to have a summary with at least ${MIN_SUMMARY_CHARS} characters. (${charCount}/${MIN_SUMMARY_CHARS})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sideTypes = ["required", "optional", "unsupported"];
|
const sideTypes = ["required", "optional", "unsupported"];
|
||||||
|
|
||||||
const patchData = computed(() => {
|
const patchData = computed(() => {
|
||||||
|
|||||||
@ -12,11 +12,6 @@
|
|||||||
A place for users to report bugs, issues, and concerns about your project.
|
A place for users to report bugs, issues, and concerns about your project.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<TriangleAlertIcon
|
|
||||||
v-if="!isIssuesUrlCommon"
|
|
||||||
v-tooltip="`You're using a link which isn't common for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
id="project-issue-tracker"
|
id="project-issue-tracker"
|
||||||
v-model="issuesUrl"
|
v-model="issuesUrl"
|
||||||
@ -36,11 +31,6 @@
|
|||||||
A page/repository containing the source code for your project
|
A page/repository containing the source code for your project
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<TriangleAlertIcon
|
|
||||||
v-if="!isSourceUrlCommon"
|
|
||||||
v-tooltip="`You're using a link which isn't common for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
id="project-source-code"
|
id="project-source-code"
|
||||||
v-model="sourceUrl"
|
v-model="sourceUrl"
|
||||||
@ -74,11 +64,6 @@
|
|||||||
<span class="label__title">Discord invite</span>
|
<span class="label__title">Discord invite</span>
|
||||||
<span class="label__description"> An invitation link to your Discord server. </span>
|
<span class="label__description"> An invitation link to your Discord server. </span>
|
||||||
</label>
|
</label>
|
||||||
<TriangleAlertIcon
|
|
||||||
v-if="!isDiscordUrlCommon"
|
|
||||||
v-tooltip="`You're using a link which isn't common for this link type.`"
|
|
||||||
class="size-6 animate-pulse text-orange"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
id="project-discord-invite"
|
id="project-discord-invite"
|
||||||
v-model="discordUrl"
|
v-model="discordUrl"
|
||||||
@ -138,8 +123,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { DropdownSelect } from "@modrinth/ui";
|
import { DropdownSelect } from "@modrinth/ui";
|
||||||
import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
|
import { SaveIcon } from "@modrinth/assets";
|
||||||
import { isCommonUrl, commonLinkDomains } from "@modrinth/moderation";
|
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
|
||||||
@ -169,21 +153,6 @@ const sourceUrl = ref(props.project.source_url);
|
|||||||
const wikiUrl = ref(props.project.wiki_url);
|
const wikiUrl = ref(props.project.wiki_url);
|
||||||
const discordUrl = ref(props.project.discord_url);
|
const discordUrl = ref(props.project.discord_url);
|
||||||
|
|
||||||
const isIssuesUrlCommon = computed(() => {
|
|
||||||
if (!issuesUrl.value || issuesUrl.value.trim().length === 0) return true;
|
|
||||||
return isCommonUrl(issuesUrl.value, commonLinkDomains.issues);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSourceUrlCommon = computed(() => {
|
|
||||||
if (!sourceUrl.value || sourceUrl.value.trim().length === 0) return true;
|
|
||||||
return isCommonUrl(sourceUrl.value, commonLinkDomains.source);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDiscordUrlCommon = computed(() => {
|
|
||||||
if (!discordUrl.value || discordUrl.value.trim().length === 0) return true;
|
|
||||||
return isCommonUrl(discordUrl.value, commonLinkDomains.discord);
|
|
||||||
});
|
|
||||||
|
|
||||||
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls));
|
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls));
|
||||||
rawDonationLinks.push({
|
rawDonationLinks.push({
|
||||||
id: null,
|
id: null,
|
||||||
|
|||||||
@ -6,31 +6,11 @@
|
|||||||
<span class="label__title size-card-header">Tags</span>
|
<span class="label__title size-card-header">Tags</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="tooManyTagsWarning && !allTagsSelectedWarning"
|
|
||||||
class="my-2 flex items-center gap-1.5 text-orange"
|
|
||||||
>
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ tooManyTagsWarning }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="multipleResolutionTagsWarning" class="my-2 flex items-center gap-1.5 text-orange">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
{{ multipleResolutionTagsWarning }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="allTagsSelectedWarning" class="my-2 flex items-center gap-1.5 text-red">
|
|
||||||
<TriangleAlertIcon class="my-auto" />
|
|
||||||
<span>{{ allTagsSelectedWarning }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Accurate tagging is important to help people find your
|
Accurate tagging is important to help people find your
|
||||||
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
||||||
that apply.
|
that apply.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-if="project.versions.length === 0" class="known-errors">
|
<p v-if="project.versions.length === 0" class="known-errors">
|
||||||
Please upload a version first in order to select tags!
|
Please upload a version first in order to select tags!
|
||||||
</p>
|
</p>
|
||||||
@ -132,69 +112,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { computed, ref } from "vue";
|
import { StarIcon, SaveIcon } from "@modrinth/assets";
|
||||||
import { StarIcon, SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
|
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils";
|
||||||
import {
|
|
||||||
formatCategory,
|
|
||||||
formatCategoryHeader,
|
|
||||||
formatProjectType,
|
|
||||||
sortedCategories,
|
|
||||||
type Project,
|
|
||||||
} from "@modrinth/utils";
|
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||||
|
|
||||||
interface Category {
|
export default defineNuxtComponent({
|
||||||
name: string;
|
components: {
|
||||||
header: string;
|
Checkbox,
|
||||||
icon?: string;
|
SaveIcon,
|
||||||
project_type: string;
|
StarIcon,
|
||||||
}
|
},
|
||||||
|
props: {
|
||||||
interface Props {
|
project: {
|
||||||
project: Project & {
|
type: Object,
|
||||||
actualProjectType: string;
|
default() {
|
||||||
};
|
return {};
|
||||||
allMembers?: any[];
|
},
|
||||||
currentMember?: any;
|
},
|
||||||
patchProject?: (data: any) => void;
|
allMembers: {
|
||||||
}
|
type: Array,
|
||||||
|
default() {
|
||||||
const tags = useTags();
|
return [];
|
||||||
|
},
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
},
|
||||||
allMembers: () => [],
|
currentMember: {
|
||||||
currentMember: null,
|
type: Object,
|
||||||
patchProject: () => {
|
default() {
|
||||||
addNotification({
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
patchProject: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return () => {
|
||||||
|
this.$notify({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: "Patch project function not found",
|
text: "Patch project function not found",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
},
|
||||||
const selectedTags = ref<Category[]>(
|
data() {
|
||||||
sortedCategories(tags.value).filter(
|
return {
|
||||||
(x: Category) =>
|
selectedTags: this.$sortedCategories().filter(
|
||||||
x.project_type === props.project.actualProjectType &&
|
(x) =>
|
||||||
(props.project.categories.includes(x.name) ||
|
x.project_type === this.project.actualProjectType &&
|
||||||
props.project.additional_categories.includes(x.name)),
|
(this.project.categories.includes(x.name) ||
|
||||||
|
this.project.additional_categories.includes(x.name)),
|
||||||
),
|
),
|
||||||
);
|
featuredTags: this.$sortedCategories().filter(
|
||||||
|
(x) =>
|
||||||
const featuredTags = ref<Category[]>(
|
x.project_type === this.project.actualProjectType &&
|
||||||
sortedCategories(tags.value).filter(
|
this.project.categories.includes(x.name),
|
||||||
(x: Category) =>
|
|
||||||
x.project_type === props.project.actualProjectType &&
|
|
||||||
props.project.categories.includes(x.name),
|
|
||||||
),
|
),
|
||||||
);
|
};
|
||||||
|
},
|
||||||
const categoryLists = computed(() => {
|
computed: {
|
||||||
const lists: Record<string, Category[]> = {};
|
categoryLists() {
|
||||||
sortedCategories(tags.value).forEach((x: Category) => {
|
const lists = {};
|
||||||
if (x.project_type === props.project.actualProjectType) {
|
this.$sortedCategories().forEach((x) => {
|
||||||
|
if (x.project_type === this.project.actualProjectType) {
|
||||||
const header = x.header;
|
const header = x.header;
|
||||||
if (!lists[header]) {
|
if (!lists[header]) {
|
||||||
lists[header] = [];
|
lists[header] = [];
|
||||||
@ -203,110 +184,73 @@ const categoryLists = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return lists;
|
return lists;
|
||||||
});
|
},
|
||||||
|
patchData() {
|
||||||
const tooManyTagsWarning = computed(() => {
|
const data = {};
|
||||||
const tagCount = selectedTags.value.length;
|
|
||||||
if (tagCount > 5) {
|
|
||||||
return `You've selected ${tagCount} tags. Consider reducing to 5 or fewer to keep your project focused and easier to discover.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const multipleResolutionTagsWarning = computed(() => {
|
|
||||||
if (props.project.project_type !== "resourcepack") return null;
|
|
||||||
|
|
||||||
const resolutionTags = selectedTags.value.filter((tag) =>
|
|
||||||
["16x", "32x", "48x", "64x", "128x", "256x", "512x", "1024x"].includes(tag.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resolutionTags.length > 1) {
|
|
||||||
return `You've selected ${resolutionTags.length} resolution tags (${resolutionTags.map((t) => t.name).join(", ")}). Resource packs should typically only have one resolution tag.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const allTagsSelectedWarning = computed(() => {
|
|
||||||
const categoriesForProjectType = sortedCategories(tags.value).filter(
|
|
||||||
(x: Category) => x.project_type === props.project.actualProjectType,
|
|
||||||
);
|
|
||||||
const totalSelectedTags = selectedTags.value.length;
|
|
||||||
|
|
||||||
if (
|
|
||||||
totalSelectedTags === categoriesForProjectType.length &&
|
|
||||||
categoriesForProjectType.length > 0
|
|
||||||
) {
|
|
||||||
return `You've selected all ${categoriesForProjectType.length} available tags. Please select only the tags that truly apply to your project.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchData = computed(() => {
|
|
||||||
const data: Record<string, string[]> = {};
|
|
||||||
|
|
||||||
// Promote selected categories to featured if there are less than 3 featured
|
// Promote selected categories to featured if there are less than 3 featured
|
||||||
const newFeaturedTags = featuredTags.value.slice();
|
const newFeaturedTags = this.featuredTags.slice();
|
||||||
if (newFeaturedTags.length < 1 && selectedTags.value.length > newFeaturedTags.length) {
|
if (newFeaturedTags.length < 1 && this.selectedTags.length > newFeaturedTags.length) {
|
||||||
const nonFeaturedCategories = selectedTags.value.filter((x) => !newFeaturedTags.includes(x));
|
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x));
|
||||||
|
|
||||||
nonFeaturedCategories
|
nonFeaturedCategories
|
||||||
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
||||||
.forEach((x) => newFeaturedTags.push(x));
|
.forEach((x) => newFeaturedTags.push(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert selected and featured categories to backend-usable arrays
|
// Convert selected and featured categories to backend-usable arrays
|
||||||
const categories = newFeaturedTags.map((x) => x.name);
|
const categories = newFeaturedTags.map((x) => x.name);
|
||||||
const additionalCategories = selectedTags.value
|
const additionalCategories = this.selectedTags
|
||||||
.filter((x) => !newFeaturedTags.includes(x))
|
.filter((x) => !newFeaturedTags.includes(x))
|
||||||
.map((x) => x.name);
|
.map((x) => x.name);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
categories.length !== props.project.categories.length ||
|
categories.length !== this.project.categories.length ||
|
||||||
categories.some((value) => !props.project.categories.includes(value))
|
categories.some((value) => !this.project.categories.includes(value))
|
||||||
) {
|
) {
|
||||||
data.categories = categories;
|
data.categories = categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
additionalCategories.length !== props.project.additional_categories.length ||
|
additionalCategories.length !== this.project.additional_categories.length ||
|
||||||
additionalCategories.some((value) => !props.project.additional_categories.includes(value))
|
additionalCategories.some((value) => !this.project.additional_categories.includes(value))
|
||||||
) {
|
) {
|
||||||
data.additional_categories = additionalCategories;
|
data.additional_categories = additionalCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
},
|
||||||
|
hasChanges() {
|
||||||
const hasChanges = computed(() => {
|
return Object.keys(this.patchData).length > 0;
|
||||||
return Object.keys(patchData.value).length > 0;
|
},
|
||||||
});
|
},
|
||||||
|
methods: {
|
||||||
const toggleCategory = (category: Category) => {
|
formatProjectType,
|
||||||
if (selectedTags.value.includes(category)) {
|
formatCategoryHeader,
|
||||||
selectedTags.value = selectedTags.value.filter((x) => x !== category);
|
formatCategory,
|
||||||
if (featuredTags.value.includes(category)) {
|
toggleCategory(category) {
|
||||||
featuredTags.value = featuredTags.value.filter((x) => x !== category);
|
if (this.selectedTags.includes(category)) {
|
||||||
|
this.selectedTags = this.selectedTags.filter((x) => x !== category);
|
||||||
|
if (this.featuredTags.includes(category)) {
|
||||||
|
this.featuredTags = this.featuredTags.filter((x) => x !== category);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedTags.value.push(category);
|
this.selectedTags.push(category);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
toggleFeaturedCategory(category) {
|
||||||
const toggleFeaturedCategory = (category: Category) => {
|
if (this.featuredTags.includes(category)) {
|
||||||
if (featuredTags.value.includes(category)) {
|
this.featuredTags = this.featuredTags.filter((x) => x !== category);
|
||||||
featuredTags.value = featuredTags.value.filter((x) => x !== category);
|
|
||||||
} else {
|
} else {
|
||||||
featuredTags.value.push(category);
|
this.featuredTags.push(category);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
saveChanges() {
|
||||||
const saveChanges = () => {
|
if (this.hasChanges) {
|
||||||
if (hasChanges.value) {
|
this.patchProject(this.patchData);
|
||||||
props.patchProject(patchData.value);
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.label__title {
|
.label__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -150,9 +150,26 @@
|
|||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm text-secondary">
|
<span class="text-sm text-secondary">
|
||||||
|
<span
|
||||||
|
v-if="charge.status === 'cancelled' && $dayjs(charge.due).isBefore($dayjs())"
|
||||||
|
class="font-bold"
|
||||||
|
>
|
||||||
|
Ended:
|
||||||
|
</span>
|
||||||
|
<span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span>
|
||||||
|
<span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span>
|
||||||
|
<span v-else class="font-bold">Due:</span>
|
||||||
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
|
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
|
||||||
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
|
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="charge.last_attempt != null" class="text-sm text-secondary">
|
||||||
|
<span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span>
|
||||||
|
<span v-else class="font-bold">Charged:</span>
|
||||||
|
{{ dayjs(charge.last_attempt).format("MMMM D, YYYY [at] h:mma") }}
|
||||||
|
<span class="text-secondary"
|
||||||
|
>({{ formatRelativeTime(charge.last_attempt) }})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<div class="flex w-full items-center gap-1 text-xs text-secondary">
|
<div class="flex w-full items-center gap-1 text-xs text-secondary">
|
||||||
{{ charge.status }}
|
{{ charge.status }}
|
||||||
⋅
|
⋅
|
||||||
|
|||||||
@ -1,2 +1,10 @@
|
|||||||
# SQLite database file location
|
MODRINTH_URL=http://localhost:3000/
|
||||||
DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
MODRINTH_API_URL=http://127.0.0.1:8000/v2/
|
||||||
|
MODRINTH_API_URL_V3=http://127.0.0.1:8000/v3/
|
||||||
|
MODRINTH_SOCKET_URL=ws://127.0.0.1:8000/
|
||||||
|
MODRINTH_LAUNCHER_META_URL=https://launcher-meta.modrinth.com/
|
||||||
|
|
||||||
|
# SQLite database file used by sqlx for type checking. Uncomment this to a valid path
|
||||||
|
# in your system and run `cargo sqlx database setup` to generate an empty database that
|
||||||
|
# can be used for developing the app DB schema
|
||||||
|
#DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||||
|
|||||||
10
packages/app-lib/.env.prod
Normal file
10
packages/app-lib/.env.prod
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
MODRINTH_URL=https://modrinth.com/
|
||||||
|
MODRINTH_API_URL=https://api.modrinth.com/v2/
|
||||||
|
MODRINTH_API_URL_V3=https://api.modrinth.com/v3/
|
||||||
|
MODRINTH_SOCKET_URL=wss://api.modrinth.com/
|
||||||
|
MODRINTH_LAUNCHER_META_URL=https://launcher-meta.modrinth.com/
|
||||||
|
|
||||||
|
# SQLite database file used by sqlx for type checking. Uncomment this to a valid path
|
||||||
|
# in your system and run `cargo sqlx database setup` to generate an empty database that
|
||||||
|
# can be used for developing the app DB schema
|
||||||
|
#DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||||
10
packages/app-lib/.env.staging
Normal file
10
packages/app-lib/.env.staging
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
MODRINTH_URL=https://staging.modrinth.com/
|
||||||
|
MODRINTH_API_URL=https://staging-api.modrinth.com/v2/
|
||||||
|
MODRINTH_API_URL_V3=https://staging-api.modrinth.com/v3/
|
||||||
|
MODRINTH_SOCKET_URL=wss://staging-api.modrinth.com/
|
||||||
|
MODRINTH_LAUNCHER_META_URL=https://launcher-meta.modrinth.com/
|
||||||
|
|
||||||
|
# SQLite database file used by sqlx for type checking. Uncomment this to a valid path
|
||||||
|
# in your system and run `cargo sqlx database setup` to generate an empty database that
|
||||||
|
# can be used for developing the app DB schema
|
||||||
|
#DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||||
@ -82,6 +82,7 @@ ariadne.workspace = true
|
|||||||
winreg.workspace = true
|
winreg.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
dotenvy.workspace = true
|
||||||
dunce.workspace = true
|
dunce.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@ -4,12 +4,31 @@ use std::process::{Command, exit};
|
|||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
println!("cargo::rerun-if-changed=.env");
|
||||||
println!("cargo::rerun-if-changed=java/gradle");
|
println!("cargo::rerun-if-changed=java/gradle");
|
||||||
println!("cargo::rerun-if-changed=java/src");
|
println!("cargo::rerun-if-changed=java/src");
|
||||||
println!("cargo::rerun-if-changed=java/build.gradle.kts");
|
println!("cargo::rerun-if-changed=java/build.gradle.kts");
|
||||||
println!("cargo::rerun-if-changed=java/settings.gradle.kts");
|
println!("cargo::rerun-if-changed=java/settings.gradle.kts");
|
||||||
println!("cargo::rerun-if-changed=java/gradle.properties");
|
println!("cargo::rerun-if-changed=java/gradle.properties");
|
||||||
|
|
||||||
|
set_env();
|
||||||
|
build_java_jars();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_env() {
|
||||||
|
for (var_name, var_value) in
|
||||||
|
dotenvy::dotenv_iter().into_iter().flatten().flatten()
|
||||||
|
{
|
||||||
|
if var_name == "DATABASE_URL" {
|
||||||
|
// The sqlx database URL is a build-time detail that should not be exposed to the crate
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo::rustc-env={var_name}={var_value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_java_jars() {
|
||||||
let out_dir =
|
let out_dir =
|
||||||
dunce::canonicalize(PathBuf::from(env::var_os("OUT_DIR").unwrap()))
|
dunce::canonicalize(PathBuf::from(env::var_os("OUT_DIR").unwrap()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -37,6 +56,7 @@ fn main() {
|
|||||||
.current_dir(dunce::canonicalize("java").unwrap())
|
.current_dir(dunce::canonicalize("java").unwrap())
|
||||||
.status()
|
.status()
|
||||||
.expect("Failed to wait on Gradle build");
|
.expect("Failed to wait on Gradle build");
|
||||||
|
|
||||||
if !exit_status.success() {
|
if !exit_status.success() {
|
||||||
println!("cargo::error=Gradle build failed with {exit_status}");
|
println!("cargo::error=Gradle build failed with {exit_status}");
|
||||||
exit(exit_status.code().unwrap_or(1));
|
exit(exit_status.code().unwrap_or(1));
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::state::ModrinthCredentials;
|
use crate::state::ModrinthCredentials;
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn authenticate_begin_flow() -> String {
|
pub fn authenticate_begin_flow() -> &'static str {
|
||||||
crate::state::get_login_url()
|
crate::state::get_login_url()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
//! Configuration structs
|
|
||||||
|
|
||||||
// pub const MODRINTH_URL: &str = "https://staging.modrinth.com/";
|
|
||||||
// pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/";
|
|
||||||
// pub const MODRINTH_API_URL_V3: &str = "https://staging-api.modrinth.com/v3/";
|
|
||||||
|
|
||||||
pub const MODRINTH_URL: &str = "https://modrinth.com/";
|
|
||||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
|
|
||||||
pub const MODRINTH_API_URL_V3: &str = "https://api.modrinth.com/v3/";
|
|
||||||
|
|
||||||
pub const MODRINTH_SOCKET_URL: &str = "wss://api.modrinth.com/";
|
|
||||||
|
|
||||||
pub const META_URL: &str = "https://launcher-meta.modrinth.com/";
|
|
||||||
@ -11,7 +11,6 @@ and launching Modrinth mod packs
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod config;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod event;
|
mod event;
|
||||||
mod launcher;
|
mod launcher;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use crate::config::{META_URL, MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
|
||||||
use crate::state::ProjectType;
|
use crate::state::ProjectType;
|
||||||
use crate::util::fetch::{FetchSemaphore, fetch_json, sha1_async};
|
use crate::util::fetch::{FetchSemaphore, fetch_json, sha1_async};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@ -8,6 +7,7 @@ use serde::de::DeserializeOwned;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -945,7 +945,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Project => {
|
CacheValueType::Project => {
|
||||||
fetch_original_values!(
|
fetch_original_values!(
|
||||||
Project,
|
Project,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"projects",
|
"projects",
|
||||||
CacheValue::Project
|
CacheValue::Project
|
||||||
)
|
)
|
||||||
@ -953,7 +953,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Version => {
|
CacheValueType::Version => {
|
||||||
fetch_original_values!(
|
fetch_original_values!(
|
||||||
Version,
|
Version,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"versions",
|
"versions",
|
||||||
CacheValue::Version
|
CacheValue::Version
|
||||||
)
|
)
|
||||||
@ -961,7 +961,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::User => {
|
CacheValueType::User => {
|
||||||
fetch_original_values!(
|
fetch_original_values!(
|
||||||
User,
|
User,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"users",
|
"users",
|
||||||
CacheValue::User
|
CacheValue::User
|
||||||
)
|
)
|
||||||
@ -969,7 +969,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Team => {
|
CacheValueType::Team => {
|
||||||
let mut teams = fetch_many_batched::<Vec<TeamMember>>(
|
let mut teams = fetch_many_batched::<Vec<TeamMember>>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
MODRINTH_API_URL_V3,
|
env!("MODRINTH_API_URL_V3"),
|
||||||
"teams?ids=",
|
"teams?ids=",
|
||||||
&keys,
|
&keys,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
@ -1008,7 +1008,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Organization => {
|
CacheValueType::Organization => {
|
||||||
let mut orgs = fetch_many_batched::<Organization>(
|
let mut orgs = fetch_many_batched::<Organization>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
MODRINTH_API_URL_V3,
|
env!("MODRINTH_API_URL_V3"),
|
||||||
"organizations?ids=",
|
"organizations?ids=",
|
||||||
&keys,
|
&keys,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
@ -1063,7 +1063,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::File => {
|
CacheValueType::File => {
|
||||||
let mut versions = fetch_json::<HashMap<String, Version>>(
|
let mut versions = fetch_json::<HashMap<String, Version>>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("{MODRINTH_API_URL}version_files"),
|
concat!(env!("MODRINTH_API_URL"), "version_files"),
|
||||||
None,
|
None,
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"algorithm": "sha1",
|
"algorithm": "sha1",
|
||||||
@ -1119,7 +1119,11 @@ impl CachedEntry {
|
|||||||
.map(|x| {
|
.map(|x| {
|
||||||
(
|
(
|
||||||
x.key().to_string(),
|
x.key().to_string(),
|
||||||
format!("{META_URL}{}/v0/manifest.json", x.key()),
|
format!(
|
||||||
|
"{}{}/v0/manifest.json",
|
||||||
|
env!("MODRINTH_LAUNCHER_META_URL"),
|
||||||
|
x.key()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -1154,7 +1158,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::MinecraftManifest => {
|
CacheValueType::MinecraftManifest => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
MinecraftManifest,
|
MinecraftManifest,
|
||||||
META_URL,
|
env!("MODRINTH_LAUNCHER_META_URL"),
|
||||||
format!(
|
format!(
|
||||||
"minecraft/v{}/manifest.json",
|
"minecraft/v{}/manifest.json",
|
||||||
daedalus::minecraft::CURRENT_FORMAT_VERSION
|
daedalus::minecraft::CURRENT_FORMAT_VERSION
|
||||||
@ -1165,7 +1169,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Categories => {
|
CacheValueType::Categories => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
Categories,
|
Categories,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/category",
|
"tag/category",
|
||||||
CacheValue::Categories
|
CacheValue::Categories
|
||||||
)
|
)
|
||||||
@ -1173,7 +1177,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::ReportTypes => {
|
CacheValueType::ReportTypes => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
ReportTypes,
|
ReportTypes,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/report_type",
|
"tag/report_type",
|
||||||
CacheValue::ReportTypes
|
CacheValue::ReportTypes
|
||||||
)
|
)
|
||||||
@ -1181,7 +1185,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::Loaders => {
|
CacheValueType::Loaders => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
Loaders,
|
Loaders,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/loader",
|
"tag/loader",
|
||||||
CacheValue::Loaders
|
CacheValue::Loaders
|
||||||
)
|
)
|
||||||
@ -1189,7 +1193,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::GameVersions => {
|
CacheValueType::GameVersions => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
GameVersions,
|
GameVersions,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/game_version",
|
"tag/game_version",
|
||||||
CacheValue::GameVersions
|
CacheValue::GameVersions
|
||||||
)
|
)
|
||||||
@ -1197,7 +1201,7 @@ impl CachedEntry {
|
|||||||
CacheValueType::DonationPlatforms => {
|
CacheValueType::DonationPlatforms => {
|
||||||
fetch_original_value!(
|
fetch_original_value!(
|
||||||
DonationPlatforms,
|
DonationPlatforms,
|
||||||
MODRINTH_API_URL,
|
env!("MODRINTH_API_URL"),
|
||||||
"tag/donation_platform",
|
"tag/donation_platform",
|
||||||
CacheValue::DonationPlatforms
|
CacheValue::DonationPlatforms
|
||||||
)
|
)
|
||||||
@ -1297,14 +1301,12 @@ impl CachedEntry {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let version_update_url =
|
|
||||||
format!("{MODRINTH_API_URL}version_files/update");
|
|
||||||
let variations =
|
let variations =
|
||||||
futures::future::try_join_all(filtered_keys.iter().map(
|
futures::future::try_join_all(filtered_keys.iter().map(
|
||||||
|((loaders_key, game_version), hashes)| {
|
|((loaders_key, game_version), hashes)| {
|
||||||
fetch_json::<HashMap<String, Version>>(
|
fetch_json::<HashMap<String, Version>>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&version_update_url,
|
concat!(env!("MODRINTH_API_URL"), "version_files/update"),
|
||||||
None,
|
None,
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"algorithm": "sha1",
|
"algorithm": "sha1",
|
||||||
@ -1368,7 +1370,11 @@ impl CachedEntry {
|
|||||||
.map(|x| {
|
.map(|x| {
|
||||||
(
|
(
|
||||||
x.key().to_string(),
|
x.key().to_string(),
|
||||||
format!("{MODRINTH_API_URL}search{}", x.key()),
|
format!(
|
||||||
|
"{}search{}",
|
||||||
|
env!("MODRINTH_API_URL"),
|
||||||
|
x.key()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
use crate::LAUNCHER_USER_AGENT;
|
use crate::LAUNCHER_USER_AGENT;
|
||||||
use crate::config::{MODRINTH_API_URL_V3, MODRINTH_SOCKET_URL};
|
|
||||||
use crate::data::ModrinthCredentials;
|
use crate::data::ModrinthCredentials;
|
||||||
use crate::event::FriendPayload;
|
use crate::event::FriendPayload;
|
||||||
use crate::event::emit::emit_friend;
|
use crate::event::emit::emit_friend;
|
||||||
@ -78,7 +77,8 @@ impl FriendsSocket {
|
|||||||
|
|
||||||
if let Some(credentials) = credentials {
|
if let Some(credentials) = credentials {
|
||||||
let mut request = format!(
|
let mut request = format!(
|
||||||
"{MODRINTH_SOCKET_URL}_internal/launcher_socket?code={}",
|
"{}_internal/launcher_socket?code={}",
|
||||||
|
env!("MODRINTH_SOCKET_URL"),
|
||||||
credentials.session
|
credentials.session
|
||||||
)
|
)
|
||||||
.into_client_request()?;
|
.into_client_request()?;
|
||||||
@ -300,7 +300,7 @@ impl FriendsSocket {
|
|||||||
) -> crate::Result<Vec<UserFriend>> {
|
) -> crate::Result<Vec<UserFriend>> {
|
||||||
fetch_json(
|
fetch_json(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!("{MODRINTH_API_URL_V3}friends"),
|
concat!(env!("MODRINTH_API_URL_V3"), "friends"),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
@ -325,7 +325,7 @@ impl FriendsSocket {
|
|||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
fetch_advanced(
|
fetch_advanced(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("{MODRINTH_API_URL_V3}friend/{user_id}"),
|
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@ -346,7 +346,7 @@ impl FriendsSocket {
|
|||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
fetch_advanced(
|
fetch_advanced(
|
||||||
Method::DELETE,
|
Method::DELETE,
|
||||||
&format!("{MODRINTH_API_URL_V3}friend/{user_id}"),
|
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use crate::config::{MODRINTH_API_URL, MODRINTH_URL};
|
|
||||||
use crate::state::{CacheBehaviour, CachedEntry};
|
use crate::state::{CacheBehaviour, CachedEntry};
|
||||||
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
|
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
|
||||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||||
@ -31,7 +30,7 @@ impl ModrinthCredentials {
|
|||||||
|
|
||||||
let resp = fetch_advanced(
|
let resp = fetch_advanced(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("{MODRINTH_API_URL}session/refresh"),
|
concat!(env!("MODRINTH_API_URL"), "session/refresh"),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(("Authorization", &*creds.session)),
|
Some(("Authorization", &*creds.session)),
|
||||||
@ -190,8 +189,8 @@ impl ModrinthCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_login_url() -> String {
|
pub const fn get_login_url() -> &'static str {
|
||||||
format!("{MODRINTH_URL}auth/sign-in?launcher=true")
|
concat!(env!("MODRINTH_URL"), "auth/sign-in?launcher=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn finish_login_flow(
|
pub async fn finish_login_flow(
|
||||||
@ -216,7 +215,7 @@ async fn fetch_info(
|
|||||||
) -> crate::Result<crate::state::cache::User> {
|
) -> crate::Result<crate::state::cache::User> {
|
||||||
let result = fetch_advanced(
|
let result = fetch_advanced(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!("{MODRINTH_API_URL}user"),
|
concat!(env!("MODRINTH_API_URL"), "user"),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(("Authorization", token)),
|
Some(("Authorization", token)),
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
//! Functions for fetching information from the Internet
|
//! Functions for fetching information from the Internet
|
||||||
use super::io::{self, IOError};
|
use super::io::{self, IOError};
|
||||||
use crate::LAUNCHER_USER_AGENT;
|
use crate::LAUNCHER_USER_AGENT;
|
||||||
use crate::config::{MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
|
||||||
use crate::event::LoadingBarId;
|
use crate::event::LoadingBarId;
|
||||||
use crate::event::emit::emit_loading;
|
use crate::event::emit::emit_loading;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@ -82,8 +81,8 @@ pub async fn fetch_advanced(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|x| &*x.0.to_lowercase() != "authorization")
|
.is_none_or(|x| &*x.0.to_lowercase() != "authorization")
|
||||||
&& (url.starts_with("https://cdn.modrinth.com")
|
&& (url.starts_with("https://cdn.modrinth.com")
|
||||||
|| url.starts_with(MODRINTH_API_URL)
|
|| url.starts_with(env!("MODRINTH_API_URL"))
|
||||||
|| url.starts_with(MODRINTH_API_URL_V3))
|
|| url.starts_with(env!("MODRINTH_API_URL_V3")))
|
||||||
{
|
{
|
||||||
crate::state::ModrinthCredentials::get_active(exec).await?
|
crate::state::ModrinthCredentials::get_active(exec).await?
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
<!-- TODO: After checklist v1.5, move everything into src directory. -->
|
|
||||||
|
|
||||||
# @modrinth/moderation
|
# @modrinth/moderation
|
||||||
|
|
||||||
This package contains both the moderation checklist system used by moderators for reviewing projects on Modrinth, and the publishing checklist (nag system) that provides automated feedback to project authors during the submission process.
|
This package contains the moderation checklist system used for reviewing projects on Modrinth. It provides a structured and transparent way to define moderation stages, actions, and messages that are displayed to moderators during the review process.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
@ -11,31 +9,22 @@ The package is organized as follows:
|
|||||||
```
|
```
|
||||||
/packages/moderation/
|
/packages/moderation/
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── checklist.ts # Main moderation checklist definition - imports and exports all stages
|
│ ├── checklist.ts # Main checklist definition - imports and exports all stages
|
||||||
│ ├── messages/ # Markdown files containing message templates for moderation
|
│ ├── messages/ # Markdown files containing message templates
|
||||||
│ │ ├── title/ # Messages for the title stage
|
│ │ ├── title/ # Messages for the title stage
|
||||||
│ │ ├── description/ # Messages for the description stage
|
│ │ ├── description/ # Messages for the description stage
|
||||||
│ │ └── ... # One directory per stage
|
│ │ └── ... # One directory per stage
|
||||||
│ ├── stages/ # Moderation stage definition files
|
│ └── stages/ # Stage definition files
|
||||||
│ │ ├── title.ts # Title stage definition
|
│ ├── title.ts # Title stage definition
|
||||||
│ │ ├── description.ts # Description stage definition
|
│ ├── description.ts # Description stage definition
|
||||||
│ │ └── ... # One file per stage
|
│ └── ... # One file per stage
|
||||||
│ └── nags/ # Publishing checklist (nag system) files
|
|
||||||
│ ├── core.ts # Core nags (required fields, basic validation)
|
|
||||||
│ ├── core.i18n.ts # Internationalization messages for core nags
|
|
||||||
│ └── ...
|
|
||||||
└── types/ # Type definitions
|
└── types/ # Type definitions
|
||||||
├── actions.ts # Action-related types (moderation)
|
├── actions.ts # Action-related types
|
||||||
├── messages.ts # Message-related types (moderation)
|
├── messages.ts # Message-related types
|
||||||
├── stage.ts # Stage-related types (moderation)
|
└── stage.ts # Stage-related types
|
||||||
└── nags.ts # Nag-related types (publishing checklist)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Moderation Checklist System
|
## Stages
|
||||||
|
|
||||||
The moderation checklist provides a structured and transparent way to define moderation stages, actions, and messages that are displayed to moderators during the review process.
|
|
||||||
|
|
||||||
### Stages
|
|
||||||
|
|
||||||
A stage represents a discrete step in the moderation process, like checking a project's title, description, or links. Each stage has:
|
A stage represents a discrete step in the moderation process, like checking a project's title, description, or links. Each stage has:
|
||||||
|
|
||||||
@ -46,7 +35,7 @@ A stage represents a discrete step in the moderation process, like checking a pr
|
|||||||
|
|
||||||
Stages are defined in individual files in the `data/stages` directory and are assembled into the complete checklist in `data/checklist.ts`.
|
Stages are defined in individual files in the `data/stages` directory and are assembled into the complete checklist in `data/checklist.ts`.
|
||||||
|
|
||||||
### Actions
|
## Actions
|
||||||
|
|
||||||
Actions represent decisions moderators can make for each stage. They can be buttons, dropdowns, toggles, etc. Actions can have:
|
Actions represent decisions moderators can make for each stage. They can be buttons, dropdowns, toggles, etc. Actions can have:
|
||||||
|
|
||||||
@ -58,11 +47,11 @@ Actions represent decisions moderators can make for each stage. They can be butt
|
|||||||
|
|
||||||
Each action requires a unique `id` field that is used for conditional logic and action relationships. The `suggestedStatus` and `severity` fields help determine the overall moderation outcome.
|
Each action requires a unique `id` field that is used for conditional logic and action relationships. The `suggestedStatus` and `severity` fields help determine the overall moderation outcome.
|
||||||
|
|
||||||
### Messages
|
## Messages
|
||||||
|
|
||||||
Messages are the actual text that will be included in communications to project authors. To promote maintainability and reuse, messages are stored as Markdown files in the `data/messages` directory, organized by stage.
|
Messages are the actual text that will be included in communications to project authors. To promote maintainability and reuse, messages are stored as Markdown files in the `data/messages` directory, organized by stage.
|
||||||
|
|
||||||
#### Variable replacement
|
### Variable replacement
|
||||||
|
|
||||||
You can use variables in your messages that will be replaced with user input:
|
You can use variables in your messages that will be replaced with user input:
|
||||||
|
|
||||||
@ -92,11 +81,11 @@ More text after the variable.
|
|||||||
|
|
||||||
The `%MESSAGE%` placeholder will be replaced with the text entered by the moderator.
|
The `%MESSAGE%` placeholder will be replaced with the text entered by the moderator.
|
||||||
|
|
||||||
### Conditional logic
|
## Conditional logic
|
||||||
|
|
||||||
The moderation system supports conditional behavior that changes based on the selection of other actions.
|
The moderation system supports conditional behavior that changes based on the selection of other actions.
|
||||||
|
|
||||||
#### Conditional messages
|
### Conditional messages
|
||||||
|
|
||||||
You can define different messages for an action based on other selected actions:
|
You can define different messages for an action based on other selected actions:
|
||||||
|
|
||||||
@ -119,7 +108,7 @@ You can define different messages for an action based on other selected actions:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Enabling and disabling actions
|
### Enabling and disabling actions
|
||||||
|
|
||||||
Actions can enable or disable other actions when selected:
|
Actions can enable or disable other actions when selected:
|
||||||
|
|
||||||
@ -142,7 +131,7 @@ Actions can enable or disable other actions when selected:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Conditional text inputs
|
### Conditional text inputs
|
||||||
|
|
||||||
Text inputs can be conditionally shown based on selected actions:
|
Text inputs can be conditionally shown based on selected actions:
|
||||||
|
|
||||||
@ -158,101 +147,3 @@ relevantExtraInput: [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Publishing Checklist (Nag System)
|
|
||||||
|
|
||||||
The nag system provides automated feedback to project authors during the submission process, helping them improve their projects before they reach moderation. It analyzes project data and provides suggestions, warnings, and requirements.
|
|
||||||
|
|
||||||
### Nags
|
|
||||||
|
|
||||||
A nag represents a specific issue or suggestion for improvement. Each nag has:
|
|
||||||
|
|
||||||
- A unique `id` for identification
|
|
||||||
- A `title` and `description` displayed to the user
|
|
||||||
- A `status` indicating severity: `'required'`, `'warning'`, or `'suggestion'`
|
|
||||||
- A `shouldShow` function that determines when the nag should be displayed
|
|
||||||
- An optional `link` to help users address the issue
|
|
||||||
|
|
||||||
### Internationalization
|
|
||||||
|
|
||||||
Each nag category has a corresponding `.i18n.ts` file containing message definitions:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Example from core.i18n.ts
|
|
||||||
export default defineMessages({
|
|
||||||
addDescriptionTitle: {
|
|
||||||
id: 'nags.add-description.title',
|
|
||||||
defaultMessage: 'Add a description',
|
|
||||||
},
|
|
||||||
addDescriptionDescription: {
|
|
||||||
id: 'nags.add-description.description',
|
|
||||||
defaultMessage:
|
|
||||||
"A description that clearly describes the project's purpose and function is required.",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to use context in the messages, you can do so like this:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(messages.descriptionTooShortDescription, {
|
|
||||||
length: context.project.body?.length || 0,
|
|
||||||
minChars: MIN_DESCRIPTION_CHARS,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nag Context
|
|
||||||
|
|
||||||
The `NagContext` type provides access to:
|
|
||||||
|
|
||||||
- `project`: Current project data
|
|
||||||
- `versions`: Project versions
|
|
||||||
- `tags`: Frontend "tags" (generated state)
|
|
||||||
- `currentRoute`: Current page route
|
|
||||||
- and other data...
|
|
||||||
|
|
||||||
### Adding New Nags
|
|
||||||
|
|
||||||
To add a new nag:
|
|
||||||
|
|
||||||
1. Add the nag definition to the appropriate category file (or make a new category file and add it to `data/nags.ts`)
|
|
||||||
2. Add corresponding i18n messages to the `.i18n.ts` file
|
|
||||||
3. Implement the `shouldShow` logic based on project state
|
|
||||||
4. Add appropriate links to help users resolve the issue
|
|
||||||
5. Run `pnpm run fix` to fix lint issues & generate the root locale index.json file.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In description.ts
|
|
||||||
{
|
|
||||||
id: 'new-nag',
|
|
||||||
title: messages.newNagTitle,
|
|
||||||
description: messages.newNagDescription,
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
// Your validation logic here
|
|
||||||
return someCondition
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: messages.editDescriptionTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In description.i18n.ts
|
|
||||||
newNagTitle: {
|
|
||||||
id: 'nags.new-nag.title',
|
|
||||||
defaultMessage: 'New Nag Title',
|
|
||||||
},
|
|
||||||
newNagDescription: {
|
|
||||||
id: 'nags.new-nag.description',
|
|
||||||
defaultMessage: 'Description of the new nag issue.',
|
|
||||||
```
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import type { Nag } from '../types/nags'
|
|
||||||
import { coreNags } from './nags/core'
|
|
||||||
import { descriptionNags } from './nags/description'
|
|
||||||
import { linksNags } from './nags/links'
|
|
||||||
import { tagsNags } from './nags/tags'
|
|
||||||
|
|
||||||
export default [...coreNags, ...linksNags, ...descriptionNags, ...tagsNags] as Nag[]
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
import { defineMessages } from '@vintl/vintl'
|
|
||||||
|
|
||||||
export default defineMessages({
|
|
||||||
moderatorFeedbackTitle: {
|
|
||||||
id: 'nags.moderator-feedback.title',
|
|
||||||
defaultMessage: 'Review moderator feedback',
|
|
||||||
},
|
|
||||||
moderatorFeedbackDescription: {
|
|
||||||
id: 'nags.moderator-feedback.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Review any feedback from moderators regarding your project before resubmitting.',
|
|
||||||
},
|
|
||||||
moderationTitle: {
|
|
||||||
id: 'nags.moderation.title',
|
|
||||||
defaultMessage: 'Visit moderation thread',
|
|
||||||
},
|
|
||||||
uploadVersionTitle: {
|
|
||||||
id: 'nags.upload-version.title',
|
|
||||||
defaultMessage: 'Upload a version',
|
|
||||||
},
|
|
||||||
uploadVersionDescription: {
|
|
||||||
id: 'nags.upload-version.description',
|
|
||||||
defaultMessage: 'At least one version is required for a project to be submitted for review.',
|
|
||||||
},
|
|
||||||
versionsTitle: {
|
|
||||||
id: 'nags.versions.title',
|
|
||||||
defaultMessage: 'Visit versions page',
|
|
||||||
},
|
|
||||||
addDescriptionTitle: {
|
|
||||||
id: 'nags.add-description.title',
|
|
||||||
defaultMessage: 'Add a description',
|
|
||||||
},
|
|
||||||
addDescriptionDescription: {
|
|
||||||
id: 'nags.add-description.description',
|
|
||||||
defaultMessage:
|
|
||||||
"A description that clearly describes the project's purpose and function is required.",
|
|
||||||
},
|
|
||||||
settingsDescriptionTitle: {
|
|
||||||
id: 'nags.settings.description.title',
|
|
||||||
defaultMessage: 'Visit description settings',
|
|
||||||
},
|
|
||||||
addIconTitle: {
|
|
||||||
id: 'nags.add-icon.title',
|
|
||||||
defaultMessage: 'Add an icon',
|
|
||||||
},
|
|
||||||
addIconDescription: {
|
|
||||||
id: 'nags.add-icon.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
|
|
||||||
},
|
|
||||||
settingsTitle: {
|
|
||||||
id: 'nags.settings.title',
|
|
||||||
defaultMessage: 'Visit general settings',
|
|
||||||
},
|
|
||||||
featureGalleryImageTitle: {
|
|
||||||
id: 'nags.feature-gallery-image.title',
|
|
||||||
defaultMessage: 'Feature a gallery image',
|
|
||||||
},
|
|
||||||
featureGalleryImageDescription: {
|
|
||||||
id: 'nags.feature-gallery-image.description',
|
|
||||||
defaultMessage: 'Featured gallery images may be the first impression of many users.',
|
|
||||||
},
|
|
||||||
galleryTitle: {
|
|
||||||
id: 'nags.gallery.title',
|
|
||||||
defaultMessage: 'Visit gallery page',
|
|
||||||
},
|
|
||||||
selectTagsTitle: {
|
|
||||||
id: 'nags.select-tags.title',
|
|
||||||
defaultMessage: 'Select tags',
|
|
||||||
},
|
|
||||||
selectTagsDescription: {
|
|
||||||
id: 'nags.select-tags.description',
|
|
||||||
defaultMessage: 'Select all tags that apply to your project.',
|
|
||||||
},
|
|
||||||
settingsTagsTitle: {
|
|
||||||
id: 'nags.settings.tags.title',
|
|
||||||
defaultMessage: 'Visit tag settings',
|
|
||||||
},
|
|
||||||
addLinksTitle: {
|
|
||||||
id: 'nags.add-links.title',
|
|
||||||
defaultMessage: 'Add external links',
|
|
||||||
},
|
|
||||||
addLinksDescription: {
|
|
||||||
id: 'nags.add-links.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
|
|
||||||
},
|
|
||||||
settingsLinksTitle: {
|
|
||||||
id: 'nags.settings.links.title',
|
|
||||||
defaultMessage: 'Visit links settings',
|
|
||||||
},
|
|
||||||
selectEnvironmentsTitle: {
|
|
||||||
id: 'nags.select-environments.title',
|
|
||||||
defaultMessage: 'Select supported environments',
|
|
||||||
},
|
|
||||||
selectEnvironmentsDescription: {
|
|
||||||
id: 'nags.select-environments.description',
|
|
||||||
defaultMessage: `Select if the {projectType} functions on the client-side and/or server-side.`,
|
|
||||||
},
|
|
||||||
settingsEnvironmentsTitle: {
|
|
||||||
id: 'nags.settings.environments.title',
|
|
||||||
defaultMessage: 'Visit general settings',
|
|
||||||
},
|
|
||||||
selectLicenseTitle: {
|
|
||||||
id: 'nags.select-license.title',
|
|
||||||
defaultMessage: 'Select license',
|
|
||||||
},
|
|
||||||
selectLicenseDescription: {
|
|
||||||
id: 'nags.select-license.description',
|
|
||||||
defaultMessage: 'Select the license your {projectType} is distributed under.',
|
|
||||||
},
|
|
||||||
settingsLicenseTitle: {
|
|
||||||
id: 'nags.settings.license.title',
|
|
||||||
defaultMessage: 'Visit license settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
|
||||||
import { useVIntl } from '@vintl/vintl'
|
|
||||||
|
|
||||||
import messages from './core.i18n'
|
|
||||||
|
|
||||||
export const coreNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'moderator-feedback',
|
|
||||||
title: messages.moderatorFeedbackTitle,
|
|
||||||
description: messages.moderatorFeedbackDescription,
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
context.tags.rejectedStatuses.includes(context.project.status),
|
|
||||||
link: {
|
|
||||||
path: 'moderation',
|
|
||||||
title: messages.moderationTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-moderation',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'upload-version',
|
|
||||||
title: messages.uploadVersionTitle,
|
|
||||||
description: messages.uploadVersionDescription,
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => context.versions.length < 1,
|
|
||||||
link: {
|
|
||||||
path: 'versions',
|
|
||||||
title: messages.versionsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-versions',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'add-description',
|
|
||||||
title: messages.addDescriptionTitle,
|
|
||||||
description: messages.addDescriptionDescription,
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
context.project.body === '' || context.project.body.startsWith('# Placeholder description'),
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: messages.settingsDescriptionTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'add-icon',
|
|
||||||
title: messages.addIconTitle,
|
|
||||||
description: messages.addIconDescription,
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) => !context.project.icon_url,
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: messages.settingsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'feature-gallery-image',
|
|
||||||
title: messages.featureGalleryImageTitle,
|
|
||||||
description: messages.featureGalleryImageDescription,
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const featuredGalleryImage = context.project.gallery?.find((img) => img.featured)
|
|
||||||
return context.project?.gallery?.length === 0 || !featuredGalleryImage
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'gallery',
|
|
||||||
title: messages.galleryTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'select-tags',
|
|
||||||
title: messages.selectTagsTitle,
|
|
||||||
description: messages.selectTagsDescription,
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
context.project.versions.length > 0 && context.project.categories.length < 1,
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: messages.settingsTagsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'add-links',
|
|
||||||
title: messages.addLinksTitle,
|
|
||||||
description: messages.addLinksDescription,
|
|
||||||
status: 'suggestion',
|
|
||||||
shouldShow: (context: NagContext) =>
|
|
||||||
!(
|
|
||||||
context.project.issues_url ||
|
|
||||||
context.project.source_url ||
|
|
||||||
context.project.wiki_url ||
|
|
||||||
context.project.discord_url ||
|
|
||||||
context.project.donation_urls.length > 0
|
|
||||||
),
|
|
||||||
link: {
|
|
||||||
path: 'settings/links',
|
|
||||||
title: messages.settingsLinksTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'select-environments',
|
|
||||||
title: messages.selectEnvironmentsTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(messages.selectEnvironmentsDescription, {
|
|
||||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const excludedTypes = ['resourcepack', 'plugin', 'shader', 'datapack']
|
|
||||||
return (
|
|
||||||
context.project.versions.length > 0 &&
|
|
||||||
!excludedTypes.includes(context.project.project_type) &&
|
|
||||||
(context.project.client_side === 'unknown' ||
|
|
||||||
context.project.server_side === 'unknown' ||
|
|
||||||
(context.project.client_side === 'unsupported' &&
|
|
||||||
context.project.server_side === 'unsupported'))
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: messages.settingsEnvironmentsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'select-license',
|
|
||||||
title: messages.selectLicenseTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(messages.selectLicenseDescription, {
|
|
||||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
|
|
||||||
link: {
|
|
||||||
path: 'settings/license',
|
|
||||||
title: messages.settingsLicenseTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-license',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import { defineMessages } from '@vintl/vintl'
|
|
||||||
|
|
||||||
export default defineMessages({
|
|
||||||
descriptionTooShortTitle: {
|
|
||||||
id: 'nags.description-too-short.title',
|
|
||||||
defaultMessage: 'Description may be insufficient',
|
|
||||||
},
|
|
||||||
descriptionTooShortDescription: {
|
|
||||||
id: 'nags.description-too-short.description',
|
|
||||||
defaultMessage:
|
|
||||||
"Your description is {length} characters. It's recommended to have at least {minChars} characters to provide users with enough information about your project.",
|
|
||||||
},
|
|
||||||
longHeadersTitle: {
|
|
||||||
id: 'nags.long-headers.title',
|
|
||||||
defaultMessage: 'Headers are too long',
|
|
||||||
},
|
|
||||||
longHeadersDescription: {
|
|
||||||
id: 'nags.long-headers.description',
|
|
||||||
defaultMessage:
|
|
||||||
'{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences.',
|
|
||||||
},
|
|
||||||
summaryTooShortTitle: {
|
|
||||||
id: 'nags.summary-too-short.title',
|
|
||||||
defaultMessage: 'Summary may be insufficient',
|
|
||||||
},
|
|
||||||
summaryTooShortDescription: {
|
|
||||||
id: 'nags.summary-too-short.description',
|
|
||||||
defaultMessage:
|
|
||||||
"Your summary is {length} characters. It's recommended to have at least {minChars} characters to provide users with enough information about your project.",
|
|
||||||
},
|
|
||||||
minecraftTitleClauseTitle: {
|
|
||||||
id: 'nags.minecraft-title-clause.title',
|
|
||||||
defaultMessage: 'Title contains "Minecraft"',
|
|
||||||
},
|
|
||||||
minecraftTitleClauseDescription: {
|
|
||||||
id: 'nags.minecraft-title-clause.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Please remove "Minecraft" from your title. You cannot use "Minecraft" in your title for legal reasons.',
|
|
||||||
},
|
|
||||||
titleContainsTechnicalInfoTitle: {
|
|
||||||
id: 'nags.title-contains-technical-info.title',
|
|
||||||
defaultMessage: 'Title contains loader or version info',
|
|
||||||
},
|
|
||||||
titleContainsTechnicalInfoDescription: {
|
|
||||||
id: 'nags.title-contains-technical-info.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Removing these helps keep titles clean and makes your project easier to find. Version and loader information is automatically displayed alongside your project.',
|
|
||||||
},
|
|
||||||
summarySameAsTitleTitle: {
|
|
||||||
id: 'nags.summary-same-as-title.title',
|
|
||||||
defaultMessage: 'Summary is project name',
|
|
||||||
},
|
|
||||||
summarySameAsTitleDescription: {
|
|
||||||
id: 'nags.summary-same-as-title.description',
|
|
||||||
defaultMessage:
|
|
||||||
"Your summary is the same as your project name. Please change it. It's recommended to have a unique summary to provide more context about your project.",
|
|
||||||
},
|
|
||||||
imageHeavyDescriptionTitle: {
|
|
||||||
id: 'nags.image-heavy-description.title',
|
|
||||||
defaultMessage: 'Description is mostly images',
|
|
||||||
},
|
|
||||||
imageHeavyDescriptionDescription: {
|
|
||||||
id: 'nags.image-heavy-description.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Please add more descriptive text to help users understand your project, especially those using screen readers or with slow internet connections.',
|
|
||||||
},
|
|
||||||
missingAltTextTitle: {
|
|
||||||
id: 'nags.missing-alt-text.title',
|
|
||||||
defaultMessage: 'Images missing alt text',
|
|
||||||
},
|
|
||||||
missingAltTextDescription: {
|
|
||||||
id: 'nags.missing-alt-text.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users.',
|
|
||||||
},
|
|
||||||
editDescriptionTitle: {
|
|
||||||
id: 'nags.edit-description.title',
|
|
||||||
defaultMessage: 'Edit description',
|
|
||||||
},
|
|
||||||
editSummaryTitle: {
|
|
||||||
id: 'nags.edit-summary.title',
|
|
||||||
defaultMessage: 'Edit summary',
|
|
||||||
},
|
|
||||||
editTitleTitle: {
|
|
||||||
id: 'nags.edit-title.title',
|
|
||||||
defaultMessage: 'Edit title',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { useVIntl } from '@vintl/vintl'
|
|
||||||
|
|
||||||
import messages from './description.i18n'
|
|
||||||
|
|
||||||
export const MIN_DESCRIPTION_CHARS = 500
|
|
||||||
export const MAX_HEADER_LENGTH = 100
|
|
||||||
export const MIN_SUMMARY_CHARS = 125
|
|
||||||
|
|
||||||
function analyzeHeaderLength(markdown: string): { hasLongHeaders: boolean; longHeaders: string[] } {
|
|
||||||
if (!markdown) return { hasLongHeaders: false, longHeaders: [] }
|
|
||||||
|
|
||||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
|
||||||
|
|
||||||
const headerRegex = /^(#{1,3})\s+(.+)$/gm
|
|
||||||
const headers = [...withoutCodeBlocks.matchAll(headerRegex)]
|
|
||||||
|
|
||||||
const longHeaders: string[] = []
|
|
||||||
|
|
||||||
headers.forEach((match) => {
|
|
||||||
const headerText = match[2].trim()
|
|
||||||
const sentenceEnders = /[.!?]+/g
|
|
||||||
const sentences = headerText.split(sentenceEnders).filter((s) => s.trim().length > 0)
|
|
||||||
|
|
||||||
const hasSentenceEnders = sentenceEnders.test(headerText)
|
|
||||||
const isVeryLong = headerText.length > MAX_HEADER_LENGTH
|
|
||||||
const hasMultipleSentences = sentences.length > 1
|
|
||||||
|
|
||||||
if (hasSentenceEnders || isVeryLong || hasMultipleSentences) {
|
|
||||||
longHeaders.push(headerText)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasLongHeaders: longHeaders.length > 0,
|
|
||||||
longHeaders,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function analyzeImageContent(markdown: string): { imageHeavy: boolean; hasEmptyAltText: boolean } {
|
|
||||||
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
|
||||||
|
|
||||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
|
||||||
|
|
||||||
const imageRegex = /!\[([^\]]*)\]\([^)]+\)/g
|
|
||||||
const images = [...withoutCodeBlocks.matchAll(imageRegex)]
|
|
||||||
|
|
||||||
const htmlImageRegex = /<img[^>]*>/gi
|
|
||||||
const htmlImages = [...withoutCodeBlocks.matchAll(htmlImageRegex)]
|
|
||||||
|
|
||||||
const totalImages = images.length + htmlImages.length
|
|
||||||
if (totalImages === 0) return { imageHeavy: false, hasEmptyAltText: false }
|
|
||||||
|
|
||||||
const textWithoutImages = withoutCodeBlocks
|
|
||||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
|
|
||||||
.replace(/<img[^>]*>/gi, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
const textLength = textWithoutImages.length
|
|
||||||
const imageHeavy = textLength < 100 || (totalImages >= 3 && textLength < 200)
|
|
||||||
|
|
||||||
const hasEmptyAltText =
|
|
||||||
images.some((match) => !match[1]?.trim()) ||
|
|
||||||
htmlImages.some((match) => {
|
|
||||||
const altMatch = match[0].match(/alt\s*=\s*["']([^"']*)["']/i)
|
|
||||||
return !altMatch || !altMatch[1]?.trim()
|
|
||||||
})
|
|
||||||
|
|
||||||
return { imageHeavy, hasEmptyAltText }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const descriptionNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'description-too-short',
|
|
||||||
title: messages.descriptionTooShortTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(messages.descriptionTooShortDescription, {
|
|
||||||
length: context.project.body?.length || 0,
|
|
||||||
minChars: MIN_DESCRIPTION_CHARS,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const bodyLength = context.project.body?.trim()?.length || 0
|
|
||||||
return bodyLength < MIN_DESCRIPTION_CHARS && bodyLength !== 0
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: messages.editDescriptionTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'long-headers',
|
|
||||||
title: messages.longHeadersTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const { longHeaders } = analyzeHeaderLength(context.project.body || '')
|
|
||||||
const count = longHeaders.length
|
|
||||||
|
|
||||||
return formatMessage(messages.longHeadersDescription, {
|
|
||||||
count,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const { hasLongHeaders } = analyzeHeaderLength(context.project.body || '')
|
|
||||||
return hasLongHeaders
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: messages.editDescriptionTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'summary-too-short',
|
|
||||||
title: messages.summaryTooShortTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(messages.summaryTooShortDescription, {
|
|
||||||
length: context.project.description?.length || 0,
|
|
||||||
minChars: MIN_SUMMARY_CHARS,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const summaryLength = context.project.description?.trim()?.length || 0
|
|
||||||
return summaryLength < MIN_SUMMARY_CHARS && summaryLength !== 0
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: messages.editSummaryTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'minecraft-title-clause',
|
|
||||||
title: messages.minecraftTitleClauseTitle,
|
|
||||||
description: messages.minecraftTitleClauseDescription,
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const title = context.project.title?.toLowerCase() || ''
|
|
||||||
const wordsInTitle = title.split(' ').filter((word) => word.length > 0)
|
|
||||||
return title.includes('minecraft') && title.length > 0 && wordsInTitle.length <= 3
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: messages.editTitleTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'title-contains-technical-info',
|
|
||||||
title: messages.titleContainsTechnicalInfoTitle,
|
|
||||||
description: messages.titleContainsTechnicalInfoDescription,
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const title = context.project.title?.toLowerCase() || ''
|
|
||||||
if (!title) return false
|
|
||||||
|
|
||||||
const loaderNames =
|
|
||||||
context.tags.loaders?.map((loader: { name: string }) => loader.name?.toLowerCase()) || []
|
|
||||||
const hasLoader = loaderNames.some((loader) => loader && title.includes(loader.toLowerCase()))
|
|
||||||
const versionPatterns = [/\b1\.\d+(\.\d+)?\b/]
|
|
||||||
const hasVersionPattern = versionPatterns.some((pattern) => pattern.test(title))
|
|
||||||
|
|
||||||
return hasLoader || hasVersionPattern
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: messages.editTitleTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'summary-same-as-title',
|
|
||||||
title: messages.summarySameAsTitleTitle,
|
|
||||||
description: messages.summarySameAsTitleDescription,
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const title = context.project.title?.trim() || ''
|
|
||||||
const summary = context.project.description?.trim() || ''
|
|
||||||
return title === summary && title.length > 0 && summary.length > 0
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: messages.editSummaryTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'image-heavy-description',
|
|
||||||
title: messages.imageHeavyDescriptionTitle,
|
|
||||||
description: messages.imageHeavyDescriptionDescription,
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const { imageHeavy } = analyzeImageContent(context.project.body || '')
|
|
||||||
return imageHeavy
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: messages.editDescriptionTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'missing-alt-text',
|
|
||||||
title: messages.missingAltTextTitle,
|
|
||||||
description: messages.missingAltTextDescription,
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const { hasEmptyAltText } = analyzeImageContent(context.project.body || '')
|
|
||||||
return hasEmptyAltText
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/description',
|
|
||||||
title: messages.editDescriptionTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from './core'
|
|
||||||
export * from './links'
|
|
||||||
export * from './description'
|
|
||||||
export * from './tags'
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { defineMessages } from '@vintl/vintl'
|
|
||||||
|
|
||||||
export default defineMessages({
|
|
||||||
verifyExternalLinksTitle: {
|
|
||||||
id: 'nags.verify-external-links.title',
|
|
||||||
defaultMessage: 'Verify external links',
|
|
||||||
},
|
|
||||||
verifyExternalLinksDescription: {
|
|
||||||
id: 'nags.verify-external-links.description',
|
|
||||||
defaultMessage:
|
|
||||||
"Some of your external links may be using domains that aren't recognized as common for their link type.",
|
|
||||||
},
|
|
||||||
invalidLicenseUrlTitle: {
|
|
||||||
id: 'nags.invalid-license-url.title',
|
|
||||||
defaultMessage: 'Invalid license URL',
|
|
||||||
},
|
|
||||||
invalidLicenseUrlDescriptionDefault: {
|
|
||||||
id: 'nags.invalid-license-url.description.default',
|
|
||||||
defaultMessage: 'License URL is invalid.',
|
|
||||||
},
|
|
||||||
invalidLicenseUrlDescriptionDomain: {
|
|
||||||
id: 'nags.invalid-license-url.description.domain',
|
|
||||||
defaultMessage:
|
|
||||||
'Your license URL points to {domain}, which is not appropriate for license information. License URLs should link to the actual license text or legal documentation, not social media, gaming platforms etc.',
|
|
||||||
},
|
|
||||||
invalidLicenseUrlDescriptionMalformed: {
|
|
||||||
id: 'nags.invalid-license-url.description.malformed',
|
|
||||||
defaultMessage:
|
|
||||||
'Your license URL appears to be malformed. Please provide a valid URL to your license text.',
|
|
||||||
},
|
|
||||||
gplLicenseSourceRequiredTitle: {
|
|
||||||
id: 'nags.gpl-license-source-required.title',
|
|
||||||
defaultMessage: 'GPL license requires source',
|
|
||||||
},
|
|
||||||
gplLicenseSourceRequiredDescription: {
|
|
||||||
id: 'nags.gpl-license-source-required.description',
|
|
||||||
defaultMessage:
|
|
||||||
'Your {projectType} uses a GPL license which requires source code to be available. Please provide a source code link or consider using a different license.',
|
|
||||||
},
|
|
||||||
visitLinksSettingsTitle: {
|
|
||||||
id: 'nags.visit-links-settings.title',
|
|
||||||
defaultMessage: 'Visit links settings',
|
|
||||||
},
|
|
||||||
editLicenseTitle: {
|
|
||||||
id: 'nags.edit-license.title',
|
|
||||||
defaultMessage: 'Edit license',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
|
||||||
import { useVIntl } from '@vintl/vintl'
|
|
||||||
|
|
||||||
import messages from './links.i18n'
|
|
||||||
|
|
||||||
export const commonLinkDomains = {
|
|
||||||
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
|
||||||
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org'],
|
|
||||||
discord: ['discord.gg', 'discord.com'],
|
|
||||||
licenseBlocklist: [
|
|
||||||
'youtube.com',
|
|
||||||
'youtu.be',
|
|
||||||
'modrinth.com',
|
|
||||||
'curseforge.com',
|
|
||||||
'twitter.com',
|
|
||||||
'x.com',
|
|
||||||
'discord.gg',
|
|
||||||
'discord.com',
|
|
||||||
'instagram.com',
|
|
||||||
'facebook.com',
|
|
||||||
'tiktok.com',
|
|
||||||
'reddit.com',
|
|
||||||
'twitch.tv',
|
|
||||||
'patreon.com',
|
|
||||||
'ko-fi.com',
|
|
||||||
'paypal.com',
|
|
||||||
'buymeacoffee.com',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCommonUrl(url: string | undefined, commonDomains: string[]): boolean {
|
|
||||||
if (!url) return false
|
|
||||||
try {
|
|
||||||
const domain = new URL(url).hostname.toLowerCase()
|
|
||||||
return commonDomains.some((allowed) => domain.includes(allowed))
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isUncommonLicenseUrl(url: string | undefined, domains: string[]): boolean {
|
|
||||||
if (!url) return false
|
|
||||||
try {
|
|
||||||
const domain = new URL(url).hostname.toLowerCase()
|
|
||||||
return domains.some((uncommonDomain) => domain.includes(uncommonDomain))
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const linksNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'verify-external-links',
|
|
||||||
title: messages.verifyExternalLinksTitle,
|
|
||||||
description: messages.verifyExternalLinksDescription,
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
return (
|
|
||||||
!isCommonUrl(context.project.source_url, commonLinkDomains.source) ||
|
|
||||||
!isCommonUrl(context.project.issues_url, commonLinkDomains.issues) ||
|
|
||||||
!isCommonUrl(context.project.discord_url, commonLinkDomains.discord)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/links',
|
|
||||||
title: messages.visitLinksSettingsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'invalid-license-url',
|
|
||||||
title: messages.invalidLicenseUrlTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const licenseUrl = context.project.license.url
|
|
||||||
|
|
||||||
if (!licenseUrl) {
|
|
||||||
return formatMessage(messages.invalidLicenseUrlDescriptionDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const domain = new URL(licenseUrl).hostname.toLowerCase()
|
|
||||||
return formatMessage(messages.invalidLicenseUrlDescriptionDomain, { domain })
|
|
||||||
} catch {
|
|
||||||
return formatMessage(messages.invalidLicenseUrlDescriptionMalformed)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const licenseUrl = context.project.license.url
|
|
||||||
if (!licenseUrl) return false
|
|
||||||
|
|
||||||
const isBlocklisted = isUncommonLicenseUrl(licenseUrl, commonLinkDomains.licenseBlocklist)
|
|
||||||
|
|
||||||
try {
|
|
||||||
new URL(licenseUrl)
|
|
||||||
return isBlocklisted
|
|
||||||
} catch {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings',
|
|
||||||
title: messages.editLicenseTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpl-license-source-required',
|
|
||||||
title: messages.gplLicenseSourceRequiredTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
|
|
||||||
return formatMessage(messages.gplLicenseSourceRequiredDescription, {
|
|
||||||
projectType: formatProjectType(context.project.project_type).toLowerCase(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const gplLicenses = [
|
|
||||||
'GPL-2.0',
|
|
||||||
'GPL-2.0+',
|
|
||||||
'GPL-2.0-only',
|
|
||||||
'GPL-2.0-or-later',
|
|
||||||
'GPL-3.0',
|
|
||||||
'GPL-3.0+',
|
|
||||||
'GPL-3.0-only',
|
|
||||||
'GPL-3.0-or-later',
|
|
||||||
'LGPL-2.1',
|
|
||||||
'LGPL-2.1+',
|
|
||||||
'LGPL-2.1-only',
|
|
||||||
'LGPL-2.1-or-later',
|
|
||||||
'LGPL-3.0',
|
|
||||||
'LGPL-3.0+',
|
|
||||||
'LGPL-3.0-only',
|
|
||||||
'LGPL-3.0-or-later',
|
|
||||||
'AGPL-3.0',
|
|
||||||
'AGPL-3.0+',
|
|
||||||
'AGPL-3.0-only',
|
|
||||||
'AGPL-3.0-or-later',
|
|
||||||
]
|
|
||||||
|
|
||||||
const isGplLicense = gplLicenses.includes(context.project.license.id)
|
|
||||||
const hasSourceUrl = !!context.project.source_url
|
|
||||||
|
|
||||||
return isGplLicense && !hasSourceUrl
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/links',
|
|
||||||
title: messages.visitLinksSettingsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { defineMessages } from '@vintl/vintl'
|
|
||||||
|
|
||||||
export default defineMessages({
|
|
||||||
tooManyTagsTitle: {
|
|
||||||
id: 'nags.too-many-tags.title',
|
|
||||||
defaultMessage: 'Too many tags selected',
|
|
||||||
},
|
|
||||||
tooManyTagsDescription: {
|
|
||||||
id: 'nags.too-many-tags.description',
|
|
||||||
defaultMessage:
|
|
||||||
"You've selected {tagCount} tags. Consider reducing to 5 or fewer to keep your project focused and easier to discover.",
|
|
||||||
},
|
|
||||||
multipleResolutionTagsTitle: {
|
|
||||||
id: 'nags.multiple-resolution-tags.title',
|
|
||||||
defaultMessage: 'Multiple resolution tags selected',
|
|
||||||
},
|
|
||||||
multipleResolutionTagsDescription: {
|
|
||||||
id: 'nags.multiple-resolution-tags.description',
|
|
||||||
defaultMessage:
|
|
||||||
"You've selected {count} resolution tags ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution.",
|
|
||||||
},
|
|
||||||
allTagsSelectedTitle: {
|
|
||||||
id: 'nags.all-tags-selected.title',
|
|
||||||
defaultMessage: 'All tags selected',
|
|
||||||
},
|
|
||||||
allTagsSelectedDescription: {
|
|
||||||
id: 'nags.all-tags-selected.description',
|
|
||||||
defaultMessage:
|
|
||||||
"You've selected all {totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that truly apply to your project.",
|
|
||||||
},
|
|
||||||
editTagsTitle: {
|
|
||||||
id: 'nags.edit-tags.title',
|
|
||||||
defaultMessage: 'Edit tags',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import type { Project } from '@modrinth/utils'
|
|
||||||
import type { Nag, NagContext } from '../../types/nags'
|
|
||||||
import { useVIntl } from '@vintl/vintl'
|
|
||||||
|
|
||||||
import messages from './tags.i18n'
|
|
||||||
|
|
||||||
function getCategories(
|
|
||||||
project: Project & { actualProjectType: string },
|
|
||||||
tags: {
|
|
||||||
categories?: {
|
|
||||||
project_type: string
|
|
||||||
}[]
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
tags.categories?.filter(
|
|
||||||
(category: { project_type: string }) => category.project_type === project.actualProjectType,
|
|
||||||
) ?? []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tagsNags: Nag[] = [
|
|
||||||
{
|
|
||||||
id: 'too-many-tags',
|
|
||||||
title: messages.tooManyTagsTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const tagCount =
|
|
||||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
|
||||||
|
|
||||||
return formatMessage(messages.tooManyTagsDescription, {
|
|
||||||
tagCount,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const tagCount =
|
|
||||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
|
||||||
return tagCount > 5
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: messages.editTagsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'multiple-resolution-tags',
|
|
||||||
title: messages.multipleResolutionTagsTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const resolutionTags = context.project.categories.filter((tag: string) =>
|
|
||||||
['16x', '32x', '48x', '64x', '128x', '256x', '512x', '1024x'].includes(tag),
|
|
||||||
)
|
|
||||||
|
|
||||||
return formatMessage(messages.multipleResolutionTagsDescription, {
|
|
||||||
count: resolutionTags.length,
|
|
||||||
tags: resolutionTags.join(', '),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'warning',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
if (context.project.project_type !== 'resourcepack') return false
|
|
||||||
|
|
||||||
const resolutionTags = context.project.categories.filter((tag: string) =>
|
|
||||||
['16x', '32x', '48x', '64x', '128x', '256x', '512x', '1024x'].includes(tag),
|
|
||||||
)
|
|
||||||
return resolutionTags.length > 1
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: messages.editTagsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'all-tags-selected',
|
|
||||||
title: messages.allTagsSelectedTitle,
|
|
||||||
description: (context: NagContext) => {
|
|
||||||
const { formatMessage } = useVIntl()
|
|
||||||
const categoriesForProjectType = getCategories(
|
|
||||||
context.project as Project & { actualProjectType: string },
|
|
||||||
context.tags,
|
|
||||||
)
|
|
||||||
const totalAvailableTags = categoriesForProjectType.length
|
|
||||||
|
|
||||||
return formatMessage(messages.allTagsSelectedDescription, {
|
|
||||||
totalAvailableTags,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
status: 'required',
|
|
||||||
shouldShow: (context: NagContext) => {
|
|
||||||
const categoriesForProjectType = getCategories(
|
|
||||||
context.project as Project & { actualProjectType: string },
|
|
||||||
context.tags,
|
|
||||||
)
|
|
||||||
const totalSelectedTags =
|
|
||||||
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
|
||||||
return totalSelectedTags === categoriesForProjectType.length
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
path: 'settings/tags',
|
|
||||||
title: messages.editTagsTitle,
|
|
||||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@ -2,10 +2,7 @@ export * from './types/actions'
|
|||||||
export * from './types/messages'
|
export * from './types/messages'
|
||||||
export * from './types/stage'
|
export * from './types/stage'
|
||||||
export * from './types/keybinds'
|
export * from './types/keybinds'
|
||||||
export * from './types/nags'
|
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
|
|
||||||
export * from './data/nags/index'
|
|
||||||
export { default as checklist } from './data/checklist'
|
export { default as checklist } from './data/checklist'
|
||||||
export { default as keybinds } from './data/keybinds'
|
export { default as keybinds } from './data/keybinds'
|
||||||
export { default as nags } from './data/nags'
|
|
||||||
|
|||||||
@ -1,191 +0,0 @@
|
|||||||
{
|
|
||||||
"nags.add-description.description": {
|
|
||||||
"defaultMessage": "A description that clearly describes the project's purpose and function is required."
|
|
||||||
},
|
|
||||||
"nags.add-description.title": {
|
|
||||||
"defaultMessage": "Add a description"
|
|
||||||
},
|
|
||||||
"nags.add-icon.description": {
|
|
||||||
"defaultMessage": "Your project should have a nice-looking icon to uniquely identify your project at a glance."
|
|
||||||
},
|
|
||||||
"nags.add-icon.title": {
|
|
||||||
"defaultMessage": "Add an icon"
|
|
||||||
},
|
|
||||||
"nags.add-links.description": {
|
|
||||||
"defaultMessage": "Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite."
|
|
||||||
},
|
|
||||||
"nags.add-links.title": {
|
|
||||||
"defaultMessage": "Add external links"
|
|
||||||
},
|
|
||||||
"nags.all-tags-selected.description": {
|
|
||||||
"defaultMessage": "You've selected all {totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that truly apply to your project."
|
|
||||||
},
|
|
||||||
"nags.all-tags-selected.title": {
|
|
||||||
"defaultMessage": "All tags selected"
|
|
||||||
},
|
|
||||||
"nags.description-too-short.description": {
|
|
||||||
"defaultMessage": "Your description is {length} characters. It's recommended to have at least {minChars} characters to provide users with enough information about your project."
|
|
||||||
},
|
|
||||||
"nags.description-too-short.title": {
|
|
||||||
"defaultMessage": "Description may be insufficient"
|
|
||||||
},
|
|
||||||
"nags.edit-description.title": {
|
|
||||||
"defaultMessage": "Edit description"
|
|
||||||
},
|
|
||||||
"nags.edit-license.title": {
|
|
||||||
"defaultMessage": "Edit license"
|
|
||||||
},
|
|
||||||
"nags.edit-summary.title": {
|
|
||||||
"defaultMessage": "Edit summary"
|
|
||||||
},
|
|
||||||
"nags.edit-tags.title": {
|
|
||||||
"defaultMessage": "Edit tags"
|
|
||||||
},
|
|
||||||
"nags.edit-title.title": {
|
|
||||||
"defaultMessage": "Edit title"
|
|
||||||
},
|
|
||||||
"nags.feature-gallery-image.description": {
|
|
||||||
"defaultMessage": "Featured gallery images may be the first impression of many users."
|
|
||||||
},
|
|
||||||
"nags.feature-gallery-image.title": {
|
|
||||||
"defaultMessage": "Feature a gallery image"
|
|
||||||
},
|
|
||||||
"nags.gallery.title": {
|
|
||||||
"defaultMessage": "Visit gallery page"
|
|
||||||
},
|
|
||||||
"nags.gpl-license-source-required.description": {
|
|
||||||
"defaultMessage": "Your {projectType} uses a GPL license which requires source code to be available. Please provide a source code link or consider using a different license."
|
|
||||||
},
|
|
||||||
"nags.gpl-license-source-required.title": {
|
|
||||||
"defaultMessage": "GPL license requires source"
|
|
||||||
},
|
|
||||||
"nags.image-heavy-description.description": {
|
|
||||||
"defaultMessage": "Please add more descriptive text to help users understand your project, especially those using screen readers or with slow internet connections."
|
|
||||||
},
|
|
||||||
"nags.image-heavy-description.title": {
|
|
||||||
"defaultMessage": "Description is mostly images"
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.description.default": {
|
|
||||||
"defaultMessage": "License URL is invalid."
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.description.domain": {
|
|
||||||
"defaultMessage": "Your license URL points to {domain}, which is not appropriate for license information. License URLs should link to the actual license text or legal documentation, not social media, gaming platforms etc."
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.description.malformed": {
|
|
||||||
"defaultMessage": "Your license URL appears to be malformed. Please provide a valid URL to your license text."
|
|
||||||
},
|
|
||||||
"nags.invalid-license-url.title": {
|
|
||||||
"defaultMessage": "Invalid license URL"
|
|
||||||
},
|
|
||||||
"nags.long-headers.description": {
|
|
||||||
"defaultMessage": "{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences."
|
|
||||||
},
|
|
||||||
"nags.long-headers.title": {
|
|
||||||
"defaultMessage": "Headers are too long"
|
|
||||||
},
|
|
||||||
"nags.minecraft-title-clause.description": {
|
|
||||||
"defaultMessage": "Please remove \"Minecraft\" from your title. You cannot use \"Minecraft\" in your title for legal reasons."
|
|
||||||
},
|
|
||||||
"nags.minecraft-title-clause.title": {
|
|
||||||
"defaultMessage": "Title contains \"Minecraft\""
|
|
||||||
},
|
|
||||||
"nags.missing-alt-text.description": {
|
|
||||||
"defaultMessage": "Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users."
|
|
||||||
},
|
|
||||||
"nags.missing-alt-text.title": {
|
|
||||||
"defaultMessage": "Images missing alt text"
|
|
||||||
},
|
|
||||||
"nags.moderation.title": {
|
|
||||||
"defaultMessage": "Visit moderation thread"
|
|
||||||
},
|
|
||||||
"nags.moderator-feedback.description": {
|
|
||||||
"defaultMessage": "Review any feedback from moderators regarding your project before resubmitting."
|
|
||||||
},
|
|
||||||
"nags.moderator-feedback.title": {
|
|
||||||
"defaultMessage": "Review moderator feedback"
|
|
||||||
},
|
|
||||||
"nags.multiple-resolution-tags.description": {
|
|
||||||
"defaultMessage": "You've selected {count} resolution tags ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution."
|
|
||||||
},
|
|
||||||
"nags.multiple-resolution-tags.title": {
|
|
||||||
"defaultMessage": "Multiple resolution tags selected"
|
|
||||||
},
|
|
||||||
"nags.select-environments.description": {
|
|
||||||
"defaultMessage": "Select if the {projectType} functions on the client-side and/or server-side."
|
|
||||||
},
|
|
||||||
"nags.select-environments.title": {
|
|
||||||
"defaultMessage": "Select supported environments"
|
|
||||||
},
|
|
||||||
"nags.select-license.description": {
|
|
||||||
"defaultMessage": "Select the license your {projectType} is distributed under."
|
|
||||||
},
|
|
||||||
"nags.select-license.title": {
|
|
||||||
"defaultMessage": "Select license"
|
|
||||||
},
|
|
||||||
"nags.select-tags.description": {
|
|
||||||
"defaultMessage": "Select all tags that apply to your project."
|
|
||||||
},
|
|
||||||
"nags.select-tags.title": {
|
|
||||||
"defaultMessage": "Select tags"
|
|
||||||
},
|
|
||||||
"nags.settings.description.title": {
|
|
||||||
"defaultMessage": "Visit description settings"
|
|
||||||
},
|
|
||||||
"nags.settings.environments.title": {
|
|
||||||
"defaultMessage": "Visit general settings"
|
|
||||||
},
|
|
||||||
"nags.settings.license.title": {
|
|
||||||
"defaultMessage": "Visit license settings"
|
|
||||||
},
|
|
||||||
"nags.settings.links.title": {
|
|
||||||
"defaultMessage": "Visit links settings"
|
|
||||||
},
|
|
||||||
"nags.settings.tags.title": {
|
|
||||||
"defaultMessage": "Visit tag settings"
|
|
||||||
},
|
|
||||||
"nags.settings.title": {
|
|
||||||
"defaultMessage": "Visit general settings"
|
|
||||||
},
|
|
||||||
"nags.summary-same-as-title.description": {
|
|
||||||
"defaultMessage": "Your summary is the same as your project name. Please change it. It's recommended to have a unique summary to provide more context about your project."
|
|
||||||
},
|
|
||||||
"nags.summary-same-as-title.title": {
|
|
||||||
"defaultMessage": "Summary is project name"
|
|
||||||
},
|
|
||||||
"nags.summary-too-short.description": {
|
|
||||||
"defaultMessage": "Your summary is {length} characters. It's recommended to have at least {minChars} characters to provide users with enough information about your project."
|
|
||||||
},
|
|
||||||
"nags.summary-too-short.title": {
|
|
||||||
"defaultMessage": "Summary may be insufficient"
|
|
||||||
},
|
|
||||||
"nags.title-contains-technical-info.description": {
|
|
||||||
"defaultMessage": "Removing these helps keep titles clean and makes your project easier to find. Version and loader information is automatically displayed alongside your project."
|
|
||||||
},
|
|
||||||
"nags.title-contains-technical-info.title": {
|
|
||||||
"defaultMessage": "Title contains loader or version info"
|
|
||||||
},
|
|
||||||
"nags.too-many-tags.description": {
|
|
||||||
"defaultMessage": "You've selected {tagCount} tags. Consider reducing to 5 or fewer to keep your project focused and easier to discover."
|
|
||||||
},
|
|
||||||
"nags.too-many-tags.title": {
|
|
||||||
"defaultMessage": "Too many tags selected"
|
|
||||||
},
|
|
||||||
"nags.upload-version.description": {
|
|
||||||
"defaultMessage": "At least one version is required for a project to be submitted for review."
|
|
||||||
},
|
|
||||||
"nags.upload-version.title": {
|
|
||||||
"defaultMessage": "Upload a version"
|
|
||||||
},
|
|
||||||
"nags.verify-external-links.description": {
|
|
||||||
"defaultMessage": "Some of your external links may be using domains that aren't recognized as common for their link type."
|
|
||||||
},
|
|
||||||
"nags.verify-external-links.title": {
|
|
||||||
"defaultMessage": "Verify external links"
|
|
||||||
},
|
|
||||||
"nags.versions.title": {
|
|
||||||
"defaultMessage": "Visit versions page"
|
|
||||||
},
|
|
||||||
"nags.visit-links-settings.title": {
|
|
||||||
"defaultMessage": "Visit links settings"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,17 +6,14 @@
|
|||||||
"types": "./index.d.ts",
|
"types": "./index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write . && pnpm run intl:extract",
|
"fix": "eslint . --fix && prettier --write ."
|
||||||
"intl:extract": "formatjs extract \"**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore \"node_modules/**/*\" --out-file locales/en-US/index.json --preserve-whitespace"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/assets": "workspace:*",
|
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
|
"@modrinth/assets": "workspace:*",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^6.2.12",
|
|
||||||
"@vintl/vintl": "^4.4.1",
|
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-custom": "workspace:*",
|
"eslint-config-custom": "workspace:*",
|
||||||
"tsconfig": "workspace:*"
|
"tsconfig": "workspace:*"
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
import type { Project, User, Version } from '@modrinth/utils'
|
|
||||||
import type { MessageDescriptor } from '@vintl/vintl'
|
|
||||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type which represents the status type of a nag.
|
|
||||||
*
|
|
||||||
* - `required` indicates that the nag must be addressed.
|
|
||||||
* - `warning` indicates that the nag is important but not critical, and can be ignored. It is often used for issues that should be resolved but do not block project submission.
|
|
||||||
* - `suggestion` indicates that the nag is a recommendation and can be ignored.
|
|
||||||
*/
|
|
||||||
export type NagStatus = 'required' | 'warning' | 'suggestion' | 'special-submit-action'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing the context in which a nag is displayed.
|
|
||||||
* It includes the project, versions, current member, all members, and the current route.
|
|
||||||
* This context is used to determine whether a nag or it's link should be shown and how it should be presented.
|
|
||||||
*/
|
|
||||||
export interface NagContext {
|
|
||||||
/**
|
|
||||||
* The project associated with the nag.
|
|
||||||
*/
|
|
||||||
project: Project
|
|
||||||
/**
|
|
||||||
* The versions associated with the project.
|
|
||||||
*/
|
|
||||||
versions: Version[]
|
|
||||||
/**
|
|
||||||
* The current project member viewing the nag.
|
|
||||||
*/
|
|
||||||
currentMember: User
|
|
||||||
/**
|
|
||||||
* The current route in the application.
|
|
||||||
*/
|
|
||||||
currentRoute: string
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
tags: any
|
|
||||||
submitProject: (...any: any) => any
|
|
||||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing a nag's link.
|
|
||||||
*/
|
|
||||||
export interface NagLink {
|
|
||||||
/**
|
|
||||||
* A relative path to the nag's link, e.g. '/settings'.
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
/**
|
|
||||||
* The text to display for the nag's link.
|
|
||||||
*/
|
|
||||||
title: MessageDescriptor | string
|
|
||||||
/**
|
|
||||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
|
||||||
*/
|
|
||||||
shouldShow?: (context: NagContext) => boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing a nag.
|
|
||||||
*/
|
|
||||||
export interface Nag {
|
|
||||||
/**
|
|
||||||
* A unique identifier for the nag.
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
/**
|
|
||||||
* The title of the nag.
|
|
||||||
*/
|
|
||||||
title: MessageDescriptor | string
|
|
||||||
/**
|
|
||||||
* A function that returns the description of the nag.
|
|
||||||
* It can accept a context to provide dynamic descriptions.
|
|
||||||
*/
|
|
||||||
description: MessageDescriptor | ((context: NagContext) => string)
|
|
||||||
/**
|
|
||||||
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
|
|
||||||
*/
|
|
||||||
status: NagStatus
|
|
||||||
/**
|
|
||||||
* An optional icon for the nag, usually from `@modrinth/assets`.
|
|
||||||
* If not specified it will use the default icon associated with the nag status.
|
|
||||||
*/
|
|
||||||
icon?: FunctionalComponent<SVGAttributes>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function that determines whether the nag should be shown based on the context.
|
|
||||||
*/
|
|
||||||
shouldShow: (context: NagContext) => boolean
|
|
||||||
/**
|
|
||||||
* An optional link associated with the nag.
|
|
||||||
* If provided, it should be displayed alongside the nag.
|
|
||||||
*/
|
|
||||||
link?: NagLink
|
|
||||||
}
|
|
||||||
@ -10,6 +10,13 @@ export type VersionEntry = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VERSIONS: VersionEntry[] = [
|
const VERSIONS: VersionEntry[] = [
|
||||||
|
{
|
||||||
|
date: `2025-07-19T15:20:00-07:00`,
|
||||||
|
product: 'web',
|
||||||
|
body: `### Improvements
|
||||||
|
- Removed Tumblr icon from footer as we no longer use it.
|
||||||
|
- Reverted changes to publishing checklist since they need more work.`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
date: `2025-07-16T12:40:00-07:00`,
|
date: `2025-07-16T12:40:00-07:00`,
|
||||||
product: 'web',
|
product: 'web',
|
||||||
|
|||||||
178
pnpm-lock.yaml
generated
178
pnpm-lock.yaml
generated
@ -473,12 +473,6 @@ importers:
|
|||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13(typescript@5.8.3)
|
version: 3.5.13(typescript@5.8.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formatjs/cli':
|
|
||||||
specifier: ^6.2.12
|
|
||||||
version: 6.2.12(@vue/compiler-core@3.5.13)(vue@3.5.13(typescript@5.8.3))
|
|
||||||
'@vintl/vintl':
|
|
||||||
specifier: ^4.4.1
|
|
||||||
version: 4.4.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.0
|
specifier: ^8.57.0
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
@ -587,7 +581,7 @@ importers:
|
|||||||
version: 7.3.1
|
version: 7.3.1
|
||||||
'@vintl/unplugin':
|
'@vintl/unplugin':
|
||||||
specifier: ^1.5.1
|
specifier: ^1.5.1
|
||||||
version: 1.5.2(@vue/compiler-core@3.5.13)(rollup@3.29.4)(vite@4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.43.1))(vue@3.5.13(typescript@5.5.4))(webpack@5.92.1)
|
version: 1.5.2(@vue/compiler-core@3.5.13)(rollup@3.29.4)(vite@4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.42.0))(vue@3.5.13(typescript@5.5.4))(webpack@5.92.1)
|
||||||
'@vintl/vintl':
|
'@vintl/vintl':
|
||||||
specifier: ^4.4.1
|
specifier: ^4.4.1
|
||||||
version: 4.4.1(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
|
version: 4.4.1(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
|
||||||
@ -1850,9 +1844,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.12':
|
|
||||||
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.5':
|
'@jridgewell/gen-mapping@0.3.5':
|
||||||
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
|
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@ -1865,24 +1856,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
'@jridgewell/source-map@0.3.10':
|
|
||||||
resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==}
|
|
||||||
|
|
||||||
'@jridgewell/source-map@0.3.6':
|
'@jridgewell/source-map@0.3.6':
|
||||||
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
|
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.0':
|
'@jridgewell/sourcemap-codec@1.5.0':
|
||||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.4':
|
|
||||||
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.29':
|
|
||||||
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
|
||||||
|
|
||||||
'@jsdevtools/ono@7.1.3':
|
'@jsdevtools/ono@7.1.3':
|
||||||
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
||||||
|
|
||||||
@ -3518,8 +3500,8 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.0:
|
||||||
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -3552,8 +3534,8 @@ packages:
|
|||||||
magicast:
|
magicast:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
c12@3.1.0:
|
c12@3.0.4:
|
||||||
resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==}
|
resolution: {integrity: sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
magicast: ^0.3.5
|
magicast: ^0.3.5
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@ -3605,8 +3587,8 @@ packages:
|
|||||||
caniuse-lite@1.0.30001687:
|
caniuse-lite@1.0.30001687:
|
||||||
resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==}
|
resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001727:
|
caniuse-lite@1.0.30001723:
|
||||||
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
|
resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
|
||||||
|
|
||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
@ -4169,8 +4151,8 @@ packages:
|
|||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.182:
|
electron-to-chromium@1.5.167:
|
||||||
resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==}
|
resolution: {integrity: sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.71:
|
electron-to-chromium@1.5.71:
|
||||||
resolution: {integrity: sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==}
|
resolution: {integrity: sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==}
|
||||||
@ -4205,8 +4187,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
|
resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
enhanced-resolve@5.18.2:
|
enhanced-resolve@5.18.1:
|
||||||
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
|
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
entities@2.2.0:
|
entities@2.2.0:
|
||||||
@ -6406,8 +6388,8 @@ packages:
|
|||||||
pkg-types@1.3.1:
|
pkg-types@1.3.1:
|
||||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||||
|
|
||||||
pkg-types@2.2.0:
|
pkg-types@2.1.1:
|
||||||
resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==}
|
resolution: {integrity: sha512-eY0QFb6eSwc9+0d/5D2lFFUq+A3n3QNGSy/X2Nvp+6MfzGw2u6EbA7S80actgjY1lkvvI0pqB+a4hioMh443Ew==}
|
||||||
|
|
||||||
pluralize@8.0.0:
|
pluralize@8.0.0:
|
||||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||||
@ -6624,8 +6606,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.5:
|
||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
posthog-js@1.158.2:
|
posthog-js@1.158.2:
|
||||||
@ -7509,11 +7491,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
terser@5.43.1:
|
|
||||||
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
text-decoder@1.1.0:
|
text-decoder@1.1.0:
|
||||||
resolution: {integrity: sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==}
|
resolution: {integrity: sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==}
|
||||||
|
|
||||||
@ -8376,8 +8353,8 @@ packages:
|
|||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
webpack-sources@3.3.3:
|
webpack-sources@3.3.2:
|
||||||
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
|
resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
webpack-virtual-modules@0.6.2:
|
webpack-virtual-modules@0.6.2:
|
||||||
@ -8932,10 +8909,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.5.4)
|
vue: 3.5.13(typescript@5.5.4)
|
||||||
|
|
||||||
'@braw/async-computed@5.0.2(vue@3.5.13(typescript@5.8.3))':
|
|
||||||
dependencies:
|
|
||||||
vue: 3.5.13(typescript@5.8.3)
|
|
||||||
|
|
||||||
'@cloudflare/kv-asset-handler@0.3.4':
|
'@cloudflare/kv-asset-handler@0.3.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
@ -9463,11 +9436,6 @@ snapshots:
|
|||||||
'@vue/compiler-core': 3.5.13
|
'@vue/compiler-core': 3.5.13
|
||||||
vue: 3.5.13(typescript@5.5.4)
|
vue: 3.5.13(typescript@5.5.4)
|
||||||
|
|
||||||
'@formatjs/cli@6.2.12(@vue/compiler-core@3.5.13)(vue@3.5.13(typescript@5.8.3))':
|
|
||||||
optionalDependencies:
|
|
||||||
'@vue/compiler-core': 3.5.13
|
|
||||||
vue: 3.5.13(typescript@5.8.3)
|
|
||||||
|
|
||||||
'@formatjs/ecma402-abstract@1.18.3':
|
'@formatjs/ecma402-abstract@1.18.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/intl-localematcher': 0.5.4
|
'@formatjs/intl-localematcher': 0.5.4
|
||||||
@ -9525,18 +9493,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
|
|
||||||
'@formatjs/intl@2.10.4(typescript@5.8.3)':
|
|
||||||
dependencies:
|
|
||||||
'@formatjs/ecma402-abstract': 2.0.0
|
|
||||||
'@formatjs/fast-memoize': 2.2.0
|
|
||||||
'@formatjs/icu-messageformat-parser': 2.7.8
|
|
||||||
'@formatjs/intl-displaynames': 6.6.8
|
|
||||||
'@formatjs/intl-listformat': 7.5.7
|
|
||||||
intl-messageformat: 10.5.14
|
|
||||||
tslib: 2.6.3
|
|
||||||
optionalDependencies:
|
|
||||||
typescript: 5.8.3
|
|
||||||
|
|
||||||
'@formatjs/ts-transformer@3.13.14':
|
'@formatjs/ts-transformer@3.13.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/icu-messageformat-parser': 2.7.8
|
'@formatjs/icu-messageformat-parser': 2.7.8
|
||||||
@ -9660,12 +9616,6 @@ snapshots:
|
|||||||
wrap-ansi: 8.1.0
|
wrap-ansi: 8.1.0
|
||||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.12':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.4
|
|
||||||
'@jridgewell/trace-mapping': 0.3.29
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.5':
|
'@jridgewell/gen-mapping@0.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/set-array': 1.2.1
|
'@jridgewell/set-array': 1.2.1
|
||||||
@ -9676,12 +9626,6 @@ snapshots:
|
|||||||
|
|
||||||
'@jridgewell/set-array@1.2.1': {}
|
'@jridgewell/set-array@1.2.1': {}
|
||||||
|
|
||||||
'@jridgewell/source-map@0.3.10':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/gen-mapping': 0.3.12
|
|
||||||
'@jridgewell/trace-mapping': 0.3.29
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@jridgewell/source-map@0.3.6':
|
'@jridgewell/source-map@0.3.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/gen-mapping': 0.3.5
|
'@jridgewell/gen-mapping': 0.3.5
|
||||||
@ -9689,20 +9633,11 @@ snapshots:
|
|||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.4':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.29':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.4
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@jsdevtools/ono@7.1.3': {}
|
'@jsdevtools/ono@7.1.3': {}
|
||||||
|
|
||||||
'@kwsites/file-exists@1.1.1':
|
'@kwsites/file-exists@1.1.1':
|
||||||
@ -9950,7 +9885,7 @@ snapshots:
|
|||||||
|
|
||||||
'@nuxt/kit@3.17.5(magicast@0.3.5)':
|
'@nuxt/kit@3.17.5(magicast@0.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
c12: 3.1.0(magicast@0.3.5)
|
c12: 3.0.4(magicast@0.3.5)
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
destr: 2.0.5
|
destr: 2.0.5
|
||||||
@ -9963,7 +9898,7 @@ snapshots:
|
|||||||
mlly: 1.7.4
|
mlly: 1.7.4
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
pkg-types: 2.2.0
|
pkg-types: 2.1.1
|
||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
semver: 7.7.2
|
semver: 7.7.2
|
||||||
std-env: 3.9.0
|
std-env: 3.9.0
|
||||||
@ -11165,7 +11100,7 @@ snapshots:
|
|||||||
- vue
|
- vue
|
||||||
- webpack
|
- webpack
|
||||||
|
|
||||||
'@vintl/unplugin@1.5.2(@vue/compiler-core@3.5.13)(rollup@3.29.4)(vite@4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.43.1))(vue@3.5.13(typescript@5.5.4))(webpack@5.92.1)':
|
'@vintl/unplugin@1.5.2(@vue/compiler-core@3.5.13)(rollup@3.29.4)(vite@4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.42.0))(vue@3.5.13(typescript@5.5.4))(webpack@5.92.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/cli-lib': 6.4.2(@vue/compiler-core@3.5.13)(vue@3.5.13(typescript@5.5.4))
|
'@formatjs/cli-lib': 6.4.2(@vue/compiler-core@3.5.13)(vue@3.5.13(typescript@5.5.4))
|
||||||
'@formatjs/icu-messageformat-parser': 2.7.8
|
'@formatjs/icu-messageformat-parser': 2.7.8
|
||||||
@ -11176,7 +11111,7 @@ snapshots:
|
|||||||
unplugin: 1.16.0
|
unplugin: 1.16.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
rollup: 3.29.4
|
rollup: 3.29.4
|
||||||
vite: 4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.43.1)
|
vite: 4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.42.0)
|
||||||
webpack: 5.92.1
|
webpack: 5.92.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@glimmer/env'
|
- '@glimmer/env'
|
||||||
@ -11224,17 +11159,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@vintl/vintl@4.4.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))':
|
|
||||||
dependencies:
|
|
||||||
'@braw/async-computed': 5.0.2(vue@3.5.13(typescript@5.8.3))
|
|
||||||
'@formatjs/icu-messageformat-parser': 2.7.8
|
|
||||||
'@formatjs/intl': 2.10.4(typescript@5.8.3)
|
|
||||||
'@formatjs/intl-localematcher': 0.4.2
|
|
||||||
intl-messageformat: 10.5.14
|
|
||||||
vue: 3.5.13(typescript@5.8.3)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@4.1.1(vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.42.0))(vue@3.5.13(typescript@5.5.4))':
|
'@vitejs/plugin-vue-jsx@4.1.1(vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.42.0))(vue@3.5.13(typescript@5.5.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
@ -12037,12 +11961,12 @@ snapshots:
|
|||||||
node-releases: 2.0.18
|
node-releases: 2.0.18
|
||||||
update-browserslist-db: 1.1.1(browserslist@4.24.2)
|
update-browserslist-db: 1.1.1(browserslist@4.24.2)
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001727
|
caniuse-lite: 1.0.30001723
|
||||||
electron-to-chromium: 1.5.182
|
electron-to-chromium: 1.5.167
|
||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.25.1)
|
update-browserslist-db: 1.1.3(browserslist@4.25.0)
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
buffer-crc32@1.0.0: {}
|
buffer-crc32@1.0.0: {}
|
||||||
@ -12081,7 +12005,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
magicast: 0.3.5
|
magicast: 0.3.5
|
||||||
|
|
||||||
c12@3.1.0(magicast@0.3.5):
|
c12@3.0.4(magicast@0.3.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 4.0.3
|
chokidar: 4.0.3
|
||||||
confbox: 0.2.2
|
confbox: 0.2.2
|
||||||
@ -12093,7 +12017,7 @@ snapshots:
|
|||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
perfect-debounce: 1.0.0
|
perfect-debounce: 1.0.0
|
||||||
pkg-types: 2.2.0
|
pkg-types: 2.1.1
|
||||||
rc9: 2.1.2
|
rc9: 2.1.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
magicast: 0.3.5
|
magicast: 0.3.5
|
||||||
@ -12145,7 +12069,7 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001687: {}
|
caniuse-lite@1.0.30001687: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001727:
|
caniuse-lite@1.0.30001723:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
@ -12621,7 +12545,7 @@ snapshots:
|
|||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.182:
|
electron-to-chromium@1.5.167:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
electron-to-chromium@1.5.71: {}
|
electron-to-chromium@1.5.71: {}
|
||||||
@ -12652,7 +12576,7 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.2.1
|
tapable: 2.2.1
|
||||||
|
|
||||||
enhanced-resolve@5.18.2:
|
enhanced-resolve@5.18.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.2.2
|
tapable: 2.2.2
|
||||||
@ -14351,7 +14275,7 @@ snapshots:
|
|||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.4.1
|
'@types/node': 20.14.11
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
optional: true
|
optional: true
|
||||||
@ -14524,7 +14448,7 @@ snapshots:
|
|||||||
local-pkg@1.1.1:
|
local-pkg@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
mlly: 1.7.4
|
mlly: 1.7.4
|
||||||
pkg-types: 2.2.0
|
pkg-types: 2.1.1
|
||||||
quansync: 0.2.10
|
quansync: 0.2.10
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -15521,7 +15445,7 @@ snapshots:
|
|||||||
citty: 0.1.6
|
citty: 0.1.6
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
pkg-types: 2.2.0
|
pkg-types: 2.1.1
|
||||||
tinyexec: 0.3.2
|
tinyexec: 0.3.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -15809,7 +15733,7 @@ snapshots:
|
|||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
pkg-types@2.2.0:
|
pkg-types@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
confbox: 0.2.2
|
confbox: 0.2.2
|
||||||
exsolve: 1.0.7
|
exsolve: 1.0.7
|
||||||
@ -16012,7 +15936,7 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
@ -17067,11 +16991,11 @@ snapshots:
|
|||||||
|
|
||||||
terser-webpack-plugin@5.3.14(webpack@5.92.1):
|
terser-webpack-plugin@5.3.14(webpack@5.92.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.29
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
jest-worker: 27.5.1
|
jest-worker: 27.5.1
|
||||||
schema-utils: 4.3.2
|
schema-utils: 4.3.2
|
||||||
serialize-javascript: 6.0.2
|
serialize-javascript: 6.0.2
|
||||||
terser: 5.43.1
|
terser: 5.42.0
|
||||||
webpack: 5.92.1
|
webpack: 5.92.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -17082,14 +17006,6 @@ snapshots:
|
|||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
terser@5.43.1:
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/source-map': 0.3.10
|
|
||||||
acorn: 8.15.0
|
|
||||||
commander: 2.20.3
|
|
||||||
source-map-support: 0.5.21
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
text-decoder@1.1.0:
|
text-decoder@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
b4a: 1.6.6
|
b4a: 1.6.6
|
||||||
@ -17365,7 +17281,7 @@ snapshots:
|
|||||||
mlly: 1.7.4
|
mlly: 1.7.4
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
pkg-types: 2.2.0
|
pkg-types: 2.1.1
|
||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
strip-literal: 3.0.0
|
strip-literal: 3.0.0
|
||||||
tinyglobby: 0.2.14
|
tinyglobby: 0.2.14
|
||||||
@ -17536,9 +17452,9 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
update-browserslist-db@1.1.3(browserslist@4.25.1):
|
update-browserslist-db@1.1.3(browserslist@4.25.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.1
|
browserslist: 4.25.0
|
||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
optional: true
|
optional: true
|
||||||
@ -17660,16 +17576,16 @@ snapshots:
|
|||||||
svgo: 3.3.2
|
svgo: 3.3.2
|
||||||
vue: 3.5.13(typescript@5.5.4)
|
vue: 3.5.13(typescript@5.5.4)
|
||||||
|
|
||||||
vite@4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.43.1):
|
vite@4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.42.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
postcss: 8.5.6
|
postcss: 8.5.5
|
||||||
rollup: 3.29.4
|
rollup: 3.29.4
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.4.1
|
'@types/node': 22.4.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sass: 1.77.6
|
sass: 1.77.6
|
||||||
terser: 5.43.1
|
terser: 5.42.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.42.0):
|
vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.42.0):
|
||||||
@ -17945,7 +17861,7 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
webpack-sources@3.3.3:
|
webpack-sources@3.3.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
webpack-virtual-modules@0.6.2: {}
|
webpack-virtual-modules@0.6.2: {}
|
||||||
@ -17959,9 +17875,9 @@ snapshots:
|
|||||||
'@webassemblyjs/wasm-parser': 1.14.1
|
'@webassemblyjs/wasm-parser': 1.14.1
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
acorn-import-attributes: 1.9.5(acorn@8.15.0)
|
acorn-import-attributes: 1.9.5(acorn@8.15.0)
|
||||||
browserslist: 4.25.1
|
browserslist: 4.25.0
|
||||||
chrome-trace-event: 1.0.4
|
chrome-trace-event: 1.0.4
|
||||||
enhanced-resolve: 5.18.2
|
enhanced-resolve: 5.18.1
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
eslint-scope: 5.1.1
|
eslint-scope: 5.1.1
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
@ -17975,7 +17891,7 @@ snapshots:
|
|||||||
tapable: 2.2.2
|
tapable: 2.2.2
|
||||||
terser-webpack-plugin: 5.3.14(webpack@5.92.1)
|
terser-webpack-plugin: 5.3.14(webpack@5.92.1)
|
||||||
watchpack: 2.4.4
|
watchpack: 2.4.4
|
||||||
webpack-sources: 3.3.3
|
webpack-sources: 3.3.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/core'
|
- '@swc/core'
|
||||||
- esbuild
|
- esbuild
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user