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

@@ -323,92 +323,15 @@
</div>
</div>
<p class="my-2 text-lg font-bold">Pay for it with</p>
<multiselect
v-model="selectedPaymentMethod"
<Combobox
v-model="selectedPaymentMethodId"
placeholder="Payment method"
label="id"
track-by="id"
:options="selectablePaymentMethods"
:option-height="104"
:show-labels="false"
:options="selectablePaymentMethodOptions"
:searchable="false"
:close-on-select="true"
:allow-empty="false"
open-direction="top"
:show-icon-in-selected="true"
force-direction="up"
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>
<p class="m-0 mt-9 text-sm text-secondary">
<strong>By clicking "Subscribe", you are purchasing a recurring subscription.</strong>
@@ -546,13 +469,13 @@ import {
import { calculateSavings, createStripeElements, getCurrency } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue'
import { Multiselect } from 'vue-multiselect'
import { useVIntl } from '../../composables/i18n'
import { useFormatDateTime, useFormatPrice } from '../../composables/index.ts'
import { paymentMethodMessages } from '../../utils/common-messages'
import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue'
import Combobox from '../base/Combobox.vue'
import Slider from '../base/Slider.vue'
import StyledInput from '../base/StyledInput.vue'
import AnimatedLogo from '../brand/AnimatedLogo.vue'
@@ -784,6 +707,67 @@ const selectablePaymentMethods = computed(() => {
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(() => {
if (
props.customer &&