aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
authorHamza Mahjoubi <hamzamahjoubi221@gmail.com>2024-06-10 16:30:53 +0200
committerHamza Mahjoubi <hamzamahjoubi221@gmail.com>2024-07-01 15:10:16 +0200
commita9774741e815c02a9f5973bf6477a7c9da653732 (patch)
tree171ab358957d002bd4e13dea0d532d0c826ca51c /apps/dav
parent3b75c5b98cd0356e99c07d9696c49732f195b3f6 (diff)
downloadnextcloud-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.xml2
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php1
-rw-r--r--apps/dav/composer/composer/autoload_static.php1
-rw-r--r--apps/dav/lib/Controller/OutOfOfficeController.php21
-rw-r--r--apps/dav/lib/Db/Absence.php14
-rw-r--r--apps/dav/lib/Migration/Version1031Date20240610134258.php44
-rw-r--r--apps/dav/lib/ResponseDefinitions.php2
-rw-r--r--apps/dav/lib/Service/AbsenceService.php4
-rw-r--r--apps/dav/openapi.json58
-rw-r--r--apps/dav/src/components/AbsenceForm.vue121
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) {