Added
- ✨(backend) make invitation validity duration configurable via env var
- ✨(frontend) enhance upload toast with progress, errors and cancel support
- ✨(frontend) add ErrorIcon component and support numeric icon sizes
- ✨(frontend) make file upload abortable in driver layer
- ✨(frontend) files preview v2
- 🔧(project) add DJANGO_EMAIL_URL_APP environment variable
Fixed
- 🐛(frontend) add actions menu on mobile My Files page
- 🐛(frontend) show actual selection count in hard delete modal
- 🐛(frontend) Responsive broken with long filters in search #659
- 🐛(front) set size and variant on trash navigate modal #666
- 🐛(frontend) fix uploads continuing after parent folder deletion
- 🐛(frontend) fix SDK picker link reach promotion
- 🐛(backend) route share invitation link to file view for files
- 🐛(frontend) fix "+ New" menu in read-only folders and virtual tabs
- 🐛(frontend) range selection freezes when there are many items in the list
- 🐛(backend) fix openapi schema for item access endpoints
- 🐛(backend) load jwks url when OIDC_RS_PRIVATE_KEY_STR is set
Share invitation emails embedded `Site.domain` directly, producing
schemeless links that mail clients failed to resolve. Introduce a
`DJANGO_EMAIL_URL_APP` environment variable holding the app's
absolute URL used when building invitation links, with a fallback
on the current Site domain when unset. Wire it in dev env, helm
dev values, the helm example and the env reference.
Clicking the invitation link for a shared file opened the folder
explorer page with an empty children listing, because
`Item.send_email` hardcoded `/explorer/items/<uuid>/` regardless of
the item type.
Branch on `item.type` to use the dedicated file route
`/explorer/items/files/<uuid>/` already served by nginx and used by
the in-app share-link copy button.
Cover the two main picker outcomes: cancelling leaves the item
private, and confirming exposes a publicly reachable URL. The
reach check opens the returned URL in a fresh anonymous context
and waits for the download event, since the backend serves
attachments with Content-Disposition and navigation aborts.
To run the test in CI, build the drive SDK package and start the
sdk-consumer dev server before the playwright job.
Switch to useMutationUpdateLinkConfiguration and preserve the
item's existing link_role. Also add the missing sdk.explorer.cancel
translations for en and nl so the Cancel button label renders
correctly. This should have been done when migration to this new
API route. The existing update did not have any effect.
When the resource server is enabled and the backend used is
JWTResourceServerBackend, then the API should expose a JWKS endpoint
to share the RSA public key to the OIDC provider. Everything is made
in the Django LaSuite library, but the URL is not included in the
Drive URLs. This commit adds it when the setting OIDC_RS_PRIVATE_KEY_STR
is set.
The invitation validity duration was hardcoded to 7 days. Expose it as
an INVITATION_VALIDITY_DURATION env var so operators can tune it per
deployment without patching settings. Default remains 7 days.
`ItemAccessViewSet.get_serializer_class` depends on `self.item`, which
reads `self.kwargs["resource_id"]`. This is unavailable during
drf-spectacular introspection, so spectacular fails to resolve the
serializer and drops the requestBody for POST/PUT/PATCH operations.
Declare the schema statically via `@extend_schema` and
`@extend_schema_view` so the generated OpenAPI exposes the proper
request and response payloads for item access endpoints.
Drop selectedItems / selectedItemsMap from GlobalExplorerContext and
move every call-site to the selection store introduced in the previous
commit. Rows of the embedded explorer grid are extracted into a memoed
EmbeddedExplorerGridRow that subscribes to its own id via
useIsItemSelected, and the name/actions/mobile cells are memoed and
read their selection status the same way.
High-level consumers that only need a boolean or a count use
useHasSelection / useSelectionCount so they no longer re-render on
every marquee tick:
- AppExplorerInner uses useHasSelection to toggle the selection bar
- ExplorerDndProvider splits the count consumers into dedicated child
components (drag overlay and move confirmation modal)
- useTableKeyboardNavigation subscribes imperatively through
selectionStore.subscribe so the focus effect runs without
invalidating the host component
EmbeddedExplorer creates its own local store so the move modal and the
SDK picker keep their selection scoped and do not leak into the main
explorer selection.
Fixes#124
Introduce a lightweight external store compatible with
useSyncExternalStore. It exposes:
- a global subscription for consumers that need the full list or count
- per-id subscriptions so a row can bail out automatically when its own
isSelected boolean has not changed
- setSelectedItems that diffs the previous and next id maps and
notifies only the listeners whose status actually flipped
No React state, no context fan-out: a marquee tick that flips N rows
notifies exactly N listeners instead of invalidating the whole tree.
Add an architecture doc covering the rationale behind the selection
store refactor: why React Context did not scale to 200+ rows during a
marquee drag, why useSyncExternalStore with per-id listeners enables
automatic bailout, and how global vs embedded scopes are wired.
Lives under docs/architecture/ for long-term reference.
Cover the four scenarios of the new "+ New" behavior:
- folder creation from a read-only shared folder redirects into the
newly created folder under My Files
- document creation from a read-only shared folder redirects to the
My Files view with the new file listed
- folder creation from the Recent / Shared with me / Starred virtual
tabs creates in My Files
- folder creation in a writable folder still happens in place, with
no fallback redirect
The "+ New" dropdown used to hide all its entries when the user lacked
children_create on the current folder, leaving an empty popover. On
virtual tabs (Recent, My Files, Shared with me, Starred) there is no
real current folder either, so the same empty state could be reached.
Always populate the menu and, when the user cannot create in place,
fall back to creating the item in "My Files" (no parent). After a
fallback creation we redirect:
- into the new folder for folder creation
- to the My Files view for file creation, since a file is not a
navigable route
The new item is pre-selected in both cases.
The WOPI page is a dynamic route keyed by item UUID, so nginx needs to
fall back to the [id].html shell when the URL is hit directly (reload,
external link). Mirrors the existing /explorer/items/[id] rule.
The search e2e fixture items lacked a filename, which made the seeded
file items diverge from real uploads where the filename is present.
We need the filename to be predictive so we can make assertion in the
e2e tests about the viewer that should open or not.
Drop a redundant clearTimeout guard, reformat the WopiEditorFrame
props, assert launch_url is defined at render time, and add a comment
clarifying the WOPI page spinner branch.
The error, not-supported, suspicious and WOPI placeholder viewers all
rendered the same "icon + title + description + action" layout with
near-duplicate scss. Extract a PreviewMessage component so styling and
backdrop-close behaviour live in one place, and update the e2e
selectors to match the new markup.
Cover the backdrop-click-to-close behavior for both image
and PDF viewers: clicking the backdrop closes the preview,
clicking the content does not, and dragging across the image
viewer does not trigger a close.
Users can now dismiss the preview by clicking the blurry area
around the content. Each viewer marks its backdrop zones with
a data attribute so the handler in FilesPreview can distinguish
backdrop from content clicks. The image viewer also guards
against accidental close during pan-drag gestures.
Ensures Print on an image creates a hidden iframe that loads the
same preview URL, waits for the image to decode, and calls
window.print() — all without opening a new tab.
Previously clicking Print on an image opened it in a new tab,
leaving the user to trigger the print dialog themselves. Now a
hidden iframe loads the image and calls window.print() directly.
Switch the non-printable file test from DOCX to an audio
file so it no longer relies on WOPI (which opens in a new
tab). Wait for the PDF page to render before interacting
with the page input to avoid flaky failures. Remove an
unused variable.
Remove the unused isSidebarOpen prop from PdfControls and
strip unnecessary position/min-height rules from the video
player that were left over from earlier layout iterations.
Add data to the useEffect dependency array so the current
file index is recalculated when the file list changes,
e.g. after a refetch or when items are added/removed.
Add a key prop to force React to remount the img element
when switching files. Without this, transitioning between
images of very different sizes causes a visual splash.
Extract a wrapper div for flex layout and overflow handling,
keeping the title element focused on typography. Replace the
hardcoded color with a design system variable for consistency.
Follow the WOPI-in-new-tab change: the double-click scenario now
asserts that a new page opens with the office iframe and that the
preview modal stays closed, a new test covers the "Open in editor"
placeholder reached by keyboard navigating onto a WOPI file, and the
copy/paste scenario drives the detached wopiPage. Refresh canvas
snapshots accordingly and drop the obsolete per-spec snapshot copies.
WOPI editors (OnlyOffice / Collabora) work better as a full-page
experience than embedded inside the preview modal. Double-clicking a
WOPI file in the grid or activating one from the search modal now
opens /wopi/:id in a new tab. The preview modal still shows an
"Open in editor" placeholder when a user navigates onto a WOPI file
via keyboard, so the preview flow stays consistent.
The former WopiEditor component is renamed WopiEditorFrame and
hosted on the new page. The inline rename-sync path is removed
because the editor no longer lives inside the items list.
The effect that toggles the pdf-sidebar class scheduled a setTimeout
without returning a cleanup, so the timer could fire after the file
preview unmounted. Capture the timeout id and clear it on cleanup.
The component lives in DurationBar.tsx but was exported as ProgressBar,
which was confusing. Rename the export and update the two call sites
in AudioPlayer and VideoPlayer.
Cover the fallback viewer that kicks in for file types the app
cannot render inline. A minimal .bin fixture exercises the redesign
so the filename, the "Unsupported format." disclaimer and the
secondary download button are all checked end to end.
Cover the video viewer with a dedicated spec that uploads a small
mp4 fixture, opens the preview and exercises the player controls.
Keeps the new viewer types consistently covered alongside the audio
and image specs.
Cover the image viewer with a dedicated spec that uploads a small
png fixture, opens the preview and asserts the image renders and
reacts to the viewer controls. Mirrors the audio/video specs so
each viewer has its own file.
Cover the audio viewer with a dedicated spec that uploads a small
mp3 fixture, opens the preview and checks the playback controls.
Ensures the audio branch of the preview rewrite stays covered as we
iterate on the other viewers.
The preview-related specs are now numerous enough to warrant their
own subfolder. Move pdf, heic and preview-actions specs into
__tests__/app-drive/file-preview/ so future preview tests land next
to them rather than at the drive-level root. Import and asset paths
are adjusted to the new depth, the heic row lookup is simplified to
getByRole("cell", ...), and the preview-actions spec gains a small
File Preview Header describe block to exercise the header controls.
The predicate in the preview close branch was (className) =>
className !== className, which compared the lambda parameter to
itself and therefore never matched, leaving stale class names on the
preview when the PDF thumbnail sidebar was closed. Rename the inner
parameter so the filter compares against the outer className as
intended.
The preview for files that cannot be rendered inline now shows the
filename as the title and folds the "unsupported format" notice into
the description paragraph as a bold disclaimer, so the panel reads as
"<filename> — Unsupported format. Download it to view." The download
CTA is promoted to a secondary neutral button to stand out on the
empty-state panel.
The i18n key file_preview.unsupported.title is renamed to disclaimer
in en/fr/nl and the copy shortened accordingly. search.spec.ts
assertions are updated to match the filename via getByRole("heading")
now that the preview wraps the title in a heading element.
The preview refactor renamed CSS classes from
pdf-preview__controls / pdf-preview__zoom-controls
to file-preview__controls / file-preview__controls__zoom.
Update the e2e tests to match.
Reorganize all viewer components under a viewers/ directory
and rewrite FilesPreview as the single orchestrator at the
preview root. Add ZoomControls shared component for image
and PDF viewers. The new layout centralizes the preview
chrome (header, controls bar, navigation) in FilesPreview
while each viewer only handles its own rendering.
Rename PreviewControls to PlayerPreviewControls and swap
material icons for ui-kit SVG icons. Move arrow-key seeking
from global keydown to the DurationBar input so it only
activates when the slider is focused, and add a focus-visible
outline for accessibility. Simplify VolumeBar styling by
removing unused btn-mute overrides.