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
Lines changed: 6 additions & 0 deletions
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
Lines changed: 9 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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:'
Lines changed: 68 additions & 0 deletions
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

Lines changed: 5 additions & 2 deletions
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

Lines changed: 19 additions & 4 deletions
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,
Lines changed: 38 additions & 0 deletions
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

Lines changed: 11 additions & 2 deletions
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 {
Lines changed: 60 additions & 0 deletions
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

Lines changed: 4 additions & 1 deletion
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>

0 commit comments

Comments
 (0)