Skip to content

Commit ded3ca0

Browse files
authored
Merge pull request #2786 from nextcloud/feat/federated-sharing
feat(federation): Federated table sharing
2 parents 6e98984 + 3693248 commit ded3ca0

48 files changed

Lines changed: 2772 additions & 468 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

appinfo/routes.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,6 @@
161161
['name' => 'Context#transfer', 'url' => '/api/2/contexts/{contextId}/transfer', 'verb' => 'PUT'],
162162
['name' => 'Context#updateContentOrder', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'PUT'],
163163

164-
['name' => 'RowOCS#createRow', 'url' => '/api/2/{nodeCollection}/{nodeId}/rows', 'verb' => 'POST', 'requirements' => ['nodeCollection' => '(tables|views)', 'nodeId' => '(\d+)']],
165-
166164
['name' => 'Config#getTableConfig', 'url' => '/api/2/config/table/{id}', 'verb' => 'GET'],
167165
['name' => 'Config#getViewConfig', 'url' => '/api/2/config/view/{id}', 'verb' => 'GET'],
168166
['name' => 'Config#setValue', 'url' => '/api/2/config/{key}', 'verb' => 'POST'],

cypress.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import vitePreprocessor from 'cypress-vite'
77
import { nodePolyfills } from 'vite-plugin-node-polyfills'
88
import vue from '@vitejs/plugin-vue'
99

10-
const baseUrl = process.env.CYPRESS_baseUrl ?? 'http://127.0.0.1:8089/index.php/'
10+
const baseUrl = process.env.CYPRESS_baseUrl ?? 'https://nextcloud.local/index.php/'
1111

