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:
@@ -40,7 +40,6 @@
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^10.0.0",
|
||||
"vue-multiselect": "3.0.0",
|
||||
"vue-router": "^4.6.0",
|
||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
||||
},
|
||||
|
||||
@@ -1599,4 +1599,3 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
|
||||
@@ -17,31 +17,17 @@
|
||||
<tr class="content">
|
||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
||||
<td>
|
||||
<multiselect
|
||||
<Combobox
|
||||
v-if="versions?.length > 1"
|
||||
v-model="selectedVersion"
|
||||
:options="versions"
|
||||
v-model="selectedVersionId"
|
||||
:options="versionOptions"
|
||||
:searchable="true"
|
||||
placeholder="Select version"
|
||||
open-direction="top"
|
||||
:show-labels="false"
|
||||
:custom-label="
|
||||
(version) =>
|
||||
`${version?.name} (${version?.loaders
|
||||
.map((name) => formatLoader(formatMessage, name))
|
||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
||||
"
|
||||
force-direction="up"
|
||||
:max-height="150"
|
||||
/>
|
||||
<span v-else>
|
||||
<span>
|
||||
{{ selectedVersion?.name }} ({{
|
||||
selectedVersion?.loaders
|
||||
.map((name) => formatLoader(formatMessage, name))
|
||||
.join(', ')
|
||||
}}
|
||||
- {{ selectedVersion?.game_versions.join(', ') }})
|
||||
</span>
|
||||
<span>{{ selectedVersionLabel }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -59,9 +45,8 @@
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import { Button, Combobox, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
@@ -79,11 +64,35 @@ const installing = ref(false)
|
||||
|
||||
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({
|
||||
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
|
||||
instance.value = instanceVal
|
||||
versions.value = projectVersions
|
||||
selectedVersion.value = selected ?? projectVersions[0]
|
||||
versions.value = projectVersions ?? []
|
||||
selectedVersion.value = selected ?? projectVersions?.[0] ?? null
|
||||
|
||||
project.value = projectVal
|
||||
|
||||
@@ -162,9 +171,5 @@ td:first-child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
:deep(.animated-dropdown .options) {
|
||||
max-height: 13.375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"semver": "^7.5.4",
|
||||
"three": "^0.172.0",
|
||||
"vue-confetti-explosion": "^1.0.2",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-typed-virtual-list": "^1.0.10",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.5.2",
|
||||
|
||||
@@ -1593,4 +1593,3 @@ const { cycle: changeTheme } = useTheme()
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
|
||||
@@ -296,19 +296,14 @@
|
||||
This project is not managed by an organization. If you are the member of any organizations,
|
||||
you can transfer management to one of them.
|
||||
</p>
|
||||
<div v-if="!organization" class="input-group">
|
||||
<Multiselect
|
||||
id="organization-picker"
|
||||
v-model="selectedOrganization"
|
||||
class="large-multiselect"
|
||||
track-by="id"
|
||||
label="name"
|
||||
open-direction="top"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:options="organizations || []"
|
||||
:disabled="!currentMember?.is_owner || organizations?.length === 0"
|
||||
<div v-if="!organization" class="flex gap-2">
|
||||
<Combobox
|
||||
v-model="selectedOrganizationId"
|
||||
:options="organizationOptions"
|
||||
:searchable="true"
|
||||
search-placeholder="Select organization..."
|
||||
force-direction="up"
|
||||
:disabled="!currentMember?.is_owner || organizationOptions.length === 0"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@@ -316,7 +311,7 @@
|
||||
@click="openTransferToOrgModal($event)"
|
||||
>
|
||||
<CheckIcon />
|
||||
Transfer management
|
||||
<span class="w-max"> Transfer management </span>
|
||||
</button>
|
||||
</div>
|
||||
<button v-if="organization" class="btn" @click="$refs.modal_remove.show()">
|
||||
@@ -561,13 +556,13 @@ import {
|
||||
Badge,
|
||||
Card,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
ConfirmModal,
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
StyledInput,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
|
||||
import ConfirmTransferProjectModal from '~/components/ui/ConfirmTransferProjectModal.vue'
|
||||
import { removeSelfFromTeam } from '~/helpers/teams.js'
|
||||
@@ -620,7 +615,7 @@ initMembers()
|
||||
|
||||
const currentUsername = ref('')
|
||||
const openTeamMembers = ref([])
|
||||
const selectedOrganization = ref(null)
|
||||
const selectedOrganizationId = ref('')
|
||||
const transferData = 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 DELETE_VERSION = 1 << 1
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
@@ -642,9 +648,9 @@ const VIEW_ANALYTICS = 1 << 8
|
||||
const VIEW_PAYOUTS = 1 << 9
|
||||
|
||||
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',
|
||||
body: JSON.stringify({
|
||||
project_id: project.value.id,
|
||||
@@ -1002,8 +1008,4 @@ const updateMembers = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.large-multiselect {
|
||||
max-width: 24rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,17 +25,14 @@
|
||||
The mod loaders you would like to package your data pack for.
|
||||
</span>
|
||||
</label>
|
||||
<multiselect
|
||||
<MultiSelect
|
||||
id="package-mod-loaders"
|
||||
v-model="packageLoaders"
|
||||
:options="['fabric', 'forge', 'quilt', 'neoforge']"
|
||||
:custom-label="(value: string) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:multiple="true"
|
||||
class="package-loader-select"
|
||||
:options="packageLoaderOptions"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose loaders..."
|
||||
open-direction="top"
|
||||
force-direction="up"
|
||||
/>
|
||||
<div class="button-group">
|
||||
<ButtonStyled>
|
||||
@@ -436,11 +433,11 @@ import {
|
||||
ENVIRONMENTS_COPY,
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
MultiSelect,
|
||||
StyledInput,
|
||||
useFormatDateTime,
|
||||
} from '@modrinth/ui'
|
||||
import { formatBytes, renderHighlightedString } from '@modrinth/utils'
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
@@ -504,6 +501,12 @@ const newFiles = ref<File[]>([])
|
||||
const deleteFiles = ref<string[]>([])
|
||||
const newFileTypes = ref<Array<{ display: string; value: string } | null>>([])
|
||||
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 shouldPreventActions = ref(false)
|
||||
const uploadedImageIds = ref<string[]>([])
|
||||
@@ -1215,11 +1218,6 @@ async function resetProjectVersions() {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
width: 8rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-grow: 2;
|
||||
}
|
||||
@@ -1270,14 +1268,6 @@ async function resetProjectVersions() {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.raised-multiselect {
|
||||
display: none;
|
||||
margin: 0 0.5rem;
|
||||
height: 40px;
|
||||
max-height: 40px;
|
||||
min-width: 235px;
|
||||
}
|
||||
|
||||
.raised-button {
|
||||
margin-left: auto;
|
||||
background-color: var(--color-raised-bg);
|
||||
@@ -1286,13 +1276,6 @@ async function resetProjectVersions() {
|
||||
&:not(:nth-child(2)) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
// TODO: Make file type editing work on mobile
|
||||
@media (min-width: 600px) {
|
||||
.raised-multiselect {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.additional-files {
|
||||
@@ -1357,7 +1340,7 @@ async function resetProjectVersions() {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
.package-loader-select {
|
||||
max-width: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,14 +182,11 @@
|
||||
<div class="push-right">
|
||||
<div class="labeled-control-row">
|
||||
Sort by
|
||||
<Multiselect
|
||||
<Combobox
|
||||
v-model="sortBy"
|
||||
:searchable="false"
|
||||
class="small-select"
|
||||
:options="['Name', 'Status', 'Type']"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:options="sortOptions"
|
||||
@update:model-value="projects = updateSort(projects, sortBy, descending)"
|
||||
/>
|
||||
<button
|
||||
@@ -323,6 +320,7 @@ import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
commonMessages,
|
||||
CopyCode,
|
||||
injectNotificationManager,
|
||||
@@ -332,7 +330,6 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
|
||||
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import { getProjectTypeForUrl } from '~/helpers/projects.js'
|
||||
@@ -356,6 +353,11 @@ const projects = ref([])
|
||||
const projectsWithMigrationWarning = ref([])
|
||||
const selectedProjects = ref([])
|
||||
const sortBy = ref('Name')
|
||||
const sortOptions = [
|
||||
{ value: 'Name', label: 'Name' },
|
||||
{ value: 'Status', label: 'Status' },
|
||||
{ value: 'Type', label: 'Type' },
|
||||
]
|
||||
const descending = ref(false)
|
||||
const editLinks = reactive({
|
||||
showAffected: false,
|
||||
|
||||
@@ -106,19 +106,22 @@
|
||||
@input="updateSearchProjects"
|
||||
/>
|
||||
<div class="sort-by">
|
||||
<span class="label">{{ formatMessage(commonMessages.sortByLabel) }}</span>
|
||||
<Multiselect
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="sortType"
|
||||
placeholder="Select one"
|
||||
class="selector"
|
||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:options="['relevance', 'downloads', 'follows', 'updated', 'newest']"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@update:model-value="updateSearchProjects"
|
||||
/>
|
||||
class="!h-9 !w-max flex-grow"
|
||||
name="Sort by"
|
||||
:options="sortOptions"
|
||||
:display-name="(value) => value?.charAt(0).toUpperCase() + value?.slice(1)"
|
||||
@change="updateSearchProjects()"
|
||||
>
|
||||
<div>
|
||||
<span class="font-semibold text-primary"
|
||||
>{{ formatMessage(commonMessages.sortByLabel) }}:
|
||||
</span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</div>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="results display-mode--list">
|
||||
@@ -444,6 +447,7 @@ import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
DropdownSelect,
|
||||
IntlFormatted,
|
||||
ProjectCard,
|
||||
StyledInput,
|
||||
@@ -451,7 +455,6 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
|
||||
import ATLauncherLogo from '~/assets/images/external/atlauncher.svg?component'
|
||||
import PrismLauncherLogo from '~/assets/images/external/prism.svg?component'
|
||||
@@ -464,6 +467,7 @@ const { formatMessage } = useVIntl()
|
||||
|
||||
const searchQuery = ref('leave')
|
||||
const sortType = ref('relevance')
|
||||
const sortOptions = ['relevance', 'downloads', 'follows', 'updated', 'newest']
|
||||
|
||||
const PROJECT_COUNT = 100000
|
||||
|
||||
@@ -955,7 +959,7 @@ const creatorFeatureMessages = defineMessages({
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.iconified-input {
|
||||
width: 100%;
|
||||
@@ -977,19 +981,6 @@ const creatorFeatureMessages = defineMessages({
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selector {
|
||||
min-width: 8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,17 +186,12 @@
|
||||
<div class="push-right">
|
||||
<div class="labeled-control-row">
|
||||
Sort by
|
||||
<Multiselect
|
||||
<DropdownSelect
|
||||
v-model="sortBy"
|
||||
:searchable="false"
|
||||
class="small-select"
|
||||
:options="['Name', 'Status', 'Type']"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@update:model-value="
|
||||
sortedProjects = updateSort(sortedProjects, sortBy, descending)
|
||||
"
|
||||
class="!w-auto"
|
||||
name="Sort by"
|
||||
:options="sortOptions"
|
||||
@change="sortedProjects = updateSort(sortedProjects, sortBy, descending)"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="descending ? 'Descending' : 'Ascending'"
|
||||
@@ -331,6 +326,7 @@ import {
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
CopyCode,
|
||||
DropdownSelect,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
ProjectStatusBadge,
|
||||
@@ -338,7 +334,6 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
|
||||
import ModalCreation from '~/components/ui/create/ProjectCreateModal.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 selectedProjects = ref([])
|
||||
const sortBy = ref('Name')
|
||||
const sortOptions = ['Name', 'Status', 'Type']
|
||||
const descending = ref(false)
|
||||
const editLinksModal = ref(null)
|
||||
|
||||
@@ -674,11 +670,6 @@ const onBulkEditLinks = async () => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.small-select {
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.label-button[data-active='true'] {
|
||||
--background-color: var(--color-red);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"three": "^0.172.0",
|
||||
"vue-i18n": "^10.0.0",
|
||||
"vue-multiselect": "3.0.0",
|
||||
"vue-select": "4.0.0-beta.6",
|
||||
"vue-typed-virtual-list": "^1.0.10",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -1002,7 +1002,7 @@
|
||||
"defaultMessage": "Singleplayer"
|
||||
},
|
||||
"label.sort-by": {
|
||||
"defaultMessage": "Sort by"
|
||||
"defaultMessage": "Sort by: "
|
||||
},
|
||||
"label.success": {
|
||||
"defaultMessage": "Success"
|
||||
|
||||
@@ -295,7 +295,7 @@ export const commonMessages = defineMessages({
|
||||
},
|
||||
sortByLabel: {
|
||||
id: 'label.sort-by',
|
||||
defaultMessage: 'Sort by',
|
||||
defaultMessage: 'Sort by: ',
|
||||
},
|
||||
stopButton: {
|
||||
id: 'button.stop',
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -143,9 +143,6 @@ importers:
|
||||
vue-i18n:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.8(vue@3.5.27(typescript@5.9.3))
|
||||
vue-multiselect:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
vue-router:
|
||||
specifier: ^4.6.0
|
||||
version: 4.6.4(vue@3.5.27(typescript@5.9.3))
|
||||
@@ -353,9 +350,6 @@ importers:
|
||||
vue-confetti-explosion:
|
||||
specifier: ^1.0.2
|
||||
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:
|
||||
specifier: ^1.0.10
|
||||
version: 1.0.10(vue@3.5.27(typescript@5.9.3))
|
||||
@@ -676,9 +670,6 @@ importers:
|
||||
vue-i18n:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.8(vue@3.5.27(typescript@5.9.3))
|
||||
vue-multiselect:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
vue-select:
|
||||
specifier: 4.0.0-beta.6
|
||||
version: 4.0.0-beta.6(vue@3.5.27(typescript@5.9.3))
|
||||
@@ -9467,14 +9458,6 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==}
|
||||
peerDependencies:
|
||||
@@ -19605,10 +19588,6 @@ snapshots:
|
||||
dependencies:
|
||||
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)):
|
||||
dependencies:
|
||||
vue: 3.5.27(typescript@5.9.3)
|
||||
|
||||
Reference in New Issue
Block a user