diff options
Diffstat (limited to 'src/components/settings_modal/helpers')
24 files changed, 1102 insertions, 297 deletions
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..c4c04b2b --- /dev/null +++ b/src/components/settings_modal/helpers/attachment_setting.js @@ -0,0 +1,44 @@ +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, + compact: Boolean, + 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..96c80ab1 --- /dev/null +++ b/src/components/settings_modal/helpers/attachment_setting.vue @@ -0,0 +1,126 @@ +<template> + <span + v-if="matchesExpertLevel" + class="AttachmentSetting" + :class="{ '-compact': compact }" + > + <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 class="controls control-field"> + <label for="path">{{ $t('settings.url') }}</label> + <input + :id="path" + class="input string-input" + :disabled="shouldBeDisabled" + :value="realDraftMode ? draft : state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + </div> + <div v-if="!compact">{{ $t('settings.preview') }}</div> + <Attachment + class="attachment" + :compact="compact" + :attachment="attachment" + size="small" + hide-description + @setMedia="onMedia" + @naturalSizeLoad="onNaturalSizeLoad" + /> + <div class="controls control-upload"> + <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; + } + + &.-compact { + .attachment-input { + flex-direction: row; + align-items: flex-end; + } + + .attachment { + flex: 0; + order: 0; + display: block; + min-width: 4em; + height: 4em; + align-self: center; + margin-bottom: 0; + } + + .control-field { + order: 1; + min-width: 12em; + margin-left: 0.5em; + } + + .control-upload { + order: 2; + min-width: 12em; + padding: 0 0.5em; + } + } + + .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 8fdbb5d3..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,7 +28,14 @@ :changed="isChanged" :onclick="reset" /> - <ServerSideIndicator :server-side="isServerSide" /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + > + {{ backendDescriptionDescription + ' ' }} + </p> </label> </template> 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/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue new file mode 100644 index 00000000..f0465dd5 --- /dev/null +++ b/src/components/settings_modal/helpers/emoji_editing_popover.vue @@ -0,0 +1,227 @@ +<template> + <Popover + ref="emojiPopover" + trigger="click" + :placement="placement" + bound-to-selector=".emoji-list" + popover-class="emoji-tab-edit-popover popover-default" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + :disabled="disabled" + :class="{'emoji-unsaved': isEdited}" + > + <template #trigger> + <slot name="trigger" /> + </template> + <template #content> + <h3> + {{ title }} + </h3> + + <StillImage + v-if="emojiPreview" + class="emoji" + :src="emojiPreview" + /> + <div + v-else + class="emoji" + /> + + <div + v-if="newUpload" + class="emoji-tab-popover-input" + > + <input + type="file" + accept="image/*" + class="emoji-tab-popover-file input" + @change="uploadFile = $event.target.files" + > + </div> + <div> + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.shortcode') }} + <input + v-model="editedShortcode" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_shortcode')" + > + </label> + </div> + + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.filename') }} + + <input + v-model="editedFile" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_filename')" + > + </label> + </div> + + <button + class="button button-default btn" + type="button" + :disabled="newUpload ? uploadFile.length == 0 : !isEdited" + @click="newUpload ? uploadEmoji() : saveEditedEmoji()" + > + {{ $t('admin_dash.emoji.save') }} + </button> + + <template v-if="!newUpload"> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="deleteModalVisible = true" + > + {{ $t('admin_dash.emoji.delete') }} + </button> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="revertEmoji" + > + {{ $t('admin_dash.emoji.revert') }} + </button> + <ConfirmModal + v-if="deleteModalVisible" + :title="$t('admin_dash.emoji.delete_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="deleteModalVisible = false" + @accepted="deleteEmoji" + > + {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }} + </ConfirmModal> + </template> + </div> + </template> + </Popover> +</template> + +<script> +import Popover from 'components/popover/popover.vue' +import ConfirmModal from 'components/confirm_modal/confirm_modal.vue' +import StillImage from 'components/still-image/still-image.vue' + +export default { + components: { Popover, ConfirmModal, StillImage }, + inject: ['emojiAddr'], + props: { + placement: String, + disabled: { + type: Boolean, + default: false + }, + + newUpload: Boolean, + + title: String, + packName: String, + shortcode: { + type: String, + // Only exists when this is not a new upload + default: '' + }, + file: { + type: String, + // Only exists when this is not a new upload + default: '' + } + }, + emits: ['updatePackFiles', 'displayError'], + data () { + return { + uploadFile: [], + editedShortcode: this.shortcode, + editedFile: this.file, + deleteModalVisible: false + } + }, + computed: { + emojiPreview () { + if (this.newUpload && this.uploadFile.length > 0) { + return URL.createObjectURL(this.uploadFile[0]) + } else if (!this.newUpload) { + return this.emojiAddr(this.file) + } + + return null + }, + isEdited () { + return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file) + } + }, + methods: { + saveEditedEmoji () { + if (!this.isEdited) return + + this.$store.state.api.backendInteractor.updateEmojiFile( + { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false } + ).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return Promise.reject(resp.error) + } + + return resp.json() + }).then(resp => this.$emit('updatePackFiles', resp)) + }, + uploadEmoji () { + this.$store.state.api.backendInteractor.addNewEmojiFile({ + packName: this.packName, + file: this.uploadFile[0], + shortcode: this.editedShortcode, + filename: this.editedFile + }).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + this.$refs.emojiPopover.hidePopover() + + this.editedFile = '' + this.editedShortcode = '' + this.uploadFile = [] + }) + }, + revertEmoji () { + this.editedFile = this.file + this.editedShortcode = this.shortcode + }, + deleteEmoji () { + this.deleteModalVisible = false + + this.$store.state.api.backendInteractor.deleteEmojiFile( + { packName: this.packName, shortcode: this.shortcode } + ).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + }) + } + } +} +</script> + +<style lang="scss"> + .emoji-tab-edit-popover { + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.5em; + + .emoji { + width: 32px; + height: 32px; + } + } +</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..a747cebd 100644 --- a/src/components/settings_modal/helpers/modified_indicator.vue +++ b/src/components/settings_modal/helpers/modified_indicator.vue @@ -5,17 +5,17 @@ > <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> <div class="modified-tooltip"> - {{ $t('settings.setting_changed') }} + {{ $t(messageKey) }} </div> </template> </Popover> @@ -33,7 +33,13 @@ library.add( export default { components: { Popover }, - props: ['changed'] + props: { + changed: Boolean, + messageKey: { + type: String, + default: 'settings.setting_changed' + } + } } </script> 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..32dc6f83 --- /dev/null +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -0,0 +1,46 @@ +<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="input 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..3b3e6268 --- /dev/null +++ b/src/components/settings_modal/helpers/setting.js @@ -0,0 +1,246 @@ +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 + }, + timedApplyMode: { + type: Boolean, + default: false + } + }, + 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: + if (this.timedApplyMode) { + return (k, v) => this.$store.dispatch('setOptionTemporarily', { name: k, value: v }) + } else { + 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 && + 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 deleted file mode 100644 index 58697412..00000000 --- a/src/components/settings_modal/helpers/size_setting.js +++ /dev/null @@ -1,67 +0,0 @@ -import { get, set } from 'lodash' -import ModifiedIndicator from './modified_indicator.vue' -import Select from 'src/components/select/select.vue' - -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 { - components: { - ModifiedIndicator, - Select - }, - props: { - path: String, - disabled: Boolean, - min: Number, - units: { - type: [String], - default: () => allCssUnits - }, - expert: [Number, String] - }, - computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, - stateUnit () { - 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 - } - }, - methods: { - update (e) { - set(this.$parent, this.path, e) - }, - reset () { - set(this.$parent, this.path, this.defaultState) - }, - updateValue (e) { - set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit) - }, - updateUnit (e) { - set(this.$parent, this.path, this.stateValue + e.target.value) - } - } -} 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..7b30d1b9 --- /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="input 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/helpers/unit_setting.js b/src/components/settings_modal/helpers/unit_setting.js new file mode 100644 index 00000000..daeddd81 --- /dev/null +++ b/src/components/settings_modal/helpers/unit_setting.js @@ -0,0 +1,64 @@ +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: { + ...Setting.components, + Select + }, + props: { + ...Setting.props, + min: Number, + units: { + type: Array, + default: () => allCssUnits + }, + unitSet: { + type: String, + default: 'none' + }, + step: { + type: Number, + default: 1 + }, + resetDefault: { + type: Object, + default: null + } + }, + computed: { + ...Setting.computed, + stateUnit () { + return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : '' + }, + stateValue () { + return typeof this.state === 'string' ? this.state.replace(/[^0-9,.]+/, '') : '' + } + }, + methods: { + ...Setting.methods, + getUnitString (value) { + if (this.unitSet === 'none') return value + return this.$t(['settings', 'units', this.unitSet, value].join('.')) + }, + updateValue (e) { + this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit) + }, + updateUnit (e) { + let value = this.stateValue + const newUnit = e.target.value + if (this.resetDefault) { + const replaceValue = this.resetDefault[newUnit] + if (replaceValue != null) { + value = replaceValue + } + } + this.configSink(this.path, value + newUnit) + } + } +} diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue index 5a78f100..40ab6880 100644 --- a/src/components/settings_modal/helpers/size_setting.vue +++ b/src/components/settings_modal/helpers/unit_setting.vue @@ -1,7 +1,7 @@ <template> <span v-if="matchesExpertLevel" - class="SizeSetting" + class="UnitSetting" > <label :for="path" @@ -9,11 +9,12 @@ > <slot /> </label> + {{ ' ' }} <input :id="path" - class="number-input" + class="input number-input" type="number" - step="1" + :step="step" :disabled="disabled" :min="min || 0" :value="stateValue" @@ -23,7 +24,7 @@ :id="path" :model-value="stateUnit" :disabled="disabled" - class="css-unit-input" + class="unit-input unstyled" @change="updateUnit" > <option @@ -31,7 +32,7 @@ :key="option" :value="option" > - {{ option }} + {{ getUnitString(option) }} </option> </Select> {{ ' ' }} @@ -42,14 +43,20 @@ </span> </template> -<script src="./size_setting.js"></script> +<script src="./unit_setting.js"></script> <style lang="scss"> -.css-unit-input, -.css-unit-input select { - margin-left: 0.5em; - width: 4em; - max-width: 4em; - min-width: 4em; +.UnitSetting { + .number-input { + max-width: 6.5em; + text-align: right; + } + + .unit-input, + .unit-input select { + min-width: 4em; + width: auto; + } } + </style> |
