Skip to content
Merged
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
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
Those groups of people can then be used by any other app for sharing purpose.
]]>
</description>
<version>36.0.0-dev.0</version>
<version>36.0.0-dev.1</version>
<licence>AGPL-3.0-or-later</licence>
<author>Maxence Lange</author>
<types>
Expand Down
3 changes: 3 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
['name' => 'TeamFolder#getLinkableTeamFolders', 'url' => '/teams/{circleId}/folder/linkable', 'verb' => 'GET'],
['name' => 'TeamFolder#linkTeamFolder', 'url' => '/teams/{circleId}/folder/link', 'verb' => 'POST'],
['name' => 'TeamFolder#updateTeamFolderQuota', 'url' => '/teams/{circleId}/folder/quota', 'verb' => 'PUT'],
['name' => 'TeamFavorites#getFavorites', 'url' => '/teams/favorites', 'verb' => 'GET'],
['name' => 'TeamFavorites#setFavorite', 'url' => '/teams/{circleId}/favorite', 'verb' => 'PUT'],
['name' => 'TeamFavorites#reorder', 'url' => '/teams/favorites/order', 'verb' => 'PUT'],

// Teams Dashboard widget endpoint
['name' => 'TeamsDashboard#getCompleteTeamsData', 'url' => '/teams/dashboard/widget', 'verb' => 'GET'],
Expand Down
55 changes: 55 additions & 0 deletions lib/Controller/TeamFavoritesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Controller;

use OCA\Circles\Service\TeamFavoriteService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;

class TeamFavoritesController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private readonly TeamFavoriteService $teamFavoriteService,
) {
parent::__construct($appName, $request);
}

#[NoAdminRequired]
public function getFavorites(): DataResponse {
return new DataResponse([
'circleIds' => $this->teamFavoriteService->getFavoriteCircleIds(),
]);
}

#[NoAdminRequired]
public function setFavorite(string $circleId, bool $isFavorite): DataResponse {
$circleIds = $this->teamFavoriteService->setFavorite($circleId, $isFavorite);

return new DataResponse([
'circleId' => $circleId,
'isFavorite' => $isFavorite,
'circleIds' => $circleIds,
]);
}

/**
* @param list<string> $circleIds
* @param list<string> $expectedCircleIds
*/
#[NoAdminRequired]
public function reorder(array $circleIds, array $expectedCircleIds): DataResponse {
return new DataResponse([
'circleIds' => $this->teamFavoriteService->reorder($circleIds, $expectedCircleIds),
]);
}
}
12 changes: 12 additions & 0 deletions lib/Db/CircleRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\Probes\DataProbe;
use OCA\Circles\Repository\TeamFavoriteRepository;
use OCA\Circles\Service\ConfigService;
use OCA\Circles\Service\TimezoneService;
use OCP\DB\QueryBuilder\IQueryBuilder;

/**
Expand All @@ -29,6 +32,14 @@
* @package OCA\Circles\Db
*/
class CircleRequest extends CircleRequestBuilder {
public function __construct(
private readonly TeamFavoriteRepository $teamFavoriteRepository,
TimezoneService $timezoneService,
ConfigService $configService,
) {
parent::__construct($timezoneService, $configService);
}

/**
* @param Circle $circle
*
Expand Down Expand Up @@ -523,6 +534,7 @@ public function delete(Circle $circle): void {
$qb->limitToUniqueId($circle->getSingleId());

$qb->executeStatement();
$this->teamFavoriteRepository->removeCircle($circle->getSingleId());
}

/**
Expand Down
40 changes: 40 additions & 0 deletions lib/Entity/TeamFavorite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Entity;

use DateTime;
use DateTimeZone;
use OCP\AppFramework\ORM\Attribute\Column;
use OCP\AppFramework\ORM\Attribute\Entity as EntityAttribute;
use OCP\AppFramework\ORM\Attribute\Id;
use OCP\DB\Schema\ColumnType;

#[EntityAttribute(name: 'circles_team_favorites')]
final class TeamFavorite {
#[Id]
#[Column(name: 'id', type: ColumnType::Integer)]
public ?int $id = null;
Comment thread
artonge marked this conversation as resolved.

#[Column(name: 'user_id', type: ColumnType::String, length: 255)]
public string $userId = '';

#[Column(name: 'circle_id', type: ColumnType::String, length: 32)]
public string $circleId = '';

#[Column(name: 'position', type: ColumnType::Integer)]
public int $position = 0;

#[Column(name: 'created', type: ColumnType::Datetime)]
public DateTime $created;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this column?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we can replace the incremental id with a snowflake id which will contain the creation date.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you’re right. The creation timestamp is not used anywhere, so I’ll remove this column in a follow-up PR.

A snowflake ID would make sense if we needed the creation timestamp. Since the feature does not use that information, I think keeping the generated integer ID and removing created is the simpler option.


public function __construct() {
$this->created = new DateTime('now', new DateTimeZone('UTC'));
}
}
3 changes: 3 additions & 0 deletions lib/Listeners/UserDeleted.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OCA\Circles\Listeners;

use Exception;
use OCA\Circles\Repository\TeamFavoriteRepository;
use OCA\Circles\Service\SyncService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
Expand All @@ -24,6 +25,7 @@ class UserDeleted implements IEventListener {
*/
public function __construct(
private readonly SyncService $syncService,
private readonly TeamFavoriteRepository $teamFavoriteRepository,
) {
}

Expand All @@ -36,6 +38,7 @@ public function handle(Event $event): void {
}

$user = $event->getUser();
$this->teamFavoriteRepository->removeUser($user->getUID());

try {
$this->syncService->userDeleted($user->getUID());
Expand Down
63 changes: 63 additions & 0 deletions lib/Migration/Version20260922120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: naming convention for migrations is Version{appVersion}Date{timestamp}. Not a big deal for this migration since it creates a new table, but might be a problem if a future migration alters the table. Not blocking though :)


declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Schema\ColumnType;
use OCP\DB\Schema\SchemaException;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version20260922120000 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The closure returns a schema wrapper.
* @param array $options
*
* @throws SchemaException
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if (!$schema->hasTable('circles_team_favorites')) {
$table = $schema->createTable('circles_team_favorites');
$table->addColumn('id', ColumnType::Integer, [
'autoincrement' => true,
'notnull' => true,
'length' => 8,
'unsigned' => true,
]);
$table->addColumn('user_id', ColumnType::String, [
'length' => 255,
'notnull' => true,
]);
$table->addColumn('circle_id', ColumnType::String, [
'length' => 32,
'notnull' => true,
]);
$table->addColumn('position', ColumnType::Integer, [
'notnull' => true,
'unsigned' => true,
]);
$table->addColumn('created', ColumnType::Datetime, [
'notnull' => true,
]);

$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['user_id', 'circle_id']);
$table->addIndex(['user_id', 'position']);
$table->addIndex(['circle_id']);
}

