Compare commits

...

16 Commits

Author SHA1 Message Date
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
17 changed files with 442 additions and 161 deletions

View File

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

View File

@@ -1,24 +1,37 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 1.3.0 - unreleased ## 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 ### Added
* [#2638](https://github.com/nextcloud/deck/pull/2638) Sharing files to cards * [#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 * [#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 * Nextcloud 21 compatiblity
### Fixed ### Fixed
* [#2622](https://github.com/nextcloud/deck/pull/2622) Fix gradient and stack header spacing for safari * [#2712](https://github.com/nextcloud/deck/pull/2712) Docs: Fix table in section "GET /api/v1.0/config" @das-g
* [#2626](https://github.com/nextcloud/deck/pull/2626) Adding a description icon to cards when they contain a description without any checkmarks @MonkeySon * [#2716](https://github.com/nextcloud/deck/pull/2716) Remove repair step which is no longer needed as we cleanup properly
* [#2659](https://github.com/nextcloud/deck/pull/2659) Matching color of description cursor with text color @JonFStr * [#2723](https://github.com/nextcloud/deck/pull/2723) Pad random color with leading zeroes @PVince81
* [#2676](https://github.com/nextcloud/deck/pull/2676) Only load filter view when shown * [#2729](https://github.com/nextcloud/deck/pull/2729) Remove invalid activity parameters @nickvergessen
* [#2680](https://github.com/nextcloud/deck/pull/2680) Do not try to add change data if it doesn't exist * [#2750](https://github.com/nextcloud/deck/pull/2750) Fix deck activity emails not being translated @nickvergessen
* [#2681](https://github.com/nextcloud/deck/pull/2681) Filter out deleted stacks from results * [#2751](https://github.com/nextcloud/deck/pull/2751) Properly set author for activity events that are triggered by cron
* [#2685](https://github.com/nextcloud/deck/pull/2685) Show all boards in move card dialog @jakobroehrl * [#2796](https://github.com/nextcloud/deck/pull/2796) Install all needed php extensions
* [#2687](https://github.com/nextcloud/deck/pull/2687) 3dots no opacity @jakobroehrl * [#2817](https://github.com/nextcloud/deck/pull/2817) Fix issues when creating a card from a talk message
* [#2688](https://github.com/nextcloud/deck/pull/2688) Title > boardname @jakobroehrl
* [#2689](https://github.com/nextcloud/deck/pull/2689) Modal > bigger view wording @jakobroehrl
## 1.3.0-beta2 ## 1.3.0-beta2

View File

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

View File

@@ -197,6 +197,10 @@ class Application20 extends App implements IBootstrap {
$resourceManager->registerResourceProvider(ResourceProviderCard::class); $resourceManager->registerResourceProvider(ResourceProviderCard::class);
$symfonyAdapter->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () { $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'); Util::addScript('deck', 'collections');
}); });
} }

View File

@@ -78,8 +78,8 @@ class CardController extends Controller {
* @param int $order * @param int $order
* @return \OCP\AppFramework\Db\Entity * @return \OCP\AppFramework\Db\Entity
*/ */
public function create($title, $stackId, $type = 'plain', $order = 999) { public function create($title, $stackId, $type = 'plain', $order = 999, string $description = '') {
return $this->cardService->create($title, $stackId, $type, $order, $this->userId); 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) { $circles = array_map(function ($circle) {
return $circle->getUniqueId(); return $circle->getUniqueId();
}, \OCA\Circles\Api\v1\Circles::joinedCircles('', true)); }, \OCA\Circles\Api\v1\Circles::joinedCircles($userId, true));
if (count($circles) === 0) { if (count($circles) === 0) {
return []; return [];
} }

View File

@@ -49,8 +49,13 @@ class BeforeTemplateRenderedListener implements IEventListener {
} }
Util::addStyle('deck', 'deck'); 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'); 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\Db\AttachmentMapper;
use OCA\Deck\StatusException; use OCA\Deck\StatusException;
use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\Exceptions\ConflictException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse; use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\IAppData; use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException; use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException; use OCP\Files\NotPermittedException;
@@ -49,6 +48,7 @@ class FileService implements IAttachmentService {
private $rootFolder; private $rootFolder;
private $config; private $config;
private $attachmentMapper; private $attachmentMapper;
private $mimeTypeDetector;
public function __construct( public function __construct(
IL10N $l10n, IL10N $l10n,
@@ -57,7 +57,8 @@ class FileService implements IAttachmentService {
ILogger $logger, ILogger $logger,
IRootFolder $rootFolder, IRootFolder $rootFolder,
IConfig $config, IConfig $config,
AttachmentMapper $attachmentMapper AttachmentMapper $attachmentMapper,
IMimeTypeDetector $mimeTypeDetector
) { ) {
$this->l10n = $l10n; $this->l10n = $l10n;
$this->appData = $appData; $this->appData = $appData;
@@ -66,6 +67,7 @@ class FileService implements IAttachmentService {
$this->rootFolder = $rootFolder; $this->rootFolder = $rootFolder;
$this->config = $config; $this->config = $config;
$this->attachmentMapper = $attachmentMapper; $this->attachmentMapper = $attachmentMapper;
$this->mimeTypeDetector = $mimeTypeDetector;
} }
/** /**
@@ -225,27 +227,14 @@ class FileService implements IAttachmentService {
/** /**
* @param Attachment $attachment * @param Attachment $attachment
* @return FileDisplayResponse|\OCP\AppFramework\Http\Response|StreamResponse * @return StreamResponse
* @throws \Exception * @throws \Exception
*/ */
public function display(Attachment $attachment) { public function display(Attachment $attachment) {
$file = $this->getFileFromRootFolder($attachment); $file = $this->getFileFromRootFolder($attachment);
if (method_exists($file, 'fopen')) { $response = new StreamResponse($file->fopen('rb'));
$response = new StreamResponse($file->fopen('r')); $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"'); $response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
} 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());
return $response; return $response;
} }

View File

@@ -26,10 +26,9 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Attachment; use OCA\Deck\Db\Attachment;
use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\StatusException; use OCA\Deck\StatusException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse; use OCP\AppFramework\Http\StreamResponse;
use OCP\Constants; use OCP\Constants;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException; use OCP\Files\NotFoundException;
use OCP\IDBConnection; use OCP\IDBConnection;
@@ -50,6 +49,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $l10n; private $l10n;
private $preview; private $preview;
private $permissionService; private $permissionService;
private $mimeTypeDetector;
public function __construct( public function __construct(
IRequest $request, IRequest $request,
@@ -60,6 +60,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
DeckShareProvider $shareProvider, DeckShareProvider $shareProvider,
IPreview $preview, IPreview $preview,
PermissionService $permissionService, PermissionService $permissionService,
IMimeTypeDetector $mimeTypeDetector,
string $userId = null string $userId = null
) { ) {
$this->request = $request; $this->request = $request;
@@ -70,6 +71,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$this->shareManager = $shareManager; $this->shareManager = $shareManager;
$this->userId = $userId; $this->userId = $userId;
$this->preview = $preview; $this->preview = $preview;
$this->mimeTypeDetector = $mimeTypeDetector;
} }
public function listAttachments(int $cardId): array { public function listAttachments(int $cardId): array {
@@ -147,22 +149,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
if ($file === null || $share->getSharedWith() !== (string)$attachment->getCardId()) { if ($file === null || $share->getSharedWith() !== (string)$attachment->getCardId()) {
throw new NotFoundException('File not found'); 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; return $response;
} }

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

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 CardSelector from './CardSelector'
import './../css/collections.css' import './../css/collections.css'
import FileSharingPicker from './views/FileSharingPicker' import FileSharingPicker from './views/FileSharingPicker'
import { buildSelector } from './helpers/selector'
// eslint-disable-next-line // eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken); __webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line // eslint-disable-next-line
@@ -41,61 +43,16 @@ window.addEventListener('DOMContentLoaded', () => {
} else { } else {
console.error('OCA.Sharing.ShareSearch not ready') console.error('OCA.Sharing.ShareSearch not ready')
} }
});
((function(OCP) { window.OCP.Collaboration.registerType('deck', {
action: () => buildSelector(BoardSelector),
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()
})
})
},
typeString: t('deck', 'Link to a board'), typeString: t('deck', 'Link to a board'),
typeIconClass: 'icon-deck', typeIconClass: 'icon-deck',
}) })
OCP.Collaboration.registerType('deck-card', { window.OCP.Collaboration.registerType('deck-card', {
action: () => { action: () => buildSelector(CardSelector),
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()
})
})
},
typeString: t('deck', 'Link to a card'), typeString: t('deck', 'Link to a card'),
typeIconClass: 'icon-deck', 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('search', query)
params.append('format', 'json') params.append('format', 'json')
params.append('perPage', 20) 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 }) const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
commit('setSharees', response.data.ocs.data) 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\Attachment;
use OCA\Deck\Db\AttachmentMapper; use OCA\Deck\Db\AttachmentMapper;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\StreamResponse; use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\Folder; use OCP\Files\Folder;
use OCP\Files\IAppData; use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder; use OCP\Files\SimpleFS\ISimpleFolder;
@@ -57,6 +57,8 @@ class FileServiceTest extends TestCase {
private $config; private $config;
/** @var AttachmentMapper|MockObject */ /** @var AttachmentMapper|MockObject */
private $attachmentMapper; private $attachmentMapper;
/** @var IMimeTypeDetector|MockObject */
private $mimeTypeDetector;
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
@@ -67,7 +69,8 @@ class FileServiceTest extends TestCase {
$this->rootFolder = $this->createMock(IRootFolder::class); $this->rootFolder = $this->createMock(IRootFolder::class);
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::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) { public function mockGetFolder($cardId) {
@@ -268,51 +271,13 @@ class FileServiceTest extends TestCase {
$file->expects($this->any()) $file->expects($this->any())
->method('fopen') ->method('fopen')
->willReturn('fileresource'); ->willReturn('fileresource');
$this->mimeTypeDetector->expects($this->once())
->method('getSecureMimeType')
->willReturn('image/jpeg');
$actual = $this->fileService->display($attachment); $actual = $this->fileService->display($attachment);
$expected = new StreamResponse('fileresource'); $expected = new StreamResponse('fileresource');
$expected->addHeader('Content-Type', 'image/jpeg'); $expected->addHeader('Content-Type', 'image/jpeg');
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"'); $expected->addHeader('Content-Disposition', 'attachment; 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);
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
} }

View File

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