Skip to content

Commit 4d6641d

Browse files
authored
Merge pull request #12142 from owncloud/feat/psec-popup
feat(password-protected-folders): open folder in an iframe
2 parents bca47d0 + fb77049 commit 4d6641d

File tree

14 files changed

+275
-23
lines changed

14 files changed

+275
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Enhancement: Add password protected folders handler
2+
3+
We've added a new file action used to handle password protected folders. When a password protected folder is opened, a popup is opened prompting the user to enter the password. After successfully entering the password, content of the folder is displayed inside of the popup.
4+
5+
https://github.com/owncloud/web/pull/12142
6+
https://github.com/owncloud/web/issues/12039
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Enhancement: Control more elements visibility via URL query
2+
3+
We've added new params into the URL query that allows configuring elements visibility. The following params can be used:
4+
5+
- `hide-app-switcher`: hides the application switcher in the top bar
6+
- `hide-account-menu`: hides the feedback action, notifications bell, and user menu
7+
- `hide-navigation`: hides the navigation sidebar and mobile navigation
8+
9+
https://github.com/owncloud/web/pull/12142

dev/docker/ocis/csp.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ directives:
1111
- '''self'''
1212
frame-ancestors:
1313
- '''self'''
14+
- 'https://host.docker.internal:9200'
15+
- 'https://host.docker.internal:9201'
1416
frame-src:
1517
- '''self'''
1618
- 'blob:'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<div class="oc-height-1-1" tabindex="0">
3+
<app-loading-spinner v-if="isLoading" />
4+
<iframe
5+
v-show="!isLoading"
6+
id="iframe-folder-view"
7+
ref="iframeRef"
8+
class="oc-width-1-1 oc-height-1-1"
9+
:title="iframeTitle"
10+
:src="iframeUrl.href"
11+
tabindex="0"
12+
@load="onLoad"
13+
></iframe>
14+
</div>
15+
</template>
16+
17+
<script lang="ts" setup>
18+
import { ref } from 'vue'
19+
import { Modal, useThemeStore } from '@ownclouders/web-pkg/src/composables'
20+
import AppLoadingSpinner from '@ownclouders/web-pkg/src/components/AppLoadingSpinner.vue'
21+
import { unref } from 'vue'
22+
23+
const props = defineProps<{
24+
modal: Modal
25+
publicLink: string
26+
}>()
27+
28+
const iframeRef = ref<HTMLIFrameElement>()
29+
const isLoading = ref(true)
30+
const themeStore = useThemeStore()
31+
32+
const iframeTitle = themeStore.currentTheme.common?.name
33+
const iframeUrl = new URL(props.publicLink)
34+
iframeUrl.searchParams.append('hide-logo', 'true')
35+
iframeUrl.searchParams.append('hide-app-switcher', 'true')
36+
iframeUrl.searchParams.append('hide-account-menu', 'true')
37+
iframeUrl.searchParams.append('hide-navigation', 'true')
38+
39+
const onLoad = () => {
40+
isLoading.value = false
41+
unref(iframeRef).contentWindow.focus()
42+
}
43+
</script>
44+
45+
<style lang="scss">
46+
.oc-modal.folder-view-modal {
47+
max-width: 80vw;
48+
border: none;
49+
overflow: hidden;
50+
51+
.oc-modal-title {
52+
display: none;
53+
}
54+
55+
.oc-modal-body {
56+
padding: 0;
57+
58+
&-message {
59+
height: 60vh;
60+
margin: 0;
61+
}
62+
}
63+
64+
.oc-modal-body-actions {
65+
background-color: var(--oc-color-swatch-brand-default);
66+
}
67+
}
68+
</style>

