mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 13:16:38 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bdd0e4155 | ||
|
|
cb4462374c | ||
|
|
f08f2d8405 | ||
|
|
65a3ac4b34 | ||
|
|
295db3fb7a | ||
|
|
7abe6f8c73 | ||
|
|
c5ce5bc9b3 | ||
|
|
572b8027ff | ||
|
|
0a53211389 | ||
|
|
22b2b7746e | ||
|
|
22bfca467a | ||
|
|
1186a8a027 | ||
|
|
3dd16180b0 |
@@ -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"
|
||||
|
||||
+3
-3
@@ -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 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'
|
||||
|
||||
+2
-2
@@ -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('.')) {
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
+3
-3
@@ -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))"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
)]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user