From bf0d5740819ce8b1ccc6273b972a09203089cd52 Mon Sep 17 00:00:00 2001 From: Luka Trovic Date: Fri, 15 Aug 2025 10:32:07 +0200 Subject: [PATCH 1/3] feat: activity support Signed-off-by: Luka Trovic --- appinfo/info.xml | 11 + lib/Activity/ActivityManager.php | 244 ++++++++++++++++++ lib/Activity/ChangeSet.php | 48 ++++ lib/Activity/Filter.php | 76 ++++++ lib/Activity/SettingChanges.php | 94 +++++++ lib/Activity/TablesProvider.php | 164 ++++++++++++ lib/Helper/CircleHelper.php | 20 ++ lib/Helper/GroupHelper.php | 15 ++ lib/Service/RowService.php | 27 ++ lib/Service/ShareService.php | 31 +++ lib/Service/TableService.php | 24 ++ package-lock.json | 11 + package.json | 2 + src/modules/main/sections/MainWrapper.vue | 3 + src/modules/modals/EditRow.vue | 115 ++++++--- .../partials/NavigationTableItem.vue | 19 +- src/modules/sidebar/sections/Sidebar.vue | 20 +- .../sidebar/sections/SidebarActivity.vue | 27 ++ src/shared/components/ActivityEntry.vue | 145 +++++++++++ src/shared/components/ActivityList.vue | 123 +++++++++ .../components/ncTable/partials/TableRow.vue | 10 +- src/shared/mixins/activityMixin.js | 16 ++ src/shared/mixins/readableDate.js | 16 ++ src/shared/mixins/relativeDate.js | 19 ++ vite.config.ts | 15 -- 25 files changed, 1239 insertions(+), 56 deletions(-) create mode 100644 lib/Activity/ActivityManager.php create mode 100644 lib/Activity/ChangeSet.php create mode 100644 lib/Activity/Filter.php create mode 100644 lib/Activity/SettingChanges.php create mode 100644 lib/Activity/TablesProvider.php create mode 100644 src/modules/sidebar/sections/SidebarActivity.vue create mode 100644 src/shared/components/ActivityEntry.vue create mode 100644 src/shared/components/ActivityList.vue create mode 100644 src/shared/mixins/activityMixin.js create mode 100644 src/shared/mixins/readableDate.js create mode 100644 src/shared/mixins/relativeDate.js diff --git a/appinfo/info.xml b/appinfo/info.xml index d3b6046dc6..4fb240e06e 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -71,6 +71,17 @@ Have a good time and manage whatever you want. OCA\Tables\Command\CleanLegacy OCA\Tables\Command\TransferLegacyRows + + + OCA\Tables\Activity\SettingChanges + + + OCA\Tables\Activity\Filter + + + OCA\Tables\Activity\TablesProvider + + Tables diff --git a/lib/Activity/ActivityManager.php b/lib/Activity/ActivityManager.php new file mode 100644 index 0000000000..8a297b4c01 --- /dev/null +++ b/lib/Activity/ActivityManager.php @@ -0,0 +1,244 @@ +userId; + } + + try { + $event = $this->createEvent($objectType, $object, $subject, $additionalParams, $author); + + if ($event !== null) { + $this->sendToUsers($event, $object); + } + } catch (\Exception $e) { + // Ignore exception for undefined activities on update events + } + } + + public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject) { + $previousEntity = $changeSet->getBefore(); + $entity = $changeSet->getAfter(); + $events = []; + + if ($previousEntity !== null) { + foreach ($entity->getUpdatedFields() as $field => $value) { + $getter = 'get' . ucfirst($field); + $subjectComplete = $subject . '_' . $field; + $changes = [ + 'before' => $previousEntity->$getter(), + 'after' => $entity->$getter() + ]; + if ($changes['before'] !== $changes['after']) { + try { + $event = $this->createEvent($objectType, $entity, $subjectComplete, $changes); + if ($event !== null) { + $events[] = $event; + } + } catch (\Exception $e) { + // Ignore exception for undefined activities on update events + } + } + } + } else { + try { + $events = [$this->createEvent($objectType, $entity, $subject)]; + } catch (\Exception $e) { + // Ignore exception for undefined activities on update events + } + } + + foreach ($events as $event) { + $this->sendToUsers($event, $entity); + } + } + + private function createEvent($objectType, $object, $subject, $additionalParams = [], $author = null) { + $objectTitle = ''; + + if ($object instanceof Table) { + $objectTitle = $object->getTitle(); + $table = $object; + } elseif ($object instanceof Row2) { + $objectTitle = '#' . $object->getId(); + $table = $this->tableMapper->find($object->getTableId()); + } else { + Server::get(LoggerInterface::class)->error('Could not create activity entry for ' . $subject . '. Invalid object.', (array)$object); + return null; + } + + /** + * Automatically fetch related details for subject parameters + * depending on the subject + */ + $eventType = 'tables'; + $subjectParams = [ + 'author' => $author === null ? $this->userId : $author, + 'table' => $table + ]; + switch ($subject) { + // No need to enhance parameters since entity already contains the required data + case self::SUBJECT_TABLE_CREATE: + case self::SUBJECT_TABLE_UPDATE_TITLE: + case self::SUBJECT_TABLE_UPDATE_DESCRIPTION: + case self::SUBJECT_TABLE_DELETE: + break; + case self::SUBJECT_ROW_CREATE: + case self::SUBJECT_ROW_UPDATE: + case self::SUBJECT_ROW_DELETE: + $subjectParams['row'] = $object; + break; + default: + throw new \Exception('Unknown subject for activity.'); + break; + } + + if ($subject === self::SUBJECT_ROW_UPDATE) { + $subjectParams['changeCols'] = []; + foreach ($additionalParams['before'] as $index => $colData) { + if ($additionalParams['after'][$index] === $colData) { + continue; // No change, skip + } else { + try { + $column = $this->columnMapper->find($colData['columnId']); + $subjectParams['changeCols'][] = [ + 'id' => $column->getId(), + 'name' => $column->getTitle(), + 'before' => $colData, + 'after' => $additionalParams['after'][$index] + ]; + } catch (\Exception $e) { + Server::get(LoggerInterface::class)->error('Could not find column for activity entry.', [ + 'columnId' => $colData['columnId'], + 'exception' => $e->getMessage() + ]); + continue; // Skip if column not found + } + } + } + unset($additionalParams['before'], $additionalParams['after']); + } + + $event = $this->manager->generateEvent(); + $event->setApp('tables') + ->setType($eventType) + ->setAuthor($subjectParams['author']) + ->setObject($objectType, (int)$object->getId(), $objectTitle) + ->setSubject($subject, $subjectParams) + ->setTimestamp(time()); + + return $event; + } + + private function sendToUsers(IEvent $event, $object) { + if ($object instanceof Table) { + $tableId = $object->getId(); + $owner = $object->getOwnership(); + } elseif ($object instanceof Row2) { + $tableId = $object->getTableId(); + $owner = $this->tableMapper->find($tableId)->getOwnership(); + } else { + Server::get(LoggerInterface::class)->error('Could not send activity notify. Invalid object.', (array)$object); + return null; + } + + $event->setAffectedUser($owner); + $this->manager->publish($event); + + foreach ($this->shareService->findSharedWithUserIds($tableId, 'table') as $userId) { + $event->setAffectedUser($userId); + + /** @noinspection DisconnectedForeachInstructionInspection */ + $this->manager->publish($event); + } + } + + public function getActivityFormat($language, $subjectIdentifier, $subjectParams = [], $ownActivity = false) { + $subject = ''; + $l = $this->l10nFactory->get(Application::APP_ID, $language); + + switch ($subjectIdentifier) { + case self::SUBJECT_TABLE_CREATE: + $subject = $ownActivity ? $l->t('You have created a new table {table}'): $l->t('{user} has created a new table {table}'); + break; + case self::SUBJECT_TABLE_DELETE: + $subject = $ownActivity ? $l->t('You have deleted the table {table}') : $l->t('{user} has deleted the table {table}'); + break; + case self::SUBJECT_TABLE_UPDATE_TITLE: + $subject = $ownActivity ? $l->t('You have renamed the table {before} to {table}') : $l->t('{user} has renamed the table {before} to {table}'); + break; + case self::SUBJECT_TABLE_UPDATE_DESCRIPTION: + $subject = $ownActivity ? $l->t('You have updated the description of table {table} to {after}') : $l->t('{user} has updated the description of table {table} to {after}'); + break; + case self::SUBJECT_ROW_CREATE: + $subject = $ownActivity ? $l->t('You have created a new row {row} in table {table}') : $l->t('{user} has created a new row {row} in table {table}'); + break; + case self::SUBJECT_ROW_UPDATE: + $columns = ''; + foreach ($subjectParams['changeCols'] as $index => $changeCol) { + $columns .= '{col-' . $changeCol['id'] . '}'; + if ($index < count($subjectParams['changeCols']) - 1) { + $columns .= ', '; + } + } + $subject = $ownActivity ? $l->t('You have updated cell(s) ' . $columns . ' on row {row} in table {table}') : $l->t('{user} has updated cell(s) ' . $columns . ' on row {row} in table {table}'); + break; + case self::SUBJECT_ROW_DELETE: + $subject = $ownActivity ? $l->t('You have deleted the row {row} in table {table}') : $l->t('{user} has deleted the row {row} in table {table}'); + break; + default: + break; + } + + return $subject; + } +} diff --git a/lib/Activity/ChangeSet.php b/lib/Activity/ChangeSet.php new file mode 100644 index 0000000000..8f5ff4adc2 --- /dev/null +++ b/lib/Activity/ChangeSet.php @@ -0,0 +1,48 @@ +setBefore($before); + } + if ($after !== null) { + $this->setAfter($after); + } + } + + public function setBefore($before) { + $this->before = clone $before; + } + + public function setAfter($after) { + $this->after = clone $after; + } + + public function getBefore() { + return $this->before; + } + + public function getAfter() { + return $this->after; + } + + public function jsonSerialize(): array { + return [ + 'before' => $this->getBefore(), + 'after' => $this->getAfter() + ]; + } +} diff --git a/lib/Activity/Filter.php b/lib/Activity/Filter.php new file mode 100644 index 0000000000..366bae3cc9 --- /dev/null +++ b/lib/Activity/Filter.php @@ -0,0 +1,76 @@ +l10n = $l10n; + $this->urlGenerator = $urlGenerator; + } + + /** + * @return string Lowercase a-z and underscore only identifier + * @since 11.0.0 + */ + public function getIdentifier(): string { + return 'tables'; + } + + /** + * @return string A translated string + * @since 11.0.0 + */ + public function getName(): string { + return $this->l10n->t('Tables'); + } + + /** + * @return int whether the filter should be rather on the top or bottom of + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * @since 11.0.0 + */ + public function getPriority(): int { + return 90; + } + + /** + * @return string Full URL to an icon, empty string when none is given + * @since 11.0.0 + */ + public function getIcon(): string { + return $this->urlGenerator->imagePath('tables', 'app-dark.svg'); + } + + /** + * @param string[] $types + * @return string[] An array of allowed apps from which activities should be displayed + * @since 11.0.0 + */ + public function filterTypes(array $types): array { + return $types; + } + + /** + * @return string[] An array of allowed apps from which activities should be displayed + * @since 11.0.0 + */ + public function allowedApps(): array { + return ['tables']; + } +} diff --git a/lib/Activity/SettingChanges.php b/lib/Activity/SettingChanges.php new file mode 100644 index 0000000000..195cceac75 --- /dev/null +++ b/lib/Activity/SettingChanges.php @@ -0,0 +1,94 @@ +l = $l; + } + + public function getGroupIdentifier() { + return 'tables'; + } + + public function getGroupName() { + return $this->l->t('Tables'); + } + + /** + * @return string Lowercase a-z and underscore only identifier + * @since 20.0.0 + */ + public function getIdentifier(): string { + return 'tables'; + } + + /** + * @return string A translated string + * @since 20.0.0 + */ + public function getName(): string { + return $this->l->t('A table or row was changed'); + } + + /** + * @return int whether the filter should be rather on the top or bottom of + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * @since 20.0.0 + */ + public function getPriority(): int { + return 90; + } + + /** + * Left in for backwards compatibility + * + * @return bool + * @since 20.0.0 + */ + public function canChangeStream(): bool { + return true; + } + + /** + * Left in for backwards compatibility + * + * @return bool + * @since 20.0.0 + */ + public function isDefaultEnabledStream(): bool { + return true; + } + + /** + * @return bool True when the option can be changed for the mail + * @since 20.0.0 + */ + public function canChangeMail(): bool { + return true; + } + + /** + * @return bool Whether or not an activity email should be send by default + * @since 20.0.0 + */ + public function isDefaultEnabledMail(): bool { + return false; + } +} diff --git a/lib/Activity/TablesProvider.php b/lib/Activity/TablesProvider.php new file mode 100644 index 0000000000..df53f48fe3 --- /dev/null +++ b/lib/Activity/TablesProvider.php @@ -0,0 +1,164 @@ +getApp() !== 'tables') { + throw new \InvalidArgumentException(); + } + + $event = $this->setIcon($event); + $subjectIdentifier = $event->getSubject(); + $subjectParams = $event->getSubjectParameters(); + $ownActivity = ($event->getAuthor() === $this->userId); + + /** + * Map stored parameter objects to rich string types + */ + $author = $event->getAuthor(); + $user = $this->userManager->get($author); + $params = []; + + if ($user !== null) { + $params = [ + 'user' => [ + 'type' => 'user', + 'id' => $author, + 'name' => $user->getDisplayName() + ], + ]; + $event->setAuthor($author); + } else { + $params = [ + 'user' => [ + 'type' => 'user', + 'id' => 'deleted_users', + 'name' => 'deleted_users', + ] + ]; + } + + if ($event->getObjectType() === ActivityManager::TABLES_OBJECT_TABLE) { + $table = [ + 'type' => 'highlight', + 'id' => (string)$event->getObjectId(), + 'name' => $event->getObjectName(), + 'link' => $this->tablesUrl('/table/' . $event->getObjectId()), + ]; + $params['table'] = $table; + $event->setLink($this->tablesUrl('/table/' . $event->getObjectId())); + } + + if ($event->getObjectType() === ActivityManager::TABLES_OBJECT_ROW) { + $table = [ + 'type' => 'highlight', + 'id' => (string)$subjectParams['table']['id'], + 'name' => (string)$subjectParams['table']['title'], + 'link' => $this->tablesUrl('/table/' . $subjectParams['table']['id']), + ]; + $params['table'] = $table; + $row = [ + 'type' => 'highlight', + 'id' => (string)$event->getObjectId(), + 'name' => '#' . $event->getObjectId(), + 'link' => $this->tablesUrl('/table/' . $subjectParams['table']['id'] . '/row/' . $event->getObjectId()), + ]; + $params['row'] = $row; + $event->setLink($this->tablesUrl('/table/' . $subjectParams['table']['id'] . '/row/' . $event->getObjectId())); + + if ($event->getSubject() === ActivityManager::SUBJECT_ROW_UPDATE) { + foreach ($subjectParams['changeCols'] as $changeCol) { + $params['col-' . $changeCol['id']] = [ + 'type' => 'highlight', + 'id' => (string)$changeCol['id'], + 'name' => $changeCol['name'] ?? '', + ]; + } + } + } + + if (array_key_exists('before', $subjectParams) && is_string($subjectParams['before'])) { + $params['before'] = [ + 'type' => 'highlight', + 'id' => (string)$subjectParams['before'], + 'name' => $subjectParams['before'] ?? '' + ]; + } + + if (array_key_exists('after', $subjectParams)) { + $params['after'] = [ + 'type' => 'highlight', + 'id' => (string)$subjectParams['after'], + 'name' => $subjectParams['after'] ?? '' + ]; + } + + try { + $subject = $this->activityManager->getActivityFormat($language, $subjectIdentifier, $subjectParams, $ownActivity); + $this->setSubjects($event, $subject, $params); + } catch (\Exception $e) { + } + + return $event; + } + + private function setIcon(IEvent $event) { + $event->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('tables', 'app-dark.svg'))); + + if (str_contains($event->getSubject(), '_update')) { + $event->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('files', 'change.svg'))); + } + + if (str_contains($event->getSubject(), '_create')) { + $event->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('files', 'add-color.svg'))); + } + + if (str_contains($event->getSubject(), '_delete')) { + $event->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('files', 'delete-color.svg'))); + } + + return $event; + } + + private function tablesUrl($endpoint) { + return $this->urlGenerator->linkToRouteAbsolute('tables.page.index') . '#/' . trim($endpoint, '/'); + } + + private function setSubjects(IEvent $event, $subject, array $parameters) { + $placeholders = $replacements = $richParameters = []; + + foreach ($parameters as $placeholder => $parameter) { + $placeholders[] = '{' . $placeholder . '}'; + if (is_array($parameter) && array_key_exists('name', $parameter)) { + $replacements[] = $parameter['name']; + $richParameters[$placeholder] = $parameter; + } else { + $replacements[] = ''; + } + } + + $event->setParsedSubject(str_replace($placeholders, $replacements, $subject)) + ->setRichSubject($subject, $richParameters); + $event->setSubject($subject, $parameters); + } +} diff --git a/lib/Helper/CircleHelper.php b/lib/Helper/CircleHelper.php index c78e547f25..f6e18c5910 100644 --- a/lib/Helper/CircleHelper.php +++ b/lib/Helper/CircleHelper.php @@ -104,4 +104,24 @@ public function getCircleIdsForUser(string $userId): ?array { return $circleIds; } + public function getUserIdsInCircle(string $circleId): array { + if (!$this->circlesEnabled || !$this->circlesManager) { + return []; + } + + try { + $circle = $this->circlesManager->getCircle($circleId); + if ($circle === null) { + return []; + } + $members = $circle->getMembers(); + return array_map(fn ($member) => $member->getUserId(), $members); + } catch (Throwable $e) { + $this->logger->warning('Failed to get users in circle: ' . $e->getMessage(), [ + 'circleId' => $circleId + ]); + return []; + } + } + } diff --git a/lib/Helper/GroupHelper.php b/lib/Helper/GroupHelper.php index 29d1b9720b..83780b1b6a 100644 --- a/lib/Helper/GroupHelper.php +++ b/lib/Helper/GroupHelper.php @@ -27,4 +27,19 @@ public function getGroupDisplayName(string $groupId): string { return $groupId; } } + + public function getUserIdsInGroup(string $groupId): array { + $users = []; + try { + $group = $this->groupManager->get($groupId); + if ($group) { + foreach ($group->getUsers() as $user) { + $users[] = $user->getUID(); + } + } + } catch (\Exception $e) { + $this->logger->error('Error fetching users in group: ' . $e->getMessage()); + } + return $users; + } } diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 34a76ee832..2c03eb9162 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -7,6 +7,7 @@ namespace OCA\Tables\Service; +use OCA\Tables\Activity\ActivityManager; use OCA\Tables\Db\Column; use OCA\Tables\Db\ColumnMapper; use OCA\Tables\Db\Row2; @@ -51,6 +52,7 @@ public function __construct( private Row2Mapper $row2Mapper, private IEventDispatcher $eventDispatcher, private ColumnsHelper $columnsHelper, + private ActivityManager $activityManager, ) { parent::__construct($logger, $userId, $permissionsService); @@ -220,6 +222,12 @@ public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): R $insertedRow = $this->row2Mapper->insert($row2); $this->eventDispatcher->dispatchTyped(new RowAddedEvent($insertedRow)); + $this->activityManager->triggerEvent( + objectType: ActivityManager::TABLES_OBJECT_ROW, + object: $insertedRow, + subject: ActivityManager::SUBJECT_ROW_CREATE, + author: $this->userId, + ); return $this->filterRowResult($view, $insertedRow); } catch (InternalError|Exception $e) { @@ -506,6 +514,19 @@ public function updateSet( $this->eventDispatcher->dispatchTyped(new RowUpdatedEvent($updatedRow, $previousData)); + if ($updatedRow->getData() !== $previousData) { + $this->activityManager->triggerEvent( + objectType: ActivityManager::TABLES_OBJECT_ROW, + object: $updatedRow, + subject: ActivityManager::SUBJECT_ROW_UPDATE, + author: $this->userId, + additionalParams: [ + 'before' => $previousData, + 'after' => $updatedRow->getData(), + ] + ); + } + return $this->filterRowResult($view ?? null, $updatedRow); } @@ -574,6 +595,12 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { $event = new RowDeletedEvent($item, $item->getData()); $this->eventDispatcher->dispatchTyped($event); + $this->activityManager->triggerEvent( + objectType: ActivityManager::TABLES_OBJECT_ROW, + object: $deletedRow, + subject: ActivityManager::SUBJECT_ROW_DELETE, + author: $this->userId, + ); return $this->filterRowResult($view ?? null, $deletedRow); } catch (Exception $e) { diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index d7ac5934f4..c1537eacee 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -498,4 +498,35 @@ public function changeSenderForNode(string $nodeType, int $nodeId, string $newOw return $newShares; } + /** + * @throws InternalError + * @return string[] + */ + public function findSharedWithUserIds(int $elementId, string $elementType): array { + try { + $shares = $this->mapper->findAllSharesForNode($elementType, $elementId, ''); + $sharedWithUserIds = []; + + /** @var Share $share */ + foreach ($shares as $share) { + if ($share->getReceiverType() === ShareReceiverType::USER) { + $sharedWithUserIds[] = $share->getReceiver(); + } + if ($share->getReceiverType() === ShareReceiverType::CIRCLE && $this->circleHelper->isCirclesEnabled()) { + $userIds = $this->circleHelper->getUserIdsInCircle($share->getReceiver()); + $sharedWithUserIds = array_merge($sharedWithUserIds, $userIds); + } + if ($share->getReceiverType() === ShareReceiverType::GROUP) { + $userIds = $this->groupHelper->getUserIdsInGroup($share->getReceiver()); + $sharedWithUserIds = array_merge($sharedWithUserIds, $userIds); + } + } + + return array_unique($sharedWithUserIds); + } catch (Exception $e) { + $this->logger->error('Could not find shared with users: ' . $e->getMessage(), ['exception' => $e]); + throw new InternalError('Could not find shared with users'); + } + } + } diff --git a/lib/Service/TableService.php b/lib/Service/TableService.php index e5b9eaef6d..dc720665f1 100644 --- a/lib/Service/TableService.php +++ b/lib/Service/TableService.php @@ -10,6 +10,8 @@ namespace OCA\Tables\Service; use DateTime; +use OCA\Tables\Activity\ActivityManager; +use OCA\Tables\Activity\ChangeSet; use OCA\Tables\AppInfo\Application; use OCA\Tables\Db\Table; use OCA\Tables\Db\TableMapper; @@ -52,6 +54,7 @@ public function __construct( protected IAppManager $appManager, protected IL10N $l, protected Defaults $themingDefaults, + private ActivityManager $activityManager, ) { parent::__construct($logger, $userId, $permissionsService); } @@ -296,6 +299,13 @@ public function create(string $title, string $template, ?string $emoji, ?string $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } + $this->activityManager->triggerEvent( + objectType: ActivityManager::TABLES_OBJECT_TABLE, + object: $table, + subject: ActivityManager::SUBJECT_TABLE_CREATE, + additionalParams: [], + author: $userId + ); return $table; } @@ -436,6 +446,13 @@ public function delete(int $id, ?string $userId = null): Table { $event = new TableDeletedEvent(table: $item); $this->eventDispatcher->dispatchTyped($event); + $this->activityManager->triggerEvent( + objectType: ActivityManager::TABLES_OBJECT_TABLE, + object: $item, + subject: ActivityManager::SUBJECT_TABLE_DELETE, + additionalParams: [], + author: $userId + ); return $item; } @@ -469,6 +486,7 @@ public function update(int $id, ?string $title, ?string $emoji, ?string $descrip throw new PermissionError('PermissionError: can not update table with id ' . $id); } + $changes = new ChangeSet($table); $time = new DateTime(); if ($title !== null) { $table->setTitle($title); @@ -496,6 +514,12 @@ public function update(int $id, ?string $title, ?string $emoji, ?string $descrip $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } + $changes->setAfter($table); + $this->activityManager->triggerUpdateEvents( + objectType: ActivityManager::TABLES_OBJECT_TABLE, + changeSet: $changes, + subject: ActivityManager::SUBJECT_TABLE_UPDATE + ); return $table; } diff --git a/package-lock.json b/package-lock.json index b227545c54..8a0b7fbb93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,8 +28,10 @@ "@tiptap/vue-2": "^3.4.2", "@vueuse/core": "^11.3.0", "debounce": "^2.2.0", + "dompurify": "^3.2.5", "pinia": "^2.3.1", "vue": "^2.7.16", + "vue-infinite-loading": "^2.4.5", "vue-material-design-icons": "^5.3.1", "vue-papa-parse": "^3.1.0", "vue-router": "^3.6.5" @@ -16679,6 +16681,15 @@ "vue": "^2.6.0" } }, + "node_modules/vue-infinite-loading": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/vue-infinite-loading/-/vue-infinite-loading-2.4.5.tgz", + "integrity": "sha512-xhq95Mxun060bRnsOoLE2Be6BR7jYwuC89kDe18+GmCLVrRA/dU0jrGb12Xu6NjmKs+iTW0AA6saSEmEW4cR7g==", + "license": "MIT", + "peerDependencies": { + "vue": "^2.6.10" + } + }, "node_modules/vue-material-design-icons": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.3.1.tgz", diff --git a/package.json b/package.json index 9f412b6dfe..2dd1240fc1 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,10 @@ "@tiptap/vue-2": "^3.4.2", "@vueuse/core": "^11.3.0", "debounce": "^2.2.0", + "dompurify": "^3.2.5", "pinia": "^2.3.1", "vue": "^2.7.16", + "vue-infinite-loading": "^2.4.5", "vue-material-design-icons": "^5.3.1", "vue-papa-parse": "^3.1.0", "vue-router": "^3.6.5" diff --git a/src/modules/main/sections/MainWrapper.vue b/src/modules/main/sections/MainWrapper.vue index 07bf013381..bbd8099484 100644 --- a/src/modules/main/sections/MainWrapper.vue +++ b/src/modules/main/sections/MainWrapper.vue @@ -89,6 +89,9 @@ export default { element() { this.reload() }, + activeRowId() { + this.reload() + }, }, beforeMount() { diff --git a/src/modules/modals/EditRow.vue b/src/modules/modals/EditRow.vue index d963985816..2568fc83ed 100644 --- a/src/modules/modals/EditRow.vue +++ b/src/modules/modals/EditRow.vue @@ -9,43 +9,76 @@ size="large" @closing="actionCancel">