diff --git a/changelog/unreleased/enhancement-enable-user-preferences-in-public-links b/changelog/unreleased/enhancement-enable-user-preferences-in-public-links new file mode 100644 index 00000000000..5aafd1872c0 --- /dev/null +++ b/changelog/unreleased/enhancement-enable-user-preferences-in-public-links @@ -0,0 +1,6 @@ +Enhancement: Enable user preferences in public links + +We've enabled user preferences in public links, so any user even without an account can open +preferences in a public link context and for example change the current language. + +https://github.com/owncloud/web/pull/10207 diff --git a/packages/web-app-admin-settings/src/index.ts b/packages/web-app-admin-settings/src/index.ts index a5e8e7fe625..c3dae358125 100644 --- a/packages/web-app-admin-settings/src/index.ts +++ b/packages/web-app-admin-settings/src/index.ts @@ -161,7 +161,13 @@ export default defineWebApplication({ isFileEditor: false, applicationMenu: { enabled: () => { - return userStore.user && can('read-all', 'Setting') + return ( + userStore.user && + (can('read-all', 'Setting') || + can('read-all', 'Account') || + can('read-all', 'Group') || + can('read-all', 'Drive')) + ) }, priority: 40 } diff --git a/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue b/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue index 379c1ff4ffd..d7209b4bebe 100644 --- a/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue +++ b/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue @@ -246,7 +246,11 @@ export default defineComponent({ }) }) const showWebDavDetails = computed(() => { - return store.getters['Files/areWebDavDetailsShown'] + /** + * webDavPath might not be set when user is navigating on public link, + * even if the user is authenticated and the file owner. + */ + return store.getters['Files/areWebDavDetailsShown'] && unref(resource).webDavPath }) const formatDateRelative = (date) => { return formatRelativeDateFromJSDate(new Date(date), language.current) diff --git a/packages/web-app-files/src/components/SideBar/Details/TagsSelect.vue b/packages/web-app-files/src/components/SideBar/Details/TagsSelect.vue index 1b2a300fe39..035b44a74ad 100644 --- a/packages/web-app-files/src/components/SideBar/Details/TagsSelect.vue +++ b/packages/web-app-files/src/components/SideBar/Details/TagsSelect.vue @@ -236,7 +236,13 @@ export default defineComponent({ if (unref(resource)?.tags) { selectedTags.value = unref(currentTags) } - loadAvailableTagsTask.perform() + + /** + * If the user can't edit the tags, for example on a public link, there is no need to load the available tags + */ + if (!unref(readonly)) { + loadAvailableTagsTask.perform() + } }) const keydownMethods = (map, vm) => { diff --git a/packages/web-app-files/src/index.ts b/packages/web-app-files/src/index.ts index 340ec8fb193..4f679303646 100644 --- a/packages/web-app-files/src/index.ts +++ b/packages/web-app-files/src/index.ts @@ -34,12 +34,9 @@ const appInfo = { icon: 'resource-type-folder', color: 'var(--oc-color-swatch-primary-muted)', isFileEditor: false, - extensions: [], - applicationMenu: { - enabled: () => true, - priority: 10 - } + extensions: [] } + export const navItems = (context): AppNavigationItem[] => { const spacesStores = useSpacesStore() const userStore = useUserStore() @@ -127,8 +124,18 @@ export const navItems = (context): AppNavigationItem[] => { export default defineWebApplication({ setup() { + const userStore = useUserStore() + return { - appInfo, + appInfo: { + ...appInfo, + applicationMenu: { + enabled: () => { + return !!userStore.user + }, + priority: 10 + } + }, store, routes: buildRoutes({ App, diff --git a/packages/web-app-files/tests/unit/views/spaces/DriveResolver.spec.ts b/packages/web-app-files/tests/unit/views/spaces/DriveResolver.spec.ts index 976c29b1a2e..52629328b5e 100644 --- a/packages/web-app-files/tests/unit/views/spaces/DriveResolver.spec.ts +++ b/packages/web-app-files/tests/unit/views/spaces/DriveResolver.spec.ts @@ -78,7 +78,7 @@ describe('DriveResolver view', () => { space, internalSpace, mocks: { $clientService: clientService }, - isUserContextReady: true + userContextReady: true }) await wrapper.vm.$nextTick() @@ -94,7 +94,7 @@ describe('DriveResolver view', () => { const { wrapper, mocks } = getMountedWrapper({ space, mocks: { $clientService: clientService }, - isUserContextReady: true + userContextReady: true }) await wrapper.vm.$nextTick() @@ -121,7 +121,7 @@ function getMountedWrapper({ space = undefined, internalSpace = undefined, currentRouteName = 'files-spaces-generic', - isUserContextReady = false, + userContextReady = false, driveAliasAndItem = 'personal/einstein/file', fileId = '1' } = {}) { @@ -157,7 +157,7 @@ function getMountedWrapper({ global: { plugins: [ ...defaultPlugins({ - piniaOptions: { authState: { userContextReady: isUserContextReady } } + piniaOptions: { authState: { userContextReady: userContextReady } } }), store ], diff --git a/packages/web-app-ocm/src/index.ts b/packages/web-app-ocm/src/index.ts index 823ff098966..16629c083e3 100644 --- a/packages/web-app-ocm/src/index.ts +++ b/packages/web-app-ocm/src/index.ts @@ -1,5 +1,5 @@ import App from './views/App.vue' -import { defineWebApplication, useRouter } from '@ownclouders/web-pkg' +import { defineWebApplication, useRouter, useUserStore } from '@ownclouders/web-pkg' import translations from '../l10n/translations.json' import { extensions } from './extensions' import { RouteRecordRaw } from 'vue-router' @@ -27,13 +27,19 @@ export default defineWebApplication({ setup() { const { $gettext } = useGettext() const router = useRouter() + const userStore = useUserStore() const appInfo = { name: $gettext('ScienceMesh'), id: 'ocm', color: '#AE291D', icon: 'contacts-book', - isFileEditor: false + isFileEditor: false, + applicationMenu: { + enabled: () => { + return !!userStore.user + } + } } router.addRoute({ diff --git a/packages/web-app-search/tests/unit/portals/SearchBar.spec.ts b/packages/web-app-search/tests/unit/portals/SearchBar.spec.ts index 8d0fde261ae..178d14eaef1 100644 --- a/packages/web-app-search/tests/unit/portals/SearchBar.spec.ts +++ b/packages/web-app-search/tests/unit/portals/SearchBar.spec.ts @@ -94,7 +94,7 @@ describe('Search Bar portal component', () => { expect(wrapper.find(selectors.search).exists()).toBeFalsy() }) test('does not render a search field if no user given', () => { - wrapper = getMountedWrapper({ isUserContextReady: false }).wrapper + wrapper = getMountedWrapper({ userContextReady: false }).wrapper expect(wrapper.find(selectors.search).exists()).toBeFalsy() }) test('updates the search term on input', () => { @@ -228,7 +228,7 @@ describe('Search Bar portal component', () => { function getMountedWrapper({ mocks = {}, - isUserContextReady = true, + userContextReady = true, providers = [providerFiles, providerContacts] } = {}) { jest.mocked(useAvailableProviders).mockReturnValue(ref(providers)) @@ -253,7 +253,7 @@ function getMountedWrapper({ global: { plugins: [ ...defaultPlugins({ - piniaOptions: { authState: { userContextReady: isUserContextReady } } + piniaOptions: { authState: { userContextReady: userContextReady } } }), store ], diff --git a/packages/web-pkg/src/apps/types.ts b/packages/web-pkg/src/apps/types.ts index 8b5fb31fc52..be8d2be6cca 100644 --- a/packages/web-pkg/src/apps/types.ts +++ b/packages/web-pkg/src/apps/types.ts @@ -42,7 +42,7 @@ export interface ApplicationQuickAction { export type AppConfigObject = Record export interface ApplicationMenuItem { - enabled?: () => boolean + enabled: () => boolean priority?: number openAsEditor?: boolean } diff --git a/packages/web-pkg/src/composables/piniaStores/apps.ts b/packages/web-pkg/src/composables/piniaStores/apps.ts index c18cbb9a82c..4738dd57afc 100644 --- a/packages/web-pkg/src/composables/piniaStores/apps.ts +++ b/packages/web-pkg/src/composables/piniaStores/apps.ts @@ -21,7 +21,7 @@ export const useAppsStore = defineStore('apps', () => { } unref(apps)[appInfo.id] = { - applicationMenu: appInfo.applicationMenu || {}, + applicationMenu: appInfo.applicationMenu || { enabled: () => false }, defaultExtension: appInfo.defaultExtension || '', icon: 'check_box_outline_blank', name: appInfo.name || appInfo.id, diff --git a/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts b/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts index 09af41c037a..3bb5ed473f0 100644 --- a/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts +++ b/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts @@ -422,7 +422,7 @@ describe('ResourceTable', () => { const resource = mockDeep({ id: '1', tags: ['1'] }) const { wrapper } = getMountedWrapper({ props: { resources: [resource] }, - isUserContextReady: false + userContextReady: false }) const resourceRow = wrapper.find(`[data-item-id="${resource.id}"]`) expect(resourceRow.find('.resource-table-tag-wrapper').element.tagName).toEqual('SPAN') @@ -456,7 +456,7 @@ describe('ResourceTable', () => { function getMountedWrapper({ props = {}, - isUserContextReady = true, + userContextReady = true, addProcessingResources = false } = {}) { const storeOptions = defaultStoreMockOptions @@ -494,7 +494,7 @@ function getMountedWrapper({ plugins: [ ...defaultPlugins({ piniaOptions: { - authState: { userContextReady: isUserContextReady }, + authState: { userContextReady: userContextReady }, capabilityState: { capabilities } } }), diff --git a/packages/web-runtime/src/components/Topbar/TopBar.vue b/packages/web-runtime/src/components/Topbar/TopBar.vue index 574910ba672..d353a52ac6b 100644 --- a/packages/web-runtime/src/components/Topbar/TopBar.vue +++ b/packages/web-runtime/src/components/Topbar/TopBar.vue @@ -9,13 +9,13 @@ v-if="appMenuItems.length && !isEmbedModeEnabled" :applications-list="appMenuItems" /> - - + +
@@ -56,13 +56,11 @@ import { useAuthStore, useCapabilityStore, useEmbedMode, - useExtensionRegistry, useRouter, useStore, useThemeStore } from '@ownclouders/web-pkg' import { isRuntimeRoute } from '../../router' -import { getExtensionNavItems } from '../../helpers/navItems' export default { components: { @@ -83,7 +81,6 @@ export default { setup(props) { const store = useStore() const capabilityStore = useCapabilityStore() - const extensionRegistry = useExtensionRegistry() const themeStore = useThemeStore() const { currentTheme } = storeToRefs(themeStore) @@ -100,6 +97,17 @@ export default { ) }) + const homeLink = computed(() => { + if (authStore.publicLinkContextReady && !authStore.userContextReady) { + return { + name: 'resolvePublicLink', + params: { token: authStore.publicLinkToken } + } + } + + return '/' + }) + const isSideBarToggleVisible = computed(() => { return authStore.userContextReady || authStore.publicLinkContextReady }) @@ -115,7 +123,7 @@ export default { } /** - * Returns well formed menuItem objects by a list of extensions. + * Returns well-formed menuItem objects by a list of extensions. * The following properties must be accessible in the wrapping code: * - applicationsList * - $language @@ -128,14 +136,10 @@ export default { return props.applicationsList .filter((app) => { if (app.type === 'extension') { - // check if the extension has at least one navItem with a matching menuId return ( - getExtensionNavItems({ extensionRegistry, appId: app.id }).filter((navItem) => - isNavItemPermitted(permittedMenus, navItem) - ).length > 0 || - (app.applicationMenu.enabled instanceof Function && - app.applicationMenu.enabled(store, ability) && - !permittedMenus.includes('user')) + app.applicationMenu.enabled instanceof Function && + app.applicationMenu.enabled(store, ability) && + !permittedMenus.includes('user') ) } return isNavItemPermitted(permittedMenus, app) @@ -209,7 +213,8 @@ export default { logoWidth, isEmbedModeEnabled, isSideBarToggleVisible, - isSideBarToggleDisabled + isSideBarToggleDisabled, + homeLink } }, computed: { diff --git a/packages/web-runtime/src/components/Topbar/UserMenu.vue b/packages/web-runtime/src/components/Topbar/UserMenu.vue index f1c15839d8e..c246016cc48 100644 --- a/packages/web-runtime/src/components/Topbar/UserMenu.vue +++ b/packages/web-runtime/src/components/Topbar/UserMenu.vue @@ -10,17 +10,17 @@ >