This commit is contained in:
@@ -43,6 +43,20 @@ export function createInventoryImageRepository(db) {
|
|||||||
return image?.image ?? null;
|
return image?.image ?? null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async listByInventoryItemForSeller(inventoryItemId, sellerId) {
|
||||||
|
return db
|
||||||
|
.select({ image: inventoryImages })
|
||||||
|
.from(inventoryImages)
|
||||||
|
.innerJoin(inventoryItems, eq(inventoryItems.id, inventoryImages.inventoryItemId))
|
||||||
|
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
||||||
|
.where(and(
|
||||||
|
eq(inventoryImages.inventoryItemId, inventoryItemId),
|
||||||
|
eq(products.sellerId, sellerId),
|
||||||
|
))
|
||||||
|
.orderBy(inventoryImages.sortOrder)
|
||||||
|
.then((rows) => rows.map(({ image }) => image));
|
||||||
|
},
|
||||||
|
|
||||||
async updateOrderForSeller(imageId, sortOrder, sellerId) {
|
async updateOrderForSeller(imageId, sortOrder, sellerId) {
|
||||||
const owned = await this.findByIdForSeller(imageId, sellerId);
|
const owned = await this.findByIdForSeller(imageId, sellerId);
|
||||||
if (!owned) return null;
|
if (!owned) return null;
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ export async function registerAdminImageRoutes(app, options) {
|
|||||||
return reply.code(201).send({ data: toAdminInventoryImageDto(image) });
|
return reply.code(201).send({ data: toAdminInventoryImageDto(image) });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/inventory/:id/images', async (request) => ({
|
||||||
|
data: (await inventoryImageService.list({
|
||||||
|
inventoryItemId: request.params.id,
|
||||||
|
sellerId: (await authenticateAdmin(request)).sellerId,
|
||||||
|
})).map(toAdminInventoryImageDto),
|
||||||
|
}));
|
||||||
|
|
||||||
async function authorizeAdminMutation(request) {
|
async function authorizeAdminMutation(request) {
|
||||||
const admin = await authenticateAdmin(request);
|
const admin = await authenticateAdmin(request);
|
||||||
if (!admin?.sellerId) {
|
if (!admin?.sellerId) {
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export function createInventoryImageService({ inventoryImageRepository, imageDer
|
|||||||
return image;
|
return image;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async list({ inventoryItemId, sellerId }) {
|
||||||
|
return inventoryImageRepository.listByInventoryItemForSeller(inventoryItemId, sellerId);
|
||||||
|
},
|
||||||
|
|
||||||
async upload(input) {
|
async upload(input) {
|
||||||
const { data, ...metadata } = input;
|
const { data, ...metadata } = input;
|
||||||
validateInventoryImageMetadata(metadata);
|
validateInventoryImageMetadata(metadata);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default { title: 'Organisms/AdminImageManager', component: AdminImageMana
|
|||||||
export const Completed = { args: { images, processingStatus: 'COMPLETED' } };
|
export const Completed = { args: { images, processingStatus: 'COMPLETED' } };
|
||||||
export const Processing = { args: { images, processingStatus: 'PROCESSING' } };
|
export const Processing = { args: { images, processingStatus: 'PROCESSING' } };
|
||||||
export const Failed = { args: { images, processingStatus: 'FAILED', errorMessage: 'Derivative generation failed.' } };
|
export const Failed = { args: { images, processingStatus: 'FAILED', errorMessage: 'Derivative generation failed.' } };
|
||||||
|
export const Saving = { args: { images, processingStatus: 'COMPLETED', isSaving: true } };
|
||||||
|
|
||||||
export const EditAltTextWithKeyboard = {
|
export const EditAltTextWithKeyboard = {
|
||||||
args: { images, processingStatus: 'COMPLETED' },
|
args: { images, processingStatus: 'COMPLETED' },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const props = defineProps({
|
|||||||
images: { type: Array, required: true },
|
images: { type: Array, required: true },
|
||||||
processingStatus: { type: String, default: 'COMPLETED' },
|
processingStatus: { type: String, default: 'COMPLETED' },
|
||||||
errorMessage: { type: String, default: '' },
|
errorMessage: { type: String, default: '' },
|
||||||
|
isSaving: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['reorder', 'primary', 'alt-text', 'retry', 'delete']);
|
const emit = defineEmits(['reorder', 'primary', 'alt-text', 'retry', 'delete']);
|
||||||
const altText = ref(props.images.find((image) => image.isPrimary)?.altText ?? '');
|
const altText = ref(props.images.find((image) => image.isPrimary)?.altText ?? '');
|
||||||
@@ -39,6 +40,7 @@ function saveAltText() {
|
|||||||
|
|
||||||
<ImageGallery :images="orderedImages" />
|
<ImageGallery :images="orderedImages" />
|
||||||
|
|
||||||
|
<p v-if="isSaving" class="state state--loading" role="status">Saving image changes...</p>
|
||||||
<div class="admin-image-manager__controls">
|
<div class="admin-image-manager__controls">
|
||||||
<div class="admin-image-manager__row">
|
<div class="admin-image-manager__row">
|
||||||
<ConditionBadge :label="orderedImages.find((image) => image.isPrimary)?.altText || 'Primary image'" :selected="true" />
|
<ConditionBadge :label="orderedImages.find((image) => image.isPrimary)?.altText || 'Primary image'" :selected="true" />
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteImage,
|
||||||
|
listAdminImages,
|
||||||
|
reorderImage,
|
||||||
|
retryImageProcessing,
|
||||||
|
setPrimaryImage,
|
||||||
|
updateImageAltText,
|
||||||
|
} from '@/services/admin-image-service.js';
|
||||||
|
|
||||||
|
function csrfToken() {
|
||||||
|
return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAdminImages(inventoryItemId) {
|
||||||
|
const images = ref([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const error = ref(null);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
isLoading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try { images.value = await listAdminImages(inventoryItemId); }
|
||||||
|
catch (requestError) { error.value = requestError; }
|
||||||
|
finally { isLoading.value = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apply(action) {
|
||||||
|
isSaving.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try { await action(csrfToken()); await load(); }
|
||||||
|
catch (requestError) { error.value = requestError; }
|
||||||
|
finally { isSaving.value = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
images,
|
||||||
|
isLoading,
|
||||||
|
isSaving,
|
||||||
|
error,
|
||||||
|
load,
|
||||||
|
reorder: (imageId, sortOrder) => apply((token) => reorderImage(imageId, sortOrder, token)),
|
||||||
|
setPrimary: (imageId) => apply((token) => setPrimaryImage(imageId, token)),
|
||||||
|
updateAltText: (imageId, altText) => apply((token) => updateImageAltText(imageId, altText, token)),
|
||||||
|
retry: (imageId) => apply((token) => retryImageProcessing(imageId, token)),
|
||||||
|
remove: (imageId) => apply((token) => deleteImage(imageId, token)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { RouterLink, useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import AdminImageManager from '@/components/organisms/AdminImageManager.vue';
|
||||||
|
import { useAdminImages } from '@/composables/useAdminImages.js';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const inventoryItemId = route.params.id;
|
||||||
|
const { images, isLoading, isSaving, error, load, reorder, setPrimary, updateAltText, retry, remove } = useAdminImages(inventoryItemId);
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="detail-page">
|
||||||
|
<RouterLink class="detail-back" :to="{ name: 'catalog' }">Back to catalog</RouterLink>
|
||||||
|
<p class="catalog-kicker">Admin inventory</p>
|
||||||
|
<h1>Manage images</h1>
|
||||||
|
<p v-if="isLoading" class="catalog-state" role="status">Loading images...</p>
|
||||||
|
<p v-else-if="error" class="catalog-state catalog-state--error" role="alert">{{ error.message }}</p>
|
||||||
|
<AdminImageManager
|
||||||
|
v-else
|
||||||
|
:images="images"
|
||||||
|
:processing-status="isSaving ? 'PROCESSING' : (images[0]?.processingStatus ?? 'COMPLETED')"
|
||||||
|
@reorder="reorder($event.id, $event.sortOrder)"
|
||||||
|
@primary="setPrimary"
|
||||||
|
@alt-text="updateAltText(images[0]?.id, $event)"
|
||||||
|
@retry="retry(images[0]?.id)"
|
||||||
|
@delete="remove"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router';
|
|||||||
import CatalogPage from '@/pages/CatalogPage.vue';
|
import CatalogPage from '@/pages/CatalogPage.vue';
|
||||||
import ListingDetailPage from '@/pages/ListingDetailPage.vue';
|
import ListingDetailPage from '@/pages/ListingDetailPage.vue';
|
||||||
import ProductDetailPage from '@/pages/ProductDetailPage.vue';
|
import ProductDetailPage from '@/pages/ProductDetailPage.vue';
|
||||||
|
import AdminImagePage from '@/pages/AdminImagePage.vue';
|
||||||
|
|
||||||
export default createRouter({
|
export default createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -10,5 +11,6 @@ export default createRouter({
|
|||||||
{ path: '/', name: 'catalog', component: CatalogPage },
|
{ path: '/', name: 'catalog', component: CatalogPage },
|
||||||
{ path: '/listings/:id', name: 'listing-detail', component: ListingDetailPage },
|
{ path: '/listings/:id', name: 'listing-detail', component: ListingDetailPage },
|
||||||
{ path: '/products/:slug', name: 'product-detail', component: ProductDetailPage },
|
{ path: '/products/:slug', name: 'product-detail', component: ProductDetailPage },
|
||||||
|
{ path: '/admin/inventory/:id/images', name: 'admin-image-manager', component: AdminImagePage },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -54,3 +54,10 @@ export function deleteImage(imageId, csrfToken) {
|
|||||||
headers: { 'x-csrf-token': csrfToken },
|
headers: { 'x-csrf-token': csrfToken },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listAdminImages(inventoryItemId) {
|
||||||
|
return request(`/admin/inventory/${encodeURIComponent(inventoryItemId)}/images`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user