refactor: project saving logic (#5225)

* fix: project data saving not visually shown immediately

* feat: useSavable improvements

* feat: migrate where possible to useSavable

* fix: gitignore

* feat: use es-toolkit
This commit is contained in:
Calum H.
2026-01-28 16:46:14 +00:00
committed by GitHub
parent e57c15b3ce
commit 400c571fe6
15 changed files with 699 additions and 507 deletions

1
.gitignore vendored
View File

@@ -65,6 +65,7 @@ app-playground-data/*
.astro .astro
.claude .claude
.letta
# labrinth demo fixtures # labrinth demo fixtures
apps/labrinth/fixtures/demo apps/labrinth/fixtures/demo

View File

@@ -1647,6 +1647,15 @@ async function resetVersions() {
await resetVersionsV3() await resetVersionsV3()
} }
// Helper to invalidate project queries after mutations settle
async function invalidateProjectQueries(projectId) {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
if (routeProjectId.value !== projectId) {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] })
}
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] })
}
// Mutation for patching project data // Mutation for patching project data
const patchProjectMutation = useMutation({ const patchProjectMutation = useMutation({
mutationFn: async ({ projectId, data }) => { mutationFn: async ({ projectId, data }) => {
@@ -1691,12 +1700,7 @@ const patchProjectMutation = useMutation({
}, },
onSettled: async (_data, _error, { projectId }) => { onSettled: async (_data, _error, { projectId }) => {
// Invalidate both slug-based and ID-based cache keys to ensure consistency await invalidateProjectQueries(projectId)
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
if (routeProjectId.value !== projectId) {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] })
}
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] })
}, },
}) })
@@ -1741,11 +1745,7 @@ const patchStatusMutation = useMutation({
}, },
onSettled: async (_data, _error, { projectId }) => { onSettled: async (_data, _error, { projectId }) => {
// Invalidate both slug-based and ID-based cache keys to ensure consistency await invalidateProjectQueries(projectId)
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
if (routeProjectId.value !== projectId) {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] })
}
}, },
}) })
@@ -1779,8 +1779,171 @@ const patchIconMutation = useMutation({
}, },
onSettled: async (_data, _error, { projectId }) => { onSettled: async (_data, _error, { projectId }) => {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] }) await invalidateProjectQueries(projectId)
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] }) },
})
const createGalleryItemMutation = useMutation({
mutationFn: async ({ projectId, file, title, description, featured, ordering }) => {
let url = `project/${projectId}/gallery?ext=${
file.type.split('/')[file.type.split('/').length - 1]
}&featured=${featured ?? false}`
if (title) {
url += `&title=${encodeURIComponent(title)}`
}
if (description) {
url += `&description=${encodeURIComponent(description)}`
}
if (ordering !== null && ordering !== undefined) {
url += `&ordering=${ordering}`
}
await useBaseFetch(url, {
method: 'POST',
body: file,
})
},
onMutate: async ({ title, description, featured, ordering }) => {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
if (!old) return old
const newItem = {
url: '',
raw_url: '',
featured: featured ?? false,
title: title ?? '',
description: description ?? '',
created: new Date().toISOString(),
ordering: ordering ?? old.gallery.length,
}
return {
...old,
gallery: [...old.gallery, newItem],
}
})
return { previousProject }
},
onError: (err, _variables, context) => {
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err.message,
type: 'error',
})
},
onSettled: async (_data, _error, { projectId }) => {
await invalidateProjectQueries(projectId)
},
})
const editGalleryItemMutation = useMutation({
mutationFn: async ({ projectId, imageUrl, title, description, featured, ordering }) => {
let url = `project/${projectId}/gallery?url=${encodeURIComponent(imageUrl)}&featured=${featured ?? false}`
if (title) {
url += `&title=${encodeURIComponent(title)}`
}
if (description) {
url += `&description=${encodeURIComponent(description)}`
}
if (ordering !== null && ordering !== undefined) {
url += `&ordering=${ordering}`
}
await useBaseFetch(url, {
method: 'PATCH',
})
},
onMutate: async ({ imageUrl, title, description, featured, ordering }) => {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
if (!old) return old
return {
...old,
gallery: old.gallery.map((item) => {
if (item.url === imageUrl) {
return {
...item,
title: title ?? item.title,
description: description ?? item.description,
featured: featured ?? item.featured,
ordering: ordering ?? item.ordering,
}
}
return item
}),
}
})
return { previousProject }
},
onError: (err, _variables, context) => {
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err.message,
type: 'error',
})
},
onSettled: async (_data, _error, { projectId }) => {
await invalidateProjectQueries(projectId)
},
})
const deleteGalleryItemMutation = useMutation({
mutationFn: async ({ projectId, imageUrl }) => {
await useBaseFetch(`project/${projectId}/gallery?url=${encodeURIComponent(imageUrl)}`, {
method: 'DELETE',
})
},
onMutate: async ({ imageUrl }) => {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
if (!old) return old
return {
...old,
gallery: old.gallery.filter((item) => item.url !== imageUrl),
}
})
return { previousProject }
},
onError: (err, _variables, context) => {
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err.message,
type: 'error',
})
},
onSettled: async (_data, _error, { projectId }) => {
await invalidateProjectQueries(projectId)
}, },
}) })
@@ -1971,6 +2134,51 @@ async function patchIcon(icon) {
}) })
} }
async function createGalleryItem(file, title, description, featured, ordering) {
startLoading()
return new Promise((resolve) => {
createGalleryItemMutation.mutate(
{ projectId: project.value.id, file, title, description, featured, ordering },
{
onSuccess: () => resolve(true),
onError: () => resolve(false),
onSettled: () => stopLoading(),
},
)
})
}
async function editGalleryItem(imageUrl, title, description, featured, ordering) {
startLoading()
return new Promise((resolve) => {
editGalleryItemMutation.mutate(
{ projectId: project.value.id, imageUrl, title, description, featured, ordering },
{
onSuccess: () => resolve(true),
onError: () => resolve(false),
onSettled: () => stopLoading(),
},
)
})
}
async function deleteGalleryItem(imageUrl) {
startLoading()
return new Promise((resolve) => {
deleteGalleryItemMutation.mutate(
{ projectId: project.value.id, imageUrl },
{
onSuccess: () => resolve(true),
onError: () => resolve(false),
onSettled: () => stopLoading(),
},
)
})
}
async function refreshMembers() { async function refreshMembers() {
// Simply invalidate and refetch - the computed allMembers will auto-update // Simply invalidate and refetch - the computed allMembers will auto-update
await queryClient.invalidateQueries({ queryKey: ['project', projectId.value, 'members'] }) await queryClient.invalidateQueries({ queryKey: ['project', projectId.value, 'members'] })
@@ -2103,6 +2311,11 @@ provideProjectPageContext({
patchProject, patchProject,
patchIcon, patchIcon,
setProcessing, setProcessing,
// Gallery mutation functions
createGalleryItem,
editGalleryItem,
deleteGalleryItem,
}) })
</script> </script>

View File

@@ -14,7 +14,7 @@
</span> </span>
</div> </div>
<MarkdownEditor <MarkdownEditor
v-model="description" v-model="current.description"
:disabled=" :disabled="
!currentMember || !currentMember ||
(currentMember?.permissions! & TeamMemberPermission.EDIT_BODY) !== (currentMember?.permissions! & TeamMemberPermission.EDIT_BODY) !==
@@ -26,36 +26,42 @@
<TriangleAlertIcon class="my-auto" /> <TriangleAlertIcon class="my-auto" />
{{ descriptionWarning }} {{ descriptionWarning }}
</div> </div>
<div class="input-group markdown-disclaimer">
<button
:disabled="!hasChanges"
class="iconified-button brand-button"
type="button"
@click="saveChanges()"
>
<SaveIcon />
Save changes
</button>
</div>
</div> </div>
<UnsavedChangesPopup
:original="saved"
:modified="current"
:saving="saving"
@reset="reset"
@save="save"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { SaveIcon, TriangleAlertIcon } from '@modrinth/assets' import { TriangleAlertIcon } from '@modrinth/assets'
import { countText, MIN_DESCRIPTION_CHARS } from '@modrinth/moderation' import { countText, MIN_DESCRIPTION_CHARS } from '@modrinth/moderation'
import { injectProjectPageContext, MarkdownEditor } from '@modrinth/ui' import {
injectProjectPageContext,
MarkdownEditor,
UnsavedChangesPopup,
useSavable,
} from '@modrinth/ui'
import { TeamMemberPermission } from '@modrinth/utils' import { TeamMemberPermission } from '@modrinth/utils'
import { computed, ref } from 'vue' import { computed } from 'vue'
import { useImageUpload } from '~/composables/image-upload.ts' import { useImageUpload } from '~/composables/image-upload.ts'
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext() const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
const description = ref(project.value.body) const { saved, current, saving, reset, save } = useSavable(
() => ({ description: project.value.body }),
async ({ description }) => {
await patchProject({ body: description })
},
)
const descriptionWarning = computed(() => { const descriptionWarning = computed(() => {
const text = description.value?.trim() || '' const text = current.value.description?.trim() || ''
const charCount = countText(text) const charCount = countText(text)
if (charCount < MIN_DESCRIPTION_CHARS) { if (charCount < MIN_DESCRIPTION_CHARS) {
@@ -65,26 +71,6 @@ const descriptionWarning = computed(() => {
return null return null
}) })
const patchRequestPayload = computed(() => {
const payload: {
body?: string
} = {}
if (description.value !== project.value.body) {
payload.body = description.value
}
return payload
})
const hasChanges = computed(() => {
return Object.keys(patchRequestPayload.value).length > 0
})
function saveChanges() {
patchProject(patchRequestPayload.value)
}
async function onUploadHandler(file: File) { async function onUploadHandler(file: File) {
const response = await useImageUpload(file, { const response = await useImageUpload(file, {
context: 'project', context: 'project',

View File

@@ -299,15 +299,19 @@ import {
ConfirmModal, ConfirmModal,
DropArea, DropArea,
FileInput, FileInput,
injectNotificationManager,
injectProjectPageContext, injectProjectPageContext,
NewModal as Modal, NewModal as Modal,
} from '@modrinth/ui' } from '@modrinth/ui'
import { isPermission } from '~/utils/permissions.ts' import { isPermission } from '~/utils/permissions.ts'
const { addNotification } = injectNotificationManager() const {
const { projectV2: project, currentMember, refreshProject } = injectProjectPageContext() projectV2: project,
currentMember,
createGalleryItem: createGalleryItemMutation,
editGalleryItem: editGalleryItemMutation,
deleteGalleryItem: deleteGalleryItemMutation,
} = injectProjectPageContext()
const title = `${project.value.title} - Gallery` const title = `${project.value.title} - Gallery`
const description = `View ${project.value.gallery?.length ?? 0} images of ${project.value.title} on Modrinth.` const description = `View ${project.value.gallery?.length ?? 0} images of ${project.value.title} on Modrinth.`
@@ -391,103 +395,42 @@ const showPreviewImage = () => {
const createGalleryItem = async () => { const createGalleryItem = async () => {
shouldPreventActions.value = true shouldPreventActions.value = true
startLoading()
try { const success = await createGalleryItemMutation(
let url = `project/${project.value.id}/gallery?ext=${ editFile.value,
editFile.value editTitle.value || undefined,
? editFile.value.type.split('/')[editFile.value.type.split('/').length - 1] editDescription.value || undefined,
: null editFeatured.value,
}&featured=${editFeatured.value}` editOrder.value ?? undefined,
)
if (editTitle.value) {
url += `&title=${encodeURIComponent(editTitle.value)}`
}
if (editDescription.value) {
url += `&description=${encodeURIComponent(editDescription.value)}`
}
if (editOrder.value) {
url += `&ordering=${editOrder.value}`
}
await useBaseFetch(url, {
method: 'POST',
body: editFile.value,
})
await refreshProject()
if (success) {
modal_edit_item.value.hide() modal_edit_item.value.hide()
} catch (err) {
addNotification({
title: 'An error occurred',
text: err.data ? err.data.description : err,
type: 'error',
})
} }
stopLoading()
shouldPreventActions.value = false shouldPreventActions.value = false
} }
const editGalleryItem = async () => { const editGalleryItem = async () => {
shouldPreventActions.value = true shouldPreventActions.value = true
startLoading()
try {
let url = `project/${project.value.id}/gallery?url=${encodeURIComponent(
project.value.gallery[editIndex.value].url,
)}&featured=${editFeatured.value}`
if (editTitle.value) { const success = await editGalleryItemMutation(
url += `&title=${encodeURIComponent(editTitle.value)}` project.value.gallery[editIndex.value].url,
} editTitle.value || undefined,
if (editDescription.value) { editDescription.value || undefined,
url += `&description=${encodeURIComponent(editDescription.value)}` editFeatured.value,
} editOrder.value ?? undefined,
if (editOrder.value) { )
url += `&ordering=${editOrder.value}`
}
await useBaseFetch(url, { if (success) {
method: 'PATCH',
})
await refreshProject()
modal_edit_item.value.hide() modal_edit_item.value.hide()
} catch (err) {
addNotification({
title: 'An error occurred',
text: err.data ? err.data.description : err,
type: 'error',
})
} }
stopLoading()
shouldPreventActions.value = false shouldPreventActions.value = false
} }
const deleteGalleryImage = async () => { const deleteGalleryImage = async () => {
startLoading() await deleteGalleryItemMutation(project.value.gallery[deleteIndex.value].url)
try {
await useBaseFetch(
`project/${project.value.id}/gallery?url=${encodeURIComponent(
project.value.gallery[deleteIndex.value].url,
)}`,
{
method: 'DELETE',
},
)
await refreshProject()
} catch (err) {
addNotification({
title: 'An error occurred',
text: err.data ? err.data.description : err,
type: 'error',
})
}
stopLoading()
} }
const handleKeydown = (e) => { const handleKeydown = (e) => {

View File

@@ -2,8 +2,6 @@
import { import {
defineMessages, defineMessages,
IconSelect, IconSelect,
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext, injectProjectPageContext,
type MessageDescriptor, type MessageDescriptor,
SettingsLabel, SettingsLabel,
@@ -14,34 +12,21 @@ import {
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { projectV2: project, refreshProject } = injectProjectPageContext() const { projectV2: project, patchProject } = injectProjectPageContext()
const { handleError } = injectNotificationManager()
const client = injectModrinthClient()
const saving = ref(false) const { saved, current, saving, reset, save } = useSavable(
const { saved, current, reset, save } = useSavable(
() => ({ () => ({
title: project.value.title, title: project.value.title,
tagline: project.value.description, tagline: project.value.description,
url: project.value.slug, url: project.value.slug,
icon: project.value.icon_url, icon: project.value.icon_url,
}), }),
({ title, tagline, url }) => { async ({ title, tagline, url }) => {
const data: Record<string, string> = { await patchProject({
...(title !== undefined && { title }), ...(title !== undefined && { title }),
...(tagline !== undefined && { description: tagline }), ...(tagline !== undefined && { description: tagline }),
...(url !== undefined && { slug: url }), ...(url !== undefined && { slug: url }),
} })
if (data) {
saving.value = true
client.labrinth.projects_v2
.edit(project.value.id, { title, description: tagline, slug: url })
.then(() => refreshProject().then(reset))
.catch(handleError)
.finally(() => (saving.value = false))
}
}, },
) )

View File

@@ -28,7 +28,7 @@
<div class="w-1/2"> <div class="w-1/2">
<DropdownSelect <DropdownSelect
v-model="license" v-model="current.license"
name="License selector" name="License selector"
:options="builtinLicenses" :options="builtinLicenses"
:display-name="(chosen: BuiltinLicense) => chosen.friendly" :display-name="(chosen: BuiltinLicense) => chosen.friendly"
@@ -37,7 +37,7 @@
</div> </div>
</div> </div>
<div v-if="license.requiresOnlyOrLater" class="adjacent-input"> <div v-if="current.license.requiresOnlyOrLater" class="adjacent-input">
<label for="or-later-checkbox"> <label for="or-later-checkbox">
<span class="label__title">Later editions</span> <span class="label__title">Later editions</span>
<span class="label__description"> <span class="label__description">
@@ -48,7 +48,7 @@
<Checkbox <Checkbox
id="or-later-checkbox" id="or-later-checkbox"
v-model="allowOrLater" v-model="current.allowOrLater"
:disabled="!hasPermission" :disabled="!hasPermission"
description="Allow later editions" description="Allow later editions"
class="w-1/2" class="w-1/2"
@@ -60,7 +60,7 @@
<div class="adjacent-input"> <div class="adjacent-input">
<label for="license-url"> <label for="license-url">
<span class="label__title">License URL</span> <span class="label__title">License URL</span>
<span v-if="license?.friendly !== 'Custom'" class="label__description"> <span v-if="current.license?.friendly !== 'Custom'" class="label__description">
The web location of the full license text. If you don't provide a link, the license text The web location of the full license text. If you don't provide a link, the license text
will be displayed instead. will be displayed instead.
</span> </span>
@@ -73,18 +73,20 @@
<div class="w-1/2"> <div class="w-1/2">
<input <input
id="license-url" id="license-url"
v-model="licenseUrl" v-model="current.licenseUrl"
type="url" type="url"
maxlength="2048" maxlength="2048"
:placeholder="license?.friendly !== 'Custom' ? `License URL (optional)` : `License URL`" :placeholder="
current.license?.friendly !== 'Custom' ? `License URL (optional)` : `License URL`
"
:disabled="!hasPermission || licenseId === 'LicenseRef-Unknown'" :disabled="!hasPermission || licenseId === 'LicenseRef-Unknown'"
class="w-full" class="w-full"
/> />
</div> </div>
</div> </div>
<div v-if="license?.friendly === 'Custom'" class="adjacent-input"> <div v-if="current.license?.friendly === 'Custom'" class="adjacent-input">
<label v-if="!nonSpdxLicense" for="license-spdx"> <label v-if="!current.nonSpdxLicense" for="license-spdx">
<span class="label__title">SPDX identifier</span> <span class="label__title">SPDX identifier</span>
<span class="label__description"> <span class="label__description">
If your license does not have an offical If your license does not have an offical
@@ -93,7 +95,7 @@
>, check the box and enter the name of the license instead. >, check the box and enter the name of the license instead.
</span> </span>
</label> </label>
<label v-else for="license-name"> <label v-if="current.nonSpdxLicense" for="license-name">
<span class="label__title">License name</span> <span class="label__title">License name</span>
<span class="label__description" <span class="label__description"
>The full name of the license. If the license has a SPDX identifier, please uncheck the >The full name of the license. If the license has a SPDX identifier, please uncheck the
@@ -103,9 +105,9 @@
<div class="input-stack w-1/2"> <div class="input-stack w-1/2">
<input <input
v-if="!nonSpdxLicense" v-if="!current.nonSpdxLicense"
id="license-spdx" id="license-spdx"
v-model="license.short" v-model="current.license.short"
class="w-full" class="w-full"
type="text" type="text"
maxlength="128" maxlength="128"
@@ -115,7 +117,7 @@
<input <input
v-else v-else
id="license-name" id="license-name"
v-model="license.short" v-model="current.license.short"
class="w-full" class="w-full"
type="text" type="text"
maxlength="128" maxlength="128"
@@ -124,8 +126,8 @@
/> />
<Checkbox <Checkbox
v-if="license?.friendly === 'Custom'" v-if="current.license?.friendly === 'Custom'"
v-model="nonSpdxLicense" v-model="current.nonSpdxLicense"
:disabled="!hasPermission" :disabled="!hasPermission"
description="License does not have a SPDX identifier" description="License does not have a SPDX identifier"
> >
@@ -133,74 +135,91 @@
</Checkbox> </Checkbox>
</div> </div>
</div> </div>
<div class="input-stack">
<button
type="button"
class="iconified-button brand-button"
:disabled="
!hasChanges ||
!hasPermission ||
(license.friendly === 'Custom' && (license.short === '' || licenseUrl === ''))
"
@click="saveChanges()"
>
<SaveIcon />
Save changes
</button>
</div>
</section> </section>
<UnsavedChangesPopup
:original="saved"
:modified="current"
:saving="saving"
:can-save="
hasPermission &&
!(
current.license.friendly === 'Custom' &&
(current.license.short === '' || current.licenseUrl === '')
)
"
@reset="reset"
@save="save"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SaveIcon } from '@modrinth/assets' import {
import { Checkbox, DropdownSelect, injectProjectPageContext } from '@modrinth/ui' Checkbox,
DropdownSelect,
injectProjectPageContext,
UnsavedChangesPopup,
useSavable,
} from '@modrinth/ui'
import { import {
type BuiltinLicense, type BuiltinLicense,
builtinLicenses, builtinLicenses,
formatProjectType, formatProjectType,
TeamMemberPermission, TeamMemberPermission,
} from '@modrinth/utils' } from '@modrinth/utils'
import { computed, type Ref, ref } from 'vue' import { computed } from 'vue'
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext() const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
const licenseUrl = ref(project.value.license.url) function getInitialLicense() {
const license: Ref<{ const oldLicenseId = project.value.license.id
friendly: string const trimmedLicenseId = oldLicenseId
short: string .replaceAll('-only', '')
requiresOnlyOrLater?: boolean .replaceAll('-or-later', '')
}> = ref({ .replaceAll('LicenseRef-', '')
friendly: '',
short: '',
requiresOnlyOrLater: false,
})
const allowOrLater = ref(project.value.license.id.includes('-or-later')) if (oldLicenseId === 'LicenseRef-Unknown') {
const nonSpdxLicense = ref(project.value.license.id.includes('LicenseRef-')) return {
friendly: '',
const oldLicenseId = project.value.license.id short: oldLicenseId.replaceAll('LicenseRef-', ''),
const trimmedLicenseId = oldLicenseId requiresOnlyOrLater: false,
.replaceAll('-only', '') }
.replaceAll('-or-later', '')
.replaceAll('LicenseRef-', '')
license.value = builtinLicenses.find((x) => x.short === trimmedLicenseId) ?? {
friendly: 'Custom',
short: oldLicenseId.replaceAll('LicenseRef-', ''),
requiresOnlyOrLater: oldLicenseId.includes('-or-later'),
}
if (oldLicenseId === 'LicenseRef-Unknown') {
// Mark it as not having a license, forcing the user to select one
license.value = {
friendly: '',
short: oldLicenseId.replaceAll('LicenseRef-', ''),
requiresOnlyOrLater: false,
} }
return (
builtinLicenses.find((x) => x.short === trimmedLicenseId) ?? {
friendly: 'Custom',
short: oldLicenseId.replaceAll('LicenseRef-', ''),
requiresOnlyOrLater: oldLicenseId.includes('-or-later'),
}
)
} }
const { saved, current, saving, reset, save } = useSavable(
() => ({
license: getInitialLicense(),
licenseUrl: project.value.license.url ?? '',
allowOrLater: project.value.license.id.includes('-or-later'),
nonSpdxLicense: project.value.license.id.includes('LicenseRef-'),
}),
async () => {
const payload: {
license_id?: string
license_url?: string | null
} = {}
if (licenseId.value !== project.value.license.id) {
payload.license_id = licenseId.value
}
if (current.value.licenseUrl !== project.value.license.url) {
payload.license_url = current.value.licenseUrl ? current.value.licenseUrl : null
}
await patchProject(payload)
},
)
const hasPermission = computed(() => { const hasPermission = computed(() => {
return (currentMember.value?.permissions ?? 0) & TeamMemberPermission.EDIT_DETAILS return (currentMember.value?.permissions ?? 0) & TeamMemberPermission.EDIT_DETAILS
}) })
@@ -209,47 +228,22 @@ const licenseId = computed(() => {
let id = '' let id = ''
if ( if (
(nonSpdxLicense.value && license.value.friendly === 'Custom') || (current.value.nonSpdxLicense && current.value.license.friendly === 'Custom') ||
license.value.short === 'All-Rights-Reserved' || current.value.license.short === 'All-Rights-Reserved' ||
license.value.short === 'Unknown' current.value.license.short === 'Unknown'
) { ) {
id += 'LicenseRef-' id += 'LicenseRef-'
} }
id += license.value.short id += current.value.license.short
if (license.value.requiresOnlyOrLater) { if (current.value.license.requiresOnlyOrLater) {
id += allowOrLater.value ? '-or-later' : '-only' id += current.value.allowOrLater ? '-or-later' : '-only'
} }
if (nonSpdxLicense.value && license.value.friendly === 'Custom') { if (current.value.nonSpdxLicense && current.value.license.friendly === 'Custom') {
id = id.replaceAll(' ', '-') id = id.replaceAll(' ', '-')
} }
return id return id
}) })
const patchRequestPayload = computed(() => {
const payload: {
license_id?: string
license_url?: string | null // null = remove url
} = {}
if (licenseId.value !== project.value.license.id) {
payload.license_id = licenseId.value
}
if (licenseUrl.value !== project.value.license.url) {
payload.license_url = licenseUrl.value ? licenseUrl.value : null
}
return payload
})
const hasChanges = computed(() => {
return Object.keys(patchRequestPayload.value).length > 0
})
function saveChanges() {
patchProject(patchRequestPayload.value)
}
</script> </script>

View File

@@ -65,7 +65,7 @@
<Checkbox <Checkbox
v-for="category in categoryLists[header]" v-for="category in categoryLists[header]"
:key="`category-${header}-${category.name}`" :key="`category-${header}-${category.name}`"
:model-value="selectedTags.includes(category)" :model-value="current.selectedTags.includes(category)"
:description="formatCategory(category.name)" :description="formatCategory(category.name)"
class="category-selector" class="category-selector"
@update:model-value="toggleCategory(category)" @update:model-value="toggleCategory(category)"
@@ -91,17 +91,17 @@
featured if you do not select all 3. featured if you do not select all 3.
</span> </span>
</div> </div>
<p v-if="selectedTags.length < 1"> <p v-if="current.selectedTags.length < 1">
Select at least one category in order to feature a category. Select at least one category in order to feature a category.
</p> </p>
<div class="category-list input-div"> <div class="category-list input-div">
<Checkbox <Checkbox
v-for="category in selectedTags" v-for="category in current.selectedTags"
:key="`featured-category-${category.name}`" :key="`featured-category-${category.name}`"
class="category-selector" class="category-selector"
:model-value="featuredTags.includes(category)" :model-value="current.featuredTags.includes(category)"
:description="formatCategory(category.name)" :description="formatCategory(category.name)"
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)" :disabled="current.featuredTags.length >= 3 && !current.featuredTags.includes(category)"
@update:model-value="toggleFeaturedCategory(category)" @update:model-value="toggleFeaturedCategory(category)"
> >
<div class="category-selector__label"> <div class="category-selector__label">
@@ -116,32 +116,27 @@
</Checkbox> </Checkbox>
</div> </div>
</template> </template>
<div class="button-group">
<button
type="button"
class="iconified-button brand-button"
:disabled="!hasChanges"
@click="saveChanges()"
>
<SaveIcon />
Save changes
</button>
</div>
</section> </section>
<UnsavedChangesPopup
:original="saved"
:modified="current"
:saving="saving"
@reset="reset"
@save="save"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SaveIcon, StarIcon, TriangleAlertIcon } from '@modrinth/assets' import { StarIcon, TriangleAlertIcon } from '@modrinth/assets'
import { Checkbox, injectProjectPageContext } from '@modrinth/ui' import { Checkbox, injectProjectPageContext, UnsavedChangesPopup, useSavable } from '@modrinth/ui'
import { import {
formatCategory, formatCategory,
formatCategoryHeader, formatCategoryHeader,
formatProjectType, formatProjectType,
sortedCategories, sortedCategories,
} from '@modrinth/utils' } from '@modrinth/utils'
import { computed, ref } from 'vue' import { computed } from 'vue'
interface Category { interface Category {
name: string name: string
@@ -154,21 +149,56 @@ const tags = useGeneratedState()
const { projectV2: project, patchProject } = injectProjectPageContext() const { projectV2: project, patchProject } = injectProjectPageContext()
const selectedTags = ref<Category[]>( const { saved, current, saving, reset, save } = useSavable(
sortedCategories(tags.value).filter( () => ({
(x: Category) => selectedTags: sortedCategories(tags.value).filter(
x.project_type === project.value.actualProjectType && (x: Category) =>
(project.value.categories.includes(x.name) || x.project_type === project.value.actualProjectType &&
project.value.additional_categories.includes(x.name)), (project.value.categories.includes(x.name) ||
), project.value.additional_categories.includes(x.name)),
) ) as Category[],
featuredTags: sortedCategories(tags.value).filter(
(x: Category) =>
x.project_type === project.value.actualProjectType &&
project.value.categories.includes(x.name),
) as Category[],
}),
async () => {
// Promote selected categories to featured if there are less than 3 featured
const newFeaturedTags = current.value.featuredTags.slice()
if (newFeaturedTags.length < 1 && current.value.selectedTags.length > newFeaturedTags.length) {
const nonFeaturedCategories = current.value.selectedTags.filter(
(x) => !newFeaturedTags.includes(x),
)
nonFeaturedCategories
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
.forEach((x) => newFeaturedTags.push(x))
}
const featuredTags = ref<Category[]>( // Convert selected and featured categories to backend-usable arrays
sortedCategories(tags.value).filter( const categories = newFeaturedTags.map((x) => x.name)
(x: Category) => const additionalCategories = current.value.selectedTags
x.project_type === project.value.actualProjectType && .filter((x) => !newFeaturedTags.includes(x))
project.value.categories.includes(x.name), .map((x) => x.name)
),
const data: Record<string, string[]> = {}
if (
categories.length !== project.value.categories.length ||
categories.some((value) => !project.value.categories.includes(value))
) {
data.categories = categories
}
if (
additionalCategories.length !== project.value.additional_categories.length ||
additionalCategories.some((value) => !project.value.additional_categories.includes(value))
) {
data.additional_categories = additionalCategories
}
await patchProject(data)
},
) )
const categoryLists = computed(() => { const categoryLists = computed(() => {
@@ -186,7 +216,7 @@ const categoryLists = computed(() => {
}) })
const tooManyTagsWarning = computed(() => { const tooManyTagsWarning = computed(() => {
const tagCount = selectedTags.value.length const tagCount = current.value.selectedTags.length
if (tagCount > 8) { if (tagCount > 8) {
return `You've selected ${tagCount} tags. Consider reducing to 8 or fewer to keep your project focused and easier to discover.` return `You've selected ${tagCount} tags. Consider reducing to 8 or fewer to keep your project focused and easier to discover.`
} }
@@ -196,7 +226,7 @@ const tooManyTagsWarning = computed(() => {
const multipleResolutionTagsWarning = computed(() => { const multipleResolutionTagsWarning = computed(() => {
if (project.value.actualProjectType !== 'resourcepack') return null if (project.value.actualProjectType !== 'resourcepack') return null
const resolutionTags = selectedTags.value.filter((tag) => const resolutionTags = current.value.selectedTags.filter((tag) =>
['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+'].includes(tag.name), ['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+'].includes(tag.name),
) )
@@ -217,7 +247,7 @@ const allTagsSelectedWarning = computed(() => {
const categoriesForProjectType = sortedCategories(tags.value).filter( const categoriesForProjectType = sortedCategories(tags.value).filter(
(x: Category) => x.project_type === project.value.actualProjectType, (x: Category) => x.project_type === project.value.actualProjectType,
) )
const totalSelectedTags = selectedTags.value.length const totalSelectedTags = current.value.selectedTags.length
if ( if (
totalSelectedTags === categoriesForProjectType.length && totalSelectedTags === categoriesForProjectType.length &&
@@ -228,68 +258,22 @@ const allTagsSelectedWarning = computed(() => {
return null return null
}) })
const patchData = computed(() => {
const data: Record<string, string[]> = {}
// Promote selected categories to featured if there are less than 3 featured
const newFeaturedTags = featuredTags.value.slice()
if (newFeaturedTags.length < 1 && selectedTags.value.length > newFeaturedTags.length) {
const nonFeaturedCategories = selectedTags.value.filter((x) => !newFeaturedTags.includes(x))
nonFeaturedCategories
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
.forEach((x) => newFeaturedTags.push(x))
}
// Convert selected and featured categories to backend-usable arrays
const categories = newFeaturedTags.map((x) => x.name)
const additionalCategories = selectedTags.value
.filter((x) => !newFeaturedTags.includes(x))
.map((x) => x.name)
if (
categories.length !== project.value.categories.length ||
categories.some((value) => !project.value.categories.includes(value))
) {
data.categories = categories
}
if (
additionalCategories.length !== project.value.additional_categories.length ||
additionalCategories.some((value) => !project.value.additional_categories.includes(value))
) {
data.additional_categories = additionalCategories
}
return data
})
const hasChanges = computed(() => {
return Object.keys(patchData.value).length > 0
})
const toggleCategory = (category: Category) => { const toggleCategory = (category: Category) => {
if (selectedTags.value.includes(category)) { if (current.value.selectedTags.includes(category)) {
selectedTags.value = selectedTags.value.filter((x) => x !== category) current.value.selectedTags = current.value.selectedTags.filter((x) => x !== category)
if (featuredTags.value.includes(category)) { if (current.value.featuredTags.includes(category)) {
featuredTags.value = featuredTags.value.filter((x) => x !== category) current.value.featuredTags = current.value.featuredTags.filter((x) => x !== category)
} }
} else { } else {
selectedTags.value.push(category) current.value.selectedTags = [...current.value.selectedTags, category]
} }
} }
const toggleFeaturedCategory = (category: Category) => { const toggleFeaturedCategory = (category: Category) => {
if (featuredTags.value.includes(category)) { if (current.value.featuredTags.includes(category)) {
featuredTags.value = featuredTags.value.filter((x) => x !== category) current.value.featuredTags = current.value.featuredTags.filter((x) => x !== category)
} else { } else {
featuredTags.value.push(category) current.value.featuredTags = [...current.value.featuredTags, category]
}
}
const saveChanges = () => {
if (hasChanges.value) {
patchProject(patchData.value)
} }
} }
</script> </script>

View File

@@ -1,6 +1,14 @@
<script setup> <script setup>
import { SaveIcon, TrashIcon, UploadIcon } from '@modrinth/assets' import { TrashIcon, UploadIcon } from '@modrinth/assets'
import { Avatar, Button, ConfirmModal, FileInput, injectNotificationManager } from '@modrinth/ui' import {
Avatar,
Button,
ConfirmModal,
FileInput,
injectNotificationManager,
UnsavedChangesPopup,
useSavable,
} from '@modrinth/ui'
import { injectOrganizationContext } from '~/providers/organization-context.ts' import { injectOrganizationContext } from '~/providers/organization-context.ts'
@@ -14,32 +22,49 @@ const {
patchOrganization, patchOrganization,
} = injectOrganizationContext() } = injectOrganizationContext()
// Icon state (separate from useSavable, like collection page)
const icon = ref(null) const icon = ref(null)
const deletedIcon = ref(false) const deletedIcon = ref(false)
const previewImage = ref(null) const previewImage = ref(null)
const name = ref(organization.value.name) const {
const slug = ref(organization.value.slug) saved,
current,
saving,
hasChanges: hasFieldChanges,
reset: resetFields,
} = useSavable(
() => ({
name: organization.value.name,
slug: organization.value.slug,
summary: organization.value.description,
}),
async ({ name, slug, summary }) => {
await patchOrganization({
...(name !== undefined && { name }),
...(slug !== undefined && { slug }),
...(summary !== undefined && { description: summary }),
})
},
)
const summary = ref(organization.value.description) // Combined state for UnsavedChangesPopup
const originalState = computed(() => ({
...saved.value,
iconChanged: false,
}))
const patchData = computed(() => { const modifiedState = computed(() => ({
const data = {} ...current.value,
if (name.value !== organization.value.name) { iconChanged: !!(deletedIcon.value || icon.value),
data.name = name.value }))
}
if (slug.value !== organization.value.slug) {
data.slug = slug.value
}
if (summary.value !== organization.value.description) {
data.description = summary.value
}
return data
})
const hasChanges = computed(() => { const reset = () => {
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value resetFields()
}) icon.value = null
deletedIcon.value = false
previewImage.value = null
}
const markIconForDeletion = () => { const markIconForDeletion = () => {
deletedIcon.value = true deletedIcon.value = true
@@ -61,11 +86,16 @@ const showPreviewImage = (files) => {
const orgId = useRouteId() const orgId = useRouteId()
const onSaveChanges = useClientTry(async () => { const save = async () => {
// Only PATCH organization details if there are actual field changes // Save field changes via useSavable
const hasOrgFieldChanges = Object.keys(patchData.value).length > 0 if (hasFieldChanges.value) {
if (hasOrgFieldChanges) { await patchOrganization({
await patchOrganization(patchData.value) ...(current.value.name !== organization.value.name && { name: current.value.name }),
...(current.value.slug !== organization.value.slug && { slug: current.value.slug }),
...(current.value.summary !== organization.value.description && {
description: current.value.summary,
}),
})
} }
// Handle icon deletion / upload separately // Handle icon deletion / upload separately
@@ -85,7 +115,7 @@ const onSaveChanges = useClientTry(async () => {
text: 'Your organization has been updated.', text: 'Your organization has been updated.',
type: 'success', type: 'success',
}) })
}) }
const onDeleteOrganization = useClientTry(async () => { const onDeleteOrganization = useClientTry(async () => {
await useBaseFetch(`organization/${orgId}`, { await useBaseFetch(`organization/${orgId}`, {
@@ -159,7 +189,7 @@ const onDeleteOrganization = useClientTry(async () => {
</label> </label>
<input <input
id="project-name" id="project-name"
v-model="name" v-model="current.name"
maxlength="2048" maxlength="2048"
type="text" type="text"
:disabled="!hasPermission" :disabled="!hasPermission"
@@ -172,7 +202,7 @@ const onDeleteOrganization = useClientTry(async () => {
<div class="text-input-wrapper__before">https://modrinth.com/organization/</div> <div class="text-input-wrapper__before">https://modrinth.com/organization/</div>
<input <input
id="project-slug" id="project-slug"
v-model="slug" v-model="current.slug"
type="text" type="text"
maxlength="64" maxlength="64"
autocomplete="off" autocomplete="off"
@@ -186,17 +216,11 @@ const onDeleteOrganization = useClientTry(async () => {
<div class="textarea-wrapper summary-input"> <div class="textarea-wrapper summary-input">
<textarea <textarea
id="project-summary" id="project-summary"
v-model="summary" v-model="current.summary"
maxlength="256" maxlength="256"
:disabled="!hasPermission" :disabled="!hasPermission"
/> />
</div> </div>
<div class="button-group">
<Button color="primary" :disabled="!hasChanges" @click="onSaveChanges">
<SaveIcon />
Save changes
</Button>
</div>
</div> </div>
<div class="universal-card"> <div class="universal-card">
<div class="label"> <div class="label">
@@ -213,6 +237,13 @@ const onDeleteOrganization = useClientTry(async () => {
Delete organization Delete organization
</Button> </Button>
</div> </div>
<UnsavedChangesPopup
:original="originalState"
:modified="modifiedState"
:saving="saving"
@reset="reset"
@save="save"
/>
</div> </div>
</template> </template>

View File

@@ -56,41 +56,32 @@
{{ formatMessage(messages.usernameDescription) }} {{ formatMessage(messages.usernameDescription) }}
</span> </span>
</label> </label>
<input id="username-field" v-model="username" type="text" /> <input id="username-field" v-model="current.username" type="text" />
<label for="bio-field"> <label for="bio-field">
<span class="label__title">{{ formatMessage(messages.bioTitle) }}</span> <span class="label__title">{{ formatMessage(messages.bioTitle) }}</span>
<span class="label__description"> <span class="label__description">
{{ formatMessage(messages.bioDescription) }} {{ formatMessage(messages.bioDescription) }}
</span> </span>
</label> </label>
<textarea id="bio-field" v-model="bio" type="text" /> <textarea id="bio-field" v-model="current.bio" type="text" />
<div v-if="hasUnsavedChanges" class="input-group"> <div class="input-group">
<Button color="primary" :action="() => saveChanges()">
<SaveIcon /> {{ formatMessage(commonMessages.saveChangesButton) }}
</Button>
<Button :action="() => cancel()">
<XIcon /> {{ formatMessage(commonMessages.cancelButton) }}
</Button>
</div>
<div v-else class="input-group">
<Button disabled color="primary" :action="() => saveChanges()">
<SaveIcon />
{{
saved
? formatMessage(commonMessages.changesSavedLabel)
: formatMessage(commonMessages.saveChangesButton)
}}
</Button>
<Button :link="`/user/${auth.user.username}`"> <Button :link="`/user/${auth.user.username}`">
<UserIcon /> {{ formatMessage(commonMessages.visitYourProfile) }} <UserIcon /> {{ formatMessage(commonMessages.visitYourProfile) }}
</Button> </Button>
</div> </div>
</section> </section>
<UnsavedChangesPopup
:original="originalState"
:modified="modifiedState"
:saving="saving"
@reset="reset"
@save="save"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { SaveIcon, TrashIcon, UndoIcon, UploadIcon, UserIcon, XIcon } from '@modrinth/assets' import { TrashIcon, UndoIcon, UploadIcon, UserIcon } from '@modrinth/assets'
import { import {
Avatar, Avatar,
Button, Button,
@@ -99,6 +90,8 @@ import {
FileInput, FileInput,
injectNotificationManager, injectNotificationManager,
IntlFormatted, IntlFormatted,
UnsavedChangesPopup,
useSavable,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -151,21 +144,43 @@ const messages = defineMessages({
const auth = await useAuth() const auth = await useAuth()
const username = ref(auth.value.user.username) // Avatar state (separate from useSavable)
const bio = ref(auth.value.user.bio)
const avatarUrl = ref(auth.value.user.avatar_url) const avatarUrl = ref(auth.value.user.avatar_url)
const icon = shallowRef(null) const icon = shallowRef(null)
const previewImage = shallowRef(null) const previewImage = shallowRef(null)
const pendingAvatarDeletion = ref(false) const pendingAvatarDeletion = ref(false)
const saved = ref(false) const saving = ref(false)
const hasUnsavedChanges = computed( const {
() => saved,
username.value !== auth.value.user.username || current,
bio.value !== auth.value.user.bio || reset: resetFields,
previewImage.value, } = useSavable(
() => ({
username: auth.value.user.username,
bio: auth.value.user.bio ?? '',
}),
async () => {}, // Save is handled manually due to complex icon logic
) )
// Combined state for UnsavedChangesPopup
const originalState = computed(() => ({
...saved.value,
avatarChanged: false,
}))
const modifiedState = computed(() => ({
...current.value,
avatarChanged: !!(previewImage.value || pendingAvatarDeletion.value),
}))
const reset = () => {
resetFields()
icon.value = null
previewImage.value = null
pendingAvatarDeletion.value = false
}
function showPreviewImage(files) { function showPreviewImage(files) {
const reader = new FileReader() const reader = new FileReader()
icon.value = files[0] icon.value = files[0]
@@ -180,16 +195,8 @@ function removePreviewImage() {
previewImage.value = 'https://cdn.modrinth.com/placeholder.png' previewImage.value = 'https://cdn.modrinth.com/placeholder.png'
} }
function cancel() { async function save() {
icon.value = null saving.value = true
previewImage.value = null
pendingAvatarDeletion.value = false
username.value = auth.value.user.username
bio.value = auth.value.user.bio
}
async function saveChanges() {
startLoading()
try { try {
if (pendingAvatarDeletion.value) { if (pendingAvatarDeletion.value) {
await useBaseFetch(`user/${auth.value.user.id}/icon`, { await useBaseFetch(`user/${auth.value.user.id}/icon`, {
@@ -215,12 +222,12 @@ async function saveChanges() {
const body = {} const body = {}
if (auth.value.user.username !== username.value) { if (auth.value.user.username !== current.value.username) {
body.username = username.value body.username = current.value.username
} }
if (auth.value.user.bio !== bio.value) { if (auth.value.user.bio !== current.value.bio) {
body.bio = bio.value body.bio = current.value.bio
} }
await useBaseFetch(`user/${auth.value.user.id}`, { await useBaseFetch(`user/${auth.value.user.id}`, {
@@ -229,7 +236,6 @@ async function saveChanges() {
}) })
await useAuth(auth.value.token) await useAuth(auth.value.token)
avatarUrl.value = auth.value.user.avatar_url avatarUrl.value = auth.value.user.avatar_url
saved.value = true
} catch (err) { } catch (err) {
addNotification({ addNotification({
title: 'An error occurred', title: 'An error occurred',
@@ -243,7 +249,7 @@ async function saveChanges() {
type: 'error', type: 'error',
}) })
} }
stopLoading() saving.value = false
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -53,8 +53,6 @@
}, },
"dependencies": { "dependencies": {
"@codemirror/commands": "^6.3.2", "@codemirror/commands": "^6.3.2",
"intl-messageformat": "^10.7.7",
"vue-i18n": "^10.0.0",
"@codemirror/lang-markdown": "^6.2.3", "@codemirror/lang-markdown": "^6.2.3",
"@codemirror/language": "^6.9.3", "@codemirror/language": "^6.9.3",
"@codemirror/state": "^6.3.2", "@codemirror/state": "^6.3.2",
@@ -73,13 +71,16 @@
"ace-builds": "^1.43.5", "ace-builds": "^1.43.5",
"apexcharts": "^4.0.0", "apexcharts": "^4.0.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"es-toolkit": "^1.44.0",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"intl-messageformat": "^10.7.7",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"postprocessing": "^6.37.6", "postprocessing": "^6.37.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-multiselect": "3.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",

View File

@@ -1,5 +1,6 @@
<script setup lang="ts" generic="T"> <script setup lang="ts" generic="T">
import { HistoryIcon, SaveIcon, SpinnerIcon } from '@modrinth/assets' import { HistoryIcon, SaveIcon, SpinnerIcon } from '@modrinth/assets'
import { isEqual } from 'es-toolkit'
import { type Component, computed } from 'vue' import { type Component, computed } from 'vue'
import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n' import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n'
@@ -38,15 +39,9 @@ const props = withDefaults(
}, },
) )
const shown = computed(() => { const shown = computed(() =>
let changed = false Object.keys(props.modified).some((key) => !isEqual(props.original[key], props.modified[key])),
for (const key of Object.keys(props.modified)) { )
if (props.original[key] !== props.modified[key]) {
changed = true
}
}
return changed
})
function localizeIfPossible(message: MessageDescriptor | string) { function localizeIfPossible(message: MessageDescriptor | string) {
return typeof message === 'string' ? message : formatMessage(message) return typeof message === 'string' ? message : formatMessage(message)

View File

@@ -12,7 +12,7 @@ import {
useSavable, useSavable,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { computed, ref } from 'vue' import { computed } from 'vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -20,8 +20,6 @@ const { currentMember, projectV2, projectV3, refreshProject } = injectProjectPag
const { handleError } = injectNotificationManager() const { handleError } = injectNotificationManager()
const client = injectModrinthClient() const client = injectModrinthClient()
const saving = ref(false)
const supportsEnvironment = computed(() => const supportsEnvironment = computed(() =>
projectV3.value.project_types.some((type) => ['mod', 'modpack'].includes(type)), projectV3.value.project_types.some((type) => ['mod', 'modpack'].includes(type)),
) )
@@ -36,26 +34,29 @@ const needsToVerify = computed(
const hasPermission = computed(() => { const hasPermission = computed(() => {
const EDIT_DETAILS = 1 << 2 const EDIT_DETAILS = 1 << 2
return (currentMember.value?.permissions & EDIT_DETAILS) === EDIT_DETAILS return ((currentMember.value?.permissions ?? 0) & EDIT_DETAILS) === EDIT_DETAILS
}) })
function getInitialEnv() { function getInitialEnv() {
return projectV3.value.environment?.length === 1 ? projectV3.value.environment[0] : undefined return projectV3.value.environment?.length === 1 ? projectV3.value.environment[0] : undefined
} }
const { saved, current, reset, save } = useSavable( const { saved, current, saving, reset, save } = useSavable(
() => ({ () => ({
environment: getInitialEnv(), environment: getInitialEnv(),
side_types_migration_review_status: projectV3.value.side_types_migration_review_status, side_types_migration_review_status: projectV3.value.side_types_migration_review_status,
}), }),
({ environment, side_types_migration_review_status }) => { async ({ environment }) => {
saving.value = true try {
side_types_migration_review_status = 'reviewed' await client.labrinth.projects_v3.edit(projectV2.value.id, {
client.labrinth.projects_v3 environment,
.edit(projectV2.value.id, { environment, side_types_migration_review_status }) side_types_migration_review_status: 'reviewed',
.then(() => refreshProject().then(reset)) })
.catch(handleError) await refreshProject()
.finally(() => (saving.value = false)) reset()
} catch (err) {
handleError(err as Error)
}
}, },
) )
// Set current to reviewed, which will trigger unsaved changes popup. // Set current to reviewed, which will trigger unsaved changes popup.

View File

@@ -31,6 +31,21 @@ export interface ProjectPageContext {
patchProject: (data: Record<string, unknown>, quiet?: boolean) => Promise<boolean> patchProject: (data: Record<string, unknown>, quiet?: boolean) => Promise<boolean>
patchIcon: (icon: File) => Promise<boolean> patchIcon: (icon: File) => Promise<boolean>
setProcessing: () => Promise<void> setProcessing: () => Promise<void>
createGalleryItem: (
file: File,
title?: string,
description?: string,
featured?: boolean,
ordering?: number,
) => Promise<boolean>
editGalleryItem: (
imageUrl: string,
title?: string,
description?: string,
featured?: boolean,
ordering?: number,
) => Promise<boolean>
deleteGalleryItem: (imageUrl: string) => Promise<boolean>
} }
export const [injectProjectPageContext, provideProjectPageContext] = export const [injectProjectPageContext, provideProjectPageContext] =

View File

@@ -1,37 +1,56 @@
import { isEqual } from 'es-toolkit'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
export function useSavable<T extends Record<string, unknown>>( export function useSavable<T extends Record<string, unknown>>(
data: () => T, data: () => T,
save: (changes: Partial<T>) => void, save: (changes: Partial<T>) => void | Promise<void>,
): { ): {
saved: ComputedRef<T> saved: ComputedRef<T>
current: Ref<T> current: Ref<T>
changes: ComputedRef<Partial<T>>
hasChanges: ComputedRef<boolean>
saving: Ref<boolean>
reset: () => void reset: () => void
save: () => void save: () => Promise<void>
} { } {
const savedValues = computed(data) const savedValues = computed(data)
const currentValues = ref({ ...data() }) as Ref<T> const currentValues = ref({ ...data() }) as Ref<T>
const saving = ref(false)
const changes = computed<Partial<T>>(() => { const changes = computed<Partial<T>>(() => {
const values: Partial<T> = {} const values: Partial<T> = {}
const keys = Object.keys(currentValues.value) as (keyof T)[] const keys = Object.keys(currentValues.value) as (keyof T)[]
for (const key of keys) { for (const key of keys) {
if (savedValues.value[key] !== currentValues.value[key]) { if (!isEqual(savedValues.value[key], currentValues.value[key])) {
values[key] = currentValues.value[key] values[key] = currentValues.value[key]
} }
} }
return values return values
}) })
const hasChanges = computed(() => Object.keys(changes.value).length > 0)
const reset = () => { const reset = () => {
currentValues.value = data() currentValues.value = data()
} }
const saveInternal = () => (changes.value ? save(changes.value) : {}) const saveInternal = async () => {
if (!hasChanges.value) return
saving.value = true
try {
await save(changes.value)
} finally {
saving.value = false
}
}
return { return {
saved: savedValues, saved: savedValues,
current: currentValues, current: currentValues,
changes,
hasChanges,
saving,
reset, reset,
save: saveInternal, save: saveInternal,
} }

140
pnpm-lock.yaml generated
View File

@@ -265,7 +265,7 @@ importers:
version: 0.11.3(magicast@0.5.1)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))) version: 0.11.3(magicast@0.5.1)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))
'@sentry/nuxt': '@sentry/nuxt':
specifier: ^10.33.0 specifier: ^10.33.0
version: 10.33.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(rollup@4.54.0)(vue@3.5.26(typescript@5.9.3)) version: 10.33.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(rollup@4.54.0)(vue@3.5.26(typescript@5.9.3))
'@tanstack/vue-query': '@tanstack/vue-query':
specifier: ^5.90.7 specifier: ^5.90.7
version: 5.92.5(vue@3.5.26(typescript@5.9.3)) version: 5.92.5(vue@3.5.26(typescript@5.9.3))
@@ -392,7 +392,7 @@ importers:
version: 10.5.0 version: 10.5.0
nuxt: nuxt:
specifier: ^3.20.2 specifier: ^3.20.2
version: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2) version: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
postcss: postcss:
specifier: ^8.4.39 specifier: ^8.4.39
version: 8.5.6 version: 8.5.6
@@ -634,6 +634,9 @@ importers:
dayjs: dayjs:
specifier: ^1.11.10 specifier: ^1.11.10
version: 1.11.19 version: 1.11.19
es-toolkit:
specifier: ^1.44.0
version: 1.44.0
floating-vue: floating-vue:
specifier: ^5.2.2 specifier: ^5.2.2
version: 5.2.2(@nuxt/kit@3.20.2(magicast@0.5.1))(vue@3.5.26(typescript@5.9.3)) version: 5.2.2(@nuxt/kit@3.20.2(magicast@0.5.1))(vue@3.5.26(typescript@5.9.3))
@@ -724,7 +727,7 @@ importers:
version: 4.0.16(@vitest/browser@4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16))(vitest@4.0.16) version: 4.0.16(@vitest/browser@4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16))(vitest@4.0.16)
eslint-plugin-storybook: eslint-plugin-storybook:
specifier: ^10.1.10 specifier: ^10.1.10
version: 10.1.11(eslint@9.39.2(jiti@2.6.1))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) version: 10.1.11(eslint@9.39.2(jiti@1.21.7))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)
playwright: playwright:
specifier: ^1.57.0 specifier: ^1.57.0
version: 1.57.0 version: 1.57.0
@@ -748,7 +751,7 @@ importers:
version: 5.1.0(vue@3.5.26(typescript@5.9.3)) version: 5.1.0(vue@3.5.26(typescript@5.9.3))
vitest: vitest:
specifier: ^4.0.16 specifier: ^4.0.16
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vue: vue:
specifier: ^3.5.13 specifier: ^3.5.13
version: 3.5.26(typescript@5.9.3) version: 3.5.26(typescript@5.9.3)
@@ -5433,6 +5436,9 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-toolkit@1.44.0:
resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
esast-util-from-estree@2.0.0: esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -8485,6 +8491,7 @@ packages:
tar@7.5.2: tar@7.5.2:
resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==}
engines: {node: '>=18'} engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
terser@5.44.1: terser@5.44.1:
resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==}
@@ -9245,8 +9252,8 @@ packages:
vue-component-type-helpers@3.2.1: vue-component-type-helpers@3.2.1:
resolution: {integrity: sha512-gKV7XOkQl4urSuLHNY1tnVQf7wVgtb/mKbRyxSLWGZUY9RK7aDPhBenTjm+i8ZFe0zC2PZeHMPtOZXZfyaFOzQ==} resolution: {integrity: sha512-gKV7XOkQl4urSuLHNY1tnVQf7wVgtb/mKbRyxSLWGZUY9RK7aDPhBenTjm+i8ZFe0zC2PZeHMPtOZXZfyaFOzQ==}
vue-component-type-helpers@3.2.2: vue-component-type-helpers@3.2.4:
resolution: {integrity: sha512-x8C2nx5XlUNM0WirgfTkHjJGO/ABBxlANZDtHw2HclHtQnn+RFPTnbjMJn8jHZW4TlUam0asHcA14lf1C6Jb+A==} resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
vue-confetti-explosion@1.0.2: vue-confetti-explosion@1.0.2:
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==} resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
@@ -10415,7 +10422,6 @@ snapshots:
dependencies: dependencies:
eslint: 9.39.2(jiti@1.21.7) eslint: 9.39.2(jiti@1.21.7)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))':
dependencies: dependencies:
@@ -11000,7 +11006,7 @@ snapshots:
dependencies: dependencies:
'@nuxt/kit': 4.2.2(magicast@0.5.1) '@nuxt/kit': 4.2.2(magicast@0.5.1)
execa: 8.0.1 execa: 8.0.1
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
@@ -11045,7 +11051,7 @@ snapshots:
sirv: 3.0.2 sirv: 3.0.2
structured-clone-es: 1.0.0 structured-clone-es: 1.0.0
tinyglobby: 0.2.15 tinyglobby: 0.2.15
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)) vite-plugin-inspect: 11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))
vite-plugin-vue-tracer: 1.2.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) vite-plugin-vue-tracer: 1.2.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
which: 5.0.0 which: 5.0.0
@@ -11141,7 +11147,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
'@nuxt/nitro-server@3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2)': '@nuxt/nitro-server@3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2)':
dependencies: dependencies:
'@nuxt/devalue': 2.0.2 '@nuxt/devalue': 2.0.2
'@nuxt/kit': 3.20.2(magicast@0.5.1) '@nuxt/kit': 3.20.2(magicast@0.5.1)
@@ -11159,7 +11165,7 @@ snapshots:
klona: 2.0.6 klona: 2.0.6
mocked-exports: 0.1.1 mocked-exports: 0.1.1
nitropack: 2.12.9(xml2js@0.6.2) nitropack: 2.12.9(xml2js@0.6.2)
nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2) nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
pathe: 2.0.3 pathe: 2.0.3
pkg-types: 2.3.0 pkg-types: 2.3.0
radix3: 1.1.2 radix3: 1.1.2
@@ -11230,7 +11236,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
'@nuxt/vite-builder@3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@1.21.7))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': '@nuxt/vite-builder@3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)':
dependencies: dependencies:
'@nuxt/kit': 3.20.2(magicast@0.5.1) '@nuxt/kit': 3.20.2(magicast@0.5.1)
'@rollup/plugin-replace': 6.0.3(rollup@4.54.0) '@rollup/plugin-replace': 6.0.3(rollup@4.54.0)
@@ -11251,7 +11257,7 @@ snapshots:
magic-string: 0.30.21 magic-string: 0.30.21
mlly: 1.8.0 mlly: 1.8.0
mocked-exports: 0.1.1 mocked-exports: 0.1.1
nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2) nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
ohash: 2.0.11 ohash: 2.0.11
pathe: 2.0.3 pathe: 2.0.3
perfect-debounce: 2.0.0 perfect-debounce: 2.0.0
@@ -11262,9 +11268,9 @@ snapshots:
std-env: 3.10.0 std-env: 3.10.0
ufo: 1.6.1 ufo: 1.6.1
unenv: 2.0.0-rc.24 unenv: 2.0.0-rc.24
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vite-node: 5.2.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite-node: 5.2.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@1.21.7))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3)) vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
vue-bundle-renderer: 2.2.0 vue-bundle-renderer: 2.2.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -12148,7 +12154,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@sentry/nuxt@10.33.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(rollup@4.54.0)(vue@3.5.26(typescript@5.9.3))': '@sentry/nuxt@10.33.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.3.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(rollup@4.54.0)(vue@3.5.26(typescript@5.9.3))':
dependencies: dependencies:
'@nuxt/kit': 3.20.2(magicast@0.5.1) '@nuxt/kit': 3.20.2(magicast@0.5.1)
'@sentry/browser': 10.33.0 '@sentry/browser': 10.33.0
@@ -12159,7 +12165,7 @@ snapshots:
'@sentry/rollup-plugin': 4.6.1(rollup@4.54.0) '@sentry/rollup-plugin': 4.6.1(rollup@4.54.0)
'@sentry/vite-plugin': 4.6.1 '@sentry/vite-plugin': 4.6.1
'@sentry/vue': 10.33.0(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3)) '@sentry/vue': 10.33.0(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2) nuxt: 3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@cloudflare/workers-types' - '@cloudflare/workers-types'
- '@opentelemetry/api' - '@opentelemetry/api'
@@ -12336,7 +12342,7 @@ snapshots:
'@vitest/browser': 4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16) '@vitest/browser': 4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16)
'@vitest/browser-playwright': 4.0.16(playwright@1.57.0)(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16) '@vitest/browser-playwright': 4.0.16(playwright@1.57.0)(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16)
'@vitest/runner': 4.0.16 '@vitest/runner': 4.0.16
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- react - react
- react-dom - react-dom
@@ -12399,7 +12405,7 @@ snapshots:
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
vue-component-type-helpers: 3.2.2 vue-component-type-helpers: 3.2.4
'@stripe/stripe-js@7.9.0': {} '@stripe/stripe-js@7.9.0': {}
@@ -12894,6 +12900,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
'@typescript-eslint/scope-manager': 8.51.0
'@typescript-eslint/types': 8.51.0
'@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3)
eslint: 9.39.2(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
@@ -13008,7 +13025,7 @@ snapshots:
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
'@rolldown/pluginutils': 1.0.0-beta.58 '@rolldown/pluginutils': 1.0.0-beta.58
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5) '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5)
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -13027,7 +13044,7 @@ snapshots:
'@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.53 '@rolldown/pluginutils': 1.0.0-beta.53
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
'@vitest/browser-playwright@4.0.16(playwright@1.57.0)(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16)': '@vitest/browser-playwright@4.0.16(playwright@1.57.0)(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16)':
@@ -13036,7 +13053,7 @@ snapshots:
'@vitest/mocker': 4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)) '@vitest/mocker': 4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))
playwright: 1.57.0 playwright: 1.57.0
tinyrainbow: 3.0.3 tinyrainbow: 3.0.3
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- msw - msw
@@ -13052,7 +13069,7 @@ snapshots:
pngjs: 7.0.0 pngjs: 7.0.0
sirv: 3.0.2 sirv: 3.0.2
tinyrainbow: 3.0.3 tinyrainbow: 3.0.3
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
ws: 8.18.3 ws: 8.18.3
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
@@ -13073,7 +13090,7 @@ snapshots:
obug: 2.1.1 obug: 2.1.1
std-env: 3.10.0 std-env: 3.10.0
tinyrainbow: 3.0.3 tinyrainbow: 3.0.3
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
optionalDependencies: optionalDependencies:
'@vitest/browser': 4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16) '@vitest/browser': 4.0.16(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1))(vitest@4.0.16)
transitivePeerDependencies: transitivePeerDependencies:
@@ -13112,13 +13129,13 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1) vite: 5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)
'@vitest/mocker@4.0.16(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))': '@vitest/mocker@4.0.16(vite@6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))':
dependencies: dependencies:
'@vitest/spy': 4.0.16 '@vitest/spy': 4.0.16
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: optionalDependencies:
vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -14495,6 +14512,8 @@ snapshots:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
es-toolkit@1.44.0: {}
esast-util-from-estree@2.0.0: esast-util-from-estree@2.0.0:
dependencies: dependencies:
'@types/estree-jsx': 1.0.5 '@types/estree-jsx': 1.0.5
@@ -14710,10 +14729,10 @@ snapshots:
dependencies: dependencies:
eslint: 9.39.2(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
eslint-plugin-storybook@10.1.11(eslint@9.39.2(jiti@2.6.1))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3): eslint-plugin-storybook@10.1.11(eslint@9.39.2(jiti@1.21.7))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3):
dependencies: dependencies:
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1) eslint: 9.39.2(jiti@1.21.7)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -14827,7 +14846,6 @@ snapshots:
jiti: 1.21.7 jiti: 1.21.7
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
optional: true
eslint@9.39.2(jiti@2.6.1): eslint@9.39.2(jiti@2.6.1):
dependencies: dependencies:
@@ -16790,16 +16808,16 @@ snapshots:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2): nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2):
dependencies: dependencies:
'@dxup/nuxt': 0.2.2(magicast@0.5.1) '@dxup/nuxt': 0.2.2(magicast@0.5.1)
'@nuxt/cli': 3.31.3(cac@6.7.14)(magicast@0.5.1) '@nuxt/cli': 3.31.3(cac@6.7.14)(magicast@0.5.1)
'@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) '@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
'@nuxt/kit': 3.20.2(magicast@0.5.1) '@nuxt/kit': 3.20.2(magicast@0.5.1)
'@nuxt/nitro-server': 3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2) '@nuxt/nitro-server': 3.20.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(typescript@5.9.3)(xml2js@0.6.2)
'@nuxt/schema': 3.20.2 '@nuxt/schema': 3.20.2
'@nuxt/telemetry': 2.6.6(magicast@0.5.1) '@nuxt/telemetry': 2.6.6(magicast@0.5.1)
'@nuxt/vite-builder': 3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@1.21.7))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@1.21.7))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) '@nuxt/vite-builder': 3.20.2(@types/node@20.19.27)(eslint@9.39.2(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.1)(@types/node@20.19.27)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(sass@1.97.1)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)
'@unhead/vue': 2.1.1(vue@3.5.26(typescript@5.9.3)) '@unhead/vue': 2.1.1(vue@3.5.26(typescript@5.9.3))
'@vue/shared': 3.5.26 '@vue/shared': 3.5.26
c12: 3.3.3(magicast@0.5.1) c12: 3.3.3(magicast@0.5.1)
@@ -18959,12 +18977,12 @@ snapshots:
vite-dev-rpc@1.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)): vite-dev-rpc@1.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)):
dependencies: dependencies:
birpc: 2.9.0 birpc: 2.9.0
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vite-hot-client: 2.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)) vite-hot-client: 2.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))
vite-hot-client@2.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)): vite-hot-client@2.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)):
dependencies: dependencies:
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vite-node@5.2.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2): vite-node@5.2.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
dependencies: dependencies:
@@ -18986,7 +19004,7 @@ snapshots:
- tsx - tsx
- yaml - yaml
vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@1.21.7))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3)): vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3)):
dependencies: dependencies:
'@babel/code-frame': 7.27.1 '@babel/code-frame': 7.27.1
chokidar: 4.0.3 chokidar: 4.0.3
@@ -18995,10 +19013,10 @@ snapshots:
picomatch: 4.0.3 picomatch: 4.0.3
tiny-invariant: 1.3.3 tiny-invariant: 1.3.3
tinyglobby: 0.2.15 tinyglobby: 0.2.15
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vscode-uri: 3.1.0 vscode-uri: 3.1.0
optionalDependencies: optionalDependencies:
eslint: 9.39.2(jiti@1.21.7) eslint: 9.39.2(jiti@2.6.1)
optionator: 0.9.4 optionator: 0.9.4
typescript: 5.9.3 typescript: 5.9.3
vue-tsc: 2.2.12(typescript@5.9.3) vue-tsc: 2.2.12(typescript@5.9.3)
@@ -19013,7 +19031,7 @@ snapshots:
perfect-debounce: 2.0.0 perfect-debounce: 2.0.0
sirv: 3.0.2 sirv: 3.0.2
unplugin-utils: 0.3.1 unplugin-utils: 0.3.1
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vite-dev-rpc: 1.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)) vite-dev-rpc: 1.1.0(vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))
optionalDependencies: optionalDependencies:
'@nuxt/kit': 4.2.2(magicast@0.5.1) '@nuxt/kit': 4.2.2(magicast@0.5.1)
@@ -19027,7 +19045,7 @@ snapshots:
magic-string: 0.30.21 magic-string: 0.30.21
pathe: 2.0.3 pathe: 2.0.3
source-map-js: 1.2.1 source-map-js: 1.2.1
vite: 7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
vite-svg-loader@5.1.0(vue@3.5.26(typescript@5.9.3)): vite-svg-loader@5.1.0(vue@3.5.26(typescript@5.9.3)):
@@ -19047,6 +19065,23 @@ snapshots:
sass: 1.97.1 sass: 1.97.1
terser: 5.44.1 terser: 5.44.1
vite@6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.54.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 20.19.27
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.30.2
sass: 1.97.1
terser: 5.44.1
yaml: 2.8.2
vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2): vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
dependencies: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12
@@ -19064,23 +19099,6 @@ snapshots:
terser: 5.44.1 terser: 5.44.1
yaml: 2.8.2 yaml: 2.8.2
vite@7.3.0(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.54.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 20.19.27
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.30.2
sass: 1.97.1
terser: 5.44.1
yaml: 2.8.2
vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2): vite@7.3.0(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
dependencies: dependencies:
esbuild: 0.27.2 esbuild: 0.27.2
@@ -19102,10 +19120,10 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2): vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/browser-playwright@4.0.16)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2):
dependencies: dependencies:
'@vitest/expect': 4.0.16 '@vitest/expect': 4.0.16
'@vitest/mocker': 4.0.16(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)) '@vitest/mocker': 4.0.16(vite@6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2))
'@vitest/pretty-format': 4.0.16 '@vitest/pretty-format': 4.0.16
'@vitest/runner': 4.0.16 '@vitest/runner': 4.0.16
'@vitest/snapshot': 4.0.16 '@vitest/snapshot': 4.0.16
@@ -19122,7 +19140,7 @@ snapshots:
tinyexec: 1.0.2 tinyexec: 1.0.2
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinyrainbow: 3.0.3 tinyrainbow: 3.0.3
vite: 6.4.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2) vite: 6.4.1(@types/node@20.19.27)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
@@ -19257,7 +19275,7 @@ snapshots:
vue-component-type-helpers@3.2.1: {} vue-component-type-helpers@3.2.1: {}
vue-component-type-helpers@3.2.2: {} vue-component-type-helpers@3.2.4: {}
vue-confetti-explosion@1.0.2(vue@3.5.26(typescript@5.9.3)): vue-confetti-explosion@1.0.2(vue@3.5.26(typescript@5.9.3)):
dependencies: dependencies: