diff options
author | Carl Schwan <carl@carlschwan.eu> | 2022-08-01 17:00:58 +0300 |
---|---|---|
committer | Carl Schwan <carl@carlschwan.eu> | 2022-08-19 21:03:30 +0300 |
commit | 757b97e5674b712f8af8050d5e3903cda45b4258 (patch) | |
tree | 367e486ddae1423ed7a0c3bf1cb4d50bcafdc763 /src | |
parent | cc3befff4c27506ac98d53fa3defac64c6786af5 (diff) |
Port settings to vue
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Activity.vue | 4 | ||||
-rw-r--r-- | src/components/ActivityGrid.vue | 136 | ||||
-rw-r--r-- | src/components/Checkbox.vue | 273 | ||||
-rw-r--r-- | src/components/EmailSettings.vue | 87 | ||||
-rw-r--r-- | src/helpers/settings.js | 52 | ||||
-rw-r--r-- | src/models/ActivityModel.js | 21 | ||||
-rw-r--r-- | src/models/ActivitySettings.js | 35 | ||||
-rw-r--r-- | src/models/EmailFrequency.js | 30 | ||||
-rw-r--r-- | src/settings-admin.js | 51 | ||||
-rw-r--r-- | src/settings-personal.js | 51 | ||||
-rw-r--r-- | src/settings-store.js | 322 | ||||
-rw-r--r-- | src/tests/setup.js | 2 | ||||
-rw-r--r-- | src/views/AdminSettings.vue | 68 | ||||
-rw-r--r-- | src/views/DailySummary.vue | 38 | ||||
-rw-r--r-- | src/views/DefaultActivitySettings.vue | 58 | ||||
-rw-r--r-- | src/views/UserSettings.vue | 68 |
16 files changed, 1279 insertions, 17 deletions
diff --git a/src/components/Activity.vue b/src/components/Activity.vue index 88c6b34c..34139887 100644 --- a/src/components/Activity.vue +++ b/src/components/Activity.vue @@ -122,7 +122,7 @@ export default { /** * Map an collection of rich text objects to rich arguments for the RichText component * - * @param {Array.<object.<string, RichObject>>} richObjects - The rich text object + * @param {Array.<object<string, RichObject>>} richObjects - The rich text object * @return {object<string, object>} */ mapRichObjectsToRichArguments(richObjects) { @@ -138,7 +138,7 @@ export default { /** * Map rich text object to rich argument for the RichText component * - * @param {object.<string, RichObject>} richObject - The rich text object + * @param {object<string, RichObject>} richObject - The rich text object * @return {object}} */ mapRichObjectToRichArgument(richObject) { diff --git a/src/components/ActivityGrid.vue b/src/components/ActivityGrid.vue new file mode 100644 index 00000000..61b18bbf --- /dev/null +++ b/src/components/ActivityGrid.vue @@ -0,0 +1,136 @@ +<!-- + - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + - + - @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 Affero 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 Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <table class="grid activitysettings"> + <caption class="sr-only"> + {{ t('activity', 'Activity notification configuration') }} + </caption> + <tbody v-for="(group, groupKey) in activityGroups" :key="groupKey"> + <tr class="group-header"> + <th> + <h3>{{ group.name }}</h3> + </th> + <th v-for="(methodName, methodKey) in methods" + :key="methodKey" + class="activity_select_group" + aria-hidden="true"> + {{ methodName }} + </th> + </tr> + <tr v-for="(activity, activityKey) in group.activities" :key="activityKey"> + <th scope="row"> + <!-- eslint-disable vue/no-v-html --> + <label @click="toggleMethodsForActivity({groupKey, activityKey})" v-html="activity.desc" /> + <!-- eslint-enable vue/no-v-html --> + </th> + <td v-for="(methodName, methodKey) in methods" :key="methodKey"> + <Checkbox :id="`${activityKey}_${methodKey}`" + :disabled="!isActivityEnabled(activity, methodKey)" + :checked="checkedActivities" + :value="`${activityKey}_${methodKey}`" + @update:checked="toggleMethodForMethodAndActivity({groupKey, activityKey, methodKey})"> + {{ actionName(methodKey) }} + </Checkbox> + </td> + </tr> + </tbody> + </table> +</template> + +<script> +import { mapActions, mapGetters, mapState } from 'vuex' +import Checkbox from './Checkbox' +import { isActivityEnabled } from '../helpers/settings' + +export default { + name: 'ActivityGrid', + components: { + Checkbox, + }, + computed: { + ...mapGetters([ + 'checkedActivities', + ]), + ...mapState([ + 'methods', + 'activityGroups', + 'emailEnabled', + 'isEmailSet', + 'settingBatchtime', + ]), + }, + methods: { + isActivityEnabled, + ...mapActions([ + 'toggleMethodForMethodAndActivity', + 'toggleMethodForGroup', + 'toggleMethodsForActivity', + ]), + actionName(method) { + if (method === 'email') { + return t('activity', 'Send email') + } else { + return t('activity', 'Send push notification') + } + }, + }, +} +</script> + +<style lang="scss" scoped> + +table.grid { + // Hack: align content of the table with the rest of the page + margin-left: -0.8em; + + h3 { + font-weight: bold; + } +} + +table.grid th { + color: var(--color-text-light); + height: 44px; +} + +table.grid .group-header { + th { + padding-top: 16px; + height: 60px; + &.activity_select_group { + padding-left: 20px; + } + } +} + +table.grid th.activity_select_group { + color: var(--color-main-text); +} + +.sr-only { + position:absolute; + left:-10000px; + top:auto; + width:1px; + height:1px; + overflow:hidden; +} +</style> diff --git a/src/components/Checkbox.vue b/src/components/Checkbox.vue new file mode 100644 index 00000000..f8439ce2 --- /dev/null +++ b/src/components/Checkbox.vue @@ -0,0 +1,273 @@ +<!-- + - @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.com> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero 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 Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <element :is="wrapperElement" + :class="{ + 'checkbox-radio-switch--checked': isChecked, + 'checkbox-radio-switch--disabled': disabled, + 'checkbox-radio-switch--indeterminate': indeterminate, + }" + :style="cssVars" + class="checkbox-radio-switch checkbox-radio-switch-checkbox"> + <input :id="id" + :checked="isChecked" + :disabled="disabled" + :indeterminate="indeterminate" + :name="name" + type="checkbox" + :value="value" + class="checkbox-radio-switch__input" + @change="onToggle"> + + <label :for="id" class="checkbox-radio-switch__label"> + <icon :is="checkboxRadioIconElement" + :size="size" + class="checkbox-radio-switch__icon" + title="" + decorative /> + <span class="sr-only"> + <slot /> + </span> + </label> + </element> +</template> + +<script> +import CheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline' +import CheckboxBlank from 'vue-material-design-icons/CheckboxBlank' +import MinusBox from 'vue-material-design-icons/MinusBox' +import CheckboxMarked from 'vue-material-design-icons/CheckboxMarked' + +export default { + name: 'Checkbox', + + props: { + + /** + * Unique id attribute of the input + */ + id: { + type: String, + required: true, + validator: id => id.trim() !== '', + }, + + /** + * Checked state. To be used with `:value.sync` + */ + checked: { + type: [Boolean, Array, String], + default: false, + }, + + /** + * Value to be synced on check + */ + value: { + type: String, + default: null, + }, + + /** + * Disabled state + */ + disabled: { + type: Boolean, + default: false, + }, + + /** + * Indeterminate state + */ + indeterminate: { + type: Boolean, + default: false, + }, + + /** + * Wrapping element tag + */ + wrapperElement: { + type: String, + default: 'span', + }, + + /** + * Input name. Required for radio, optional for checkbox + */ + name: { + type: String, + default: null, + }, + }, + + computed: { + /** + * Icon size + * + @return {number} + */ + size() { + return 24 + }, + + /** + * Css local variables for this component + * + * @return {object} + */ + cssVars() { + return { + '--icon-size': this.size + 'px', + } + }, + + isChecked() { + return [...this.checked].indexOf(this.value) > -1 + }, + + /** + * Returns the proper Material icon depending on the select case + * + * @return {Component} + */ + checkboxRadioIconElement() { + if (this.indeterminate) { + return MinusBox + } + if (this.disabled && !this.isChecked) { + return CheckboxBlank + } + if (this.isChecked) { + return CheckboxMarked + } + return CheckboxBlankOutline + }, + }, + + methods: { + onToggle() { + if (this.disabled) { + return + } + + // If the initial value was a boolean, let's keep it that way + if (typeof this.checked === 'boolean') { + this.$emit('update:checked', !this.isChecked) + return + } + + // Dispatch the checked values as an array if multiple, or single value otherwise + const values = this.getInputsSet() + .filter(input => input.checked) + .map(input => input.value) + this.$emit('update:checked', values) + }, + + /** + * Get the input set based on this name + * + * @return {Node[]} + */ + getInputsSet() { + return [...document.getElementsByName(this.name)] + }, + }, +} +</script> + +<style lang="scss" scoped> +$spacing: 4px; + +.checkbox-radio-switch { + display: flex; + + &__input { + position: fixed; + z-index: -1; + top: -5000px; + left: -5000px; + opacity: 0; + } + + &__label { + display: flex; + align-items: center; + user-select: none; + height: 32px; + width: 32px; + border-radius: 44px; + padding: 0; + margin: 2px; + + &, * { + cursor: pointer; + } + } + + &__icon { + margin-right: $spacing; + margin-left: $spacing; + color: var(--color-primary-element); + width: var(--icon-size); + height: var(--icon-size); + } + + &--disabled &__label { + opacity: 0.7; + .checkbox-radio-switch__icon { + color: var(--color-text-light) + } + } + + &:not(&--disabled) &__input:hover + &__label, + &:not(&--disabled) &__input:focus + &__label { + background-color: var(--color-primary-light); + } + + // Increase focus effect + &:not(&--disabled) &__input:focus + &__label { + box-shadow: 0 0 0 2px var(--color-primary); + } + + // Switch specific rules + &-switch:not(&--checked) &__icon { + color: var(--color-text-lighter); + } + + // If switch is checked AND disabled, use the fade primary colour + &-switch.checkbox-radio-switch--disabled.checkbox-radio-switch--checked &__icon { + color: var(--color-primary-element-light); + } + + .sr-only { + position:absolute; + left:-10000px; + top:auto; + width:1px; + height:1px; + overflow:hidden; + } +} + +</style> diff --git a/src/components/EmailSettings.vue b/src/components/EmailSettings.vue new file mode 100644 index 00000000..e7f38549 --- /dev/null +++ b/src/components/EmailSettings.vue @@ -0,0 +1,87 @@ +<!-- + - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + - + - @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 Affero 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 Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div v-if="emailEnabled"> + <p v-if="!isEmailSet"> + <strong>{{ t('activity', 'You need to set up your email address before you can receive notification emails.') }}</strong> + </p> + <p> + <label for="activity_setting_batchtime" class="activity-frequency__label"> + {{ t('activity', 'Send activity emails') }} + </label> + <select class="notification-frequency__select" + name="notify_setting_batchtime" + @change="setSettingBatchtime({settingBatchtime: $event.target.value})"> + <option :value="EmailFrequency.EMAIL_SEND_ASAP" + :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_ASAP"> + {{ t('activity', 'As soon as possible') }} + </option> + <option :value="EmailFrequency.EMAIL_SEND_HOURLY" + :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_HOURLY"> + {{ t('activity', 'Hourly') }} + </option> + <option :value="EmailFrequency.EMAIL_SEND_DAILY" + :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_DAILY"> + {{ t('activity', 'Daily') }} + </option> + <option :value="EmailFrequency.EMAIL_SEND_WEEKLY" + :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_WEEKLY"> + {{ t('activity', 'Weekly') }} + </option> + </select> + </p> + </div> +</template> + +<script> +import { mapActions, mapState } from 'vuex' +import EmailFrequency from '../models/EmailFrequency' + +export default { + name: 'EmailSettings', + + data() { + return { + EmailFrequency: EmailFrequency.EmailFrequency, + } + }, + computed: { + ...mapState([ + 'emailEnabled', + 'isEmailSet', + 'settingBatchtime', + ]), + }, + methods: { + ...mapActions([ + 'setSettingBatchtime', + ]), + }, +} + +</script> + +<style lang="scss" scoped> +.activity-frequency__label { + margin-top: 24px; + display: inline-block; +} +</style> diff --git a/src/helpers/settings.js b/src/helpers/settings.js new file mode 100644 index 00000000..0e6a6cb2 --- /dev/null +++ b/src/helpers/settings.js @@ -0,0 +1,52 @@ +/** + * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me> + * + * @author Louis Chemineau <louis@chmn.me> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** + * Return wether the notification method can be checked for the activity + * + * @param {ActivityType} activity - the concerned activity + * @param {string} methodKey - the concerned method + * @return {boolean} + */ +function isActivityEnabled(activity, methodKey) { + return activity.methods.includes(methodKey) +} + +/** + * @param {Array<ActivityType>} activities - List of the activities to check + * @param {string} methodKey - the method key for which to verify the checked value + * @return {boolean} wether at least one input is checked for the given set of activities + */ +function isOneInputUnChecked(activities, methodKey) { + for (const activity of activities) { + if (isActivityEnabled(activity, methodKey) && !activity[methodKey]) { + return true + } + } + + return false +} + +export { + isActivityEnabled, + isOneInputUnChecked, +} diff --git a/src/models/ActivityModel.js b/src/models/ActivityModel.js index 07ae0f25..e8c99f08 100644 --- a/src/models/ActivityModel.js +++ b/src/models/ActivityModel.js @@ -3,7 +3,7 @@ * * @author Louis Chemineau <louis@chmn.me> * - * @license GPL-3.0-or-later + * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,13 +22,6 @@ import moment from '@nextcloud/moment' -/** - * @typedef RichObject - * @type {object} - * @property {string} id - The id of the riche object. - * @property {string} type - The type of the file object. - */ - export default class ActivityModel { _activity @@ -67,7 +60,7 @@ export default class ActivityModel { /** * get the activity id * - * @return {number} + * @return {int} * @readonly * @memberof ActivityModel */ @@ -89,7 +82,7 @@ export default class ActivityModel { /** * Get the activity type * - * @return {number} + * @return {int} * @readonly * @memberof ActivityModel */ @@ -133,7 +126,7 @@ export default class ActivityModel { /** * Get the activity subject_rich objects * - * @return {object.<string, RichObject>} + * @return {object<string, RichObject>} * @readonly * @memberof ActivityModel */ @@ -170,7 +163,7 @@ export default class ActivityModel { /** * Get the activity message_rich objects * - * @return {object.<string, RichObject>} + * @return {object<string, RichObject>} * @readonly * @memberof ActivityModel */ @@ -196,7 +189,7 @@ export default class ActivityModel { /** * Get the activity object_id * - * @return {number} + * @return {int} * @readonly * @memberof ActivityModel */ @@ -273,7 +266,7 @@ export default class ActivityModel { /** * Get the activity timestamp * - * @return {number} + * @return {string} * @readonly * @memberof ActivityModel */ diff --git a/src/models/ActivitySettings.js b/src/models/ActivitySettings.js new file mode 100644 index 00000000..93eca87f --- /dev/null +++ b/src/models/ActivitySettings.js @@ -0,0 +1,35 @@ +/** + * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me> + * + * @author Louis Chemineau <louis@chmn.me> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** + * + * @typedef {object} ActivityGroup + * @property {string} name - The name of the activity group + * @property {object<string, ActivityType>} activities - List off activities + */ + +/** + * + * @typedef {object} ActivityType + * @property {string} desc - The activity's description + * @property {Array<string>} methods - List of available methods to send a notification + */ diff --git a/src/models/EmailFrequency.js b/src/models/EmailFrequency.js new file mode 100644 index 00000000..6465aa18 --- /dev/null +++ b/src/models/EmailFrequency.js @@ -0,0 +1,30 @@ +/** + * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me> + * + * @author Louis Chemineau <louis@chmn.me> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +export default { + EmailFrequency: { + EMAIL_SEND_HOURLY: 0, + EMAIL_SEND_DAILY: 1, + EMAIL_SEND_WEEKLY: 2, + EMAIL_SEND_ASAP: 3, + }, +} diff --git a/src/settings-admin.js b/src/settings-admin.js new file mode 100644 index 00000000..48307279 --- /dev/null +++ b/src/settings-admin.js @@ -0,0 +1,51 @@ +/** + * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + * + * @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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import Vue from 'vue' +import Vuex from 'vuex' + +import AdminSettings from './views/AdminSettings' +import DefaultActivtiySettings from './views/DefaultActivitySettings' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import { generateFilePath } from '@nextcloud/router' +import store from './settings-store' + +Vue.prototype.t = t +Vue.prototype.n = n + +// eslint-disable-next-line no-undef, camelcase +__webpack_public_path__ = generateFilePath(appName, '', 'js/') + +Vue.use(Vuex) + +export default { + adminSetting: new Vue({ + el: '#activity-admin-settings', + store, + name: 'ActivityPersonalSettings', + render: h => h(AdminSettings), + }), + defaultSetting: new Vue({ + el: '#activity-default-settings', + store, + name: 'ActivityDefaultSettings', + render: h => h(DefaultActivtiySettings), + }), +} diff --git a/src/settings-personal.js b/src/settings-personal.js new file mode 100644 index 00000000..748dbc83 --- /dev/null +++ b/src/settings-personal.js @@ -0,0 +1,51 @@ +/** + * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + * + * @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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import Vue from 'vue' +import Vuex from 'vuex' + +import UserSettings from './views/UserSettings' +import DailySummary from './views/DailySummary' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import { generateFilePath } from '@nextcloud/router' +import store from './settings-store' + +Vue.prototype.t = t +Vue.prototype.n = n + +// eslint-disable-next-line no-undef, camelcase +__webpack_public_path__ = generateFilePath(appName, '', 'js/') + +Vue.use(Vuex) + +export default { + userSetting: new Vue({ + el: '#activity-user-settings', + store, + name: 'ActivityPersonalSettings', + render: h => h(UserSettings), + }), + digestSetting: new Vue({ + el: '#activity-digest-user-settings', + name: 'ActivityDigestPersonalSettings', + store, + render: h => h(DailySummary), + }), +} diff --git a/src/settings-store.js b/src/settings-store.js new file mode 100644 index 00000000..61a9d5ef --- /dev/null +++ b/src/settings-store.js @@ -0,0 +1,322 @@ +/** + * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me> + * + * @author Louis Chemineau <louis@chmn.me> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import Vue from 'vue' +import Vuex from 'vuex' +import { translate as t } from '@nextcloud/l10n' + +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' +import { showSuccess, showError } from '@nextcloud/dialogs' +import '@nextcloud/dialogs/styles/toast.scss' + +import { isActivityEnabled, isOneInputUnChecked } from './helpers/settings' +import logger from './logger' + +Vue.use(Vuex) + +/** + * @typedef {object} SettingsState + * @property {object} setting + * @property {object<string, ActivityGroup>} activityGroups + * @property {boolean} isEmailSet + * @property {boolean} emailEnabled + * @property {boolean} activityDigestEnabled + * @property {0|1|2|3} settingBatchtime + * @property {Array<string>} methods + * @property {string} endpoint + */ + +const store = new Vuex.Store({ + strict: true, + /** @type {SettingsState} */ + state: { + setting: loadState('activity', 'setting'), + activityGroups: loadState('activity', 'activity_groups'), + isEmailSet: loadState('activity', 'is_email_set'), + emailEnabled: loadState('activity', 'email_enabled'), + activityDigestEnabled: loadState('activity', 'activity_digest_enabled', false), + settingBatchtime: loadState('activity', 'setting_batchtime'), + methods: loadState('activity', 'methods'), + endpoint: '', + }, + getters: { + /** + * Return an array of checked activities. + * + * @param {SettingsState} state - The current state. + * @return {Array<string>} + */ + checkedActivities(state) { + const methodsEnabled = (activityKey, activity) => { + const methods = [] + if (activity.email) { + methods.push({ activityKey, method: 'email', activity }) + } + if (activity.notification) { + methods.push({ activityKey, method: 'notification', activity }) + } + return methods + } + + return Object.values(state.activityGroups) + .map(group => Object.entries(group.activities)) // [[[activityKey, activity], ...], [[activityKey, activity], ...]] + .reduce((acc, val) => acc.concat(val), []) // [[activityKey, activity], ...] + .map(([activityKey, activity]) => methodsEnabled(activityKey, activity)) // [[{activityKey, method, activity}, ...], ...] + .reduce((acc, val) => acc.concat(val), []) + .filter(({ activity, method }) => activity[method]) + .map(({ activityKey, method }) => `${activityKey}_${method}`) // ['enabled_activity_key', ...] + }, + }, + mutations: { + /** + * Update the 'enabled' state of a notification method for a given group/activity/method tuple + * + * @param {SettingsState} state - The current state. + * @param {object} payload - The payload. + * @param {string} payload.groupKey - The targeted group + * @param {string} payload.activityKey - The targeted activity + * @param {string} payload.methodKey - The targeted method + * @param {string} payload.value - The value to set + */ + SET_METHOD_FOR_METHOD_AND_ACTIVITY(state, { groupKey, activityKey, methodKey, value }) { + const group = state.activityGroups[groupKey] + const activity = group.activities[activityKey] + + if (isActivityEnabled(activity, methodKey)) { + activity[methodKey] = value + } + }, + /** + * Set the endpoint used to save the settings. + * + * @param {SettingsState} state - The current state. + * @param {object} payload - The payload. + * @param {string} payload.endpoint - Where to POST the saveSettings request. + */ + SET_ENDPOINT(state, { endpoint }) { + state.endpoint = endpoint + }, + /** + * Set the batch time. + * + * @param {SettingsState} state - The current state. + * @param {object} payload - The payload. + * @param {0|1|2|3} payload.settingBatchtime - The selected batch time. + */ + SET_SETTING_BATCHTIME(state, { settingBatchtime }) { + state.settingBatchtime = settingBatchtime + }, + /** + * Toggle activity digest. + * + * @param {SettingsState} state - The current state. + * @param {object} payload - The payload. + * @param {boolean} payload.activityDigestEnabled - Enabled status of the activity digest. + */ + TOGGLE_ACTIVITY_DIGEST(state, { activityDigestEnabled }) { + state.activityDigestEnabled = activityDigestEnabled + }, + /** + * Toggle the availability of mail notifications + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {boolean} payload.emailEnabled - Enabled status of the email notifications. + * @param state + */ + TOGGLE_EMAIL_ENABLED(state, { emailEnabled }) { + state.emailEnabled = emailEnabled + }, + }, + actions: { + /** + * Set the endpoint used to save the settings. + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {string} payload.endpoint - Where to POST the saveSettings request. + */ + setEndpoint({ commit }, { endpoint }) { + commit('SET_ENDPOINT', { endpoint }) + }, + /** + * Toggle the 'enabled' state of a notification method for a given group/activity/method tuple + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {string} payload.groupKey - The targeted group + * @param {string} payload.activityKey - The targeted activity + * @param {string} payload.methodKey - The targeted method + */ + toggleMethodForMethodAndActivity({ commit, state, dispatch }, { groupKey, activityKey, methodKey }) { + const activity = state.activityGroups[groupKey].activities[activityKey] + const oneInputIsChecked = isOneInputUnChecked([activity], methodKey) + + commit( + 'SET_METHOD_FOR_METHOD_AND_ACTIVITY', + { + groupKey, + activityKey, + methodKey, + value: oneInputIsChecked, + }) + + dispatch('saveSettings') + }, + /** + * Toggle the 'enabled' state of a notification method for a given group/method tuple + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {string} payload.groupKey - The targeted group + * @param {string} payload.methodKey - The targeted method + */ + toggleMethodForGroup({ commit, state, dispatch }, { groupKey, methodKey }) { + const activities = Object.values(state.activityGroups[groupKey].activities) + const oneInputIsChecked = isOneInputUnChecked(activities, methodKey) + + for (const activityKey in state.activityGroups[groupKey].activities) { + commit( + 'SET_METHOD_FOR_METHOD_AND_ACTIVITY', + { + groupKey, + activityKey, + methodKey, + value: oneInputIsChecked, + }) + } + + dispatch('saveSettings') + }, + /** + * Toggle the 'enabled' state of a notification method for a given group/activity tuple + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {string} payload.groupKey - The targeted group + * @param {string} payload.activityKey - The targeted activity + */ + toggleMethodsForActivity({ commit, state, dispatch }, { groupKey, activityKey }) { + const activity = state.activityGroups[groupKey].activities[activityKey] + const oneInputIsChecked = activity.methods.map(method => isOneInputUnChecked([activity], method)).includes(true) + + for (const methodKey of activity.methods) { + commit( + 'SET_METHOD_FOR_METHOD_AND_ACTIVITY', + { + groupKey, + activityKey, + methodKey, + value: oneInputIsChecked, + }) + } + + dispatch('saveSettings') + }, + /** + * Set the batch time. + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {0|1|2|3} payload.settingBatchtime - The selected batch time. + */ + setSettingBatchtime({ commit, dispatch }, { settingBatchtime }) { + commit( + 'SET_SETTING_BATCHTIME', + { + settingBatchtime, + }) + + dispatch('saveSettings') + }, + /** + * Toggle the activity digest. + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {boolean} payload.activityDigestEnabled - Enabled status of the activity digest. + */ + toggleActivityDigestEnabled({ commit, dispatch }, { activityDigestEnabled }) { + commit( + 'TOGGLE_ACTIVITY_DIGEST', + { + activityDigestEnabled, + }) + + dispatch('saveSettings') + }, + /** + * Toggle the availability of mail notifications + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store - + * @param {object} payload - The payload. + * @param {boolean} payload.emailEnabled - Enabled status of the email notifications. + */ + toggleEmailEnabled({ commit }, { emailEnabled }) { + commit( + 'TOGGLE_EMAIL_ENABLED', + { + emailEnabled, + }) + + try { + + OCP.AppConfig.setValue( + 'activity', 'enable_email', + emailEnabled ? 'yes' : 'no' + ) + + showSuccess(t('activity', 'Your settings have been updated.')) + } catch (error) { + showError(t('activity', 'Unable to save the settings')) + logger.error('An error occurred while saving the activity settings', error) + } + }, + /** + * Save the currently displayed settings + * + * @param {import('vuex').ActionContext<SettingsState, SettingsState>} _ - + */ + async saveSettings({ state, getters }) { + try { + const form = new FormData() + getters.checkedActivities.forEach(activity => { + form.append(activity, '1') + }) + + form.append('notify_setting_batchtime', `${state.settingBatchtime}`) + form.append('activity_digest', `${state.activityDigestEnabled ? 1 : 0}`) + + const response = await axios.post(generateUrl(state.endpoint), form) + + showSuccess(response.data.data.message) + } catch (error) { + showError(t('activity', 'Unable to save the settings')) + logger.error('An error occurred while saving the activity settings', error) + } + }, + }, +}) + +export default store diff --git a/src/tests/setup.js b/src/tests/setup.js index b29e9938..96b67d34 100644 --- a/src/tests/setup.js +++ b/src/tests/setup.js @@ -3,7 +3,7 @@ * * @author Louis Chemineau <louis@chmn.me> * - * @license GPL-3.0-or-later + * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue new file mode 100644 index 00000000..c31f335a --- /dev/null +++ b/src/views/AdminSettings.vue @@ -0,0 +1,68 @@ +<!-- + - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + - + - @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 Affero 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 Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <SettingsSection :title="t('activity', 'Notification')"> + <CheckboxRadioSwitch type="checkbox" + :checked="emailEnabled" + @update:checked="toggleEmailEnabled({emailEnabled: $event})"> + {{ t('activity', 'Enable notification emails') }} + </CheckboxRadioSwitch> + </SettingsSection> +</template> + +<script> +import { mapActions, mapState } from 'vuex' +import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch' +import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection' + +export default { + name: 'AdminSettings', + components: { + CheckboxRadioSwitch, + SettingsSection, + }, + + mounted() { + this.setEndpoint({ endpoint: '/apps/activity/settings/admin' }) + }, + + methods: { + ...mapActions([ + 'setEndpoint', + 'toggleEmailEnabled', + ]), + }, + + computed: { + ...mapState({ + emailEnabled: 'emailEnabled', + }), + settingDescription() { + if (this.emailEnabled) { + return t('activity', 'Choose for which activities you want to get an email or push notification.') + } else { + return t('activity', 'Choose for which activities you want to get a push notification.') + } + }, + }, +} + +</script> diff --git a/src/views/DailySummary.vue b/src/views/DailySummary.vue new file mode 100644 index 00000000..a7dc4f4f --- /dev/null +++ b/src/views/DailySummary.vue @@ -0,0 +1,38 @@ +<template> + <SettingsSection :title="t('activity', 'Daily activtiy summary')"> + <CheckboxRadioSwitch :checked="activityDigestEnabled" @update:checked="toggleActivityDigestEnabled({activityDigestEnabled: $event})"> + {{ t('activity', 'Send daily activity summary in the morning') }} + </CheckboxRadioSwitch> + </SettingsSection> +</template> + +<script> +import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch' +import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection' +import { mapActions, mapState } from 'vuex' + +export default { + name: 'DailySummary', + components: { + CheckboxRadioSwitch, + SettingsSection, + }, + + computed: { + ...mapState([ + 'activityDigestEnabled', + ]), + }, + + mounted() { + this.setEndpoint({ endpoint: '/apps/activity/settings' }) + }, + + methods: { + ...mapActions([ + 'setEndpoint', + 'toggleActivityDigestEnabled', + ]), + }, +} +</script> diff --git a/src/views/DefaultActivitySettings.vue b/src/views/DefaultActivitySettings.vue new file mode 100644 index 00000000..fc2c0c52 --- /dev/null +++ b/src/views/DefaultActivitySettings.vue @@ -0,0 +1,58 @@ +<!-- + - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + - + - @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 Affero 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 Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <SettingsSection :title="t('activity', 'Default settings')" + :description="t('activity', 'Configure the default notification settings for new users.')"> + <ActivityGrid /> + </SettingsSection> +</template> + +<script> +import { mapActions, mapState } from 'vuex' +import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection' +import ActivityGrid from '../components/ActivityGrid' + +export default { + name: 'DefaultActivitySettings', + components: { + ActivityGrid, + SettingsSection, + }, + + mounted() { + this.setEndpoint({ endpoint: '/apps/activity/settings/admin' }) + }, + + methods: { + ...mapActions([ + 'setEndpoint', + 'toggleEmailEnabled', + ]), + }, + + computed: { + ...mapState({ + emailEnabled: 'emailEnabled', + }), + }, +} + +</script> diff --git a/src/views/UserSettings.vue b/src/views/UserSettings.vue new file mode 100644 index 00000000..b9d6beeb --- /dev/null +++ b/src/views/UserSettings.vue @@ -0,0 +1,68 @@ +<!-- + - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + - + - @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 Affero 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 Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <SettingsSection :title="t('activity', 'Activity')" + :description="settingDescription"> + <ActivityGrid /> + <EmailSettings /> + </SettingsSection> +</template> + +<script> +import { mapActions, mapState } from 'vuex' +import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection' +import EmailSettings from '../components/EmailSettings' +import ActivityGrid from '../components/ActivityGrid' + +export default { + name: 'UserSettings', + components: { + SettingsSection, + EmailSettings, + ActivityGrid, + }, + + mounted() { + this.setEndpoint({ endpoint: '/apps/activity/settings' }) + }, + + methods: { + ...mapActions([ + 'setEndpoint', + 'toggleEmailEnabled', + ]), + }, + + computed: { + ...mapState({ + emailEnabled: 'emailEnabled', + }), + settingDescription() { + if (this.emailEnabled) { + return t('activity', 'Choose for which activities you want to get an email or push notification.') + } else { + return t('activity', 'Choose for which activities you want to get a push notification.') + } + }, + }, +} + +</script> |