Merge pull request #54949 from nextcloud/backport/54565/stable30

[stable30] feat(file_sharing): Provide template creator list in public shares
This commit is contained in:
Andy Scherzinger 2025-09-15 16:17:21 +02:00 committed by GitHub
commit 1fbd6fcea7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 98 additions and 51 deletions

View file

@ -9,6 +9,7 @@ import type { TemplateFile } from '../types.ts'
import { Folder, Node, Permission, addNewFileMenuEntry } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { isPublicShare } from '@nextcloud/sharing/public'
import { newNodeName } from '../utils/newNodeDialog'
import { translate as t } from '@nextcloud/l10n'
import Vue, { defineAsyncComponent } from 'vue'
@ -46,7 +47,12 @@ const getTemplatePicker = async (context: Folder) => {
* Register all new-file-menu entries for all template providers
*/
export function registerTemplateEntries() {
const templates = loadState<TemplateFile[]>('files', 'templates', [])
let templates: TemplateFile[]
if (isPublicShare()) {
templates = loadState<TemplateFile[]>('files_sharing', 'templates', [])
} else {
templates = loadState<TemplateFile[]>('files', 'templates', [])
}
// Init template files menu
templates.forEach((provider, index) => {

View file

@ -50,7 +50,8 @@ import type { TemplateFile } from '../types.ts'
import { getCurrentUser } from '@nextcloud/auth'
import { showError, spawnDialog } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { File } from '@nextcloud/files'
import { File, Node } from '@nextcloud/files'
import { getClient, getRootPath, resultToNode, getDefaultPropfind } from '@nextcloud/files/dav'
import { translate as t } from '@nextcloud/l10n'
import { generateRemoteUrl } from '@nextcloud/router'
import { normalize, extname, join } from 'path'
@ -62,6 +63,7 @@ import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import TemplatePreview from '../components/TemplatePreview.vue'
import TemplateFiller from '../components/TemplateFiller.vue'
import logger from '../logger.ts'
import type { FileStat, ResponseDataDetailed } from 'webdav'
const border = 2
const margin = 8
@ -165,6 +167,12 @@ export default defineComponent({
this.name = name
this.provider = provider
// Skip templates logic for external users.
if (getCurrentUser() === null) {
this.onSubmit()
return
}
const templates = await getTemplates()
const fetchedProvider = templates.find((fetchedProvider) => fetchedProvider.app === provider.app && fetchedProvider.label === provider.label)
if (fetchedProvider === null) {
@ -216,56 +224,80 @@ export default defineComponent({
this.name = `${this.name}${this.provider?.extension ?? ''}`
}
try {
const fileInfo = await createFromTemplate(
normalize(`${currentDirectory}/${this.name}`),
this.selectedTemplate?.filename as string ?? '',
this.selectedTemplate?.templateType as string ?? '',
templateFields,
)
logger.debug('Created new file', fileInfo)
// Create a blank file for external users as we can't use the templates.
if (getCurrentUser() === null) {
const client = getClient()
const filename = join(getRootPath(), currentDirectory, this.name ?? '')
const owner = getCurrentUser()?.uid || null
const node = new File({
id: fileInfo.fileid,
source: generateRemoteUrl(join(`dav/files/${owner}`, fileInfo.filename)),
root: `/files/${owner}`,
mime: fileInfo.mime,
mtime: new Date(fileInfo.lastmod * 1000),
owner,
size: fileInfo.size,
permissions: fileInfo.permissions,
attributes: {
// Inherit some attributes from parent folder like the mount type and real owner
'mount-type': this.parent?.attributes?.['mount-type'],
'owner-id': this.parent?.attributes?.['owner-id'],
'owner-display-name': this.parent?.attributes?.['owner-display-name'],
...fileInfo,
'has-preview': fileInfo.hasPreview,
},
})
await client.putFileContents(filename, '')
const response = await client.stat(filename, { data: getDefaultPropfind(), details: true }) as ResponseDataDetailed<FileStat>
logger.debug('Created new file', { fileInfo: response.data })
// Update files list
emit('files:node:created', node)
const node = resultToNode(response.data)
// Open the new file
window.OCP.Files.Router.goToRoute(
null, // use default route
{ view: 'files', fileid: node.fileid },
{ dir: node.dirname, openfile: 'true' },
)
this.handleFileCreation(node)
} else {
try {
const fileInfo = await createFromTemplate(
normalize(`${currentDirectory}/${this.name}`),
this.selectedTemplate?.filename as string ?? '',
this.selectedTemplate?.templateType as string ?? '',
templateFields,
)
logger.debug('Created new file', { fileInfo })
// Close the picker
this.close()
} catch (error) {
logger.error('Error while creating the new file from template', { error })
showError(t('files', 'Unable to create new file from template'))
} finally {
this.loading = false
const owner = getCurrentUser()?.uid || null
const node = new File({
id: fileInfo.fileid,
source: generateRemoteUrl(join(`dav/files/${owner}`, fileInfo.filename)),
root: `/files/${owner}`,
mime: fileInfo.mime,
mtime: new Date(fileInfo.lastmod * 1000),
owner,
size: fileInfo.size,
permissions: fileInfo.permissions,
attributes: {
// Inherit some attributes from parent folder like the mount type and real owner
'mount-type': this.parent?.attributes?.['mount-type'],
'owner-id': this.parent?.attributes?.['owner-id'],
'owner-display-name': this.parent?.attributes?.['owner-display-name'],
...fileInfo,
'has-preview': fileInfo.hasPreview,
},
})
this.handleFileCreation(node)
// Close the picker
this.close()
} catch (error) {
logger.error('Error while creating the new file from template', { error })
showError(t('files', 'Unable to create new file from template'))
} finally {
this.loading = false
}
}
},
handleFileCreation(node: Node) {
// Update files list
emit('files:node:created', node)
// Open the new file
window.OCP.Files.Router.goToRoute(
null, // use default route
{ view: 'files', fileid: node.fileid },
{ dir: node.dirname, openfile: 'true' },
)
},
async onSubmit() {
// Skip templates logic for external users.
if (getCurrentUser() === null) {
this.loading = true
return this.createFile()
}
if (this.selectedTemplate?.fields?.length > 0) {
spawnDialog(TemplateFiller, {
fields: this.selectedTemplate.fields,

View file

@ -25,6 +25,7 @@ use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\Template\ITemplateManager;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
@ -50,6 +51,7 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider
private Defaults $defaults,
private IConfig $config,
private IRequest $request,
private ITemplateManager $templateManager,
private IInitialState $initialState,
) {
}
@ -219,6 +221,8 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider
Util::addHeader('meta', ['property' => 'og:type', 'content' => 'object']);
Util::addHeader('meta', ['property' => 'og:image', 'content' => $ogPreview]);
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
$this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share));
$csp = new ContentSecurityPolicy();

View file

@ -31,6 +31,7 @@ use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\Storage;
use OCP\Files\Template\ITemplateManager;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
@ -72,6 +73,8 @@ class ShareControllerTest extends \Test\TestCase {
private $shareManager;
/** @var IUserManager|MockObject */
private $userManager;
/** @var ITemplateManager&MockObject */
private $templateManager;
/** @var FederatedShareProvider|MockObject */
private $federatedShareProvider;
/** @var IAccountManager|MockObject */
@ -97,6 +100,7 @@ class ShareControllerTest extends \Test\TestCase {
$this->previewManager = $this->createMock(IPreview::class);
$this->config = $this->createMock(IConfig::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->templateManager = $this->createMock(ITemplateManager::class);
$this->federatedShareProvider = $this->createMock(FederatedShareProvider::class);
$this->federatedShareProvider->expects($this->any())
->method('isOutgoingServer2serverShareEnabled')->willReturn(true);
@ -123,6 +127,7 @@ class ShareControllerTest extends \Test\TestCase {
$this->defaults,
$this->config,
$this->createMock(IRequest::class),
$this->templateManager,
$this->createMock(IInitialState::class)
)
);

2
dist/4013-4013.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
4013-4013.js.license

2
dist/5708-5708.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/5708-5708.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/5708-5708.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
5708-5708.js.license

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long