diff --git a/css/apps/files.scss b/css/apps/files.scss index 631ef4b..8cecee0 100644 --- a/css/apps/files.scss +++ b/css/apps/files.scss @@ -421,8 +421,17 @@ table.files-filestable { .files-list__header { margin-block: 15px; margin-inline: 3rem 1.5rem; + display: flex; + align-items: center; + + // NC33 renders "New" as a direct header child; push it to the right. + > div:has(> .files-list__header-upload-button) { + order: 2; + margin-left: auto; + } .files-list__header-grid-button { + order: 3; border: 1px solid var(--color-main-text); border-radius: 50%; width: 40px !important; @@ -576,9 +585,107 @@ table.files-filestable { padding-inline-end: 1rem; } + // Chip styling for the NC33 filters; nmcfiles.ts relocates them here. + [data-test-id="files-list-filters"] { + display: flex; + align-items: center; + gap: 0.5rem; + + // Hide the People filter (absent in the legacy layout). + > div:nth-child(3) { + display: none; + } + + button.button-vue { + background-color: var(--nmc-ods-blue-primary); + color: var(--nmc-color-text-and-icon-black); + border: none; + border-radius: var(--border-radius-small); + height: 28px; + min-height: 28px; + min-width: unset; + padding: 0 10px; + font-weight: normal; + + &:hover { + background-color: var(--nmc-ods-blue-hover); + } + + &:active, + &[aria-expanded="true"] { + background-color: var(--nmc-ods-blue-active); + } + + // Hide the leading icon; replace with a trailing caret like the chips. + .button-vue__icon { + display: none; + } + + .button-vue__text { + color: var(--nmc-color-text-and-icon-black); + } + + .button-vue__wrapper::after { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + border-left: 1.5px solid var(--nmc-color-text-and-icon-black); + border-bottom: 1.5px solid var(--nmc-color-text-and-icon-black); + transform: rotate(-45deg); + margin-left: 8px; + margin-bottom: 2px; + transition: transform 0.2s ease; + } + + &[aria-expanded="true"] .button-vue__wrapper::after { + transform: rotate(135deg); + margin-bottom: -2px; + } + } + } + .files-list__filters { padding-block: 1rem; padding-inline: 4rem 1rem; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.5rem; + + // NC33 renders the toggles after the chips; show them first (left) and + // cap their container so it can't stretch and push the chips off-screen. + > [data-test-id="files-list-filters"] { + order: -1; + flex: 0 0 auto; + width: max-content; + max-width: max-content; + } + + > ul[aria-label="Active filters"] { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 0.5rem; + list-style: none; + margin: 0; + padding: 0; + } + + // Divider anchored as the toggles' last flex item so it sits a fixed gap + // after "Modified"; only shown when filters are active. + &:has(> ul[aria-label="Active filters"] li) { + gap: 0.25rem; + + > [data-test-id="files-list-filters"]::after { + content: ""; + flex: 0 0 auto; + width: 1px; + height: 1.5rem; + margin-inline-start: -0.25rem; + background-color: var(--color-border); + } + } .file-list-filters { display: flex; diff --git a/css/components/ncactions.scss b/css/components/ncactions.scss index f7bc1eb..4ca7ae0 100644 --- a/css/components/ncactions.scss +++ b/css/components/ncactions.scss @@ -59,6 +59,81 @@ padding: 1rem; } + // NC33 file-type filter uses NcButton items (no ul/li); reshape the + // popover to match the legacy action menu below. + files-file-list-filter-type .icon-vue svg path { + fill: var(--color-main-text); + } + + &:has(files-file-list-filter-type) { + width: fit-content; + min-width: 213px; + max-height: min(312px, 60vh); + overflow-y: auto; + + > div, + > div > div, + files-file-list-filter-type, + files-file-list-filter-type > div, + .button-vue { + min-width: 0; + } + + > div, + > div > div { + padding: 0; + } + + files-file-list-filter-type > div { + display: flex; + flex-direction: column; + gap: 0; + padding: 0; + } + + .button-vue { + margin: 0; + border-radius: var(--border-radius); + padding-inline: 4px 16px; + + // Grey on hover only, not the first item's focus-on-open. + &:hover, + &:focus-visible { + background-color: var(--color-background-hover); + } + + .button-vue__text { + font-weight: normal; + } + + // Selected: strip NcButton's magenta pressed fill, keep a dark + // label and add a checkmark like the legacy menu. + &[aria-pressed="true"] { + color: var(--color-main-text); + + &:not(:hover):not(:focus-visible) { + background-color: transparent; + } + + .button-vue__text { + color: var(--color-main-text); + } + + .button-vue__wrapper::after { + content: ""; + display: inline-block; + flex: 0 0 auto; + width: 16px; + height: 16px; + margin-inline-start: auto; + background-color: currentColor; + -webkit-mask: var(--icon-check-dark) no-repeat center / contain; + mask: var(--icon-check-dark) no-repeat center / contain; + } + } + } + } + ul { display: flex; flex-direction: column; diff --git a/css/components/ncappnavigation.scss b/css/components/ncappnavigation.scss index 54400a7..0ede619 100644 --- a/css/components/ncappnavigation.scss +++ b/css/components/ncappnavigation.scss @@ -115,6 +115,11 @@ // default navbar link styling #app-navigation-vue .app-navigation-entry-wrapper { + // Cancel the framework's margin-top:auto so "Deleted files" isn't pinned to the bottom. + &.app-navigation-entry--pinned { + margin-top: 0; + } + .app-navigation-entry { position: relative; border-radius: var(--border-radius-large); diff --git a/css/components/ncusermenu.scss b/css/components/ncusermenu.scss index 86ff68f..376c697 100644 --- a/css/components/ncusermenu.scss +++ b/css/components/ncusermenu.scss @@ -24,7 +24,11 @@ order: 1; } - li#profile { + li:has(a#logout) { + order: 3; + } + + li:has(a#profile) { display: none; } @@ -150,6 +154,18 @@ background-image: var(--icon-logout-dark); } + > a#admin_settings::after { + background-image: var(--icon-admin-dark); + } + + > a#core_apps::after { + background-image: var(--icon-apps-dark); + } + + > a#core_users::after { + background-image: var(--icon-users-dark); + } + > a#nmc_welcome_popup-about { .loading-icon { diff --git a/lib/JSResourceLocatorExtension.php b/lib/JSResourceLocatorExtension.php index 70ec292..74d9133 100644 --- a/lib/JSResourceLocatorExtension.php +++ b/lib/JSResourceLocatorExtension.php @@ -17,6 +17,7 @@ use OC\Template\JSResourceLocator; use OCP\IConfig; use OCP\App\IAppManager; +use OCP\IConfig; use Psr\Log\LoggerInterface; class JSResourceLocatorExtension extends JSResourceLocator { @@ -29,14 +30,17 @@ class JSResourceLocatorExtension extends JSResourceLocator { * so we try to handle different backports in this constructor. * */ - public function __construct( - LoggerInterface $logger, - IConfig $config, - protected JSCombiner $jsCombiner, - protected IAppManager $appManager, - ) { + public function __construct(LoggerInterface $logger, IConfig $config, JSCombiner $jsCombiner, IAppManager $appManager) { $this->ownAppManager = $appManager; + // V33 + try { + parent::__construct($logger, $config, $jsCombiner, $appManager); + return; + } catch (\Throwable $eWrongConstructNC33) { + // ignore the exception, try another constructor + } + // later try { parent::__construct($logger, $config, $jsCombiner, $appManager); diff --git a/src/js/filessettings.js b/src/js/filessettings.js index fcc5d38..85395b9 100644 --- a/src/js/filessettings.js +++ b/src/js/filessettings.js @@ -11,30 +11,51 @@ const View = new StorageQuotaView({ pinia, }) -window.addEventListener('DOMContentLoaded', () => { - // Select the
  • that should be moved - const settingsButton = document.querySelector( - 'li[data-cy-files-navigation-settings-button]' - ) +let isMounted = false - // Target Files navigation list - const filesNavList = document.querySelector('.files-navigation__list') - - if (!settingsButton || !filesNavList) { - return +const setupStorageQuota = () => { + if (isMounted) { + return true } - // Move the settings button into the Files navigation list - filesNavList.appendChild(settingsButton) - - // Create the mount anchor + // Rendered by Vue after DOMContentLoaded; retry via the observer until present. const entrySettings = document.querySelector('.app-navigation-entry__settings') - if (!entrySettings) return + if (!entrySettings) { + return false + } + + // Best-effort: lift the "Files settings" button into the main nav list. + const settingsButton = document.querySelector('li[data-cy-files-navigation-settings-button]') + const filesNavList = document.querySelector('.files-navigation__list, .app-navigation__list') + if (settingsButton && filesNavList) { + filesNavList.appendChild(settingsButton) + } const anchor = document.createElement('div') anchor.id = 'storage-quota-app' entrySettings.appendChild(anchor) - - // Mount the vue component View.$mount('#storage-quota-app') -}) + + isMounted = true + return true +} + +const waitForNavigation = () => { + if (setupStorageQuota()) { + return + } + + const observer = new MutationObserver(() => { + if (setupStorageQuota()) { + observer.disconnect() + } + }) + observer.observe(document.body, { childList: true, subtree: true }) +} + +// Handle the bundle loading after DOMContentLoaded has already fired. +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', waitForNavigation) +} else { + waitForNavigation() +} diff --git a/src/nmcfiles.ts b/src/nmcfiles.ts index 04a9823..b561abd 100644 --- a/src/nmcfiles.ts +++ b/src/nmcfiles.ts @@ -294,6 +294,37 @@ if (document.readyState === 'loading') { setupGridViewScrollFix() } +/** + * NC33 renders the Type/Modified filters inline in the header; move them into the + * .files-list__filters row below. Re-runs on Vue re-renders that re-insert them. + */ +function setupFilterRelocation(): void { + const relocate = (): void => { + const target = document.querySelector('.files-list__filters') + if (!target) return + + const headerFilters = document.querySelector( + '.files-list__header [data-test-id="files-list-filters"]', + ) + if (!headerFilters) return + + // Drop any stale copy before moving, in case Vue recreated the element. + target.querySelectorAll('[data-test-id="files-list-filters"]').forEach(el => el.remove()) + target.appendChild(headerFilters) + } + + relocate() + + const observer = new MutationObserver(() => relocate()) + observer.observe(document.body, { childList: true, subtree: true }) +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupFilterRelocation) +} else { + setupFilterRelocation() +} + window.addEventListener('DOMContentLoaded', function() { const breadcrumb = document.querySelector('.breadcrumb') const empty = document.querySelector('.files-list__empty')