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:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import { HistoryIcon, SaveIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { isEqual } from 'es-toolkit'
|
||||
import { type Component, computed } from 'vue'
|
||||
|
||||
import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n'
|
||||
@@ -38,15 +39,9 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const shown = computed(() => {
|
||||
let changed = false
|
||||
for (const key of Object.keys(props.modified)) {
|
||||
if (props.original[key] !== props.modified[key]) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
})
|
||||
const shown = computed(() =>
|
||||
Object.keys(props.modified).some((key) => !isEqual(props.original[key], props.modified[key])),
|
||||
)
|
||||
|
||||
function localizeIfPossible(message: MessageDescriptor | string) {
|
||||
return typeof message === 'string' ? message : formatMessage(message)
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useSavable,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -20,8 +20,6 @@ const { currentMember, projectV2, projectV3, refreshProject } = injectProjectPag
|
||||
const { handleError } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
const supportsEnvironment = computed(() =>
|
||||
projectV3.value.project_types.some((type) => ['mod', 'modpack'].includes(type)),
|
||||
)
|
||||
@@ -36,26 +34,29 @@ const needsToVerify = computed(
|
||||
|
||||
const hasPermission = computed(() => {
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
return (currentMember.value?.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||
return ((currentMember.value?.permissions ?? 0) & EDIT_DETAILS) === EDIT_DETAILS
|
||||
})
|
||||
|
||||
function getInitialEnv() {
|
||||
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(),
|
||||
side_types_migration_review_status: projectV3.value.side_types_migration_review_status,
|
||||
}),
|
||||
({ environment, side_types_migration_review_status }) => {
|
||||
saving.value = true
|
||||
side_types_migration_review_status = 'reviewed'
|
||||
client.labrinth.projects_v3
|
||||
.edit(projectV2.value.id, { environment, side_types_migration_review_status })
|
||||
.then(() => refreshProject().then(reset))
|
||||
.catch(handleError)
|
||||
.finally(() => (saving.value = false))
|
||||
async ({ environment }) => {
|
||||
try {
|
||||
await client.labrinth.projects_v3.edit(projectV2.value.id, {
|
||||
environment,
|
||||
side_types_migration_review_status: 'reviewed',
|
||||
})
|
||||
await refreshProject()
|
||||
reset()
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
},
|
||||
)
|
||||
// Set current to reviewed, which will trigger unsaved changes popup.
|
||||
|
||||
@@ -31,6 +31,21 @@ export interface ProjectPageContext {
|
||||
patchProject: (data: Record<string, unknown>, quiet?: boolean) => Promise<boolean>
|
||||
patchIcon: (icon: File) => Promise<boolean>
|
||||
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] =
|
||||
|
||||
@@ -1,37 +1,56 @@
|
||||
import { isEqual } from 'es-toolkit'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export function useSavable<T extends Record<string, unknown>>(
|
||||
data: () => T,
|
||||
save: (changes: Partial<T>) => void,
|
||||
save: (changes: Partial<T>) => void | Promise<void>,
|
||||
): {
|
||||
saved: ComputedRef<T>
|
||||
current: Ref<T>
|
||||
changes: ComputedRef<Partial<T>>
|
||||
hasChanges: ComputedRef<boolean>
|
||||
saving: Ref<boolean>
|
||||
reset: () => void
|
||||
save: () => void
|
||||
save: () => Promise<void>
|
||||
} {
|
||||
const savedValues = computed(data)
|
||||
const currentValues = ref({ ...data() }) as Ref<T>
|
||||
const saving = ref(false)
|
||||
|
||||
const changes = computed<Partial<T>>(() => {
|
||||
const values: Partial<T> = {}
|
||||
const keys = Object.keys(currentValues.value) as (keyof T)[]
|
||||
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]
|
||||
}
|
||||
}
|
||||
return values
|
||||
})
|
||||
|
||||
const hasChanges = computed(() => Object.keys(changes.value).length > 0)
|
||||
|
||||
const reset = () => {
|
||||
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 {
|
||||
saved: savedValues,
|
||||
current: currentValues,
|
||||
changes,
|
||||
hasChanges,
|
||||
saving,
|
||||
reset,
|
||||
save: saveInternal,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user