packages/web-app-password-protected-folders/src/composables/useCreateFileHandler.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const useCreateFileHandler = () => {
3030
const folder = await clientService.webdav.createFolder(unref(space), { path: folderPath })
3131
upsertResource(folder)
3232

33-
await addLink({
33+
const share = await addLink({
3434
clientService,
3535
space,
3636
resource: folder,
@@ -39,7 +39,10 @@ export const useCreateFileHandler = () => {
3939

4040
const path = urlJoin(currentFolder.path, fileName + '.psec')
4141

42-
const file = await clientService.webdav.putFileContents(unref(space), { path })
42+
const file = await clientService.webdav.putFileContents(unref(space), {
43+
path,
44+
content: btoa(share.webUrl)
45+
})
4346
upsertResource(file)
4447
}
4548

packages/web-app-password-protected-folders/src/composables/useOpenFolderAction.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
1-
import { FileAction } from '@ownclouders/web-pkg'
1+
import { FileAction, useClientService, useModals } from '@ownclouders/web-pkg'
22
import { computed } from 'vue'
33
import { useGettext } from 'vue3-gettext'
4+
import FolderViewModal from '../components/FolderViewModal.vue'
45

56
export const useOpenFolderAction = () => {
67
const { $gettext } = useGettext()
8+
const { dispatchModal } = useModals()
9+
const clientService = useClientService()
710

811
const action = computed<FileAction>(() => ({
912
name: 'open-password-protected-folder',
1013
icon: 'external-link',
11-
handler: () => {
12-
// TODO: add handler
13-
console.warn('NOT IMPLEMENTED')
14+
async handler({ resources, space }) {
15+
const [file] = resources
16+
const { body } = await clientService.webdav.getFileContents(space, file)
17+
const publicLink = atob(body)
18+
19+
dispatchModal({
20+
title: resources.at(0).name,
21+
elementClass: 'folder-view-modal',
22+
customComponent: FolderViewModal,
23+
customComponentAttrs: () => ({
24+
publicLink
25+
}),
26+
hideConfirmButton: true,
27+
cancelText: $gettext('Close folder')
28+
})
1429
},
1530
label: () => $gettext('Open folder'),
1631
isDisabled: () => false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { defaultComponentMocks, defaultPlugins, shallowMount } from '@ownclouders/web-test-helpers'
2+
import FolderViewModal from '../../../src/components/FolderViewModal.vue'
3+
import { Modal } from '@ownclouders/web-pkg'
4+
import { mock } from 'vitest-mock-extended'
5+
6+
const SELECTORS = Object.freeze({
7+
iframe: '#iframe-folder-view'
8+
})
9+
10+
describe('FolderViewModal', () => {
11+
it('should set iframe src', () => {
12+
const { wrapper } = getWrapper()
13+
const iframe = wrapper.find(SELECTORS.iframe)
14+
15+
expect(iframe.attributes('src')).toEqual(
16+
'https://example.org/public-link?hide-logo=true&hide-app-switcher=true&hide-account-menu=true&hide-navigation=true'
17+
)
18+
})
19+
})
20+
21+
function getWrapper() {
22+
const mocks = defaultComponentMocks()
23+
24+
return {
25+
mocks,
26+
wrapper: shallowMount(FolderViewModal, {
27+
props: {
28+
modal: mock<Modal>(),
29+
publicLink: 'https://example.org/public-link'
30+
},
31+
global: {
32+
plugins: defaultPlugins(),
33+
mocks,
34+
provide: mocks
35+
}
36+
})
37+
}
38+
}

packages/web-app-password-protected-folders/tests/unit/composables/useCreateFileHandler.spec.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { defaultComponentMocks, getComposableWrapper } from '@ownclouders/web-test-helpers'
22
import { useCreateFileHandler } from '../../../src/composables/useCreateFileHandler'
33
import { mock } from 'vitest-mock-extended'
4-
import { Resource, SpaceResource } from '@ownclouders/web-client'
4+
import { LinkShare, Resource, SpaceResource } from '@ownclouders/web-client'
55
import { useSharesStore } from '@ownclouders/web-pkg'
66
import { SharingLinkType } from '@ownclouders/web-client/graph/generated'
7+
import { MockedFunction } from 'vitest'
78

89
const space = mock<SpaceResource>()
910
const currentFolder = mock<Resource>({ path: '/current/folder' })
@@ -15,6 +16,12 @@ describe('createFileHandler', () => {
1516
async setup(instance, mocks) {
1617
const { addLink } = useSharesStore()
1718

19+
;(addLink as MockedFunction<typeof addLink>).mockResolvedValue(
20+
mock<LinkShare>({
21+
webUrl: 'https://example.org/public-link'
22+
})
23+
)
24+
1825
await instance.createFileHandler({
1926
fileName: 'protected',
2027
space,
@@ -33,7 +40,8 @@ describe('createFileHandler', () => {
3340
options: { password: 'Pass$123', type: SharingLinkType.Edit }
3441
})
3542
expect(mocks.$clientService.webdav.putFileContents).toHaveBeenCalledWith(space, {
36-
path: '/current/folder/protected.psec'
43+
path: '/current/folder/protected.psec',
44+
content: btoa('https://example.org/public-link')
3745
})
3846
}
3947
})
@@ -49,6 +57,7 @@ function getWrapper({
4957
) => void
5058
}) {
5159
const mocks = defaultComponentMocks()
60+
5261
mocks.$clientService.webdav.createFolder.mockResolvedValue(createdFolder)
5362

5463
return {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { defaultComponentMocks, getComposableWrapper } from '@ownclouders/web-test-helpers'
2+
import { useOpenFolderAction } from '../../../src/composables/useOpenFolderAction'
3+
import { unref } from 'vue'
4+
import { mock } from 'vitest-mock-extended'
5+
import { Resource, SpaceResource } from '@ownclouders/web-client'
6+
import { useModals } from '@ownclouders/web-pkg'
7+
import { MockedFunction } from 'vitest'
8+
import FolderViewModal from '../../../src/components/FolderViewModal.vue'
9+
10+
describe('openFolderAction', () => {
11+
it('should open a modal with the public link', () => {
12+
getWrapper({
13+
async setup(instance) {
14+
const { dispatchModal } = useModals()
15+
16+
await unref(instance).handler({
17+
resources: [mock<Resource>()],
18+
space: mock<SpaceResource>()
19+
})
20+
21+
const modalConfig = (dispatchModal as MockedFunction<typeof dispatchModal>).mock.calls
22+
.at(0)
23+
.at(0)
24+
const attrs = modalConfig.customComponentAttrs()
25+
26+
expect(dispatchModal).toHaveBeenCalledWith(
27+
expect.objectContaining({ customComponent: FolderViewModal })
28+
)
29+
expect(attrs).toStrictEqual({ publicLink: 'https://example.org/public-link' })
30+
}
31+
})
32+
})
33+
})
34+
35+
function getWrapper({
36+
setup
37+
}: {
38+
setup: (
39+
instance: ReturnType<typeof useOpenFolderAction>,
40+
mocks: ReturnType<typeof defaultComponentMocks>
41+
) => void
42+
}) {
43+
const mocks = defaultComponentMocks()
44+
mocks.$clientService.webdav.getFileContents.mockResolvedValue({
45+
body: btoa('https://example.org/public-link')
46+
})
47+
48+
return {
49+
wrapper: getComposableWrapper(
50+
() => {
51+
const instance = useOpenFolderAction()
52+
setup(instance, mocks)
53+
},
54+
{
55+
mocks,
56+
provide: mocks
57+
}
58+
)
59+
}
60+
}

packages/web-pkg/src/composables/piniaStores/config/types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ const OptionsConfigSchema = z.object({
122122
})
123123
.optional(),
124124
userListRequiresFilter: z.boolean().optional(),
125-
hideLogo: z.boolean().optional()
125+
hideLogo: z.boolean().optional(),
126+
hideAppSwitcher: z.boolean().optional(),
127+
hideAccountMenu: z.boolean().optional(),
128+
hideNavigation: z.boolean().optional()
126129
})
127130

128131
export type OptionsConfig = z.infer<typeof OptionsConfigSchema>

packages/web-runtime/src/components/Topbar/TopBar.vue

+12-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
>
77
<div class="oc-topbar-left oc-flex oc-flex-middle oc-flex-start">
88
<applications-menu
9-
v-if="appMenuExtensions.length && !isEmbedModeEnabled"
9+
v-if="appMenuExtensions.length && !isEmbedModeEnabled && !hideAppSwitcher"
1010
:menu-items="appMenuExtensions"
1111
/>
1212
<router-link v-if="!hideLogo" :to="homeLink" class="oc-width-1-1 oc-logo-href">
@@ -21,12 +21,15 @@
2121
</div>
2222
<template v-if="!isEmbedModeEnabled">
2323
<portal to="app.runtime.header.right" :order="50">
24-
<feedback-link v-if="isFeedbackLinkEnabled" v-bind="feedbackLinkOptions" />
24+
<feedback-link
25+
v-if="isFeedbackLinkEnabled && !hideAccountMenu"
26+
v-bind="feedbackLinkOptions"
27+
/>
2528
</portal>
2629
<portal to="app.runtime.header.right" :order="100">
27-
<notifications v-if="isNotificationBellEnabled" />
30+
<notifications v-if="isNotificationBellEnabled && !hideAccountMenu" />
2831
<side-bar-toggle v-if="isSideBarToggleVisible" :disabled="isSideBarToggleDisabled" />
29-
<user-menu />
32+
<user-menu v-if="!hideAccountMenu" />
3033
</portal>
3134
</template>
3235
<portal-target name="app.runtime.header.left" @change="updateLeftPortal" />
@@ -92,6 +95,8 @@ export default {
9295
9396
const logoWidth = ref('150px')
9497
const hideLogo = computed(() => unref(configOptions).hideLogo)
98+
const hideAppSwitcher = computed(() => unref(configOptions).hideAppSwitcher)
99+
const hideAccountMenu = computed(() => unref(configOptions).hideAccountMenu)
95100
96101
const isNotificationBellEnabled = computed(() => {
97102
return (
@@ -157,7 +162,9 @@ export default {
157162
isSideBarToggleDisabled,
158163
homeLink,
159164
topBarCenterExtensionPoint,
160-
appMenuExtensions
165+
appMenuExtensions,
166+
hideAppSwitcher,
167+
hideAccountMenu
161168
}
162169
},
163170
computed: {

packages/web-runtime/src/container/bootstrap.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ export const announceConfiguration = async ({
162162
rawConfig.options = {
163163
...rawConfig.options,
164164
embed: { ...rawConfig.options?.embed, ...embedConfigFromQuery },
165-
hideLogo: getQueryParam('hide-logo') === 'true'
165+
hideLogo: getQueryParam('hide-logo') === 'true',
166+
hideAppSwitcher: getQueryParam('hide-app-switcher') === 'true',
167+
hideAccountMenu: getQueryParam('hide-account-menu') === 'true',
168+
hideNavigation: getQueryParam('hide-navigation') === 'true'
166169
}
167170

168171
configStore.loadConfig(rawConfig)

0 commit comments

Comments
 (0)