Compare commits

...

27 Commits

Author SHA1 Message Date
Julius Härtl
2326281e04 Merge pull request #2965 from nextcloud/backport/2964/stable1.3
[stable1.3] Fix navigating to board details
2021-04-13 08:01:31 -01:00
Julius Härtl
f04a2f6c3b Fix navigating to board details (fix #2951)
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-12 14:11:01 +00:00
Julius Härtl
ccffe1094e Bump version to 1.3.2
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-07 18:47:23 +02:00
Julius Härtl
b698c42a1d Merge pull request #2946 from nextcloud/backport/2793/stable1.3
[stable1.3] fix desc save bug
2021-04-07 18:41:22 +02:00
Jakob Röhrl
454a9115d8 fix desc save bug
Signed-off-by: Jakob Röhrl <jakob.roehrl@web.de>
2021-04-07 08:36:54 +02:00
Julius Härtl
e359c0582a Merge pull request #2924 from nextcloud/backport/2923/stable1.3
[stable1.3] Avoid reusing the existing route object to make navigation work properly
2021-03-29 14:13:24 +02:00
Julius Härtl
3d912f4af1 Avoid reusing the existing route object to make navigation work properly
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-29 12:00:28 +00:00
Julius Härtl
6e60c5ab4b Merge pull request #2880 from nextcloud/backport/2871/stable1.3
[stable1.3] Only extract additional attributes from query when doing a raw search
2021-03-11 15:39:26 +01:00
Julius Härtl
770d34814d Only extract additional attributes from query when not using the entity mapping for the result
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-11 14:20:01 +00:00
Julius Härtl
49569150cb Merge pull request #2869 from nextcloud/backport/2857/stable1.3
[stable1.3] Don't close tempfile as it is already done
2021-03-08 10:39:08 +01:00
Julius Härtl
1beed6be61 No need to close the temp file as the View is already taking care of it
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-08 09:25:51 +00:00
Julius Härtl
fc85a09f69 Bump version to 1.3.1
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-05 15:51:14 +01:00
Julius Härtl
8719f7a592 Merge pull request #2859 from nextcloud/backport/2823/stable1.3
[stable1.3] Properly pass the user to fetch circles when calling through occ
2021-03-05 15:49:27 +01:00
Julius Härtl
bf002b773f Properly pass the user to fetch circles when calling through occ
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-05 14:40:18 +00:00
Julius Härtl
4e2577ce53 Merge pull request #2849 from nextcloud/backport/2847/stable1.3
[stable1.3] Switch to Content-Disposition attachment and check for sane mimetypes
2021-03-04 11:18:00 +01:00
Julius Härtl
11e642af56 Switch to Content-Disposition attachment and check for sane mimetypes
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-04 07:25:55 +00:00
Julius Härtl
0956e6b2f6 Merge pull request #2844 from nextcloud/backport/2843/stable1.3 2021-03-03 09:19:57 +01:00
Julius Härtl
217c1b3104 Use proper debounce on the sharing input
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-03 08:07:32 +00:00
Julius Härtl
f5452a63be Search by mail on the board sharing input
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-03 08:07:32 +00:00
Julius Härtl
7162f1185d Merge pull request #2827 from nextcloud/backport/2822/stable1.3
[stable1.3] Also include /apps/spreed urls in the listener for loading the talk integration
2021-02-25 15:12:44 +01:00
Julius Härtl
9d5ebf8877 Also include /apps/spreed urls in the listener for loading the talk integration
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-02-24 18:14:14 +00:00
Julius Härtl
28219c91f3 Bump version to 1.3.0
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-02-20 12:36:23 +01:00
Julius Härtl
f75de55d14 Merge pull request #2817 from nextcloud/backport/2812/stable1.3
[stable1.3] Fix issues when creating a card from a talk message
2021-02-20 11:13:01 +01:00
Julius Härtl
1279e7536c Fix issues when creating a card from a talk message
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-02-20 10:05:58 +00:00
Julius Härtl
0c7da5618c Merge pull request #2810 from nextcloud/backport/2795/stable1.3
[stable1.3] Register talk message action for creating deck cards
2021-02-19 16:08:47 +01:00
Julius Härtl
ec73794b25 Register talk message action for creating deck cards
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-02-19 11:56:38 +00:00
Julius Härtl
2c26e98d3b Keep using ubuntu 18.04 for the app build for now
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-02-16 12:32:42 +01:00
21 changed files with 462 additions and 180 deletions

View File

@@ -13,7 +13,7 @@ env:
jobs:
integration:
runs-on: ubuntu-latest
runs-on: ubuntu-18.04
strategy:
fail-fast: false

View File

@@ -1,24 +1,46 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.3.0 - unreleased
## 1.3.2 - 2021-04-07
### Fixed
* [#2869](https://github.com/nextcloud/deck/pull/2869) Don't close tempfile as it is already done
* [#2880](https://github.com/nextcloud/deck/pull/2880) Only extract additional attributes from query when doing a raw search
* [#2924](https://github.com/nextcloud/deck/pull/2924) Avoid reusing the existing route object to make navigation work properly
* [#2946](https://github.com/nextcloud/deck/pull/2946) Fix bug when saving the description
## 1.3.1 - 2021-03-05
### Fixed
* [#2827](https://github.com/nextcloud/deck/pull/2827) Also include /apps/spreed urls in the listener for loading the talk integration
* [#2844](https://github.com/nextcloud/deck/pull/2844) Search by mail on the board sharing input
* [#2849](https://github.com/nextcloud/deck/pull/2849) Switch to Content-Disposition attachment and check for sane mimetypes
* [#2859](https://github.com/nextcloud/deck/pull/2859) Properly pass the user to fetch circles when calling through occ
## 1.3.0 - 2021-02-20
### Added
* [#2638](https://github.com/nextcloud/deck/pull/2638) Sharing files to cards
* [#2683](https://github.com/nextcloud/deck/pull/2683) Handle clicks on calendar entries
* [#2810](https://github.com/nextcloud/deck/pull/2810) Register talk message action for creating deck cards (Requires Nextcloud Talk 11.1.0)
* [#2700](https://github.com/nextcloud/deck/pull/2700) Attempt to copy file on dropping it to deck
* [#2701](https://github.com/nextcloud/deck/pull/2701) Fix uploading files by drag and drop
* [#2707](https://github.com/nextcloud/deck/pull/2707) L10n: Change to a capital letter @Valdnet
* [#2772](https://github.com/nextcloud/deck/pull/2772) Add API to register card actions
* Nextcloud 21 compatiblity
### Fixed
* [#2622](https://github.com/nextcloud/deck/pull/2622) Fix gradient and stack header spacing for safari
* [#2626](https://github.com/nextcloud/deck/pull/2626) Adding a description icon to cards when they contain a description without any checkmarks @MonkeySon
* [#2659](https://github.com/nextcloud/deck/pull/2659) Matching color of description cursor with text color @JonFStr
* [#2676](https://github.com/nextcloud/deck/pull/2676) Only load filter view when shown
* [#2680](https://github.com/nextcloud/deck/pull/2680) Do not try to add change data if it doesn't exist
* [#2681](https://github.com/nextcloud/deck/pull/2681) Filter out deleted stacks from results
* [#2685](https://github.com/nextcloud/deck/pull/2685) Show all boards in move card dialog @jakobroehrl
* [#2687](https://github.com/nextcloud/deck/pull/2687) 3dots no opacity @jakobroehrl
* [#2688](https://github.com/nextcloud/deck/pull/2688) Title > boardname @jakobroehrl
* [#2689](https://github.com/nextcloud/deck/pull/2689) Modal > bigger view wording @jakobroehrl
* [#2712](https://github.com/nextcloud/deck/pull/2712) Docs: Fix table in section "GET /api/v1.0/config" @das-g
* [#2716](https://github.com/nextcloud/deck/pull/2716) Remove repair step which is no longer needed as we cleanup properly
* [#2723](https://github.com/nextcloud/deck/pull/2723) Pad random color with leading zeroes @PVince81
* [#2729](https://github.com/nextcloud/deck/pull/2729) Remove invalid activity parameters @nickvergessen
* [#2750](https://github.com/nextcloud/deck/pull/2750) Fix deck activity emails not being translated @nickvergessen
* [#2751](https://github.com/nextcloud/deck/pull/2751) Properly set author for activity events that are triggered by cron
* [#2796](https://github.com/nextcloud/deck/pull/2796) Install all needed php extensions
* [#2817](https://github.com/nextcloud/deck/pull/2817) Fix issues when creating a card from a talk message
## 1.3.0-beta2

View File

@@ -17,7 +17,7 @@
- 🚀 Get your project organized
</description>
<version>1.3.0-beta2</version>
<version>1.3.2</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>

View File

@@ -197,6 +197,10 @@ class Application20 extends App implements IBootstrap {
$resourceManager->registerResourceProvider(ResourceProviderCard::class);
$symfonyAdapter->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () {
if (strpos(\OC::$server->getRequest()->getPathInfo(), '/call/') === 0) {
// Talk integration has its own entrypoint which already includes collections handling
return;
}
Util::addScript('deck', 'collections');
});
}

View File

@@ -78,8 +78,8 @@ class CardController extends Controller {
* @param int $order
* @return \OCP\AppFramework\Db\Entity
*/
public function create($title, $stackId, $type = 'plain', $order = 999) {
return $this->cardService->create($title, $stackId, $type, $order, $this->userId);
public function create($title, $stackId, $type = 'plain', $order = 999, string $description = '') {
return $this->cardService->create($title, $stackId, $type, $order, $this->userId, $description);
}
/**

View File

@@ -169,7 +169,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
}
$circles = array_map(function ($circle) {
return $circle->getUniqueId();
}, \OCA\Circles\Api\v1\Circles::joinedCircles('', true));
}, \OCA\Circles\Api\v1\Circles::joinedCircles($userId, true));
if (count($circles) === 0) {
return [];
}

View File

@@ -149,8 +149,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function queryCardsByBoards(array $boardIds): IQueryBuilder {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*', 's.board_id')
->selectAlias('s.title', 'stack_title')
$qb->select('c.*')
->from('deck_cards', 'c')
->innerJoin('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id'))
->andWhere($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)));
@@ -281,7 +280,9 @@ class CardMapper extends QBMapper implements IPermissionMapper {
}
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
$qb = $this->queryCardsByBoards($boardIds);
$qb = $this->queryCardsByBoards($boardIds)
->select('s.board_id', 'board_id')
->selectAlias('s.title', 'stack_title');
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere(
$qb->expr()->orX(

View File

@@ -49,8 +49,13 @@ class BeforeTemplateRenderedListener implements IEventListener {
}
Util::addStyle('deck', 'deck');
if (strpos($this->request->getPathInfo(), '/apps/calendar') === 0) {
$pathInfo = $this->request->getPathInfo();
if (strpos($pathInfo, '/apps/calendar') === 0) {
Util::addScript('deck', 'calendar');
}
if (strpos($pathInfo, '/call/') === 0 || strpos($pathInfo, '/apps/spreed') === 0) {
Util::addScript('deck', 'talk');
}
}
}

View File

@@ -27,10 +27,9 @@ use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\StatusException;
use OCA\Deck\Exceptions\ConflictException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
@@ -49,6 +48,7 @@ class FileService implements IAttachmentService {
private $rootFolder;
private $config;
private $attachmentMapper;
private $mimeTypeDetector;
public function __construct(
IL10N $l10n,
@@ -57,7 +57,8 @@ class FileService implements IAttachmentService {
ILogger $logger,
IRootFolder $rootFolder,
IConfig $config,
AttachmentMapper $attachmentMapper
AttachmentMapper $attachmentMapper,
IMimeTypeDetector $mimeTypeDetector
) {
$this->l10n = $l10n;
$this->appData = $appData;
@@ -66,6 +67,7 @@ class FileService implements IAttachmentService {
$this->rootFolder = $rootFolder;
$this->config = $config;
$this->attachmentMapper = $attachmentMapper;
$this->mimeTypeDetector = $mimeTypeDetector;
}
/**
@@ -225,27 +227,14 @@ class FileService implements IAttachmentService {
/**
* @param Attachment $attachment
* @return FileDisplayResponse|\OCP\AppFramework\Http\Response|StreamResponse
* @return StreamResponse
* @throws \Exception
*/
public function display(Attachment $attachment) {
$file = $this->getFileFromRootFolder($attachment);
if (method_exists($file, 'fopen')) {
$response = new StreamResponse($file->fopen('r'));
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
} else {
$response = new FileDisplayResponse($file);
}
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$response->setContentSecurityPolicy($policy);
$response->addHeader('Content-Type', $file->getMimeType());
$response = new StreamResponse($file->fopen('rb'));
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
return $response;
}

View File

@@ -26,10 +26,9 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Constants;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IDBConnection;
@@ -50,6 +49,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $l10n;
private $preview;
private $permissionService;
private $mimeTypeDetector;
public function __construct(
IRequest $request,
@@ -60,6 +60,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
DeckShareProvider $shareProvider,
IPreview $preview,
PermissionService $permissionService,
IMimeTypeDetector $mimeTypeDetector,
string $userId = null
) {
$this->request = $request;
@@ -70,6 +71,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$this->shareManager = $shareManager;
$this->userId = $userId;
$this->preview = $preview;
$this->mimeTypeDetector = $mimeTypeDetector;
}
public function listAttachments(int $cardId): array {
@@ -147,22 +149,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
if ($file === null || $share->getSharedWith() !== (string)$attachment->getCardId()) {
throw new NotFoundException('File not found');
}
if (method_exists($file, 'fopen')) {
$response = new StreamResponse($file->fopen('r'));
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
} else {
$response = new FileDisplayResponse($file);
}
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$response->setContentSecurityPolicy($policy);
$response->addHeader('Content-Type', $file->getMimeType());
$response = new StreamResponse($file->fopen('rb'));
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
return $response;
}
@@ -184,7 +174,6 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
throw new StatusException('Could not read file');
}
$target->putContent($content);
fclose($content);
$share = $this->shareManager->newShare();
$share->setNode($target);

252
src/CardCreateDialog.vue Normal file
View File

@@ -0,0 +1,252 @@
<!--
- @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<Modal class="card-selector" @close="close">
<div class="modal-scroller">
<div v-if="!creating && !created" id="modal-inner" :class="{ 'icon-loading': loading }">
<h3>{{ t('deck', 'Create a new card') }}</h3>
<Multiselect v-model="selectedBoard"
:placeholder="t('deck', 'Select a board')"
:options="boards"
:disabled="loading"
label="title"
class="multiselect-board"
@select="fetchCardsFromBoard">
<template slot="singleLabel" slot-scope="props">
<span>
<span :style="{ 'backgroundColor': '#' + props.option.color }" class="board-bullet" />
<span>{{ props.option.title }}</span>
</span>
</template>
<template slot="option" slot-scope="props">
<span>
<span :style="{ 'backgroundColor': '#' + props.option.color }" class="board-bullet" />
<span>{{ props.option.title }}</span>
</span>
</template>
</Multiselect>
<Multiselect v-model="selectedStack"
:placeholder="t('deck', 'Select a list')"
:options="stacksFromBoard"
:max-height="100"
:disabled="loading || !selectedBoard"
class="multiselect-list"
label="title" />
<input v-model="pendingTitle"
type="text"
:placeholder="t('deck', 'Card title')"
:disabled="loading || !selectedStack">
<textarea v-model="pendingDescription" :disabled="loading || !selectedStack" />
<div class="modal-buttons">
<button @click="close">
{{ t('deck', 'Cancel') }}
</button>
<button :disabled="loading || !isBoardAndStackChoosen"
class="primary"
@click="select">
{{ action }}
</button>
</div>
</div>
<div v-else id="modal-inner">
<EmptyContent v-if="creating" icon="icon-loading">
{{ t('deck', 'Creating the new card') }}
</EmptyContent>
<EmptyContent v-else-if="created" icon="icon-checkmark">
{{ t('deck', '"{card}" was added to "{board}"', { card: pendingTitle, board: selectedBoard.title }) }}
<template #desc>
<button class="primary" @click="openNewCard">
{{ t('deck', 'Open card') }}
</button>
<button @click="close">
{{ t('deck', 'Close') }}
</button>
</template>
</EmptyContent>
</div>
</div>
</Modal>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
import axios from '@nextcloud/axios'
import { CardApi } from './services/CardApi'
const cardApi = new CardApi()
export default {
name: 'CardCreateDialog',
components: {
EmptyContent,
Modal,
Multiselect,
},
props: {
title: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
action: {
type: String,
default: t('deck', 'Create card'),
},
},
data() {
return {
boards: [],
stacksFromBoard: [],
loading: true,
pendingTitle: '',
pendingDescription: '',
selectedStack: '',
selectedBoard: '',
creating: false,
created: false,
newCard: null,
}
},
computed: {
isBoardAndStackChoosen() {
return !(this.selectedBoard === '')
},
},
beforeMount() {
this.fetchBoards()
},
mounted() {
this.pendingTitle = this.title
this.pendingDescription = this.description
},
methods: {
fetchBoards() {
axios.get(generateUrl('/apps/deck/boards')).then((response) => {
this.boards = response.data
this.loading = false
})
},
async fetchCardsFromBoard(board) {
try {
this.cardsFromBoard = []
const url = generateUrl('/apps/deck/stacks/' + board.id)
const response = await axios.get(url)
response.data.forEach(stack => {
this.stacksFromBoard.push(stack)
})
} catch (err) {
return err
}
},
close() {
this.$root.$emit('close')
},
async select() {
this.creating = true
const response = await cardApi.addCard({
boardId: this.selectedBoard.id,
stackId: this.selectedStack.id,
title: this.pendingTitle,
description: this.pendingDescription,
})
this.newCard = response
this.creating = false
this.created = true
// We do not emit here since we want to give feedback to the user that the card was created
// this.$root.$emit('select', createdCard)
},
openNewCard() {
window.location = generateUrl('/apps/deck') + `#/board/${this.selectedBoard.id}/card/${this.newCard.id}`
},
},
}
</script>
<style lang="scss" scoped>
.modal-scroller {
overflow: scroll;
max-height: calc(80vh - 40px);
margin: 10px;
}
#modal-inner {
width: 90vw;
max-width: 400px;
padding: 10px;
min-height: 200px;
}
.multiselect-board, .multiselect-list, input, textarea {
width: 100%;
margin-bottom: 10px !important;
}
ul {
min-height: 100px;
}
li {
padding: 6px;
border: 1px solid transparent;
}
li:hover, li:focus {
background-color: var(--color-background-dark);
}
.board-bullet {
display: inline-block;
width: 12px;
height: 12px;
border: none;
border-radius: 50%;
cursor: pointer;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
}
.card-selector::v-deep .modal-container {
overflow: visible !important;
}
.empty-content {
margin-top: 5vh !important;
&::v-deep h2 {
margin-bottom: 5vh;
}
}
</style>

View File

@@ -10,7 +10,7 @@
:loading="isLoading || !!isSearching"
:disabled="isLoading"
track-by="multiselectKey"
:internal-search="true"
:internal-search="false"
@input="clickAddAcl"
@search-change="asyncFind">
<template #noOptions>
@@ -73,6 +73,7 @@ import { CollectionList } from 'nextcloud-vue-collections'
import { mapGetters, mapState } from 'vuex'
import { getCurrentUser } from '@nextcloud/auth'
import { showError } from '@nextcloud/dialogs'
import { debounce } from 'lodash'
export default {
name: 'SharingTabSidebar',
@@ -148,18 +149,13 @@ export default {
this.asyncFind('')
},
methods: {
debouncedFind: debounce(async function(query) {
this.isSearching = true
await this.$store.dispatch('loadSharees', query)
this.isSearching = false
}, 300),
async asyncFind(query) {
// manual debounce to handle async searching more easily and have more control over the loading state
const timestamp = (new Date()).getTime()
if (!this.isSearching || timestamp > this.isSearching + 300) {
this.isSearching = timestamp
await this.$store.dispatch('loadSharees', query)
// only reset searching flag if the most recent search finished
if (this.isSearching === timestamp) {
this.isSearching = false
}
}
await this.debouncedFind(query)
},
async clickAddAcl() {
this.addAclForAPI = {

View File

@@ -117,7 +117,7 @@
type="deck-card" />
</div>
<Description :key="card.id" :card="card" />
<Description :key="card.id" :card="card" @change="descriptionChanged" />
</div>
</template>
@@ -234,6 +234,9 @@ export default {
this.initialize()
},
methods: {
descriptionChanged(newDesc) {
this.copiedCard.description = newDesc
},
async initialize() {
if (!this.card) {
return
@@ -253,9 +256,6 @@ export default {
}
},
setDue() {
this.$store.dispatch('updateCardDue', this.copiedCard)
},
removeDue() {
this.copiedCard.duedate = null
this.$store.dispatch('updateCardDue', this.copiedCard)

View File

@@ -218,6 +218,7 @@ export default {
await this.$store.dispatch('updateCardDesc', { ...this.card, description: this.description })
this.descriptionLastEdit = 0
this.descriptionSaving = false
this.$emit('change', this.description)
},
updateDescription() {
this.descriptionLastEdit = Date.now()

View File

@@ -235,9 +235,7 @@ export default {
try {
const newBoard = await this.$store.dispatch('cloneBoard', this.board)
this.loading = false
const route = this.routeTo
route.params.id = newBoard.id
this.$router.push(route)
this.$router.push({ name: 'board', params: { id: newBoard.id } })
} catch (e) {
OC.Notification.showTemporary(t('deck', 'An error occurred'))
console.error(e)
@@ -278,9 +276,7 @@ export default {
)
},
actionDetails() {
const route = this.routeTo
route.name = 'board.details'
this.$router.push(route)
this.$router.push({ name: 'board.details', params: { id: this.board.id } })
},
applyEdit(e) {
this.editing = false
@@ -298,11 +294,6 @@ export default {
cancelEdit(e) {
this.editing = false
},
showSidebar() {
const route = this.routeTo
route.name = 'board.details'
this.$router.push(route)
},
async updateSetting(key, value) {
this.updateDueSetting = value
const setting = {}

47
src/helpers/selector.js Normal file
View File

@@ -0,0 +1,47 @@
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
const buildSelector = (selector, propsData = {}) => {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
document.getElementById('body-user').append(container)
const View = Vue.extend(selector)
const ComponentVM = new View({
propsData,
}).$mount(container)
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove()
ComponentVM.$destroy()
reject(new Error('Selection canceled'))
})
ComponentVM.$root.$on('select', (id) => {
ComponentVM.$el.remove()
ComponentVM.$destroy()
resolve(id)
})
})
}
export {
buildSelector,
}

View File

@@ -26,6 +26,8 @@ import BoardSelector from './BoardSelector'
import CardSelector from './CardSelector'
import './../css/collections.css'
import FileSharingPicker from './views/FileSharingPicker'
import { buildSelector } from './helpers/selector'
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line
@@ -41,61 +43,16 @@ window.addEventListener('DOMContentLoaded', () => {
} else {
console.error('OCA.Sharing.ShareSearch not ready')
}
});
((function(OCP) {
OCP.Collaboration.registerType('deck', {
action: () => {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
container.id = 'deck-board-select'
const body = document.getElementById('body-user')
body.append(container)
const ComponentVM = new Vue({
render: h => h(BoardSelector),
})
ComponentVM.$mount(container)
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove()
ComponentVM.$destroy()
reject(new Error('Board selection canceled'))
})
ComponentVM.$root.$on('select', (id) => {
resolve(id)
ComponentVM.$el.remove()
ComponentVM.$destroy()
})
})
},
window.OCP.Collaboration.registerType('deck', {
action: () => buildSelector(BoardSelector),
typeString: t('deck', 'Link to a board'),
typeIconClass: 'icon-deck',
})
OCP.Collaboration.registerType('deck-card', {
action: () => {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
container.id = 'deck-board-select'
const body = document.getElementById('body-user')
body.append(container)
const ComponentVM = new Vue({
render: h => h(CardSelector),
})
ComponentVM.$mount(container)
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove()
ComponentVM.$destroy()
reject(new Error('Card selection canceled'))
})
ComponentVM.$root.$on('select', (id) => {
resolve(id)
ComponentVM.$el.remove()
ComponentVM.$destroy()
})
})
},
window.OCP.Collaboration.registerType('deck-card', {
action: () => buildSelector(CardSelector),
typeString: t('deck', 'Link to a card'),
typeIconClass: 'icon-deck',
})
})(window.OCP))
})

62
src/init-talk.js Normal file
View File

@@ -0,0 +1,62 @@
/*
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import { generateUrl } from '@nextcloud/router'
import CardCreateDialog from './CardCreateDialog'
import { buildSelector } from './helpers/selector'
import './init-collections'
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('deck', 'js/');
Vue.prototype.t = t
Vue.prototype.n = n
Vue.prototype.OC = OC
window.addEventListener('DOMContentLoaded', () => {
if (!window.OCA?.Talk?.registerMessageAction) {
return
}
window.OCA.Talk.registerMessageAction({
label: t('deck', 'Create a card'),
icon: 'icon-deck',
async callback({ message: { message, actorDisplayName }, metadata: { name: conversationName, token: conversationToken } }) {
const shortenedMessageCandidate = message.replace(/^(.{255}[^\s]*).*/, '$1')
const shortenedMessage = shortenedMessageCandidate === '' ? message.substr(0, 255) : shortenedMessageCandidate
try {
await buildSelector(CardCreateDialog, {
title: shortenedMessage,
description: message + '\n\n' + '['
+ t('deck', 'Message from {author} in {conversationName}', { author: actorDisplayName, conversationName })
+ '](' + generateUrl('/call/' + conversationToken) + ')',
})
} catch (e) {
console.debug('Card creation dialog was canceled')
}
},
})
})

View File

@@ -416,7 +416,7 @@ export default new Vuex.Store({
params.append('search', query)
params.append('format', 'json')
params.append('perPage', 20)
params.append('itemType', [0, 1, 7])
params.append('itemType', [0, 1, 4, 7])
const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
commit('setSharees', response.data.ocs.data)

View File

@@ -25,10 +25,10 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\Folder;
use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
@@ -57,6 +57,8 @@ class FileServiceTest extends TestCase {
private $config;
/** @var AttachmentMapper|MockObject */
private $attachmentMapper;
/** @var IMimeTypeDetector|MockObject */
private $mimeTypeDetector;
public function setUp(): void {
parent::setUp();
@@ -67,7 +69,8 @@ class FileServiceTest extends TestCase {
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->config = $this->createMock(IConfig::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper);
$this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class);
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper, $this->mimeTypeDetector);
}
public function mockGetFolder($cardId) {
@@ -268,51 +271,13 @@ class FileServiceTest extends TestCase {
$file->expects($this->any())
->method('fopen')
->willReturn('fileresource');
$this->mimeTypeDetector->expects($this->once())
->method('getSecureMimeType')
->willReturn('image/jpeg');
$actual = $this->fileService->display($attachment);
$expected = new StreamResponse('fileresource');
$expected->addHeader('Content-Type', 'image/jpeg');
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$expected->setContentSecurityPolicy($policy);
$this->assertEquals($expected, $actual);
}
public function testDisplayPdf() {
$this->config->expects($this->once())
->method('getSystemValue')
->willReturn('123');
$appDataFolder = $this->createMock(Folder::class);
$deckAppDataFolder = $this->createMock(Folder::class);
$cardFolder = $this->createMock(Folder::class);
$this->rootFolder->expects($this->once())->method('get')->willReturn($appDataFolder);
$appDataFolder->expects($this->once())->method('get')->willReturn($deckAppDataFolder);
$deckAppDataFolder->expects($this->once())->method('get')->willReturn($cardFolder);
$attachment = $this->getAttachment();
$file = $this->createMock(\OCP\Files\File::class);
$cardFolder->expects($this->once())->method('get')->willReturn($file);
$file->expects($this->any())
->method('getMimeType')
->willReturn('application/pdf');
$file->expects($this->any())
->method('getName')
->willReturn('file1');
$file->expects($this->any())
->method('fopen')
->willReturn('fileresource');
$actual = $this->fileService->display($attachment);
$expected = new StreamResponse('fileresource');
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
$expected->addHeader('Content-Type', 'application/pdf');
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$expected->setContentSecurityPolicy($policy);
$expected->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$this->assertEquals($expected, $actual);
}

View File

@@ -7,6 +7,7 @@ const config = {
collections: path.join(__dirname, 'src', 'init-collections.js'),
dashboard: path.join(__dirname, 'src', 'init-dashboard.js'),
calendar: path.join(__dirname, 'src', 'init-calendar.js'),
talk: path.join(__dirname, 'src', 'init-talk.js'),
},
output: {
filename: '[name].js',