Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions cypress/component/ImportContextSchemePreview.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import ImportContextSchemePreview from '../../src/modules/modals/ImportContextSchemePreview.vue'

const MENU_SWITCH = '[data-cy="importSchemeMenu"]'
const SKIPPED = '[data-cy="importSchemeMenuSkipped"]'

const schemeMenu = [
{ label: 'Dashboard', targetType: 'page', targetPage: 'dashboard', technicalName: 'dashboard' },
{ label: 'Docs', targetType: 'url', url: 'https://example.org', technicalName: 'docs', parent: 'dashboard' },
]

describe('Import application scheme preview', () => {
it('sends the menu of the scheme with the import', () => {
mountPreview({ from: [{ label: 'Old item' }], to: schemeMenu, skipped: [] })
.then(({ wrapper }) => {
cy.get(MENU_SWITCH).should('exist')
cy.contains('Old item').should('exist')
cy.contains('Docs').should('exist')
cy.wrap(null).then(() => {
expect(lastPayload(wrapper).menuItems).to.deep.equal(schemeMenu)
})
})
})

it('leaves the menu out when the switch is turned off', () => {
mountPreview({ from: [], to: schemeMenu, skipped: [] })
.then(({ wrapper }) => {
cy.get(MENU_SWITCH).click({ force: true })
cy.wrap(null).then(() => {
expect(lastPayload(wrapper)).to.not.have.property('menuItems')
})
})
})

it('lists the items the import would skip', () => {
mountPreview({ from: [], to: [], skipped: [{ index: 0, label: 'Missing table', reason: 'its table is not here or cannot be managed by the user' }] })

cy.get(SKIPPED).should('contain', 'Missing table')
})

it('has no menu section for a preview without a menu', () => {
mountPreview(undefined)
.then(({ wrapper }) => {
cy.get(MENU_SWITCH).should('not.exist')
cy.wrap(null).then(() => {
expect(lastPayload(wrapper)).to.not.have.property('menuItems')
})
})
})
})

/**
* @param {object|undefined} menuItems the menu part of the preview response
* @return {Cypress.Chainable} the mount result
*/
function mountPreview(menuItems) {
return cy.mount(ImportContextSchemePreview, {
props: {
previewData: {
name: { from: 'Intake', to: 'Intake' },
icon: { from: 'star', to: 'star' },
description: { from: '', to: '' },
nodes: { from: [], to: [] },
addTables: {},
modifyTables: {},
...(menuItems === undefined ? {} : { menuItems }),
},
},
})
}

/**
* @param {object} wrapper the mounted component
* @return {object} the last import body the preview emitted
*/
function lastPayload(wrapper) {
const emitted = wrapper.emitted('update')
return emitted[emitted.length - 1][0]
}
12 changes: 10 additions & 2 deletions lib/Controller/ContextController.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,10 @@ public function exportScheme(int $contextId): DataResponse {
/**
* [api v2] Preview the changes that would be applied to a context scheme
*
* When the scheme carries `menuItems`, the changes hold `menuItems` with the current
* menu under `from`, the items an import would save under `to` and the items it would
* skip, with the reason, under `skipped`.
*
* @param int $contextId ID of the context
* @psalm-param TablesContext $updateScheme New scheme of the context
*
Expand Down Expand Up @@ -366,6 +370,7 @@ public function previewSchemeChanges(int $contextId, array $updateScheme): DataR
* @param string $description Description of the context
* @param list<array{node_type: int, node_uuid: string, permissions: int}> $nodes Ordered meta data of the related nodes
* @psalm-param array{addTables: list<TablesTable>, modifyTables: list<TablesTable>} $tables Tables to be added or modified
* @param list<array<string, mixed>>|null $menuItems Menu of the scheme, replacing the menu of the context: tables and views are named by `targetUuid`, pages by their technical name in `targetPage` and parents by their technical name in `parent`; items that cannot be used here are skipped. Left out, the menu stays as it is
*
* @return DataResponse<Http::STATUS_OK, array<string, mixed>, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND|Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST, array{message: string}, array{}>
*
Expand All @@ -376,12 +381,15 @@ public function previewSchemeChanges(int $contextId, array $updateScheme): DataR
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_MANAGE, typeParam: 'context', idParam: 'contextId')]
public function importScheme(int $contextId, string $name, string $iconName, string $description, array $nodes, array $tables): DataResponse {
public function importScheme(int $contextId, string $name, string $iconName, string $description, array $nodes, array $tables, ?array $menuItems = null): DataResponse {
try {
$this->db->beginTransaction();
$context = $this->contextService->importScheme($contextId, $name, $iconName, $description, $nodes, $tables, $this->userId);
$context = $this->contextService->importScheme($contextId, $name, $iconName, $description, $nodes, $tables, $this->userId, $menuItems);
$this->db->commit();
return new DataResponse($context->jsonSerialize());
} catch (BadRequestError $e) {
$this->db->rollBack();
return $this->handleBadRequestError($e);
} catch (\InvalidArgumentException $e) {
$this->db->rollBack();
return $this->handleBadRequestError(new BadRequestError($e->getMessage(), $e->getCode(), $e));
Expand Down
13 changes: 8 additions & 5 deletions lib/Db/TableMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,21 @@ public function find(int $id): Table {
}

/**
* @param string $uuid
* @return Table
* @throws DoesNotExistException
* The table with the given uuid, or null when there is none.
*
* @throws Exception
* @throws MultipleObjectsReturnedException
*/
public function findByUuid(string $uuid): Table {
public function findByUuid(string $uuid): ?Table {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->table)
->where($qb->expr()->eq('uuid', $qb->createNamedParameter($uuid, IQueryBuilder::PARAM_STR)));
return $this->findEntity($qb);
try {
return $this->findEntity($qb);
} catch (DoesNotExistException) {
return null;
}
}

/**
Expand Down
9 changes: 7 additions & 2 deletions lib/Db/ViewMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ public function findIdToTitleMap(array $ids): array {
}

/**
* @throws DoesNotExistException
* The view with the given uuid, or null when there is none.
*
* @throws Exception
* @throws MultipleObjectsReturnedException
*/
Expand All @@ -249,6 +250,10 @@ public function findByUuid(string $uuid): ?View {
->from($this->table, 'v')
->innerJoin('v', 'tables_tables', 't', 't.id = v.table_id')
->where($qb->expr()->eq('v.uuid', $qb->createNamedParameter($uuid)));
return $this->findEntity($qb);
try {
return $this->findEntity($qb);
} catch (DoesNotExistException) {
return null;
}
}
}
6 changes: 6 additions & 0 deletions lib/Model/ContextScheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function __construct(
protected ?array $nodes = [],
protected ?array $pages = [],
protected ?array $tables = [],
protected ?array $menuItems = [],
) {
}

Expand Down Expand Up @@ -45,6 +46,10 @@ public function getTables(): ?array {
return $this->tables;
}

public function getMenuItems(): ?array {
return $this->menuItems;
}

public function jsonSerialize(): mixed {
return [
'name' => $this->name,
Expand All @@ -53,6 +58,7 @@ public function jsonSerialize(): mixed {
'nodes' => $this->nodes,
'pages' => $this->pages,
'tables' => $this->tables,
'menuItems' => $this->menuItems,
];
}
}
Loading
Loading