Skip to content

Commit 4d9660b

Browse files
fix: product edit save not working
1 parent 97cf88b commit 4d9660b

4 files changed

Lines changed: 99 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,14 @@ await user.load((loader) => loader.load('orders'))
171171
// Role check — role middleware handles this, but in code:
172172
// admin implicitly has supplier access (check middleware/role.ts)
173173

174-
// Method spoofing for DELETE/PUT in HTML forms:
175-
// <input type="hidden" name="_method" value="DELETE">
174+
// Method spoofing works ONLY from the query string — never from the body:
175+
// POST /supplier/products/16?_method=PUT ✅
176+
// POST /supplier/products/16 + `_method: 'PUT'` field in the body ❌ 404
177+
// The bodyparser is router middleware, so it runs AFTER route matching —
178+
// `_method` in the body is invisible to the router. From Inertia always issue a
179+
// real router.put()/form.put()/router.delete(); it works with forceFormData too
180+
// (unlike PHP, the Adonis bodyparser parses multipart on PUT/PATCH/DELETE), and
181+
// the Inertia middleware upgrades the 302 redirect to 303 for you.
176182
```
177183

178184
### Vue / Inertia Patterns

inertia/pages/supplier/products/edit.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,14 @@ function onDescriptionEnter() {
141141
function submit() {
142142
if (submitDisabled.value) return
143143
144+
// Must be a real PUT — AdonisJS only honours `_method` spoofing from the query
145+
// string, never from the request body, and the body is parsed after routing.
144146
form
145147
.transform((data) => ({
146148
...data,
147-
_method: 'PUT',
148149
allergenIds: JSON.stringify(data.allergenIds),
149150
}))
150-
.post(`/supplier/products/${props.product.id}`, {
151+
.put(`/supplier/products/${props.product.id}`, {
151152
forceFormData: true,
152153
preserveScroll: true,
153154
onFinish: () => {

tests/e2e/supplier_products.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,43 @@ test.describe('Supplier products flow', () => {
5454
await expect(saveButton).toBeEnabled()
5555
})
5656

57+
test('saving the edit form persists the change', async ({ page }) => {
58+
const createdName = `E2E Editace ${Date.now()}`
59+
const renamedTo = `${createdName} upraveno`
60+
61+
await loginAs(page, 'supplier')
62+
63+
// Work on an own product so the seeded catalogue stays untouched.
64+
await page.goto('/supplier/products/new')
65+
await page.locator('#product-name').fill(createdName)
66+
await page.locator('#product-description').fill('E2E editace produktu')
67+
await page.getByRole('combobox').first().click()
68+
await page.getByRole('option', { name: 'Nealko' }).click()
69+
await page.locator('input[type="file"]').setInputFiles({
70+
name: 'e2e-edit.png',
71+
mimeType: 'image/png',
72+
buffer: Buffer.from(
73+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9WlH0JkAAAAASUVORK5CYII=',
74+
'base64'
75+
),
76+
})
77+
await page.getByRole('button', { name: 'Vytvořit produkt' }).click()
78+
await expect(page).toHaveURL(/\/supplier\/stock\?preselect=\d+/)
79+
80+
const productId = page.url().match(/preselect=(\d+)/)![1]
81+
82+
await page.goto(`/supplier/products/${productId}/edit`)
83+
await page.locator('#edit-product-name').fill(renamedTo)
84+
await page.getByRole('button', { name: 'Uložit změny' }).click()
85+
86+
await expect(page).toHaveURL(/\/supplier\/stock$/)
87+
await expect(page.getByText(`Produkt „${renamedTo}“ byl upraven.`)).toBeVisible()
88+
89+
// The change must really be persisted, not just flashed.
90+
await page.goto(`/supplier/products/${productId}/edit`)
91+
await expect(page.locator('#edit-product-name')).toHaveValue(renamedTo)
92+
})
93+
5794
test('supplier products category tags keep white text color', async ({ page }) => {
5895
await loginAs(page, 'supplier')
5996
await page.goto('/supplier/products')

tests/functional/web/supplier.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,57 @@ test.group('Web Supplier - products', (group) => {
195195
assert.isNotNull(product)
196196
})
197197

198+
test('updating product via PUT persists changes and redirects to stock', async ({
199+
client,
200+
assert,
201+
}) => {
202+
const supplier = await UserFactory.apply('supplier').create()
203+
const category = await CategoryFactory.create()
204+
const product = await ProductFactory.merge({
205+
categoryId: category.id,
206+
displayName: 'Před úpravou',
207+
}).create()
208+
209+
const response = await client
210+
.put(`/supplier/products/${product.id}`)
211+
.loginAs(supplier)
212+
.withCsrfToken()
213+
.field('displayName', 'Po úpravě')
214+
.field('description', 'Nový popis')
215+
.field('categoryId', category.id)
216+
.field('barcode', '987654321')
217+
.field('allergenIds', JSON.stringify([]))
218+
.redirects(0)
219+
220+
response.assertStatus(302)
221+
assert.equal(response.header('location'), '/supplier/stock')
222+
223+
await product.refresh()
224+
assert.equal(product.displayName, 'Po úpravě')
225+
assert.equal(product.description, 'Nový popis')
226+
assert.equal(product.barcode, '987654321')
227+
})
228+
229+
test('product update is not routable as POST with _method in the body', async ({ client }) => {
230+
// AdonisJS only honours `_method` spoofing from the query string, and the
231+
// bodyparser runs after route matching — so a body-only `_method` 404s.
232+
const supplier = await UserFactory.apply('supplier').create()
233+
const category = await CategoryFactory.create()
234+
const product = await ProductFactory.merge({ categoryId: category.id }).create()
235+
236+
const response = await client
237+
.post(`/supplier/products/${product.id}`)
238+
.loginAs(supplier)
239+
.withCsrfToken()
240+
.field('displayName', 'Spoofed')
241+
.field('description', 'Spoofed')
242+
.field('categoryId', category.id)
243+
.field('_method', 'PUT')
244+
.redirects(0)
245+
246+
response.assertStatus(404)
247+
})
248+
198249
test('stock page returns preselect value from query string', async ({ client, assert }) => {
199250
const supplier = await UserFactory.apply('supplier').create()
200251
const response = await client

0 commit comments

Comments
 (0)