return $schema;
}
}
107 changes: 107 additions & 0 deletions lib/Repository/TeamFavoriteRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Repository;

use OCA\Circles\Entity\TeamFavorite;
use OCP\AppFramework\ORM\Repository;

/**
* @extends Repository<TeamFavorite>
*/
class TeamFavoriteRepository extends Repository {
public const string entityClass = TeamFavorite::class;

/**
* @return list<string>
*/
public function getCircleIds(string $userId, bool $accessibleOnly = false): array {
$qb = $this->connection->getQueryBuilder();
$qb->selectDistinct(['favorite.circle_id', 'favorite.position'])
->from($this->getTableName(), 'favorite')
->where($qb->expr()->eq('favorite.user_id', $qb->createNamedParameter($userId)));

if ($accessibleOnly) {
$expr = $qb->expr();
$qb->innerJoin('favorite', 'circles_member', 'user_member',
$expr->eq('user_member.user_id', 'favorite.user_id'));
$qb->leftJoin('user_member', 'circles_membership', 'membership', $expr->andX(
$expr->eq('membership.single_id', 'user_member.single_id'),
$expr->eq('membership.circle_id', 'favorite.circle_id'),
));
$qb->leftJoin('membership', 'circles_member', 'inherited_member', $expr->andX(
$expr->eq('inherited_member.single_id', 'membership.inheritance_first'),
$expr->eq('inherited_member.circle_id', 'favorite.circle_id'),
));
$qb->andWhere($expr->orX(
$expr->eq('user_member.circle_id', 'favorite.circle_id'),
$expr->isNotNull('inherited_member.single_id'),
));
}

$qb->orderBy('favorite.position', 'ASC')
->addOrderBy('favorite.circle_id', 'ASC');

$result = $qb->executeQuery();
$circleIds = [];
while ($row = $result->fetch()) {
$circleIds[] = $row['circle_id'];
}
$result->closeCursor();

return $circleIds;
}

public function add(string $userId, string $circleId): void {
$last = iterator_to_array($this->findBy(
['userId' => $userId],
['position' => \SortDirection::Descending],
1,
), false);

$favorite = new TeamFavorite();
$favorite->userId = $userId;
$favorite->circleId = $circleId;
$favorite->position = $last === [] ? 0 : $last[0]->position + 1;
$this->insert($favorite);
}

public function remove(string $userId, string $circleId): void {
$this->deleteBy(['userId' => $userId, 'circleId' => $circleId]);
}

/**
* @param list<string> $circleIds
*/
public function replaceOrder(string $userId, array $circleIds): void {
$positions = array_flip($circleIds);
$this->connection->beginTransaction();
try {
foreach ($this->findBy(['userId' => $userId]) as $favorite) {
if (!isset($positions[$favorite->circleId])) {
continue;
}
$favorite->position = $positions[$favorite->circleId];
$this->update($favorite);
}
$this->connection->commit();
} catch (\Throwable $exception) {
$this->connection->rollBack();
throw $exception;
}
}

public function removeCircle(string $circleId): void {
$this->deleteBy(['circleId' => $circleId]);
}

public function removeUser(string $userId): void {
$this->deleteBy(['userId' => $userId]);
}
}
Loading
Loading