Skip to content

Commit 656c86b

Browse files
authored
Merge pull request #4794 from nextcloud/fix/create-file-public
fix: Create new files on public pages for 31+
2 parents 6e43e3b + 7d181a9 commit 656c86b

9 files changed

Lines changed: 287 additions & 245 deletions

File tree

css/files.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,3 @@
134134
margin-left: 12px;
135135
}
136136
}
137-
138-
@import 'templatePicker';

css/templatePicker.scss

Lines changed: 0 additions & 45 deletions
This file was deleted.

cypress/e2e/templates.spec.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,13 @@ describe('Global templates', function() {
9393
cy.waitForCollabora()
9494
})
9595

96-
// FIXME: Unskip once server API for new menu entries works on public shares
97-
// https://github.com/nextcloud/richdocuments/issues/3170
98-
it.skip('Create a file from a system template as guest', () => {
99-
cy.uploadSystemTemplate()
96+
it('Create a file from a system template as guest', () => {
97+
cy.uploadSystemTemplate({
98+
fixturePath: 'templates/presentation.otp',
99+
fileName: 'myslides.otp',
100+
mimeType: 'application/vnd.oasis.opendocument.presentation-template',
101+
})
102+
100103
cy.createFolder(randUser, '/my-share')
101104

102105
cy.shareLink(randUser, '/my-share', { permissions: 31 }).then((token) => {
@@ -110,25 +113,22 @@ describe('Global templates', function() {
110113
.should('be.visible')
111114
.click()
112115

113-
cy.get('.newFileMenu', { timeout: 10000 })
116+
cy.get('button[role="menuitem"]')
117+
.contains('New presentation')
114118
.should('be.visible')
115-
.contains('.menuitem', 'New presentation')
116-
.as('menuitem')
119+
.click()
120+
cy.get('.input-field__input')
121+
.type('FileFromTemplate')
122+
cy.get('.template:contains("myslides")')
123+
.scrollIntoView()
117124
.should('be.visible')
118125
.click()
119126

120-
cy.get('@menuitem').find('.filenameform input[type=text]').type('FileFromTemplate')
121-
cy.get('@menuitem').find('.filenameform .icon-confirm').click()
122-
123-
cy.get('#template-picker')
124-
.as('form')
125-
.should('be.visible')
126-
.contains('h2', 'systemtemplate')
127+
cy.get('button:contains("Create")')
128+
.scrollIntoView()
127129
.should('be.visible')
128130
.click()
129131

130-
cy.get('.oc-dialog').find('button.primary').click()
131-
132132
cy.waitForViewer()
133133
cy.waitForCollabora()
134134
})
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<NcModal :title="t('richdocuments', 'Select template')" @close="onCancel">
7+
<div class="template-picker">
8+
<NcTextField v-model="filename"
9+
class="filename-input"
10+
type="text"
11+
:label="t('richdocuments', 'File name')"
12+
:error="!!filenameError"
13+
:helper-text="filenameError ?? ''"
14+
@keyup.enter="onCreate" />
15+
<div class="template-container">
16+
<div v-for="template in templates"
17+
:key="template.id"
18+
class="template"
19+
:class="{ selected: selectedTemplateId === template.id }"
20+
@click="selectTemplate(template.id)">
21+
<img v-if="template.preview" :src="templatePreviewUrl(template.id)" :alt="template.name">
22+
<h2>{{ stripFileExtension(template.name) }}</h2>
23+
</div>
24+
</div>
25+
<div class="buttons">
26+
<NcButton type="tertiary" @click="onCancel">
27+
{{ t('core', 'Cancel') }}
28+
</NcButton>
29+
<NcButton type="primary" :disabled="!!filenameError" @click="onCreate">
30+
{{ t('richdocuments', 'Create') }}
31+
</NcButton>
32+
</div>
33+
</div>
34+
</NcModal>
35+
</template>
36+
37+
<script>
38+
import { generateUrl } from '@nextcloud/router'
39+
import { translate as t } from '@nextcloud/l10n'
40+
import { NcModal, NcButton, NcTextField } from '@nextcloud/vue'
41+
import { isFilenameValid, getUniqueName } from '@nextcloud/files'
42+
43+
export default {
44+
name: 'TemplatePicker',
45+
components: {
46+
NcModal,
47+
NcButton,
48+
NcTextField,
49+
},
50+
props: {
51+
suggestedFilename: {
52+
type: String,
53+
default: '',
54+
},
55+
templates: {
56+
type: Array,
57+
required: true,
58+
},
59+
initialTemplateId: {
60+
type: [String, Number],
61+
default: null,
62+
},
63+
content: {
64+
type: Array,
65+
default: () => [],
66+
},
67+
},
68+
data() {
69+
return {
70+
selectedTemplateId: this.initialTemplateId || (this.templates[0]?.id || null),
71+
filename: '',
72+
}
73+
},
74+
computed: {
75+
filenameError() {
76+
if (!isFilenameValid(this.filename)) {
77+
return t('core', 'Invalid file name')
78+
}
79+
80+
if (this.content.some((n) => n.basename === this.filenameWithExtension || n.basename === this.filename)) {
81+
return t('core', 'File name already exists')
82+
}
83+
84+
return null
85+
},
86+
filenameWithExtension() {
87+
if (!this.filename.includes('.')) {
88+
const extension = this.suggestedFilename.split('.').pop()
89+
return this.filename + '.' + extension
90+
}
91+
return this.filename
92+
},
93+
},
94+
mounted() {
95+
this.filename = getUniqueName(this.suggestedFilename, this.content.map((n) => n.basename))
96+
},
97+
methods: {
98+
t,
99+
stripFileExtension(filename) {
100+
return filename.replace(/\.[^/.]+$/, '')
101+
},
102+
templatePreviewUrl(templateId) {
103+
return generateUrl('apps/richdocuments/template/preview/' + templateId)
104+
},
105+
selectTemplate(templateId) {
106+
this.selectedTemplateId = templateId
107+
},
108+
onCancel() {
109+
this.$emit('close')
110+
},
111+
onCreate() {
112+
if (this.filenameError) {
113+
return
114+
}
115+
116+
this.$emit('close', this.selectedTemplateId, this.filenameWithExtension)
117+
},
118+
},
119+
}
120+
</script>
121+
122+
<style lang="scss" scoped>
123+
.template-picker {
124+
padding: calc(var(--default-grid-baseline) * 3);
125+
126+
.filename-input {
127+
margin-bottom: calc(var(--default-grid-baseline) * 3);
128+
}
129+
130+
.template-container {
131+
display: grid;
132+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
133+
gap: calc(var(--default-grid-baseline) * 3);
134+
margin-bottom: calc(var(--default-grid-baseline) * 3);
135+
}
136+
137+
.template {
138+
cursor: pointer;
139+
padding: 10px;
140+
border: var(--border-width-input-focused) solid transparent;
141+
border-radius: var(--border-radius-large);
142+
transition: all 0.2s ease;
143+
display: flex;
144+
flex-direction: column;
145+
align-items: center;
146+
justify-content: center;
147+
148+
&:hover {
149+
background-color: var(--color-background-hover);
150+
}
151+
152+
&.selected {
153+
border-color: var(--color-primary);
154+
}
155+
156+
img {
157+
width: 100%;
158+
height: auto;
159+
border-radius: var(--border-radius-small);
160+
}
161+
162+
h2 {
163+
margin: calc(var(--default-grid-baseline) * 2) 0 0;
164+
text-align: center;
165+
margin-top: auto;
166+
font-size: var(--font-size-small);
167+
}
168+
}
169+
170+
.buttons {
171+
display: flex;
172+
justify-content: flex-end;
173+
gap: calc(var(--default-grid-baseline) * 2);
174+
}
175+
}
176+
</style>

src/helpers/types.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ const getFileTypes = () => {
4949
}
5050

5151
const getFileType = (document) => {
52-
return getFileTypes()[document]
52+
return {
53+
...getFileTypes()[document],
54+
name: document,
55+
}
5356
}
5457

5558
export default {

src/public.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@ import {
1010
isDownloadHidden,
1111
} from './helpers/index.js'
1212
import { getCapabilities } from './services/capabilities.ts'
13-
import NewFileMenu from './view/NewFileMenu.js'
13+
import { registerNewFileMenuEntries } from './view/NewFileMenu.js'
1414

1515
document.addEventListener('DOMContentLoaded', () => {
1616
if (!isPublicShare() || !OCA.Viewer) {
1717
return
1818
}
1919

20-
if (OCA.Files && OCA.Files.fileActions) {
21-
OC.Plugins.register('OCA.Files.NewFileMenu', NewFileMenu)
22-
}
20+
registerNewFileMenuEntries()
2321

2422
const isEnabledFilesPdfViewer = getCapabilities().mimetypesNoDefaultOpen.includes('application/pdf')
2523

src/services/api.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
import axios from '@nextcloud/axios'
77
import { generateOcsUrl, generateFilePath } from '@nextcloud/router'
88
import { getSharingToken } from '@nextcloud/sharing/public'
9-
import { getCurrentDirectory } from '../helpers/filesApp.js'
109

11-
export const createEmptyFile = async (mimeType, fileName, templateId = null) => {
10+
export const createEmptyFile = async (context, mimeType, fileName, templateId = null) => {
1211
const shareToken = getSharingToken()
13-
const directoryPath = getCurrentDirectory()
1412

1513
const response = await axios.post(generateOcsUrl('apps/richdocuments/api/v1/file', 2), {
1614
mimeType,
1715
fileName,
18-
directoryPath,
16+
directoryPath: context.dirname,
1917
shareToken,
2018
templateId,
2119
})

0 commit comments

Comments
 (0)