Compare commits

...
Author SHA1 Message Date
Calum H. (IMB11) 4bdd0e4155 prepr 2026-07-31 12:17:07 +01:00
Calum H. (IMB11) cb4462374c refactor: rename component 2026-07-31 12:05:45 +01:00
Calum H. (IMB11) f08f2d8405 Merge remote-tracking branch 'origin/main' into cal/change-styled-input
# Conflicts:
#	packages/ui/.storybook/main.ts
#	packages/ui/src/components/base/StyledInput.vue
#	packages/ui/src/components/search/SearchSidebarFilter.vue
2026-07-31 11:39:08 +01:00
Prospector 65a3ac4b34 changelog 2026-07-30 23:10:28 -07:00
ProspectorandGitHub 295db3fb7a only refresh tokens and clear auth cookies on auth fails, not any ran… (#6947)
only refresh tokens and clear auth cookies on auth fails, not any random error
2026-07-30 23:10:08 -07:00
Michael H. 7abe6f8c73 feat: sccache on windows 2026-07-30 20:27:08 +02:00
Michael H. c5ce5bc9b3 fix: create clickhouse db in tests only 2026-07-30 20:08:49 +02:00
Michael H. 572b8027ff fix: clickhouse migrations should not run normally 2026-07-30 19:23:10 +02:00
aecsocketandGitHub 0a53211389 fix: analytics event endpoint (#6938)
* fix: analytics event endpoint

* split admin-only routes into internal

* fix pr comment
2026-07-30 04:40:52 +00:00
Prospector 22b2b7746e changelog 2026-07-29 14:34:41 -07:00
Calum H. (IMB11) 22bfca467a refactor: migrate to new input components 2026-07-15 14:46:08 +01:00
Calum H. (IMB11) 1186a8a027 feat: new input components 2026-07-15 14:45:57 +01:00
Calum H. (IMB11) 3dd16180b0 feat: align style 2026-07-15 14:21:04 +01:00
152 changed files with 1624 additions and 1304 deletions
-2
View File
@@ -92,7 +92,6 @@ jobs:
run: corepack enable
- name: Set up caches
if: contains(matrix.artifact-target-name, 'linux') || contains(matrix.artifact-target-name, 'darwin')
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
with:
cache: |
@@ -100,7 +99,6 @@ jobs:
pnpm
- name: Configure sccache
if: contains(matrix.artifact-target-name, 'linux') || contains(matrix.artifact-target-name, 'darwin')
run: nsc cache sccache setup --cache_name default >> "$GITHUB_ENV"
- name: Generate tauri-dev.conf.json
@@ -14,7 +14,7 @@ import {
DropdownSelect,
formatLoader,
injectNotificationManager,
StyledInput,
Input,
useVIntl,
} from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
@@ -275,7 +275,7 @@ const filteredResults = computed(() => {
</script>
<template>
<div class="flex gap-2">
<StyledInput
<Input
v-model="search"
:icon="SearchIcon"
type="text"
@@ -6,8 +6,9 @@ import {
defineMessages,
FileTreeSelect,
injectNotificationManager,
Input,
NewModal,
StyledInput,
Textarea,
useVIntl,
} from '@modrinth/ui'
import { save } from '@tauri-apps/plugin-dialog'
@@ -244,7 +245,7 @@ function isExportCandidateDisabled(path) {
<div class="grid grid-cols-2 gap-4">
<div class="labeled_input w-full">
<p class="text-contrast font-semibold">{{ formatMessage(messages.modpackNameLabel) }}</p>
<StyledInput
<Input
v-model="nameInput"
type="text"
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
@@ -256,7 +257,7 @@ function isExportCandidateDisabled(path) {
<p class="text-contrast font-semibold">
{{ formatMessage(messages.versionNumberLabel) }}
</p>
<StyledInput
<Input
v-model="versionInput"
type="text"
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
@@ -269,9 +270,8 @@ function isExportCandidateDisabled(path) {
<p class="m-0 text-contrast font-semibold">
{{ formatMessage(commonMessages.descriptionLabel) }}
</p>
<StyledInput
<Textarea
v-model="exportDescription"
multiline
:placeholder="formatMessage(messages.descriptionPlaceholder)"
wrapper-class="w-full"
/>
@@ -2,7 +2,7 @@
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
<div :id="props.id" class="toggle-setting" :class="{ compact }">
<div class="input-with-status">
<StyledInput
<Input
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
@@ -93,7 +93,7 @@ import {
ButtonStyled,
defineMessages,
injectNotificationManager,
StyledInput,
Input,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
@@ -5,8 +5,8 @@ import {
ButtonStyled,
defineMessages,
injectNotificationManager,
Input,
IntlFormatted,
StyledInput,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
@@ -232,7 +232,7 @@ const messages = defineMessages({
{{ formatMessage(messages.usernameDescription) }}
</p>
<div class="flex items-center gap-2 mt-4">
<StyledInput
<Input
v-model="username"
:icon="UserIcon"
type="text"
@@ -260,7 +260,7 @@ const messages = defineMessages({
<UserPlusIcon />
</button>
</ButtonStyled>
<StyledInput
<Input
v-model="search"
:icon="SearchIcon"
type="text"
@@ -1,12 +1,6 @@
<script setup>
import { CheckIcon, PlusIcon, SearchIcon } from '@modrinth/assets'
import {
Admonition,
Avatar,
ButtonStyled,
injectNotificationManager,
StyledInput,
} from '@modrinth/ui'
import { Admonition, Avatar, ButtonStyled, injectNotificationManager, Input } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, ref } from 'vue'
@@ -85,7 +79,7 @@ async function addServer(instance) {
<ModalWrapper ref="modal" header="Add server to instance">
<div class="flex flex-col gap-4 min-w-[350px]">
<Admonition type="warning" body="This server may not be compatible with all instances." />
<StyledInput
<Input
v-model="searchFilter"
:icon="SearchIcon"
type="search"
@@ -7,8 +7,8 @@ import {
Chips,
defineMessages,
injectNotificationManager,
Input,
OverflowMenu,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
@@ -339,7 +339,7 @@ const messages = defineMessages({
{{ formatMessage(messages.name) }}
</label>
<div class="flex">
<StyledInput
<Input
id="instance-name"
v-model="title"
autocomplete="off"
@@ -382,7 +382,7 @@ const messages = defineMessages({
@click="toggleGroup(group)"
/>
<div class="flex gap-2 items-center">
<StyledInput
<Input
v-model="newCategoryInput"
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
class="w-full max-w-[300px]"
@@ -1,11 +1,5 @@
<script setup lang="ts">
import {
Checkbox,
defineMessages,
injectNotificationManager,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { Checkbox, defineMessages, injectNotificationManager, Input, useVIntl } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import { edit } from '@/helpers/instance'
@@ -112,7 +106,7 @@ const messages = defineMessages({
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.preLaunch) }}
</h2>
<StyledInput
<Input
id="pre-launch"
v-model="hooks.pre_launch"
autocomplete="off"
@@ -127,7 +121,7 @@ const messages = defineMessages({
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.wrapper) }}
</h2>
<StyledInput
<Input
id="wrapper"
v-model="hooks.wrapper"
autocomplete="off"
@@ -142,7 +136,7 @@ const messages = defineMessages({
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.postExit) }}
</h2>
<StyledInput
<Input
id="post-exit"
v-model="hooks.post_exit"
autocomplete="off"
@@ -13,8 +13,8 @@ import {
Checkbox,
defineMessages,
injectNotificationManager,
Input,
Slider,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
@@ -216,7 +216,7 @@ const messages = defineMessages({
>Java {{ optimalJava?.parsed_version }}</span
>
<div class="flex gap-2 items-center">
<StyledInput
<Input
:model-value="activePath"
:disabled="!overrideJavaInstall"
autocomplete="off"
@@ -294,7 +294,7 @@ const messages = defineMessages({
:label="formatMessage(messages.customJavaArguments)"
class="my-2"
/>
<StyledInput
<Input
id="java-args"
v-model="javaArgs"
autocomplete="off"
@@ -310,7 +310,7 @@ const messages = defineMessages({
:label="formatMessage(messages.customEnvironmentVariables)"
class="mb-2"
/>
<StyledInput
<Input
id="env-vars"
v-model="envVars"
autocomplete="off"
@@ -3,7 +3,7 @@ import {
Checkbox,
defineMessages,
injectNotificationManager,
StyledInput,
Input,
Toggle,
useVIntl,
} from '@modrinth/ui'
@@ -129,7 +129,7 @@ const messages = defineMessages({
{{ formatMessage(messages.widthDescription) }}
</p>
</div>
<StyledInput
<Input
id="width"
v-model="resolution[0]"
autocomplete="off"
@@ -148,7 +148,7 @@ const messages = defineMessages({
{{ formatMessage(messages.heightDescription) }}
</p>
</div>
<StyledInput
<Input
id="height"
v-model="resolution[1]"
autocomplete="off"
@@ -2,8 +2,8 @@
import {
defineMessages,
injectNotificationManager,
Input,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
@@ -187,7 +187,7 @@ watch(
</p>
</div>
<StyledInput
<Input
id="width"
v-model="settings.game_resolution[0]"
:disabled="settings.force_fullscreen"
@@ -207,7 +207,7 @@ watch(
</p>
</div>
<StyledInput
<Input
id="height"
v-model="settings.game_resolution[1]"
:disabled="settings.force_fullscreen"
@@ -244,7 +244,7 @@ watch(
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.javaArgumentsTitle) }}
</h2>
<StyledInput
<Input
id="java-args"
v-model="settings.launchArgs"
autocomplete="off"
@@ -261,7 +261,7 @@ watch(
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.environmentVariablesTitle) }}
</h2>
<StyledInput
<Input
id="env-vars"
v-model="settings.envVars"
autocomplete="off"
@@ -282,7 +282,7 @@ watch(
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.preLaunchHookTitle) }}
</h3>
<StyledInput
<Input
id="pre-launch"
v-model="settings.hooks.pre_launch"
autocomplete="off"
@@ -299,7 +299,7 @@ watch(
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.wrapperHookTitle) }}
</h3>
<StyledInput
<Input
id="wrapper"
v-model="settings.hooks.wrapper"
autocomplete="off"
@@ -316,7 +316,7 @@ watch(
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.postExitHookTitle) }}
</h3>
<StyledInput
<Input
id="post-exit"
v-model="settings.hooks.post_exit"
autocomplete="off"
@@ -4,8 +4,8 @@ import {
ButtonStyled,
defineMessages,
injectNotificationManager,
Input,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
@@ -175,7 +175,7 @@ async function findLauncherDir() {
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.appDirectoryTitle) }}
</h2>
<StyledInput
<Input
id="appDir"
v-model="settings.custom_dir"
:icon="BoxIcon"
@@ -194,7 +194,7 @@ async function findLauncherDir() {
</button>
</ButtonStyled>
</template>
</StyledInput>
</Input>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.appDirectoryDescription) }}
</p>
@@ -6,7 +6,7 @@ import {
commonMessages,
defineMessages,
injectNotificationManager,
StyledInput,
Input,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
@@ -104,7 +104,7 @@ const messages = defineMessages({
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
{{ formatMessage(messages.name) }}
</h2>
<StyledInput
<Input
v-model="name"
:placeholder="formatMessage(messages.placeholderName)"
autocomplete="off"
@@ -1,11 +1,5 @@
<script setup lang="ts">
import {
Combobox,
defineMessages,
type MessageDescriptor,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { Combobox, defineMessages, Input, type MessageDescriptor, useVIntl } from '@modrinth/ui'
import type { ServerPackStatus } from '@/helpers/worlds.ts'
@@ -65,7 +59,7 @@ defineExpose({ resourcePackOptions })
<div class="space-y-4 w-full">
<label class="flex flex-col gap-2">
<span class="font-semibold text-contrast">{{ formatMessage(messages.name) }}</span>
<StyledInput
<Input
v-model="name"
:placeholder="formatMessage(messages.placeholderName)"
autocomplete="off"
@@ -74,7 +68,7 @@ defineExpose({ resourcePackOptions })
</label>
<label class="flex flex-col gap-2">
<span class="font-semibold text-contrast">{{ formatMessage(messages.address) }}</span>
<StyledInput
<Input
v-model="address"
:placeholder="formatMessage(messages.placeholderAddress)"
autocomplete="off"
@@ -21,7 +21,7 @@
<ReadyTransition :pending="worldsReadyPending">
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2">
<StyledInput
<Input
v-model="searchFilter"
:icon="SearchIcon"
type="text"
@@ -154,8 +154,8 @@ import {
GAME_MODES,
type GameVersion,
injectNotificationManager,
Input,
ReadyTransition,
StyledInput,
useReadyState,
useVIntl,
} from '@modrinth/ui'
@@ -2,7 +2,7 @@
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<StyledInput
<Input
v-model="search"
:icon="SearchIcon"
:placeholder="`Search ${rows.length} users...`"
@@ -150,8 +150,8 @@ import {
Avatar,
ButtonStyled,
defineMessages,
Input,
type SortDirection,
StyledInput,
Table,
type TableColumn,
truncatedTooltip,
@@ -23,7 +23,7 @@
</div>
<div class="flex w-full flex-wrap items-center gap-2 md:w-auto">
<StyledInput
<Input
v-model="searchQuery"
:icon="SearchIcon"
:placeholder="formatMessage(analyticsTableMessages.searchPlaceholder)"
@@ -193,10 +193,10 @@
import { DownloadIcon, DropdownIcon, SearchIcon, UserIcon } from '@modrinth/assets'
import {
ButtonStyled,
Input,
OverflowMenu,
type OverflowMenuOption,
Pagination,
StyledInput,
Table,
useFormatNumber,
useVIntl,
@@ -48,7 +48,7 @@
To confirm this transfer, type
<span class="font-bold text-contrast">{{ organization.name }}</span> below
</p>
<StyledInput
<Input
v-model="confirmationText"
:placeholder="`Enter ${organization.name}`"
wrapper-class="w-full"
@@ -76,7 +76,7 @@
<script setup lang="ts">
import { RightArrowIcon, TransferIcon, XIcon } from '@modrinth/assets'
import { Admonition, Avatar, ButtonStyled, NewModal, StyledInput } from '@modrinth/ui'
import { Admonition, Avatar, ButtonStyled, Input, NewModal } from '@modrinth/ui'
import { computed, ref } from 'vue'
const props = defineProps<{
@@ -55,7 +55,7 @@
To confirm this transfer, type
<span class="font-bold text-contrast">{{ project.name }}</span> below
</p>
<StyledInput
<Input
v-model="confirmationText"
:placeholder="`Enter ${project.name}`"
wrapper-class="w-full"
@@ -83,7 +83,7 @@
<script setup lang="ts">
import { RightArrowIcon, TransferIcon, XIcon } from '@modrinth/assets'
import { Admonition, Avatar, ButtonStyled, NewModal, StyledInput } from '@modrinth/ui'
import { Admonition, Avatar, ButtonStyled, Input, NewModal } from '@modrinth/ui'
import { computed, ref } from 'vue'
const props = defineProps<{
@@ -11,7 +11,7 @@
>
<BookmarkIcon aria-hidden="true" :fill="saved ? 'currentColor' : 'none'" />
<template #menu>
<StyledInput
<Input
v-model="displayCollectionsSearch"
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
wrapper-class="menu-search"
@@ -52,14 +52,7 @@
<script setup lang="ts">
import { BookmarkIcon, PlusIcon } from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
commonMessages,
PopoutMenu,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { ButtonStyled, Checkbox, commonMessages, Input, PopoutMenu, useVIntl } from '@modrinth/ui'
import { computed, ref } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
@@ -6,9 +6,9 @@ import {
ButtonStyled,
injectModrinthClient,
injectNotificationManager,
Input,
NewModal,
ServerNotice,
StyledInput,
TagItem,
} from '@modrinth/ui'
import { ref } from 'vue'
@@ -175,7 +175,7 @@ defineExpose({ show, hide })
<span v-else class="mb-2"> No nodes assigned yet </span>
</div>
<div class="flex w-[45rem] items-center gap-2">
<StyledInput
<Input
id="server-assign-field"
v-model="inputField"
wrapper-class="w-full"
@@ -20,7 +20,7 @@
<label for="days" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Days to credit </span>
</label>
<StyledInput
<Input
id="days"
v-model="days"
wrapper-class="w-32"
@@ -36,12 +36,7 @@
<span class="text-lg font-semibold text-contrast"> Node hostnames </span>
</label>
<div class="flex items-center gap-2">
<StyledInput
id="node-input"
v-model="nodeInput"
wrapper-class="w-32"
autocomplete="off"
/>
<Input id="node-input" v-model="nodeInput" wrapper-class="w-32" autocomplete="off" />
<ButtonStyled color="blue" color-fill="text">
<button class="shrink-0" @click="addNode">
<PlusIcon />
@@ -92,13 +87,7 @@
class="text-muted flex flex-col gap-2 rounded-lg border border-surface-5 bg-button-bg p-4"
>
<span>Hi {user.name},</span>
<StyledInput
id="message-batch"
v-model="message"
multiline
:rows="3"
input-class="!bg-surface-3"
/>
<Textarea id="message-batch" v-model="message" :rows="3" input-class="!bg-surface-3" />
<span>
To make up for it, we've added {{ days }} day{{ pluralize(days) }} to your Modrinth
Servers subscription.
@@ -135,9 +124,10 @@ import {
Combobox,
injectModrinthClient,
injectNotificationManager,
Input,
NewModal,
StyledInput,
TagItem,
Textarea,
Toggle,
} from '@modrinth/ui'
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
@@ -25,10 +25,9 @@
</span>
<span>Server IDs (one per line or comma-separated.)</span>
</label>
<StyledInput
<Textarea
id="server-ids"
v-model="serverIdsInput"
multiline
:rows="4"
input-class="bg-surface-3"
placeholder="123e4569-e89b-12d3-a456-426614174005&#10;123e9569-e89b-12d3-a456-413678919876"
@@ -48,7 +47,7 @@
<span>Add nodes to transfer (comma or space-separated).</span>
</label>
<div class="flex items-center gap-2">
<StyledInput
<Input
id="node-input"
v-model="nodeInput"
wrapper-class="w-64"
@@ -86,7 +85,7 @@
<span class="text-lg font-semibold text-contrast">Tag transferred nodes</span>
<span>Optional tag to add to the transferred nodes.</span>
</label>
<StyledInput
<Input
id="tag-nodes"
v-model="tagNodes"
wrapper-class="max-w-[12rem]"
@@ -114,7 +113,7 @@
<span>Optional preferred node tags for node selection.</span>
</label>
<div class="flex items-center gap-2">
<StyledInput
<Input
id="tag-input"
v-model="tagInput"
wrapper-class="w-40"
@@ -147,7 +146,7 @@
:format-label="(item) => scheduleOptionLabels[item]"
:capitalize="false"
/>
<StyledInput
<DateInput
v-if="scheduleOption === 'later'"
v-model="scheduledDate"
type="datetime-local"
@@ -164,10 +163,9 @@
</span>
<span>Provide a reason for this transfer batch.</span>
</label>
<StyledInput
<Textarea
id="reason"
v-model="reason"
multiline
:rows="2"
input-class="bg-surface-3"
placeholder="Node maintenance scheduled"
@@ -198,11 +196,13 @@ import {
ButtonStyled,
Chips,
Combobox,
DateInput,
injectModrinthClient,
injectNotificationManager,
Input,
NewModal,
StyledInput,
TagItem,
Textarea,
Toggle,
} from '@modrinth/ui'
import dayjs from 'dayjs'
@@ -45,7 +45,7 @@
<label class="text-md font-semibold text-contrast" for="create-account-username">
{{ formatMessage(messages.usernameOptionalLabel) }}
</label>
<StyledInput
<Input
id="create-account-username"
v-model="usernameModel"
type="text"
@@ -106,7 +106,7 @@ import {
DatePicker,
defineMessages,
injectNotificationManager,
StyledInput,
Input,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
@@ -111,7 +111,7 @@
<div class="text-md font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<StyledInput
<Input
id="passkey-name"
v-model="pendingPasskeyName"
:maxlength="255"
@@ -150,7 +150,7 @@
<div class="text-md font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<StyledInput
<Input
id="passkey-rename"
v-model="passkeyToRenameName"
:maxlength="255"
@@ -215,8 +215,8 @@ import {
defineMessages,
injectModrinthClient,
injectNotificationManager,
Input,
NewModal,
StyledInput,
useFormatDateTime,
useRelativeTime,
useVIntl,
@@ -18,7 +18,7 @@
{{ formatMessage(messages.twoFactorCodeLabelDescription) }}
</span>
</label>
<StyledInput
<Input
id="two-factor-code"
v-model="twoFactorCodeModel"
:maxlength="11"
@@ -89,7 +89,7 @@
<section class="mx-auto flex w-full flex-col gap-2.5">
<label for="email" hidden>{{ formatMessage(commonMessages.emailUsernameLabel) }}</label>
<StyledInput
<Input
id="email"
v-model="emailModel"
:icon="MailIcon"
@@ -101,7 +101,7 @@
/>
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
<StyledInput
<Input
id="password"
v-model="passwordModel"
:icon="KeyIcon"
@@ -167,7 +167,7 @@ import {
SteamColorIcon,
UserKeyIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, defineMessages, StyledInput, useVIntl } from '@modrinth/ui'
import { ButtonStyled, commonMessages, defineMessages, Input, useVIntl } from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
import type { LocationQuery } from 'vue-router'
@@ -84,7 +84,7 @@
<section class="flex flex-col gap-2.5">
<label for="email" hidden>{{ formatMessage(commonMessages.emailLabel) }}</label>
<StyledInput
<Input
id="email"
v-model="emailModel"
:icon="MailIcon"
@@ -95,7 +95,7 @@
/>
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
<StyledInput
<Input
id="password"
v-model="passwordModel"
:icon="KeyIcon"
@@ -162,8 +162,8 @@ import {
ButtonStyled,
commonMessages,
defineMessages,
Input,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
@@ -13,7 +13,7 @@
size="small"
/>
</div>
<StyledInput
<Input
v-model="searchQuery"
:icon="SearchIcon"
type="text"
@@ -68,7 +68,7 @@
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { SearchIcon } from '@modrinth/assets'
import { ButtonStyled, Chips, StyledInput } from '@modrinth/ui'
import { ButtonStyled, Chips, Input } from '@modrinth/ui'
import { useMagicKeys } from '@vueuse/core'
import { computed, nextTick, onMounted, ref } from 'vue'
@@ -24,7 +24,7 @@
<span class="font-semibold text-contrast">
{{ formatMessage(messages.versionNumber) }} <span class="text-red">*</span>
</span>
<StyledInput
<Input
id="version-number"
v-model="draftVersion.version_number"
:disabled="isUploading"
@@ -36,7 +36,7 @@
</div>
<div class="flex flex-col gap-2">
<span class="font-semibold text-contrast">{{ formatMessage(messages.versionSubtitle) }}</span>
<StyledInput
<Input
id="version-number"
v-model="draftVersion.name"
:placeholder="formatMessage(messages.versionSubtitlePlaceholder)"
@@ -66,8 +66,8 @@
import {
Chips,
defineMessages,
Input,
MarkdownEditor,
StyledInput,
Tabs,
type TabsTab,
useVIntl,
@@ -9,7 +9,7 @@
<span class="text-brand-red">*</span>
</span>
</label>
<StyledInput
<Input
id="name"
v-model="name"
:maxlength="64"
@@ -25,10 +25,9 @@
}}</span>
<span>{{ formatMessage(messages.summaryDescription) }}</span>
</label>
<StyledInput
<Textarea
id="additional-information"
v-model="description"
multiline
:maxlength="256"
:placeholder="formatMessage(messages.summaryPlaceholder)"
:disabled="hasHitLimit"
@@ -61,8 +60,9 @@ import {
commonMessages,
defineMessages,
injectNotificationManager,
Input,
NewModal,
StyledInput,
Textarea,
useVIntl,
} from '@modrinth/ui'
@@ -9,7 +9,7 @@
<span class="text-brand-red">*</span>
</span>
</label>
<StyledInput
<Input
id="name"
v-model="name"
:maxlength="64"
@@ -28,7 +28,7 @@
</label>
<div class="text-input-wrapper">
<div class="text-input-wrapper__before">https://modrinth.com/organization/</div>
<StyledInput
<Input
id="slug"
v-model="slug"
:maxlength="64"
@@ -46,10 +46,9 @@
</span>
<span>{{ formatMessage(messages.summaryDescription) }}</span>
</label>
<StyledInput
<Textarea
id="additional-information"
v-model="description"
multiline
:maxlength="256"
:placeholder="formatMessage(messages.summaryPlaceholder)"
:disabled="hasHitLimit"
@@ -83,8 +82,9 @@ import {
commonMessages,
defineMessages,
injectNotificationManager,
Input,
NewModal,
StyledInput,
Textarea,
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
@@ -32,7 +32,7 @@
{{ formatMessage(messages.nameLabel) }}
</span>
</label>
<StyledInput
<Input
id="name"
v-model="name"
:maxlength="64"
@@ -48,7 +48,7 @@
</span>
<div class="text-input-wrapper !w-full">
<div class="text-input-wrapper__before">https://modrinth.com/project/</div>
<StyledInput
<Input
id="slug"
v-model="slug"
:maxlength="64"
@@ -100,10 +100,9 @@
{{ formatMessage(messages.summaryLabel) }}
</span>
</label>
<StyledInput
<Textarea
id="additional-information"
v-model="description"
multiline
:maxlength="256"
:placeholder="formatMessage(messages.summaryPlaceholder)"
:disabled="hasHitLimit"
@@ -140,8 +139,9 @@ import {
defineMessages,
injectModrinthClient,
injectNotificationManager,
Input,
NewModal,
StyledInput,
Textarea,
useVIntl,
} from '@modrinth/ui'
import { computed, defineAsyncComponent, h } from 'vue'
@@ -2,7 +2,7 @@
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<div class="relative flex-1">
<StyledInput
<Input
ref="amountInput"
:model-value="modelValue"
type="number"
@@ -12,7 +12,7 @@
:disabled="isDisabled"
:placeholder="formatMessage(formFieldPlaceholders.amountPlaceholder)"
wrapper-class="w-full"
@update:model-value="handleStyledInput"
@update:model-value="handleInput"
/>
</div>
<Combobox
@@ -54,7 +54,7 @@ import {
Combobox,
commonMessages,
formFieldPlaceholders,
StyledInput,
Input,
useFormatMoney,
useVIntl,
} from '@modrinth/ui'
@@ -83,7 +83,7 @@ const emit = defineEmits<{
const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const amountInput = ref<InstanceType<typeof StyledInput> | null>(null)
const amountInput = ref<InstanceType<typeof Input> | null>(null)
const safeMaxAmount = computed(() => {
return Math.max(0, props.maxAmount)
@@ -108,7 +108,7 @@ function setMaxAmount() {
emit('update:modelValue', maxValue)
}
function handleStyledInput(val: string | number) {
function handleInput(val: string | number) {
const value = String(val)
if (value && value.includes('.')) {
@@ -41,7 +41,7 @@
>
</label>
<div class="flex flex-row gap-2">
<StyledInput
<Input
v-model="venmoHandle"
:placeholder="formatMessage(messages.venmoHandlePlaceholder)"
wrapper-class="w-full"
@@ -110,9 +110,9 @@ import {
defineMessages,
financialMessages,
formFieldLabels,
Input,
IntlFormatted,
normalizeChildren,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useDebounceFn } from '@vueuse/core'
@@ -56,7 +56,7 @@
</span>
<div class="flex flex-col gap-3 sm:flex-row sm:gap-4">
<div class="flex flex-1 flex-col gap-2.5">
<StyledInput
<Input
v-model="formData.bankAccountOwnerFirstName"
:placeholder="formatMessage(formFieldPlaceholders.firstNamePlaceholder)"
autocomplete="given-name"
@@ -64,7 +64,7 @@
/>
</div>
<div class="flex flex-1 flex-col gap-2.5">
<StyledInput
<Input
v-model="formData.bankAccountOwnerLastName"
:placeholder="formatMessage(formFieldPlaceholders.lastNamePlaceholder)"
autocomplete="family-name"
@@ -91,7 +91,7 @@
class="h-10"
/>
<StyledInput
<Input
v-else
v-model="formData.bankName"
:placeholder="formatMessage(formFieldPlaceholders.bankNamePlaceholder)"
@@ -108,7 +108,7 @@
</span>
</label>
<StyledInput
<Input
v-if="['text', 'email', 'tel'].includes(field.type)"
v-model="formData[field.name]"
:type="field.type === 'tel' ? undefined : field.type === 'text' ? undefined : field.type"
@@ -158,7 +158,7 @@
<span v-if="dynamicDocumentNumberField.required" class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.documentNumber"
:placeholder="dynamicDocumentNumberField.placeholder"
autocomplete="off"
@@ -238,9 +238,9 @@ import {
getBlockchainIcon,
getCurrencyColor,
getCurrencyIcon,
Input,
IntlFormatted,
normalizeChildren,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useDebounceFn } from '@vueuse/core'
@@ -32,7 +32,7 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.businessName"
:placeholder="formatMessage(formFieldPlaceholders.businessNamePlaceholder)"
autocomplete="organization"
@@ -47,7 +47,7 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.email"
type="email"
:placeholder="formatMessage(formFieldPlaceholders.emailPlaceholder)"
@@ -65,7 +65,7 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.firstName"
:placeholder="formatMessage(formFieldPlaceholders.firstNamePlaceholder)"
autocomplete="given-name"
@@ -79,7 +79,7 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.lastName"
:placeholder="formatMessage(formFieldPlaceholders.lastNamePlaceholder)"
autocomplete="family-name"
@@ -112,7 +112,7 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.physicalAddress.address1"
:placeholder="formatMessage(formFieldPlaceholders.addressPlaceholder)"
autocomplete="address-line1"
@@ -126,7 +126,7 @@
{{ formatMessage(formFieldLabels.addressLine2) }}
</span>
</label>
<StyledInput
<Input
v-model="formData.physicalAddress.address2"
:placeholder="formatMessage(formFieldPlaceholders.address2Placeholder)"
autocomplete="address-line2"
@@ -142,7 +142,7 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.physicalAddress.city"
:placeholder="formatMessage(formFieldPlaceholders.cityPlaceholder)"
autocomplete="address-level2"
@@ -164,7 +164,7 @@
searchable
:search-placeholder="formatMessage(messages.searchSubdivisionsPlaceholder)"
/>
<StyledInput
<Input
v-else
v-model="formData.physicalAddress.state"
:placeholder="formatMessage(formFieldPlaceholders.statePlaceholder)"
@@ -182,7 +182,7 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<Input
v-model="formData.physicalAddress.zip"
:placeholder="formatMessage(formFieldPlaceholders.postalCodePlaceholder)"
autocomplete="postal-code"
@@ -217,7 +217,7 @@ import {
defineMessages,
formFieldLabels,
formFieldPlaceholders,
StyledInput,
Input,
useVIntl,
} from '@modrinth/ui'
// TODO: Switch to using Muralpay's improved endpoint when it's available.
@@ -39,7 +39,7 @@
>{{ formatMessage(formFieldLabels.email) }} <span class="text-red">*</span></span
>
</label>
<StyledInput
<Input
v-model="deliveryEmail"
type="email"
:placeholder="formatMessage(formFieldPlaceholders.emailPlaceholder)"
@@ -131,7 +131,7 @@
<div v-if="showGiftCardSelector && useFixedDenominations" class="flex flex-col gap-2.5">
<template v-if="useDenominationSuggestions">
<StyledInput
<Input
v-model="denominationSearchInput"
type="number"
:icon="SearchIcon"
@@ -321,10 +321,10 @@ import {
financialMessages,
formFieldLabels,
formFieldPlaceholders,
Input,
IntlFormatted,
normalizeChildren,
paymentMethodMessages,
StyledInput,
useDebugLogger,
useFormatMoney,
useVIntl,
@@ -1,7 +1,7 @@
<template>
<div>
<form class="flex flex-col gap-2 sm:flex-row" @submit.prevent="executeSearch">
<StyledInput
<Input
v-model="query"
:icon="SearchIcon"
type="text"
@@ -128,8 +128,8 @@ import {
EmptyState,
injectModrinthClient,
injectNotificationManager,
Input,
Pagination,
StyledInput,
} from '@modrinth/ui'
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
@@ -218,10 +218,9 @@
:heading-buttons="false"
:on-image-upload="onUploadHandler"
/>
<StyledInput
<Textarea
v-else
v-model="messageText"
multiline
placeholder="No message generated."
autocomplete="off"
input-class="h-[400px] font-mono"
@@ -414,7 +413,7 @@ import {
injectNotificationManager,
injectProjectPageContext,
MarkdownEditor,
StyledInput,
Textarea,
useDebugLogger,
} from '@modrinth/ui'
import TeleportOverflowMenu from '@modrinth/ui/src/components/base/TeleportOverflowMenu.vue'
@@ -34,7 +34,7 @@
<label for="proof">
<span class="label__title">Proof</span>
</label>
<StyledInput
<Input
id="proof"
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).proof"
autocomplete="off"
@@ -44,7 +44,7 @@
<label for="link">
<span class="label__title">Link</span>
</label>
<StyledInput
<Input
id="link"
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).url"
autocomplete="off"
@@ -54,7 +54,7 @@
<label for="title">
<span class="label__title">Title</span>
</label>
<StyledInput
<Input
id="title"
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).title"
autocomplete="off"
@@ -143,7 +143,7 @@
<script setup lang="ts">
import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
import { ButtonStyled, StyledInput } from '@modrinth/ui'
import { ButtonStyled, Input } from '@modrinth/ui'
import type {
ModerationFlameModpackItem,
ModerationJudgements,
@@ -30,11 +30,11 @@ import {
Checkbox,
Combobox,
injectProjectPageContext,
Input,
MarkdownEditor,
StyledInput,
} from '@modrinth/ui'
import { renderHighlightedString, renderString } from '@modrinth/utils'
import { inject, nextTick, onMounted, reactive, watchEffect } from 'vue'
import { inject, onMounted, reactive, watchEffect } from 'vue'
import { NODE_META_KEY, STATE_KEY } from './checklist-context'
@@ -266,7 +266,6 @@ function setTextState(node: IdentifiedNodeBuilder, v: string): void {
const def = resolveDefault(node as ValueNodeBuilder)
const defStr = typeof def === 'string' ? def : ''
setNodeState(node, ov === defStr ? undefined : ov || undefined)
nextTick(() => textInputRefs.get(node)?.setValue(ov))
return
}
@@ -277,7 +276,7 @@ function setTextState(node: IdentifiedNodeBuilder, v: string): void {
function handleButtonClick(node: ButtonNodeBuilder): void {
const before = new Map<NodeBuilder, string>()
for (const inputNode of textInputRefs.keys()) {
for (const inputNode of textInputNodes) {
before.set(inputNode, getTextState(asIdentified(inputNode)))
}
node._onClick?.(props.showContext)
@@ -288,7 +287,7 @@ function handleButtonClick(node: ButtonNodeBuilder): void {
}
onMounted(() => {
for (const inputNode of textInputRefs.keys()) {
for (const inputNode of textInputNodes) {
setTextState(asIdentified(inputNode), getTextState(asIdentified(inputNode)))
}
})
@@ -325,7 +324,7 @@ function toggleChip(parent: IdentifiedNodeBuilder, child: IdentifiedNodeBuilder)
setNodeState(parent, selected.size > 0 ? selected : undefined)
}
const textInputRefs = new Map<NodeBuilder, { setValue: (v: string) => void }>()
const textInputNodes = new Set<NodeBuilder>()
const overrideHelpers = { override: (v: string): OverrideValue => ({ __override: v }) }
function isOverrideValue(v: unknown): v is OverrideValue {
@@ -720,9 +719,9 @@ watchEffect(async () => {
<!-- text -->
<template v-else-if="item.type === 'text'">
<StyledInput
<Input
:id="`node-${asIdentified(item).id}`"
:ref="(el: any) => (el ? textInputRefs.set(item, el) : textInputRefs.delete(item))"
:ref="(el: any) => (el ? textInputNodes.add(item) : textInputNodes.delete(item))"
v-tooltip="getTooltipConfig(item, showContext)"
:model-value="getTextState(asIdentified(item))"
:placeholder="getPlaceholder(asInput(item))"
+48 -16
View File
@@ -30,6 +30,29 @@ const normalizeAuthToken = (value: unknown) => {
return ''
}
const getErrorStatus = (error: unknown): number | undefined => {
if (!error || typeof error !== 'object') {
return undefined
}
const typedError = error as { statusCode?: unknown; status?: unknown }
const status = typedError.statusCode ?? typedError.status
return typeof status === 'number' ? status : undefined
}
// only when labrinth actually gives us an auth error
const isAuthFailure = (error: unknown): boolean => {
const status = getErrorStatus(error)
return status === 401 || status === 403
}
const clearAuthCookie = (auth: AuthState, authCookie: { value: string | null }) => {
authCookie.value = null
auth.token = ''
auth.user = null
}
const getQueryString = (value: QueryValue) => {
if (Array.isArray(value)) {
return value[0] ?? null
@@ -90,6 +113,7 @@ export const initAuth = async (oldToken: string | null | undefined = null) => {
}
const tokenStr = normalizeAuthToken(authCookie.value)
let shouldRefresh = false
if (authCookie.value != null && tokenStr === '') {
authCookie.value = null
@@ -111,12 +135,13 @@ export const initAuth = async (oldToken: string | null | undefined = null) => {
},
true,
)) as Labrinth.Users.v2.User
} catch {
/* empty */
} catch (error) {
// only refresh when the token was rejected. not on timeouts or other errors (think this was the cause of random logouts)
shouldRefresh = isAuthFailure(error)
}
}
if (!auth.user && auth.token) {
if (!auth.user && auth.token && shouldRefresh) {
try {
const session = (await useBaseFetch(
'session/refresh',
@@ -132,22 +157,29 @@ export const initAuth = async (oldToken: string | null | undefined = null) => {
auth.token = normalizeAuthToken(session.session)
if (auth.token) {
authCookie.value = auth.token
auth.user = (await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
try {
auth.user = (await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
},
},
},
true,
)) as Labrinth.Users.v2.User
true,
)) as Labrinth.Users.v2.User
} catch (error) {
if (isAuthFailure(error)) {
clearAuthCookie(auth, authCookie)
}
}
} else {
authCookie.value = null
auth.token = ''
clearAuthCookie(auth, authCookie)
}
} catch (error) {
if (isAuthFailure(error)) {
clearAuthCookie(auth, authCookie)
}
} catch {
authCookie.value = null
}
}
@@ -43,7 +43,7 @@
<label for="gallery-image-title">
<span class="label__title">Title</span>
</label>
<StyledInput
<Input
id="gallery-image-title"
v-model="editTitle"
:maxlength="64"
@@ -52,17 +52,16 @@
<label for="gallery-image-desc">
<span class="label__title">Description</span>
</label>
<StyledInput
<Textarea
id="gallery-image-desc"
v-model="editDescription"
multiline
:maxlength="255"
placeholder="Enter description..."
/>
<label for="gallery-image-ordering">
<span class="label__title">Order Index</span>
</label>
<StyledInput
<Input
id="gallery-image-ordering"
v-model="editOrder"
type="number"
@@ -305,8 +304,9 @@ import {
DropArea,
FileInput,
injectProjectPageContext,
Input,
NewModal as Modal,
StyledInput,
Textarea,
useFormatDateTime,
} from '@modrinth/ui'
import { useEventListener } from '@vueuse/core'
@@ -43,7 +43,7 @@
<label for="gallery-image-title">
<span class="label__title">Title</span>
</label>
<StyledInput
<Input
id="gallery-image-title"
v-model="editTitle"
:maxlength="64"
@@ -52,17 +52,16 @@
<label for="gallery-image-desc">
<span class="label__title">Description</span>
</label>
<StyledInput
<Textarea
id="gallery-image-desc"
v-model="editDescription"
multiline
:maxlength="255"
placeholder="Enter description..."
/>
<label for="gallery-image-ordering">
<span class="label__title">Order Index</span>
</label>
<StyledInput
<Input
id="gallery-image-ordering"
v-model="editOrder"
type="number"
@@ -295,8 +294,9 @@ import {
DropArea,
FileInput,
injectProjectPageContext,
Input,
NewModal as Modal,
StyledInput,
Textarea,
useFormatDateTime,
} from '@modrinth/ui'
@@ -4,9 +4,9 @@ import {
defineMessages,
IconSelect,
injectProjectPageContext,
Input,
type MessageDescriptor,
SettingsLabel,
StyledInput,
UnsavedChangesPopup,
usePageLeaveSafety,
useSavable,
@@ -140,7 +140,7 @@ const placeholder = computed(() => placeholders[placeholderIndex.value] ?? place
:description="messages.nameDescription"
/>
<div class="flex">
<StyledInput
<Input
id="project-name"
v-model="current.title"
:placeholder="formatMessage(placeholder.name)"
@@ -156,7 +156,7 @@ const placeholder = computed(() => placeholders[placeholderIndex.value] ?? place
:title="messages.taglineTitle"
:description="messages.taglineDescription"
/>
<StyledInput
<Input
id="project-tagline"
v-model="current.tagline"
:placeholder="formatMessage(placeholder.tagline)"
@@ -169,7 +169,7 @@ const placeholder = computed(() => placeholders[placeholderIndex.value] ?? place
<SettingsLabel id="project-url" :title="messages.urlTitle" />
<div class="text-input-wrapper">
<div class="text-input-wrapper__before">https://modrinth.com/project/</div>
<StyledInput id="project-url" v-model="current.url" :maxlength="64" autocomplete="off" />
<Input id="project-url" v-model="current.url" :maxlength="64" autocomplete="off" />
</div>
</div>
</div>
@@ -21,12 +21,7 @@
<label for="project-name">
<span class="label__title">Name</span>
</label>
<StyledInput
id="project-name"
v-model="name"
:maxlength="2048"
:disabled="!hasPermission"
/>
<Input id="project-name" v-model="name" :maxlength="2048" :disabled="!hasPermission" />
</div>
<div>
@@ -39,7 +34,7 @@
$getProjectTypeForUrl(project.project_type, project.loaders)
}}/
</div>
<StyledInput
<Input
id="project-slug"
v-model="slug"
:maxlength="64"
@@ -53,10 +48,9 @@
<label for="project-summary">
<span class="label__title">Summary</span>
</label>
<StyledInput
<Textarea
id="project-summary"
v-model="summary"
multiline
:maxlength="256"
:disabled="!hasPermission"
resize="vertical"
@@ -341,7 +335,8 @@ import {
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
StyledInput,
Input,
Textarea,
Toggle,
UnsavedChangesPopup,
useFormatBytes,
@@ -72,7 +72,7 @@
</label>
<div class="w-1/2">
<StyledInput
<Input
id="license-url"
v-model="current.licenseUrl"
type="url"
@@ -105,7 +105,7 @@
</label>
<div class="flex w-1/2 flex-col gap-2">
<StyledInput
<Input
v-if="!current.nonSpdxLicense"
id="license-spdx"
v-model="current.license.short"
@@ -114,7 +114,7 @@
placeholder="SPDX identifier"
:disabled="!hasPermission"
/>
<StyledInput
<Input
v-else
id="license-name"
v-model="current.license.short"
@@ -158,7 +158,7 @@ import {
ConfirmLeaveModal,
DropdownSelect,
injectProjectPageContext,
StyledInput,
Input,
UnsavedChangesPopup,
usePageLeaveSafety,
useSavable,
@@ -142,7 +142,7 @@
v-tooltip="`Link includes a domain which isn't common for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<StyledInput
<Input
id="project-issue-tracker"
v-model="issuesUrl"
type="url"
@@ -176,7 +176,7 @@
v-tooltip="`Link includes a domain which isn't common for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<StyledInput
<Input
id="project-source-code"
v-model="sourceUrl"
type="url"
@@ -205,7 +205,7 @@
v-tooltip="`Discord invites are not appropriate for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<StyledInput
<Input
id="project-wiki-page"
v-model="wikiUrl"
type="url"
@@ -229,7 +229,7 @@
v-tooltip="`You're using a link which isn't common for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<StyledInput
<Input
id="project-discord-invite"
v-model="discordUrl"
type="url"
@@ -250,7 +250,7 @@
:key="`donation-link-${index}`"
class="input-group donation-link-group"
>
<StyledInput
<Input
v-model="donationLink.url"
type="url"
:maxlength="2048"
@@ -292,7 +292,7 @@ import {
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
StyledInput,
Input,
} from '@modrinth/ui'
const tags = useGeneratedState()
@@ -33,7 +33,7 @@
</span>
</span>
<div class="input-group">
<StyledInput
<Input
id="username"
v-model="currentUsername"
placeholder="Username"
@@ -112,7 +112,7 @@
The title of the role that this member plays for this project.
</span>
</label>
<StyledInput
<Input
:id="`member-${allTeamMembers[index].user.username}-role`"
v-model="allTeamMembers[index].role"
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
@@ -126,7 +126,7 @@
this project's revenue goes to this member.
</span>
</label>
<StyledInput
<Input
:id="`member-${allTeamMembers[index].user.username}-monetization-weight`"
v-model="allTeamMembers[index].payouts_split"
type="number"
@@ -378,7 +378,7 @@
The title of the role that this member plays for this project.
</span>
</label>
<StyledInput
<Input
:id="`member-${allOrgMembers[index].user.username}-role`"
v-model="allOrgMembers[index].role"
:disabled="
@@ -395,7 +395,7 @@
this project's revenue goes to this member.
</span>
</label>
<StyledInput
<Input
:id="`member-${allOrgMembers[index].user.username}-monetization-weight`"
v-model="allOrgMembers[index].payouts_split"
type="number"
@@ -568,7 +568,7 @@ import {
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
StyledInput,
Input,
Toggle,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
@@ -29,8 +29,8 @@ import {
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
Input,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { isStaff } from '@modrinth/utils'
@@ -563,7 +563,7 @@ function dismissInfoBanner() {
</template>
<div class="flex flex-col gap-2">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_auto_auto_auto]">
<StyledInput
<Input
v-model="searchQuery"
type="text"
autocomplete="off"
@@ -62,7 +62,7 @@
}
"
>
<StyledInput
<Input
id="java-address"
v-model="javaAddress"
:placeholder="formatMessage(messages.enterAddressPlaceholder)"
@@ -142,7 +142,7 @@
</span>
</label>
<div class="mt-2 flex items-center gap-2">
<StyledInput
<Input
id="bedrock-address"
v-model="bedrockAddress"
:placeholder="formatMessage(messages.enterAddressPlaceholder)"
@@ -177,12 +177,12 @@ import {
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
Input,
IntlFormatted,
MultiSelect,
normalizeChildren,
SERVER_LANGUAGES,
SERVER_REGIONS,
StyledInput,
UnsavedChangesPopup,
usePageLeaveSafety,
useVIntl,
+2 -2
View File
@@ -19,7 +19,7 @@
>
<h1 class="m-0 grow text-2xl font-extrabold">Manage affiliate links</h1>
<div class="flex items-center gap-2">
<StyledInput
<Input
v-model="filterQuery"
:icon="SearchIcon"
type="text"
@@ -94,7 +94,7 @@ import {
ConfirmModal,
injectModrinthClient,
injectNotificationManager,
StyledInput,
Input,
} from '@modrinth/ui'
import type { User } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
@@ -18,7 +18,7 @@
<div class="flex flex-col gap-5" @submit.prevent="saveEvent">
<div class="flex flex-col gap-2">
<span class="label__title font-semibold">Title</span>
<StyledInput
<Input
id="analytics-event-title"
ref="titleInput"
v-model="form.title"
@@ -48,7 +48,7 @@
</ButtonStyled>
</div>
<div class="flex items-center gap-2">
<StyledInput
<Input
id="analytics-event-link"
v-model="form.announcementUrl"
type="url"
@@ -129,7 +129,7 @@
<h1 class="m-0 text-2xl font-extrabold text-contrast">Analytics Events</h1>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<StyledInput
<Input
v-model="searchQuery"
:icon="SearchIcon"
type="search"
@@ -245,11 +245,11 @@ import {
DatePicker,
injectModrinthClient,
injectNotificationManager,
Input,
MultiSelect,
type MultiSelectOption,
NewModal,
type SortDirection,
StyledInput,
Table,
type TableColumn,
} from '@modrinth/ui'
@@ -319,7 +319,7 @@ const allMetricKinds = metricKindOptions.map((option) => option.value)
const deleteEventModal = ref<InstanceType<typeof ConfirmModal> | null>(null)
const eventModal = ref<InstanceType<typeof NewModal> | null>(null)
const titleInput = ref<InstanceType<typeof StyledInput> | null>(null)
const titleInput = ref<InstanceType<typeof Input> | null>(null)
const searchQuery = ref('')
const sortColumn = ref<EventColumnKey | undefined>('date')
const sortDirection = ref<SortDirection>('desc')
@@ -485,9 +485,9 @@ async function saveEvent() {
const payload = buildEventPayload()
if (modalMode.value === 'edit' && editingEventId.value !== null) {
await client.labrinth.analytics_v3.editEvent(editingEventId.value, payload)
await client.labrinth.analytics_internal.editEvent(editingEventId.value, payload)
} else {
await client.labrinth.analytics_v3.createEvent(payload)
await client.labrinth.analytics_internal.createEvent(payload)
}
await queryClient.invalidateQueries({ queryKey: analyticsEventsQueryKey })
@@ -528,7 +528,7 @@ async function deleteEvent(eventId: Labrinth.Analytics.v3.AnalyticsEventId) {
setDeletingEvent(eventId, true)
try {
await client.labrinth.analytics_v3.deleteEvent(eventId)
await client.labrinth.analytics_internal.deleteEvent(eventId)
await queryClient.invalidateQueries({ queryKey: analyticsEventsQueryKey })
addNotification({
title: 'Analytics event deleted',
@@ -581,7 +581,7 @@ function commitAnnouncementUrl() {
committedAnnouncementUrl.value = form.value.announcementUrl
}
function buildEventPayload(): Labrinth.Analytics.v3.AnalyticsEventUpsert {
function buildEventPayload(): Labrinth.Analytics.Internal.AnalyticsEventUpsert {
const selectedRange = getEventFormDateRange()
if (!selectedRange) {
throw new Error('Select a valid start and end date')
@@ -30,7 +30,7 @@
{{ selectedCharge.net }})
</span>
</label>
<StyledInput id="amount" v-model="refundAmount" type="number" autocomplete="off" />
<Input id="amount" v-model="refundAmount" type="number" autocomplete="off" />
</div>
<div class="flex flex-col gap-2">
<label for="unprovision" class="flex flex-col gap-1">
@@ -107,7 +107,7 @@
<span class="text-lg font-semibold text-contrast">Days to credit</span>
<span>Enter the number of days to add to the next due date.</span>
</label>
<StyledInput id="days" v-model="creditDays" type="number" :min="1" autocomplete="off" />
<Input id="days" v-model="creditDays" type="number" :min="1" autocomplete="off" />
</div>
<div class="flex flex-col gap-2">
<label for="sendEmail" class="flex flex-col gap-1">
@@ -232,8 +232,8 @@ import {
DropdownSelect,
injectModrinthClient,
injectNotificationManager,
Input,
NewModal,
StyledInput,
Toggle,
useFormatDateTime,
useRelativeTime,
+2 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { CopyIcon, LibraryIcon, PlayIcon, SearchIcon } from '@modrinth/assets'
import { ButtonStyled, Card, StyledInput } from '@modrinth/ui'
import { ButtonStyled, Card, Input } from '@modrinth/ui'
import { computed, onMounted, ref } from 'vue'
import docs from '~/templates/docs'
@@ -51,7 +51,7 @@ onMounted(() => {
<div class="normal-page__content">
<Card class="mb-6 flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-3">
<StyledInput
<Input
id="doc-search"
v-model="query"
type="search"
+3 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { CopyIcon, LibraryIcon, PlayIcon, SearchIcon } from '@modrinth/assets'
import { ButtonStyled, NewModal, StyledInput } from '@modrinth/ui'
import { ButtonStyled, Input, NewModal } from '@modrinth/ui'
import { computed, onMounted, ref } from 'vue'
import emails from '~/templates/emails'
@@ -153,7 +153,7 @@ onMounted(() => {
class="flex flex-col"
>
<span class="label__title">{{ variable }}</span>
<StyledInput
<Input
:id="`preview-${variable}`"
v-model="variableValues[variable]"
type="text"
@@ -191,7 +191,7 @@ onMounted(() => {
</NewModal>
<div class="normal-page__content">
<div class="flex flex-wrap items-center gap-3">
<StyledInput
<Input
id="email-search"
v-model="query"
type="search"
@@ -24,7 +24,7 @@
<label for="notice-title" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Title </span>
</label>
<StyledInput
<Input
id="notice-title"
v-model="newNoticeTitle"
placeholder="E.g. Maintenance"
@@ -38,20 +38,14 @@
<span class="text-brand-red">*</span>
</span>
</label>
<StyledInput
<Input
v-if="newNoticeSurvey"
id="notice-message"
v-model="newNoticeMessage"
placeholder="E.g. rXGtq2"
autocomplete="off"
/>
<StyledInput
v-else
id="notice-message"
v-model="newNoticeMessage"
multiline
wrapper-class="h-32"
/>
<Textarea v-else id="notice-message" v-model="newNoticeMessage" wrapper-class="h-32" />
</div>
<div v-if="!newNoticeSurvey" class="flex items-center justify-between gap-2">
<label for="dismissable-toggle" class="flex flex-col gap-1">
@@ -65,7 +59,7 @@
<span class="text-lg font-semibold text-contrast"> Announcement date </span>
<span>Leave blank for notice to be available immediately.</span>
</label>
<StyledInput
<DateInput
id="scheduled-date"
v-model="newNoticeScheduledDate"
type="datetime-local"
@@ -77,7 +71,7 @@
<span class="text-lg font-semibold text-contrast"> Expiration date </span>
<span>The notice will automatically be deleted after this date.</span>
</label>
<StyledInput
<DateInput
id="expiration-date"
v-model="newNoticeExpiresDate"
type="datetime-local"
@@ -267,13 +261,15 @@ import {
Combobox,
commonMessages,
CopyCode,
DateInput,
defineMessages,
injectModrinthClient,
injectNotificationManager,
Input,
NewModal,
ServerNotice,
StyledInput,
TagItem,
Textarea,
Toggle,
useFormatDateTime,
useRelativeTime,
+2 -2
View File
@@ -10,7 +10,7 @@
<span class="text-brand-red">*</span>
</span>
</label>
<StyledInput
<Input
id="name"
v-model="userEmail"
type="email"
@@ -33,7 +33,7 @@
</template>
<script setup lang="ts">
import { MailIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
import { ButtonStyled, injectNotificationManager, Input } from '@modrinth/ui'
const { addNotification } = injectNotificationManager()
@@ -20,7 +20,7 @@
<label class="text-md font-semibold text-contrast" for="email">
{{ formatMessage(commonMessages.emailLabel) }}
</label>
<StyledInput
<Input
id="email"
v-model="email"
:icon="MailIcon"
@@ -61,7 +61,7 @@
<div class="flex flex-col gap-2.5">
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
<StyledInput
<Input
id="password"
v-model="newPassword"
:icon="KeyIcon"
@@ -74,7 +74,7 @@
<label for="confirm-password" hidden>
{{ formatMessage(commonMessages.passwordLabel) }}
</label>
<StyledInput
<Input
id="confirm-password"
v-model="confirmNewPassword"
:icon="KeyIcon"
@@ -102,7 +102,7 @@ import {
defineMessages,
injectModrinthClient,
injectNotificationManager,
StyledInput,
Input,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
@@ -80,7 +80,7 @@
<label class="mb-2 block text-lg font-semibold text-contrast" for="collection-title">
{{ formatMessage(commonMessages.titleLabel) }}
</label>
<StyledInput
<Input
id="collection-title"
v-model="current.name"
:maxlength="255"
@@ -94,10 +94,9 @@
>
{{ formatMessage(commonMessages.descriptionLabel) }}
</label>
<StyledInput
<Textarea
id="collection-description"
v-model="current.description"
multiline
:maxlength="255"
wrapper-class="h-24"
/>
@@ -412,6 +411,7 @@ import {
HorizontalRule,
injectModrinthClient,
injectNotificationManager,
Input,
IntlFormatted,
NavTabs,
NewModal,
@@ -422,7 +422,7 @@ import {
ProjectCardList,
RadioButtons,
SidebarCard,
StyledInput,
Textarea,
useCompactNumber,
useFormatDateTime,
useRelativeTime,
@@ -18,7 +18,7 @@
{{ formatMessage(messages.yourAffiliateLinks) }}
</h1>
<div class="flex items-center gap-2">
<StyledInput
<Input
v-model="filterQuery"
:icon="SearchIcon"
type="text"
@@ -68,7 +68,7 @@ import {
defineMessages,
injectModrinthClient,
injectNotificationManager,
StyledInput,
Input,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
@@ -4,7 +4,7 @@
<h2 class="text-2xl">{{ formatMessage(commonMessages.collectionsLabel) }}</h2>
<div class="mb-3 flex flex-col gap-3">
<label for="search-input" hidden>{{ formatMessage(messages.searchInputLabel) }}</label>
<StyledInput
<Input
id="search-input"
v-model="filterQuery"
:icon="SearchIcon"
@@ -149,7 +149,7 @@ import {
defineMessages,
DropdownSelect,
injectModrinthClient,
StyledInput,
Input,
useCompactNumber,
useVIntl,
} from '@modrinth/ui'
@@ -8,7 +8,7 @@
<span class="label__title">{{ formatMessage(messages.issueTrackerLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="issue-tracker-input"
v-model="editLinks.issues.val"
:disabled="editLinks.issues.clear"
@@ -33,7 +33,7 @@
<span class="label__title">{{ formatMessage(messages.sourceCodeLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="source-code-input"
v-model="editLinks.source.val"
:disabled="editLinks.source.clear"
@@ -58,7 +58,7 @@
<span class="label__title">{{ formatMessage(messages.wikiPageLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="wiki-page-input"
v-model="editLinks.wiki.val"
:disabled="editLinks.wiki.clear"
@@ -86,7 +86,7 @@
<span class="label__title">{{ formatMessage(messages.discordInviteLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="discord-invite-input"
v-model="editLinks.discord.val"
:disabled="editLinks.discord.clear"
@@ -364,10 +364,10 @@ import {
defineMessages,
FloatingActionBar,
injectNotificationManager,
Input,
IntlFormatted,
NewModal,
ProjectStatusBadge,
StyledInput,
Table,
useVIntl,
} from '@modrinth/ui'
+2 -2
View File
@@ -94,7 +94,7 @@
<label class="hidden" for="search">{{
formatMessage(commonMessages.searchLabel)
}}</label>
<StyledInput
<Input
id="search"
v-model="searchQuery"
:icon="SearchIcon"
@@ -449,9 +449,9 @@ import {
defineMessages,
DropdownSelect,
injectModrinthClient,
Input,
IntlFormatted,
ProjectCard,
StyledInput,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
@@ -2,13 +2,13 @@
<NewModal ref="editModal" header="Edit external project">
<form class="flex flex-col gap-2" @submit.prevent="saveExternalProjectEdit">
<label class="font-semibold text-contrast" for="edit-form-title">Title</label>
<StyledInput id="edit-form-title" v-model="editForm.title" type="text" />
<Input id="edit-form-title" v-model="editForm.title" type="text" />
<label class="mt-2 font-semibold text-contrast" for="edit-form-link">Link</label>
<StyledInput id="edit-form-link" v-model="editForm.link" type="text" />
<Input id="edit-form-link" v-model="editForm.link" type="text" />
<label class="mt-2 font-semibold text-contrast" for="edit-form-cf-id">
CurseForge project ID
</label>
<StyledInput id="edit-form-cf-id" v-model="editForm.flameProjectId" type="text" />
<Input id="edit-form-cf-id" v-model="editForm.flameProjectId" type="text" />
<label class="mt-2 font-semibold text-contrast" for="edit-form-status">Allowed?</label>
<Combobox
id="edit-form-status"
@@ -17,22 +17,13 @@
class="!w-full"
/>
<label class="mt-2 font-semibold text-contrast" for="edit-form-proof">Proof</label>
<StyledInput
id="edit-form-proof"
v-model="editForm.proof"
type="text"
multiline
resize="both"
class="w-[30rem]"
/>
<Textarea id="edit-form-proof" v-model="editForm.proof" resize="both" class="w-[30rem]" />
<label class="mt-2 font-semibold text-contrast" for="edit-form-exceptions">
Exceptions / notes
</label>
<StyledInput
<Textarea
id="edit-form-exceptions"
v-model="editForm.exceptions"
type="text"
multiline
resize="both"
class="w-[30rem]"
/>
@@ -50,7 +41,7 @@
</NewModal>
<div>
<form class="flex gap-2" @submit.prevent="executeSearch">
<StyledInput
<Input
v-model="query"
:icon="SearchIcon"
type="text"
@@ -139,8 +130,9 @@ import {
externalProjectLicenseStatusMessages,
ExternalProjectLookupCard,
injectModrinthClient,
Input,
NewModal,
StyledInput,
Textarea,
useVIntl,
} from '@modrinth/ui'
+2 -2
View File
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col justify-between gap-3 lg:flex-row">
<StyledInput
<Input
v-model="query"
:icon="SearchIcon"
type="text"
@@ -148,8 +148,8 @@ import {
EmptyState,
injectModrinthClient,
injectNotificationManager,
Input,
Pagination,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col justify-between gap-3 lg:flex-row">
<StyledInput
<Input
v-model="query"
:icon="SearchIcon"
type="text"
@@ -210,10 +210,10 @@ import {
commonMessages,
FloatingPanel,
injectModrinthClient,
Input,
MultiSelect,
type MultiSelectItem,
Pagination,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import Fuse from 'fuse.js'
@@ -14,8 +14,8 @@ import {
commonMessages,
FloatingPanel,
injectModrinthClient,
Input,
Pagination,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
@@ -582,7 +582,7 @@ watch(totalPages, (pages) => {
/> -->
<div class="flex flex-col justify-between gap-2 lg:flex-row">
<StyledInput
<Input
v-model="query"
:icon="SearchIcon"
type="text"
@@ -6,7 +6,8 @@ import {
ConfirmModal,
FileInput,
injectNotificationManager,
StyledInput,
Input,
Textarea,
UnsavedChangesPopup,
useSavable,
} from '@modrinth/ui'
@@ -188,7 +189,7 @@ const onDeleteOrganization = useClientTry(async () => {
<label for="project-name">
<span class="label__title">Name</span>
</label>
<StyledInput
<Input
id="project-name"
v-model="current.name"
:maxlength="2048"
@@ -200,7 +201,7 @@ const onDeleteOrganization = useClientTry(async () => {
</label>
<div class="text-input-wrapper">
<div class="text-input-wrapper__before">https://modrinth.com/organization/</div>
<StyledInput
<Input
id="project-slug"
v-model="current.slug"
:maxlength="64"
@@ -212,10 +213,9 @@ const onDeleteOrganization = useClientTry(async () => {
<label for="project-summary">
<span class="label__title">Summary</span>
</label>
<StyledInput
<Textarea
id="project-summary"
v-model="current.summary"
multiline
:maxlength="256"
:disabled="!hasPermission"
resize="vertical"
@@ -30,7 +30,7 @@
</span>
</span>
<div class="input-group">
<StyledInput
<Input
id="username"
v-model="currentUsername"
placeholder="Username"
@@ -118,7 +118,7 @@
The title of the role that this member plays for this organization.
</span>
</label>
<StyledInput
<Input
:id="`member-${member.user.id}-role`"
v-model="member.role"
:disabled="
@@ -137,7 +137,7 @@
the organization projects' revenue goes to this member.
</span>
</label>
<StyledInput
<Input
:id="`member-${member.user.id}-monetization-weight`"
v-model="member.payouts_split"
type="number"
@@ -249,7 +249,7 @@ import {
ButtonStyled,
Checkbox,
injectNotificationManager,
StyledInput,
Input,
} from '@modrinth/ui'
import { nextTick, ref } from 'vue'
@@ -8,7 +8,7 @@
<span class="label__title">{{ formatMessage(messages.issueTrackerLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="issue-tracker-input"
v-model="editLinks.issues.val"
:disabled="editLinks.issues.clear"
@@ -33,7 +33,7 @@
<span class="label__title">{{ formatMessage(messages.sourceCodeLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="source-code-input"
v-model="editLinks.source.val"
:disabled="editLinks.source.clear"
@@ -58,7 +58,7 @@
<span class="label__title">{{ formatMessage(messages.wikiPageLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="wiki-page-input"
v-model="editLinks.wiki.val"
:disabled="editLinks.wiki.clear"
@@ -86,7 +86,7 @@
<span class="label__title">{{ formatMessage(messages.discordInviteLabel) }}</span>
</label>
<div class="flex gap-2">
<StyledInput
<Input
id="discord-invite-input"
v-model="editLinks.discord.val"
:disabled="editLinks.discord.clear"
@@ -365,10 +365,10 @@ import {
FloatingActionBar,
injectModrinthClient,
injectNotificationManager,
Input,
IntlFormatted,
NewModal,
ProjectStatusBadge,
StyledInput,
Table,
useVIntl,
} from '@modrinth/ui'
+2 -2
View File
@@ -136,7 +136,7 @@
}}
</span>
<div class="flex gap-4">
<StyledInput
<Input
id="report-item-id"
v-model="reportItemID"
placeholder="ex: Dc7EYhxG"
@@ -293,12 +293,12 @@ import {
defineMessages,
formatReportItemType,
injectNotificationManager,
Input,
IntlFormatted,
MarkdownEditor,
type MessageDescriptor,
RadialHeader,
RadioButtons,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import type { Project, Report, User, Version } from '@modrinth/utils'
+7 -7
View File
@@ -24,7 +24,7 @@
formatMessage(messages.emailAddressLabel)
}}</span>
</label>
<StyledInput
<Input
id="email-input"
v-model="email"
:maxlength="2048"
@@ -72,7 +72,7 @@
formatMessage(messages.oldPasswordLabel)
}}</span>
</label>
<StyledInput
<Input
id="old-password"
v-model="oldPassword"
:maxlength="2048"
@@ -96,7 +96,7 @@
formatMessage(messages.newPasswordLabel)
}}</span></label
>
<StyledInput
<Input
id="new-password"
v-model="newPassword"
:maxlength="2048"
@@ -112,7 +112,7 @@
formatMessage(messages.confirmNewPasswordLabel)
}}</span>
</label>
<StyledInput
<Input
id="confirm-new-password"
v-model="confirmNewPassword"
:maxlength="2048"
@@ -191,7 +191,7 @@
formatMessage(messages.twoFactorEnterCodeDescription)
}}</span>
</label>
<StyledInput
<Input
id="two-factor-code"
v-model="twoFactorCode"
:maxlength="11"
@@ -260,7 +260,7 @@
formatMessage(messages.twoFactorVerifyCodeDescription)
}}</span>
</label>
<StyledInput
<Input
id="verify-code"
v-model="twoFactorCode"
:maxlength="6"
@@ -499,9 +499,9 @@ import {
ConfirmModal,
defineMessages,
injectNotificationManager,
Input,
IntlFormatted,
NewModal,
StyledInput,
Table,
useVIntl,
} from '@modrinth/ui'
@@ -12,7 +12,7 @@
<label for="app-name" class="m-0 mb-2 text-lg font-semibold text-contrast">
{{ formatMessage(messages.nameLabel) }}
</label>
<StyledInput
<Input
id="app-name"
v-model="name"
:maxlength="2048"
@@ -43,7 +43,7 @@
<label v-if="editingId" for="app-url" class="mb-2 mt-4 text-lg font-semibold text-contrast">
{{ formatMessage(messages.urlLabel) }}
</label>
<StyledInput
<Input
v-if="editingId"
id="app-url"
v-model="url"
@@ -59,11 +59,10 @@
>
{{ formatMessage(messages.descriptionLabel) }}
</label>
<StyledInput
<Textarea
v-if="editingId"
id="app-description"
v-model="description"
multiline
:maxlength="255"
autocomplete="off"
:placeholder="formatMessage(messages.descriptionPlaceholder)"
@@ -94,7 +93,7 @@
<div class="uri-input-list">
<div v-for="(_, index) in redirectUris" :key="index">
<div class="input-group url-input-group-fixes">
<StyledInput
<Input
v-model="redirectUris[index]"
:maxlength="2048"
type="url"
@@ -262,10 +261,11 @@ import {
FileInput,
injectModrinthClient,
injectNotificationManager,
Input,
IntlFormatted,
NewModal,
normalizeChildren,
StyledInput,
Textarea,
useFormatDateTime,
useVIntl,
} from '@modrinth/ui'
+2 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { SearchIcon } from '@modrinth/assets'
import { ButtonStyled, StyledInput, Toggle } from '@modrinth/ui'
import { ButtonStyled, Input, Toggle } from '@modrinth/ui'
import Fuse from 'fuse.js'
import { computed, ref, shallowReactive } from 'vue'
@@ -42,7 +42,7 @@ useSeoMeta({
<template>
<div class="mb-2">
<StyledInput
<Input
v-model="searchQuery"
type="search"
:icon="SearchIcon"
+2 -2
View File
@@ -21,7 +21,7 @@
<label for="pat-name">
<span class="font-semibold">{{ formatMessage(createModalMessages.nameLabel) }}</span>
</label>
<StyledInput
<Input
id="pat-name"
v-model="name"
:maxlength="2048"
@@ -216,9 +216,9 @@ import {
defineMessages,
injectModrinthClient,
injectNotificationManager,
Input,
IntlFormatted,
NewModal,
StyledInput,
useFormatDateTime,
useRelativeTime,
useVIntl,
+3
View File
@@ -168,6 +168,9 @@ pub async fn update_bank_balances(pool: PgPool) -> eyre::Result<()> {
pub async fn run_migrations() -> eyre::Result<()> {
database::check_for_migrations().await?;
crate::clickhouse::run_migrations()
.await
.wrap_err("failed to run ClickHouse migrations")?;
Ok(())
}
+34 -19
View File
@@ -19,29 +19,44 @@ pub async fn init_client() -> clickhouse::error::Result<clickhouse::Client> {
pub async fn init_client_with_database(
database: &str,
) -> clickhouse::error::Result<clickhouse::Client> {
const MINECRAFT_JAVA_SERVER_PINGS: &str = server_ping::CLICKHOUSE_TABLE;
Ok(connect()?.with_database(database))
}
let client = {
let https_connector = HttpsConnectorBuilder::new()
.with_native_roots()?
.https_or_http()
.enable_all_versions()
.build();
let hyper_client =
hyper_util::client::legacy::Client::builder(TokioExecutor::new())
.build(https_connector);
fn connect() -> clickhouse::error::Result<clickhouse::Client> {
let https_connector = HttpsConnectorBuilder::new()
.with_native_roots()?
.https_or_http()
.enable_all_versions()
.build();
let hyper_client =
hyper_util::client::legacy::Client::builder(TokioExecutor::new())
.build(https_connector);
clickhouse::Client::with_http_client(hyper_client)
.with_url(&ENV.CLICKHOUSE_URL)
.with_user(&ENV.CLICKHOUSE_USER)
.with_password(&ENV.CLICKHOUSE_PASSWORD)
.with_validation(false)
};
Ok(clickhouse::Client::with_http_client(hyper_client)
.with_url(&ENV.CLICKHOUSE_URL)
.with_user(&ENV.CLICKHOUSE_USER)
.with_password(&ENV.CLICKHOUSE_PASSWORD)
.with_validation(false))
}
client
#[cfg(feature = "test")]
pub async fn create_database(database: &str) -> clickhouse::error::Result<()> {
connect()?
.query(&format!("CREATE DATABASE IF NOT EXISTS {database}"))
.execute()
.await?;
.await
}
pub async fn run_migrations() -> clickhouse::error::Result<()> {
run_migrations_on_database(&ENV.CLICKHOUSE_DATABASE).await
}
pub async fn run_migrations_on_database(
database: &str,
) -> clickhouse::error::Result<()> {
const MINECRAFT_JAVA_SERVER_PINGS: &str = server_ping::CLICKHOUSE_TABLE;
let client = connect()?;
let clickhouse_replicated = ENV.CLICKHOUSE_REPLICATED;
let cluster_line = if clickhouse_replicated {
@@ -270,5 +285,5 @@ pub async fn init_client_with_database(
.execute()
.await?;
Ok(client.with_database(database))
Ok(())
}
+1 -1
View File
@@ -98,7 +98,7 @@ async fn app() -> std::io::Result<()> {
info!("Starting labrinth on {}", &ENV.BIND_ADDR);
if !args.no_migrations {
database::check_for_migrations()
labrinth::background_task::run_migrations()
.await
.expect("An error occurred while running migrations.");
}
@@ -0,0 +1,192 @@
use actix_web::{HttpRequest, delete, patch, post, web};
use chrono::{DateTime, Utc};
use eyre::eyre;
use serde::{Deserialize, Serialize};
use xredis::RedisPool;
use crate::{
auth::get_user_from_headers,
database::{
PgPool,
models::{
DBAnalyticsEvent, DBAnalyticsEventId, generate_analytics_event_id,
},
},
models::{
ids::AnalyticsEventId,
pats::Scopes,
v3::analytics_event::{AnalyticsEvent, AnalyticsEventMeta},
},
queue::session::AuthQueue,
routes::ApiError,
util::error::Context,
};
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(analytics_event_create)
.service(analytics_event_edit)
.service(analytics_event_delete);
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct AnalyticsEventUpsert {
#[serde(flatten)]
pub meta: AnalyticsEventMeta,
pub starts: DateTime<Utc>,
pub ends: DateTime<Utc>,
}
/// Create an analytics event.
#[utoipa::path(
context_path = "/analytics-event",
tag = "analytics events", responses((status = OK, body = AnalyticsEvent))
)]
#[post("")]
pub async fn analytics_event_create(
req: HttpRequest,
event: web::Json<AnalyticsEventUpsert>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<AnalyticsEvent>, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?
.1;
if !user.role.is_admin() {
return Err(ApiError::Auth(eyre!(
"you do not have permission to manage analytics events"
)));
}
let mut transaction = pool
.begin()
.await
.wrap_internal_err("failed to begin transaction")?;
let id = generate_analytics_event_id(&mut transaction)
.await
.wrap_internal_err("failed to generate analytics event ID")?;
let event = DBAnalyticsEvent {
id,
meta: event.meta.clone(),
starts: event.starts,
ends: event.ends,
};
event
.insert(&mut transaction)
.await
.wrap_internal_err("failed to insert analytics event")?;
transaction
.commit()
.await
.wrap_internal_err("failed to commit transaction")?;
DBAnalyticsEvent::clear_cache(&redis)
.await
.wrap_internal_err("failed to clear analytics event cache")?;
Ok(web::Json(event.into()))
}
/// Update an analytics event.
#[utoipa::path(
context_path = "/analytics-event",
tag = "analytics events", responses((status = OK, body = AnalyticsEvent))
)]
#[patch("/{id}")]
pub async fn analytics_event_edit(
req: HttpRequest,
id: web::Path<(AnalyticsEventId,)>,
event: web::Json<AnalyticsEventUpsert>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<AnalyticsEvent>, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?
.1;
if !user.role.is_admin() {
return Err(ApiError::Auth(eyre!(
"you do not have permission to manage analytics events"
)));
}
let event = DBAnalyticsEvent {
id: DBAnalyticsEventId::from(id.into_inner().0),
meta: event.meta.clone(),
starts: event.starts,
ends: event.ends,
};
let updated = event
.update(&**pool)
.await
.wrap_internal_err("failed to update analytics event")?;
if !updated {
return Err(ApiError::NotFound);
}
DBAnalyticsEvent::clear_cache(&redis)
.await
.wrap_internal_err("failed to clear analytics event cache")?;
Ok(web::Json(event.into()))
}
/// Delete an analytics event.
#[utoipa::path(
context_path = "/analytics-event",
tag = "analytics events", responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
pub async fn analytics_event_delete(
req: HttpRequest,
id: web::Path<(AnalyticsEventId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<(), ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?
.1;
if !user.role.is_admin() {
return Err(ApiError::Auth(eyre!(
"you do not have permission to manage analytics events"
)));
}
let deleted = DBAnalyticsEvent::remove(
DBAnalyticsEventId::from(id.into_inner().0),
&**pool,
)
.await
.wrap_internal_err("failed to delete analytics event")?;
if !deleted {
return Err(ApiError::NotFound);
}
DBAnalyticsEvent::clear_cache(&redis)
.await
.wrap_internal_err("failed to clear analytics event cache")?;
Ok(())
}
+8 -9
View File
@@ -1,5 +1,6 @@
pub mod admin;
pub mod affiliate;
pub mod analytics_event;
pub mod attribution;
pub mod billing;
pub mod blocked_users;
@@ -35,6 +36,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.configure(flows::config)
.configure(pats::config)
.configure(oauth_clients::config)
.service(
web::scope("/analytics-event")
.configure(analytics_event::config),
)
.service(web::scope("/moderation").configure(moderation::config))
.service(web::scope("/affiliate").configure(affiliate::config))
.service(web::scope("/campaign").configure(campaign::config))
@@ -50,11 +55,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.configure(medal::config)
.configure(mural::config)
.configure(statuses::config),
)
.service(
web::scope("/v3/analytics-event")
.wrap(default_cors())
.configure(super::v3::analytics_event::config),
);
}
@@ -180,10 +180,9 @@ pub fn config(cfg: &mut web::ServiceConfig) {
medal::redeem,
mural::get_bank_details,
statuses::ws_init,
super::v3::analytics_event::analytics_events_get,
super::v3::analytics_event::analytics_event_create,
super::v3::analytics_event::analytics_event_edit,
super::v3::analytics_event::analytics_event_delete,
analytics_event::analytics_event_create,
analytics_event::analytics_event_edit,
analytics_event::analytics_event_delete,
),
modifiers(&InternalPathModifier, &SecurityAddon)
)]
+6 -187
View File
@@ -1,48 +1,22 @@
use actix_web::{HttpRequest, delete, get, patch, post, web};
use chrono::{DateTime, Utc};
use eyre::eyre;
use serde::{Deserialize, Serialize};
use actix_web::{get, web};
use xredis::RedisPool;
use crate::{
auth::get_user_from_headers,
database::{
PgPool,
models::{
DBAnalyticsEvent, DBAnalyticsEventId, generate_analytics_event_id,
},
},
models::{
ids::AnalyticsEventId,
pats::Scopes,
v3::analytics_event::{AnalyticsEvent, AnalyticsEventMeta},
},
queue::session::AuthQueue,
database::{PgPool, models::DBAnalyticsEvent},
models::v3::analytics_event::AnalyticsEvent,
routes::ApiError,
util::error::Context,
};
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(analytics_events_get)
.service(analytics_event_create)
.service(analytics_event_edit)
.service(analytics_event_delete);
cfg.service(analytics_events_get);
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct AnalyticsEventUpsert {
#[serde(flatten)]
pub meta: AnalyticsEventMeta,
pub starts: DateTime<Utc>,
pub ends: DateTime<Utc>,
}
/// List analytics events.
/// List analytics events.
#[utoipa::path(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = OK, body = Vec<AnalyticsEvent>))
)]
#[get("")]
#[get("/analytics-event")]
pub async fn analytics_events_get(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -56,158 +30,3 @@ pub async fn analytics_events_get(
Ok(web::Json(events))
}
/// Create an analytics event.
#[utoipa::path(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = OK, body = AnalyticsEvent))
)]
#[post("")]
pub async fn analytics_event_create(
req: HttpRequest,
event: web::Json<AnalyticsEventUpsert>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<AnalyticsEvent>, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?
.1;
if !user.role.is_admin() {
return Err(ApiError::Auth(eyre!(
"you do not have permission to manage analytics events"
)));
}
let mut transaction = pool
.begin()
.await
.wrap_internal_err("failed to begin transaction")?;
let id = generate_analytics_event_id(&mut transaction)
.await
.wrap_internal_err("failed to generate analytics event ID")?;
let event = DBAnalyticsEvent {
id,
meta: event.meta.clone(),
starts: event.starts,
ends: event.ends,
};
event
.insert(&mut transaction)
.await
.wrap_internal_err("failed to insert analytics event")?;
transaction
.commit()
.await
.wrap_internal_err("failed to commit transaction")?;
DBAnalyticsEvent::clear_cache(&redis)
.await
.wrap_internal_err("failed to clear analytics event cache")?;
Ok(web::Json(event.into()))
}
/// Update an analytics event.
#[utoipa::path(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = OK, body = AnalyticsEvent))
)]
#[patch("/{id}")]
pub async fn analytics_event_edit(
req: HttpRequest,
id: web::Path<(AnalyticsEventId,)>,
event: web::Json<AnalyticsEventUpsert>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<AnalyticsEvent>, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?
.1;
if !user.role.is_admin() {
return Err(ApiError::Auth(eyre!(
"you do not have permission to manage analytics events"
)));
}
let event = DBAnalyticsEvent {
id: DBAnalyticsEventId::from(id.into_inner().0),
meta: event.meta.clone(),
starts: event.starts,
ends: event.ends,
};
let updated = event
.update(&**pool)
.await
.wrap_internal_err("failed to update analytics event")?;
if !updated {
return Err(ApiError::NotFound);
}
DBAnalyticsEvent::clear_cache(&redis)
.await
.wrap_internal_err("failed to clear analytics event cache")?;
Ok(web::Json(event.into()))
}
/// Delete an analytics event.
#[utoipa::path(
context_path = "/v3/analytics-event",
tag = "v3 analytics", responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
pub async fn analytics_event_delete(
req: HttpRequest,
id: web::Path<(AnalyticsEventId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<(), ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?
.1;
if !user.role.is_admin() {
return Err(ApiError::Auth(eyre!(
"you do not have permission to manage analytics events"
)));
}
let deleted = DBAnalyticsEvent::remove(
DBAnalyticsEventId::from(id.into_inner().0),
&**pool,
)
.await
.wrap_internal_err("failed to delete analytics event")?;
if !deleted {
return Err(ApiError::NotFound);
}
DBAnalyticsEvent::clear_cache(&redis)
.await
.wrap_internal_err("failed to clear analytics event cache")?;
Ok(())
}
+2
View File
@@ -49,6 +49,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/v3")
.wrap(default_cors())
.configure(analytics_event::config)
.configure(limits::config)
.configure(collections::config)
.configure(images::config)
@@ -80,6 +81,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
description = include_str!("../../api_v3_description.md"),
),
paths(
analytics_event::analytics_events_get,
analytics_get::fetch_analytics,
analytics_get::facets::fetch_facets,
analytics_get::old::playtimes_get,
+4
View File
@@ -37,6 +37,10 @@ pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
let search_backend = db.search_backend.clone();
let file_host: Arc<dyn FileHost> = Arc::new(file_hosting::MockHost::new());
let file_host = web::Data::<dyn FileHost>::from(file_host);
clickhouse::create_database(&ENV.CLICKHOUSE_DATABASE)
.await
.unwrap();
clickhouse::run_migrations().await.unwrap();
let mut clickhouse = clickhouse::init_client().await.unwrap();
let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone());
+2
View File
@@ -19,6 +19,7 @@ import { KyrosLogsV1Module } from './kyros/logs/v1'
import { KyrosUploadSessionsV1Module } from './kyros/upload-sessions/v1'
import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth'
import { LabrinthAffiliateInternalModule } from './labrinth/affiliate/internal'
import { LabrinthAnalyticsInternalModule } from './labrinth/analytics/internal'
import { LabrinthAnalyticsV3Module } from './labrinth/analytics/v3'
import { LabrinthAttributionInternalModule } from './labrinth/attribution/internal'
import { LabrinthAuthInternalModule } from './labrinth/auth/internal'
@@ -97,6 +98,7 @@ export const MODULE_REGISTRY = {
kyros_logs_v1: KyrosLogsV1Module,
kyros_upload_sessions_v1: KyrosUploadSessionsV1Module,
labrinth_affiliate_internal: LabrinthAffiliateInternalModule,
labrinth_analytics_internal: LabrinthAnalyticsInternalModule,
labrinth_analytics_v3: LabrinthAnalyticsV3Module,
labrinth_auth_internal: LabrinthAuthInternalModule,
labrinth_auth_v2: LabrinthAuthV2Module,
@@ -0,0 +1,51 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Labrinth } from '../types'
export class LabrinthAnalyticsInternalModule extends AbstractModule {
public getModuleID(): string {
return 'labrinth_analytics_internal'
}
/**
* Create an analytics event.
* POST /_internal/analytics-event
*/
public async createEvent(
data: Labrinth.Analytics.Internal.AnalyticsEventUpsert,
): Promise<Labrinth.Analytics.v3.AnalyticsEvent> {
return this.client.request<Labrinth.Analytics.v3.AnalyticsEvent>('/analytics-event', {
api: 'labrinth',
version: 'internal',
method: 'POST',
body: data,
})
}
/**
* Edit an analytics event.
* PATCH /_internal/analytics-event/{id}
*/
public async editEvent(
id: Labrinth.Analytics.v3.AnalyticsEventId,
data: Labrinth.Analytics.Internal.AnalyticsEventUpsert,
): Promise<Labrinth.Analytics.v3.AnalyticsEvent> {
return this.client.request<Labrinth.Analytics.v3.AnalyticsEvent>(`/analytics-event/${id}`, {
api: 'labrinth',
version: 'internal',
method: 'PATCH',
body: data,
})
}
/**
* Delete an analytics event.
* DELETE /_internal/analytics-event/{id}
*/
public async deleteEvent(id: Labrinth.Analytics.v3.AnalyticsEventId): Promise<void> {
return this.client.request<void>(`/analytics-event/${id}`, {
api: 'labrinth',
version: 'internal',
method: 'DELETE',
})
}
}
@@ -70,47 +70,4 @@ export class LabrinthAnalyticsV3Module extends AbstractModule {
method: 'GET',
})
}
/**
* Create an analytics event.
* POST /v3/analytics-event
*/
public async createEvent(
data: Labrinth.Analytics.v3.AnalyticsEventUpsert,
): Promise<Labrinth.Analytics.v3.AnalyticsEvent> {
return this.client.request<Labrinth.Analytics.v3.AnalyticsEvent>('/analytics-event', {
api: 'labrinth',
version: 3,
method: 'POST',
body: data,
})
}
/**
* Edit an analytics event.
* PATCH /v3/analytics-event/{id}
*/
public async editEvent(
id: Labrinth.Analytics.v3.AnalyticsEventId,
data: Labrinth.Analytics.v3.AnalyticsEventUpsert,
): Promise<Labrinth.Analytics.v3.AnalyticsEvent> {
return this.client.request<Labrinth.Analytics.v3.AnalyticsEvent>(`/analytics-event/${id}`, {
api: 'labrinth',
version: 3,
method: 'PATCH',
body: data,
})
}
/**
* Delete an analytics event.
* DELETE /v3/analytics-event/{id}
*/
public async deleteEvent(id: Labrinth.Analytics.v3.AnalyticsEventId): Promise<void> {
return this.client.request<void>(`/analytics-event/${id}`, {
api: 'labrinth',
version: 3,
method: 'DELETE',
})
}
}
@@ -1,3 +1,4 @@
export * from './analytics/internal'
export * from './analytics/v3'
export * from './attribution/internal'
export * from './auth/internal'
@@ -463,6 +463,16 @@ export namespace Labrinth {
}
export namespace Analytics {
export namespace Internal {
export type AnalyticsEventUpsert = {
announcement_url: string | null
for_metric_kind: v3.AnalyticsEventMetricKind[] | null
title: string
ends: string
starts: string
}
}
export namespace v3 {
export type AnalyticsEventId = number
export type AnalyticsEventMetricKind = 'views' | 'revenue' | 'downloads' | 'playtime'
@@ -476,14 +486,6 @@ export namespace Labrinth {
starts: string
}
export type AnalyticsEventUpsert = {
announcement_url: string | null
for_metric_kind: AnalyticsEventMetricKind[] | null
title: string
ends: string
starts: string
}
export type FetchRequest = {
time_range: TimeRange
return_metrics: ReturnMetrics
+48 -13
View File
@@ -10,6 +10,41 @@ export type VersionEntry = {
}
const VERSIONS: VersionEntry[] = [
{
date: `2026-07-31T06:10:23+00:00`,
product: 'web',
body: `## Fixed
- Fixed randomly getting signed out of Modrinth account due to random non-auth related errors.`,
},
{
date: `2026-07-29T21:32:06+00:00`,
product: 'app',
version: '0.17.3',
body: `## Added
- Added button to create a new instance on the Library page.
## Changed
- Added a toggle to hide modpacks that are already installed.
- Added tooltip to installation settings button in installed modpack card.
## Fixed
- Improved error when shared instances reach a config file limit.
- When pushing updates to shared instances, will now scroll to the top when entering a sub-page.
- Fixed instance installations getting stuck when the instance is deleted.
- Fixed error when failing to fetch a shared instance icon.
- Installing content from Discover content will now install the latest that matches the game version and loader filters, not the absolute latest.`,
},
{
date: `2026-07-29T21:32:06+00:00`,
product: 'web',
body: `## Changed
- Incomplete current day revenue hides that day's line segment instead of showing a dip to \$0.
- Updated the modal for creating OAuth applications.
## Fixed
- Fixed dependencies in project download modal could give dependency with wrong Minecraft version.
- Fixed PATS page new generated tokens invalidating in the same session.`,
},
{
date: `2026-07-28T23:36:10+00:00`,
product: 'web',
@@ -71,21 +106,21 @@ const VERSIONS: VersionEntry[] = [
date: `2026-07-28T20:40:11+00:00`,
product: 'web',
body: `## Added
- You can now block users on their profile page. This prevents the user from sending you invites to shared instances and Modrinth Hosting server panels.
- Added new social settings page, where you can manage users you have blocked and in the future set who can send you friend requests and invitations to shared instances and Modrinth Hosting server panels.
- You can now block users on their profile page. This prevents the user from sending you invites to shared instances and Modrinth Hosting server panels.
- Added new social settings page, where you can manage users you have blocked and in the future set who can send you friend requests and invitations to shared instances and Modrinth Hosting server panels.
## Changed
- Cleaned up the layout of and renamed the Public profile settings page to Profile settings.
- Updated "Advanced" toggle filter design to be the same as the other filters, just with only an exclude button as the primary action.
- Updated translations. Want to help translate Modrinth's website? [Click here](https://translate.modrinth.com)
## Changed
- Cleaned up the layout of and renamed the Public profile settings page to Profile settings.
- Updated "Advanced" toggle filter design to be the same as the other filters, just with only an exclude button as the primary action.
- Updated translations. Want to help translate Modrinth's website? [Click here](https://translate.modrinth.com)
## Fixed
- Fixed profile pictures still being deleted after resetting a pending removal in Profile settings.
- Fixed shared instance reports not showing up in the dashboard's Reports page.
- Fixed shared instance report emails saying "Unknown" rather than the shared instance's name.
- Fixed the settings page for Authorized apps being broken.
- Broken Retro theme colors
- Improved vertical alignment of status indicators in notifications.`,
## Fixed
- Fixed profile pictures still being deleted after resetting a pending removal in Profile settings.
- Fixed shared instance reports not showing up in the dashboard's Reports page.
- Fixed shared instance report emails saying "Unknown" rather than the shared instance's name.
- Fixed the settings page for Authorized apps being broken.
- Broken Retro theme colors
- Improved vertical alignment of status indicators in notifications.`,
},
{
date: `2026-07-28T20:40:11+00:00`,
@@ -7,7 +7,7 @@
</span>
<span class="text-secondary mb-2">{{ formatMessage(messages.createUserDescription) }}</span>
</label>
<StyledInput
<Input
v-if="showUserField"
id="create-affiliate-user-input"
v-model="affiliateUsername"
@@ -28,14 +28,7 @@
</label>
<div class="flex items-center gap-2">
<div class="relative inline-flex items-center flex-1">
<AutoBrandIcon
:keyword="affiliateLinkTitle"
aria-hidden="true"
class="absolute left-3 h-5 w-5 z-[1] pointer-events-none text-secondary"
>
<AffiliateIcon />
</AutoBrandIcon>
<StyledInput
<Input
id="create-affiliate-title-input"
v-model="affiliateLinkTitle"
autocomplete="off"
@@ -43,8 +36,13 @@
:placeholder="formatMessage(messages.createTitlePlaceholder)"
clearable
wrapper-class="w-full"
input-class="pl-10"
/>
>
<template #leading>
<AutoBrandIcon :keyword="affiliateLinkTitle">
<AffiliateIcon />
</AutoBrandIcon>
</template>
</Input>
</div>
<ButtonStyled color="brand">
<button :disabled="creatingLink || !canCreate" @click="createAffiliateLink">
@@ -64,7 +62,7 @@ import { computed, ref, useTemplateRef } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils/common-messages'
import { AutoBrandIcon, ButtonStyled, NewModal, StyledInput } from '../index'
import { AutoBrandIcon, ButtonStyled, Input, NewModal } from '../index'
export type CreateAffiliateProps = { sourceName: string; username?: string }
const props = withDefaults(
@@ -24,14 +24,14 @@
ref="inputRef"
class="border-t border-solid border-b-0 border-x-0 border-surface-4 bg-surface-3 p-4"
>
<StyledInput
<Input
v-model="commandInput"
v-tooltip="disableInput ? disableInputTooltip : undefined"
:icon="TerminalSquareIcon"
:placeholder="disableInput ? disabledInputPlaceholder : 'Send a command'"
:disabled="disableInput"
wrapper-class="w-full"
input-class="!h-10"
size="medium"
@keydown.enter="submitCommand"
/>
</div>
@@ -44,7 +44,7 @@ import type { Terminal } from '@xterm/xterm'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import { useTerminal } from '#ui/composables/terminal'
const props = withDefaults(
+11 -16
View File
@@ -2,21 +2,17 @@
<div ref="containerRef" class="relative inline-block w-full">
<!-- Searchable mode: input trigger -->
<div v-if="searchable" class="relative w-full rounded-xl bg-surface-4">
<!--
Selection mirror: horizontal padding must match StyledInput (filled + left icon uses `pl-10`,
else `pl-3`) and `searchableInputClass` when the chevron is shown (`!pr-9`), or the overlay
text will not line up with the transparent input text / caret.
-->
<!-- Selection mirror must match InputFrame's leading and trailing slot geometry. -->
<div
v-if="searchSelectionOverlayVisible"
class="pointer-events-none absolute inset-y-0 left-0 right-0 z-0 flex min-w-0 items-center gap-2 font-medium text-primary"
:class="[showSearchIcon ? 'pl-10' : 'pl-3', showChevron ? 'pr-9' : 'pr-3']"
class="pointer-events-none absolute inset-y-0 left-0 right-0 z-[2] flex min-w-0 items-center gap-2 font-medium text-primary"
:class="[showSearchIcon ? 'pl-10' : 'pl-3', showChevron ? 'pr-10' : 'pr-3']"
aria-hidden="true"
>
<span class="min-w-0 truncate">{{ searchQuery }}</span>
<slot name="search-selection-affix" :option="selectedOption" />
</div>
<StyledInput
<Input
ref="searchTriggerRef"
v-model="searchQuery"
:icon="showSearchIcon ? SearchIcon : undefined"
@@ -30,7 +26,7 @@
:spellcheck="searchSpellcheck"
:inputmode="searchInputmode"
:input-attrs="searchInputAttrs"
wrapper-class="w-full !bg-transparent"
wrapper-class="w-full"
:input-class="searchableInputClass"
class="relative z-[1]"
@input="handleSearchInput"
@@ -39,13 +35,13 @@
@focusout="handleSearchFocusout"
@click="handleSearchClick"
>
<template v-if="showChevron" #right>
<template v-if="showChevron" #trailing>
<ChevronLeftIcon
class="pointer-events-none absolute right-3 top-1/2 size-5 -translate-y-1/2 text-secondary transition-transform duration-150"
class="pointer-events-none size-5 text-secondary transition-transform duration-150"
:class="isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'"
/>
</template>
</StyledInput>
</Input>
</div>
<!-- Standard mode: button trigger -->
@@ -217,7 +213,7 @@ import {
watch,
} from 'vue'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
export interface ComboboxOption<T> {
value: T
@@ -344,7 +340,7 @@ const userHasTyped = ref(false)
const focusedIndex = ref(-1)
const containerRef = ref<HTMLElement>()
const triggerRef = ref<HTMLElement>()
const searchTriggerRef = ref<InstanceType<typeof StyledInput>>()
const searchTriggerRef = ref<InstanceType<typeof Input>>()
const dropdownRef = ref<HTMLElement>()
const optionsScrollbarRef = ref<HTMLElement>()
const optionsContainerRef = ref<HTMLElement>()
@@ -389,8 +385,7 @@ const searchSelectionOverlayVisible = computed(() => {
})
const searchableInputClass = computed(() => {
const parts = ['!bg-transparent']
if (props.showChevron) parts.push('!pr-9')
const parts: string[] = []
if (searchSelectionOverlayVisible.value) {
parts.push('!text-transparent [caret-color:var(--color-text-primary)] selection:bg-transparent')
}
@@ -192,13 +192,12 @@
v-if="activeCategory.searchable"
class="flex justify-between border-0 border-b border-solid border-b-surface-5 py-1.5 w-full"
>
<StyledInput
<Input
v-model="categorySearchQuery"
:icon="SearchIcon"
type="text"
:placeholder="activeCategory.searchPlaceholder ?? 'Search...'"
wrapper-class="grow bg-surface-4 mx-1"
input-class="ps-9 mx-1.5"
wrapper-class="grow mx-1"
/>
<slot
name="search-actions"
@@ -387,8 +386,8 @@ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useVirtualScroll } from '../../composables/virtual-scroll'
import ButtonStyled from './ButtonStyled.vue'
import Input from './inputs/Input.vue'
import MultiSelect, { type MultiSelectItem } from './MultiSelect.vue'
import StyledInput from './StyledInput.vue'
export type DropdownFilterBarOption = {
value: string
@@ -12,7 +12,7 @@ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { injectI18nDebug } from '../../composables/i18n-debug'
import ButtonStyled from './ButtonStyled.vue'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
const debugContext = injectI18nDebug()
@@ -341,7 +341,7 @@ const listMaxHeight = computed(() => `${panelHeight.value - 120}px`)
<div v-if="!minimized" class="flex flex-col overflow-hidden w-full">
<!-- Search -->
<div class="px-3 py-2.5 !w-full">
<StyledInput
<Input
ref="searchInputRef"
v-model="searchQuery"
placeholder="Search keys or values..."
@@ -18,7 +18,7 @@
<slot />
<DropdownIcon class="h-5 w-5 text-secondary" />
<template #menu>
<StyledInput
<Input
v-if="search"
id="search-input"
ref="searchInput"
@@ -74,7 +74,7 @@
import { CheckIcon, DropdownIcon, SearchIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
import { Button, ButtonStyled, PopoutMenu, StyledInput } from '../index'
import { Button, ButtonStyled, Input, PopoutMenu } from '../index'
import ScrollablePanel from './ScrollablePanel.vue'
type Option = string | number | object
@@ -4,7 +4,7 @@
<label class="label" for="insert-link-label">
<span class="label__title">{{ formatMessage(messages.linkModalLabelFieldTitle) }}</span>
</label>
<StyledInput
<Input
id="insert-link-label"
v-model="linkText"
:icon="AlignLeftIcon"
@@ -18,7 +18,7 @@
{{ formatMessage(messages.urlLabel) }}<span class="required">*</span>
</span>
</label>
<StyledInput
<Input
id="insert-link-url"
v-model="linkUrl"
:icon="LinkIcon"
@@ -78,7 +78,7 @@
{{ formatMessage(messages.imageModalDescriptionFieldDescription) }}
</span>
</label>
<StyledInput
<Input
id="insert-image-alt"
v-model="linkText"
:icon="AlignLeftIcon"
@@ -115,7 +115,7 @@
<UploadIcon />
</FileInput>
</div>
<StyledInput
<Input
v-if="!props.onImageUpload || imageUploadOption === 'link'"
id="insert-link-url"
v-model="linkUrl"
@@ -175,7 +175,7 @@
{{ formatMessage(messages.videoModalUrlFieldDescription) }}
</span>
</label>
<StyledInput
<Input
id="insert-video-url"
v-model="linkUrl"
:icon="YouTubeIcon"
@@ -339,8 +339,8 @@ import NewModal from '../modal/NewModal.vue'
import ButtonStyled from './ButtonStyled.vue'
import Chips from './Chips.vue'
import FileInput from './FileInput.vue'
import Input from './inputs/Input.vue'
import IntlFormatted from './IntlFormatted.vue'
import StyledInput from './StyledInput.vue'
import Toggle from './Toggle.vue'
const { formatMessage } = useVIntl()
@@ -127,14 +127,13 @@
v-if="searchable"
class="px-0 py-1.5 border-0 border-solid border-b border-b-surface-5 flex"
>
<StyledInput
<Input
ref="searchInputRef"
v-model="searchQuery"
:icon="SearchIcon"
type="text"
:placeholder="searchPlaceholder"
wrapper-class="grow bg-surface-4 mx-0"
input-class="ps-9 mx-1.5"
wrapper-class="grow"
@input="handleSearchInput"
@keydown="handleSearchKeydown"
/>
@@ -415,7 +414,7 @@ import {
} from 'vue'
import { useVirtualScroll } from '../../composables/virtual-scroll'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
export interface MultiSelectOption<T> {
value: T
@@ -547,7 +546,7 @@ const dropdownRef = ref<HTMLElement>()
const optionsScrollbarRef = ref<HTMLElement>()
const optionsContainerRef = ref<HTMLElement>()
const selectionActionsRef = ref<HTMLElement>()
const searchInputRef = ref<InstanceType<typeof StyledInput>>()
const searchInputRef = ref<InstanceType<typeof Input>>()
const rafId = ref<number | null>(null)
const tagsContainerRef = ref<HTMLElement>()
const optionsOverlayScrollbars = ref<OverlayScrollbarsInstance | null>(null)
+2 -2
View File
@@ -44,7 +44,7 @@
<span> {{ max }} {{ unit }} </span>
</div>
</div>
<StyledInput
<Input
:model-value="String(currentValue)"
type="number"
class="w-24 ml-3"
@@ -60,7 +60,7 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
const emit = defineEmits<{ 'update:modelValue': [number] }>()
@@ -1,216 +0,0 @@
<template>
<div
class="relative"
:class="[
wrapperClass,
multiline ? 'flex' : 'inline-flex',
{ 'opacity-50 cursor-not-allowed': disabled },
!multiline && variant === 'outlined' ? 'items-stretch' : 'items-center',
]"
>
<!-- Left icon (filled variant, single-line only) -->
<component
:is="icon"
v-if="icon && variant === 'filled' && !multiline"
class="absolute left-3 h-5 w-5 z-[1] pointer-events-none transition-colors"
:class="[isFocused ? 'opacity-100 text-contrast' : 'opacity-60 text-secondary']"
aria-hidden="true"
/>
<!-- Multiline textarea -->
<textarea
v-if="multiline"
:id="id"
v-bind="inputAttrs"
ref="inputRef"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:autocorrect="autocorrect"
:autocapitalize="autocapitalize"
:spellcheck="spellcheck"
:maxlength="maxlength"
:rows="rows"
class="w-full touch-manipulation text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none bg-surface-4 border-none rounded-xl"
:class="[
inputClass,
'pl-3 pr-3 py-2 text-base',
feedbackRingClass,
disabled ? 'cursor-not-allowed' : '',
resizeClass,
]"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<!-- Single-line input -->
<input
v-else
:id="id"
v-bind="inputAttrs"
ref="inputRef"
:type="type"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:autocorrect="autocorrect"
:autocapitalize="autocapitalize"
:spellcheck="spellcheck"
:inputmode="inputmode"
:maxlength="maxlength"
:min="min"
:max="max"
:step="step"
class="w-full touch-manipulation text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none"
:class="[
inputClass,
variant === 'filled' && icon ? 'pl-10' : 'pl-3',
clearable && model && variant === 'filled' ? 'pr-8' : 'pr-3',
size === 'small' ? 'h-8 py-1.5 text-sm' : 'h-9 py-2 text-base',
feedbackRingClass,
disabled ? 'cursor-not-allowed' : '',
variant === 'outlined'
? 'bg-transparent border border-solid border-button-bg rounded-l-xl border-r-0'
: 'bg-surface-4 border-none rounded-xl',
]"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<!-- Clear button (right side, filled variant, single-line only) -->
<button
v-if="!multiline && clearable && model && !disabled && !readonly && variant === 'filled'"
type="button"
class="absolute right-0.5 z-[1] p-2 touch-manipulation bg-transparent border-none text-secondary hover:text-contrast transition-colors cursor-pointer select-none"
aria-label="Clear input"
@click="clear"
>
<XIcon class="h-5 w-5" />
</button>
<!-- Right icon button (outlined variant, single-line only) -->
<button
v-if="!multiline && variant === 'outlined'"
type="button"
class="flex touch-manipulation items-center justify-center px-2 bg-transparent border border-solid border-button-bg rounded-r-xl text-secondary hover:text-contrast transition-colors shrink-0"
:aria-label="clearable && model ? 'Clear input' : 'Search'"
:tabindex="clearable && model ? undefined : -1"
@click="clearable && model ? clear() : undefined"
>
<XIcon v-if="clearable && model" class="h-4 w-4" />
<component :is="icon" v-else-if="icon" class="h-4 w-4" />
<SearchIcon v-else class="h-4 w-4" />
</button>
<!-- Custom rightside slot -->
<slot name="right" />
</div>
</template>
<script setup lang="ts">
import { SearchIcon, XIcon } from '@modrinth/assets'
import { type Component, computed, ref } from 'vue'
const RING = {
error: 'ring-4 ring-red focus:ring-4 focus:ring-red',
none: 'outline-none focus:ring-4 focus:ring-brand-shadow',
} as const
const model = defineModel<string | number | undefined>()
const props = withDefaults(
defineProps<{
icon?: Component
type?: 'text' | 'email' | 'password' | 'number' | 'url' | 'search' | 'date' | 'datetime-local'
placeholder?: string
id?: string
name?: string
autocomplete?: string
autocorrect?: 'on' | 'off'
autocapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters'
spellcheck?: boolean
inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
maxlength?: number
min?: number
max?: number
step?: number
clamp?: boolean
disabled?: boolean
readonly?: boolean
error?: boolean
size?: 'standard' | 'small'
variant?: 'filled' | 'outlined'
clearable?: boolean
multiline?: boolean
rows?: number
resize?: 'none' | 'vertical' | 'both'
inputClass?: string
wrapperClass?: string
inputAttrs?: Record<string, string | number | boolean | undefined>
}>(),
{
type: 'text',
size: 'standard',
variant: 'filled',
clamp: false,
disabled: false,
readonly: false,
error: false,
clearable: false,
multiline: false,
rows: 3,
resize: 'none',
},
)
const emit = defineEmits<{
clear: []
}>()
const inputRef = ref<HTMLInputElement | HTMLTextAreaElement>()
const isFocused = ref(false)
const feedbackRingClass = computed(() => (props.error ? RING.error : RING.none))
const resizeClass = computed(
() => ({ none: 'resize-none', vertical: 'resize-y', both: 'resize' })[props.resize ?? 'none'],
)
defineExpose({
focus: () => inputRef.value?.focus(),
setValue: (v: string) => {
if (inputRef.value) inputRef.value.value = v
},
})
function onInput(event: Event) {
const target = event.target as HTMLInputElement | HTMLTextAreaElement
if (props.type !== 'number' || props.multiline) {
model.value = target.value
return
}
if (target.value === '') {
model.value = undefined
return
}
let value = Number(target.value)
if (props.clamp) {
if (props.min !== undefined) value = Math.max(props.min, value)
if (props.max !== undefined) value = Math.min(props.max, value)
target.value = String(value)
}
model.value = value
}
function clear() {
model.value = props.type === 'number' && !props.multiline ? undefined : ''
emit('clear')
}
</script>

Some files were not shown because too many files have changed in this diff Show More