-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(dav): implement personal absence settings #40767
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| * | ||
| * @author Georg Ehrke <[email protected]> | ||
| * @author Roeland Jago Douma <[email protected]> | ||
| * @author Richard Steinmetz <[email protected]> | ||
| * | ||
| * @license GNU AGPL version 3 or any later version | ||
| * | ||
|
|
@@ -28,7 +29,9 @@ | |
| ['name' => 'invitation_response#accept', 'url' => '/invitation/accept/{token}', 'verb' => 'GET'], | ||
| ['name' => 'invitation_response#decline', 'url' => '/invitation/decline/{token}', 'verb' => 'GET'], | ||
| ['name' => 'invitation_response#options', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'GET'], | ||
| ['name' => 'invitation_response#processMoreOptionsResult', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'POST'] | ||
| ['name' => 'invitation_response#processMoreOptionsResult', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'POST'], | ||
| ['name' => 'availability_settings#updateAbsence', 'url' => '/settings/absence', 'verb' => 'POST'], | ||
| ['name' => 'availability_settings#clearAbsence', 'url' => '/settings/absence', 'verb' => 'DELETE'], | ||
| ], | ||
| 'ocs' => [ | ||
| ['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * @copyright Copyright (c) 2023 Richard Steinmetz <[email protected]> | ||
| * | ||
| * @author Richard Steinmetz <[email protected]> | ||
| * | ||
| * @license AGPL-3.0-or-later | ||
| * | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU 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 General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU General Public License | ||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| * | ||
| */ | ||
|
|
||
| namespace OCA\DAV\Controller; | ||
|
|
||
| use DateTimeImmutable; | ||
| use OCA\DAV\AppInfo\Application; | ||
| use OCA\DAV\Service\AbsenceService; | ||
| use OCP\AppFramework\Controller; | ||
| use OCP\AppFramework\Http; | ||
| use OCP\AppFramework\Http\Attribute\NoAdminRequired; | ||
| use OCP\AppFramework\Http\JSONResponse; | ||
| use OCP\AppFramework\Http\Response; | ||
| use OCP\IRequest; | ||
|
|
||
| class AvailabilitySettingsController extends Controller { | ||
| public function __construct( | ||
| IRequest $request, | ||
| private ?string $userId, | ||
| private AbsenceService $absenceService, | ||
| ) { | ||
| parent::__construct(Application::APP_ID, $request); | ||
| } | ||
|
|
||
| /** | ||
| * @throws \OCP\DB\Exception | ||
| * @throws \Exception | ||
| */ | ||
| #[NoAdminRequired] | ||
| public function updateAbsence( | ||
| string $firstDay, | ||
| string $lastDay, | ||
| string $status, | ||
| string $message, | ||
| ): Response { | ||
| $userId = $this->userId; | ||
| if ($userId === null) { | ||
| return new JSONResponse([], Http::STATUS_FORBIDDEN); | ||
| } | ||
|
|
||
| $parsedFirstDay = new DateTimeImmutable($firstDay); | ||
| $parsedLastDay = new DateTimeImmutable($lastDay); | ||
| if ($parsedFirstDay->getTimestamp() >= $parsedLastDay->getTimestamp()) { | ||
| throw new \Exception('First day is on or after last day'); | ||
| } | ||
|
|
||
| $absence = $this->absenceService->createOrUpdateAbsence( | ||
|
||
| $userId, | ||
| $firstDay, | ||
| $lastDay, | ||
| $status, | ||
| $message, | ||
| ); | ||
| return new JSONResponse($absence); | ||
| } | ||
|
|
||
| /** | ||
| * @throws \OCP\DB\Exception | ||
| */ | ||
| #[NoAdminRequired] | ||
| public function clearAbsence(): Response { | ||
| $userId = $this->userId; | ||
| if ($userId === null) { | ||
| return new JSONResponse([], Http::STATUS_FORBIDDEN); | ||
| } | ||
|
|
||
| $this->absenceService->clearAbsence($userId); | ||
|
||
| return new JSONResponse([]); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| * @copyright 2021 Christoph Wurst <[email protected]> | ||
| * | ||
| * @author 2021 Christoph Wurst <[email protected]> | ||
| * @author Richard Steinmetz <[email protected]> | ||
| * | ||
| * @license GNU AGPL version 3 or any later version | ||
| * | ||
|
|
@@ -26,10 +27,13 @@ | |
| namespace OCA\DAV\Settings; | ||
|
|
||
| use OCA\DAV\AppInfo\Application; | ||
| use OCA\DAV\Db\AbsenceMapper; | ||
| use OCP\AppFramework\Db\DoesNotExistException; | ||
| use OCP\AppFramework\Http\TemplateResponse; | ||
| use OCP\AppFramework\Services\IInitialState; | ||
| use OCP\IConfig; | ||
| use OCP\Settings\ISettings; | ||
| use Psr\Log\LoggerInterface; | ||
|
|
||
| class AvailabilitySettings implements ISettings { | ||
| protected IConfig $config; | ||
|
|
@@ -38,7 +42,9 @@ class AvailabilitySettings implements ISettings { | |
|
|
||
| public function __construct(IConfig $config, | ||
| IInitialState $initialState, | ||
| ?string $userId) { | ||
| ?string $userId, | ||
| private LoggerInterface $logger, | ||
|
||
| private AbsenceMapper $absenceMapper) { | ||
|
||
| $this->config = $config; | ||
| $this->initialState = $initialState; | ||
| $this->userId = $userId; | ||
|
|
@@ -54,6 +60,25 @@ public function getForm(): TemplateResponse { | |
| 'no' | ||
| ) | ||
| ); | ||
| $hideAbsenceSettings = $this->config->getAppValue( | ||
| Application::APP_ID, | ||
| 'hide_absence_settings', | ||
| 'yes', | ||
| ) === 'yes'; | ||
| $this->initialState->provideInitialState('hide_absence_settings', $hideAbsenceSettings); | ||
| if (!$hideAbsenceSettings) { | ||
| try { | ||
| $absence = $this->absenceMapper->findByUserId($this->userId); | ||
Check noticeCode scanning / Psalm PossiblyNullArgument
Argument 1 of OCA\DAV\Db\AbsenceMapper::findByUserId cannot be null, possibly null value provided
|
||
| $this->initialState->provideInitialState('absence', $absence); | ||
| } catch (DoesNotExistException) { | ||
| // The user has not yet set up an absence period. | ||
| // Logging this error is not necessary. | ||
| } catch (\OCP\DB\Exception $e) { | ||
| $this->logger->error("Could not find absence data for user $this->userId: " . $e->getMessage(), [ | ||
|
||
| 'exception' => $e, | ||
| ]); | ||
| } | ||
st3iny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return new TemplateResponse(Application::APP_ID, 'settings-personal-availability'); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| <!-- | ||
| - @copyright Copyright (c) 2023 Richard Steinmetz <[email protected]> | ||
| - | ||
| - @author Richard Steinmetz <[email protected]> | ||
| - | ||
| - @license AGPL-3.0-or-later | ||
| - | ||
| - This program is free software: you can redistribute it and/or modify | ||
| - it under the terms of the GNU 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 General Public License for more details. | ||
| - | ||
| - You should have received a copy of the GNU General Public License | ||
| - along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| - | ||
| --> | ||
|
|
||
| <template> | ||
| <div class="absence"> | ||
| <div class="absence__dates"> | ||
| <NcDateTimePickerNative id="absence-first-day" | ||
| v-model="firstDay" | ||
| :label="$t('dav', 'First day')" | ||
| class="absence__dates__picker" /> | ||
| <NcDateTimePickerNative id="absence-last-day" | ||
| v-model="lastDay" | ||
| :label="$t('dav', 'Last day (inclusive)')" | ||
| class="absence__dates__picker" /> | ||
| </div> | ||
| <NcTextField :value.sync="status" :label="$t('dav', 'Short absence status')" /> | ||
| <NcTextArea :value.sync="message" :label="$t('dav', 'Long absence Message')" /> | ||
|
|
||
| <div class="absence__buttons"> | ||
| <NcButton :disabled="loading || !valid" | ||
| type="primary" | ||
| @click="saveForm"> | ||
| {{ $t('dav', 'Save') }} | ||
| </NcButton> | ||
| <NcButton :disabled="loading || !valid" | ||
| type="error" | ||
| @click="clearAbsence"> | ||
| {{ $t('dav', 'Disable absence') }} | ||
| </NcButton> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script> | ||
| 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 NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' | ||
| import { generateUrl } from '@nextcloud/router' | ||
| import axios from '@nextcloud/axios' | ||
| import { formatDateAsYMD } from '../utils/date.js' | ||
| import { loadState } from '@nextcloud/initial-state' | ||
| import { showError } from '@nextcloud/dialogs' | ||
| export default { | ||
| name: 'AbsenceForm', | ||
| components: { | ||
| NcButton, | ||
| NcTextField, | ||
| NcTextArea, | ||
| NcDateTimePickerNative, | ||
| }, | ||
| data() { | ||
| const { firstDay, lastDay, status, message } = loadState('dav', 'absence', {}) | ||
| return { | ||
| loading: false, | ||
| status: status ?? '', | ||
| message: message ?? '', | ||
| firstDay: firstDay ? new Date(firstDay) : new Date(), | ||
| lastDay: lastDay ? new Date(lastDay) : null, | ||
| } | ||
| }, | ||
| computed: { | ||
| /** | ||
| * @return {boolean} | ||
| */ | ||
| valid() { | ||
| return !!this.firstDay | ||
| && !!this.lastDay | ||
| && !!this.status | ||
| && this.lastDay > this.firstDay | ||
| }, | ||
| }, | ||
| methods: { | ||
| resetForm() { | ||
| this.status = '' | ||
| this.message = '' | ||
| this.firstDay = new Date() | ||
| this.lastDay = null | ||
| }, | ||
| async saveForm() { | ||
| if (!this.valid) { | ||
| return | ||
| } | ||
| this.loading = true | ||
| try { | ||
| await axios.post(generateUrl('/apps/dav/settings/absence'), { | ||
| firstDay: formatDateAsYMD(this.firstDay), | ||
| lastDay: formatDateAsYMD(this.lastDay), | ||
| status: this.status, | ||
| message: this.message, | ||
| }) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should show success after this? |
||
| } catch (error) { | ||
| showError(this.$t('dav', 'Failed to save your absence settings')) | ||
| } finally { | ||
| this.loading = false | ||
| } | ||
| }, | ||
| async clearAbsence() { | ||
| this.loading = true | ||
| try { | ||
| await axios.delete(generateUrl('/apps/dav/settings/absence')) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should show success after this? |
||
| this.resetForm() | ||
| } catch (error) { | ||
| showError(this.$t('dav', 'Failed to clear your absence settings')) | ||
| } finally { | ||
| this.loading = false | ||
| } | ||
| }, | ||
| }, | ||
| } | ||
| </script> | ||
| <style lang="scss" scoped> | ||
| .absence { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 5px; | ||
| &__dates { | ||
| display: flex; | ||
| gap: 10px; | ||
| width: 100%; | ||
| &__picker { | ||
| flex: 1 auto; | ||
| ::v-deep .native-datetime-picker--input { | ||
| margin-bottom: 0; | ||
| } | ||
| } | ||
| } | ||
| &__buttons { | ||
| display: flex; | ||
| gap: 5px; | ||
| } | ||
| } | ||
| </style> | ||
Uh oh!
There was an error while loading. Please reload this page.