fix(web): [OCISDEV-1518] show correct message when Safe access is denied for lacking permission (#13111)
* fix(web): [OCISDEV-1518] show correct message when Safe access is denied for lacking permission
* chore: changelog
fix(web): [OCISDEV-1511] refresh files stuck in processing after upload or copy (#13134)
* fix(web): [OCISDEV-1511] refresh files stuck in processing after upload or copy
A postprocessing-finished event that arrives while a folder reload or a
fetch after copy is still in flight is dropped, because the resource is
not in the store yet. The response then adds the resource as processing
and nothing clears it until a page reload.
Remember finished items for a short time and refetch them when they show
up in the store as processing.
* chore: [OCISDEV-1511] add changelog
Remove the "Session expiring" extend/dismiss popup and its expiry
worker. Drop the frontend session_duration capability and config;
OCIS_MFA_SESSION_DURATION stays in use by the proxy.
`requireAcr()`, used by the admin settings route guards, redirected to the
IdP whenever the `acr` did not match, so a user without a second factor
looped forever.
It now uses the same protection as the vault: the step-up carries its
target as OIDC state, and if the IdP answers it without the required
`acr`, `requireAcr()` returns false. The admin settings guards then
redirect to the default view and an error message is shown.
Follow-up to https://github.com/owncloud/ocis/pull/12985, which fixed the same loop for the vault.
Selecting a language dropped focus to the page body and the save
confirmation went unannounced, because the select's own best-effort
refocus raced against the handler's translation reload and network
save. Restore focus explicitly once that work settles, before
announcing the save.
* fix(web): stop redirect loop when vault MFA step-up fails
Signed-off-by: Samuel Schumacher <samuel.schumacher@werkspiel.de>
Signed-off-by: Samuel Schumacher <schumacher@b1-systems.de>
* fix(web): leave vault with a full page load after failed MFA step-up
Returning to `/` in-app kept the vault clients and loaded vault spaces
with the non-MFA token, which ended on the RUNTIME-BOOTSTRAP-040 crash
page. Vault mode is fixed per page load, so reload outside the vault
instead and show the MFA error message after the reload. The spaces
load error handler no longer routes to the crash page while leaving.
Also addresses review feedback:
- use $pgettext with translator context for the error message
- test that an expired user with a fresh marker is still redirected
* fix(web): only treat a vault step-up as failed after the IdP answered it
The step-up marker relied on a 5 minute TTL, so a user who went to the
IdP, pressed back and opened the vault again got the error without ever
seeing the IdP. The step-up request now carries its target as OIDC
state; the sign-in callback picks it up and only the navigation right
after the callback counts as the answer. The sessionStorage marker and
the TTL are gone.
Also addresses review feedback:
- simplify the auth guard check to `redirect !== undefined`
- drop the storage key re-export, the spec imports it from the helper
Signed-off-by: Samuel Schumacher <schumacher@b1-systems.de>
---------
Signed-off-by: Samuel Schumacher <samuel.schumacher@werkspiel.de>
Signed-off-by: Samuel Schumacher <schumacher@b1-systems.de>
* fix(web): [OCISDEV-1478] move combobox role to the focusable input
The combobox role and its ARIA state lived on vue-select's non-focusable
wrapper div, so screen readers never announced the actual input as a
combobox and its accessible name fell back to a generic hardcoded label.
Moves role, aria-expanded, aria-controls, aria-haspopup and
aria-activedescendant to the focusable search input and gives it its own
label.
* fix(web): [OCISDEV-1478] fix CI, drop QuotaSelect's own search slot
It rendered a plain unlabeled input, bypassing OcSelect's accessible
one. That only worked by accident because vue-select's wrapper div
used to carry a fallback label, which the combobox fix removes.
fix(web): break web-pkg/design-system/web-test-helpers dependency cycle
web-pkg, design-system and web-test-helpers formed a cycle: the mount
helpers have to install the design system (its components are registered
globally) and web-pkg's mocked pinia stores, so web-test-helpers was
imported by both packages that imported it back.
Resolved by layering the helpers onto the packages that already need
them, instead of adding another one:
design-system/testing mount/shallowMount, prop types, design system +
gettext + router-link stub plugins
web-pkg/src/testing the above, extended with casl abilities, mocked
pinia stores and component mocks
web-test-helpers re-export of web-pkg/src/testing
design-system sits at the bottom of the workspace graph, so it cannot
depend on a package that installs it - hence the generic half lives
there. web-pkg composes those plugins rather than duplicating them, so
every helper is still defined exactly once, and no package depends on
one above it.
The public API of `@ownclouders/web-test-helpers` is unchanged, so
external extensions are unaffected.
Also:
- pin `typescript` via `pnpm.overrides` - without it, a second peer-
resolved typescript version forks `vue`, which forks `vue3-gettext`
into two module instances, breaking `inject(GetTextSymbol)` for any
component mounted through these helpers.
- keep `@ownclouders/design-system/testing` external in web-pkg's lib
build. rollup matches `external` strings exactly, so the subpath was
inlined, shipping a second copy of the design system and of
@vue/test-utils into the published bundle - components and wrappers
were then not identity-equal to the consumer's own, and gettext
injection broke.
- name the web-test-helpers umd output `.cjs`. The package is ESM, so the
`.js` file that publishConfig.exports pointed `require` at could not be
required.
Verified: `pnpm install --frozen-lockfile` clean, zero cyclic-workspace
warnings; `pnpm test:unit` 380 files / 3129 tests pass; `pnpm check:types`
0 errors; `pnpm lint`/`check:format` 0 errors; `pnpm build` succeeds;
published artifacts checked by mounting through the built `dist/` bundles
the way an external extension resolves them via publishConfig.exports.
Toast/status messages (e.g. clipboard copy, file restore) were often
dropped by screen readers because the live region was never cleared
between messages. Clear it before each message and refill after a
short delay so announcements survive focus changes. Toasts themselves
are no longer live regions, since a freshly inserted aria-live node
isn't reliably announced.
The notification bell's accessible name was a static "Notifications"
string regardless of the unread count, so screen reader users never
heard the count shown visually on the badge.
The accessible name (and tooltip) now include the exact unread count,
with correct singular/plural wording.
OCISDEV-1461
Arrow-key navigation in the notification bell's dropdown only started
working after an extra Tab press once it was open. Pressing an arrow
key too early fell through to the browser's default scroll action,
which looked like the dropdown had closed.
Added focus-on-open to the dropdown, matching other click-triggered
dropdowns in the codebase, so focus moves to the first item as soon
as it opens.
* fix: improve contrast on nav item and primary button hover states
Non-active sidebar navigation items and primary-variation buttons
(filled and outline appearances) rendered black text/icons on top of
a dark brown hover background in the vault theme, producing
insufficient contrast. Both now use the text-inverse color for their
hover state instead of the primary-contrast token, which resolves
correctly across all themes.
OCISDEV-1451
* docs: add changelog for nav/primary button hover contrast fix
* docs: link changelog entry to PR #12993
* feat(web-client): add native fetch http core
* test(web-test-helpers): replace axios mocks with fetch-shaped helpers
* refactor(web-pkg): build HttpClient on the fetch core
* refactor(web-pkg): unify ClientService interceptors on the fetch core
* refactor(web-pkg): drop axios types from useRequest
* fix(web): align error and header access with the fetch core
* refactor(web-client): move ocs onto the fetch core
* refactor(web-client): move webdav helpers onto the fetch core
* chore(web-client): regenerate graph client with typescript-fetch
* refactor(web-client): adapt graph wrappers to the fetch-based generated client
Regenerate the libre-graph client with the typescript-fetch template and
rewrite the eight facade factories against it:
- the generated APIs are classes (`new XxxApi(config)`), take a single
request-parameters object and resolve to the payload rather than an
envelope, so the wrappers no longer destructure `{ data }`
- every generated request is routed through `FetchClient` via
`Configuration.fetchApi`, keeping header injection, maintenance
detection and `HttpError`-on-non-2xx intact. Callers keep seeing
`HttpError` with `statusCode` and `data`, never `ResponseError`
- `toInitOverrides` merges our per-request headers into the generated
`RequestInit` instead of replacing them, which a plain object would do
because the runtime spreads `initOverrides` shallowly
- undeclared query parameters travel on a symbol-keyed channel, since the
runtime assembles the URL before it applies `initOverrides`
`--type-mappings=DateTime=string` keeps date-times as strings, matching
what the previous template emitted and what the app expects.
* refactor: use the generated property names for OData-annotated graph fields
The typescript-fetch template emits real JSON serializers, so annotated
fields like `@libre.graph.permissions.actions`, `@odata.id`, `@UI.Hidden`
and `@client.synchronize` are mapped to camelCase properties on the way in
and back to their wire names on the way out. The wire format is unchanged;
only the TypeScript-side names are. Reads left on the old names would have
silently resolved to `undefined`, and writes would have been dropped.
The OData `$filter` expression in FileSideBar keeps its annotated spelling,
being a query value rather than a property.
Response-only fields are now `readonly`, which the editable copies and test
fixtures built from them need to opt out of, hence `writeable()`.
* chore: drop the axios dependency
Nothing imports axios any more, so remove it from the eight manifests that
declared it and update the web-client README, whose usage examples still
instantiated an axios client.
Also applies prettier to the files touched over the course of the migration.
* fix(web-pkg): keep the axios-era surface on HttpClient
Dropping axios changed more than the transport. Four spellings that consumers
outside this repo were written against disappeared with it, and none of them had
to.
Response headers were a plain lowercased object under axios, so `headers['etag']`
was the only way to read one; a fetch `Response` exposes a `Headers`, where
`get('etag')` is. `httpHeaders` wraps the native object in a proxy that answers
both, case-insensitively, and enumerates the lowercased names — it stays a real
`Headers`, so neither spelling is a migration.
A rejected request carried its body on `error.response.data` and its status on
`error.response.status`. `HttpError` names them `data` and `statusCode`, which
this repo unifies on, but the response those shadow is reachable from outside it,
so `buildError` defines both on the response as well and keeps axios's
`Request failed with status code <n>` message. The body is read from a clone, so
callers still get an unread one.
The config `HttpClient` takes has always named the request body `data`, and web
extensions pass `data` when they issue raw WebDAV requests through
`httpAuthenticated`. Carrying the core's `body` up would have dropped those
bodies silently, so `RequestConfig` keeps `data` and `send` translates it. For
the same reason a default-responseType body that is not JSON falls back to the
raw text instead of throwing: a WebDAV multistatus answered without an explicit
`responseType: 'text'` used to arrive as a string, and a `SyntaxError` there
would turn a working request into a failing one.
`cancel()` is back, on an `AbortController` instead of a `CancelToken`. Combining
the client-wide signal with a per-request one needs a listener per request, so
`send` disposes of them once the request has settled rather than letting them
accumulate on a long-lived client.
`mockAxiosResolve` and `mockAxiosReject` stay exported from web-test-helpers as
deprecated aliases, so suites written against the axios era keep compiling.
* fix(web-client): keep read-only graph fields in request bodies
The typescript-fetch templates omit fields the spec marks `readOnly: true` from
the generated `*ToJSON` serializers. Every field of `Quota` is annotated that
way, so `updateDrive(id, { quota: { total: 500 } })` went out as
`{ quota: {} }` and quotas could no longer be changed. Seventeen other models
were affected the same way, among them `UserUpdate` and `ItemReference`.
The annotation is now stripped from the spec before generating, which is what
the axios-generated client effectively did. That also drops the `readonly`
modifiers, so the `writeable` escape hatch is no longer needed.
* fix(web-client): let graph errors keep their status and body
The generated runtime wraps whatever its `fetchApi` throws in a `FetchError`,
which buried the `HttpError` the core raises for a non-2xx response. Callers
that branch on the status saw neither `statusCode` nor `data`, so the banned
password hint on a public link never appeared and an aborted graph request
stopped looking like an abort.
Rethrowing from an `onError` middleware escapes the wrap for every graph
operation at once.
* fix(web-client): let fetch set the multipart boundary for FormData bodies
A caller-supplied `Content-Type: multipart/form-data` carries no boundary. Axios
replaced the header before sending, fetch passes it through, so the server could not
parse the body and the admin settings logo upload silently did nothing.
* fix(web): merge request headers case-insensitively, plus PR review fixes
Header names are case-insensitive, but the header layers were merged with an
object spread, where object keys are not. `Authorization` and `authorization`
both survived it, and a spec-compliant `Headers` applies a record init with
`append`, so the two were joined into `Bearer stale, Bearer fresh` instead of
the override replacing the token. The graph bridge lowercased every per-request
header name while the client-wide ones are canonically cased, which made the
collision the normal case rather than an edge one.
Merge each layer with `Headers.set()` instead, in the fetch client and in the
graph `initOverrides` bridge, which had its own copy of the same spread. The
new specs run under `@vitest-environment node`: happy-dom's `Headers` applies a
record init with `set` semantics and would hide the bug.
Also from the PR review:
- read an error body with the caller's `responseType` rather than cloning the
response, so a `blob` caller keeps getting a Blob on `error.data`
- treat any rejection on an aborted signal as an abort, since `abort(reason)`
rejects with the reason verbatim and only defaults to an `AbortError`
- share the maintenance-mode `onResponse` handler between the webdav client and
`ClientService`, with their pre-existing disagreement about clearing on an
unrelated error made explicit instead of silently unified
- pass the graph headers straight through as a `HeadersInit` and drop the
unused `httpClient` from the graph factory options
- collapse `combineSignals` onto `AbortSignal.any`
- let an abort through `getFileContents` unwrapped
- assert maintenance detection against the real `shouldResponseTriggerMaintenance`
instead of a mock, which had been hiding that a 500 is not a maintenance signal
* chore(web): drop leaked test title prefixes, document the maintenance-mode widening
* fix(web): keep graph fields the generated decoder used to lose
The typescript-fetch templates rebuild every response from the fields the
spec declares, which is what lets them rename the OData annotated fields.
Two consequences were unhandled.
A field the spec does not declare is dropped. oCIS returns `attributes` on
users, filled from OCIS_USER_SEARCH_DISPLAYED_ATTRIBUTES and displayed by the
sharing autocomplete, and the field is not part of the upstream spec. It is
now declared before generating, and the generation script fails if the spec
stops matching the line it patches, so the field cannot go missing silently.
A declared field the server omits is present holding `undefined`, so
`'accountEnabled' in user` no longer says whether the server sent it. Three
places read it that way: the login select in the user edit panel showed no
value at all, its watcher kept reporting unsaved changes, and sorting the user
list by login threw on `undefined.toString()`. The same went for
`'total' in quota`, which showed a personal quota of `?` for users without one.
Five review comments landed on #12892 after it merged.
**The tag overflow popover dismissed itself on click.** `mode="hover"` maps to
tippy's `mouseenter focus` trigger, and `hideOnClick` is hardcoded true in
`OcDrop`. tippy only skips its document-press hide when the trigger contains
`click`, so a click on the button fell through to `hide()` and set
`didHideDueToDocumentMouseDown`, which then blocked the focus trigger from
reopening it — the pointer had to leave and come back.
`mode="click"` fixes it without touching the design system: tippy toggles a
click-triggered instance, so the button opens and closes it, Enter/Space work
because a native button activation dispatches a click, and Escape still closes
via the `hideOnEsc` plugin with a click able to reopen it. Under `hover` that
last one was a dead end — Escape hid the popover and no new focus event could
follow. The trade is that hovering no longer reveals the tags; a disclosure
that answers to the keyboard is worth more than a hover shortcut.
That click also had to stop propagating: the button sits inside the `<tr>` that
`OcTable` highlights on click, so opening the popover selected the file as
well. tippy's listener is on the same element, so stopping propagation still
toggles the popover.
`is-nested` goes with it. It was set purely to suppress the `hideAll` that ran
when a pointer crossed the button, and pointer movement no longer opens
anything. Dropping it also un-silences `showDrop`/`hideDrop`, which `OcDrop`
only emits for drops that are not nested.
**The button claimed to be a popover trigger without saying so.** It now
carries `aria-haspopup` and an `aria-expanded` driven by those two events.
tippy manages `aria-expanded` itself only when the reference does not already
have one (`hasAriaExpanded` is read once at create time), and Vue renders the
attribute before `OcDrop` mounts, so there is one owner, not two.
**The accessible name dropped the visible text.** WCAG 2.5.3 Label in Name
wants the visible `+ 3` inside the name; `Show 3 more tags` left speech input
with nothing to match. The count now leads the string.
**The read-only tag lock was invisible to screen readers.** No
`accessible-label` means `OcIcon` renders it `aria-hidden`, so the only signal
that a tag cannot be removed was the absent deselect button.
**Themes inherited no tag palette.** `tagColorsList` was missing from
`defaults.designTokens`, so a deployment serving its own theme.json gets grey
chips. Seeding the defaults with the light palette turned out to be the wrong
place for it: every other value in that block is an empty-string placeholder,
because supplying colours is a theme's responsibility. The defaults stay
colourless. `useTagColor` already returns -1 without a list and `OcTag` then
renders the plain chip, which is what tags looked like before this feature.
* feat: [OCISDEV-115] colour tag chips by tag name
Tag chips were all the same grey, so a file's tags were hard to tell apart
at a glance and the same tag looked like nothing in particular across the app.
Fill each chip with a colour derived from the tag name: `hash(tag) % length`
picks a slot in the theme's new `designTokens.tagColorsList`, emitted as
`--oc-color-tag-N`. The colour is derived, never stored, so a tag looks the
same for every user on every device with no API, storage or migration change.
The label colour is computed per fill rather than pinned to a token: the
fills come from the theme, so one token cannot stay legible against all of
them, and `--oc-color-text-inverse` gets it backwards in the light theme. The
calculation lives in `useTagColor` rather than in `OcTag` because the theme
store is a reactive dependency and `getComputedStyle` is not — resolving it
in the component would leave the label stale after a theme switch. The fill
is read straight out of `tagColorsList` rather than from the emitted css var,
since `getHexFromCssVar` reports an unresolvable var as `#000000` and would
hand a pale fill an illegible white label.
A filled chip carries `oc-tag-filled`, which forces the colours of everything
nested inside it down as `inherit`/`currentColor`. Those nested elements
otherwise repaint themselves — a `router-link` takes the global anchor colour,
a raw `OcButton` its own swatch, `.oc-tag .oc-icon > svg` a muted fill —
which left navy labels and a washed-out close icon on the deep light-theme
fills, and labels that were too light on the pale dark-theme ones. Nothing
hardcodes a colour, so both themes are covered by construction.
`OcTag` keeps its default appearance: it is the generic badge (the app-store
"most recent" pill, instance badges, the disabled-space indicator, the `+N`
counter), so both `fillColor` and `labelColor` are opt-in.
`initializeThemes` now merges with replace semantics for arrays. deepmerge
concatenates by default, which would turn a `tagColorsList` present in both
`defaults` and a theme into 60 entries and shift every hue.
Also make the tag overflow indicator on the files list a real, keyboard
reachable button that shows the hidden tags in a popover instead of opening
the sidebar. That popover is sized to its tags: `OcDrop` is a fixed 300px
wide, which left it nearly empty when it held a single chip capped at 80px by
`.resource-table-tag`, with the right alignment the tags column passes down
pushing that chip to the far edge. 300px is kept only as a cap so a long tag
still truncates.
Both `theme.json` copies gain 30 placeholder fills per theme, per theme
rather than in `defaults`, so the feature can be exercised end to end while
design's real values are outstanding. They were generated by binary-searching
HSL lightness for a target relative luminance per hue, since equal-lightness
hues differ a lot in perceived brightness: deep fills with white labels in
the light themes, pale fills with black labels in the dark ones. Worst-case
label contrast is 6.31:1 (light) and 8.52:1 (dark), higher in both
high-contrast themes. Both copies are needed — web-runtime's file is what
`pnpm build` publishes to `web/dist/themes`, while an ocis binary serves the
go:embed-ed `services/web/assets/themes` copy instead
(services/web/pkg/server/http/server.go layers it over the embedded core
assets). Every theme in a file has the same list length, so a tag keeps its
colour slot across a theme switch.
* fix: apply renewed access token in embed mode with delegated authentication
The "owncloud-embed:update-token" message listener was registered unbound,
so it crashed on `this.configStore` before applying the renewed token. It
also passed the whole message payload to updateContext instead of the
access token string. The embedded instance therefore kept the initial
access token until it expired and the user got logged out.
* test: merge delegated token update tests into the existing describe block
Move the window.dispatchEvent based tests into the pre-existing
handleDelegatedTokenUpdate block and add coverage for update-token
messages arriving without an access token.
* test: assert renewed token payload and shorten changelog title
Assert the exact updateContext payload in the trusted-origin test so it
guards againsst the payload regression, and parameterize the message event
helper so the positive case can send a distinct renewed token. Shorten the
changelog title to fit the 80 character lint limit.
* fix(web): [OCISDEV-1128] app drawer leaks menu items to public link visitors
The top bar app drawer offered "Activities" and "Library" to anonymous
visitors following a public link. Both apps registered their app menu
item unconditionally at bootstrap, before any authentication context
exists, and the extension registry is shared between the authenticated
and the public link context.
Gate both registrations on a signed-in user, matching the idiom already
used by the files, text editor, app store, admin settings and ScienceMesh
apps. Add a cross-package spec pinning the invariant for all seven apps
that contribute a drawer entry.