diff options
author | Hamza Mahjoubi <hamzamahjoubi221@gmail.com> | 2024-06-10 16:30:53 +0200 |
---|---|---|
committer | Hamza Mahjoubi <hamzamahjoubi221@gmail.com> | 2024-07-01 15:10:16 +0200 |
commit | a9774741e815c02a9f5973bf6477a7c9da653732 (patch) | |
tree | 171ab358957d002bd4e13dea0d532d0c826ca51c /apps/dav | |
parent | 3b75c5b98cd0356e99c07d9696c49732f195b3f6 (diff) | |
download | nextcloud-server-a9774741e815c02a9f5973bf6477a7c9da653732.tar.gz nextcloud-server-a9774741e815c02a9f5973bf6477a7c9da653732.zip |
Feat: Allow users to select another user as their out-of-office replacement
Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
Diffstat (limited to 'apps/dav')
-rw-r--r-- | apps/dav/appinfo/info.xml | 2 | ||||
-rw-r--r-- | apps/dav/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/dav/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/dav/lib/Controller/OutOfOfficeController.php | 21 | ||||
-rw-r--r-- | apps/dav/lib/Db/Absence.php | 14 | ||||
-rw-r--r-- | apps/dav/lib/Migration/Version1031Date20240610134258.php | 44 | ||||
-rw-r--r-- | apps/dav/lib/ResponseDefinitions.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/Service/AbsenceService.php | 4 | ||||
-rw-r--r-- | apps/dav/openapi.json | 58 | ||||
-rw-r--r-- | apps/dav/src/components/AbsenceForm.vue | 121 |
10 files changed, 263 insertions, 5 deletions
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index ed530e2d09b..173c45a7fef 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -10,7 +10,7 @@ <name>WebDAV</name> <summary>WebDAV endpoint</summary> <description>WebDAV endpoint</description> - <version>1.31.0</version> + <version>1.31.1</version> <licence>agpl</licence> <author>owncloud.org</author> <namespace>DAV</namespace> diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 5d6ad572b9c..1fc6b817fe5 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -326,6 +326,7 @@ return array( 'OCA\\DAV\\Migration\\Version1029Date20221114151721' => $baseDir . '/../lib/Migration/Version1029Date20221114151721.php', 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => $baseDir . '/../lib/Migration/Version1029Date20231004091403.php', 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => $baseDir . '/../lib/Migration/Version1030Date20240205103243.php', + 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => $baseDir . '/../lib/Migration/Version1031Date20240610134258.php', 'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 246bddbfdc7..2bcfb04b777 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -341,6 +341,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1029Date20221114151721' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20221114151721.php', 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20231004091403.php', 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => __DIR__ . '/..' . '/../lib/Migration/Version1030Date20240205103243.php', + 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => __DIR__ . '/..' . '/../lib/Migration/Version1031Date20240610134258.php', 'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', diff --git a/apps/dav/lib/Controller/OutOfOfficeController.php b/apps/dav/lib/Controller/OutOfOfficeController.php index b127c5c3cd3..b77b77f4b5e 100644 --- a/apps/dav/lib/Controller/OutOfOfficeController.php +++ b/apps/dav/lib/Controller/OutOfOfficeController.php @@ -93,6 +93,8 @@ class OutOfOfficeController extends OCSController { 'lastDay' => $data->getLastDay(), 'status' => $data->getStatus(), 'message' => $data->getMessage(), + 'replacementUserId' => $data->getReplacementUserId(), + 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(), ]); } @@ -103,11 +105,14 @@ class OutOfOfficeController extends OCSController { * @param string $lastDay Last day of the absence in format `YYYY-MM-DD` * @param string $status Short text that is set as user status during the absence * @param string $message Longer multiline message that is shown to others during the absence - * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'firstDay'}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}> + * @param string $replacementUserId User id of the replacement user + * @param string $replacementUserDisplayName Display name of the replacement user + * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'firstDay'}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}> * * 200: Absence data * 400: When the first day is not before the last day * 401: When the user is not logged in + * 404: When the replacementUserId was provided but replacement user was not found */ #[NoAdminRequired] public function setOutOfOffice( @@ -115,12 +120,22 @@ class OutOfOfficeController extends OCSController { string $lastDay, string $status, string $message, + string $replacementUserId = '', + string $replacementUserDisplayName = '' + ): DataResponse { $user = $this->userSession?->getUser(); if ($user === null) { return new DataResponse(null, Http::STATUS_UNAUTHORIZED); } + if ($replacementUserId !== '') { + $replacementUser = $this->userManager->get($replacementUserId); + if ($replacementUser === null) { + return new DataResponse(null, Http::STATUS_NOT_FOUND); + } + } + $parsedFirstDay = new DateTimeImmutable($firstDay); $parsedLastDay = new DateTimeImmutable($lastDay); if ($parsedFirstDay->getTimestamp() > $parsedLastDay->getTimestamp()) { @@ -133,6 +148,8 @@ class OutOfOfficeController extends OCSController { $lastDay, $status, $message, + $replacementUserId, + $replacementUserDisplayName ); $this->coordinator->clearCache($user->getUID()); @@ -143,6 +160,8 @@ class OutOfOfficeController extends OCSController { 'lastDay' => $data->getLastDay(), 'status' => $data->getStatus(), 'message' => $data->getMessage(), + 'replacementUserId' => $data->getReplacementUserId(), + 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(), ]); } diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php index 6b6d608ffd8..c58593e3e3f 100644 --- a/apps/dav/lib/Db/Absence.php +++ b/apps/dav/lib/Db/Absence.php @@ -29,6 +29,10 @@ use OCP\User\IOutOfOfficeData; * @method void setStatus(string $status) * @method string getMessage() * @method void setMessage(string $message) + * @method string getReplacementUserId() + * @method void setReplacementUserId(string $replacementUserId) + * @method string getReplacementUserDisplayName() + * @method void setReplacementUserDisplayName(string $replacementUserDisplayName) */ class Absence extends Entity implements JsonSerializable { protected string $userId = ''; @@ -43,12 +47,18 @@ class Absence extends Entity implements JsonSerializable { protected string $message = ''; + protected string $replacementUserId = ''; + + protected string $replacementUserDisplayName = ''; + public function __construct() { $this->addType('userId', 'string'); $this->addType('firstDay', 'string'); $this->addType('lastDay', 'string'); $this->addType('status', 'string'); $this->addType('message', 'string'); + $this->addType('replacementUserId', 'string'); + $this->addType('replacementUserDisplayName', 'string'); } public function toOutOufOfficeData(IUser $user, string $timezone): IOutOfOfficeData { @@ -70,6 +80,8 @@ class Absence extends Entity implements JsonSerializable { $endDate->getTimestamp(), $this->getStatus(), $this->getMessage(), + $this->getReplacementUserId(), + $this->getReplacementUserDisplayName(), ); } @@ -80,6 +92,8 @@ class Absence extends Entity implements JsonSerializable { 'lastDay' => $this->lastDay, 'status' => $this->status, 'message' => $this->message, + 'replacementUserId' => $this->replacementUserId, + 'replacementUserDisplayName' => $this->replacementUserDisplayName, ]; } } diff --git a/apps/dav/lib/Migration/Version1031Date20240610134258.php b/apps/dav/lib/Migration/Version1031Date20240610134258.php new file mode 100644 index 00000000000..4e5e2218c41 --- /dev/null +++ b/apps/dav/lib/Migration/Version1031Date20240610134258.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1031Date20240610134258 extends SimpleMigrationStep { + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $tableDavAbsence = $schema->getTable('dav_absence'); + + if (!$tableDavAbsence->hasColumn('replacement_user_id')) { + $tableDavAbsence->addColumn('replacement_user_id', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 64, + ]); + } + + if (!$tableDavAbsence->hasColumn('replacement_user_display_name')) { + $tableDavAbsence->addColumn('replacement_user_display_name', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 64, + ]); + } + + return $schema; + } + +} diff --git a/apps/dav/lib/ResponseDefinitions.php b/apps/dav/lib/ResponseDefinitions.php index 911780f6aa3..467c9a7b8bb 100644 --- a/apps/dav/lib/ResponseDefinitions.php +++ b/apps/dav/lib/ResponseDefinitions.php @@ -13,6 +13,8 @@ namespace OCA\DAV; * @psalm-type DAVOutOfOfficeDataCommon = array{ * userId: string, * message: string, + * replacementUserId: string, + * replacementUserDisplayName: string, * } * * @psalm-type DAVOutOfOfficeData = DAVOutOfOfficeDataCommon&array{ diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index 699f15ae2e0..91e0b538c43 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -47,6 +47,8 @@ class AbsenceService { string $lastDay, string $status, string $message, + ?string $replacementUserId = null, + ?string $replacementUserDisplayName = null, ): Absence { try { $absence = $this->absenceMapper->findByUserId($user->getUID()); @@ -59,6 +61,8 @@ class AbsenceService { $absence->setLastDay($lastDay); $absence->setStatus($status); $absence->setMessage($message); + $absence->setReplacementUserId($replacementUserId ?? ''); + $absence->setReplacementUserDisplayName($replacementUserDisplayName ?? ''); if ($absence->getId() === null) { $absence = $this->absenceMapper->insert($absence); diff --git a/apps/dav/openapi.json b/apps/dav/openapi.json index da514da7bb7..71cebfa684b 100644 --- a/apps/dav/openapi.json +++ b/apps/dav/openapi.json @@ -133,7 +133,9 @@ "type": "object", "required": [ "userId", - "message" + "message", + "replacementUserId", + "replacementUserDisplayName" ], "properties": { "userId": { @@ -141,6 +143,12 @@ }, "message": { "type": "string" + }, + "replacementUserId": { + "type": "string" + }, + "replacementUserDisplayName": { + "type": "string" } } } @@ -571,6 +579,24 @@ } }, { + "name": "replacementUserId", + "in": "query", + "description": "User id of the replacement user", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "replacementUserDisplayName", + "in": "query", + "description": "Display name of the replacement user", + "schema": { + "type": "string", + "default": "" + } + }, + { "name": "userId", "in": "path", "required": true, @@ -690,6 +716,36 @@ } } } + }, + "404": { + "description": "When the replacementUserId was provided but replacement user was not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } } } }, diff --git a/apps/dav/src/components/AbsenceForm.vue b/apps/dav/src/components/AbsenceForm.vue index fa62c56039d..33f1483a7fb 100644 --- a/apps/dav/src/components/AbsenceForm.vue +++ b/apps/dav/src/components/AbsenceForm.vue @@ -17,6 +17,21 @@ class="absence__dates__picker" :required="true" /> </div> + <label for="replacement-search-input">{{ $t('dav', 'Out of office replacement (optional)') }}</label> + <NcSelect ref="select" + v-model="replacementUser" + input-id="replacement-search-input" + :loading="searchLoading" + :placeholder="$t('dav', 'Name of the replacement')" + :clear-search-on-blur="() => false" + :user-select="true" + :options="options" + @search="asyncFind" + > + <template #no-options="{ search }"> + {{ search ?$t('dav', 'No results.') : $t('dav', 'Start typing.') }} + </template> + </NcSelect> <NcTextField :value.sync="status" :label="$t('dav', 'Short absence status')" :required="true" /> <NcTextArea :value.sync="message" :label="$t('dav', 'Long absence Message')" :required="true" /> @@ -39,13 +54,16 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js' +import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' import { generateOcsUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' +import debounce from 'debounce' import axios from '@nextcloud/axios' import { formatDateAsYMD } from '../utils/date.js' import { loadState } from '@nextcloud/initial-state' import { showError, showSuccess } from '@nextcloud/dialogs' +import { Type as ShareTypes } from '@nextcloud/sharing' import logger from '../service/logger.js' @@ -56,16 +74,20 @@ export default { NcTextField, NcTextArea, NcDateTimePickerNative, + NcSelect }, data() { - const { firstDay, lastDay, status, message } = loadState('dav', 'absence', {}) - + const { firstDay, lastDay, status, message ,replacementUserId ,replacementUserDisplayName } = loadState('dav', 'absence', {}) return { loading: false, status: status ?? '', message: message ?? '', firstDay: firstDay ? new Date(firstDay) : new Date(), lastDay: lastDay ? new Date(lastDay) : null, + replacementUserId: replacementUserId , + replacementUser: replacementUserId ? { user: replacementUserId, displayName: replacementUserDisplayName } : null, + searchLoading: false, + options: [], } }, computed: { @@ -93,6 +115,99 @@ export default { this.firstDay = new Date() this.lastDay = null }, + + /** + * Format shares for the multiselect options + * + * @param {object} result select entry item + * @return {object} + */ + formatForMultiselect(result) { + return { + user: result.uuid || result.value.shareWith, + displayName: result.name || result.label, + subtitle: result.dsc | '' + } + }, + + async asyncFind(query) { + this.searchLoading = true + await this.debounceGetSuggestions(query.trim()) + }, + /** + * Get suggestions + * + * @param {string} search the search query + */ + async getSuggestions(search) { + + const shareType = [ + ShareTypes.SHARE_TYPE_USER, + ] + + let request = null + try { + request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), { + params: { + format: 'json', + itemType: 'file', + search, + shareType, + }, + }) + } catch (error) { + console.error('Error fetching suggestions', error) + return + } + + const data = request.data.ocs.data + const exact = request.data.ocs.data.exact + data.exact = [] // removing exact from general results + const rawExactSuggestions = exact.users + const rawSuggestions = data.users + console.info('rawExactSuggestions', rawExactSuggestions) + console.info('rawSuggestions', rawSuggestions) + // remove invalid data and format to user-select layout + const exactSuggestions = rawExactSuggestions + .map(share => this.formatForMultiselect(share)) + const suggestions = rawSuggestions + .map(share => this.formatForMultiselect(share)) + + const allSuggestions = exactSuggestions.concat(suggestions) + + // Count occurrences of display names in order to provide a distinguishable description if needed + const nameCounts = allSuggestions.reduce((nameCounts, result) => { + if (!result.displayName) { + return nameCounts + } + if (!nameCounts[result.displayName]) { + nameCounts[result.displayName] = 0 + } + nameCounts[result.displayName]++ + return nameCounts + }, {}) + + this.options = allSuggestions.map(item => { + // Make sure that items with duplicate displayName get the shareWith applied as a description + if (nameCounts[item.displayName] > 1 && !item.desc) { + return { ...item, desc: item.shareWithDisplayNameUnique } + } + return item + }) + + this.searchLoading = false + console.info('suggestions', this.options) + }, + + /** + * Debounce getSuggestions + * + * @param {...*} args the arguments + */ + debounceGetSuggestions: debounce(function(...args) { + this.getSuggestions(...args) + }, 300), + async saveForm() { if (!this.valid) { return @@ -105,6 +220,8 @@ export default { lastDay: formatDateAsYMD(this.lastDay), status: this.status, message: this.message, + replacementUserId: this.replacementUser?.user ?? null, + replacementUserDisplayName: this.replacementUser?.displayName ?? null, }) showSuccess(this.$t('dav', 'Absence saved')) } catch (error) { |