1212
export default defineConfig({
1313
allowCypressEnv: false,

cypress/component/ContentReferenceWidget.cy.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ describe('ContentReferenceWidget', () => {
8181
// Load a fixture which is used to reply to the edit row request
8282
cy.fixture('widgets/editRow.json')
8383
.then((rowData) => {
84-
cy.reply('**/index.php/apps/tables/row/*', rowData)
84+
cy.reply('**/ocs/v2.php/apps/tables/api/2/**/rows/*', rowData)
8585

8686
const updatedRows = richObject.rows.map(row => row.id === rowData.id ? rowData : row)
87-
cy.reply('**/apps/tables/row/table/*', updatedRows)
87+
cy.intercept('GET', '**/apps/tables/row/table/*', (req) => {
88+
req.reply({ delay: 1000, body: updatedRows })
89+
})
8890
})
8991

9092
// Open the row action menu on the first row, then click Edit

cypress/e2e/view-filtering-selection-row-removal.cy.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('Filtering view with row removal', () => {
2121
})
2222

2323
it('Removes rows from the filtered view once they no longer match', () => {
24-
cy.intercept({ method: 'PUT', url: '**/apps/tables/row/*' }).as('updateCheckedRow')
24+
cy.intercept({ method: 'PUT', url: '**/ocs/v2.php/apps/tables/api/2/**' }).as('updateCheckedRow')
2525
cy.contains('[data-cy="ncTable"] [data-cy="customTableRow"]', 'first row').closest('[data-cy="customTableRow"]').find('[data-cy="rowActionMenu"] button').click()
2626
cy.get('[data-cy="editRowBtn"]').click()
2727
cy.get('[data-cy="editRowModal"] .checkbox-radio-switch').click()
@@ -31,7 +31,7 @@ describe('Filtering view with row removal', () => {
3131
cy.contains('[data-cy="ncTable"] [data-cy="customTableRow"]', 'first row', { timeout: 8000 }).should('not.exist')
3232
cy.get('[data-cy="editRowModal"]').should('not.exist')
3333

34-
cy.intercept({ method: 'PUT', url: '**/apps/tables/row/*' }).as('inlineUpdateRow')
34+
cy.intercept({ method: 'PUT', url: '**/ocs/v2.php/apps/tables/api/2/**' }).as('inlineUpdateRow')
3535
cy.contains('[data-cy="ncTable"] [data-cy="customTableRow"]', 'second row').closest('[data-cy="customTableRow"]').find('.inline-editing-container input').click({ force: true })
3636
cy.wait('@inlineUpdateRow')
3737
// Wait for the row to be removed from the filtered view (async removal)
Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,55 @@
11
{
2-
"id": 226,
3-
"tableId": 49,
4-
"createdBy": null,
5-
"createdAt": null,
6-
"lastEditBy": null,
7-
"lastEditAt": null,
8-
"data": [
9-
{
10-
"columnId": 159,
11-
"value": "Giraffe"
12-
},
13-
{
14-
"columnId": 160,
15-
"value": "Mr. Smith"
16-
},
17-
{
18-
"columnId": 161,
19-
"value": "Dog food every week"
20-
},
21-
{
22-
"columnId": 162,
23-
"value": "2023-01-01"
24-
},
25-
{
26-
"columnId": 163,
27-
"value": "2023-12-31"
28-
},
29-
{
30-
"columnId": 164,
31-
"value": "The dog is our best friend."
32-
},
33-
{
34-
"columnId": 165,
35-
"value": "Standard, SLA Level 2"
36-
},
37-
{
38-
"columnId": 166,
39-
"value": 80
40-
},
41-
{
42-
"columnId": 167,
43-
"value": "Likes treats"
2+
"ocs": {
3+
"meta": {
4+
"status": "ok",
5+
"statuscode": 200,
6+
"message": "OK"
7+
},
8+
"data": {
9+
"id": 226,
10+
"tableId": 49,
11+
"createdBy": null,
12+
"createdAt": null,
13+
"lastEditBy": null,
14+
"lastEditAt": null,
15+
"data": [
16+
{
17+
"columnId": 159,
18+
"value": "Giraffe"
19+
},
20+
{
21+
"columnId": 160,
22+
"value": "Mr. Smith"
23+
},
24+
{
25+
"columnId": 161,
26+
"value": "Dog food every week"
27+
},
28+
{
29+
"columnId": 162,
30+
"value": "2023-01-01"
31+
},
32+
{
33+
"columnId": 163,
34+
"value": "2023-12-31"
35+
},
36+
{
37+
"columnId": 164,
38+
"value": "The dog is our best friend."
39+
},
40+
{
41+
"columnId": 165,
42+
"value": "Standard, SLA Level 2"
43+
},
44+
{
45+
"columnId": 166,
46+
"value": 80
47+
},
48+
{
49+
"columnId": 167,
50+
"value": "Likes treats"
51+
}
52+
]
4453
}
45-
]
54+
}
4655
}

lib/AppInfo/Application.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,22 @@
88
namespace OCA\Tables\AppInfo;
99

1010
use Exception;
11+
use OC\OCM\OCMSignatoryManager;
1112
use OCA\Analytics\Datasource\DatasourceEvent;
1213
use OCA\Circles\Events\CircleDestroyedEvent;
1314
use OCA\Tables\Capabilities;
15+
use OCA\Tables\Config\ConfigLexicon;
1416
use OCA\Tables\Event\RowDeletedEvent;
1517
use OCA\Tables\Event\TableDeletedEvent;
1618
use OCA\Tables\Event\TableOwnershipTransferredEvent;
1719
use OCA\Tables\Event\ViewDeletedEvent;
20+
use OCA\Tables\Federation\FederationProvider;
1821
use OCA\Tables\Listener\AddMissingIndicesListener;
1922
use OCA\Tables\Listener\AnalyticsDatasourceListener;
2023
use OCA\Tables\Listener\LoadAdditionalEntriesListener;
2124
use OCA\Tables\Listener\LoadAdditionalListener;
2225
use OCA\Tables\Listener\ReceiverCleanupListener;
26+
use OCA\Tables\Listener\ResourceTypeRegisterListener;
2327
use OCA\Tables\Listener\TablesReferenceListener;
2428
use OCA\Tables\Listener\UserDeletedListener;
2529
use OCA\Tables\Listener\WhenRowDeletedAuditLogListener;
@@ -43,8 +47,13 @@
4347
use OCP\Collaboration\Reference\RenderReferenceEvent;
4448
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
4549
use OCP\DB\Events\AddMissingIndicesEvent;
50+
use OCP\Federation\ICloudFederationProvider;
51+
use OCP\Federation\ICloudFederationProviderManager;
4652
use OCP\Group\Events\GroupDeletedEvent;
4753
use OCP\Navigation\Events\LoadAdditionalEntriesEvent;
54+
use OCP\OCM\Events\LocalOCMDiscoveryEvent;
55+
use OCP\Security\Signature\ISignatoryManager;
56+
use OCP\Server;
4857
use OCP\Share\ShareReview\RegisterShareReviewSourceEvent;
4958
use OCP\User\Events\BeforeUserDeletedEvent;
5059
use OCP\User\Events\UserDeletedEvent;
@@ -83,6 +92,7 @@ public function register(IRegistrationContext $context): void {
8392
}
8493

8594
$context->registerService(AuditLogServiceInterface::class, fn (ContainerInterface $c) => $c->get(DefaultAuditLogService::class));
95+
$context->registerService(ISignatoryManager::class, fn (ContainerInterface $c) => $c->get(OCMSignatoryManager::class));
8696

8797
$context->registerEventListener(BeforeUserDeletedEvent::class, UserDeletedListener::class);
8898
$context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class);
@@ -98,6 +108,7 @@ public function register(IRegistrationContext $context): void {
98108
$context->registerEventListener(UserDeletedEvent::class, ReceiverCleanupListener::class);
99109
$context->registerEventListener(GroupDeletedEvent::class, ReceiverCleanupListener::class);
100110
$context->registerEventListener(CircleDestroyedEvent::class, ReceiverCleanupListener::class);
111+
$context->registerEventListener(LocalOCMDiscoveryEvent::class, ResourceTypeRegisterListener::class);
101112

102113
$context->registerSearchProvider(SearchTablesProvider::class);
103114

@@ -112,8 +123,19 @@ public function register(IRegistrationContext $context): void {
112123
$context->registerMiddleware(ShareControlMiddleware::class);
113124

114125
$context->registerUserMigrator(TablesMigrator::class);
126+
127+
$context->registerConfigLexicon(ConfigLexicon::class);
115128
}
116129

117130
public function boot(IBootContext $context): void {
131+
$context->injectFn([$this, 'registerCloudFederationProviderManager']);
132+
}
133+
134+
public function registerCloudFederationProviderManager(ICloudFederationProviderManager $manager): void {
135+
$manager->addCloudFederationProvider(
136+
FederationProvider::PROVIDER_ID,
137+
'Tables Federation',
138+
static fn (): ICloudFederationProvider => Server::get(FederationProvider::class),
139+
);
118140
}
119141
}

lib/Config/ConfigLexicon.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Tables\Config;
11+
12+
use OCP\Config\Lexicon\Entry;
13+
use OCP\Config\Lexicon\ILexicon;
14+
use OCP\Config\Lexicon\Strictness;
15+
use OCP\Config\ValueType;
16+
17+
/**
18+
* Config Lexicon for tables.
19+
*
20+
* Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
21+
*
22+
* {@see ILexicon}
23+
*/
24+
class ConfigLexicon implements ILexicon {
25+
public const FEDERATION_ENABLED = 'federationEnabled';
26+
27+
#[\Override]
28+
public function getStrictness(): Strictness {
29+
return Strictness::IGNORE;
30+
}
31+
32+
#[\Override]
33+
public function getAppConfigs(): array {
34+
return [
35+
new Entry(self::FEDERATION_ENABLED, ValueType::BOOL, true, 'Enable or disable federated table sharing'),
36+
];
37+
}
38+
39+
#[\Override]
40+
public function getUserConfigs(): array {
41+
return [];
42+
}
43+
}

lib/Constants/ShareReceiverType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ class ShareReceiverType {
1515
public const GROUP = 'group';
1616
public const CIRCLE = 'circle';
1717
public const LINK = 'link';
18+
public const REMOTE = 'remote';
1819
}

lib/Controller/Api1Controller.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use OCA\Tables\Model\ViewUpdateInput;
2525
use OCA\Tables\ResponseDefinitions;
2626
use OCA\Tables\Service\ColumnService;
27+
use OCA\Tables\Service\FederationService;
2728
use OCA\Tables\Service\ImportService;
2829
use OCA\Tables\Service\RelationService;
2930
use OCA\Tables\Service\RowService;
@@ -88,6 +89,7 @@ public function __construct(
8889
LoggerInterface $logger,
8990
IL10N $l10N,
9091
?string $userId,
92+
private FederationService $federationService,
9193
) {
9294
parent::__construct(Application::APP_ID, $request);
9395
$this->tableService = $service;
@@ -781,6 +783,11 @@ public function updateShareDisplayMode(int $shareId, int $displayMode, string $t
781783
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
782784
public function indexTableColumns(int $tableId, ?int $viewId): DataResponse {
783785
try {
786+
if ($this->federationService->isNodeFederated($tableId, 'table')) {
787+
$table = $this->tableService->find($tableId, true);
788+
return new DataResponse($this->federationService->getColumns($table));
789+
}
790+
784791
if ($viewId) {
785792
$view = $this->viewService->find($viewId, false, $this->userId);
786793
if ($tableId !== $view->getTableId()) {
@@ -824,6 +831,11 @@ public function indexTableColumns(int $tableId, ?int $viewId): DataResponse {
824831
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
825832
public function indexViewColumns(int $viewId): DataResponse {
826833
try {
834+
if ($this->federationService->isNodeFederated($viewId, 'view')) {
835+
$view = $this->viewService->find($viewId, true);
836+
return new DataResponse($this->federationService->getColumns($view));
837+
}
838+
827839
return new DataResponse($this->columnService->formatColumns($this->columnService->findAllByView($viewId)));
828840
} catch (PermissionError $e) {
829841
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);

lib/Controller/RowController.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
use OCA\Tables\AppInfo\Application;
1111
use OCA\Tables\Middleware\Attribute\RequirePermission;
12+
use OCA\Tables\Service\FederationService;
1213
use OCA\Tables\Service\RowService;
14+
use OCA\Tables\Service\TableService;
15+
use OCA\Tables\Service\ViewService;
1316
use OCP\AppFramework\Controller;
1417
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
1518
use OCP\AppFramework\Http\DataResponse;
@@ -24,6 +27,9 @@ public function __construct(
2427
protected LoggerInterface $logger,
2528
private RowService $service,
2629
private ?string $userId,
30+
private TableService $tableService,
31+
private ViewService $viewService,
32+
private FederationService $federationService,
2733
) {
2834
parent::__construct(Application::APP_ID, $request);
2935
}
@@ -32,6 +38,10 @@ public function __construct(
3238
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')]
3339
public function index(int $tableId): DataResponse {
3440
return $this->handleError(function () use ($tableId) {
41+
if ($this->federationService->isNodeFederated($tableId, 'table')) {
42+
$table = $this->tableService->find($tableId, true);
43+
return $this->federationService->getRows($table);
44+
}
3545
return $this->service->findAllByTable($tableId, $this->userId);
3646
});
3747
}
@@ -40,6 +50,10 @@ public function index(int $tableId): DataResponse {
4050
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')]
4151
public function indexView(int $viewId): DataResponse {
4252
return $this->handleError(function () use ($viewId) {
53+
if ($this->federationService->isNodeFederated($viewId, 'view')) {
54+
$view = $this->viewService->find($viewId, false, $this->userId);
55+
return $this->federationService->getRows($view);
56+
}
4357
return $this->service->findAllByView($viewId, $this->userId);
4458
});
4559
}
@@ -99,6 +113,9 @@ public function destroyByView(int $id, int $viewId): DataResponse {
99113
#[NoAdminRequired]
100114
public function presentInView(int $id, int $viewId): DataResponse {
101115
return $this->handleError(function () use ($id, $viewId) {
116+
if ($this->federationService->isNodeFederated($viewId, 'view')) {
117+
return ['present' => true];
118+
}
102119
$present = $this->service->isRowInViewPresent($id, $viewId, $this->userId);
103120
return ['present' => $present];
104121
});

0 commit comments

Comments
 (0)