refactor: no more vue multiselect (#5523)

* start multiselect component

* update styles

* small fix

* fix padding and styles

* add border bottom on sticky items

* add border bottom to search as well

* fix select all showing line

* use multi-select component for languages field

* add no options story for empty state

* refactor: remove vue-multiselect, replace with either our own combobox and multiselect

* pnpm prepr

* pnpm prepr

* fix combobox in transfer organization
This commit is contained in:
Truman Gao
2026-03-16 05:46:48 -07:00
committed by GitHub
parent d50a8efb26
commit 01c9dee612
15 changed files with 172 additions and 240 deletions

View File

@@ -40,7 +40,6 @@
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^10.0.0", "vue-i18n": "^10.0.0",
"vue-multiselect": "3.0.0",
"vue-router": "^4.6.0", "vue-router": "^4.6.0",
"vue-virtual-scroller": "v2.0.0-beta.8" "vue-virtual-scroller": "v2.0.0-beta.8"
}, },

View File

@@ -1599,4 +1599,3 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
} }
} }
</style> </style>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -17,31 +17,17 @@
<tr class="content"> <tr class="content">
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td> <td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
<td> <td>
<multiselect <Combobox
v-if="versions?.length > 1" v-if="versions?.length > 1"
v-model="selectedVersion" v-model="selectedVersionId"
:options="versions" :options="versionOptions"
:searchable="true" :searchable="true"
placeholder="Select version" placeholder="Select version"
open-direction="top" force-direction="up"
:show-labels="false"
:custom-label="
(version) =>
`${version?.name} (${version?.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version?.game_versions.join(', ')})`
"
:max-height="150" :max-height="150"
/> />
<span v-else> <span v-else>
<span> <span>{{ selectedVersionLabel }}</span>
{{ selectedVersion?.name }} ({{
selectedVersion?.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')
}}
- {{ selectedVersion?.game_versions.join(', ') }})
</span>
</span> </span>
</td> </td>
</tr> </tr>
@@ -59,9 +45,8 @@
<script setup> <script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets' import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui' import { Button, Combobox, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { ref } from 'vue' import { computed, ref } from 'vue'
import Multiselect from 'vue-multiselect'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
@@ -79,11 +64,35 @@ const installing = ref(false)
const onInstall = ref(() => {}) const onInstall = ref(() => {})
const selectedVersionLabel = computed(() => {
if (!selectedVersion.value) return ''
return `${selectedVersion.value.name} (${selectedVersion.value.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${selectedVersion.value.game_versions.join(', ')})`
})
const versionOptions = computed(() =>
(versions.value ?? []).map((version) => ({
value: version.id,
label: `${version.name} (${version.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version.game_versions.join(', ')})`,
})),
)
const selectedVersionId = computed({
get: () => selectedVersion.value?.id ?? null,
set: (value) => {
if (!value) return
selectedVersion.value = (versions.value ?? []).find((version) => version.id === value) ?? null
},
})
defineExpose({ defineExpose({
show: (instanceVal, projectVal, projectVersions, selected, callback) => { show: (instanceVal, projectVal, projectVersions, selected, callback) => {
instance.value = instanceVal instance.value = instanceVal
versions.value = projectVersions versions.value = projectVersions ?? []
selectedVersion.value = selected ?? projectVersions[0] selectedVersion.value = selected ?? projectVersions?.[0] ?? null
project.value = projectVal project.value = projectVal
@@ -162,9 +171,5 @@ td:first-child {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
:deep(.animated-dropdown .options) {
max-height: 13.375rem;
}
} }
</style> </style>

View File

@@ -75,7 +75,6 @@
"semver": "^7.5.4", "semver": "^7.5.4",
"three": "^0.172.0", "three": "^0.172.0",
"vue-confetti-explosion": "^1.0.2", "vue-confetti-explosion": "^1.0.2",
"vue-multiselect": "3.0.0-alpha.2",
"vue-typed-virtual-list": "^1.0.10", "vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4", "vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.5.2", "vue3-apexcharts": "^1.5.2",

View File

@@ -1593,4 +1593,3 @@ const { cycle: changeTheme } = useTheme()
} }
} }
</style> </style>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -296,19 +296,14 @@
This project is not managed by an organization. If you are the member of any organizations, This project is not managed by an organization. If you are the member of any organizations,
you can transfer management to one of them. you can transfer management to one of them.
</p> </p>
<div v-if="!organization" class="input-group"> <div v-if="!organization" class="flex gap-2">
<Multiselect <Combobox
id="organization-picker" v-model="selectedOrganizationId"
v-model="selectedOrganization" :options="organizationOptions"
class="large-multiselect" :searchable="true"
track-by="id" search-placeholder="Select organization..."
label="name" force-direction="up"
open-direction="top" :disabled="!currentMember?.is_owner || organizationOptions.length === 0"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
:options="organizations || []"
:disabled="!currentMember?.is_owner || organizations?.length === 0"
/> />
<button <button
class="btn btn-primary" class="btn btn-primary"
@@ -316,7 +311,7 @@
@click="openTransferToOrgModal($event)" @click="openTransferToOrgModal($event)"
> >
<CheckIcon /> <CheckIcon />
Transfer management <span class="w-max"> Transfer management </span>
</button> </button>
</div> </div>
<button v-if="organization" class="btn" @click="$refs.modal_remove.show()"> <button v-if="organization" class="btn" @click="$refs.modal_remove.show()">
@@ -561,13 +556,13 @@ import {
Badge, Badge,
Card, Card,
Checkbox, Checkbox,
Combobox,
ConfirmModal, ConfirmModal,
injectNotificationManager, injectNotificationManager,
injectProjectPageContext, injectProjectPageContext,
StyledInput, StyledInput,
Toggle, Toggle,
} from '@modrinth/ui' } from '@modrinth/ui'
import { Multiselect } from 'vue-multiselect'
import ConfirmTransferProjectModal from '~/components/ui/ConfirmTransferProjectModal.vue' import ConfirmTransferProjectModal from '~/components/ui/ConfirmTransferProjectModal.vue'
import { removeSelfFromTeam } from '~/helpers/teams.js' import { removeSelfFromTeam } from '~/helpers/teams.js'
@@ -620,7 +615,7 @@ initMembers()
const currentUsername = ref('') const currentUsername = ref('')
const openTeamMembers = ref([]) const openTeamMembers = ref([])
const selectedOrganization = ref(null) const selectedOrganizationId = ref('')
const transferData = ref(null) const transferData = ref(null)
const transferModal = ref(null) const transferModal = ref(null)
@@ -630,6 +625,17 @@ const { data: organizations } = useAsyncData('organizations', () => {
}) })
}) })
const organizationOptions = computed(() =>
(organizations.value ?? []).map((organization) => ({
value: organization.id,
label: organization.name,
})),
)
const selectedOrganization = computed(() =>
(organizations.value ?? []).find((org) => org.id === selectedOrganizationId.value),
)
const UPLOAD_VERSION = 1 << 0 const UPLOAD_VERSION = 1 << 0
const DELETE_VERSION = 1 << 1 const DELETE_VERSION = 1 << 1
const EDIT_DETAILS = 1 << 2 const EDIT_DETAILS = 1 << 2
@@ -642,9 +648,9 @@ const VIEW_ANALYTICS = 1 << 8
const VIEW_PAYOUTS = 1 << 9 const VIEW_PAYOUTS = 1 << 9
const onAddToOrg = useClientTry(async () => { const onAddToOrg = useClientTry(async () => {
if (!selectedOrganization.value) return if (!selectedOrganizationId.value) return
await useBaseFetch(`organization/${selectedOrganization.value.id}/projects`, { await useBaseFetch(`organization/${selectedOrganizationId.value}/projects`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
project_id: project.value.id, project_id: project.value.id,
@@ -1002,8 +1008,4 @@ const updateMembers = async () => {
} }
} }
} }
.large-multiselect {
max-width: 24rem;
}
</style> </style>

View File

@@ -25,17 +25,14 @@
The mod loaders you would like to package your data pack for. The mod loaders you would like to package your data pack for.
</span> </span>
</label> </label>
<multiselect <MultiSelect
id="package-mod-loaders" id="package-mod-loaders"
v-model="packageLoaders" v-model="packageLoaders"
:options="['fabric', 'forge', 'quilt', 'neoforge']" class="package-loader-select"
:custom-label="(value: string) => value.charAt(0).toUpperCase() + value.slice(1)" :options="packageLoaderOptions"
:multiple="true"
:searchable="false" :searchable="false"
:show-no-results="false"
:show-labels="false"
placeholder="Choose loaders..." placeholder="Choose loaders..."
open-direction="top" force-direction="up"
/> />
<div class="button-group"> <div class="button-group">
<ButtonStyled> <ButtonStyled>
@@ -436,11 +433,11 @@ import {
ENVIRONMENTS_COPY, ENVIRONMENTS_COPY,
injectNotificationManager, injectNotificationManager,
injectProjectPageContext, injectProjectPageContext,
MultiSelect,
StyledInput, StyledInput,
useFormatDateTime, useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatBytes, renderHighlightedString } from '@modrinth/utils' import { formatBytes, renderHighlightedString } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue' import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue' import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
@@ -504,6 +501,12 @@ const newFiles = ref<File[]>([])
const deleteFiles = ref<string[]>([]) const deleteFiles = ref<string[]>([])
const newFileTypes = ref<Array<{ display: string; value: string } | null>>([]) const newFileTypes = ref<Array<{ display: string; value: string } | null>>([])
const packageLoaders = ref(['forge', 'fabric', 'quilt', 'neoforge']) const packageLoaders = ref(['forge', 'fabric', 'quilt', 'neoforge'])
const packageLoaderOptions = [
{ value: 'fabric', label: 'Fabric' },
{ value: 'forge', label: 'Forge' },
{ value: 'quilt', label: 'Quilt' },
{ value: 'neoforge', label: 'Neoforge' },
]
const showKnownErrors = ref(false) const showKnownErrors = ref(false)
const shouldPreventActions = ref(false) const shouldPreventActions = ref(false)
const uploadedImageIds = ref<string[]>([]) const uploadedImageIds = ref<string[]>([])
@@ -1215,11 +1218,6 @@ async function resetProjectVersions() {
margin-bottom: var(--spacing-card-sm); margin-bottom: var(--spacing-card-sm);
} }
.multiselect {
width: 8rem;
flex-grow: 1;
}
input { input {
flex-grow: 2; flex-grow: 2;
} }
@@ -1270,14 +1268,6 @@ async function resetProjectVersions() {
font-weight: 300; font-weight: 300;
} }
.raised-multiselect {
display: none;
margin: 0 0.5rem;
height: 40px;
max-height: 40px;
min-width: 235px;
}
.raised-button { .raised-button {
margin-left: auto; margin-left: auto;
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
@@ -1286,13 +1276,6 @@ async function resetProjectVersions() {
&:not(:nth-child(2)) { &:not(:nth-child(2)) {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
// TODO: Make file type editing work on mobile
@media (min-width: 600px) {
.raised-multiselect {
display: block;
}
}
} }
.additional-files { .additional-files {
@@ -1357,7 +1340,7 @@ async function resetProjectVersions() {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.multiselect { .package-loader-select {
max-width: 20rem; max-width: 20rem;
} }
} }

View File

@@ -182,14 +182,11 @@
<div class="push-right"> <div class="push-right">
<div class="labeled-control-row"> <div class="labeled-control-row">
Sort by Sort by
<Multiselect <Combobox
v-model="sortBy" v-model="sortBy"
:searchable="false" :searchable="false"
class="small-select" class="small-select"
:options="['Name', 'Status', 'Type']" :options="sortOptions"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@update:model-value="projects = updateSort(projects, sortBy, descending)" @update:model-value="projects = updateSort(projects, sortBy, descending)"
/> />
<button <button
@@ -323,6 +320,7 @@ import {
Avatar, Avatar,
ButtonStyled, ButtonStyled,
Checkbox, Checkbox,
Combobox,
commonMessages, commonMessages,
CopyCode, CopyCode,
injectNotificationManager, injectNotificationManager,
@@ -332,7 +330,6 @@ import {
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue' import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import { getProjectTypeForUrl } from '~/helpers/projects.js' import { getProjectTypeForUrl } from '~/helpers/projects.js'
@@ -356,6 +353,11 @@ const projects = ref([])
const projectsWithMigrationWarning = ref([]) const projectsWithMigrationWarning = ref([])
const selectedProjects = ref([]) const selectedProjects = ref([])
const sortBy = ref('Name') const sortBy = ref('Name')
const sortOptions = [
{ value: 'Name', label: 'Name' },
{ value: 'Status', label: 'Status' },
{ value: 'Type', label: 'Type' },
]
const descending = ref(false) const descending = ref(false)
const editLinks = reactive({ const editLinks = reactive({
showAffected: false, showAffected: false,

View File

@@ -106,19 +106,22 @@
@input="updateSearchProjects" @input="updateSearchProjects"
/> />
<div class="sort-by"> <div class="sort-by">
<span class="label">{{ formatMessage(commonMessages.sortByLabel) }}</span> <DropdownSelect
<Multiselect v-slot="{ selected }"
v-model="sortType" v-model="sortType"
placeholder="Select one" class="!h-9 !w-max flex-grow"
class="selector" name="Sort by"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)" :options="sortOptions"
:options="['relevance', 'downloads', 'follows', 'updated', 'newest']" :display-name="(value) => value?.charAt(0).toUpperCase() + value?.slice(1)"
:searchable="false" @change="updateSearchProjects()"
:close-on-select="true" >
:show-labels="false" <div>
:allow-empty="false" <span class="font-semibold text-primary"
@update:model-value="updateSearchProjects" >{{ formatMessage(commonMessages.sortByLabel) }}:
/> </span>
<span class="font-semibold text-secondary">{{ selected }}</span>
</div>
</DropdownSelect>
</div> </div>
</div> </div>
<div class="results display-mode--list"> <div class="results display-mode--list">
@@ -444,6 +447,7 @@ import {
ButtonStyled, ButtonStyled,
commonMessages, commonMessages,
defineMessages, defineMessages,
DropdownSelect,
IntlFormatted, IntlFormatted,
ProjectCard, ProjectCard,
StyledInput, StyledInput,
@@ -451,7 +455,6 @@ import {
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import { Multiselect } from 'vue-multiselect'
import ATLauncherLogo from '~/assets/images/external/atlauncher.svg?component' import ATLauncherLogo from '~/assets/images/external/atlauncher.svg?component'
import PrismLauncherLogo from '~/assets/images/external/prism.svg?component' import PrismLauncherLogo from '~/assets/images/external/prism.svg?component'
@@ -464,6 +467,7 @@ const { formatMessage } = useVIntl()
const searchQuery = ref('leave') const searchQuery = ref('leave')
const sortType = ref('relevance') const sortType = ref('relevance')
const sortOptions = ['relevance', 'downloads', 'follows', 'updated', 'newest']
const PROJECT_COUNT = 100000 const PROJECT_COUNT = 100000
@@ -955,7 +959,7 @@ const creatorFeatureMessages = defineMessages({
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 1rem; margin-bottom: 1rem;
gap: 1rem; gap: 0.5rem;
.iconified-input { .iconified-input {
width: 100%; width: 100%;
@@ -977,19 +981,6 @@ const creatorFeatureMessages = defineMessages({
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
align-items: center; align-items: center;
.label {
white-space: nowrap;
}
.selector {
min-width: 8rem;
white-space: nowrap;
}
@media screen and (max-width: 500px) {
display: none;
}
} }
} }

View File

@@ -186,17 +186,12 @@
<div class="push-right"> <div class="push-right">
<div class="labeled-control-row"> <div class="labeled-control-row">
Sort by Sort by
<Multiselect <DropdownSelect
v-model="sortBy" v-model="sortBy"
:searchable="false" class="!w-auto"
class="small-select" name="Sort by"
:options="['Name', 'Status', 'Type']" :options="sortOptions"
:close-on-select="true" @change="sortedProjects = updateSort(sortedProjects, sortBy, descending)"
:show-labels="false"
:allow-empty="false"
@update:model-value="
sortedProjects = updateSort(sortedProjects, sortBy, descending)
"
/> />
<button <button
v-tooltip="descending ? 'Descending' : 'Ascending'" v-tooltip="descending ? 'Descending' : 'Ascending'"
@@ -331,6 +326,7 @@ import {
Checkbox, Checkbox,
commonMessages, commonMessages,
CopyCode, CopyCode,
DropdownSelect,
injectNotificationManager, injectNotificationManager,
NewModal, NewModal,
ProjectStatusBadge, ProjectStatusBadge,
@@ -338,7 +334,6 @@ import {
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue' import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue' import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue'
@@ -466,6 +461,7 @@ const updateSort = (inputProjects, sort, descending) => {
const sortedProjects = ref(updateSort(projects.value, 'Name')) const sortedProjects = ref(updateSort(projects.value, 'Name'))
const selectedProjects = ref([]) const selectedProjects = ref([])
const sortBy = ref('Name') const sortBy = ref('Name')
const sortOptions = ['Name', 'Status', 'Type']
const descending = ref(false) const descending = ref(false)
const editLinksModal = ref(null) const editLinksModal = ref(null)
@@ -674,11 +670,6 @@ const onBulkEditLinks = async () => {
white-space: nowrap; white-space: nowrap;
} }
.small-select {
width: -moz-fit-content;
width: fit-content;
}
.label-button[data-active='true'] { .label-button[data-active='true'] {
--background-color: var(--color-red); --background-color: var(--color-red);
--text-color: var(--color-brand-inverted); --text-color: var(--color-brand-inverted);

View File

@@ -80,7 +80,6 @@
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.4.1",
"three": "^0.172.0", "three": "^0.172.0",
"vue-i18n": "^10.0.0", "vue-i18n": "^10.0.0",
"vue-multiselect": "3.0.0",
"vue-select": "4.0.0-beta.6", "vue-select": "4.0.0-beta.6",
"vue-typed-virtual-list": "^1.0.10", "vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4", "vue3-ace-editor": "^2.2.4",

View File

@@ -323,92 +323,15 @@
</div> </div>
</div> </div>
<p class="my-2 text-lg font-bold">Pay for it with</p> <p class="my-2 text-lg font-bold">Pay for it with</p>
<multiselect <Combobox
v-model="selectedPaymentMethod" v-model="selectedPaymentMethodId"
placeholder="Payment method" placeholder="Payment method"
label="id" :options="selectablePaymentMethodOptions"
track-by="id"
:options="selectablePaymentMethods"
:option-height="104"
:show-labels="false"
:searchable="false" :searchable="false"
:close-on-select="true" :show-icon-in-selected="true"
:allow-empty="false" force-direction="up"
open-direction="top"
class="max-w-[20rem]" class="max-w-[20rem]"
@select="selectPaymentMethod" />
>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<template #singleLabel="props">
<div class="flex items-center gap-2">
<CardIcon v-if="props.option.type === 'card'" class="h-8 w-8" />
<CurrencyIcon v-else-if="props.option.type === 'cashapp'" class="h-8 w-8" />
<PayPalIcon v-else-if="props.option.type === 'paypal'" class="h-8 w-8" />
<span v-if="props.option.type === 'card'">
{{
formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
card_brand:
formatMessage(paymentMethodMessages[props.option.card.brand]) ??
formatMessage(paymentMethodMessages.unknown),
last_four: props.option.card.last4,
})
}}
</span>
<template v-else>
{{
formatMessage(paymentMethodMessages[props.option.type]) ??
formatMessage(paymentMethodMessages.unknown)
}}
</template>
<span v-if="props.option.type === 'cashapp' && props.option.cashapp.cashtag">
({{ props.option.cashapp.cashtag }})
</span>
<span v-else-if="props.option.type === 'paypal' && props.option.paypal.payer_email">
({{ props.option.paypal.payer_email }})
</span>
</div>
</template>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<template #option="props">
<div class="flex items-center gap-2">
<template v-if="props.option.id === 'new'">
<PlusIcon class="h-8 w-8" />
<span class="text-secondary">Add payment method</span>
</template>
<template v-else>
<CardIcon v-if="props.option.type === 'card'" class="h-8 w-8" />
<CurrencyIcon v-else-if="props.option.type === 'cashapp'" class="h-8 w-8" />
<PayPalIcon v-else-if="props.option.type === 'paypal'" class="h-8 w-8" />
<span v-if="props.option.type === 'card'">
{{
formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
card_brand:
formatMessage(paymentMethodMessages[props.option.card.brand]) ??
formatMessage(paymentMethodMessages.unknown),
last_four: props.option.card.last4,
})
}}
</span>
<template v-else>
{{
formatMessage(paymentMethodMessages[props.option.type]) ??
formatMessage(paymentMethodMessages.unknown)
}}
</template>
<span v-if="props.option.type === 'cashapp'">
({{ props.option.cashapp.cashtag }})
</span>
<span v-else-if="props.option.type === 'paypal'">
({{ props.option.paypal.payer_email }})
</span>
</template>
</div>
</template>
</multiselect>
</div> </div>
<p class="m-0 mt-9 text-sm text-secondary"> <p class="m-0 mt-9 text-sm text-secondary">
<strong>By clicking "Subscribe", you are purchasing a recurring subscription.</strong> <strong>By clicking "Subscribe", you are purchasing a recurring subscription.</strong>
@@ -546,13 +469,13 @@ import {
import { calculateSavings, createStripeElements, getCurrency } from '@modrinth/utils' import { calculateSavings, createStripeElements, getCurrency } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue' import { computed, nextTick, reactive, ref, watch } from 'vue'
import { Multiselect } from 'vue-multiselect'
import { useVIntl } from '../../composables/i18n' import { useVIntl } from '../../composables/i18n'
import { useFormatDateTime, useFormatPrice } from '../../composables/index.ts' import { useFormatDateTime, useFormatPrice } from '../../composables/index.ts'
import { paymentMethodMessages } from '../../utils/common-messages' import { paymentMethodMessages } from '../../utils/common-messages'
import Admonition from '../base/Admonition.vue' import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue' import Checkbox from '../base/Checkbox.vue'
import Combobox from '../base/Combobox.vue'
import Slider from '../base/Slider.vue' import Slider from '../base/Slider.vue'
import StyledInput from '../base/StyledInput.vue' import StyledInput from '../base/StyledInput.vue'
import AnimatedLogo from '../brand/AnimatedLogo.vue' import AnimatedLogo from '../brand/AnimatedLogo.vue'
@@ -784,6 +707,67 @@ const selectablePaymentMethods = computed(() => {
return values return values
}) })
function formatPaymentMethodLabel(paymentMethod) {
if (!paymentMethod) {
return formatMessage(paymentMethodMessages.unknown)
}
if (paymentMethod.id === 'new') {
return 'Add payment method'
}
if (paymentMethod.type === 'card') {
return formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
card_brand:
formatMessage(paymentMethodMessages[paymentMethod.card?.brand]) ??
formatMessage(paymentMethodMessages.unknown),
last_four: paymentMethod.card?.last4 ?? '****',
})
}
const typeLabel =
formatMessage(paymentMethodMessages[paymentMethod.type]) ??
formatMessage(paymentMethodMessages.unknown)
let suffix = ''
if (paymentMethod.type === 'cashapp' && paymentMethod.cashapp?.cashtag) {
suffix = ` (${paymentMethod.cashapp.cashtag})`
} else if (paymentMethod.type === 'paypal' && paymentMethod.paypal?.payer_email) {
suffix = ` (${paymentMethod.paypal.payer_email})`
}
return `${typeLabel}${suffix}`
}
function getPaymentMethodIcon(paymentMethod) {
if (paymentMethod.id === 'new') return PlusIcon
if (paymentMethod.type === 'card') return CardIcon
if (paymentMethod.type === 'cashapp') return CurrencyIcon
if (paymentMethod.type === 'paypal') return PayPalIcon
return undefined
}
const selectablePaymentMethodOptions = computed(() =>
selectablePaymentMethods.value.map((paymentMethod) => ({
value: paymentMethod.id,
label: formatPaymentMethodLabel(paymentMethod),
icon: getPaymentMethodIcon(paymentMethod),
})),
)
const selectedPaymentMethodId = computed({
get: () => selectedPaymentMethod.value?.id ?? null,
set: (value) => {
if (!value) return
const paymentMethod = selectablePaymentMethods.value.find((method) => method.id === value)
if (paymentMethod) {
selectedPaymentMethod.value = paymentMethod
void selectPaymentMethod(paymentMethod)
}
},
})
const primaryPaymentMethodId = computed(() => { const primaryPaymentMethodId = computed(() => {
if ( if (
props.customer && props.customer &&

View File

@@ -1002,7 +1002,7 @@
"defaultMessage": "Singleplayer" "defaultMessage": "Singleplayer"
}, },
"label.sort-by": { "label.sort-by": {
"defaultMessage": "Sort by" "defaultMessage": "Sort by: "
}, },
"label.success": { "label.success": {
"defaultMessage": "Success" "defaultMessage": "Success"

View File

@@ -295,7 +295,7 @@ export const commonMessages = defineMessages({
}, },
sortByLabel: { sortByLabel: {
id: 'label.sort-by', id: 'label.sort-by',
defaultMessage: 'Sort by', defaultMessage: 'Sort by: ',
}, },
stopButton: { stopButton: {
id: 'button.stop', id: 'button.stop',

21
pnpm-lock.yaml generated
View File

@@ -143,9 +143,6 @@ importers:
vue-i18n: vue-i18n:
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.8(vue@3.5.27(typescript@5.9.3)) version: 10.0.8(vue@3.5.27(typescript@5.9.3))
vue-multiselect:
specifier: 3.0.0
version: 3.0.0
vue-router: vue-router:
specifier: ^4.6.0 specifier: ^4.6.0
version: 4.6.4(vue@3.5.27(typescript@5.9.3)) version: 4.6.4(vue@3.5.27(typescript@5.9.3))
@@ -353,9 +350,6 @@ importers:
vue-confetti-explosion: vue-confetti-explosion:
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.2(vue@3.5.27(typescript@5.9.3)) version: 1.0.2(vue@3.5.27(typescript@5.9.3))
vue-multiselect:
specifier: 3.0.0-alpha.2
version: 3.0.0-alpha.2
vue-typed-virtual-list: vue-typed-virtual-list:
specifier: ^1.0.10 specifier: ^1.0.10
version: 1.0.10(vue@3.5.27(typescript@5.9.3)) version: 1.0.10(vue@3.5.27(typescript@5.9.3))
@@ -676,9 +670,6 @@ importers:
vue-i18n: vue-i18n:
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.8(vue@3.5.27(typescript@5.9.3)) version: 10.0.8(vue@3.5.27(typescript@5.9.3))
vue-multiselect:
specifier: 3.0.0
version: 3.0.0
vue-select: vue-select:
specifier: 4.0.0-beta.6 specifier: 4.0.0-beta.6
version: 4.0.0-beta.6(vue@3.5.27(typescript@5.9.3)) version: 4.0.0-beta.6(vue@3.5.27(typescript@5.9.3))
@@ -9467,14 +9458,6 @@ packages:
peerDependencies: peerDependencies:
vue: '>=2' vue: '>=2'
vue-multiselect@3.0.0:
resolution: {integrity: sha512-uupKdINgz7j83lQToCL7KkgQQxvG43el++hsR39YT9pCe1DwzUGmKzPxjVP6rqskXed5P6DtUASYAlCliW740Q==}
engines: {node: '>= 14.18.1', npm: '>= 6.14.15'}
vue-multiselect@3.0.0-alpha.2:
resolution: {integrity: sha512-Xp9fGJECns45v+v8jXbCIsAkCybYkEg0lNwr7Z6HDUSMyx2TEIK2giipPE+qXiShEc1Ipn+ZtttH2iq9hwXP4Q==}
engines: {node: '>= 4.0.0', npm: '>= 3.0.0'}
vue-observe-visibility@2.0.0-alpha.1: vue-observe-visibility@2.0.0-alpha.1:
resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==} resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==}
peerDependencies: peerDependencies:
@@ -19605,10 +19588,6 @@ snapshots:
dependencies: dependencies:
vue: 3.5.27(typescript@5.9.3) vue: 3.5.27(typescript@5.9.3)
vue-multiselect@3.0.0: {}
vue-multiselect@3.0.0-alpha.2: {}
vue-observe-visibility@2.0.0-alpha.1(vue@3.5.27(typescript@5.9.3)): vue-observe-visibility@2.0.0-alpha.1(vue@3.5.27(typescript@5.9.3)):
dependencies: dependencies:
vue: 3.5.27(typescript@5.9.3) vue: 3.5.27(typescript@5.9.3)