diff --git a/appinfo/routes.php b/appinfo/routes.php index 506ed181f8..bda4b2d2fe 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -510,11 +510,6 @@ 'url' => '/api/textBlocks/{id}/shares', 'verb' => 'GET', ], - [ - 'name' => 'actionStep#findAllStepsForAction', - 'url' => '/api/action-step/{actionId}/steps', - 'verb' => 'GET' - ], ], 'resources' => [ 'accounts' => ['url' => '/api/accounts'], diff --git a/lib/Controller/ActionStepController.php b/lib/Controller/ActionStepController.php index 514df85546..3ae6b4851d 100644 --- a/lib/Controller/ActionStepController.php +++ b/lib/Controller/ActionStepController.php @@ -33,21 +33,6 @@ public function __construct( $this->uid = $userId; } - /** - * @NoAdminRequired - * - * @return JsonResponse - */ - #[TrapError] - public function findAllStepsForAction(int $actionId): JsonResponse { - if ($this->uid === null) { - return JsonResponse::error('User not found', Http::STATUS_UNAUTHORIZED); - } - $actionSteps = $this->quickActionsService->findAllActionSteps($actionId, $this->uid); - - return JsonResponse::success($actionSteps); - } - /** * @NoAdminRequired * @param string $name diff --git a/lib/Db/ActionStep.php b/lib/Db/ActionStep.php index 5a2b87fc1a..fbd454c233 100644 --- a/lib/Db/ActionStep.php +++ b/lib/Db/ActionStep.php @@ -36,7 +36,6 @@ public function __construct() { $this->addType('name', 'string'); $this->addType('order', 'integer'); $this->addType('actionId', 'integer'); - $this->addType('parameter', 'string'); $this->addType('tagId', 'integer'); $this->addType('mailboxId', 'integer'); } diff --git a/lib/Db/ActionStepMapper.php b/lib/Db/ActionStepMapper.php index 772318839e..892da8ab7d 100644 --- a/lib/Db/ActionStepMapper.php +++ b/lib/Db/ActionStepMapper.php @@ -42,12 +42,12 @@ public function find(int $id, string $owner): ActionStep { ); return $this->findEntity($qb); } - /** - * @param mixed $actionId - * @param string $owner Action's owner - * @return ActionStep[] - */ - public function findAllStepsForOneAction(int $actionId, string $owner) { + + public function findStepsByActionIds(array $actionIds, string $owner): array { + if (empty($actionIds)) { + return []; + } + $qb = $this->db->getQueryBuilder(); $qb->select('step.*') ->from($this->getTableName(), 'step') @@ -55,7 +55,7 @@ public function findAllStepsForOneAction(int $actionId, string $owner) { ->join('actions', 'mail_accounts', 'accounts', $qb->expr()->eq('actions.account_id', 'accounts.id')) ->where( $qb->expr()->andX( - $qb->expr()->eq('step.action_id', $qb->createNamedParameter($actionId, IQueryBuilder::PARAM_INT)), + $qb->expr()->in('step.action_id', $qb->createNamedParameter($actionIds, IQueryBuilder::PARAM_INT_ARRAY)), $qb->expr()->eq('accounts.user_id', $qb->createNamedParameter($owner)) ) ) diff --git a/lib/Db/Actions.php b/lib/Db/Actions.php index 20fbec1a03..97096fd5aa 100644 --- a/lib/Db/Actions.php +++ b/lib/Db/Actions.php @@ -22,18 +22,30 @@ class Actions extends Entity implements JsonSerializable { protected $name; protected $accountId; + protected $actionSteps = []; + protected $icon = ''; public function __construct() { $this->addType('name', 'string'); $this->addType('accountId', 'integer'); } + public function setActionSteps(array $actionSteps): void { + $this->actionSteps = $actionSteps; + } + + public function setIcon(string $icon): void { + $this->icon = $icon; + } + #[ReturnTypeWillChange] public function jsonSerialize() { return [ 'id' => $this->getId(), 'name' => $this->getName(), 'accountId' => $this->getAccountId(), + 'actionSteps' => $this->actionSteps, + 'icon' => $this->icon, ]; } diff --git a/lib/Service/QuickActionsService.php b/lib/Service/QuickActionsService.php index 3dc337605f..af6ce3cfc3 100644 --- a/lib/Service/QuickActionsService.php +++ b/lib/Service/QuickActionsService.php @@ -35,13 +35,24 @@ public function __construct( private ActionStepMapper $actionStepMapper, ) { } - /** * @param string $userId * @return Actions[] */ public function findAll(string $userId): array { - return $this->actionsMapper->findAll($userId); + $actions = $this->actionsMapper->findAll($userId); + $actionIds = array_map(fn (Actions $action) => $action->getId(), $actions); + $actionSteps = $this->actionStepMapper->findStepsByActionIds($actionIds, $userId); + return array_map(function (Actions $action) use ($actionSteps) { + $steps = array_values(array_filter($actionSteps, function (ActionStep $step) use ($action) { + return $step->getActionId() === $action->getId(); + })); + $action->setActionSteps($steps); + if (!empty($steps)) { + $action->setIcon($steps[0]->getName()); + } + return $action; + }, $actions); } /** @@ -74,14 +85,6 @@ public function delete(int $actionId, string $userId): void { $this->actionsMapper->delete($action); } - /** - * @param string $userId - * @return ActionStep[] - */ - public function findAllActionSteps(int $actionId, string $userId): array { - return $this->actionStepMapper->findAllStepsForOneAction($actionId, $userId); - } - /** * @throws DoesNotExistException */ diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index d8732251b7..787335e825 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -479,7 +479,6 @@ import NoTrashMailboxConfiguredError from '../errors/NoTrashMailboxConfiguredError.js' import logger from '../logger.js' import { buildRecipients as buildReplyRecipients } from '../ReplyBuilder.js' -import { findAllStepsForAction } from '../service/QuickActionsService.js' import { FOLLOW_UP_TAG_LABEL } from '../store/constants.js' import useMainStore from '../store/mainStore.js' import { mailboxHasRights } from '../util/acl.js' @@ -589,7 +588,6 @@ export default { customSnoozeDateTime: new Date(moment().add(2, 'hours').minute(0).second(0).valueOf()), overwriteOneLineMobile: false, hoveringAvatar: false, - filteredQuickActions: [], quickActionLoading: false, } }, @@ -846,20 +844,36 @@ export default { }, ].filter((option) => option.timestamp !== null) }, - }, - watch: { - storeActions() { - this.filterAndEnrichQuickActions() + filteredQuickActions() { + const filteredQuickActions = [] + const quickActions = this.mainStore.getQuickActions().filter((action) => action.accountId === this.data.accountId) + for (const action of quickActions) { + const check = action.actionSteps.every((step) => { + if (['markAsSpam', 'applyTag', 'markAsImportant', 'markAsFavorite'].includes(step.name) && !this.hasWriteAcl) { + return false + } + if (['markAsRead', 'markAsUnread'].includes(step.name) && !this.hasSeenAcl) { + return false + } + if (['moveThread', 'deleteThread'].includes(step.name) && !this.hasDeleteAcl) { + return false + } + return true + }) + if (check) { + filteredQuickActions.push({ + ...action, + }) + } + } + return filteredQuickActions }, }, - async mounted() { + mounted() { this.onWindowResize() window.addEventListener('resize', this.onWindowResize) - if (this.filteredQuickActions.length === 0) { - await this.filterAndEnrichQuickActions() - } }, methods: { @@ -874,38 +888,11 @@ export default { return shortRelativeDatetime(new Date(this.data.dateInt * 1000)) }, - async filterAndEnrichQuickActions() { - this.filteredQuickActions = [] - const quickActions = this.mainStore.getQuickActions().filter((action) => action.accountId === this.data.accountId) - for (const action of quickActions) { - const steps = await findAllStepsForAction(action.id) - const check = steps.every((step) => { - if (['markAsSpam', 'applyTag', 'markAsImportant', 'markAsFavorite'].includes(step.type) && !this.hasWriteAcl) { - return false - } - if (['markAsRead', 'markAsUnread'].includes(step.type) && !this.hasSeenAcl) { - return false - } - if (['moveThread', 'deleteThread'].includes(step.type) && !this.hasDeleteAcl) { - return false - } - return true - }) - if (check) { - this.filteredQuickActions.push({ - ...action, - steps, - icon: steps[0]?.name, - }) - } - } - }, - async executeQuickAction(action) { this.closeQuickActionsMenu() this.quickActionLoading = true try { - for (const step of action.steps) { + for (const step of action.actionSteps) { switch (step.name) { case 'markAsSpam': if (this.layoutMessageViewThreaded) { diff --git a/src/components/quickActions/Settings.vue b/src/components/quickActions/Settings.vue index a2f35a5b9a..db87ac24d4 100644 --- a/src/components/quickActions/Settings.vue +++ b/src/components/quickActions/Settings.vue @@ -132,7 +132,7 @@ import TagIcon from 'vue-material-design-icons/TagOutline.vue' import IconDelete from 'vue-material-design-icons/TrashCanOutline.vue' import Action from './Action.vue' import logger from '../../logger.js' -import { createActionStep, deleteActionStep, findAllStepsForAction, updateActionStep } from '../../service/QuickActionsService.js' +import { createActionStep, deleteActionStep, updateActionStep } from '../../service/QuickActionsService.js' import useMainStore from '../../store/mainStore.js' export default { @@ -229,7 +229,8 @@ export default { this.actions = [] } else { this.localAction = { ...action } - this.actions = await findAllStepsForAction(action.id) + delete this.localAction.actionSteps + this.actions = action.actionSteps this.highestOrder = Math.max(...this.actions.map((a) => a.order), 0) this.editMode = true } @@ -260,7 +261,7 @@ export default { for (const [index, action] of this.actions.entries()) { if (action?.id !== null && action?.id !== undefined) { try { - await updateActionStep(action.id, action.name, action.order, action?.tagId, action?.mailboxId) + this.actions[index] = await updateActionStep(action.id, action.name, action.order, action?.tagId, action?.mailboxId) } catch (error) { logger.error('Could not update quick action step', { error, @@ -273,10 +274,12 @@ export default { this.actions[index] = createdStep } } + this.localAction = quickAction } showSuccess(t('mail', 'Quick action updated')) } else { let quickAction + const createdSteps = [] try { quickAction = await this.mainStore.createQuickAction(this.localAction.name, this.account.id) } catch (error) { @@ -288,8 +291,12 @@ export default { } try { for (const action of this.actions) { - await createActionStep(action.name, action.order, quickAction.id, action?.tagId, action?.mailboxId) + const createdStep = await createActionStep(action.name, action.order, quickAction.id, action?.tagId, action?.mailboxId) + if (createdStep) { + createdSteps.push(createdStep) + } } + this.actions = createdSteps } catch (error) { logger.error('Could not add step to quick action', { error, @@ -297,8 +304,10 @@ export default { showError(t('mail', 'Failed to add steps to quick action')) this.closeEditModal() } + this.localAction = quickAction showSuccess(t('mail', 'Quick action created')) } + this.mainStore.patchActionStepsLocally(this.localAction.id, this.actions) this.closeEditModal() }, @@ -338,9 +347,13 @@ export default { }, async deleteAction(item) { + this.actions = this.actions.filter((action) => action.order !== item.order).map((action, index) => ({ ...action, order: index + 1 })) + this.highestOrder = Math.max(...this.actions.map((a) => a.order), 0) if (item.id) { try { await deleteActionStep(item.id) + const actions = this.actions.filter((action) => action.id) + this.mainStore.patchActionStepsLocally(this.localAction.id, actions) } catch (error) { logger.error('Could not delete action step', { error, @@ -349,8 +362,6 @@ export default { return } } - this.actions = this.actions.filter((action) => action.order !== item.order).map((action, index) => ({ ...action, order: index + 1 })) - this.highestOrder = Math.max(...this.actions.map((a) => a.order), 0) }, }, } diff --git a/src/service/QuickActionsService.js b/src/service/QuickActionsService.js index a261cf456d..cdcc5c4d23 100644 --- a/src/service/QuickActionsService.js +++ b/src/service/QuickActionsService.js @@ -40,14 +40,6 @@ export async function deleteQuickAction(id) { }) } -export async function findAllStepsForAction(actionId) { - const url = generateUrl('/apps/mail/api/action-step/{id}/steps', { id: actionId }) - return handleHttpAuthErrors(async () => { - const response = await axios.get(url) - return response.data.data - }) -} - export async function createActionStep(name, order, actionId, tagId = null, mailboxId = null) { const url = generateUrl('/apps/mail/api/action-step') return handleHttpAuthErrors(async () => { diff --git a/src/store/mainStore/actions.js b/src/store/mainStore/actions.js index 691499a2ca..96135f3bc3 100644 --- a/src/store/mainStore/actions.js +++ b/src/store/mainStore/actions.js @@ -2322,6 +2322,14 @@ export default function mainStoreActions() { Vue.set(this.quickActions, index, quickAction) } }, + patchActionStepsLocally(id, steps) { + const index = this.quickActions.findIndex((s) => s.id === id) + if (index !== -1) { + const updatedQuickAction = this.quickActions[index] + updatedQuickAction.actionSteps = steps + Vue.set(this.quickActions, index, updatedQuickAction) + } + }, deleteQuickActionLocally(id) { const index = this.quickActions.findIndex((s) => s.id === id) if (index !== -1) { diff --git a/tests/Unit/Service/QuickActionsServiceTest.php b/tests/Unit/Service/QuickActionsServiceTest.php index 9e923a12fc..f4a142d913 100644 --- a/tests/Unit/Service/QuickActionsServiceTest.php +++ b/tests/Unit/Service/QuickActionsServiceTest.php @@ -43,18 +43,36 @@ protected function setUp(): void { public function testFindAll(): void { $userId = 'user123'; + $action = new Actions(); + $action->setId(1); + $action->setName('Test Action'); + $action->setAccountId(1); + $actionStep = new ActionStep(); + $actionStep->setName('markAsUnread'); + $actionStep->setOrder(1); + $actionStep->setActionId(1); + $this->actionsMapper->expects($this->once()) ->method('findAll') ->with($userId) - ->willReturn([]); + ->willReturn([$action]); + + $this->actionStepMapper->expects($this->once()) + ->method('findStepsByActionIds') + ->with([$action->getId()], $userId) + ->willReturn([$actionStep]); $result = $this->quickActionsService->findAll($userId); + $action->setActionSteps([$actionStep]); + + $this->assertEquals($result, [$action]); $this->assertIsArray($result); - $this->assertCount(0, $result); + $this->assertCount(1, $result); } + public function testFind(): void { $actionId = 1; $userId = 'user123'; @@ -154,18 +172,6 @@ public function testDelete(): void { $this->quickActionsService->delete($actionId, $userId); } - public function testFindAllActionSteps(): void { - $actionId = 1; - $userId = 'user123'; - $this->actionStepMapper->expects($this->once()) - ->method('findAllStepsForOneAction') - ->with($actionId, $userId) - ->willReturn([]); - $result = $this->quickActionsService->findAllActionSteps($actionId, $userId); - $this->assertIsArray($result); - $this->assertCount(0, $result); - } - public function testFindActionStep(): void { $actionId = 1; $userId = 'user123';