Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Augment the category menu by system tags and already used categories #5161

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
namespace OCA\Calendar\Controller;

use OCA\Calendar\Service\Appointments\AppointmentConfigService;
use OCA\Calendar\Service\CategoriesService;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\FileDisplayResponse;
Expand All @@ -44,6 +45,9 @@ class ViewController extends Controller {
/** @var AppointmentConfigService */
private $appointmentConfigService;

/** @var CategoriesService */
private $categoriesService;

/** @var IInitialState */
private $initialStateService;

Expand All @@ -59,13 +63,15 @@ public function __construct(string $appName,
IRequest $request,
IConfig $config,
AppointmentConfigService $appointmentConfigService,
CategoriesService $categoriesService,
IInitialState $initialStateService,
IAppManager $appManager,
?string $userId,
IAppData $appData) {
parent::__construct($appName, $request);
$this->config = $config;
$this->appointmentConfigService = $appointmentConfigService;
$this->categoriesService = $categoriesService;
$this->initialStateService = $initialStateService;
$this->appManager = $appManager;
$this->userId = $userId;
Expand Down Expand Up @@ -135,6 +141,7 @@ public function index():TemplateResponse {
$this->initialStateService->provideInitialState('appointmentConfigs', $this->appointmentConfigService->getAllAppointmentConfigurations($this->userId));
$this->initialStateService->provideInitialState('disable_appointments', $disableAppointments);
$this->initialStateService->provideInitialState('can_subscribe_link', $canSubscribeLink);
$this->initialStateService->provideInitialState('categories', $this->categoriesService->getCategories($this->userId));

return new TemplateResponse($this->appName, 'main');
}
Expand Down
144 changes: 144 additions & 0 deletions lib/Service/CategoriesService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

declare(strict_types=1);

/**
* Calendar App
*
* @copyright 2023 Claus-Justus Heine <himself@claus-justus-heine.de>
*
* @author Claus-Justus Heine <himself@claus-justus-heine.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Calendar\Service;

use OCP\Calendar\ICalendarQuery;
use OCP\Calendar\IManager as ICalendarManager;
use OCP\IL10N;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;

/**
* @psalm-type Category = array{label: string, value: string}
* @psalm-type CategoryGroup = array{group: string, options: array<int, Category>}
*/
class CategoriesService {
/** @var ICalendarManager */
private $calendarManager;

/** @var ISystemTagManager */
private $systemTagManager;

/** @var IL10N */
private $l;

public function __construct(ICalendarManager $calendarManager,
ISystemTagManager $systemTagManager,
IL10N $l10n) {
$this->calendarManager = $calendarManager;
$this->systemTagManager = $systemTagManager;
$this->l = $l10n;
}

/**
* This is a simplistic brute-force extraction of all already used
* categories from all events accessible to the given user.
*
* @return array
*/
private function getUsedCategories(string $userId): array {
$categories = [];
$principalUri = 'principals/users/' . $userId;
$query = $this->calendarManager->newQuery($principalUri);
$query->addSearchProperty(ICalendarQuery::SEARCH_PROPERTY_CATEGORIES);
$calendarObjects = $this->calendarManager->searchForPrincipal($query);
foreach ($calendarObjects as $objectInfo) {
foreach ($objectInfo['objects'] as $calendarObject) {
if (isset($calendarObject['CATEGORIES'])) {
$categories[] = explode(',', $calendarObject['CATEGORIES'][0][0]);
}
}
}

// Avoid injecting "broken" categories into the UI (avoid empty
// categories and categories surrounded by spaces)
$categories = array_filter(array_map(fn ($label) => trim($label), array_unique(array_merge(...$categories))));

return $categories;
}

/**
* Return a grouped array with all previously used categories, all system
* tags and all categories found in the iCalendar RFC.
*
* @return CategoryGroup[]
*/
public function getCategories(string $userId): array {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I have moved the userId from an injectable to a parameter because that makes the service usable without a user context, e.g. if we make use of it in a background job or CLI task one day

$systemTags = $this->systemTagManager->getAllTags(true);

$systemTagCategoryLabels = [];
/** @var ISystemTag $systemTag */
foreach ($systemTags as $systemTag) {
if (!$systemTag->isUserAssignable() || !$systemTag->isUserVisible()) {
continue;
}
$systemTagCategoryLabels[] = $systemTag->getName();
}
sort($systemTagCategoryLabels);
$systemTagCategoryLabels = array_values(array_filter(array_unique($systemTagCategoryLabels)));

$rfcCategoryLabels = [
$this->l->t('Anniversary'),
$this->l->t('Appointment'),
$this->l->t('Business'),
$this->l->t('Education'),
$this->l->t('Holiday'),
$this->l->t('Meeting'),
$this->l->t('Miscellaneous'),
$this->l->t('Non-working hours'),
$this->l->t('Not in office'),
$this->l->t('Personal'),
$this->l->t('Phone call'),
$this->l->t('Sick day'),
$this->l->t('Special occasion'),
$this->l->t('Travel'),
$this->l->t('Vacation'),
];
sort($rfcCategoryLabels);
$rfcCategoryLabels = array_values(array_filter(array_unique($rfcCategoryLabels)));

$standardCategories = array_merge($systemTagCategoryLabels, $rfcCategoryLabels);
$customCategoryLabels = array_values(array_filter($this->getUsedCategories($userId), fn ($label) => !in_array($label, $standardCategories)));

$categories = [
[
'group' => $this->l->t('Custom Categories'),
'options' => array_map(fn (string $label) => [ 'label' => $label, 'value' => $label ], $customCategoryLabels),
],
[
'group' => $this->l->t('Collaborative Tags'),
'options' => array_map(fn (string $label) => [ 'label' => $label, 'value' => $label ], $systemTagCategoryLabels),
],
[
'group' => $this->l->t('Standard Categories'),
'options' => array_map(fn (string $label) => [ 'label' => $label, 'value' => $label ], $rfcCategoryLabels),
],
];

return $categories;
}
}
47 changes: 37 additions & 10 deletions src/components/Editor/Properties/PropertySelectMultiple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
:multiple="true"
:taggable="true"
track-by="label"
group-values="options"

Check warning on line 45 in src/components/Editor/Properties/PropertySelectMultiple.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/Properties/PropertySelectMultiple.vue#L45

Added line #L45 was not covered by tests
group-label="group"
:group-select="false"
label="label"
@select="selectValue"
@tag="tag"
Expand Down Expand Up @@ -99,6 +102,10 @@
type: Boolean,
default: false,
},
customLabelHeading: {
type: String,
default: 'Custom Categories',
},
},
data() {
return {
Expand All @@ -111,45 +118,56 @@
},
options() {
const options = this.propModel.options.slice()
let customOptions = options.find((optionGroup) => optionGroup.group === this.customLabelHeading)
if (!customOptions) {
customOptions = {
group: this.customLabelHeading,
options: [],
}
options.unshift(customOptions)
}
for (const category of (this.selectionData ?? [])) {
if (options.find(option => option.value === category.value)) {
if (this.findOption(category, options)) {
continue
}

// Add pseudo options for unknown values
options.push({
customOptions.options.push({
value: category.value,
label: category.label,
})
}

for (const category of this.value) {
if (!options.find(option => option.value === category)) {
options.push({ value: category, label: category })
const categoryOption = { value: category, label: category }
if (!this.findOption(categoryOption, options)) {
customOptions.options.push(categoryOption)
}
}

if (this.customLabelBuffer) {
for (const category of this.customLabelBuffer) {
if (!options.find(option => option.value === category.value)) {
options.push(category)
if (!this.findOption(category, options)) {
customOptions.options.push(category)
}
}
}

return options
.sort((a, b) => {
for (const optionGroup of options) {
optionGroup.options = optionGroup.options.sort((a, b) => {
return a.label.localeCompare(
b.label,
getLocale().replace('_', '-'),
{ sensitivity: 'base' },
)
})
}

Check warning on line 164 in src/components/Editor/Properties/PropertySelectMultiple.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/Properties/PropertySelectMultiple.vue#L164

Added line #L164 was not covered by tests
return options
},
},
created() {
for (const category of this.value) {
const option = this.options.find(option => option.value === category)
const option = this.findOption({ value: category }, this.options)
if (option) {
this.selectionData.push(option)
}
Expand All @@ -172,7 +190,7 @@

// store removed custom options to keep it in the option list
const options = this.propModel.options.slice()
if (!options.find(option => option.value === value.value)) {
if (!this.findOption(value, options)) {

Check warning on line 193 in src/components/Editor/Properties/PropertySelectMultiple.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/Properties/PropertySelectMultiple.vue#L193

Added line #L193 was not covered by tests
if (!this.customLabelBuffer) {
this.customLabelBuffer = []
}
Expand All @@ -187,6 +205,15 @@
this.selectionData.push({ value, label: value })
this.$emit('add-single-value', value)
},
findOption(value, availableOptions) {
for (const optionGroup of availableOptions) {

Check warning on line 209 in src/components/Editor/Properties/PropertySelectMultiple.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/Properties/PropertySelectMultiple.vue#L208-L209

Added lines #L208 - L209 were not covered by tests
const option = optionGroup.options.find(option => option.value === value.value)
if (option) {

Check warning on line 211 in src/components/Editor/Properties/PropertySelectMultiple.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/Properties/PropertySelectMultiple.vue#L211

Added line #L211 was not covered by tests
return option
}
}
return undefined
},

Check warning on line 216 in src/components/Editor/Properties/PropertySelectMultiple.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/Properties/PropertySelectMultiple.vue#L215-L216

Added lines #L215 - L216 were not covered by tests
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<template>
<span class="property-select-multiple-colored-tag">
<div class="property-select-multiple-colored-tag__color-indicator" :style="{ 'background-color': color}" />
<div v-if="!isGroupLabel" class="property-select-multiple-colored-tag__color-indicator" :style="{ 'background-color': color }" />
<span class="property-select-multiple-colored-tag__label">{{ label }}</span>
</span>
</template>
Expand All @@ -41,14 +41,17 @@
},
},
computed: {
isGroupLabel() {
return this.option.$isLabel && this.option.$groupLabel

Check warning on line 45 in src/components/Editor/Properties/PropertySelectMultipleColoredOption.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor/Properties/PropertySelectMultipleColoredOption.vue#L44-L45

Added lines #L44 - L45 were not covered by tests
},
label() {
const option = this.option
logger.debug('Option render', { option })
if (typeof this.option === 'string') {
return this.option
}

return this.option.label
return this.option.$groupLabel ? this.option.$groupLabel : this.option.label
},
colorObject() {
return uidToColor(this.label)
Expand Down
6 changes: 6 additions & 0 deletions src/mixins/EditorMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
} from 'vuex'
import { translate as t } from '@nextcloud/l10n'
import { removeMailtoPrefix } from '../utils/attendee.js'
import { loadState } from '@nextcloud/initial-state'

/**
* This is a mixin for the editor. It contains common Vue stuff, that is
Expand Down Expand Up @@ -314,6 +315,11 @@
rfcProps() {
return getRFCProperties()
},
categoryOptions() {
const categories = { ...this.rfcProps.categories }
categories.options = loadState('calendar', 'categories')
return categories

Check warning on line 321 in src/mixins/EditorMixin.js

View check run for this annotation

Codecov / codecov/patch

src/mixins/EditorMixin.js#L318-L321

Added lines #L318 - L321 were not covered by tests
},
/**
* Returns whether or not this event can be downloaded from the server
*
Expand Down
3 changes: 2 additions & 1 deletion src/views/EditSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@

<PropertySelectMultiple :colored-options="true"
:is-read-only="isReadOnly"
:prop-model="rfcProps.categories"
:prop-model="categoryOptions"
:value="categories"
:custom-label-heading="t('calendar', 'Custom Categories')"

Check warning on line 152 in src/views/EditSidebar.vue

View check run for this annotation

Codecov / codecov/patch

src/views/EditSidebar.vue#L152

Added line #L152 was not covered by tests
@add-single-value="addCategory"
@remove-single-value="removeCategory" />

Expand Down
Loading
Loading