From 1289b217d04f7213046225d58cacf7960fdb99f2 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 29 Apr 2026 15:15:29 +0200 Subject: [PATCH] fix(app): resolve Bucket E.2-E.5 lint findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WS-3 session 1b-ii Task 5b+c (audit Bucket E.2-E.5 — 6 items resolved, 2 promise/no-promise-in-callback warnings remain on dynamic-import sites — see deviations). This commit is split out from the originally-planned grouped Task 5 because the API stream timed out mid-session. E.1 (isAxiosError) is in the preceding commit 0f155d9. E.2 — vitest spec to Composition API (1× vue/component-api-style): - useFormFailures.spec.ts: rewrote the test wrapper from \`{ setup() { return { result } }, render: () => h('div') }\` to \`setup(_, { expose }) { expose({ result }); return () => h('div') }\`. Pure Composition API: setup returns the render function; expose() declares the instance-visible \`result\` that the 7 \`vm.result.*\` assertions consume. Tests still pass green (49 tests). E.3 — REAL BUG: missing return in computed (1× vue/return-in-computed-property): - useTimeSlotDropdown.ts:80: the \`fetchParams\` computed had a switch over the \`DropdownScenario\` type (4 string-literal cases) without a \`default\` branch. If \`scenario.value\` ever returned a value outside the four narrowed cases (e.g. via a future type-assertion drift), the computed silently returned \`undefined\`, and the consumer code (\`fetchParams.value.includeParent\`) would throw \`Cannot read property 'includeParent' of undefined\`. Added a \`default\` branch returning \`{ includeParent: false, includeChildren: false }\` — same as the 'flat' case (the safest baseline: include only own slots, no hierarchy). E.4 — SECURITY (1× vue/no-template-target-blank): - pages/organisation/index.vue:343: the external website anchor had \`target='_blank'\` with \`rel='noopener'\` (only one). The rule requires the full \`rel='noopener noreferrer'\` pair. Updated. Mitigates reverse-tabnabbing (window.opener) AND referrer-leakage to the linked third-party site. E.5 — axios fire-and-forget (3× promise/no-promise-in-callback, 1 fully resolved + 2 warnings remain): - lib/axios.ts:42: changed \`error => Promise.reject(error)\` to \`async error => { throw error }\`. Semantically identical (axios interceptor onRejected returns a rejected promise either way) and satisfies the lint rule. - lib/axios.ts:61, 73: prefixed the dynamic-import chains with \`void\` per Q4's option-a decision (\`void import('@/stores/...').then(...)\`). This makes the discard intent explicit, but empirically does NOT satisfy promise/no-promise-in-callback — the rule fires on any promise creation inside a callback, regardless of the discard pattern. The 2 warnings remain in the post-Task-5 baseline. Resolution path is Bert's call: either keep \`void\` and accept the warnings as documentation, or rewrite to \`async error => { const { useStore } = await import(...); ... }\` which sequentializes the dynamic-import resolution with the rejection. Out of scope for this session per the literal Q4 recipe. Tests + typecheck verified green. Lint baseline: 34 → 32. Co-Authored-By: Claude Opus 4.7 --- .../src/composables/api/__tests__/useFormFailures.spec.ts | 7 ++++--- apps/app/src/composables/useTimeSlotDropdown.ts | 3 +++ apps/app/src/lib/axios.ts | 6 +++--- apps/app/src/pages/organisation/index.vue | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/app/src/composables/api/__tests__/useFormFailures.spec.ts b/apps/app/src/composables/api/__tests__/useFormFailures.spec.ts index d9831f0c..6434dca6 100644 --- a/apps/app/src/composables/api/__tests__/useFormFailures.spec.ts +++ b/apps/app/src/composables/api/__tests__/useFormFailures.spec.ts @@ -30,12 +30,13 @@ function mountWithQuery(setup: () => T): { vm: { result: T }; client: QueryCl const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) const Component = defineComponent({ - setup() { + setup(_props, { expose }) { const result = setup() - return { result } + expose({ result }) + + return () => h('div') }, - render: () => h('div'), }) const wrapper = mount(Component, { diff --git a/apps/app/src/composables/useTimeSlotDropdown.ts b/apps/app/src/composables/useTimeSlotDropdown.ts index 4cdf92ad..a2373bf0 100644 --- a/apps/app/src/composables/useTimeSlotDropdown.ts +++ b/apps/app/src/composables/useTimeSlotDropdown.ts @@ -88,6 +88,9 @@ export function useTimeSlotDropdown( case 'cross_event': return { includeParent: false, includeChildren: true } + + default: + return { includeParent: false, includeChildren: false } } }) diff --git a/apps/app/src/lib/axios.ts b/apps/app/src/lib/axios.ts index dc1c2097..c8660f8f 100644 --- a/apps/app/src/lib/axios.ts +++ b/apps/app/src/lib/axios.ts @@ -39,7 +39,7 @@ apiClient.interceptors.request.use( return config }, - error => Promise.reject(error), + async error => { throw error }, ) apiClient.interceptors.response.use( @@ -58,7 +58,7 @@ apiClient.interceptors.response.use( // Handle impersonation session expiry if (status === 403 && error.response?.data?.impersonation_ended) { - import('@/stores/useImpersonationStore').then(({ useImpersonationStore }) => { + void import('@/stores/useImpersonationStore').then(({ useImpersonationStore }) => { const impersonationStore = useImpersonationStore() impersonationStore.clearState() @@ -70,7 +70,7 @@ apiClient.interceptors.response.use( if (status === 401) { // Lazy import to avoid circular dependency - import('@/stores/useAuthStore').then(({ useAuthStore }) => { + void import('@/stores/useAuthStore').then(({ useAuthStore }) => { const authStore = useAuthStore() if (authStore.isInitialized) authStore.handleUnauthorized() diff --git a/apps/app/src/pages/organisation/index.vue b/apps/app/src/pages/organisation/index.vue index 0ac93292..9cd7c7ab 100644 --- a/apps/app/src/pages/organisation/index.vue +++ b/apps/app/src/pages/organisation/index.vue @@ -340,7 +340,7 @@ function describeActivity(entry: ActivityLogEntry): string { v-if="organisation.website" :href="organisation.website" target="_blank" - rel="noopener" + rel="noopener noreferrer" class="text-body-1 text-primary" >{{ organisation.website }}