refactor(federation): migrate app frontend (admin settings) to Vue 3

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen
2026-01-05 13:43:48 +01:00
parent 8a05a3e01b
commit 5d3e1f70b2
16 changed files with 938 additions and 180 deletions
-116
View File
@@ -1,116 +0,0 @@
/*!
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* @param $ - The jQuery instance
*/
(function($) {
// ocFederationAddServer
$.fn.ocFederationAddServer = function() {
/* Go easy on jquery and define some vars
========================================================================== */
const $wrapper = $(this),
// Buttons
$btnAddServer = $wrapper.find('#ocFederationAddServerButton'),
$btnSubmit = $wrapper.find('#ocFederationSubmit'),
// Inputs
$inpServerUrl = $wrapper.find('#serverUrl'),
// misc
$msgBox = $wrapper.find('#ocFederationAddServer .msg'),
$srvList = $wrapper.find('#listOfTrustedServers')
/* Interaction
========================================================================== */
$btnAddServer.on('click', function() {
$btnAddServer.addClass('hidden')
$wrapper.find('.serverUrl').removeClass('hidden')
$inpServerUrl
.focus()
})
// trigger server removal
$srvList.on('click', 'li > .icon-delete', function() {
const $this = $(this).parent()
const id = $this.attr('id')
removeServer(id)
})
$btnSubmit.on('click', function() {
addServer($inpServerUrl.val())
})
$inpServerUrl.on('change keyup', function(e) {
const url = $(this).val()
// toggle add-button visibility based on input length
if (url.length > 0) { $btnSubmit.removeClass('hidden') } else { $btnSubmit.addClass('hidden') }
if (e.keyCode === 13) { // add server on "enter"
addServer(url)
} else if (e.keyCode === 27) { // hide input filed again in ESC
$btnAddServer.removeClass('hidden')
$inpServerUrl.val('').addClass('hidden')
$btnSubmit.addClass('hidden')
}
})
}
/* private Functions
========================================================================== */
/**
*
* @param url
*/
function addServer(url) {
OC.msg.startSaving('#ocFederationAddServer .msg')
$.post(
OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers',
{
url,
},
null,
'json',
).done(function({ ocs }) {
const data = ocs.data
$('#serverUrl').attr('value', '')
$('#listOfTrustedServers').prepend($('<li>')
.attr('id', data.id)
.html('<span class="status indeterminate"></span>'
+ data.url
+ '<span class="icon icon-delete"></span>'))
OC.msg.finishedSuccess('#ocFederationAddServer .msg', data.message)
})
.fail(function(jqXHR) {
OC.msg.finishedError('#ocFederationAddServer .msg', JSON.parse(jqXHR.responseText).ocs.meta.message)
})
}
/**
*
* @param id
*/
function removeServer(id) {
$.ajax({
url: OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers/' + id,
type: 'DELETE',
success: function(response) {
$('#ocFederationSettings').find('#' + id).remove()
},
})
}
})(jQuery)
window.addEventListener('DOMContentLoaded', function() {
$('#ocFederationSettings').ocFederationAddServer()
})
+3 -1
View File
@@ -18,11 +18,13 @@ use OCP\Federation\Events\TrustedServerRemovedEvent;
class Application extends App implements IBootstrap {
public const APP_ID = 'federation';
/**
* @param array $urlParams
*/
public function __construct($urlParams = []) {
parent::__construct('federation', $urlParams);
parent::__construct(self::APP_ID, $urlParams);
}
public function register(IRegistrationContext $context): void {
+13 -2
View File
@@ -1,19 +1,25 @@
<?php
/**
/*!
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Federation\Settings;
use OCA\Federation\AppInfo\Application;
use OCA\Federation\TrustedServers;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IDelegatedSettings;
use OCP\Util;
class Admin implements IDelegatedSettings {
public function __construct(
private TrustedServers $trustedServers,
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
private IL10N $l,
) {
}
@@ -24,9 +30,14 @@ class Admin implements IDelegatedSettings {
public function getForm() {
$parameters = [
'trustedServers' => $this->trustedServers->getServers(),
'docUrl' => $this->urlGenerator->linkToDocs('admin-sharing-federated') . '#configuring-trusted-nextcloud-servers',
];
return new TemplateResponse('federation', 'settings-admin', $parameters, '');
$this->initialState->provideInitialState('adminSettings', $parameters);
Util::addStyle(Application::APP_ID, 'settings-admin');
Util::addScript(Application::APP_ID, 'settings-admin');
return new TemplateResponse(Application::APP_ID, 'settings-admin', renderAs: '');
}
/**
@@ -0,0 +1,90 @@
<!--
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { ITrustedServer } from '../services/api.ts'
import { mdiPlus } from '@mdi/js'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { nextTick, ref, useTemplateRef } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import { addServer, ApiError } from '../services/api.ts'
import { logger } from '../services/logger.ts'
const emit = defineEmits<{
add: [server: ITrustedServer]
}>()
const formElement = useTemplateRef<HTMLFormElement>('form')
const newServerUrl = ref('')
/**
* Handle add trusted server form submission
*/
async function onAdd() {
try {
const server = await addServer(newServerUrl.value)
newServerUrl.value = ''
emit('add', server)
nextTick(() => formElement.value?.reset()) // Reset native form validation state
showSuccess(t('federation', 'Added to the list of trusted servers'))
} catch (error) {
logger.error('Failed to add trusted server', { error })
if (error instanceof ApiError) {
showError(error.message)
} else {
showError(t('federation', 'Could not add trusted server. Please try again later.'))
}
}
}
</script>
<template>
<form ref="form" @submit.prevent="onAdd">
<h3 :class="$style.addTrustedServerForm__heading">
{{ t('federation', 'Add trusted server') }}
</h3>
<div :class="$style.addTrustedServerForm__wrapper">
<NcTextField
v-model="newServerUrl"
:label="t('federation', 'Server url')"
placeholder="https://…"
required
type="url" />
<NcButton
:class="$style.addTrustedServerForm__submitButton"
:aria-label="t('federation', 'Add')"
:title="t('federation', 'Add')"
type="submit"
variant="primary">
<template #icon>
<NcIconSvgWrapper :path="mdiPlus" />
</template>
</NcButton>
</div>
</form>
</template>
<style module>
.addTrustedServerForm__heading {
font-size: 1.2rem;
margin-block: 0.5lh 0.25lh;
}
.addTrustedServerForm__wrapper {
display: flex;
gap: var(--default-grid-baseline);
align-items: end;
max-width: 600px;
}
.addTrustedServerForm__submitButton {
max-height: var(--default-clickable-area);
}
</style>
@@ -0,0 +1,121 @@
<!--
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { ITrustedServer } from '../services/api.ts'
import { mdiCheckNetworkOutline, mdiCloseNetworkOutline, mdiHelpNetworkOutline, mdiTrashCanOutline } from '@mdi/js'
import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { computed, ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import { TrustedServerStatus } from '../services/api.ts'
import { deleteServer } from '../services/api.ts'
import { logger } from '../services/logger.ts'
const props = defineProps<{
server: ITrustedServer
}>()
const emit = defineEmits<{
delete: [ITrustedServer]
}>()
const isLoading = ref(false)
const hasError = computed(() => props.server.status === TrustedServerStatus.STATUS_FAILURE)
const serverIcon = computed(() => {
switch (props.server.status) {
case TrustedServerStatus.STATUS_OK:
return mdiCheckNetworkOutline
case TrustedServerStatus.STATUS_PENDING:
case TrustedServerStatus.STATUS_ACCESS_REVOKED:
return mdiHelpNetworkOutline
case TrustedServerStatus.STATUS_FAILURE:
default:
return mdiCloseNetworkOutline
}
})
const serverStatus = computed(() => {
switch (props.server.status) {
case TrustedServerStatus.STATUS_OK:
return [t('federation', 'Server ok'), t('federation', 'User list was exchanged at least once successfully with the remote server.')]
case TrustedServerStatus.STATUS_PENDING:
return [t('federation', 'Server pending'), t('federation', 'Waiting for shared secret or initial user list exchange.')]
case TrustedServerStatus.STATUS_ACCESS_REVOKED:
return [t('federation', 'Server access revoked'), t('federation', 'Server access revoked')]
case TrustedServerStatus.STATUS_FAILURE:
default:
return [t('federation', 'Server failure'), t('federation', 'Connection to the remote server failed or the remote server is misconfigured.')]
}
})
/**
* Emit delete event
*/
async function onDelete() {
try {
isLoading.value = true
await deleteServer(props.server.id)
emit('delete', props.server)
} catch (error) {
isLoading.value = false
logger.error('Failed to delete trusted server', { error })
showError(t('federation', 'Failed to delete trusted server. Please try again later.'))
}
}
</script>
<template>
<li :class="$style.trustedServer">
<NcIconSvgWrapper
:class="{
[$style.trustedServer__icon_error]: hasError,
}"
:path="serverIcon"
:name="serverStatus[0]"
:title="serverStatus[1]" />
<code :class="$style.trustedServer__url" v-text="server.url" />
<NcButton
:aria-label="t('federation', 'Delete')"
:title="t('federation', 'Delete')"
:disabled="isLoading"
@click="onDelete">
<template #icon>
<NcLoadingIcon v-if="isLoading" />
<NcIconSvgWrapper v-else :path="mdiTrashCanOutline" />
</template>
</NcButton>
</li>
</template>
<style module>
.trustedServer {
display: flex;
flex-direction: row;
gap: var(--default-grid-baseline);
align-items: center;
border-radius: var(--border-radius-element);
padding-inline-start: var(--default-grid-baseline);
}
.trustedServer:hover {
background-color: var(--color-background-hover);
}
.trustedServer__icon_error {
color: var(--color-element-error);
}
.trustedServer__url {
padding-inline: 1ch;
flex: 1 0 auto;
}
</style>
+107
View File
@@ -0,0 +1,107 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
import { addServer, ApiError, deleteServer, TrustedServerStatus } from './api.ts'
export const handlers = [
http.post('/ocs/v2.php/apps/federation/trusted-servers', async ({ request }) => {
const { url } = (await request.json()) as { url: string }
if (url === 'https://network-error.com') {
return HttpResponse.error()
}
if (url === 'https://existing-server.com') {
return HttpResponse.json({
ocs: {
meta: {
status: 'failure',
statuscode: 409,
message: 'Server already exists',
},
},
}, { status: 409 })
}
return HttpResponse.json({
ocs: {
meta: {
status: 'ok',
},
data: {
id: 1,
url,
},
},
})
}),
http.delete('/ocs/v2.php/apps/federation/trusted-servers/:id', async ({ params }) => {
if (params.id === '1') {
return HttpResponse.json({
ocs: {
meta: {
status: 'ok',
},
},
})
}
if (params.id === '2') {
return HttpResponse.json({
ocs: {
meta: {
status: 'failure',
statuscode: 404,
message: 'Server does not exist',
},
},
}, { status: 404 })
}
return HttpResponse.error()
}),
]
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
describe('addServer', () => {
test('returns a trusted server object on success', async () => {
const server = await addServer('https://trusted-server.com')
expect(server).toEqual({
id: 1,
url: 'https://trusted-server.com',
status: TrustedServerStatus.STATUS_PENDING,
})
})
test('throws API error when already added', async () => {
await expect(() => addServer('https://existing-server.com')).rejects.toThrowError(ApiError)
await expect(() => addServer('https://existing-server.com')).rejects.toThrow('Server already exists')
})
test('throws error when network error occurs', async () => {
await expect(() => addServer('https://network-error.com')).rejects.toThrowError(Error)
await expect(() => addServer('https://network-error.com')).rejects.not.toThrowError(ApiError)
})
})
describe('deleteServer', () => {
test('resolves on success', async () => {
await expect(deleteServer(1)).resolves.not.toThrow()
})
test('throws API error when already added', async () => {
await expect(() => deleteServer(2)).rejects.toThrowError(ApiError)
await expect(() => deleteServer(2)).rejects.toThrow('Server does not exist')
})
test('throws error when network error occurs', async () => {
await expect(() => deleteServer(3)).rejects.toThrowError(Error)
await expect(() => deleteServer(3)).rejects.not.toThrowError(ApiError)
})
})
+74
View File
@@ -0,0 +1,74 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { OCSResponse } from '@nextcloud/typings/ocs'
import axios, { isAxiosError } from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
export const TrustedServerStatus = Object.freeze({
/** after a user list was exchanged at least once successfully */
STATUS_OK: 1,
/** waiting for shared secret or initial user list exchange */
STATUS_PENDING: 2,
/** something went wrong, misconfigured server, software bug,... user interaction needed */
STATUS_FAILURE: 3,
/** remote server revoked access */
STATUS_ACCESS_REVOKED: 4,
})
export interface ITrustedServer {
id: number
url: string
status: typeof TrustedServerStatus[keyof typeof TrustedServerStatus]
}
export class ApiError extends Error {}
/**
* Add a new trusted server
*
* @param url - The new URL to add
*/
export async function addServer(url: string): Promise<ITrustedServer> {
try {
const { data } = await axios.post<OCSResponse<Omit<ITrustedServer, 'status'>>>(
generateOcsUrl('apps/federation/trusted-servers'),
{ url },
)
const serverData = data.ocs.data
return {
id: serverData.id,
url: serverData.url,
status: TrustedServerStatus.STATUS_PENDING,
}
} catch (error) {
throw mapError(error)
}
}
/**
* @param id - The id of the trusted server to remove
*/
export async function deleteServer(id: number): Promise<void> {
try {
await axios.delete(generateOcsUrl(`apps/federation/trusted-servers/${id}`))
} catch (error) {
throw mapError(error)
}
}
/**
* Error handling for API calls
*
* @param error - The catch error
*/
function mapError(error: unknown): ApiError | unknown {
if (isAxiosError(error) && error.response?.data?.ocs) {
return new ApiError((error.response.data as OCSResponse).ocs.meta.message, { cause: error })
}
return error
}
+8
View File
@@ -0,0 +1,8 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getLoggerBuilder } from '@nextcloud/logger'
export const logger = getLoggerBuilder().setApp('federation').build()
+10
View File
@@ -0,0 +1,10 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { createApp } from 'vue'
import AdminSettings from './views/AdminSettings.vue'
const app = createApp(AdminSettings)
app.mount('#federation-admin-settings')
@@ -0,0 +1,91 @@
<!--
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { ITrustedServer } from '../services/api.ts'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { computed, ref } from 'vue'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import AddTrustedServerForm from '../components/AddTrustedServerForm.vue'
import TrustedServer from '../components/TrustedServer.vue'
import { TrustedServerStatus } from '../services/api.ts'
const adminSettings = loadState<{ docUrl: string, trustedServers: ITrustedServer[] }>('federation', 'adminSettings')
const trustedServers = ref(adminSettings.trustedServers)
const showPendingServerInfo = computed(() => trustedServers.value.some((server) => server.status === TrustedServerStatus.STATUS_PENDING))
/**
* Handle add trusted server form submission
*
* @param server - The server to add
*/
async function onAdd(server: ITrustedServer) {
trustedServers.value.unshift(server)
}
/**
* Handle delete trusted server event
*
* @param server - The server to delete
*/
function onDelete(server: ITrustedServer) {
trustedServers.value = trustedServers.value.filter((s) => s.id !== server.id)
}
</script>
<template>
<NcSettingsSection
:name="t('federation', 'Trusted servers')"
:doc-url="adminSettings.docUrl"
:description="t('federation', 'Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share.')">
<NcNoteCard
v-if="showPendingServerInfo"
type="info"
:text="t('federation', 'Each server must validate the other. This process may require a few cron cycles.')" />
<TransitionGroup
:class="$style.federationAdminSettings__trustedServersList"
:aria-label="t('federation', 'Trusted servers')"
tag="ul"
:enter-from-class="$style.transition_hidden"
:enter-active-class="$style.transition_active"
:leave-active-class="$style.transition_active"
:leave-to-class="$style.transition_hidden">
<TrustedServer
v-for="server in trustedServers"
:key="server.id"
:class="$style.federationAdminSettings__trustedServersListItem"
:server="server"
@delete="onDelete" />
</TransitionGroup>
<AddTrustedServerForm @add="onAdd" />
</NcSettingsSection>
</template>
<style module>
.federationAdminSettings__trustedServersList {
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
width: fit-content;
}
.federationAdminSettings__trustedServersListItem {
width: 100%;
}
.transition_active {
transition: all 0.5s ease;
}
.transition_hidden {
opacity: 0;
transform: translateX(30px);
}
</style>
+3 -60
View File
@@ -1,64 +1,7 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
use OCA\Federation\TrustedServers;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Server;
use OCP\Util;
/** @var IL10N $l */
Util::addScript('federation', 'settings-admin');
Util::addStyle('federation', 'settings-admin');
$urlGenerator = Server::get(IURLGenerator::class);
$documentationLink = $urlGenerator->linkToDocs('admin-sharing-federated') . '#configuring-trusted-nextcloud-servers';
$documentationLabel = $l->t('External documentation for Federated Cloud Sharing');
?>
<div id="ocFederationSettings" class="section">
<h2>
<?php p($l->t('Trusted servers')); ?>
<a target="_blank" rel="noreferrer noopener" class="icon-info"
title="<?php p($documentationLabel);?>"
href="<?php p($documentationLink); ?>"></a>
</h2>
<p class="settings-hint"><?php p($l->t('Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share.')); ?></p>
<p class="settings-hint"><?php p($l->t('Each server must validate the other. This process may require a few cron cycles.')); ?></p>
<ul id="listOfTrustedServers">
<?php foreach ($_['trustedServers'] as $trustedServer) { ?>
<li id="<?php p($trustedServer['id']); ?>">
<?php if ((int)$trustedServer['status'] === TrustedServers::STATUS_OK) { ?>
<span class="status success"></span>
<?php
} elseif (
(int)$trustedServer['status'] === TrustedServers::STATUS_PENDING
|| (int)$trustedServer['status'] === TrustedServers::STATUS_ACCESS_REVOKED
) { ?>
<span class="status indeterminate"></span>
<?php } else {?>
<span class="status error"></span>
<?php } ?>
<?php p($trustedServer['url']); ?>
<span class="icon icon-delete"></span>
</li>
<?php } ?>
</ul>
<div id="ocFederationAddServer">
<button id="ocFederationAddServerButton"><?php p($l->t('+ Add trusted server')); ?></button>
<div class="serverUrl hidden">
<div class="serverUrl-block">
<label for="serverUrl"><?php p($l->t('Trusted server')); ?></label>
<input id="serverUrl" type="text" value="" placeholder="<?php p($l->t('Trusted server')); ?>" name="server_url"/>
<button id="ocFederationSubmit" class="hidden"><?php p($l->t('Add')); ?></button>
</div>
<span class="msg"></span>
</div>
</div>
</div>
<div id="federation-admin-settings"></div>
+19 -1
View File
@@ -10,24 +10,35 @@ namespace OCA\Federation\Tests\Settings;
use OCA\Federation\Settings\Admin;
use OCA\Federation\TrustedServers;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IL10N;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class AdminTest extends TestCase {
private TrustedServers&MockObject $trustedServers;
private IInitialState&MockObject $initialState;
private IURLGenerator&MockObject $urlGenerator;
private Admin $admin;
protected function setUp(): void {
parent::setUp();
$this->trustedServers = $this->createMock(TrustedServers::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->admin = new Admin(
$this->trustedServers,
$this->initialState,
$this->urlGenerator,
$this->createMock(IL10N::class)
);
}
public function testGetForm(): void {
$this->urlGenerator->method('linkToDocs')
->with('admin-sharing-federated')
->willReturn('docs://federated_sharing');
$this->trustedServers
->expects($this->once())
->method('getServers')
@@ -35,8 +46,15 @@ class AdminTest extends TestCase {
$params = [
'trustedServers' => ['myserver', 'secondserver'],
'docUrl' => 'docs://federated_sharing#configuring-trusted-nextcloud-servers',
];
$expected = new TemplateResponse('federation', 'settings-admin', $params, '');
$this->initialState
->expects($this->once())
->method('provideInitialState')
->with('adminSettings', $params);
$expected = new TemplateResponse('federation', 'settings-admin', renderAs: '');
$this->assertEquals($expected, $this->admin->getForm());
}
+1
View File
@@ -0,0 +1 @@
../../../apps/federation
+3
View File
@@ -12,6 +12,9 @@ const modules = {
'settings-admin-example-content': resolve(import.meta.dirname, 'apps/dav/src', 'settings-admin-example-content.ts'),
'settings-personal-availability': resolve(import.meta.dirname, 'apps/dav/src', 'settings-personal-availability.ts'),
},
federation: {
'settings-admin': resolve(import.meta.dirname, 'apps/federation/src', 'settings-admin.ts'),
},
federatedfilesharing: {
'init-files': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'init-files.js'),
'settings-admin': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'settings-admin.ts'),
+394
View File
@@ -65,6 +65,7 @@
"is-svg": "^6.1.0",
"jsdom": "^27.4.0",
"jsdom-testing-mocks": "^1.16.0",
"msw": "^2.12.7",
"sass": "^1.97.1",
"stylelint": "^16.26.1",
"stylelint-use-logical": "^2.1.2",
@@ -1731,6 +1732,122 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@inquirer/ansi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
"integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@inquirer/confirm": {
"version": "5.1.21",
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
"integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/core": {
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
"signal-exit": "^4.1.0",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/core/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@inquirer/core/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/figures": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@inquirer/type": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
"integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -2093,6 +2210,24 @@
"dev": true,
"license": "MIT"
},
"node_modules/@mswjs/interceptors": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz",
"integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@open-draft/deferred-promise": "^2.2.0",
"@open-draft/logger": "^0.3.0",
"@open-draft/until": "^2.0.0",
"is-node-process": "^1.2.0",
"outvariant": "^1.4.3",
"strict-event-emitter": "^0.5.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@nextcloud/auth": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.5.3.tgz",
@@ -2627,6 +2762,31 @@
"dev": true,
"license": "MIT"
},
"node_modules/@open-draft/deferred-promise": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
"dev": true,
"license": "MIT"
},
"node_modules/@open-draft/logger": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
"integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-node-process": "^1.2.0",
"outvariant": "^1.4.0"
}
},
"node_modules/@open-draft/until": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
"dev": true,
"license": "MIT"
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
@@ -3938,6 +4098,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/statuses": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
"integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/tmp": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
@@ -6053,6 +6220,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 12"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -6278,6 +6455,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/copy-anything": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
@@ -9095,6 +9286,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/graphql": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
"integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -9304,6 +9505,13 @@
"he": "bin/he"
}
},
"node_modules/headers-polyfill": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
"integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
"dev": true,
"license": "MIT"
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
@@ -9940,6 +10148,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-node-process": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
"integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
"dev": true,
"license": "MIT"
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -12012,6 +12227,101 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/msw": {
"version": "2.12.7",
"resolved": "https://registry.npmjs.org/msw/-/msw-2.12.7.tgz",
"integrity": "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@inquirer/confirm": "^5.0.0",
"@mswjs/interceptors": "^0.40.0",
"@open-draft/deferred-promise": "^2.2.0",
"@types/statuses": "^2.0.6",
"cookie": "^1.0.2",
"graphql": "^16.12.0",
"headers-polyfill": "^4.0.2",
"is-node-process": "^1.2.0",
"outvariant": "^1.4.3",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"rettime": "^0.7.0",
"statuses": "^2.0.2",
"strict-event-emitter": "^0.5.1",
"tough-cookie": "^6.0.0",
"type-fest": "^5.2.0",
"until-async": "^3.0.2",
"yargs": "^17.7.2"
},
"bin": {
"msw": "cli/index.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mswjs"
},
"peerDependencies": {
"typescript": ">= 4.8.x"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/msw/node_modules/tldts": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
"integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"tldts-core": "^7.0.19"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/msw/node_modules/tldts-core": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
"integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
"dev": true,
"license": "MIT"
},
"node_modules/msw/node_modules/tough-cookie": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^7.0.5"
},
"engines": {
"node": ">=16"
}
},
"node_modules/msw/node_modules/type-fest": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz",
"integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
@@ -12019,6 +12329,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/mute-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/nan": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
@@ -12384,6 +12704,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/outvariant": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
"integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
"dev": true,
"license": "MIT"
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -12665,6 +12992,13 @@
"dev": true,
"license": "ISC"
},
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"dev": true,
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -13691,6 +14025,13 @@
"node": ">=8"
}
},
"node_modules/rettime": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz",
"integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==",
"dev": true,
"license": "MIT"
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -14605,6 +14946,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/std-env": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
@@ -14680,6 +15031,13 @@
"node": ">= 6"
}
},
"node_modules/strict-event-emitter": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
"integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -15426,6 +15784,19 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@@ -16043,6 +16414,16 @@
"node": ">= 10.0.0"
}
},
"node_modules/until-async": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
"integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/kettanaito"
}
},
"node_modules/untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
@@ -17497,6 +17878,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoctocolors-cjs": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+1
View File
@@ -94,6 +94,7 @@
"is-svg": "^6.1.0",
"jsdom": "^27.4.0",
"jsdom-testing-mocks": "^1.16.0",
"msw": "^2.12.7",
"sass": "^1.97.1",
"stylelint": "^16.26.1",
"stylelint-use-logical": "^2.1.2",