diff options
Diffstat (limited to 'src/components/settings_modal')
54 files changed, 2088 insertions, 477 deletions
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js new file mode 100644 index 00000000..a2c27c2a --- /dev/null +++ b/src/components/settings_modal/admin_tabs/frontends_tab.js @@ -0,0 +1,64 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import StringSetting from '../helpers/string_setting.vue' +import GroupSetting from '../helpers/group_setting.vue' +import Popover from 'src/components/popover/popover.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faGlobe +) + +const FrontendsTab = { + provide () { + return { + defaultDraftMode: true, + defaultSource: 'admin' + } + }, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + StringSetting, + GroupSetting, + Popover + }, + created () { + if (this.user.rights.admin) { + this.$store.dispatch('loadFrontendsStuff') + } + }, + computed: { + frontends () { + return this.$store.state.adminSettings.frontends + }, + ...SharedComputedObject() + }, + methods: { + update (frontend, suggestRef) { + const ref = suggestRef || frontend.refs[0] + const { name } = frontend + const payload = { name, ref } + + this.$store.state.api.backendInteractor.installFrontend({ payload }) + .then((externalUser) => { + this.$store.dispatch('loadFrontendsStuff') + }) + }, + setDefault (frontend, suggestRef) { + const ref = suggestRef || frontend.refs[0] + const { name } = frontend + + this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } }) + } + } +} + +export default FrontendsTab diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.scss b/src/components/settings_modal/admin_tabs/frontends_tab.scss new file mode 100644 index 00000000..e3e04bc6 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/frontends_tab.scss @@ -0,0 +1,13 @@ +.frontends-tab { + .cards-list { + padding: 0; + } + + dd { + text-overflow: ellipsis; + word-wrap: nowrap; + white-space: nowrap; + overflow-x: hidden; + max-width: 10em; + } +} diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue new file mode 100644 index 00000000..13b8fa6b --- /dev/null +++ b/src/components/settings_modal/admin_tabs/frontends_tab.vue @@ -0,0 +1,184 @@ +<template> + <div + class="frontends-tab" + :label="$t('admin_dash.tabs.frontends')" + > + <div class="setting-item"> + <h2>{{ $t('admin_dash.tabs.frontends') }}</h2> + <p>{{ $t('admin_dash.frontend.wip_notice') }}</p> + <ul class="setting-list"> + <li> + <h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3> + <p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p> + <p>{{ $t('admin_dash.frontend.default_frontend_tip2') }}</p> + <ul class="setting-list"> + <li> + <StringSetting path=":pleroma.:frontends.:primary.name" /> + </li> + <li> + <StringSetting path=":pleroma.:frontends.:primary.ref" /> + </li> + <li> + <GroupSetting path=":pleroma.:frontends.:primary" /> + </li> + </ul> + </li> + </ul> + <div class="setting-list"> + <h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3> + <ul class="cards-list"> + <li + v-for="frontend in frontends" + :key="frontend.name" + > + <strong>{{ frontend.name }}</strong> + {{ ' ' }} + <span v-if="adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name"> + <i18n-t + v-if="adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]" + keypath="admin_dash.frontend.is_default" + /> + <i18n-t + v-else + keypath="admin_dash.frontend.is_default_custom" + > + <template #version> + <code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> + </template> + </i18n-t> + </span> + <dl> + <dt>{{ $t('admin_dash.frontend.repository') }}</dt> + <dd> + <a + :href="frontend.git" + target="_blank" + >{{ frontend.git }}</a> + </dd> + <template v-if="expertLevel"> + <dt>{{ $t('admin_dash.frontend.versions') }}</dt> + <dd + v-for="ref in frontend.refs" + :key="ref" + > + <code>{{ ref }}</code> + </dd> + </template> + <dt v-if="expertLevel"> + {{ $t('admin_dash.frontend.build_url') }} + </dt> + <dd v-if="expertLevel"> + <a + :href="frontend.build_url" + target="_blank" + >{{ frontend.build_url }}</a> + </dd> + </dl> + <div> + <span class="btn-group"> + <button + class="button button-default btn" + type="button" + @click="update(frontend)" + > + {{ + frontend.installed + ? $t('admin_dash.frontend.reinstall') + : $t('admin_dash.frontend.install') + }} + </button> + <Popover + v-if="frontend.refs.length > 1" + trigger="click" + class="button-dropdown" + placement="bottom" + > + <template #content> + <div class="dropdown-menu"> + <button + v-for="ref in frontend.refs" + :key="ref" + class="button-default dropdown-item" + @click="update(frontend, ref)" + > + <i18n-t keypath="admin_dash.frontend.install_version"> + <template #version> + <code>{{ ref }}</code> + </template> + </i18n-t> + </button> + </div> + </template> + <template #trigger> + <button + class="button button-default btn dropdown-button" + type="button" + :title="$t('admin_dash.frontend.more_install_options')" + > + <FAIcon icon="chevron-down" /> + </button> + </template> + </Popover> + </span> + <span + v-if="frontend.installed && frontend.name !== 'admin-fe'" + class="btn-group" + > + <button + class="button button-default btn" + type="button" + :disabled=" + adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name && + adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0] + " + @click="setDefault(frontend)" + > + {{ + $t('admin_dash.frontend.set_default') + }} + </button> + {{ ' ' }} + <Popover + v-if="frontend.refs.length > 1" + trigger="click" + class="button-dropdown" + placement="bottom" + > + <template #content> + <div class="dropdown-menu"> + <button + v-for="ref in frontend.refs.slice(1)" + :key="ref" + class="button-default dropdown-item" + @click="setDefault(frontend, ref)" + > + <i18n-t keypath="admin_dash.frontend.set_default_version"> + <template #version> + <code>{{ ref }}</code> + </template> + </i18n-t> + </button> + </div> + </template> + <template #trigger> + <button + class="button button-default btn dropdown-button" + type="button" + :title="$t('admin_dash.frontend.more_default_options')" + > + <FAIcon icon="chevron-down" /> + </button> + </template> + </Popover> + </span> + </div> + </li> + </ul> + </div> + </div> + </div> +</template> + +<script src="./frontends_tab.js"></script> + +<style lang="scss" src="./frontends_tab.scss"></style> diff --git a/src/components/settings_modal/admin_tabs/instance_tab.js b/src/components/settings_modal/admin_tabs/instance_tab.js new file mode 100644 index 00000000..b07bafe8 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/instance_tab.js @@ -0,0 +1,38 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import StringSetting from '../helpers/string_setting.vue' +import GroupSetting from '../helpers/group_setting.vue' +import AttachmentSetting from '../helpers/attachment_setting.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faGlobe +) + +const InstanceTab = { + provide () { + return { + defaultDraftMode: true, + defaultSource: 'admin' + } + }, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + StringSetting, + AttachmentSetting, + GroupSetting + }, + computed: { + ...SharedComputedObject() + } +} + +export default InstanceTab diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue new file mode 100644 index 00000000..a6be776b --- /dev/null +++ b/src/components/settings_modal/admin_tabs/instance_tab.vue @@ -0,0 +1,196 @@ +<template> + <div :label="$t('admin_dash.tabs.instance')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.instance.instance') }}</h2> + <ul class="setting-list"> + <li> + <StringSetting path=":pleroma.:instance.:name" /> + </li> + <li> + <StringSetting path=":pleroma.:instance.:email" /> + </li> + <li> + <StringSetting path=":pleroma.:instance.:description" /> + </li> + <li> + <StringSetting path=":pleroma.:instance.:short_description" /> + </li> + <li> + <AttachmentSetting path=":pleroma.:instance.:instance_thumbnail" /> + </li> + <li> + <AttachmentSetting path=":pleroma.:instance.:background_image" /> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('admin_dash.instance.registrations') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path=":pleroma.:instance.:registrations_open" /> + <ul class="setting-list suboptions"> + <li> + <BooleanSetting + path=":pleroma.:instance.:invites_enabled" + parent-path=":pleroma.:instance.:registrations_open" + parent-invert + /> + </li> + </ul> + </li> + <li> + <BooleanSetting path=":pleroma.:instance.:birthday_required" /> + <ul class="setting-list suboptions"> + <li> + <IntegerSetting + path=":pleroma.:instance.:birthday_min_age" + parent-path=":pleroma.:instance.:birthday_required" + /> + </li> + </ul> + </li> + <li> + <BooleanSetting path=":pleroma.:instance.:account_activation_required" /> + </li> + <li> + <BooleanSetting path=":pleroma.:instance.:account_approval_required" /> + </li> + <li> + <h3>{{ $t('admin_dash.instance.captcha_header') }}</h3> + <ul class="setting-list"> + <li> + <BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" /> + <ul class="setting-list suboptions"> + <li> + <ChoiceSetting + :path="[':pleroma', 'Pleroma.Captcha', ':method']" + :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']" + :option-label-map="{ + 'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'), + 'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha') + }" + /> + <IntegerSetting + :path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']" + :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']" + /> + </li> + <li + v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'" + > + <h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4> + <ul class="setting-list"> + <li> + <StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" /> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('admin_dash.instance.access') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting + override-backend-description + override-backend-description-label + path=":pleroma.:instance.:public" + /> + </li> + <li> + <ChoiceSetting + override-backend-description + override-backend-description-label + path=":pleroma.:instance.:limit_to_local_content" + /> + </li> + <li v-if="expertLevel"> + <h3>{{ $t('admin_dash.instance.restrict.header') }}</h3> + <p> + {{ $t('admin_dash.instance.restrict.description') }} + </p> + <ul class="setting-list"> + <li> + <h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:timelines.:local" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:timelines.:federated" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" /> + </li> + </ul> + </li> + <li> + <h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:profiles.:local" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:profiles.:remote" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" /> + </li> + </ul> + </li> + <li> + <h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:activities.:local" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:activities.:remote" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" /> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </div> + </div> +</template> + +<script src="./instance_tab.js"></script> diff --git a/src/components/settings_modal/admin_tabs/limits_tab.js b/src/components/settings_modal/admin_tabs/limits_tab.js new file mode 100644 index 00000000..684739c3 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/limits_tab.js @@ -0,0 +1,29 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import StringSetting from '../helpers/string_setting.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faGlobe +) + +const LimitsTab = { + data () {}, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + StringSetting + }, + computed: { + ...SharedComputedObject() + } +} + +export default LimitsTab diff --git a/src/components/settings_modal/admin_tabs/limits_tab.vue b/src/components/settings_modal/admin_tabs/limits_tab.vue new file mode 100644 index 00000000..ef4b9271 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/limits_tab.vue @@ -0,0 +1,136 @@ +<template> + <div :label="$t('admin_dash.tabs.limits')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2> + <ul class="setting-list"> + <li> + <h3>{{ $t('admin_dash.limits.posts') }}</h3> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:remote_limit" + expert="1" + draft-mode + /> + </li> + </ul> + </li> + <li> + <h3>{{ $t('admin_dash.limits.uploads') }}</h3> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:description_limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:upload_limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_media_attachments" + draft-mode + /> + </li> + </ul> + </li> + <li> + <h3>{{ $t('admin_dash.limits.users') }}</h3> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_pinned_statuses" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:user_bio_length" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:user_name_length" + draft-mode + /> + </li> + <li> + <h4>{{ $t('admin_dash.limits.profile_fields') }}</h4> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_account_fields" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_remote_account_fields" + draft-mode + expert="1" + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:account_field_name_length" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:account_field_value_length" + draft-mode + /> + </li> + </ul> + </li> + <li> + <h4>{{ $t('admin_dash.limits.user_uploads') }}</h4> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:avatar_upload_limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:banner_upload_limit" + draft-mode + /> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </div> + </div> +</template> + +<script src="./limits_tab.js"></script> diff --git a/src/components/settings_modal/helpers/attachment_setting.js b/src/components/settings_modal/helpers/attachment_setting.js new file mode 100644 index 00000000..ac5c6f86 --- /dev/null +++ b/src/components/settings_modal/helpers/attachment_setting.js @@ -0,0 +1,43 @@ +import Setting from './setting.js' +import { fileTypeExt } from 'src/services/file_type/file_type.service.js' +import MediaUpload from 'src/components/media_upload/media_upload.vue' +import Attachment from 'src/components/attachment/attachment.vue' + +export default { + ...Setting, + props: { + ...Setting.props, + acceptTypes: { + type: String, + required: false, + default: 'image/*' + } + }, + components: { + ...Setting.components, + MediaUpload, + Attachment + }, + computed: { + ...Setting.computed, + attachment () { + const path = this.realDraftMode ? this.draft : this.state + // The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage. + const url = path.includes('://') ? path : this.$store.state.instance.server + path + return { + mimetype: fileTypeExt(url), + url + } + } + }, + methods: { + ...Setting.methods, + setMediaFile (fileInfo) { + if (this.realDraftMode) { + this.draft = fileInfo.url + } else { + this.configSink(this.path, fileInfo.url) + } + } + } +} diff --git a/src/components/settings_modal/helpers/attachment_setting.vue b/src/components/settings_modal/helpers/attachment_setting.vue new file mode 100644 index 00000000..bbc5172c --- /dev/null +++ b/src/components/settings_modal/helpers/attachment_setting.vue @@ -0,0 +1,96 @@ +<template> + <span + v-if="matchesExpertLevel" + class="AttachmentSetting" + > + <label + :for="path" + :class="{ 'faint': shouldBeDisabled }" + > + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> + + </label> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> + <div class="attachment-input"> + <div>{{ $t('settings.url') }}</div> + <div class="controls"> + <input + :id="path" + class="string-input" + :disabled="shouldBeDisabled" + :value="realDraftMode ? draft : state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + </div> + <div>{{ $t('settings.preview') }}</div> + <Attachment + class="attachment" + :compact="compact" + :attachment="attachment" + size="small" + hide-description + @setMedia="onMedia" + @naturalSizeLoad="onNaturalSizeLoad" + /> + <div class="controls"> + <MediaUpload + ref="mediaUpload" + class="media-upload-icon" + :drop-files="dropFiles" + normal-button + :accept-types="acceptTypes" + @uploaded="setMediaFile" + @upload-failed="uploadFailed" + /> + </div> + </div> + <DraftButtons /> + </span> +</template> + +<script src="./attachment_setting.js"></script> + +<style lang="scss"> +.AttachmentSetting { + .attachment { + display: block; + width: 100%; + height: 15em; + margin-bottom: 0.5em; + } + + .attachment-input { + margin-left: 1em; + display: flex; + flex-direction: column; + width: 20em; + } + + .controls { + margin-bottom: 0.5em; + + input, + button { + width: 100%; + } + } +} +</style> diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js index 2e6992cb..199d3d0f 100644 --- a/src/components/settings_modal/helpers/boolean_setting.js +++ b/src/components/settings_modal/helpers/boolean_setting.js @@ -1,56 +1,31 @@ -import { get, set } from 'lodash' import Checkbox from 'src/components/checkbox/checkbox.vue' -import ModifiedIndicator from './modified_indicator.vue' -import ServerSideIndicator from './server_side_indicator.vue' +import Setting from './setting.js' + export default { + ...Setting, + props: { + ...Setting.props, + indeterminateState: [String, Object] + }, components: { - Checkbox, - ModifiedIndicator, - ServerSideIndicator + ...Setting.components, + Checkbox }, - props: [ - 'path', - 'disabled', - 'expert' - ], computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value - } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isServerSide () { - return this.path.startsWith('serverSide_') - }, - isChanged () { - return !this.path.startsWith('serverSide_') && this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel + ...Setting.computed, + isIndeterminate () { + return this.visibleState === this.indeterminateState } }, methods: { - update (e) { - const [firstSegment, ...rest] = this.path.split('.') - set(this.$parent, this.path, e) - // Updating nested properties does not trigger update on its parent. - // probably still not as reliable, but works for depth=1 at least - if (rest.length > 0) { - set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) }) + ...Setting.methods, + getValue (e) { + // Basic tri-state toggle implementation + if (!!this.indeterminateState && !e && this.visibleState === true) { + // If we have indeterminate state, switching from true to false first goes through indeterminate + return this.indeterminateState } - }, - reset () { - set(this.$parent, this.path, this.defaultState) + return e } } } diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue index 41142966..5a9eab34 100644 --- a/src/components/settings_modal/helpers/boolean_setting.vue +++ b/src/components/settings_modal/helpers/boolean_setting.vue @@ -4,23 +4,37 @@ class="BooleanSetting" > <Checkbox - :model-value="state" - :disabled="disabled" + :model-value="visibleState" + :disabled="shouldBeDisabled" + :indeterminate="isIndeterminate" @update:modelValue="update" > <span - v-if="!!$slots.default" class="label" + :class="{ 'faint': shouldBeDisabled }" > - <slot /> + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> </span> - {{ ' ' }} - <ModifiedIndicator - :changed="isChanged" - :onclick="reset" - /> - <ServerSideIndicator :server-side="isServerSide" /> </Checkbox> + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> </label> </template> diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js index 3da559fe..bdeece76 100644 --- a/src/components/settings_modal/helpers/choice_setting.js +++ b/src/components/settings_modal/helpers/choice_setting.js @@ -1,51 +1,41 @@ -import { get, set } from 'lodash' import Select from 'src/components/select/select.vue' -import ModifiedIndicator from './modified_indicator.vue' -import ServerSideIndicator from './server_side_indicator.vue' +import Setting from './setting.js' + export default { + ...Setting, components: { - Select, - ModifiedIndicator, - ServerSideIndicator + ...Setting.components, + Select }, - props: [ - 'path', - 'disabled', - 'options', - 'expert' - ], - computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') + props: { + ...Setting.props, + options: { + type: Array, + required: false }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value + optionLabelMap: { + type: Object, + required: false, + default: {} + } + }, + computed: { + ...Setting.computed, + realOptions () { + if (this.realSource === 'admin') { + return this.backendDescriptionSuggestions.map(x => ({ + key: x, + value: x, + label: this.optionLabelMap[x] || x + })) } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isServerSide () { - return this.path.startsWith('serverSide_') - }, - isChanged () { - return !this.path.startsWith('serverSide_') && this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel + return this.options } }, methods: { - update (e) { - set(this.$parent, this.path, e) - }, - reset () { - set(this.$parent, this.path, this.defaultState) + ...Setting.methods, + getValue (e) { + return e } } } diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue index d141a0d6..114e9b7d 100644 --- a/src/components/settings_modal/helpers/choice_setting.vue +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -3,15 +3,20 @@ v-if="matchesExpertLevel" class="ChoiceSetting" > - <slot /> + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel }} + </template> + <template v-else> + <slot /> + </template> {{ ' ' }} <Select - :model-value="state" + :model-value="realDraftMode ? draft :state" :disabled="disabled" @update:modelValue="update" > <option - v-for="option in options" + v-for="option in realOptions" :key="option.key" :value="option.value" > @@ -23,13 +28,15 @@ :changed="isChanged" :onclick="reset" /> - <ServerSideIndicator :server-side="isServerSide" /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + > + {{ backendDescriptionDescription + ' ' }} + </p> </label> </template> <script src="./choice_setting.js"></script> - -<style lang="scss"> -.ChoiceSetting { -} -</style> diff --git a/src/components/settings_modal/helpers/draft_buttons.vue b/src/components/settings_modal/helpers/draft_buttons.vue new file mode 100644 index 00000000..46a70e86 --- /dev/null +++ b/src/components/settings_modal/helpers/draft_buttons.vue @@ -0,0 +1,88 @@ +<!-- this is a helper exclusive to Setting components --> +<!-- TODO make it reusable --> +<template> + <span + class="DraftButtons" + > + <Popover + v-if="$parent.isDirty" + trigger="hover" + normal-button + :trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }" + @click="$parent.commitDraft" + > + <template #trigger> + {{ $t('settings.commit_value') }} + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.commit_value_tooltip') }} + </div> + </template> + </Popover> + <Popover + v-if="$parent.isDirty" + trigger="hover" + normal-button + :trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }" + @click="$parent.reset" + > + <template #trigger> + {{ $t('settings.reset_value') }} + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.reset_value_tooltip') }} + </div> + </template> + </Popover> + <Popover + v-if="$parent.canHardReset" + trigger="hover" + normal-button + :trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }" + @click="$parent.hardReset" + > + <template #trigger> + {{ $t('settings.hard_reset_value') }} + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.hard_reset_value_tooltip') }} + </div> + </template> + </Popover> + </span> +</template> + +<script> +import Popover from 'src/components/popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faWrench } from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench +) + +export default { + components: { Popover }, + props: ['changed'] +} +</script> + +<style lang="scss"> +.DraftButtons { + display: inline-block; + position: relative; + + .button-default { + margin-left: 0.5em; + } +} + +.draft-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; +} +</style> diff --git a/src/components/settings_modal/helpers/float_setting.vue b/src/components/settings_modal/helpers/float_setting.vue new file mode 100644 index 00000000..15edb3c3 --- /dev/null +++ b/src/components/settings_modal/helpers/float_setting.vue @@ -0,0 +1,16 @@ +<template> + <NumberSetting + v-bind="$attrs" + > + <slot /> + </NumberSetting> +</template> + +<script> +import NumberSetting from './number_setting.vue' +export default { + components: { + NumberSetting + } +} +</script> diff --git a/src/components/settings_modal/helpers/group_setting.js b/src/components/settings_modal/helpers/group_setting.js new file mode 100644 index 00000000..23a2a202 --- /dev/null +++ b/src/components/settings_modal/helpers/group_setting.js @@ -0,0 +1,13 @@ +import { isEqual } from 'lodash' + +import Setting from './setting.js' + +export default { + ...Setting, + computed: { + ...Setting.computed, + isDirty () { + return !isEqual(this.state, this.draft) + } + } +} diff --git a/src/components/settings_modal/helpers/group_setting.vue b/src/components/settings_modal/helpers/group_setting.vue new file mode 100644 index 00000000..a4df4bf3 --- /dev/null +++ b/src/components/settings_modal/helpers/group_setting.vue @@ -0,0 +1,15 @@ +<template> + <span + v-if="matchesExpertLevel" + class="GroupSetting" + > + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + </span> +</template> + +<script src="./group_setting.js"></script> diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js deleted file mode 100644 index e64d0cee..00000000 --- a/src/components/settings_modal/helpers/integer_setting.js +++ /dev/null @@ -1,44 +0,0 @@ -import { get, set } from 'lodash' -import ModifiedIndicator from './modified_indicator.vue' -export default { - components: { - ModifiedIndicator - }, - props: { - path: String, - disabled: Boolean, - min: Number, - expert: [Number, String] - }, - computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value - } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isChanged () { - return this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel - } - }, - methods: { - update (e) { - set(this.$parent, this.path, parseInt(e.target.value)) - }, - reset () { - set(this.$parent, this.path, this.defaultState) - } - } -} diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue index 695e2673..43fa7e1a 100644 --- a/src/components/settings_modal/helpers/integer_setting.vue +++ b/src/components/settings_modal/helpers/integer_setting.vue @@ -1,27 +1,17 @@ <template> - <span - v-if="matchesExpertLevel" - class="IntegerSetting" + <NumberSetting + v-bind="$attrs" + truncate="1" > - <label :for="path"> - <slot /> - </label> - <input - :id="path" - class="number-input" - type="number" - step="1" - :disabled="disabled" - :min="min || 0" - :value="state" - @change="update" - > - {{ ' ' }} - <ModifiedIndicator - :changed="isChanged" - :onclick="reset" - /> - </span> + <slot /> + </NumberSetting> </template> -<script src="./integer_setting.js"></script> +<script> +import NumberSetting from './number_setting.vue' +export default { + components: { + NumberSetting + } +} +</script> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue index 8311533a..45db3fc2 100644 --- a/src/components/settings_modal/helpers/modified_indicator.vue +++ b/src/components/settings_modal/helpers/modified_indicator.vue @@ -5,12 +5,12 @@ > <Popover trigger="hover" + :trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }" > <template #trigger> <FAIcon icon="wrench" - :aria-label="$t('settings.setting_changed')" /> </template> <template #content> diff --git a/src/components/settings_modal/helpers/number_setting.js b/src/components/settings_modal/helpers/number_setting.js new file mode 100644 index 00000000..676a0d22 --- /dev/null +++ b/src/components/settings_modal/helpers/number_setting.js @@ -0,0 +1,24 @@ +import Setting from './setting.js' + +export default { + ...Setting, + props: { + ...Setting.props, + truncate: { + type: Number, + required: false, + default: 1 + } + }, + methods: { + ...Setting.methods, + getValue (e) { + if (!this.truncate === 1) { + return parseInt(e.target.value) + } else if (this.truncate > 1) { + return Math.trunc(e.target.value / this.truncate) * this.truncate + } + return parseFloat(e.target.value) + } + } +} diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue new file mode 100644 index 00000000..93f11331 --- /dev/null +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -0,0 +1,45 @@ +<template> + <span + v-if="matchesExpertLevel" + class="NumberSetting" + > + <label + :for="path" + :class="{ 'faint': shouldBeDisabled }" + > + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> + </label> + <input + :id="path" + class="number-input" + type="number" + :step="step || 1" + :disabled="shouldBeDisabled" + :min="min || 0" + :value="realDraftMode ? draft :state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> + </span> +</template> + +<script src="./number_setting.js"></script> diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/profile_setting_indicator.vue index bf181959..d160781b 100644 --- a/src/components/settings_modal/helpers/server_side_indicator.vue +++ b/src/components/settings_modal/helpers/profile_setting_indicator.vue @@ -1,7 +1,7 @@ <template> <span - v-if="serverSide" - class="ServerSideIndicator" + v-if="isProfile" + class="ProfileSettingIndicator" > <Popover trigger="hover" @@ -14,7 +14,7 @@ /> </template> <template #content> - <div class="serverside-tooltip"> + <div class="profilesetting-tooltip"> {{ $t('settings.setting_server_side') }} </div> </template> @@ -33,17 +33,17 @@ library.add( export default { components: { Popover }, - props: ['serverSide'] + props: ['isProfile'] } </script> <style lang="scss"> -.ServerSideIndicator { +.ProfileSettingIndicator { display: inline-block; position: relative; } -.serverside-tooltip { +.profilesetting-tooltip { margin: 0.5em 1em; min-width: 10em; text-align: center; diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js new file mode 100644 index 00000000..b3add346 --- /dev/null +++ b/src/components/settings_modal/helpers/setting.js @@ -0,0 +1,237 @@ +import ModifiedIndicator from './modified_indicator.vue' +import ProfileSettingIndicator from './profile_setting_indicator.vue' +import DraftButtons from './draft_buttons.vue' +import { get, set, cloneDeep } from 'lodash' + +export default { + components: { + ModifiedIndicator, + DraftButtons, + ProfileSettingIndicator + }, + props: { + path: { + type: [String, Array], + required: true + }, + disabled: { + type: Boolean, + default: false + }, + parentPath: { + type: [String, Array] + }, + parentInvert: { + type: Boolean, + default: false + }, + expert: { + type: [Number, String], + default: 0 + }, + source: { + type: String, + default: undefined + }, + hideDescription: { + type: Boolean + }, + swapDescriptionAndLabel: { + type: Boolean + }, + overrideBackendDescription: { + type: Boolean + }, + overrideBackendDescriptionLabel: { + type: Boolean + }, + draftMode: { + type: Boolean, + default: undefined + } + }, + inject: { + defaultSource: { + default: 'default' + }, + defaultDraftMode: { + default: false + } + }, + data () { + return { + localDraft: null + } + }, + created () { + if (this.realDraftMode && this.realSource !== 'admin') { + this.draft = this.state + } + }, + computed: { + draft: { + // TODO allow passing shared draft object? + get () { + if (this.realSource === 'admin') { + return get(this.$store.state.adminSettings.draft, this.canonPath) + } else { + return this.localDraft + } + }, + set (value) { + if (this.realSource === 'admin') { + this.$store.commit('updateAdminDraft', { path: this.canonPath, value }) + } else { + this.localDraft = value + } + } + }, + state () { + const value = get(this.configSource, this.canonPath) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + visibleState () { + return this.realDraftMode ? this.draft : this.state + }, + realSource () { + return this.source || this.defaultSource + }, + realDraftMode () { + return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode + }, + backendDescription () { + return get(this.$store.state.adminSettings.descriptions, this.path) + }, + backendDescriptionLabel () { + if (this.realSource !== 'admin') return '' + if (!this.backendDescription || this.overrideBackendDescriptionLabel) { + return this.$t([ + 'admin_dash', + 'temp_overrides', + ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')), + 'label' + ].join('.')) + } else { + return this.swapDescriptionAndLabel + ? this.backendDescription?.description + : this.backendDescription?.label + } + }, + backendDescriptionDescription () { + if (this.realSource !== 'admin') return '' + if (this.hideDescription) return null + if (!this.backendDescription || this.overrideBackendDescription) { + return this.$t([ + 'admin_dash', + 'temp_overrides', + ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')), + 'description' + ].join('.')) + } else { + return this.swapDescriptionAndLabel + ? this.backendDescription?.label + : this.backendDescription?.description + } + }, + backendDescriptionSuggestions () { + return this.backendDescription?.suggestions + }, + shouldBeDisabled () { + const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null + return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false) + }, + configSource () { + switch (this.realSource) { + case 'profile': + return this.$store.state.profileConfig + case 'admin': + return this.$store.state.adminSettings.config + default: + return this.$store.getters.mergedConfig + } + }, + configSink () { + switch (this.realSource) { + case 'profile': + return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v }) + case 'admin': + return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v }) + default: + return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) + } + }, + defaultState () { + switch (this.realSource) { + case 'profile': + return {} + default: + return get(this.$store.getters.defaultConfig, this.path) + } + }, + isProfileSetting () { + return this.realSource === 'profile' + }, + isChanged () { + switch (this.realSource) { + case 'profile': + case 'admin': + return false + default: + return this.state !== this.defaultState + } + }, + canonPath () { + return Array.isArray(this.path) ? this.path : this.path.split('.') + }, + isDirty () { + if (this.realSource === 'admin' && this.canonPath.length > 3) { + return false // should not show draft buttons for "grouped" values + } else { + return this.realDraftMode && this.draft !== this.state + } + }, + canHardReset () { + return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> ')) + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$store.state.config.expertLevel > 0 + } + }, + methods: { + getValue (e) { + return e.target.value + }, + update (e) { + if (this.realDraftMode) { + this.draft = this.getValue(e) + } else { + this.configSink(this.path, this.getValue(e)) + } + }, + commitDraft () { + if (this.realDraftMode) { + this.configSink(this.path, this.draft) + } + }, + reset () { + if (this.realDraftMode) { + this.draft = cloneDeep(this.state) + } else { + set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState)) + } + }, + hardReset () { + switch (this.realSource) { + case 'admin': + return this.$store.dispatch('resetAdminSetting', { path: this.path }) + .then(() => { this.draft = this.state }) + default: + console.warn('Hard reset not implemented yet!') + } + } + } +} diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js index 12431dca..bb3d36ac 100644 --- a/src/components/settings_modal/helpers/shared_computed_object.js +++ b/src/components/settings_modal/helpers/shared_computed_object.js @@ -1,52 +1,18 @@ -import { defaultState as configDefaultState } from 'src/modules/config.js' -import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js' - const SharedComputedObject = () => ({ user () { return this.$store.state.users.currentUser }, - // Getting values for default properties - ...Object.keys(configDefaultState) - .map(key => [ - key + 'DefaultValue', - function () { - return this.$store.getters.defaultConfig[key] - } - ]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - // Generating computed values for vuex properties - ...Object.keys(configDefaultState) - .map(key => [key, { - get () { return this.$store.getters.mergedConfig[key] }, - set (value) { - this.$store.dispatch('setOption', { name: key, value }) - } - }]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - ...Object.keys(serverSideConfigDefaultState) - .map(key => ['serverSide_' + key, { - get () { return this.$store.state.serverSideConfig[key] }, - set (value) { - this.$store.dispatch('setServerSideOption', { name: key, value }) - } - }]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - // Special cases (need to transform values or perform actions first) - useStreamingApi: { - get () { return this.$store.getters.mergedConfig.useStreamingApi }, - set (value) { - const promise = value - ? this.$store.dispatch('enableMastoSockets') - : this.$store.dispatch('disableMastoSockets') - - promise.then(() => { - this.$store.dispatch('setOption', { name: 'useStreamingApi', value }) - }).catch((e) => { - console.error('Failed starting MastoAPI Streaming socket', e) - this.$store.dispatch('disableMastoSockets') - this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false }) - }) - } + expertLevel () { + return this.$store.getters.mergedConfig.expertLevel > 0 + }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, + adminConfig () { + return this.$store.state.adminSettings.config + }, + adminDraft () { + return this.$store.state.adminSettings.draft } }) diff --git a/src/components/settings_modal/helpers/size_setting.js b/src/components/settings_modal/helpers/size_setting.js index 58697412..12cef705 100644 --- a/src/components/settings_modal/helpers/size_setting.js +++ b/src/components/settings_modal/helpers/size_setting.js @@ -1,67 +1,40 @@ -import { get, set } from 'lodash' -import ModifiedIndicator from './modified_indicator.vue' import Select from 'src/components/select/select.vue' +import Setting from './setting.js' export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'] export const defaultHorizontalUnits = ['px', 'rem', 'vw'] export const defaultVerticalUnits = ['px', 'rem', 'vh'] export default { + ...Setting, components: { - ModifiedIndicator, + ...Setting.components, Select }, props: { - path: String, - disabled: Boolean, + ...Setting.props, min: Number, units: { - type: [String], + type: Array, default: () => allCssUnits - }, - expert: [Number, String] + } }, computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, + ...Setting.computed, stateUnit () { - return (this.state || '').replace(/\d+/, '') + return this.state.replace(/\d+/, '') }, stateValue () { - return (this.state || '').replace(/\D+/, '') - }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value - } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isChanged () { - return this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel + return this.state.replace(/\D+/, '') } }, methods: { - update (e) { - set(this.$parent, this.path, e) - }, - reset () { - set(this.$parent, this.path, this.defaultState) - }, + ...Setting.methods, updateValue (e) { - set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit) + this.configSink(this.path, parseInt(e.target.value) + this.stateUnit) }, updateUnit (e) { - set(this.$parent, this.path, this.stateValue + e.target.value) + this.configSink(this.path, this.stateValue + e.target.value) } } } diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/size_setting.vue index 90c9f538..6c3fbaeb 100644 --- a/src/components/settings_modal/helpers/size_setting.vue +++ b/src/components/settings_modal/helpers/size_setting.vue @@ -45,10 +45,18 @@ <script src="./size_setting.js"></script> <style lang="scss"> -.css-unit-input, .css-unit-input select { - margin-left: 0.5em; - width: 4em !important; - max-width: 4em !important; - min-width: 4em !important; +.SizeSetting { + .number-input { + max-width: 6.5em; + } + + .css-unit-input, + .css-unit-input select { + margin-left: 0.5em; + width: 4em; + max-width: 4em; + min-width: 4em; + } } + </style> diff --git a/src/components/settings_modal/helpers/string_setting.js b/src/components/settings_modal/helpers/string_setting.js new file mode 100644 index 00000000..b368cfc8 --- /dev/null +++ b/src/components/settings_modal/helpers/string_setting.js @@ -0,0 +1,5 @@ +import Setting from './setting.js' + +export default { + ...Setting +} diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue new file mode 100644 index 00000000..0cfa61ce --- /dev/null +++ b/src/components/settings_modal/helpers/string_setting.vue @@ -0,0 +1,42 @@ +<template> + <label + v-if="matchesExpertLevel" + class="StringSetting" + > + <label + :for="path" + :class="{ 'faint': shouldBeDisabled }" + > + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> + </label> + <input + :id="path" + class="string-input" + :disabled="shouldBeDisabled" + :value="realDraftMode ? draft : state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> + </label> +</template> + +<script src="./string_setting.js"></script> diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js index 0a72dca1..ff58f2c3 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component import Popover from '../popover/popover.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' -import { cloneDeep } from 'lodash' +import { cloneDeep, isEqual } from 'lodash' import { newImporter, newExporter @@ -53,8 +53,16 @@ const SettingsModal = { Modal, Popover, Checkbox, - SettingsModalContent: getResettableAsyncComponent( - () => import('./settings_modal_content.vue'), + SettingsModalUserContent: getResettableAsyncComponent( + () => import('./settings_modal_user_content.vue'), + { + loadingComponent: PanelLoading, + errorComponent: AsyncComponentError, + delay: 0 + } + ), + SettingsModalAdminContent: getResettableAsyncComponent( + () => import('./settings_modal_admin_content.vue'), { loadingComponent: PanelLoading, errorComponent: AsyncComponentError, @@ -147,6 +155,12 @@ const SettingsModal = { PLEROMAFE_SETTINGS_MINOR_VERSION ] return clone + }, + resetAdminDraft () { + this.$store.commit('resetAdminDraft') + }, + pushAdminDraft () { + this.$store.dispatch('pushAdminDraft') } }, computed: { @@ -156,8 +170,14 @@ const SettingsModal = { modalActivated () { return this.$store.state.interface.settingsModalState !== 'hidden' }, - modalOpenedOnce () { - return this.$store.state.interface.settingsModalLoaded + modalMode () { + return this.$store.state.interface.settingsModalMode + }, + modalOpenedOnceUser () { + return this.$store.state.interface.settingsModalLoadedUser + }, + modalOpenedOnceAdmin () { + return this.$store.state.interface.settingsModalLoadedAdmin }, modalPeeked () { return this.$store.state.interface.settingsModalState === 'minimized' @@ -167,9 +187,14 @@ const SettingsModal = { return this.$store.state.config.expertLevel > 0 }, set (value) { - console.log(value) this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 }) } + }, + adminDraftAny () { + return !isEqual( + this.$store.state.adminSettings.config, + this.$store.state.adminSettings.draft + ) } } } diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 13cb0e65..49ef83e0 100644 --- a/src/components/settings_modal/settings_modal.scss +++ b/src/components/settings_modal/settings_modal.scss @@ -1,4 +1,5 @@ -@import 'src/_variables.scss'; +@import "src/variables"; + .settings-modal { overflow: hidden; @@ -6,33 +7,20 @@ .option-list { list-style-type: none; padding-left: 2em; + li { margin-bottom: 0.5em; } + .suboptions { - margin-top: 0.3em + margin-top: 0.3em; } } - &.peek { - .settings-modal-panel { - /* Explanation: - * Modal is positioned vertically centered. - * 100vh - 100% = Distance between modal's top+bottom boundaries and screen - * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen - * + 100% - we move modal completely off-screen, it's top boundary touches - * bottom of the screen - * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible - */ - transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px)); - - @media all and (max-width: 800px) { - /* For mobile, the modal takes 100% of the available screen. - This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible. - */ - transform: translateY(calc(100% - 50px)); - } - } + .setting-description { + margin-top: 0.2em; + margin-bottom: 2em; + font-size: 70%; } .settings-modal-panel { @@ -55,7 +43,9 @@ .btn { min-height: 2em; - min-width: 10em; + } + + .btn:not(.dropdown-button) { padding: 0 2em; } } @@ -63,6 +53,9 @@ .settings-footer { display: flex; + flex-wrap: wrap; + line-height: 2; + >* { margin-right: 0.5em; } @@ -72,4 +65,26 @@ flex-grow: 1; } } + + &.peek { + .settings-modal-panel { + /* Explanation: + * Modal is positioned vertically centered. + * 100vh - 100% = Distance between modal's top+bottom boundaries and screen + * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen + * + 100% - we move modal completely off-screen, it's top boundary touches + * bottom of the screen + * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible + */ + transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px)); + + @media all and (max-width: 800px) { + /* For mobile, the modal takes 100% of the available screen. + This ensures the minimized modal is always 50px above the browser bottom + bar regardless of whether or not it is visible. + */ + transform: translateY(calc(100% - 50px)); + } + } + } } diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 7b457371..4e7fd931 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -8,7 +8,7 @@ <div class="settings-modal-panel panel"> <div class="panel-heading"> <span class="title"> - {{ $t('settings.settings') }} + {{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }} </span> <transition name="fade"> <div @@ -42,10 +42,12 @@ </button> </div> <div class="panel-body"> - <SettingsModalContent v-if="modalOpenedOnce" /> + <SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" /> + <SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" /> </div> - <div class="panel-footer settings-footer"> + <div class="panel-footer settings-footer -flexible-height"> <Popover + v-if="modalMode === 'user'" class="export" trigger="click" placement="top" @@ -107,10 +109,42 @@ > {{ $t("settings.expert_mode") }} </Checkbox> + <span v-if="modalMode === 'admin'"> + <i18n-t keypath="admin_dash.wip_notice"> + <template #adminFeLink> + <a + href="/pleroma/admin/#/login-pleroma" + target="_blank" + > + {{ $t("admin_dash.old_ui_link") }} + </a> + </template> + </i18n-t> + </span> <span id="unscrolled-content" class="extra-content" /> + <span + v-if="modalMode === 'admin'" + class="admin-buttons" + > + <button + class="button-default btn" + :disabled="!adminDraftAny" + @click="resetAdminDraft" + > + {{ $t("admin_dash.reset_all") }} + </button> + {{ ' ' }} + <button + class="button-default btn" + :disabled="!adminDraftAny" + @click="pushAdminDraft" + > + {{ $t("admin_dash.commit_all") }} + </button> + </span> </div> </div> </Modal> diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js new file mode 100644 index 00000000..f94721ec --- /dev/null +++ b/src/components/settings_modal/settings_modal_admin_content.js @@ -0,0 +1,93 @@ +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' + +import InstanceTab from './admin_tabs/instance_tab.vue' +import LimitsTab from './admin_tabs/limits_tab.vue' +import FrontendsTab from './admin_tabs/frontends_tab.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faWrench, + faHand, + faLaptopCode, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench, + faHand, + faLaptopCode, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +) + +const SettingsModalAdminContent = { + components: { + TabSwitcher, + + InstanceTab, + LimitsTab, + FrontendsTab + }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + open () { + return this.$store.state.interface.settingsModalState !== 'hidden' + }, + bodyLock () { + return this.$store.state.interface.settingsModalState === 'visible' + }, + adminDbLoaded () { + return this.$store.state.adminSettings.loaded + }, + adminDescriptionsLoaded () { + return this.$store.state.adminSettings.descriptions !== null + }, + noDb () { + return this.$store.state.adminSettings.dbConfigEnabled === false + } + }, + created () { + if (this.user.rights.admin) { + this.$store.dispatch('loadAdminStuff') + } + }, + methods: { + onOpen () { + const targetTab = this.$store.state.interface.settingsModalTargetTab + // We're being told to open in specific tab + if (targetTab) { + const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => { + return elm.props && elm.props['data-tab-name'] === targetTab + }) + if (tabIndex >= 0) { + this.$refs.tabSwitcher.setTab(tabIndex) + } + } + // Clear the state of target tab, so that next time settings is opened + // it doesn't force it. + this.$store.dispatch('clearSettingsModalTargetTab') + } + }, + mounted () { + this.onOpen() + }, + watch: { + open: function (value) { + if (value) this.onOpen() + } + } +} + +export default SettingsModalAdminContent diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss index 81ab434b..c984d703 100644 --- a/src/components/settings_modal/settings_modal_content.scss +++ b/src/components/settings_modal/settings_modal_admin_content.scss @@ -1,4 +1,5 @@ -@import 'src/_variables.scss'; +@import "src/variables"; + .settings_tab-switcher { height: 100%; @@ -10,7 +11,8 @@ > div, > label { display: block; - margin-bottom: .5em; + margin-bottom: 0.5em; + &:last-child { margin-bottom: 0; } @@ -21,7 +23,7 @@ .option-list { margin: 0; - padding-left: .5em; + padding-left: 0.5em; } } @@ -46,9 +48,5 @@ color: var(--cRed, $fallback--cRed); color: $fallback--cRed; } - - .number-input { - max-width: 6em; - } } } diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue new file mode 100644 index 00000000..a7a2ac9a --- /dev/null +++ b/src/components/settings_modal/settings_modal_admin_content.vue @@ -0,0 +1,68 @@ +<template> + <tab-switcher + v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)" + ref="tabSwitcher" + class="settings_tab-switcher" + :side-tab-bar="true" + :scrollable-tabs="true" + :render-only-focused="true" + :body-scroll-lock="bodyLock" + > + <div + v-if="noDb" + :label="$t('admin_dash.tabs.nodb')" + icon="exclamation-triangle" + data-tab-name="nodb-notice" + > + <div :label="$t('admin_dash.tabs.nodb')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.nodb.heading') }}</h2> + <i18n-t keypath="admin_dash.nodb.text"> + <template #documentation> + <a + href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/" + target="_blank" + > + {{ $t("admin_dash.nodb.documentation") }} + </a> + </template> + <template #property> + <code>config :pleroma, configurable_from_database</code> + </template> + <template #value> + <code>true</code> + </template> + </i18n-t> + <p>{{ $t('admin_dash.nodb.text2') }}</p> + </div> + </div> + </div> + <div + v-if="adminDbLoaded" + :label="$t('admin_dash.tabs.instance')" + icon="wrench" + data-tab-name="general" + > + <InstanceTab /> + </div> + <div + v-if="adminDbLoaded" + :label="$t('admin_dash.tabs.limits')" + icon="hand" + data-tab-name="limits" + > + <LimitsTab /> + </div> + <div + :label="$t('admin_dash.tabs.frontends')" + icon="laptop-code" + data-tab-name="frontends" + > + <FrontendsTab /> + </div> + </tab-switcher> +</template> + +<script src="./settings_modal_admin_content.js"></script> + +<style src="./settings_modal_admin_content.scss" lang="scss"></style> diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_user_content.js index 9ac0301f..9ac0301f 100644 --- a/src/components/settings_modal/settings_modal_content.js +++ b/src/components/settings_modal/settings_modal_user_content.js diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss new file mode 100644 index 00000000..c984d703 --- /dev/null +++ b/src/components/settings_modal/settings_modal_user_content.scss @@ -0,0 +1,52 @@ +@import "src/variables"; + +.settings_tab-switcher { + height: 100%; + + .setting-item { + border-bottom: 2px solid var(--fg, $fallback--fg); + margin: 1em 1em 1.4em; + padding-bottom: 1.4em; + + > div, + > label { + display: block; + margin-bottom: 0.5em; + + &:last-child { + margin-bottom: 0; + } + } + + .select-multiple { + display: flex; + + .option-list { + margin: 0; + padding-left: 0.5em; + } + } + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 1em; + } + + select { + min-width: 10em; + } + + textarea { + width: 100%; + max-width: 100%; + height: 100px; + } + + .unavailable, + .unavailable svg { + color: var(--cRed, $fallback--cRed); + color: $fallback--cRed; + } + } +} diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_user_content.vue index 0be76d22..0221cccb 100644 --- a/src/components/settings_modal/settings_modal_content.vue +++ b/src/components/settings_modal/settings_modal_user_content.vue @@ -78,6 +78,6 @@ </tab-switcher> </template> -<script src="./settings_modal_content.js"></script> +<script src="./settings_modal_user_content.js"></script> -<style src="./settings_modal_content.scss" lang="scss"></style> +<style src="./settings_modal_user_content.scss" lang="scss"></style> diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue index e3b7f407..48356c9b 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.vue +++ b/src/components/settings_modal/tabs/data_import_export_tab.vue @@ -78,6 +78,16 @@ {{ $t('settings.download_backup') }} </a> <span + v-else-if="backup.state === 'running'" + > + {{ $tc('settings.backup_running', backup.processed_number, { number: backup.processed_number }) }} + </span> + <span + v-else-if="backup.state === 'failed'" + > + {{ $t('settings.backup_failed') }} + </span> + <span v-else > {{ $t('settings.backup_not_ready') }} diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 5354e5db..7c37f0bc 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,4 +1,4 @@ -import { filter, trim } from 'lodash' +import { filter, trim, debounce } from 'lodash' import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue' @@ -29,11 +29,16 @@ const FilteringTab = { }, set (value) { this.muteWordsStringLocal = value + this.debouncedSetMuteWords(value) + } + }, + debouncedSetMuteWords () { + return debounce((value) => { this.$store.dispatch('setOption', { name: 'muteWords', value: filter(value.split('\n'), (word) => trim(word).length > 0) }) - } + }, 1000) } }, // Updating nested properties diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index 97046ff0..41d1b54f 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -7,13 +7,11 @@ <BooleanSetting path="hideFilteredStatuses"> {{ $t('settings.hide_filtered_statuses') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting - :disabled="hideFilteredStatuses" + parent-path="hideFilteredStatuses" + :parent-invert="true" path="hideWordFilteredPosts" > {{ $t('settings.hide_wordfiltered_statuses') }} @@ -22,7 +20,8 @@ <li> <BooleanSetting v-if="user" - :disabled="hideFilteredStatuses" + parent-path="hideFilteredStatuses" + :parent-invert="true" path="hideMutedThreads" > {{ $t('settings.hide_muted_threads') }} @@ -31,7 +30,8 @@ <li> <BooleanSetting v-if="user" - :disabled="hideFilteredStatuses" + parent-path="hideFilteredStatuses" + :parent-invert="true" path="hideMutedPosts" > {{ $t('settings.hide_muted_posts') }} diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index ea24d6ad..3f2bcb13 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -2,11 +2,12 @@ import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_setting.vue' +import FloatSetting from '../helpers/float_setting.vue' import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' -import ServerSideIndicator from '../helpers/server_side_indicator.vue' +import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faGlobe @@ -62,10 +63,11 @@ const GeneralTab = { BooleanSetting, ChoiceSetting, IntegerSetting, + FloatSetting, SizeSetting, InterfaceLanguageSwitcher, ScopeSelector, - ServerSideIndicator + ProfileSettingIndicator }, computed: { horizontalUnits () { @@ -108,7 +110,7 @@ const GeneralTab = { }, methods: { changeDefaultScope (value) { - this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value }) + this.$store.dispatch('setProfileOption', { name: 'defaultScope', value }) } } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 8561647b..f56fa8e0 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -29,14 +29,11 @@ <BooleanSetting path="streaming"> {{ $t('settings.streaming') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting path="pauseOnUnfocused" - :disabled="!streaming" + parent-path="streaming" > {{ $t('settings.pause_on_unfocused') }} </BooleanSetting> @@ -148,6 +145,56 @@ </SizeSetting> </div> </li> + <li class="select-multiple"> + <span class="label">{{ $t('settings.confirm_dialogs') }}</span> + <ul class="option-list"> + <li> + <BooleanSetting path="modalOnRepeat"> + {{ $t('settings.confirm_dialogs_repeat') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnUnfollow"> + {{ $t('settings.confirm_dialogs_unfollow') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnBlock"> + {{ $t('settings.confirm_dialogs_block') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnMute"> + {{ $t('settings.confirm_dialogs_mute') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnDelete"> + {{ $t('settings.confirm_dialogs_delete') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnLogout"> + {{ $t('settings.confirm_dialogs_logout') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnApproveFollow"> + {{ $t('settings.confirm_dialogs_approve_follow') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnDenyFollow"> + {{ $t('settings.confirm_dialogs_deny_follow') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnRemoveUserFromFollowers"> + {{ $t('settings.confirm_dialogs_remove_follower') }} + </BooleanSetting> + </li> + </ul> + </li> </ul> </div> <div class="setting-item"> @@ -163,7 +210,7 @@ </ChoiceSetting> </li> <ul - v-if="conversationDisplay !== 'linear'" + v-if="mergedConfig.conversationDisplay !== 'linear'" class="setting-list suboptions" > <li> @@ -215,12 +262,22 @@ <li> <BooleanSetting v-if="user" - path="serverSide_stripRichContent" + source="profile" + path="stripRichContent" expert="1" > {{ $t('settings.no_rich_text_description') }} </BooleanSetting> </li> + <li> + <FloatSetting + v-if="user" + path="emojiReactionsScale" + expert="1" + > + {{ $t('settings.emoji_reactions_scale') }} + </FloatSetting> + </li> <h3>{{ $t('settings.attachments') }}</h3> <li> <BooleanSetting @@ -240,7 +297,7 @@ <BooleanSetting path="preloadImage" expert="1" - :disabled="!hideNsfw" + parent-path="hideNsfw" > {{ $t('settings.preload_images') }} </BooleanSetting> @@ -249,7 +306,7 @@ <BooleanSetting path="useOneClickNsfw" expert="1" - :disabled="!hideNsfw" + parent-path="hideNsfw" > {{ $t('settings.use_one_click_nsfw') }} </BooleanSetting> @@ -262,15 +319,13 @@ > {{ $t('settings.loop_video') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting path="loopVideoSilentOnly" expert="1" - :disabled="!loopVideo || !loopSilentAvailable" + parent-path="loopVideo" + :disabled="!loopSilentAvailable" > {{ $t('settings.loop_video_silent_only') }} </BooleanSetting> @@ -368,18 +423,18 @@ <ul class="setting-list"> <li> <label for="default-vis"> - {{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" /> + {{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" /> <ScopeSelector class="scope-selector" :show-all="true" - :user-default="serverSide_defaultScope" - :initial-scope="serverSide_defaultScope" + :user-default="$store.state.profileConfig.defaultScope" + :initial-scope="$store.state.profileConfig.defaultScope" :on-scope-change="changeDefaultScope" /> </label> </li> <li> - <!-- <BooleanSetting path="serverSide_defaultNSFW"> --> + <!-- <BooleanSetting source="profile" path="defaultNSFW"> --> <BooleanSetting path="sensitiveByDefault"> {{ $t('settings.sensitive_by_default') }} </BooleanSetting> @@ -451,6 +506,14 @@ {{ $t('settings.pad_emoji') }} </BooleanSetting> </li> + <li> + <BooleanSetting + path="autocompleteSelect" + expert="1" + > + {{ $t('settings.autocomplete_select_first') }} + </BooleanSetting> + </li> </ul> </div> </div> @@ -464,6 +527,7 @@ justify-content: space-evenly; flex-wrap: wrap; } + .column-settings .size-label { display: block; margin-bottom: 0.5em; diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js index 6cfeea35..51974f9f 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -9,17 +9,20 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue import SelectableList from 'src/components/selectable_list/selectable_list.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue' import withSubscription from 'src/components/../hocs/with_subscription/with_subscription' +import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more' import Checkbox from 'src/components/checkbox/checkbox.vue' -const BlockList = withSubscription({ +const BlockList = withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchBlocks'), select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), + destroy: () => {}, childPropName: 'items' })(SelectableList) -const MuteList = withSubscription({ +const MuteList = withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchMutes'), select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), + destroy: () => {}, childPropName: 'items' })(SelectableList) diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss index 2adff847..5fa3a27b 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss @@ -1,29 +1,29 @@ .mutes-and-blocks-tab { - height: 100%; + height: 100%; - .usersearch-wrapper { - padding: 1em; - } + .usersearch-wrapper { + padding: 1em; + } - .bulk-actions { - text-align: right; - padding: 0 1em; - min-height: 2em; - } + .bulk-actions { + text-align: right; + padding: 0 1em; + min-height: 2em; + } - .bulk-action-button { - width: 10em - } + .bulk-action-button { + width: 10em; + } - .domain-mute-form { - padding: 1em; - display: flex; - flex-direction: column - } + .domain-mute-form { + padding: 1em; + display: flex; + flex-direction: column; + } - .domain-mute-button { - align-self: flex-end; - margin-top: 1em; - width: 10em - } + .domain-mute-button { + align-self: flex-end; + margin-top: 1em; + width: 10em; + } } diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index dd3806ed..fcb92135 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -4,7 +4,10 @@ <h2>{{ $t('settings.notification_setting_filters') }}</h2> <ul class="setting-list"> <li> - <BooleanSetting path="serverSide_blockNotificationsFromStrangers"> + <BooleanSetting + source="profile" + path="blockNotificationsFromStrangers" + > {{ $t('settings.notification_setting_block_from_strangers') }} </BooleanSetting> </li> @@ -67,7 +70,8 @@ </li> <li> <BooleanSetting - path="serverSide_webPushHideContents" + source="profile" + path="webPushHideContents" expert="1" > {{ $t('settings.notification_setting_hide_notification_contents') }} diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index b86faef0..eeacad48 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import localeService from 'src/services/locale/locale.service.js' +import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -32,6 +33,8 @@ const ProfileTab = { newName: this.$store.state.users.currentUser.name_unescaped, newBio: unescape(this.$store.state.users.currentUser.description), newLocked: this.$store.state.users.currentUser.locked, + newBirthday: this.$store.state.users.currentUser.birthday, + showBirthday: this.$store.state.users.currentUser.show_birthday, newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })), showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, @@ -43,7 +46,7 @@ const ProfileTab = { bannerPreview: null, background: null, backgroundPreview: null, - emailLanguage: this.$store.state.users.currentUser.language || '' + emailLanguage: this.$store.state.users.currentUser.language || [''] } }, components: { @@ -125,12 +128,14 @@ const ProfileTab = { display_name: this.newName, fields_attributes: this.newFields.filter(el => el != null), bot: this.bot, - show_role: this.showRole + show_role: this.showRole, + birthday: this.newBirthday || '', + show_birthday: this.showBirthday /* eslint-enable camelcase */ } if (this.emailLanguage) { - params.language = localeService.internalToBackendLocale(this.emailLanguage) + params.language = localeService.internalToBackendLocaleMulti(this.emailLanguage) } this.$store.state.api.backendInteractor @@ -153,7 +158,7 @@ const ProfileTab = { return false }, deleteField (index, event) { - this.$delete(this.newFields, index) + this.newFields.splice(index, 1) }, uploadFile (slot, e) { const file = e.target.files[0] @@ -257,6 +262,9 @@ const ProfileTab = { messageArgs: [error.message], level: 'error' }) + }, + propsToNative (props) { + return propsToNative(props) } } } diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss index 201f1a76..ee253ffe 100644 --- a/src/components/settings_modal/tabs/profile_tab.scss +++ b/src/components/settings_modal/tabs/profile_tab.scss @@ -1,4 +1,5 @@ -@import '../../../_variables.scss'; +@import "../../../variables"; + .profile-tab { .bio { margin: 0; @@ -8,7 +9,7 @@ padding-top: 5px; } - input[type=file] { + input[type="file"] { padding: 5px; height: auto; } @@ -52,7 +53,7 @@ right: 0.2em; border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - background-color: rgba(0, 0, 0, 0.6); + background-color: rgb(0 0 0 / 60%); opacity: 0.7; width: 1.5em; height: 1.5em; @@ -128,4 +129,9 @@ padding: 0 0.5em; } } + + .birthday-input { + display: block; + margin-bottom: 1em; + } } diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index 642d54ca..1cc850cb 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -8,11 +8,14 @@ enable-emoji-picker :suggest="emojiSuggestor" > - <input - id="username" - v-model="newName" - class="name-changer" - > + <template #default="inputProps"> + <input + id="username" + v-model="newName" + class="name-changer" + v-bind="propsToNative(inputProps)" + > + </template> </EmojiInput> <p>{{ $t('settings.bio') }}</p> <EmojiInput @@ -20,10 +23,13 @@ enable-emoji-picker :suggest="emojiUserSuggestor" > - <textarea - v-model="newBio" - class="bio resize-height" - /> + <template #default="inputProps"> + <textarea + v-model="newBio" + class="bio resize-height" + v-bind="propsToNative(inputProps)" + /> + </template> </EmojiInput> <p v-if="role === 'admin' || role === 'moderator'"> <Checkbox v-model="showRole"> @@ -35,6 +41,18 @@ </template> </Checkbox> </p> + <div> + <p>{{ $t('settings.birthday.label') }}</p> + <input + id="birthday" + v-model="newBirthday" + type="date" + class="birthday-input" + > + <Checkbox v-model="showBirthday"> + {{ $t('settings.birthday.show_birthday') }} + </Checkbox> + </div> <div v-if="maxFields > 0"> <p>{{ $t('settings.profile_fields.label') }}</p> <div @@ -48,10 +66,13 @@ hide-emoji-button :suggest="userSuggestor" > - <input - v-model="newFields[i].name" - :placeholder="$t('settings.profile_fields.name')" - > + <template #default="inputProps"> + <input + v-model="newFields[i].name" + :placeholder="$t('settings.profile_fields.name')" + v-bind="propsToNative(inputProps)" + > + </template> </EmojiInput> <EmojiInput v-model="newFields[i].value" @@ -59,10 +80,13 @@ hide-emoji-button :suggest="userSuggestor" > - <input - v-model="newFields[i].value" - :placeholder="$t('settings.profile_fields.value')" - > + <template #default="inputProps"> + <input + v-model="newFields[i].value" + :placeholder="$t('settings.profile_fields.value')" + v-bind="propsToNative(inputProps)" + > + </template> </EmojiInput> <button class="delete-field button-unstyled -hover-highlight" @@ -230,37 +254,50 @@ <h2>{{ $t('settings.account_privacy') }}</h2> <ul class="setting-list"> <li> - <BooleanSetting path="serverSide_locked"> + <BooleanSetting + source="profile" + path="locked" + > {{ $t('settings.lock_account_description') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_discoverable"> + <BooleanSetting + source="profile" + path="discoverable" + > {{ $t('settings.discoverable') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_allowFollowingMove"> + <BooleanSetting + source="profile" + path="allowFollowingMove" + > {{ $t('settings.allow_following_move') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_hideFavorites"> + <BooleanSetting + source="profile" + path="hideFavorites" + > {{ $t('settings.hide_favorites_description') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_hideFollowers"> + <BooleanSetting + source="profile" + path="hideFollowers" + > {{ $t('settings.hide_followers_description') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !serverSide_hideFollowers}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting - path="serverSide_hideFollowersCount" - :disabled="!serverSide_hideFollowers" + source="profile" + path="hideFollowersCount" + parent-path="hideFollowers" > {{ $t('settings.hide_followers_count_description') }} </BooleanSetting> @@ -268,17 +305,18 @@ </ul> </li> <li> - <BooleanSetting path="serverSide_hideFollows"> + <BooleanSetting + source="profile" + path="hideFollows" + > {{ $t('settings.hide_follows_description') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !serverSide_hideFollows}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting - path="serverSide_hideFollowsCount" - :disabled="!serverSide_hideFollows" + source="profile" + path="hideFollowsCount" + parent-path="hideFollows" > {{ $t('settings.hide_follows_count_description') }} </BooleanSetting> diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue index 455d17b6..ee5b6b13 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa.vue +++ b/src/components/settings_modal/tabs/security_tab/mfa.vue @@ -137,9 +137,11 @@ <script src="./mfa.js"></script> <style lang="scss"> -@import '../../../../_variables.scss'; +@import "../../../../variables"; + .mfa-settings { - .mfa-heading, .method-item { + .mfa-heading, + .method-item { display: flex; flex-wrap: wrap; justify-content: space-between; @@ -155,18 +157,19 @@ display: flex; justify-content: center; flex-wrap: wrap; + .qr-code { flex: 1; padding-right: 10px; } .verify { flex: 1; } - .error { margin: 4px 0 0 0; } + .error { margin: 4px 0 0; } + .confirm-otp-actions { button { width: 15em; margin-top: 5px; } - } } } diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue index d7e98b3c..923161b2 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue +++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue @@ -21,13 +21,14 @@ </template> <script src="./mfa_backup_codes.js"></script> <style lang="scss"> -@import '../../../../_variables.scss'; +@import "../../../../variables"; .mfa-backup-codes { .warning { color: $fallback--cOrange; color: var(--cOrange, $fallback--cOrange); } + .backup-codes { font-family: var(--postCodeFont, monospace); } diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue index 6e03bef4..d36d478f 100644 --- a/src/components/settings_modal/tabs/security_tab/security_tab.vue +++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -143,8 +143,8 @@ /> </div> <div> - <i18n - path="settings.new_alias_target" + <i18n-t + keypath="settings.new_alias_target" tag="p" > <code @@ -152,7 +152,7 @@ > foo@example.org </code> - </i18n> + </i18n-t> <input v-model="addAliasTarget" > @@ -175,16 +175,16 @@ <h2>{{ $t('settings.move_account') }}</h2> <p>{{ $t('settings.move_account_notes') }}</p> <div> - <i18n - path="settings.move_account_target" + <i18n-t + keypath="settings.move_account_target" tag="p" > - <code - place="example" - > - foo@example.org - </code> - </i18n> + <template #example> + <code> + foo@example.org + </code> + </template> + </i18n-t> <input v-model="moveAccountTarget" > diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue index ba6bd529..d755279a 100644 --- a/src/components/settings_modal/tabs/theme_tab/preview.vue +++ b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -33,10 +33,10 @@ scope="global" keypath="settings.style.preview.text" > - <code style="font-family: var(--postCodeFont)"> + <code style="font-family: var(--postCodeFont);"> {{ $t('settings.style.preview.mono') }} </code> - <a style="color: var(--link)"> + <a style="color: var(--link);"> {{ $t('settings.style.preview.link') }} </a> </i18n-t> @@ -44,25 +44,25 @@ <div class="icons"> <FAIcon fixed-width - style="color: var(--cBlue)" + style="color: var(--cBlue);" class="fa-scale-110 fa-old-padding" icon="reply" /> <FAIcon fixed-width - style="color: var(--cGreen)" + style="color: var(--cGreen);" class="fa-scale-110 fa-old-padding" icon="retweet" /> <FAIcon fixed-width - style="color: var(--cOrange)" + style="color: var(--cOrange);" class="fa-scale-110 fa-old-padding" icon="star" /> <FAIcon fixed-width - style="color: var(--cRed)" + style="color: var(--cRed);" class="fa-scale-110 fa-old-padding" icon="times" /> @@ -81,7 +81,7 @@ class="faint" scope="global" > - <a style="color: var(--faintLink)"> + <a style="color: var(--faintLink);"> {{ $t('settings.style.preview.faint_link') }} </a> </i18n-t> @@ -138,6 +138,7 @@ export default {} .preview-container { position: relative; } + .underlay-preview { position: absolute; top: 0; diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss index bad6f51b..9935c2e7 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -1,20 +1,17 @@ -@import 'src/_variables.scss'; +@import "src/variables"; + .theme-tab { padding-bottom: 2em; - .theme-warning { - display: flex; - align-items: baseline; - margin-bottom: .5em; - .buttons { - .btn { - margin-bottom: .5em; - } - } - } + .preset-switcher { margin-right: 1em; } + .btn { + margin-left: 0.25em; + margin-right: 0.25em; + } + .style-control { display: flex; align-items: baseline; @@ -24,35 +21,37 @@ flex: 1; } - &.disabled { - input, select { - opacity: .5 - } - } - .opt { - margin: .5em; + margin: 0.5em; } .color-input { flex: 0 0 0; } - input, select { + input, + select { min-width: 3em; margin: 0; flex: 0; - &[type=number] { + &[type="number"] { min-width: 5em; } - &[type=range] { + &[type="range"] { flex: 1; min-width: 3em; align-self: flex-start; } } + + &.disabled { + input, + select { + opacity: 0.5; + } + } } .reset-container { @@ -63,8 +62,7 @@ .reset-container, .apply-container, .radius-container, - .color-container, - { + .color-container, { display: flex; } @@ -73,10 +71,11 @@ flex-direction: column; } - .color-container{ + .color-container { > h4 { width: 99%; } + flex-wrap: wrap; justify-content: space-between; } @@ -100,7 +99,7 @@ p { flex: 1; margin: 0; - margin-right: .5em; + margin-right: 0.5em; } } @@ -112,15 +111,16 @@ min-width: 1px; flex: 0 auto; padding: 0 1em; - margin-bottom: .5em; + margin-bottom: 0.5em; } } .shadow-selector { .override { flex: 1; - margin-left: .5em; + margin-left: 0.5em; } + .select-container { margin-top: -4px; margin-bottom: -3px; @@ -136,7 +136,7 @@ .presets, .import-export { - margin-bottom: .5em; + margin-bottom: 0.5em; } .import-export { @@ -144,16 +144,17 @@ } .override { - margin-left: .5em; + margin-left: 0.5em; } } .save-load-options { flex-wrap: wrap; - margin-top: .5em; + margin-top: 0.5em; justify-content: center; + .keep-option { - margin: 0 .5em .5em; + margin: 0 0.5em 0.5em; min-width: 25%; } } @@ -179,11 +180,11 @@ flex: 1; h4 { - margin-bottom: .25em; + margin-bottom: 0.25em; } .icons { - margin-top: .5em; + margin-top: 0.5em; display: flex; i { @@ -199,8 +200,20 @@ align-items: center; } - .avatar, .avatar-alt{ - background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%); + .avatar, + .avatar-alt { + background: + linear-gradient( + 135deg, + #b8e1fc 0%, + #a9d2f3 10%, + #90bae4 25%, + #90bcea 37%, + #90bff0 50%, + #6ba8e5 51%, + #a2daf5 83%, + #bdf3fd 100% + ); color: black; font-family: sans-serif; text-align: center; @@ -251,33 +264,33 @@ } } + .radius-item { + flex-basis: auto; + } + .radius-item, .color-item { min-width: 20em; margin: 5px 6px 0 0; - display:flex; + display: flex; flex-direction: column; flex: 1 1 0; &.wide { - min-width: 60% + min-width: 60%; } &:not(.wide):nth-child(2n+1) { margin-right: 7px; - } - .color, .opacity { - display:flex; + .color, + .opacity { + display: flex; align-items: baseline; } } - .radius-item { - flex-basis: auto; - } - .theme-radius-rn, .theme-color-cl { border: 0; @@ -295,14 +308,11 @@ .theme-radius-in { min-width: 1em; - } - - .theme-radius-in { max-width: 7em; flex: 1; } - .theme-radius-lb{ + .theme-radius-lb { max-width: 50em; } @@ -310,9 +320,16 @@ padding: 20px; } - .btn { - margin-left: .25em; - margin-right: .25em; + .theme-warning { + display: flex; + align-items: baseline; + margin-bottom: 0.5em; + + .buttons { + .btn { + margin-bottom: 0.5em; + } + } } } @@ -323,6 +340,7 @@ justify-content: space-around; flex-grow: 1; + /* stylelint-disable-next-line no-descending-specificity */ .btn { flex-grow: 1; min-height: 2em; |
