Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions src/dialogs/IconUploadSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -359,12 +359,33 @@ export default {
* emits `updated` with a `null` ref. Never throws: a failed request surfaces in
* `uploadError`.
*
* ⚠️ THE DELETE IS ADDRESSED BY NUMERIC FILE ID, NOT BY FILENAME.
*
* This method used to call
* `DELETE .../objects/{register}/{schema}/{uuid}/files/app-icon-dark.svg`.
* OpenRegister's route for that verb is
*
* ['name' => 'files#delete',
* 'url' => '/api/objects/{register}/{schema}/{id}/files/{fileId}',
* 'verb' => 'DELETE',
* 'requirements' => ['id' => '[^/]+', 'fileId' => '\d+']]
*
* — `fileId` is constrained to `\d+`, so a filename never matched the
* route at all and Nextcloud answered its HTML **404** page. The
* `catch` below then painted the generic "Remove failed" string, so the
* button looked implemented and could never work. Measured both ways on
* a live instance: DELETE by filename → 404; DELETE by the numeric id
* from `GET .../files` → 200 and the attachment is gone.
*
* The id is therefore resolved from the object's own file index, where
* each entry carries `{ id: <int>, title: '<filename>' }`.
*
* @param {'light'|'dark'} variant - Which icon slot to clear; selects both the
* attached filename to delete (`app-icon.svg` / `app-icon-dark.svg`) and the
* Application field to null out (`icon` / `iconDark`).
* @return {Promise<void>}
*
* @spec openspec/changes/retrofit-2026-05-26-creation-wizard-ui/tasks.md#task-4
* @spec openspec/specs/app-icon-management/spec.md#user-removes-the-dark-icon
*/
async removeIcon(variant) {
if (!this.objectUuid) return
Expand All @@ -374,11 +395,26 @@ export default {
const field = variant === 'dark' ? 'iconDark' : 'icon'

try {
// 1. Delete the file from OR.
const deleteUrl = generateUrl(
`/apps/openregister/api/objects/${REGISTER}/${SCHEMA}/${this.objectUuid}/files/${filename}`,
// 1. Resolve the attachment's NUMERIC id, then delete by it.
const filesUrl = generateUrl(
`/apps/openregister/api/objects/${REGISTER}/${SCHEMA}/${this.objectUuid}/files`,
)
await axios.delete(deleteUrl)
const listed = await axios.get(filesUrl)
const attachment = (listed?.data?.results || [])
.find((f) => f?.title === filename)

if (!attachment?.id) {
// The ref points at a file OR no longer holds. Nothing to
// detach — fall through and clear the ref so the record stops
// advertising an attachment that is not there.
// eslint-disable-next-line no-console
console.warn(`[openbuild] no OR attachment named ${filename}; clearing the ref only`)
} else {
const deleteUrl = generateUrl(
`/apps/openregister/api/objects/${REGISTER}/${SCHEMA}/${this.objectUuid}/files/${attachment.id}`,
)
await axios.delete(deleteUrl)
}

// 2. Clear the ref on the Application (partial merge, not replace).
const patchUrl = generateUrl(
Expand Down
12 changes: 12 additions & 0 deletions tests/e2e/iconUpload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ test.describe('Icon upload on the Application detail page (spec A task 7.5)', ()
await expect(inputs.nth(1)).toHaveAttribute('accept', '.svg')
})

// @e2e app-icon-management::non-svg-file-is-rejected-client-side
//
// The scenario is "the uploader displays an inline error message and does not
// submit the file to OR". Both halves are asserted below: the inline error by
// its literal text, and the negative half by recording every POST the page
// issues and requiring the list to be empty.
test('a non-SVG pick is rejected inline and never reaches the server', async ({ page, request }) => {
const { objectId } = await resolveApp(request)
await openIconsTab(page, objectId)
Expand Down Expand Up @@ -156,6 +162,12 @@ test.describe('Icon upload on the Application detail page (spec A task 7.5)', ()
expect(uploads, 'a rejected file must not be uploaded').toEqual([])
})

// @e2e app-icon-management::user-uploads-a-light-icon
//
// The scenario's three clauses map onto the assertions below: the file is
// POSTed to OR's attachment endpoint and `icon.ref` is patched (proven by
// reading the Application back independently rather than trusting optimistic
// UI state), and the light-background preview renders the SVG.
test('uploading an SVG persists it on the Application and shows it in the preview', async ({ page, request }) => {
// Two navigations plus an upload round-trip. The 30s project default is
// sized for single-navigation tests; every assertion below keeps its own
Expand Down
Loading
Loading