diff --git a/.gitignore b/.gitignore index ec6a56c66..da91bfc92 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ app-playground-data/* .astro .claude +.letta # labrinth demo fixtures apps/labrinth/fixtures/demo diff --git a/apps/frontend/src/pages/[type]/[id].vue b/apps/frontend/src/pages/[type]/[id].vue index a44ae0e9c..f6bc2767c 100644 --- a/apps/frontend/src/pages/[type]/[id].vue +++ b/apps/frontend/src/pages/[type]/[id].vue @@ -1647,6 +1647,15 @@ async function resetVersions() { 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 const patchProjectMutation = useMutation({ mutationFn: async ({ projectId, data }) => { @@ -1691,12 +1700,7 @@ const patchProjectMutation = useMutation({ }, onSettled: async (_data, _error, { projectId }) => { - // Invalidate both slug-based and ID-based cache keys to ensure consistency - 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] }) + await invalidateProjectQueries(projectId) }, }) @@ -1741,11 +1745,7 @@ const patchStatusMutation = useMutation({ }, onSettled: async (_data, _error, { projectId }) => { - // Invalidate both slug-based and ID-based cache keys to ensure consistency - await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] }) - if (routeProjectId.value !== projectId) { - await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] }) - } + await invalidateProjectQueries(projectId) }, }) @@ -1779,8 +1779,171 @@ const patchIconMutation = useMutation({ }, onSettled: async (_data, _error, { projectId }) => { - await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId] }) - await queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] }) + await invalidateProjectQueries(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() { // Simply invalidate and refetch - the computed allMembers will auto-update await queryClient.invalidateQueries({ queryKey: ['project', projectId.value, 'members'] }) @@ -2103,6 +2311,11 @@ provideProjectPageContext({ patchProject, patchIcon, setProcessing, + + // Gallery mutation functions + createGalleryItem, + editGalleryItem, + deleteGalleryItem, }) diff --git a/apps/frontend/src/pages/[type]/[id]/settings/description.vue b/apps/frontend/src/pages/[type]/[id]/settings/description.vue index 7c09d0b39..78fa0482c 100644 --- a/apps/frontend/src/pages/[type]/[id]/settings/description.vue +++ b/apps/frontend/src/pages/[type]/[id]/settings/description.vue @@ -14,7 +14,7 @@ {{ descriptionWarning }} -
- -
+ diff --git a/apps/frontend/src/pages/[type]/[id]/settings/tags.vue b/apps/frontend/src/pages/[type]/[id]/settings/tags.vue index 1d827c7ce..203730ba7 100644 --- a/apps/frontend/src/pages/[type]/[id]/settings/tags.vue +++ b/apps/frontend/src/pages/[type]/[id]/settings/tags.vue @@ -65,7 +65,7 @@ -

+

Select at least one category in order to feature a category.

@@ -116,32 +116,27 @@
- -
- -
+
diff --git a/apps/frontend/src/pages/organization/[id]/settings/index.vue b/apps/frontend/src/pages/organization/[id]/settings/index.vue index c4575b0de..fe013f247 100644 --- a/apps/frontend/src/pages/organization/[id]/settings/index.vue +++ b/apps/frontend/src/pages/organization/[id]/settings/index.vue @@ -1,6 +1,14 @@