diff options
author | Christoph Wurst <ChristophWurst@users.noreply.github.com> | 2021-05-28 23:50:44 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-28 23:50:44 +0300 |
commit | ea38d17e40d97686695accca2ee0a0fdca76ccba (patch) | |
tree | c45677185f146463f644ac77a5a20434a822655f | |
parent | 0d8c31751b5fed084511bdd04ee25ee5b9f0fae4 (diff) | |
parent | 6c834c1540d8c02d384121b3c1e653ba180ea590 (diff) |
Merge pull request #5083 from nextcloud/enhanc/new-personalized-tagsv1.10.0-alpha.4
Add new personalized tags
-rw-r--r-- | appinfo/routes.php | 10 | ||||
-rw-r--r-- | lib/Controller/TagsController.php | 140 | ||||
-rw-r--r-- | lib/Db/TagMapper.php | 6 | ||||
-rw-r--r-- | lib/Model/IMAPMessage.php | 52 | ||||
-rw-r--r-- | src/components/Envelope.vue | 1 | ||||
-rw-r--r-- | src/components/TagModal.vue | 98 | ||||
-rw-r--r-- | src/service/MessageService.js | 6 | ||||
-rw-r--r-- | src/store/actions.js | 6 |
8 files changed, 288 insertions, 31 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 7a1d14649..bf642752e 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -100,6 +100,16 @@ return [ 'verb' => 'GET' ], [ + 'name' => 'tags#create', + 'url' => '/api/tags', + 'verb' => 'POST' + ], + [ + 'name' => 'tags#update', + 'url' => '/api/tags/{id}', + 'verb' => 'PUT' + ], + [ 'name' => 'aliases#updateSignature', 'url' => '/api/accounts/{accountId}/aliases/{id}/signature', 'verb' => 'PUT' diff --git a/lib/Controller/TagsController.php b/lib/Controller/TagsController.php new file mode 100644 index 000000000..feccf38ed --- /dev/null +++ b/lib/Controller/TagsController.php @@ -0,0 +1,140 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Controller; + +use OCA\Mail\AppInfo\Application; +use OCA\Mail\Db\Tag; +use OCA\Mail\Db\TagMapper; +use OCA\Mail\Exception\ClientException; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +class TagsController extends Controller { + + /** @var string */ + private $currentUserId; + + /** @var TagMapper */ + private $tagMapper; + + /** + * TagsController constructor. + * + * @param IRequest $request + * @param $UserId + * @param TagMapper $tagMapper + */ + public function __construct(IRequest $request, + $UserId, + TagMapper $tagMapper + ) { + parent::__construct(Application::APP_ID, $request); + $this->currentUserId = $UserId; + $this->tagMapper = $tagMapper; + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param string $displayName + * @param string $color + * + * @return JSONResponse + * @throws ClientException + */ + public function create(string $displayName, string $color): JSONResponse { + $this->validateDisplayName($displayName); + $this->validateColor($color); + + $imapLabel = str_replace(' ', '_', $displayName); + /** @var string|false $imapLabel */ + $imapLabel = mb_convert_encoding($imapLabel, 'UTF7-IMAP', 'UTF-8'); + if ($imapLabel === false) { + throw new ClientException('Error converting display name to UTF7-IMAP ', 0); + } + $imapLabel = mb_strcut($imapLabel, 0, 64); + + try { + return new JSONResponse($this->tagMapper->getTagByImapLabel($imapLabel, $this->currentUserId)); + } catch (DoesNotExistException $e) { + // it's valid that a tag does not exist. + } + + $tag = new Tag(); + $tag->setUserId($this->currentUserId); + $tag->setDisplayName($displayName); + $tag->setImapLabel($imapLabel); + $tag->setColor($color); + $tag->setIsDefaultTag(false); + + $tag = $this->tagMapper->insert($tag); + return new JSONResponse($tag); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id + * @param string $displayName + * @param string $color + * + * @return JSONResponse + * @throws ClientException + * @throws DoesNotExistException + */ + public function update(int $id, string $displayName, string $color): JSONResponse { + $this->validateDisplayName($displayName); + $this->validateColor($color); + + $tag = $this->tagMapper->getTagForUser($id, $this->currentUserId); + + $tag->setDisplayName($displayName); + $tag->setColor($color); + + $tag = $this->tagMapper->update($tag); + return new JSONResponse($tag); + } + + /** + * @throws ClientException + */ + private function validateDisplayName(string $displayName): void { + if (mb_strlen($displayName) > 128) { + throw new ClientException('The maximum length for displayName is 128'); + } + } + + /** + * @throws ClientException + */ + private function validateColor(string $color): void { + if (mb_strlen($color) > 9) { + throw new ClientException('The maximum length for color is 9'); + } + } +} diff --git a/lib/Db/TagMapper.php b/lib/Db/TagMapper.php index 3c626b9e3..43794c112 100644 --- a/lib/Db/TagMapper.php +++ b/lib/Db/TagMapper.php @@ -28,7 +28,6 @@ namespace OCA\Mail\Db; use function array_map; use function array_chunk; use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -51,7 +50,7 @@ class TagMapper extends QBMapper { /** * @throws DoesNotExistException */ - public function getTagByImapLabel(string $imapLabel, string $userId): Entity { + public function getTagByImapLabel(string $imapLabel, string $userId): Tag { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) @@ -65,7 +64,7 @@ class TagMapper extends QBMapper { /** * @throws DoesNotExistException */ - public function getTagForUser(int $id, string $userId): Entity { + public function getTagForUser(int $id, string $userId): Tag { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) @@ -78,7 +77,6 @@ class TagMapper extends QBMapper { /** * @return Tag[] - * @throws DoesNotExistException */ public function getAllTagsForUser(string $userId): array { $qb = $this->db->getQueryBuilder(); diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php index e1393d8ac..c163ebd0c 100644 --- a/lib/Model/IMAPMessage.php +++ b/lib/Model/IMAPMessage.php @@ -29,33 +29,33 @@ declare(strict_types=1); namespace OCA\Mail\Model; -use OC; use Exception; -use function trim; -use OCP\Files\File; -use Horde_Mime_Part; -use OCA\Mail\Db\Tag; -use JsonSerializable; -use function in_array; use Horde_Imap_Client; -use Horde_Mime_Headers; -use OCA\Mail\Db\Message; -use OCA\Mail\AddressList; +use Horde_Imap_Client_Data_Envelope; +use Horde_Imap_Client_Data_Fetch; +use Horde_Imap_Client_DateTime; +use Horde_Imap_Client_Fetch_Query; use Horde_Imap_Client_Ids; -use OCA\Mail\Service\Html; -use OCA\Mail\Db\MailAccount; -use Horde_Imap_Client_Socket; use Horde_Imap_Client_Mailbox; -use Horde_Imap_Client_DateTime; +use Horde_Imap_Client_Socket; +use Horde_Mime_Headers; +use Horde_Mime_Headers_MessageId; +use Horde_Mime_Part; +use JsonSerializable; +use OC; +use OCA\Mail\AddressList; use OCA\Mail\Db\LocalAttachment; +use OCA\Mail\Db\MailAccount; +use OCA\Mail\Db\Message; +use OCA\Mail\Db\Tag; +use OCA\Mail\Service\Html; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\Files\File; +use OCP\Files\SimpleFS\ISimpleFile; +use function in_array; use function mb_convert_encoding; use function mb_strcut; -use Horde_Imap_Client_Data_Fetch; -use Horde_Mime_Headers_MessageId; -use Horde_Imap_Client_Fetch_Query; -use OCP\Files\SimpleFS\ISimpleFile; -use Horde_Imap_Client_Data_Envelope; -use OCP\AppFramework\Db\DoesNotExistException; +use function trim; class IMAPMessage implements IMessage, JsonSerializable { use ConvertAddresses; @@ -769,9 +769,19 @@ class IMAPMessage implements IMessage, JsonSerializable { if ($keyword === '$important') { $keyword = Tag::LABEL_IMPORTANT; } + + $displayName = str_replace('_', ' ', $keyword); + $displayName = strtoupper($displayName); + $displayName = mb_convert_encoding($displayName, 'UTF-8', 'UTF7-IMAP'); + $displayName = strtolower($displayName); + $displayName = ucwords($displayName); + + $keyword = mb_strcut($keyword, 0, 64); + $displayName = mb_strcut($displayName, 0, 128); + $tag = new Tag(); $tag->setImapLabel($keyword); - $tag->setDisplayName(str_replace('$', '', $keyword)); + $tag->setDisplayName($displayName); $tag->setUserId($userId); $t[] = $tag; } diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index 439d41e29..265c9b236 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -49,7 +49,6 @@ </div> </template> <template #subtitle> - <span v-if="data.flags.answered" class="icon-reply" /> <span v-if="data.flags.hasAttachments === true" class="icon-public icon-attachment" /> <span v-if="draft" class="draft"> <em>{{ t('mail', 'Draft: ') }}</em> diff --git a/src/components/TagModal.vue b/src/components/TagModal.vue index 00f885e80..a2989cb0e 100644 --- a/src/components/TagModal.vue +++ b/src/components/TagModal.vue @@ -23,7 +23,7 @@ <Modal size="large" @close="onClose"> <div class="modal-content"> <h2 class="tag-title"> - {{ t('mail', 'Add tags') }} + {{ t('mail', 'Add default tags') }} </h2> <div v-for="tag in tags" :key="tag.id" class="tag-group"> <div class="tag-group__bg button" @@ -42,20 +42,49 @@ {{ t('mail','Remove') }} </button> </div> + <h2 class="tag-title"> + {{ t('mail', 'Add tag') }} + </h2> + <div class="create-tag"> + <button v-if="!editing" + class="tagButton" + @click="addTagInput"> + {{ t('mail', 'Add tag') }} + </button> + <ActionInput v-if="editing" icon="icon-tag" @submit="createTag" /> + <ActionText + v-if="showSaving" + icon="icon-loading-small"> + {{ t('mail', 'Saving tag …') }} + </ActionText> + </div> </div> </Modal> </template> <script> import Modal from '@nextcloud/vue/dist/Components/Modal' +import ActionText from '@nextcloud/vue/dist/Components/ActionText' +import ActionInput from '@nextcloud/vue/dist/Components/ActionInput' +import { showError } from '@nextcloud/dialogs' + +function randomColor() { + let randomHexColor = ((1 << 24) * Math.random() | 0).toString(16) + while (randomHexColor.length < 6) { + randomHexColor = '0' + randomHexColor + } + return '#' + randomHexColor +} export default { name: 'TagModal', components: { Modal, + ActionText, + ActionInput, }, props: { envelope: { - // The envelope on which this menu will act + // The envelope on which this menu will act required: true, type: Object, }, @@ -63,6 +92,11 @@ export default { data() { return { isAdded: false, + editing: false, + tagLabel: true, + tagInput: false, + showSaving: false, + color: randomColor(), } }, computed: { @@ -85,14 +119,33 @@ export default { this.isAdded = false this.$store.dispatch('removeEnvelopeTag', { envelope: this.envelope, imapLabel }) }, + addTagInput() { + this.editing = true + this.showSaving = false + }, + async createTag(event) { + this.editing = true + const displayName = event.target.querySelector('input[type=text]').value + + try { + await this.$store.dispatch('createTag', { + displayName, + color: randomColor(displayName), + }) + } catch (error) { + console.debug(error) + showError(this.t('mail', 'An error occurred, unable to create the tag.')) + } finally { + this.showSaving = false + this.tagLabel = true + } + }, }, } + </script> <style lang="scss" scoped> -::v-deep .modal-wrapper .modal-container { - overflow: scroll !important; -} ::v-deep .modal-content { padding-left: 20px; padding-right: 20px; @@ -134,4 +187,39 @@ export default { font-weight: bold; margin: 8px 24px; } +.app-navigation-entry-bullet-wrapper { + width: 44px; + height: 44px; + display: inline-block; + position: absolute; + list-style: none; + + .color0 { + width: 30px !important; + height: 30px; + border-radius: 50%; + background-size: 14px; + margin-top: -65px; + margin-left: 180px; + z-index: 2; + display: flex; + position: relative; + } +} +.icon-colorpicker { + background-image: var(--icon-add-fff); +} +.tagButton { + display: inline-block; + margin-left: 10px; +} +.action { + list-style: none; +} +::v-deep .action-input { + margin-left: -31px; +} +::v-deep .icon-tag { + background-image: none; +} </style> diff --git a/src/service/MessageService.js b/src/service/MessageService.js index 6e51aa8f0..3c671c2e7 100644 --- a/src/service/MessageService.js +++ b/src/service/MessageService.js @@ -116,6 +116,12 @@ export function setEnvelopeFlag(id, flag, value) { }, }) } +export async function createEnvelopeTag(displayName, color) { + const url = generateUrl('/apps/mail/api/tags') + + const { data } = await axios.post(url, { displayName, color }) + return data +} export async function setEnvelopeTag(id, imapLabel) { const url = generateUrl('/apps/mail/api/messages/{id}/tags/{imapLabel}', { diff --git a/src/store/actions.js b/src/store/actions.js index 8deae447e..ad223710d 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -58,6 +58,7 @@ import { patchMailbox, } from '../service/MailboxService' import { + createEnvelopeTag, deleteMessage, fetchEnvelope, fetchEnvelopes, @@ -777,6 +778,11 @@ export default { throw error } }, + async createTag({ commit }, { displayName, color }) { + const tag = await createEnvelopeTag(displayName, color) + commit('addTag', { tag }) + + }, async addEnvelopeTag({ commit, getters }, { envelope, imapLabel }) { // TODO: fetch tags indepently of envelopes and only send tag id here const tag = await setEnvelopeTag(envelope.databaseId, imapLabel) |