Repository navigation
feat(contexts): keep widgets in step with pages and applications #3132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,7 +37,7 @@ protected function getFindContextBaseQuery(?string $userId): IQueryBuilder { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->select( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'c.*', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'r.id as node_rel_id', 'r.node_id', 'r.node_type', 'r.permissions', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'p.id as page_id', 'p.page_type', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'p.id as page_id', 'p.page_type', 'p.technical_name as page_technical_name', 'p.title as page_title', 'p.icon as page_icon', 'p.description as page_description', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'pc.id as content_id', 'pc.order', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'n.display_mode as display_mode_default', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 's.id as share_id', 's.receiver', 's.receiver_type' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -58,7 +58,11 @@ protected function getFindContextBaseQuery(?string $userId): IQueryBuilder { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->leftJoin('c', 'tables_contexts_page', 'p', $qb->expr()->eq('c.id', 'p.context_id')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // only the start page: other pages are loaded by withOtherPages() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->leftJoin('c', 'tables_contexts_page', 'p', $qb->expr()->andX( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->expr()->eq('c.id', 'p.context_id'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->expr()->eq('p.page_type', $qb->createNamedParameter(Page::TYPE_STARTPAGE)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->leftJoin('p', 'tables_contexts_page_content', 'pc', $qb->expr()->eq('p.id', 'pc.page_id')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->leftJoin('c', 'tables_contexts_rel_context_node', 'r', $qb->expr()->eq('c.id', 'r.context_id')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->leftJoin('s', 'tables_contexts_navigation', 'n', $qb->expr()->andX( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -72,6 +76,7 @@ protected function getFindContextBaseQuery(?string $userId): IQueryBuilder { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->orderBy('pc.order', 'ASC'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->addOrderBy('r.id', 'ASC'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $qb; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -131,7 +136,11 @@ protected function formatResultRows(array $rows, ?string $userId) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']] ??= ['content' => []]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']]['id'] = (int)$item['page_id']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']]['page_type'] = $item['page_type']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($item['node_rel_id'] !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']]['technical_name'] = $item['page_technical_name']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']]['title'] = $item['page_title']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']]['icon'] = $item['page_icon']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']]['description'] = $item['page_description']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($item['node_rel_id'] !== null && $item['content_id'] !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $carry[$item['page_id']]['content'][$item['content_id']] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'order' => (int)$item['order'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'node_rel_id' => (int)$item['node_rel_id'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -152,7 +161,7 @@ public function findAll(?string $userId = null): array { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb = $this->getFindContextBaseQuery($userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $result = $qb->executeQuery(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $r = $result->fetchAllAssociative(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $r = $this->withOtherPages($result->fetchAllAssociative()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $contextIds = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach ($r as $row) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -234,7 +243,7 @@ public function findById(int $contextId, ?string $userId = null): Context { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->andWhere($qb->expr()->eq('c.id', $qb->createNamedParameter($contextId, IQueryBuilder::PARAM_INT))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $result = $qb->executeQuery(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $r = $result->fetchAllAssociative(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $r = $this->withOtherPages($result->fetchAllAssociative()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following up on #3132 (comment): the reply and the PR description say the permission checks skip the extra page query, but the context checks go through On the dev instance the profiler shows Each query is small, but they sit on hot paths, while only the context responses, |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (empty($r)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new NotFoundError('Context does not exist'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -304,6 +313,42 @@ public function findIdToNameMap(array $ids): array { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $map; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Adds a row for every page other than the start page of the contexts in the rows, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * holding only the context id and the page fields. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @throws Exception | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private function withOtherPages(array $rows): array { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $contextIds = array_values(array_unique(array_map(static fn (array $row): int => (int)$row['id'], $rows))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($contextIds === []) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $rows; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb = $this->db->getQueryBuilder(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $qb->select('id', 'context_id', 'page_type', 'technical_name', 'title', 'icon', 'description') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ->from('tables_contexts_page') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ->where($qb->expr()->in('context_id', $qb->createNamedParameter($contextIds, IQueryBuilder::PARAM_INT_ARRAY))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ->andWhere($qb->expr()->neq('page_type', $qb->createNamedParameter(Page::TYPE_STARTPAGE))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ->orderBy('id'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $result = $qb->executeQuery(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while ($page = $result->fetchAssociative()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $rows[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'id' => $page['context_id'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'share_id' => null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'node_rel_id' => null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'content_id' => null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'page_id' => $page['id'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'page_type' => $page['page_type'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'page_technical_name' => $page['technical_name'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'page_title' => $page['title'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'page_icon' => $page['icon'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'page_description' => $page['description'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $result->closeCursor(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+330
to
+348
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same Oracle limit as in #3131 (comment), but this list covers whole instances: Reproduced on the dev instance with 1001 throwaway applications:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $rows; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected function applyOwnedOrSharedQuery(IQueryBuilder $qb, string $userId): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $sharedToConditions = $qb->expr()->orX(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |||||
| use OCA\Tables\Errors\InternalError; | ||||||
| use OCA\Tables\Errors\NotFoundError; | ||||||
| use OCA\Tables\Errors\PermissionError; | ||||||
| use OCA\Tables\Helper\IconHelper; | ||||||
| use OCA\Tables\Model\ColumnSettings; | ||||||
| use OCA\Tables\Model\ContextScheme; | ||||||
| use OCA\Tables\Model\FilterSet; | ||||||
|
|
@@ -63,6 +64,8 @@ public function __construct( | |||||
| protected IURLGenerator $urlGenerator, | ||||||
| private TableMapper $tableMapper, | ||||||
| private ViewMapper $viewMapper, | ||||||
| private WidgetService $widgetService, | ||||||
| private GridWidgetTypes $gridWidgetTypes, | ||||||
| ) { | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -342,6 +345,7 @@ public function delete(int $contextId, string $userId): Context { | |||||
| $this->shareService->deleteAllForContext($context); | ||||||
| $this->contextNodeRelMapper->deleteAllByContextId($context->getId()); | ||||||
| $pageIds = $this->pageMapper->getPageIdsForContext($context->getId()); | ||||||
| $this->widgetService->deleteByPageIds($pageIds); | ||||||
| foreach ($pageIds as $pageId) { | ||||||
| $this->pageContentMapper->deleteByPageId($pageId); | ||||||
| $this->pageMapper->deleteByPageId($pageId); | ||||||
|
|
@@ -359,6 +363,7 @@ public function deleteNodeRel(int $nodeId, int $nodeType): void { | |||||
| try { | ||||||
| $nodeRelIds = $this->contextNodeRelMapper->getRelIdsForNode($nodeId, $nodeType); | ||||||
| $this->atomic(function () use ($nodeRelIds): void { | ||||||
| $this->widgetService->deleteSourcesByNodeRelIds($nodeRelIds); | ||||||
| $this->pageContentMapper->deleteByNodeRelIds($nodeRelIds); | ||||||
| $this->contextNodeRelMapper->deleteByNodeRelIds($nodeRelIds); | ||||||
| }, $this->dbc); | ||||||
|
|
@@ -454,9 +459,12 @@ public function addNodeToContextAndStartpage(int $contextId, int $nodeId, int $n | |||||
| * @throws Exception | ||||||
| */ | ||||||
| public function removeNodeFromContextAndPages(int $nodeRelationId): array { | ||||||
| $nodeRelation = $this->removeNodeFromContextById($nodeRelationId); | ||||||
| $contents = $this->removeNodeRelFromAllPages($nodeRelation); | ||||||
| return [$nodeRelation, $contents]; | ||||||
| return $this->atomic(function () use ($nodeRelationId): array { | ||||||
| $this->widgetService->deleteSourcesByNodeRelIds([$nodeRelationId]); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Reproduced on the dev instance: an application with one table on its start page and a data widget showing it, then Checking the added nodes before the removal loop, or running all of |
||||||
| $nodeRelation = $this->removeNodeFromContextById($nodeRelationId); | ||||||
| $contents = $this->removeNodeRelFromAllPages($nodeRelation); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new transaction takes its locks in the opposite order to
Deleting the contents with |
||||||
| return [$nodeRelation, $contents]; | ||||||
| }, $this->dbc); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
|
|
@@ -666,15 +674,26 @@ public function importContext(Table $table, array $context, int $tableOldId): ?C | |||||
|
|
||||||
| // insert nodes | ||||||
| $nodeRelMap = []; | ||||||
| // the relations of the table being imported, the only node a widget source can keep | ||||||
| $sourceRelMap = []; | ||||||
|
Comment on lines
+677
to
+678
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We seem to have a bug: If I have an application with 2 tables and export an account. When i import, the application is created twice 🤔
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for finding this. It's older than this PR: the migrator called |
||||||
| if (!empty($context['nodes'])) { | ||||||
| foreach ($context['nodes'] as $nodeArr) { | ||||||
| $nodeType = $nodeArr['node_type'] ?? $nodeArr['nodeType'] ?? 0; | ||||||
| $contextNodeRel = new ContextNodeRelation(); | ||||||
| $contextNodeRel->setContextId($newContext->getId()); | ||||||
| $contextNodeRel->setNodeId($table->getId()); | ||||||
| $contextNodeRel->setNodeType($nodeArr['node_type'] ?? $nodeArr['nodeType'] ?? 0); | ||||||
| $contextNodeRel->setNodeType($nodeType); | ||||||
| $contextNodeRel->setPermissions($nodeArr['permissions'] ?? 0); | ||||||
| $contextNodeRel = $this->contextNodeRelMapper->insert($contextNodeRel); | ||||||
| $nodeRelMap[$nodeArr['id'] ?? null] = $contextNodeRel; | ||||||
| $oldRelId = self::importedKey($nodeArr['id'] ?? null); | ||||||
| if ($oldRelId === null) { | ||||||
| continue; | ||||||
| } | ||||||
| $nodeRelMap[$oldRelId] = $contextNodeRel; | ||||||
| $oldNodeId = $nodeArr['node_id'] ?? $nodeArr['nodeId'] ?? null; | ||||||
| if (is_numeric($nodeType) && (int)$nodeType === Application::NODE_TYPE_TABLE && is_numeric($oldNodeId) && (int)$oldNodeId === $tableOldId) { | ||||||
| $sourceRelMap[$oldRelId] = $contextNodeRel; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -687,6 +706,9 @@ public function importContext(Table $table, array $context, int $tableOldId): ?C | |||||
| $page = new Page(); | ||||||
| $page->setContextId($newContext->getId()); | ||||||
| $page->setPageType($pageArr['page_type'] ?? $pageArr['pageType'] ?? ''); | ||||||
| $page->setTitle($this->importedText($pageArr['title'] ?? null, Page::TITLE_MAX_LENGTH, 'page title')); | ||||||
| $page->setIcon($this->importedIcon($pageArr['icon'] ?? null)); | ||||||
| $page->setDescription($this->importedText($pageArr['description'] ?? null, Page::DESCRIPTION_MAX_LENGTH, 'page description')); | ||||||
| if ($technicalNames[$pageKey] === null) { | ||||||
| $page = $this->pageMapper->insertWithDefaultName($page); | ||||||
| } else { | ||||||
|
|
@@ -698,15 +720,26 @@ public function importContext(Table $table, array $context, int $tableOldId): ?C | |||||
| // insert page conten | ||||||
| if (!empty($pageArr['content'])) { | ||||||
| foreach ($pageArr['content'] as $contentArr) { | ||||||
| $oldNodeRelId = self::importedKey($contentArr['node_rel_id'] ?? $contentArr['nodeRelId'] ?? null); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Reproduced on the dev instance (PHP 8.5):
|
||||||
| $newNodeRel = $oldNodeRelId !== null ? ($nodeRelMap[$oldNodeRelId] ?? null) : null; | ||||||
| if ($newNodeRel === null) { | ||||||
| continue; | ||||||
| } | ||||||
| $pageContent = new PageContent(); | ||||||
| $pageContent->setPageId($page->getId()); | ||||||
| $oldNodeRelId = $contentArr['node_rel_id'] ?? $contentArr['nodeRelId'] ?? null; | ||||||
| $newNodeRel = $nodeRelMap[$oldNodeRelId] ?? null; | ||||||
| $pageContent->setNodeRelId($newNodeRel ? $newNodeRel->getId() : null); | ||||||
| $pageContent->setNodeRelId($newNodeRel->getId()); | ||||||
| $pageContent->setOrder($contentArr['order'] ?? 100); | ||||||
| $this->pageContentMapper->insert($pageContent); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| $widgets = is_array($pageArr['widgets'] ?? null) ? array_values($pageArr['widgets']) : []; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Widgets are imported for every importable page, the start page included, while Reproduced on the dev instance by calling
Suggested change
|
||||||
| if (count($widgets) > GridWidgetTypes::MAX_WIDGETS_PER_PAGE) { | ||||||
| $this->logger->warning('Skipping ' . (count($widgets) - GridWidgetTypes::MAX_WIDGETS_PER_PAGE) . ' imported widgets: a page holds at most ' . GridWidgetTypes::MAX_WIDGETS_PER_PAGE . '.'); | ||||||
| } | ||||||
| foreach (array_slice($widgets, 0, GridWidgetTypes::MAX_WIDGETS_PER_PAGE) as $index => $widgetArr) { | ||||||
| $this->importWidget($page->getId(), $index, $widgetArr, $sourceRelMap); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -743,6 +776,91 @@ private function importedPageNames(array $pages): array { | |||||
| return $names; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * The pages of an application with the widgets of each page under `widgets`. | ||||||
| * | ||||||
| * @throws Exception | ||||||
| */ | ||||||
| public function getPagesWithWidgets(Context $context): array { | ||||||
| $pages = $context->getPages() ?? []; | ||||||
| $widgets = $this->widgetService->findByPageIds(array_keys($pages)); | ||||||
| foreach ($pages as $pageId => $page) { | ||||||
| $pages[$pageId]['widgets'] = $widgets[$pageId] ?? []; | ||||||
| } | ||||||
| return $pages; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Inserts an exported widget on a page. Its source is remapped to the imported node | ||||||
| * relation of the table being imported, and dropped when it showed another node. A | ||||||
| * widget that would not be saved through the API, for its type, settings, title, | ||||||
| * description, place on the grid or the shape of its source, is skipped. | ||||||
| * | ||||||
| * @param array<array-key, ContextNodeRelation> $nodeRelMap imported node relations of the table by their exported id | ||||||
| * @throws Exception | ||||||
| * @throws \JsonException | ||||||
| */ | ||||||
| private function importWidget(int $pageId, int $index, mixed $widgetArr, array $nodeRelMap): void { | ||||||
| if (!is_array($widgetArr) || !is_string($widgetArr['type'] ?? null)) { | ||||||
| return; | ||||||
| } | ||||||
| try { | ||||||
| $hasSource = $this->gridWidgetTypes->get($widgetArr['type'])['hasSource']; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The per-widget rules are assembled by each caller: here five AGENTS.md asks for structured input to be parsed into a value object at the boundary and for service methods to take value objects instead of raw arrays. A Smaller, in the skip handling here: a widget that isn't an object or has no type is dropped without a log line (line 804), while every other skip is logged with |
||||||
| $settings = $this->gridWidgetTypes->sanitizeSettings($widgetArr['type'], $widgetArr['settings'] ?? null); | ||||||
| $title = $this->gridWidgetTypes->validateTitle($index, $widgetArr['title'] ?? null); | ||||||
| $description = $this->gridWidgetTypes->validateDescription($index, $widgetArr['description'] ?? null); | ||||||
| $position = $this->gridWidgetTypes->validatePosition($index, $widgetArr); | ||||||
| $source = $widgetArr['source'] ?? null; | ||||||
| $oldNodeRelId = is_array($source) ? ($source['nodeRelId'] ?? null) : null; | ||||||
| if (($source !== null && !is_array($source)) || ($oldNodeRelId !== null && !is_int($oldNodeRelId))) { | ||||||
| throw new BadRequestError('The source of widget ' . $index . ' must be an object with a numeric nodeRelId.'); | ||||||
| } | ||||||
| } catch (BadRequestError $e) { | ||||||
| $this->logger->warning('Skipping an imported widget: ' . $e->getMessage(), ['exception' => $e]); | ||||||
| return; | ||||||
| } | ||||||
| $newNodeRel = $hasSource && $oldNodeRelId !== null ? ($nodeRelMap[$oldNodeRelId] ?? null) : null; | ||||||
| $this->widgetService->insert( | ||||||
| $pageId, | ||||||
| $widgetArr['type'], | ||||||
| $title, | ||||||
| $position, | ||||||
| $settings, | ||||||
| $newNodeRel?->getId(), | ||||||
| $this->importedIcon($widgetArr['icon'] ?? null), | ||||||
| $description, | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * An exported id usable as an array key, or null for a value of another type. | ||||||
| */ | ||||||
| private static function importedKey(mixed $id): int|string|null { | ||||||
| return is_int($id) || is_string($id) ? $id : null; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * An exported text, trimmed, or null when it is empty, not a text or too long. | ||||||
| */ | ||||||
| private function importedText(mixed $text, int $maxLength, string $what): ?string { | ||||||
| if ($text === null) { | ||||||
| return null; | ||||||
| } | ||||||
| if (!is_string($text) || mb_strlen(trim($text)) > $maxLength) { | ||||||
| $this->logger->warning('Dropping an imported ' . $what . ': it is not a text of at most ' . $maxLength . ' characters.'); | ||||||
| return null; | ||||||
| } | ||||||
| $text = trim($text); | ||||||
| return $text === '' ? null : $text; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * An exported icon name, or null when this instance has no such icon. | ||||||
| */ | ||||||
| private function importedIcon(mixed $icon): ?string { | ||||||
| return is_string($icon) && IconHelper::isValid($icon) ? $icon : null; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * @param int $contextId | ||||||
| * | ||||||
|
|
@@ -792,7 +910,7 @@ public function getScheme(int $contextId, string $userId): ContextScheme { | |||||
| $context->getIcon(), | ||||||
| $context->getDescription(), | ||||||
| array_values($nodes), | ||||||
| $context->getPages(), | ||||||
| $this->getPagesWithWidgets($context), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The scheme export now includes pages and widgets, but the scheme import ignores them though. Why?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was a gap: I added the export side and left the import for later. #3201 imports the pages and widgets of a scheme, with a Pages switch on the import screen next to the Menu one. |
||||||
| array_values($tables), | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we join only the start page here, and load the page fields in a separate query? Otherwise, imagine someone opens just a start page, we might be wasting time processing stuff that doesn't matter for them? Maybe i misunderstand
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You understood it right: every grid page multiplied the joined rows by the number of tables and shares. The base query now joins only the start page, as before this PR.
findAll()andfindById()load the other pages in one extra query for all applications in the result.findForNavBar()andfindAllContainingNode()skip it, because the navigation shows names and the permission checks only read the nodes.