-
+
+
+
+
+
Drop files to upload
+
+ Release to add to the current directory
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Upload
@@ -27,7 +80,7 @@
@@ -41,7 +94,7 @@
t-ref="fileInput"
multiple="1"
accept="*"
- t-on-change="onChangeFileInput"
+ t-on-change="this.onChangeFileInput"
/>
diff --git a/dms/static/src/js/views/file_list_view.esm.js b/dms/static/src/js/views/file_list_view.esm.js
index e41dadff5..a14579374 100644
--- a/dms/static/src/js/views/file_list_view.esm.js
+++ b/dms/static/src/js/views/file_list_view.esm.js
@@ -17,7 +17,6 @@ import {registry} from "@web/core/registry";
patch(FileListRenderer.prototype, createFileDropZoneExtension());
patch(ListController.prototype, createFileUploadExtension());
-FileListRenderer.template = "dms.ListRenderer";
export const FileListView = {
...listView,
diff --git a/dms/static/src/scss/file_preview_pane.scss b/dms/static/src/scss/file_preview_pane.scss
new file mode 100644
index 000000000..7f46f21cc
--- /dev/null
+++ b/dms/static/src/scss/file_preview_pane.scss
@@ -0,0 +1,554 @@
+// Copyright 2026 ledoent — Don Kendall
+// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+// Side-pane preview layout for the dms file list + kanban views.
+//
+// The list/kanban view is wrapped in `.o_dms_list_split` / `.o_dms_kanban_split`.
+// When the preview pane is open, the wrapper becomes a horizontal flex
+// container with the list on the left and the preview pane (~40% of the
+// width) on the right. The pane animates in from the right; the selected
+// row gets a primary-color accent rail.
+//
+// View-level toggles (preview, density) live inside `.o_dms_view_toolbar`,
+// a normal in-flow strip rendered above the grid/list — not an overlay.
+// Earlier iterations stacked four absolute-positioned floaters in the
+// top-right corner, which sat on top of the list column headers and forced
+// a `right: calc(40% + 12px)` shift + density-hide hack when the pane
+// opened. In-flow placement removes the entire cascade.
+
+// ---- View toolbar: shared chrome for list + kanban ----
+//
+// One in-flow row above the renderer body. Kanban populates it with the
+// density btn-group on the left + the preview-pane pill on the right; the
+// `__spacer` between them carries `flex: 1` so the pill always anchors
+// right regardless of how many controls join on the left in future. List
+// view only has the pill, also right-anchored via the spacer.
+.o_dms_view_toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 12px;
+ flex: 0 0 auto;
+ border-bottom: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.06));
+ background-color: var(--bs-body-bg, #fff);
+
+ &__spacer {
+ flex: 1 1 auto;
+ }
+
+ &__toggle {
+ padding: 4px 10px;
+ font-size: 0.8rem;
+ font-weight: 500;
+ letter-spacing: 0.02em;
+ border-radius: 999px;
+ // Animate background/border/shadow/transform together so the hover
+ // state reads as a deliberate response, not just a 1px nudge.
+ transition:
+ background-color 160ms ease,
+ border-color 160ms ease,
+ color 160ms ease,
+ box-shadow 160ms ease,
+ transform 160ms ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ }
+ &:active {
+ transform: translateY(0);
+ }
+
+ &--off {
+ background-color: var(--bs-body-bg, #fff);
+ color: var(--bs-emphasis-color, #212529);
+ border: 1px solid var(--bs-border-color, rgba(0, 0, 0, 0.15));
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
+
+ // Hover on the "Preview pane" (off) state: deepen the tint
+ // toward the primary so the affordance pre-figures the on state.
+ &:hover {
+ background-color: rgba(113, 75, 103, 0.06);
+ border-color: rgba(113, 75, 103, 0.35);
+ color: var(--bs-primary, #714b67);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+ }
+ }
+
+ &--on {
+ background-color: var(--bs-primary, #714b67);
+ color: #fff;
+ border: 1px solid var(--bs-primary, #714b67);
+ box-shadow: 0 2px 6px rgba(113, 75, 103, 0.3);
+
+ // Hover on the "Hide preview" (on) state: darken via filter
+ // (works against any primary token value) + lift the shadow so
+ // the button feels tactile when about to dismiss the pane.
+ &:hover {
+ filter: brightness(0.92);
+ box-shadow: 0 3px 10px rgba(113, 75, 103, 0.45);
+ }
+ }
+ }
+}
+
+.o_dms_kanban_split {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ min-height: 0;
+ width: 100%;
+
+ &__main {
+ flex: 1 1 auto;
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ // The kanban renderer fills the space below the in-flow toolbar and
+ // scrolls within it. Core sizes the renderer at min-height:100%, which
+ // — added to the toolbar's height — overflows __main (overflow:hidden)
+ // by the toolbar height: the last row and the scroll viewport's bottom
+ // are clipped and the wheel appears dead. Force flex-fill + own scroll,
+ // mirroring the list split below.
+ > .o_renderer,
+ > .o_kanban_renderer {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+ }
+ }
+}
+
+// Responsive collapse: at narrow viewports (Bootstrap `md` and below — the
+// stock backend collapses the searchpanel below this breakpoint too), the
+// 40%/min-320px side-pane squeezes the grid to one cramped column AND
+// overlaps the in-flow toolbar. Hide the pane wholesale below `lg` and
+// suppress the toggle pill so it doesn't promise a feature the viewport
+// can't host. Re-appears at ≥992px, where there's room for both halves.
+@media (max-width: 991.98px) {
+ .o_dms_kanban_split,
+ .o_dms_list_split {
+ .o_dms_preview_pane {
+ display: none;
+ }
+ }
+ .o_dms_view_toolbar__toggle {
+ display: none;
+ }
+}
+
+.o_dms_list_split {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ min-height: 0;
+ // Fill the space beside the searchpanel and allow shrinking below the list
+ // table's intrinsic (min-content) width. Without min-width:0 the table's
+ // column widths keep the split wider than `.o_content`, overflowing it so
+ // the whole content area scrolls horizontally (the searchpanel scrolls off
+ // the left). The kanban split avoids this because its cards reflow to any
+ // width; the list table does not, so it must scroll inside its own column.
+ flex: 1 1 auto;
+ min-width: 0;
+
+ &__list {
+ flex: 1 1 auto;
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ transition: flex-basis 240ms ease-out;
+
+ // The list renderer body scrolls inside the column; the toolbar
+ // above stays pinned.
+ > .o_list_renderer,
+ > .o_renderer {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+ }
+ }
+}
+
+// ---- Selected-row accent: vertical primary rail + tint + subtle weight ----
+// All rows get a smooth tint transition so toggling selection feels intentional.
+.o_dms_list_split[data-preview-open="true"] .o_data_row > td {
+ transition: background-color 120ms ease-out;
+}
+
+.o_dms_preview_selected_row {
+ position: relative;
+
+ & > td:first-child::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ background-color: var(--bs-primary, #714b67);
+ box-shadow: 0 0 0 1px var(--bs-primary, #714b67);
+ }
+
+ & > td {
+ background-color: rgba(113, 75, 103, 0.08);
+ font-weight: 500;
+ }
+
+ // The first cell after the checkbox needs left padding to avoid the rail.
+ & > td:nth-child(2) {
+ padding-left: 12px;
+ }
+}
+
+// ---- Preview pane ----
+.o_dms_preview_pane {
+ flex: 0 0 40%;
+ max-width: 40%;
+ min-width: 320px;
+
+ // Empty state — collapse to a compact rail so the listing reclaims most
+ // of the screen until the user actually picks a file. The full pane
+ // returns the moment state.file is populated. Matches the macOS Finder
+ // / GNOME Files pattern: invisible until needed.
+ &--empty {
+ flex: 0 0 240px;
+ max-width: 240px;
+ min-width: 240px;
+ }
+ display: flex;
+ flex-direction: column;
+ border-left: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
+ background-color: var(--bs-body-bg, #fff);
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.04);
+ animation: dms-preview-slide-in 240ms cubic-bezier(0.2, 0.8, 0.2, 1);
+
+ &__header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 12px 16px;
+ border-bottom: 1px solid
+ var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
+ background-color: rgba(113, 75, 103, 0.02);
+ }
+
+ &__title {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ flex: 1 1 auto;
+ gap: 2px;
+ }
+
+ // Action toolbar — three primary actions grouped as a btn-group so the
+ // visual weight reads as a single control. The pane itself is 40% of
+ // viewport so even on a wide desktop the inner width is narrow; force
+ // icon-only buttons here (the per-button d-none d-md-inline rule uses
+ // VIEWPORT width, not container width — wrong axis for our case).
+ &__actions {
+ flex: 0 0 auto;
+
+ .btn {
+ padding: 0.25rem 0.55rem;
+ font-size: 0.78rem;
+ line-height: 1.2;
+
+ i.fa {
+ font-size: 0.85rem;
+ }
+
+ // Override the per-button d-md-inline label classes — labels
+ // never fit when the pane is open at any viewport size.
+ span {
+ display: none !important;
+ }
+ }
+ }
+
+ // Preview / Details tab strip — mirrors the Info & Tags pane of the
+ // first-party Documents app. Underline-style tabs keep the chrome light.
+ &__tabs {
+ display: flex;
+ gap: 4px;
+ padding: 6px 12px 0;
+ border-bottom: 1px solid
+ var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
+ background-color: rgba(113, 75, 103, 0.02);
+ flex: 0 0 auto;
+ }
+
+ &__tab {
+ appearance: none;
+ border: 0;
+ background: transparent;
+ padding: 6px 12px;
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: var(--bs-secondary-color, #6c757d);
+ border-bottom: 2px solid transparent;
+ margin-bottom: -1px;
+ cursor: pointer;
+ transition:
+ color 140ms ease,
+ border-color 140ms ease;
+
+ &:hover {
+ color: var(--bs-primary, #714b67);
+ }
+
+ &--active {
+ color: var(--bs-primary, #714b67);
+ border-bottom-color: var(--bs-primary, #714b67);
+ }
+ }
+
+ &__body {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+ padding: 16px;
+ }
+
+ &__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ gap: 16px;
+ color: var(--bs-secondary-color, #6c757d);
+ text-align: center;
+
+ // The mouse-pointer icon in the empty state — large + soft.
+ i.fa {
+ font-size: 2.5rem;
+ opacity: 0.35;
+ }
+
+ &--error {
+ color: var(--bs-danger, #dc3545);
+
+ i.fa {
+ opacity: 1;
+ }
+ }
+ }
+
+ // ---- Details tab: metadata definition-list + tag pills ----
+ &__details {
+ font-size: 0.85rem;
+ }
+
+ // Visual anchor atop Details: the file's icon/thumbnail (icon_url renders
+ // an image_128 thumb for images, a type icon otherwise) + name.
+ &__details_head {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding-bottom: 12px;
+ margin-bottom: 12px;
+ border-bottom: 1px solid
+ var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
+ }
+
+ &__details_icon {
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+ border-radius: 4px;
+ flex: 0 0 auto;
+ }
+
+ &__details_name {
+ font-weight: 600;
+ font-size: 0.95rem;
+ min-width: 0;
+ }
+
+ &__meta {
+ display: grid;
+ grid-template-columns: minmax(72px, auto) 1fr;
+ gap: 6px 12px;
+ margin: 0;
+
+ dt {
+ font-weight: 600;
+ color: var(--bs-secondary-color, #6c757d);
+ }
+
+ dd {
+ margin: 0;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--bs-emphasis-color, #212529);
+ }
+ }
+
+ &__tags {
+ margin-top: 16px;
+ }
+
+ &__tags_label {
+ display: block;
+ font-weight: 600;
+ font-size: 0.85rem;
+ color: var(--bs-secondary-color, #6c757d);
+ margin-bottom: 6px;
+ }
+
+ &__tag_pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+
+ &__tag {
+ padding: 2px 10px;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ background-color: rgba(113, 75, 103, 0.08);
+ color: var(--bs-primary, #714b67);
+ border: 1px solid rgba(113, 75, 103, 0.2);
+ }
+}
+
+@keyframes dms-preview-slide-in {
+ from {
+ transform: translateX(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+// ---- Per-handler styles ----
+.o_dms_preview__media {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+
+ img,
+ video {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ border-radius: 4px;
+ }
+
+ &--audio {
+ flex-direction: column;
+ gap: 12px;
+ padding: 24px;
+
+ audio {
+ width: 100%;
+ max-width: 480px;
+ }
+ }
+}
+
+.o_dms_preview__iframe {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ min-height: 480px;
+ border-radius: 4px;
+ // White surface for the PDF/Office preview loading paint. Chromium's
+ // built-in PDF viewer shows a dark grey backdrop before render, which
+ // photographs and screen-shares badly. Force a clean white start.
+ background: #fff;
+}
+
+// ---- Code editor (syntax-highlighted source/text preview) ----
+.o_dms_preview__code {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+
+ // The CodeEditor (ACE) needs a sized container to fill; let it take the
+ // pane body's height and scroll internally.
+ .o_code_editor,
+ .ace_editor {
+ flex: 1 1 auto;
+ width: 100%;
+ min-height: 320px;
+ border-radius: 4px;
+ }
+}
+
+// ---- Fallback card (office / download) ----
+.o_dms_preview__fallback {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ gap: 16px;
+ padding: 32px 24px;
+ height: 100%;
+
+ // The fallback "card" sits inside the pane body — give it a soft visual
+ // boundary so it reads as a distinct block, not raw empty space.
+ background-color: var(--bs-tertiary-bg, #f4f5f7);
+ border-radius: 6px;
+ border: 1px dashed var(--bs-border-color, rgba(0, 0, 0, 0.15));
+}
+
+.o_dms_preview__fallback_icon {
+ font-size: 3rem;
+ color: var(--bs-primary, #714b67);
+ opacity: 0.6;
+}
+
+.o_dms_preview__fallback_msg {
+ margin: 0;
+ color: var(--bs-emphasis-color, #212529);
+ max-width: 360px;
+ line-height: 1.4;
+
+ code {
+ background-color: var(--bs-body-bg, #fff);
+ padding: 1px 5px;
+ border-radius: 3px;
+ font-size: 0.85em;
+ color: var(--bs-primary, #714b67);
+ }
+}
+
+.o_dms_preview__fallback_actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.o_dms_preview__fallback_note {
+ margin: 0;
+ max-width: 360px;
+ line-height: 1.35;
+}
+
+// ---- List row interactions when preview is open ----
+.o_dms_list_split[data-preview-open="true"] {
+ // Communicate that row click = preview (not navigate).
+ .o_list_renderer .o_data_row:hover {
+ cursor: zoom-in;
+ }
+}