Skip to content

Commit be9d9f6

Browse files
Merge pull request #7474 from nextcloud/backport/7412/stable31
[stable31] feat(sharing): ability to leave a shared board
2 parents 0bf862f + eb16d87 commit be9d9f6

File tree

6 files changed

+111
-0
lines changed

6 files changed

+111
-0
lines changed

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
['name' => 'board#update', 'url' => '/boards/{boardId}', 'verb' => 'PUT'],
2323
['name' => 'board#delete', 'url' => '/boards/{boardId}', 'verb' => 'DELETE'],
2424
['name' => 'board#deleteUndo', 'url' => '/boards/{boardId}/deleteUndo', 'verb' => 'POST'],
25+
['name' => 'board#leave', 'url' => '/boards/{boardId}/leave', 'verb' => 'POST'],
2526
['name' => 'board#getUserPermissions', 'url' => '/boards/{boardId}/permissions', 'verb' => 'GET'],
2627
['name' => 'board#addAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'POST'],
2728
['name' => 'board#updateAcl', 'url' => '/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],

lib/Controller/BoardController.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ public function deleteUndo($boardId) {
8282
return $this->boardService->deleteUndo($boardId);
8383
}
8484

85+
/**
86+
* @NoAdminRequired
87+
* @param $boardId
88+
* @return \OCP\AppFramework\Db\Entity|null
89+
*/
90+
public function leave(int $boardId) {
91+
return $this->boardService->leave($boardId);
92+
}
93+
8594
/**
8695
* @NoAdminRequired
8796
* @param $boardId

lib/Db/AclMapper.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ public function findByParticipant($type, $participant): array {
9898
return $this->findEntities($qb);
9999
}
100100

101+
public function findParticipantFromBoard(int $boardId, int $type, string $participant): ?Acl {
102+
$qb = $this->db->getQueryBuilder();
103+
$qb->select('*')
104+
->from('deck_board_acl')
105+
->where($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
106+
->where($qb->expr()->eq('participant', $qb->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
107+
->andWhere($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)));
108+
return $this->findEntity($qb);
109+
}
110+
101111
/**
102112
* @throws \OCP\DB\Exception
103113
*/

lib/Service/BoardService.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,37 @@ public function deleteAcl(int $id): ?Acl {
466466
return $deletedAcl;
467467
}
468468

469+
public function leave(int $boardId): ?Acl {
470+
if ($this->permissionService->userIsBoardOwner($boardId)) {
471+
throw new BadRequestException('Board owner cannot leave board');
472+
}
473+
474+
$acl = $this->aclMapper->findParticipantFromBoard($boardId, Acl::PERMISSION_TYPE_USER, $this->userId);
475+
476+
if (!$acl) {
477+
throw new BadRequestException('Not a participant of this board');
478+
}
479+
480+
$this->assignedUsersMapper->deleteByParticipantOnBoard($acl->getParticipant(), $acl->getBoardId());
481+
482+
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $acl, ActivityManager::SUBJECT_BOARD_UNSHARE);
483+
$this->changeHelper->boardChanged($acl->getBoardId());
484+
485+
$version = \OCP\Util::getVersion()[0];
486+
if ($version >= 16) {
487+
try {
488+
$resourceProvider = Server::get(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
489+
$resourceProvider->invalidateAccessCache($acl->getBoardId());
490+
} catch (\Exception $e) {
491+
}
492+
}
493+
494+
$deletedAcl = $this->aclMapper->delete($acl);
495+
$this->eventDispatcher->dispatchTyped(new AclDeletedEvent($acl));
496+
497+
return $deletedAcl;
498+
}
499+
469500
/**
470501
* @throws BadRequestException
471502
* @throws DbException

src/components/navigation/AppNavigationBoard.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@
112112
@click="actionDelete">
113113
{{ t('deck', 'Delete board') }}
114114
</NcActionButton>
115+
116+
<NcActionButton v-if="canLeave && !isDueSubmenuActive"
117+
icon="icon-delete"
118+
:close-after-click="true"
119+
@click="actionLeave">
120+
<template #icon>
121+
<LeaveIcon :size="20" decorative />
122+
</template>
123+
{{ t('deck', 'Leave board') }}
124+
</NcActionButton>
115125
</template>
116126
</NcAppNavigationItem>
117127
<div v-else-if="editing" class="board-edit">
@@ -152,12 +162,15 @@ import { NcAppNavigationIconBullet, NcAppNavigationItem, NcColorPicker, NcButton
152162
import ClickOutside from 'vue-click-outside'
153163
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
154164
import CloneIcon from 'vue-material-design-icons/ContentDuplicate.vue'
165+
import LeaveIcon from 'vue-material-design-icons/ExitRun.vue'
155166
import AccountIcon from 'vue-material-design-icons/Account.vue'
156167
import CloseIcon from 'vue-material-design-icons/Close.vue'
157168
import CheckIcon from 'vue-material-design-icons/Check.vue'
158169
159170
import { loadState } from '@nextcloud/initial-state'
160171
import { emit } from '@nextcloud/event-bus'
172+
import { getCurrentUser } from '@nextcloud/auth'
173+
import { showError } from '@nextcloud/dialogs'
161174
162175
import isTouchDevice from '../../mixins/isTouchDevice.js'
163176
import BoardCloneModal from './BoardCloneModal.vue'
@@ -178,6 +191,7 @@ export default {
178191
CloneIcon,
179192
CloseIcon,
180193
CheckIcon,
194+
LeaveIcon,
181195
BoardCloneModal,
182196
},
183197
directives: {
@@ -207,6 +221,7 @@ export default {
207221
updateDueSetting: null,
208222
canCreate: canCreateState,
209223
cloneModalOpen: false,
224+
currentUser: getCurrentUser(),
210225
}
211226
},
212227
computed: {
@@ -228,6 +243,9 @@ export default {
228243
canManage() {
229244
return this.board.permissions.PERMISSION_MANAGE
230245
},
246+
canLeave() {
247+
return this.board.acl?.find((acl) => acl.participant.uid === this.currentUser?.uid && acl.participant.type === 0) !== undefined
248+
},
231249
dueDateReminderIcon() {
232250
if (this.board.settings['notify-due'] === 'all') {
233251
return 'icon-sound'
@@ -315,6 +333,33 @@ export default {
315333
true,
316334
)
317335
},
336+
actionLeave() {
337+
OC.dialogs.confirmDestructive(
338+
t('deck', 'Are you sure you want to leave the board {title}?', { title: this.board.title }),
339+
t('deck', 'Leave the board?'),
340+
{
341+
type: OC.dialogs.YES_NO_BUTTONS,
342+
confirm: t('deck', 'Leave'),
343+
confirmClasses: 'error',
344+
cancel: t('deck', 'Cancel'),
345+
},
346+
(result) => {
347+
if (result) {
348+
this.loading = true
349+
this.boardApi.leaveBoard(this.board)
350+
.then(() => {
351+
this.loading = false
352+
this.$store.dispatch('removeBoard', this.board)
353+
})
354+
.catch(() => {
355+
showError(t('deck', 'Failed to leave the board'))
356+
this.loading = false
357+
})
358+
}
359+
},
360+
true,
361+
)
362+
},
318363
actionDetails() {
319364
this.$router.push({ name: 'board.details', params: { id: this.board.id } })
320365
},

src/services/BoardApi.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ export class BoardApi {
9393
})
9494
}
9595

96+
leaveBoard(board) {
97+
return axios.post(this.url(`/boards/${board.id}/leave`))
98+
.then(
99+
() => {
100+
return Promise.resolve()
101+
},
102+
(err) => {
103+
return Promise.reject(err)
104+
},
105+
)
106+
.catch((err) => {
107+
return Promise.reject(err)
108+
})
109+
}
110+
96111
loadBoards() {
97112
return axios.get(this.url('/boards'))
98113
.then(

0 commit comments

Comments
 (0)