Merge branch 'improve_settings_reusability' into 'develop'
AdminFE functionality in PleromaFE See merge request pleroma/pleroma-fe!1800
This commit is contained in:
commit
c730c9b6d0
|
@ -0,0 +1 @@
|
||||||
|
Implemented a very basic instance administration screen
|
23
src/App.scss
23
src/App.scss
|
@ -645,6 +645,20 @@ option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cards-list {
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row dense;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
||||||
|
li {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--inputRadius);
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-block {
|
.btn-block {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -655,16 +669,19 @@ option {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
button {
|
button,
|
||||||
|
.button-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child),
|
||||||
|
&:not(:last-child) .button-default {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child),
|
||||||
|
&:not(:first-child) .button-default {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:class="{ disabled, indeterminate }"
|
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -14,6 +14,7 @@
|
||||||
<i
|
<i
|
||||||
class="checkbox-indicator"
|
class="checkbox-indicator"
|
||||||
:aria-hidden="true"
|
:aria-hidden="true"
|
||||||
|
@transitionend.capture="onTransitionEnd"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="!!$slots.default"
|
v-if="!!$slots.default"
|
||||||
|
@ -31,7 +32,24 @@ export default {
|
||||||
'indeterminate',
|
'indeterminate',
|
||||||
'disabled'
|
'disabled'
|
||||||
],
|
],
|
||||||
emits: ['update:modelValue']
|
emits: ['update:modelValue'],
|
||||||
|
data: (vm) => ({
|
||||||
|
indeterminateTransitionFix: vm.indeterminate
|
||||||
|
}),
|
||||||
|
watch: {
|
||||||
|
indeterminate (e) {
|
||||||
|
if (e) {
|
||||||
|
this.indeterminateTransitionFix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTransitionEnd (e) {
|
||||||
|
if (!this.indeterminate) {
|
||||||
|
this.indeterminateTransitionFix = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -98,6 +116,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.indeterminate-fix {
|
||||||
|
input[type="checkbox"] + .checkbox-indicator::before {
|
||||||
|
content: "–";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,10 @@ export default {
|
||||||
this.searchBarHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal () {
|
||||||
this.$store.dispatch('openSettingsModal')
|
this.$store.dispatch('openSettingsModal', 'user')
|
||||||
|
},
|
||||||
|
openAdminModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal', 'admin')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,20 +48,19 @@
|
||||||
icon="cog"
|
icon="cog"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<button
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
class="button-unstyled nav-icon"
|
||||||
class="nav-icon"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="$t('nav.administration')"
|
:title="$t('nav.administration')"
|
||||||
@click.stop
|
@click.stop="openAdminModal"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
icon="tachometer-alt"
|
icon="tachometer-alt"
|
||||||
/>
|
/>
|
||||||
</a>
|
</button>
|
||||||
<span class="spacer" />
|
<span class="spacer" />
|
||||||
<button
|
<button
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
|
|
|
@ -36,7 +36,9 @@
|
||||||
<button
|
<button
|
||||||
class="button-default btn"
|
class="button-default btn"
|
||||||
@click="addLanguage"
|
@click="addLanguage"
|
||||||
>{{ $t('settings.add_language') }}</button>
|
>
|
||||||
|
{{ $t('settings.add_language') }}
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,6 +23,11 @@ const mediaUpload = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
if (this.uploadReady) {
|
||||||
|
this.$refs.input.click()
|
||||||
|
}
|
||||||
|
},
|
||||||
uploadFile (file) {
|
uploadFile (file) {
|
||||||
const self = this
|
const self = this
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
|
@ -69,10 +74,15 @@ const mediaUpload = {
|
||||||
this.multiUpload(target.files)
|
this.multiUpload(target.files)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: {
|
||||||
'dropFiles',
|
dropFiles: Object,
|
||||||
'disabled'
|
disabled: Boolean,
|
||||||
],
|
normalButton: Boolean,
|
||||||
|
acceptTypes: {
|
||||||
|
type: String,
|
||||||
|
default: '*/*'
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
dropFiles: function (fileInfos) {
|
dropFiles: function (fileInfos) {
|
||||||
if (!this.uploading) {
|
if (!this.uploading) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<label
|
<button
|
||||||
class="media-upload"
|
class="media-upload"
|
||||||
:class="{ disabled: disabled }"
|
:class="[normalButton ? 'button-default btn' : 'button-unstyled', { disabled }]"
|
||||||
:title="$t('tool_tip.media_upload')"
|
:title="$t('tool_tip.media_upload')"
|
||||||
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
v-if="uploading"
|
v-if="uploading"
|
||||||
|
@ -15,15 +16,21 @@
|
||||||
class="new-icon"
|
class="new-icon"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
/>
|
/>
|
||||||
|
<template v-if="normalButton">
|
||||||
|
{{ ' ' }}
|
||||||
|
{{ uploading ? $t('general.loading') : $t('tool_tip.media_upload') }}
|
||||||
|
</template>
|
||||||
<input
|
<input
|
||||||
v-if="uploadReady"
|
v-if="uploadReady"
|
||||||
|
ref="input"
|
||||||
class="hidden-input-file"
|
class="hidden-input-file"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
type="file"
|
type="file"
|
||||||
multiple="true"
|
multiple="true"
|
||||||
|
:accept="acceptTypes"
|
||||||
@change="change"
|
@change="change"
|
||||||
>
|
>
|
||||||
</label>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./media_upload.js"></script>
|
<script src="./media_upload.js"></script>
|
||||||
|
@ -32,10 +39,12 @@
|
||||||
@import "../../variables";
|
@import "../../variables";
|
||||||
|
|
||||||
.media-upload {
|
.media-upload {
|
||||||
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
|
|
||||||
|
|
||||||
.hidden-input-file {
|
.hidden-input-file {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
label.media-upload {
|
||||||
|
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -163,8 +163,8 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled expand-icon"
|
class="button-unstyled expand-icon"
|
||||||
:aria-expanded="statusExpanded"
|
|
||||||
:title="$t('tool_tip.toggle_expand')"
|
:title="$t('tool_tip.toggle_expand')"
|
||||||
|
:aria-expanded="statusExpanded"
|
||||||
@click.prevent="toggleStatusExpanded"
|
@click.prevent="toggleStatusExpanded"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
|
|
@ -45,6 +45,9 @@ const Popover = {
|
||||||
// Lets hover popover stay when clicking inside of it
|
// Lets hover popover stay when clicking inside of it
|
||||||
stayOnClick: Boolean,
|
stayOnClick: Boolean,
|
||||||
|
|
||||||
|
// Use styled button (to avoid nested buttons)
|
||||||
|
normalButton: Boolean,
|
||||||
|
|
||||||
triggerAttrs: {
|
triggerAttrs: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: {}
|
default: {}
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
ref="trigger"
|
ref="trigger"
|
||||||
class="button-unstyled popover-trigger-button"
|
class="popover-trigger-button"
|
||||||
|
:class="normalButton ? 'button-default btn' : 'button-unstyled'"
|
||||||
type="button"
|
type="button"
|
||||||
v-bind="triggerAttrs"
|
v-bind="triggerAttrs"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -1,56 +1,31 @@
|
||||||
import { get, set } from 'lodash'
|
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
import Setting from './setting.js'
|
||||||
import ServerSideIndicator from './server_side_indicator.vue'
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
...Setting,
|
||||||
Checkbox,
|
props: {
|
||||||
ModifiedIndicator,
|
...Setting.props,
|
||||||
ServerSideIndicator
|
indeterminateState: [String, Object]
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
...Setting.components,
|
||||||
|
Checkbox
|
||||||
},
|
},
|
||||||
props: [
|
|
||||||
'path',
|
|
||||||
'disabled',
|
|
||||||
'expert'
|
|
||||||
],
|
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
...Setting.computed,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
isIndeterminate () {
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
return this.visibleState === this.indeterminateState
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update (e) {
|
...Setting.methods,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
getValue (e) {
|
||||||
set(this.$parent, this.path, e)
|
// Basic tri-state toggle implementation
|
||||||
// Updating nested properties does not trigger update on its parent.
|
if (!!this.indeterminateState && !e && this.visibleState === true) {
|
||||||
// probably still not as reliable, but works for depth=1 at least
|
// If we have indeterminate state, switching from true to false first goes through indeterminate
|
||||||
if (rest.length > 0) {
|
return this.indeterminateState
|
||||||
set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) })
|
|
||||||
}
|
}
|
||||||
},
|
return e
|
||||||
reset () {
|
|
||||||
set(this.$parent, this.path, this.defaultState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,37 @@
|
||||||
class="BooleanSetting"
|
class="BooleanSetting"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="state"
|
:model-value="visibleState"
|
||||||
:disabled="disabled"
|
:disabled="shouldBeDisabled"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
@update:modelValue="update"
|
@update:modelValue="update"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="!!$slots.default"
|
|
||||||
class="label"
|
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>
|
</span>
|
||||||
{{ ' ' }}
|
|
||||||
<ModifiedIndicator
|
|
||||||
:changed="isChanged"
|
|
||||||
:onclick="reset"
|
|
||||||
/>
|
|
||||||
<ServerSideIndicator :server-side="isServerSide" />
|
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,51 +1,41 @@
|
||||||
import { get, set } from 'lodash'
|
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
import Setting from './setting.js'
|
||||||
import ServerSideIndicator from './server_side_indicator.vue'
|
|
||||||
export default {
|
export default {
|
||||||
|
...Setting,
|
||||||
components: {
|
components: {
|
||||||
Select,
|
...Setting.components,
|
||||||
ModifiedIndicator,
|
Select
|
||||||
ServerSideIndicator
|
},
|
||||||
|
props: {
|
||||||
|
...Setting.props,
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
optionLabelMap: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
props: [
|
|
||||||
'path',
|
|
||||||
'disabled',
|
|
||||||
'options',
|
|
||||||
'expert'
|
|
||||||
],
|
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
...Setting.computed,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
realOptions () {
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
if (this.realSource === 'admin') {
|
||||||
},
|
return this.backendDescriptionSuggestions.map(x => ({
|
||||||
state () {
|
key: x,
|
||||||
const value = get(this.$parent, this.path)
|
value: x,
|
||||||
if (value === undefined) {
|
label: this.optionLabelMap[x] || x
|
||||||
return this.defaultState
|
}))
|
||||||
} else {
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
},
|
return this.options
|
||||||
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update (e) {
|
...Setting.methods,
|
||||||
set(this.$parent, this.path, e)
|
getValue (e) {
|
||||||
},
|
return e
|
||||||
reset () {
|
|
||||||
set(this.$parent, this.path, this.defaultState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,20 @@
|
||||||
v-if="matchesExpertLevel"
|
v-if="matchesExpertLevel"
|
||||||
class="ChoiceSetting"
|
class="ChoiceSetting"
|
||||||
>
|
>
|
||||||
<slot />
|
<template v-if="backendDescriptionLabel">
|
||||||
|
{{ backendDescriptionLabel }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<Select
|
<Select
|
||||||
:model-value="state"
|
:model-value="realDraftMode ? draft :state"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@update:modelValue="update"
|
@update:modelValue="update"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in options"
|
v-for="option in realOptions"
|
||||||
:key="option.key"
|
:key="option.key"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
|
@ -23,7 +28,14 @@
|
||||||
:changed="isChanged"
|
:changed="isChanged"
|
||||||
:onclick="reset"
|
:onclick="reset"
|
||||||
/>
|
/>
|
||||||
<ServerSideIndicator :server-side="isServerSide" />
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -1,56 +1,24 @@
|
||||||
import { get, set } from 'lodash'
|
import Setting from './setting.js'
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
...Setting,
|
||||||
ModifiedIndicator
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
path: String,
|
...Setting.props,
|
||||||
disabled: Boolean,
|
truncate: {
|
||||||
min: Number,
|
type: Number,
|
||||||
step: Number,
|
required: false,
|
||||||
truncate: Number,
|
default: 1
|
||||||
expert: [Number, String]
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
pathDefault () {
|
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
|
||||||
},
|
|
||||||
parent () {
|
|
||||||
return this.$parent.$parent
|
|
||||||
},
|
|
||||||
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: {
|
methods: {
|
||||||
truncateValue (value) {
|
...Setting.methods,
|
||||||
if (!this.truncate) {
|
getValue (e) {
|
||||||
return value
|
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)
|
||||||
return Math.trunc(value / this.truncate) * this.truncate
|
|
||||||
},
|
|
||||||
update (e) {
|
|
||||||
set(this.parent, this.path, this.truncateValue(parseFloat(e.target.value)))
|
|
||||||
},
|
|
||||||
reset () {
|
|
||||||
set(this.parent, this.path, this.defaultState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,26 @@
|
||||||
v-if="matchesExpertLevel"
|
v-if="matchesExpertLevel"
|
||||||
class="NumberSetting"
|
class="NumberSetting"
|
||||||
>
|
>
|
||||||
<label :for="path">
|
<label
|
||||||
<slot />
|
: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>
|
</label>
|
||||||
<input
|
<input
|
||||||
:id="path"
|
:id="path"
|
||||||
class="number-input"
|
class="number-input"
|
||||||
type="number"
|
type="number"
|
||||||
:step="step || 1"
|
:step="step || 1"
|
||||||
:disabled="disabled"
|
:disabled="shouldBeDisabled"
|
||||||
:min="min || 0"
|
:min="min || 0"
|
||||||
:value="state"
|
:value="realDraftMode ? draft :state"
|
||||||
@change="update"
|
@change="update"
|
||||||
>
|
>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
|
@ -21,6 +30,15 @@
|
||||||
:changed="isChanged"
|
:changed="isChanged"
|
||||||
:onclick="reset"
|
:onclick="reset"
|
||||||
/>
|
/>
|
||||||
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
v-if="serverSide"
|
v-if="isProfile"
|
||||||
class="ServerSideIndicator"
|
class="ProfileSettingIndicator"
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="serverside-tooltip">
|
<div class="profilesetting-tooltip">
|
||||||
{{ $t('settings.setting_server_side') }}
|
{{ $t('settings.setting_server_side') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,17 +33,17 @@ library.add(
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Popover },
|
components: { Popover },
|
||||||
props: ['serverSide']
|
props: ['isProfile']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.ServerSideIndicator {
|
.ProfileSettingIndicator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.serverside-tooltip {
|
.profilesetting-tooltip {
|
||||||
margin: 0.5em 1em;
|
margin: 0.5em 1em;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
text-align: center;
|
text-align: center;
|
|
@ -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!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +1,18 @@
|
||||||
import { defaultState as configDefaultState } from 'src/modules/config.js'
|
|
||||||
import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
|
|
||||||
|
|
||||||
const SharedComputedObject = () => ({
|
const SharedComputedObject = () => ({
|
||||||
user () {
|
user () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
// Getting values for default properties
|
expertLevel () {
|
||||||
...Object.keys(configDefaultState)
|
return this.$store.getters.mergedConfig.expertLevel > 0
|
||||||
.map(key => [
|
},
|
||||||
key + 'DefaultValue',
|
mergedConfig () {
|
||||||
function () {
|
return this.$store.getters.mergedConfig
|
||||||
return this.$store.getters.defaultConfig[key]
|
},
|
||||||
}
|
adminConfig () {
|
||||||
])
|
return this.$store.state.adminSettings.config
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
},
|
||||||
// Generating computed values for vuex properties
|
adminDraft () {
|
||||||
...Object.keys(configDefaultState)
|
return this.$store.state.adminSettings.draft
|
||||||
.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 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,67 +1,40 @@
|
||||||
import { get, set } from 'lodash'
|
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
|
||||||
import Select from 'src/components/select/select.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 allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
|
||||||
export const defaultHorizontalUnits = ['px', 'rem', 'vw']
|
export const defaultHorizontalUnits = ['px', 'rem', 'vw']
|
||||||
export const defaultVerticalUnits = ['px', 'rem', 'vh']
|
export const defaultVerticalUnits = ['px', 'rem', 'vh']
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
...Setting,
|
||||||
components: {
|
components: {
|
||||||
ModifiedIndicator,
|
...Setting.components,
|
||||||
Select
|
Select
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
path: String,
|
...Setting.props,
|
||||||
disabled: Boolean,
|
|
||||||
min: Number,
|
min: Number,
|
||||||
units: {
|
units: {
|
||||||
type: [String],
|
type: Array,
|
||||||
default: () => allCssUnits
|
default: () => allCssUnits
|
||||||
},
|
}
|
||||||
expert: [Number, String]
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
...Setting.computed,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
|
||||||
},
|
|
||||||
stateUnit () {
|
stateUnit () {
|
||||||
return (this.state || '').replace(/\d+/, '')
|
return this.state.replace(/\d+/, '')
|
||||||
},
|
},
|
||||||
stateValue () {
|
stateValue () {
|
||||||
return (this.state || '').replace(/\D+/, '')
|
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: {
|
methods: {
|
||||||
update (e) {
|
...Setting.methods,
|
||||||
set(this.$parent, this.path, e)
|
|
||||||
},
|
|
||||||
reset () {
|
|
||||||
set(this.$parent, this.path, this.defaultState)
|
|
||||||
},
|
|
||||||
updateValue (e) {
|
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) {
|
updateUnit (e) {
|
||||||
set(this.$parent, this.path, this.stateValue + e.target.value)
|
this.configSink(this.path, this.stateValue + e.target.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,11 +45,18 @@
|
||||||
<script src="./size_setting.js"></script>
|
<script src="./size_setting.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.css-unit-input,
|
.SizeSetting {
|
||||||
.css-unit-input select {
|
.number-input {
|
||||||
margin-left: 0.5em;
|
max-width: 6.5em;
|
||||||
width: 4em;
|
}
|
||||||
max-width: 4em;
|
|
||||||
min-width: 4em;
|
.css-unit-input,
|
||||||
|
.css-unit-input select {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
width: 4em;
|
||||||
|
max-width: 4em;
|
||||||
|
min-width: 4em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Setting from './setting.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...Setting
|
||||||
|
}
|
|
@ -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>
|
|
@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep, isEqual } from 'lodash'
|
||||||
import {
|
import {
|
||||||
newImporter,
|
newImporter,
|
||||||
newExporter
|
newExporter
|
||||||
|
@ -53,8 +53,16 @@ const SettingsModal = {
|
||||||
Modal,
|
Modal,
|
||||||
Popover,
|
Popover,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
SettingsModalContent: getResettableAsyncComponent(
|
SettingsModalUserContent: getResettableAsyncComponent(
|
||||||
() => import('./settings_modal_content.vue'),
|
() => import('./settings_modal_user_content.vue'),
|
||||||
|
{
|
||||||
|
loadingComponent: PanelLoading,
|
||||||
|
errorComponent: AsyncComponentError,
|
||||||
|
delay: 0
|
||||||
|
}
|
||||||
|
),
|
||||||
|
SettingsModalAdminContent: getResettableAsyncComponent(
|
||||||
|
() => import('./settings_modal_admin_content.vue'),
|
||||||
{
|
{
|
||||||
loadingComponent: PanelLoading,
|
loadingComponent: PanelLoading,
|
||||||
errorComponent: AsyncComponentError,
|
errorComponent: AsyncComponentError,
|
||||||
|
@ -147,6 +155,12 @@ const SettingsModal = {
|
||||||
PLEROMAFE_SETTINGS_MINOR_VERSION
|
PLEROMAFE_SETTINGS_MINOR_VERSION
|
||||||
]
|
]
|
||||||
return clone
|
return clone
|
||||||
|
},
|
||||||
|
resetAdminDraft () {
|
||||||
|
this.$store.commit('resetAdminDraft')
|
||||||
|
},
|
||||||
|
pushAdminDraft () {
|
||||||
|
this.$store.dispatch('pushAdminDraft')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -156,8 +170,14 @@ const SettingsModal = {
|
||||||
modalActivated () {
|
modalActivated () {
|
||||||
return this.$store.state.interface.settingsModalState !== 'hidden'
|
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||||
},
|
},
|
||||||
modalOpenedOnce () {
|
modalMode () {
|
||||||
return this.$store.state.interface.settingsModalLoaded
|
return this.$store.state.interface.settingsModalMode
|
||||||
|
},
|
||||||
|
modalOpenedOnceUser () {
|
||||||
|
return this.$store.state.interface.settingsModalLoadedUser
|
||||||
|
},
|
||||||
|
modalOpenedOnceAdmin () {
|
||||||
|
return this.$store.state.interface.settingsModalLoadedAdmin
|
||||||
},
|
},
|
||||||
modalPeeked () {
|
modalPeeked () {
|
||||||
return this.$store.state.interface.settingsModalState === 'minimized'
|
return this.$store.state.interface.settingsModalState === 'minimized'
|
||||||
|
@ -167,9 +187,14 @@ const SettingsModal = {
|
||||||
return this.$store.state.config.expertLevel > 0
|
return this.$store.state.config.expertLevel > 0
|
||||||
},
|
},
|
||||||
set (value) {
|
set (value) {
|
||||||
console.log(value)
|
|
||||||
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
adminDraftAny () {
|
||||||
|
return !isEqual(
|
||||||
|
this.$store.state.adminSettings.config,
|
||||||
|
this.$store.state.adminSettings.draft
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-description {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-modal-panel {
|
.settings-modal-panel {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform;
|
transition: transform;
|
||||||
|
@ -37,7 +43,9 @@
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 2em;
|
min-height: 2em;
|
||||||
min-width: 10em;
|
}
|
||||||
|
|
||||||
|
.btn:not(.dropdown-button) {
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +53,8 @@
|
||||||
|
|
||||||
.settings-footer {
|
.settings-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
line-height: 2;
|
||||||
|
|
||||||
>* {
|
>* {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="settings-modal-panel panel">
|
<div class="settings-modal-panel panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span class="title">
|
<span class="title">
|
||||||
{{ $t('settings.settings') }}
|
{{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
|
||||||
</span>
|
</span>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
|
@ -42,10 +42,12 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
<SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" />
|
||||||
|
<SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" />
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer settings-footer">
|
<div class="panel-footer settings-footer -flexible-height">
|
||||||
<Popover
|
<Popover
|
||||||
|
v-if="modalMode === 'user'"
|
||||||
class="export"
|
class="export"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
|
@ -107,10 +109,42 @@
|
||||||
>
|
>
|
||||||
{{ $t("settings.expert_mode") }}
|
{{ $t("settings.expert_mode") }}
|
||||||
</Checkbox>
|
</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
|
<span
|
||||||
id="unscrolled-content"
|
id="unscrolled-content"
|
||||||
class="extra-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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -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
|
|
@ -48,9 +48,5 @@
|
||||||
color: var(--cRed, $fallback--cRed);
|
color: var(--cRed, $fallback--cRed);
|
||||||
color: $fallback--cRed;
|
color: $fallback--cRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.number-input {
|
|
||||||
max-width: 6em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,6 @@
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</template>
|
</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>
|
|
@ -7,13 +7,11 @@
|
||||||
<BooleanSetting path="hideFilteredStatuses">
|
<BooleanSetting path="hideFilteredStatuses">
|
||||||
{{ $t('settings.hide_filtered_statuses') }}
|
{{ $t('settings.hide_filtered_statuses') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
:disabled="hideFilteredStatuses"
|
parent-path="hideFilteredStatuses"
|
||||||
|
:parent-invert="true"
|
||||||
path="hideWordFilteredPosts"
|
path="hideWordFilteredPosts"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_wordfiltered_statuses') }}
|
{{ $t('settings.hide_wordfiltered_statuses') }}
|
||||||
|
@ -22,7 +20,8 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
v-if="user"
|
v-if="user"
|
||||||
:disabled="hideFilteredStatuses"
|
parent-path="hideFilteredStatuses"
|
||||||
|
:parent-invert="true"
|
||||||
path="hideMutedThreads"
|
path="hideMutedThreads"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_muted_threads') }}
|
{{ $t('settings.hide_muted_threads') }}
|
||||||
|
@ -31,7 +30,8 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
v-if="user"
|
v-if="user"
|
||||||
:disabled="hideFilteredStatuses"
|
parent-path="hideFilteredStatuses"
|
||||||
|
:parent-invert="true"
|
||||||
path="hideMutedPosts"
|
path="hideMutedPosts"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_muted_posts') }}
|
{{ $t('settings.hide_muted_posts') }}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue
|
||||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faGlobe
|
faGlobe
|
||||||
|
@ -67,7 +67,7 @@ const GeneralTab = {
|
||||||
SizeSetting,
|
SizeSetting,
|
||||||
InterfaceLanguageSwitcher,
|
InterfaceLanguageSwitcher,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
ServerSideIndicator
|
ProfileSettingIndicator
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
horizontalUnits () {
|
horizontalUnits () {
|
||||||
|
@ -110,7 +110,7 @@ const GeneralTab = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeDefaultScope (value) {
|
changeDefaultScope (value) {
|
||||||
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
|
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,14 +29,11 @@
|
||||||
<BooleanSetting path="streaming">
|
<BooleanSetting path="streaming">
|
||||||
{{ $t('settings.streaming') }}
|
{{ $t('settings.streaming') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="pauseOnUnfocused"
|
path="pauseOnUnfocused"
|
||||||
:disabled="!streaming"
|
parent-path="streaming"
|
||||||
>
|
>
|
||||||
{{ $t('settings.pause_on_unfocused') }}
|
{{ $t('settings.pause_on_unfocused') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -213,7 +210,7 @@
|
||||||
</ChoiceSetting>
|
</ChoiceSetting>
|
||||||
</li>
|
</li>
|
||||||
<ul
|
<ul
|
||||||
v-if="conversationDisplay !== 'linear'"
|
v-if="mergedConfig.conversationDisplay !== 'linear'"
|
||||||
class="setting-list suboptions"
|
class="setting-list suboptions"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
|
@ -265,7 +262,8 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
v-if="user"
|
v-if="user"
|
||||||
path="serverSide_stripRichContent"
|
source="profile"
|
||||||
|
path="stripRichContent"
|
||||||
expert="1"
|
expert="1"
|
||||||
>
|
>
|
||||||
{{ $t('settings.no_rich_text_description') }}
|
{{ $t('settings.no_rich_text_description') }}
|
||||||
|
@ -299,7 +297,7 @@
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="preloadImage"
|
path="preloadImage"
|
||||||
expert="1"
|
expert="1"
|
||||||
:disabled="!hideNsfw"
|
parent-path="hideNsfw"
|
||||||
>
|
>
|
||||||
{{ $t('settings.preload_images') }}
|
{{ $t('settings.preload_images') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -308,7 +306,7 @@
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="useOneClickNsfw"
|
path="useOneClickNsfw"
|
||||||
expert="1"
|
expert="1"
|
||||||
:disabled="!hideNsfw"
|
parent-path="hideNsfw"
|
||||||
>
|
>
|
||||||
{{ $t('settings.use_one_click_nsfw') }}
|
{{ $t('settings.use_one_click_nsfw') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -321,15 +319,13 @@
|
||||||
>
|
>
|
||||||
{{ $t('settings.loop_video') }}
|
{{ $t('settings.loop_video') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="loopVideoSilentOnly"
|
path="loopVideoSilentOnly"
|
||||||
expert="1"
|
expert="1"
|
||||||
:disabled="!loopVideo || !loopSilentAvailable"
|
parent-path="loopVideo"
|
||||||
|
:disabled="!loopSilentAvailable"
|
||||||
>
|
>
|
||||||
{{ $t('settings.loop_video_silent_only') }}
|
{{ $t('settings.loop_video_silent_only') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -427,18 +423,18 @@
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<label for="default-vis">
|
<label for="default-vis">
|
||||||
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
|
{{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
|
||||||
<ScopeSelector
|
<ScopeSelector
|
||||||
class="scope-selector"
|
class="scope-selector"
|
||||||
:show-all="true"
|
:show-all="true"
|
||||||
:user-default="serverSide_defaultScope"
|
:user-default="$store.state.profileConfig.defaultScope"
|
||||||
:initial-scope="serverSide_defaultScope"
|
:initial-scope="$store.state.profileConfig.defaultScope"
|
||||||
:on-scope-change="changeDefaultScope"
|
:on-scope-change="changeDefaultScope"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
|
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
|
||||||
<BooleanSetting path="sensitiveByDefault">
|
<BooleanSetting path="sensitiveByDefault">
|
||||||
{{ $t('settings.sensitive_by_default') }}
|
{{ $t('settings.sensitive_by_default') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_blockNotificationsFromStrangers">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="blockNotificationsFromStrangers"
|
||||||
|
>
|
||||||
{{ $t('settings.notification_setting_block_from_strangers') }}
|
{{ $t('settings.notification_setting_block_from_strangers') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
@ -67,7 +70,8 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="serverSide_webPushHideContents"
|
source="profile"
|
||||||
|
path="webPushHideContents"
|
||||||
expert="1"
|
expert="1"
|
||||||
>
|
>
|
||||||
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
||||||
|
|
|
@ -254,37 +254,50 @@
|
||||||
<h2>{{ $t('settings.account_privacy') }}</h2>
|
<h2>{{ $t('settings.account_privacy') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_locked">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="locked"
|
||||||
|
>
|
||||||
{{ $t('settings.lock_account_description') }}
|
{{ $t('settings.lock_account_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_discoverable">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="discoverable"
|
||||||
|
>
|
||||||
{{ $t('settings.discoverable') }}
|
{{ $t('settings.discoverable') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_allowFollowingMove">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="allowFollowingMove"
|
||||||
|
>
|
||||||
{{ $t('settings.allow_following_move') }}
|
{{ $t('settings.allow_following_move') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_hideFavorites">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="hideFavorites"
|
||||||
|
>
|
||||||
{{ $t('settings.hide_favorites_description') }}
|
{{ $t('settings.hide_favorites_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_hideFollowers">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="hideFollowers"
|
||||||
|
>
|
||||||
{{ $t('settings.hide_followers_description') }}
|
{{ $t('settings.hide_followers_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !serverSide_hideFollowers}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="serverSide_hideFollowersCount"
|
source="profile"
|
||||||
:disabled="!serverSide_hideFollowers"
|
path="hideFollowersCount"
|
||||||
|
parent-path="hideFollowers"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_followers_count_description') }}
|
{{ $t('settings.hide_followers_count_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -292,17 +305,18 @@
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_hideFollows">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="hideFollows"
|
||||||
|
>
|
||||||
{{ $t('settings.hide_follows_description') }}
|
{{ $t('settings.hide_follows_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !serverSide_hideFollows}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="serverSide_hideFollowsCount"
|
source="profile"
|
||||||
:disabled="!serverSide_hideFollows"
|
path="hideFollowsCount"
|
||||||
|
parent-path="hideFollows"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_follows_count_description') }}
|
{{ $t('settings.hide_follows_count_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
|
|
@ -143,8 +143,8 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<i18n
|
<i18n-t
|
||||||
path="settings.new_alias_target"
|
keypath="settings.new_alias_target"
|
||||||
tag="p"
|
tag="p"
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
|
@ -152,7 +152,7 @@
|
||||||
>
|
>
|
||||||
foo@example.org
|
foo@example.org
|
||||||
</code>
|
</code>
|
||||||
</i18n>
|
</i18n-t>
|
||||||
<input
|
<input
|
||||||
v-model="addAliasTarget"
|
v-model="addAliasTarget"
|
||||||
>
|
>
|
||||||
|
@ -175,16 +175,16 @@
|
||||||
<h2>{{ $t('settings.move_account') }}</h2>
|
<h2>{{ $t('settings.move_account') }}</h2>
|
||||||
<p>{{ $t('settings.move_account_notes') }}</p>
|
<p>{{ $t('settings.move_account_notes') }}</p>
|
||||||
<div>
|
<div>
|
||||||
<i18n
|
<i18n-t
|
||||||
path="settings.move_account_target"
|
keypath="settings.move_account_target"
|
||||||
tag="p"
|
tag="p"
|
||||||
>
|
>
|
||||||
<code
|
<template #example>
|
||||||
place="example"
|
<code>
|
||||||
>
|
foo@example.org
|
||||||
foo@example.org
|
</code>
|
||||||
</code>
|
</template>
|
||||||
</i18n>
|
</i18n-t>
|
||||||
<input
|
<input
|
||||||
v-model="moveAccountTarget"
|
v-model="moveAccountTarget"
|
||||||
>
|
>
|
||||||
|
|
|
@ -115,7 +115,10 @@ const SideDrawer = {
|
||||||
GestureService.updateSwipe(e, this.closeGesture)
|
GestureService.updateSwipe(e, this.closeGesture)
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal () {
|
||||||
this.$store.dispatch('openSettingsModal')
|
this.$store.dispatch('openSettingsModal', 'user')
|
||||||
|
},
|
||||||
|
openAdminModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal', 'admin')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,16 +180,16 @@
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
class="button-unstyled -link -fullwidth"
|
||||||
target="_blank"
|
@click.stop="openAdminModal"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
icon="tachometer-alt"
|
icon="tachometer-alt"
|
||||||
/> {{ $t("nav.administration") }}
|
/> {{ $t("nav.administration") }}
|
||||||
</a>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser && supportsAnnouncements"
|
v-if="currentUser && supportsAnnouncements"
|
||||||
|
|
|
@ -60,13 +60,7 @@ export default {
|
||||||
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
|
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
|
||||||
return this.$slots.default().findIndex(isWanted) === this.activeIndex
|
return this.$slots.default().findIndex(isWanted) === this.activeIndex
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
settingsModalVisible () {
|
|
||||||
return this.settingsModalState === 'visible'
|
|
||||||
},
|
|
||||||
...mapState({
|
|
||||||
settingsModalState: state => state.interface.settingsModalState
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
const currentSlot = this.slots()[this.active]
|
const currentSlot = this.slots()[this.active]
|
||||||
|
|
|
@ -519,6 +519,8 @@
|
||||||
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||||
"mutes_tab": "Mutes",
|
"mutes_tab": "Mutes",
|
||||||
"play_videos_in_modal": "Play videos in a popup frame",
|
"play_videos_in_modal": "Play videos in a popup frame",
|
||||||
|
"url": "URL",
|
||||||
|
"preview": "Preview",
|
||||||
"file_export_import": {
|
"file_export_import": {
|
||||||
"backup_restore": "Settings backup",
|
"backup_restore": "Settings backup",
|
||||||
"backup_settings": "Backup settings to file",
|
"backup_settings": "Backup settings to file",
|
||||||
|
@ -830,6 +832,98 @@
|
||||||
"title": "Version",
|
"title": "Version",
|
||||||
"backend_version": "Backend version",
|
"backend_version": "Backend version",
|
||||||
"frontend_version": "Frontend version"
|
"frontend_version": "Frontend version"
|
||||||
|
},
|
||||||
|
"commit_value": "Save",
|
||||||
|
"commit_value_tooltip": "Value is not saved, press this button to commit your changes",
|
||||||
|
"reset_value": "Reset",
|
||||||
|
"reset_value_tooltip": "Reset draft",
|
||||||
|
"hard_reset_value": "Hard reset",
|
||||||
|
"hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value"
|
||||||
|
},
|
||||||
|
"admin_dash": {
|
||||||
|
"window_title": "Administration",
|
||||||
|
"wip_notice": "This admin dashboard is experimental and WIP, {adminFeLink}.",
|
||||||
|
"old_ui_link": "old admin UI available here",
|
||||||
|
"reset_all": "Reset all",
|
||||||
|
"commit_all": "Save all",
|
||||||
|
"tabs": {
|
||||||
|
"nodb": "No DB Config",
|
||||||
|
"instance": "Instance",
|
||||||
|
"limits": "Limits",
|
||||||
|
"frontends": "Front-ends"
|
||||||
|
},
|
||||||
|
"nodb": {
|
||||||
|
"heading": "Database config is disabled",
|
||||||
|
"text": "You need to change backend config files so that {property} is set to {value}, see more in {documentation}.",
|
||||||
|
"documentation": "documentation",
|
||||||
|
"text2": "Most configuration options will be unavailable."
|
||||||
|
},
|
||||||
|
"captcha": {
|
||||||
|
"native": "Native",
|
||||||
|
"kocaptcha": "KoCaptcha"
|
||||||
|
},
|
||||||
|
"instance": {
|
||||||
|
"instance": "Instance information",
|
||||||
|
"registrations": "User sign-ups",
|
||||||
|
"captcha_header": "CAPTCHA",
|
||||||
|
"kocaptcha": "KoCaptcha settings",
|
||||||
|
"access": "Instance access",
|
||||||
|
"restrict": {
|
||||||
|
"header": "Restrict access for anonymous visitors",
|
||||||
|
"description": "Detailed setting for allowing/disallowing access to certain aspects of API. By default (indeterminate state) it will disallow if instance is not public, ticked checkbox means disallow access even if instance is public, unticked means allow access even if instance is private. Please note that unexpected behavior might happen if some settings are set, i.e. if profile access is disabled posts will show without profile information.",
|
||||||
|
"timelines": "Timelines access",
|
||||||
|
"profiles": "User profiles access",
|
||||||
|
"activities": "Statues/activities access"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"arbitrary_limits": "Arbitrary limits",
|
||||||
|
"posts": "Post limits",
|
||||||
|
"uploads": "Attachments limits",
|
||||||
|
"users": "User profile limits",
|
||||||
|
"profile_fields": "Profile fields limits",
|
||||||
|
"user_uploads": "Profile media limits"
|
||||||
|
},
|
||||||
|
"frontend": {
|
||||||
|
"repository": "Repository link",
|
||||||
|
"versions": "Available versions",
|
||||||
|
"build_url": "Build URL",
|
||||||
|
"reinstall": "Reinstall",
|
||||||
|
"is_default": "(Default)",
|
||||||
|
"is_default_custom": "(Default, version: {version})",
|
||||||
|
"install": "Install",
|
||||||
|
"install_version": "Install version {version}",
|
||||||
|
"more_install_options": "More install options",
|
||||||
|
"more_default_options": "More default setting options",
|
||||||
|
"set_default": "Set default",
|
||||||
|
"set_default_version": "Set version {version} as default",
|
||||||
|
"wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.",
|
||||||
|
"default_frontend": "Default front-end",
|
||||||
|
"default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.",
|
||||||
|
"default_frontend_tip2": "WIP: Since Pleroma backend doesn't properly list all installed frontends you'll have to enter name and reference manually. List below provides shortcuts to fill the values.",
|
||||||
|
"available_frontends": "Available for install"
|
||||||
|
},
|
||||||
|
"temp_overrides": {
|
||||||
|
":pleroma": {
|
||||||
|
":instance": {
|
||||||
|
":public": {
|
||||||
|
"label": "Instance is public",
|
||||||
|
"description": "Disabling this will make all API accessible only for logged-in users, this will make Public and Federated timelines inaccessible to anonymous visitors."
|
||||||
|
},
|
||||||
|
":limit_to_local_content": {
|
||||||
|
"label": "Limit search to local content",
|
||||||
|
"description": "Disables global network search for unauthenticated (default), all users or none"
|
||||||
|
},
|
||||||
|
":description_limit": {
|
||||||
|
"label": "Limit",
|
||||||
|
"description": "Character limit for attachment descriptions"
|
||||||
|
},
|
||||||
|
":background_image": {
|
||||||
|
"label": "Background image",
|
||||||
|
"description": "Background image (primarily used by PleromaFE)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
|
|
|
@ -10,8 +10,9 @@ import listsModule from './modules/lists.js'
|
||||||
import usersModule from './modules/users.js'
|
import usersModule from './modules/users.js'
|
||||||
import apiModule from './modules/api.js'
|
import apiModule from './modules/api.js'
|
||||||
import configModule from './modules/config.js'
|
import configModule from './modules/config.js'
|
||||||
import serverSideConfigModule from './modules/serverSideConfig.js'
|
import profileConfigModule from './modules/profileConfig.js'
|
||||||
import serverSideStorageModule from './modules/serverSideStorage.js'
|
import serverSideStorageModule from './modules/serverSideStorage.js'
|
||||||
|
import adminSettingsModule from './modules/adminSettings.js'
|
||||||
import shoutModule from './modules/shout.js'
|
import shoutModule from './modules/shout.js'
|
||||||
import oauthModule from './modules/oauth.js'
|
import oauthModule from './modules/oauth.js'
|
||||||
import authFlowModule from './modules/auth_flow.js'
|
import authFlowModule from './modules/auth_flow.js'
|
||||||
|
@ -80,8 +81,9 @@ const persistedStateOptions = {
|
||||||
lists: listsModule,
|
lists: listsModule,
|
||||||
api: apiModule,
|
api: apiModule,
|
||||||
config: configModule,
|
config: configModule,
|
||||||
serverSideConfig: serverSideConfigModule,
|
profileConfig: profileConfigModule,
|
||||||
serverSideStorage: serverSideStorageModule,
|
serverSideStorage: serverSideStorageModule,
|
||||||
|
adminSettings: adminSettingsModule,
|
||||||
shout: shoutModule,
|
shout: shoutModule,
|
||||||
oauth: oauthModule,
|
oauth: oauthModule,
|
||||||
authFlow: authFlowModule,
|
authFlow: authFlowModule,
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
import { set, get, cloneDeep, differenceWith, isEqual, flatten } from 'lodash'
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
frontends: [],
|
||||||
|
loaded: false,
|
||||||
|
needsReboot: null,
|
||||||
|
config: null,
|
||||||
|
modifiedPaths: null,
|
||||||
|
descriptions: null,
|
||||||
|
draft: null,
|
||||||
|
dbConfigEnabled: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newUserFlags = {
|
||||||
|
...defaultState.flagStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminSettingsStorage = {
|
||||||
|
state: {
|
||||||
|
...cloneDeep(defaultState)
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setInstanceAdminNoDbConfig (state) {
|
||||||
|
state.loaded = false
|
||||||
|
state.dbConfigEnabled = false
|
||||||
|
},
|
||||||
|
setAvailableFrontends (state, { frontends }) {
|
||||||
|
state.frontends = frontends.map(f => {
|
||||||
|
if (f.name === 'pleroma-fe') {
|
||||||
|
f.refs = ['master', 'develop']
|
||||||
|
} else {
|
||||||
|
f.refs = [f.ref]
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateAdminSettings (state, { config, modifiedPaths }) {
|
||||||
|
state.loaded = true
|
||||||
|
state.dbConfigEnabled = true
|
||||||
|
state.config = config
|
||||||
|
state.modifiedPaths = modifiedPaths
|
||||||
|
},
|
||||||
|
updateAdminDescriptions (state, { descriptions }) {
|
||||||
|
state.descriptions = descriptions
|
||||||
|
},
|
||||||
|
updateAdminDraft (state, { path, value }) {
|
||||||
|
const [group, key, subkey] = path
|
||||||
|
const parent = [group, key, subkey]
|
||||||
|
|
||||||
|
set(state.draft, path, value)
|
||||||
|
|
||||||
|
// force-updating grouped draft to trigger refresh of group settings
|
||||||
|
if (path.length > parent.length) {
|
||||||
|
set(state.draft, parent, cloneDeep(get(state.draft, parent)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetAdminDraft (state) {
|
||||||
|
state.draft = cloneDeep(state.config)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
loadFrontendsStuff ({ state, rootState, dispatch, commit }) {
|
||||||
|
rootState.api.backendInteractor.fetchAvailableFrontends()
|
||||||
|
.then(frontends => commit('setAvailableFrontends', { frontends }))
|
||||||
|
},
|
||||||
|
loadAdminStuff ({ state, rootState, dispatch, commit }) {
|
||||||
|
rootState.api.backendInteractor.fetchInstanceDBConfig()
|
||||||
|
.then(backendDbConfig => {
|
||||||
|
if (backendDbConfig.error) {
|
||||||
|
if (backendDbConfig.error.status === 400) {
|
||||||
|
backendDbConfig.error.json().then(errorJson => {
|
||||||
|
if (/configurable_from_database/.test(errorJson.error)) {
|
||||||
|
commit('setInstanceAdminNoDbConfig')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch('setInstanceAdminSettings', { backendDbConfig })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (state.descriptions === null) {
|
||||||
|
rootState.api.backendInteractor.fetchInstanceConfigDescriptions()
|
||||||
|
.then(backendDescriptions => dispatch('setInstanceAdminDescriptions', { backendDescriptions }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setInstanceAdminSettings ({ state, commit, dispatch }, { backendDbConfig }) {
|
||||||
|
const config = state.config || {}
|
||||||
|
const modifiedPaths = new Set()
|
||||||
|
backendDbConfig.configs.forEach(c => {
|
||||||
|
const path = [c.group, c.key]
|
||||||
|
if (c.db) {
|
||||||
|
// Path elements can contain dot, therefore we use ' -> ' as a separator instead
|
||||||
|
// Using strings for modified paths for easier searching
|
||||||
|
c.db.forEach(x => modifiedPaths.add([...path, x].join(' -> ')))
|
||||||
|
}
|
||||||
|
const convert = (value) => {
|
||||||
|
if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
|
||||||
|
return value.reduce((acc, c) => {
|
||||||
|
return { ...acc, [c.tuple[0]]: convert(c.tuple[1]) }
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set(config, path, convert(c.value))
|
||||||
|
})
|
||||||
|
console.log(config[':pleroma'])
|
||||||
|
commit('updateAdminSettings', { config, modifiedPaths })
|
||||||
|
commit('resetAdminDraft')
|
||||||
|
},
|
||||||
|
setInstanceAdminDescriptions ({ state, commit, dispatch }, { backendDescriptions }) {
|
||||||
|
const convert = ({ children, description, label, key = '<ROOT>', group, suggestions }, path, acc) => {
|
||||||
|
const newPath = group ? [group, key] : [key]
|
||||||
|
const obj = { description, label, suggestions }
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
children.forEach(c => {
|
||||||
|
convert(c, newPath, obj)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
set(acc, newPath, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptions = {}
|
||||||
|
backendDescriptions.forEach(d => convert(d, '', descriptions))
|
||||||
|
console.log(descriptions[':pleroma']['Pleroma.Captcha'])
|
||||||
|
commit('updateAdminDescriptions', { descriptions })
|
||||||
|
},
|
||||||
|
|
||||||
|
// This action takes draft state, diffs it with live config state and then pushes
|
||||||
|
// only differences between the two. Difference detection only work up to subkey (third) level.
|
||||||
|
pushAdminDraft ({ rootState, state, commit, dispatch }) {
|
||||||
|
// TODO cleanup paths in modifiedPaths
|
||||||
|
const convert = (value) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return value
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return value.map(convert)
|
||||||
|
} else {
|
||||||
|
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting all group-keys used in config
|
||||||
|
const allGroupKeys = flatten(
|
||||||
|
Object
|
||||||
|
.entries(state.config)
|
||||||
|
.map(
|
||||||
|
([group, lv1data]) => Object
|
||||||
|
.keys(lv1data)
|
||||||
|
.map((key) => ({ group, key }))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Only using group-keys where there are changes detected
|
||||||
|
const changedGroupKeys = allGroupKeys.filter(({ group, key }) => {
|
||||||
|
return !isEqual(state.config[group][key], state.draft[group][key])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Here we take all changed group-keys and get all changed subkeys
|
||||||
|
const changed = changedGroupKeys.map(({ group, key }) => {
|
||||||
|
const config = state.config[group][key]
|
||||||
|
const draft = state.draft[group][key]
|
||||||
|
|
||||||
|
// We convert group-key value into entries arrays
|
||||||
|
const eConfig = Object.entries(config)
|
||||||
|
const eDraft = Object.entries(draft)
|
||||||
|
|
||||||
|
// Then those entries array we diff so only changed subkey entries remain
|
||||||
|
// We use the diffed array to reconstruct the object and then shove it into convert()
|
||||||
|
return ({ group, key, value: convert(Object.fromEntries(differenceWith(eDraft, eConfig, isEqual))) })
|
||||||
|
})
|
||||||
|
|
||||||
|
rootState.api.backendInteractor.pushInstanceDBConfig({
|
||||||
|
payload: {
|
||||||
|
configs: changed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
|
||||||
|
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
|
||||||
|
},
|
||||||
|
pushAdminSetting ({ rootState, state, commit, dispatch }, { path, value }) {
|
||||||
|
const [group, key, ...rest] = Array.isArray(path) ? path : path.split(/\./g)
|
||||||
|
const clone = {} // not actually cloning the entire thing to avoid excessive writes
|
||||||
|
set(clone, rest, value)
|
||||||
|
|
||||||
|
// TODO cleanup paths in modifiedPaths
|
||||||
|
const convert = (value) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return value
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return value.map(convert)
|
||||||
|
} else {
|
||||||
|
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootState.api.backendInteractor.pushInstanceDBConfig({
|
||||||
|
payload: {
|
||||||
|
configs: [{
|
||||||
|
group,
|
||||||
|
key,
|
||||||
|
value: convert(clone)
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
|
||||||
|
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
|
||||||
|
},
|
||||||
|
resetAdminSetting ({ rootState, state, commit, dispatch }, { path }) {
|
||||||
|
const [group, key, subkey] = path.split(/\./g)
|
||||||
|
|
||||||
|
state.modifiedPaths.delete(path)
|
||||||
|
|
||||||
|
return rootState.api.backendInteractor.pushInstanceDBConfig({
|
||||||
|
payload: {
|
||||||
|
configs: [{
|
||||||
|
group,
|
||||||
|
key,
|
||||||
|
delete: true,
|
||||||
|
subkeys: [subkey]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
|
||||||
|
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default adminSettingsStorage
|
|
@ -1,6 +1,7 @@
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
|
import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
|
||||||
import messages from '../i18n/messages'
|
import messages from '../i18n/messages'
|
||||||
|
import { set } from 'lodash'
|
||||||
import localeService from '../services/locale/locale.service.js'
|
import localeService from '../services/locale/locale.service.js'
|
||||||
|
|
||||||
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
|
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
|
||||||
|
@ -148,7 +149,7 @@ const config = {
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setOption (state, { name, value }) {
|
setOption (state, { name, value }) {
|
||||||
state[name] = value
|
set(state, name, value)
|
||||||
},
|
},
|
||||||
setHighlight (state, { user, color, type }) {
|
setHighlight (state, { user, color, type }) {
|
||||||
const data = this.state.config.highlight[user]
|
const data = this.state.config.highlight[user]
|
||||||
|
@ -178,32 +179,52 @@ const config = {
|
||||||
commit('setHighlight', { user, color, type })
|
commit('setHighlight', { user, color, type })
|
||||||
},
|
},
|
||||||
setOption ({ commit, dispatch, state }, { name, value }) {
|
setOption ({ commit, dispatch, state }, { name, value }) {
|
||||||
commit('setOption', { name, value })
|
const exceptions = new Set([
|
||||||
switch (name) {
|
'useStreamingApi'
|
||||||
case 'theme':
|
])
|
||||||
setPreset(value)
|
|
||||||
break
|
if (exceptions.has(name)) {
|
||||||
case 'sidebarColumnWidth':
|
switch (name) {
|
||||||
case 'contentColumnWidth':
|
case 'useStreamingApi': {
|
||||||
case 'notifsColumnWidth':
|
const action = value ? 'enableMastoSockets' : 'disableMastoSockets'
|
||||||
case 'emojiReactionsScale':
|
|
||||||
applyConfig(state)
|
dispatch(action).then(() => {
|
||||||
break
|
commit('setOption', { name: 'useStreamingApi', value })
|
||||||
case 'customTheme':
|
}).catch((e) => {
|
||||||
case 'customThemeSource':
|
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||||
applyTheme(value)
|
dispatch('disableMastoSockets')
|
||||||
break
|
dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||||
case 'interfaceLanguage':
|
})
|
||||||
messages.setLanguage(this.getters.i18n, value)
|
}
|
||||||
dispatch('loadUnicodeEmojiData', value)
|
}
|
||||||
Cookies.set(
|
} else {
|
||||||
BACKEND_LANGUAGE_COOKIE_NAME,
|
commit('setOption', { name, value })
|
||||||
localeService.internalToBackendLocaleMulti(value)
|
switch (name) {
|
||||||
)
|
case 'theme':
|
||||||
break
|
setPreset(value)
|
||||||
case 'thirdColumnMode':
|
break
|
||||||
dispatch('setLayoutWidth', undefined)
|
case 'sidebarColumnWidth':
|
||||||
break
|
case 'contentColumnWidth':
|
||||||
|
case 'notifsColumnWidth':
|
||||||
|
case 'emojiReactionsScale':
|
||||||
|
applyConfig(state)
|
||||||
|
break
|
||||||
|
case 'customTheme':
|
||||||
|
case 'customThemeSource':
|
||||||
|
applyTheme(value)
|
||||||
|
break
|
||||||
|
case 'interfaceLanguage':
|
||||||
|
messages.setLanguage(this.getters.i18n, value)
|
||||||
|
dispatch('loadUnicodeEmojiData', value)
|
||||||
|
Cookies.set(
|
||||||
|
BACKEND_LANGUAGE_COOKIE_NAME,
|
||||||
|
localeService.internalToBackendLocaleMulti(value)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'thirdColumnMode':
|
||||||
|
dispatch('setLayoutWidth', undefined)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
settingsModalState: 'hidden',
|
settingsModalState: 'hidden',
|
||||||
settingsModalLoaded: false,
|
settingsModalLoadedUser: false,
|
||||||
|
settingsModalLoadedAdmin: false,
|
||||||
settingsModalTargetTab: null,
|
settingsModalTargetTab: null,
|
||||||
|
settingsModalMode: 'user',
|
||||||
settings: {
|
settings: {
|
||||||
currentSaveStateNotice: null,
|
currentSaveStateNotice: null,
|
||||||
noticeClearTimeout: null,
|
noticeClearTimeout: null,
|
||||||
|
@ -54,10 +56,17 @@ const interfaceMod = {
|
||||||
throw new Error('Illegal minimization state of settings modal')
|
throw new Error('Illegal minimization state of settings modal')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openSettingsModal (state) {
|
openSettingsModal (state, value) {
|
||||||
|
state.settingsModalMode = value
|
||||||
state.settingsModalState = 'visible'
|
state.settingsModalState = 'visible'
|
||||||
if (!state.settingsModalLoaded) {
|
if (value === 'user') {
|
||||||
state.settingsModalLoaded = true
|
if (!state.settingsModalLoadedUser) {
|
||||||
|
state.settingsModalLoadedUser = true
|
||||||
|
}
|
||||||
|
} else if (value === 'admin') {
|
||||||
|
if (!state.settingsModalLoadedAdmin) {
|
||||||
|
state.settingsModalLoadedAdmin = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setSettingsModalTargetTab (state, value) {
|
setSettingsModalTargetTab (state, value) {
|
||||||
|
@ -92,8 +101,8 @@ const interfaceMod = {
|
||||||
closeSettingsModal ({ commit }) {
|
closeSettingsModal ({ commit }) {
|
||||||
commit('closeSettingsModal')
|
commit('closeSettingsModal')
|
||||||
},
|
},
|
||||||
openSettingsModal ({ commit }) {
|
openSettingsModal ({ commit }, value = 'user') {
|
||||||
commit('openSettingsModal')
|
commit('openSettingsModal', value)
|
||||||
},
|
},
|
||||||
togglePeekSettingsModal ({ commit }) {
|
togglePeekSettingsModal ({ commit }) {
|
||||||
commit('togglePeekSettingsModal')
|
commit('togglePeekSettingsModal')
|
||||||
|
|
|
@ -22,9 +22,9 @@ const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
|
||||||
.updateNotificationSettings({ settings })
|
.updateNotificationSettings({ settings })
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
commit('confirmServerSideOption', { name, value })
|
commit('confirmProfileOption', { name, value })
|
||||||
} else {
|
} else {
|
||||||
commit('confirmServerSideOption', { name, value: oldValue })
|
commit('confirmProfileOption', { name, value: oldValue })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -94,16 +94,16 @@ export const settingsMap = {
|
||||||
|
|
||||||
export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
|
export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
|
||||||
|
|
||||||
const serverSideConfig = {
|
const profileConfig = {
|
||||||
state: { ...defaultState },
|
state: { ...defaultState },
|
||||||
mutations: {
|
mutations: {
|
||||||
confirmServerSideOption (state, { name, value }) {
|
confirmProfileOption (state, { name, value }) {
|
||||||
set(state, name, value)
|
set(state, name, value)
|
||||||
},
|
},
|
||||||
wipeServerSideOption (state, { name }) {
|
wipeProfileOption (state, { name }) {
|
||||||
set(state, name, null)
|
set(state, name, null)
|
||||||
},
|
},
|
||||||
wipeAllServerSideOptions (state) {
|
wipeAllProfileOptions (state) {
|
||||||
Object.keys(settingsMap).forEach(key => {
|
Object.keys(settingsMap).forEach(key => {
|
||||||
set(state, key, null)
|
set(state, key, null)
|
||||||
})
|
})
|
||||||
|
@ -118,23 +118,23 @@ const serverSideConfig = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) {
|
setProfileOption ({ rootState, state, commit, dispatch }, { name, value }) {
|
||||||
const oldValue = get(state, name)
|
const oldValue = get(state, name)
|
||||||
const map = settingsMap[name]
|
const map = settingsMap[name]
|
||||||
if (!map) throw new Error('Invalid server-side setting')
|
if (!map) throw new Error('Invalid server-side setting')
|
||||||
const { set: path = map, api = defaultApi } = map
|
const { set: path = map, api = defaultApi } = map
|
||||||
commit('wipeServerSideOption', { name })
|
commit('wipeProfileOption', { name })
|
||||||
|
|
||||||
api({ rootState, commit }, { path, value, oldValue })
|
api({ rootState, commit }, { path, value, oldValue })
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.warn('Error setting server-side option:', e)
|
console.warn('Error setting server-side option:', e)
|
||||||
commit('confirmServerSideOption', { name, value: oldValue })
|
commit('confirmProfileOption', { name, value: oldValue })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
logout ({ commit }) {
|
logout ({ commit }) {
|
||||||
commit('wipeAllServerSideOptions')
|
commit('wipeAllProfileOptions')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default serverSideConfig
|
export default profileConfig
|
|
@ -577,6 +577,7 @@ const users = {
|
||||||
loginUser (store, accessToken) {
|
loginUser (store, accessToken) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const commit = store.commit
|
const commit = store.commit
|
||||||
|
const dispatch = store.dispatch
|
||||||
commit('beginLogin')
|
commit('beginLogin')
|
||||||
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
|
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -591,57 +592,57 @@ const users = {
|
||||||
commit('setServerSideStorage', user)
|
commit('setServerSideStorage', user)
|
||||||
commit('addNewUsers', [user])
|
commit('addNewUsers', [user])
|
||||||
|
|
||||||
store.dispatch('fetchEmoji')
|
dispatch('fetchEmoji')
|
||||||
|
|
||||||
getNotificationPermission()
|
getNotificationPermission()
|
||||||
.then(permission => commit('setNotificationPermission', permission))
|
.then(permission => commit('setNotificationPermission', permission))
|
||||||
|
|
||||||
// Set our new backend interactor
|
// Set our new backend interactor
|
||||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||||
store.dispatch('pushServerSideStorage')
|
dispatch('pushServerSideStorage')
|
||||||
|
|
||||||
if (user.token) {
|
if (user.token) {
|
||||||
store.dispatch('setWsToken', user.token)
|
dispatch('setWsToken', user.token)
|
||||||
|
|
||||||
// Initialize the shout socket.
|
// Initialize the shout socket.
|
||||||
store.dispatch('initializeSocket')
|
dispatch('initializeSocket')
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
// Start getting fresh posts.
|
// Start getting fresh posts.
|
||||||
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||||
|
|
||||||
// Start fetching notifications
|
// Start fetching notifications
|
||||||
store.dispatch('startFetchingNotifications')
|
dispatch('startFetchingNotifications')
|
||||||
|
|
||||||
// Start fetching chats
|
// Start fetching chats
|
||||||
store.dispatch('startFetchingChats')
|
dispatch('startFetchingChats')
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch('startFetchingLists')
|
dispatch('startFetchingLists')
|
||||||
|
|
||||||
if (user.locked) {
|
if (user.locked) {
|
||||||
store.dispatch('startFetchingFollowRequests')
|
dispatch('startFetchingFollowRequests')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.getters.mergedConfig.useStreamingApi) {
|
if (store.getters.mergedConfig.useStreamingApi) {
|
||||||
store.dispatch('fetchTimeline', { timeline: 'friends', since: null })
|
dispatch('fetchTimeline', { timeline: 'friends', since: null })
|
||||||
store.dispatch('fetchNotifications', { since: null })
|
dispatch('fetchNotifications', { since: null })
|
||||||
store.dispatch('enableMastoSockets', true).catch((error) => {
|
dispatch('enableMastoSockets', true).catch((error) => {
|
||||||
console.error('Failed initializing MastoAPI Streaming socket', error)
|
console.error('Failed initializing MastoAPI Streaming socket', error)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
store.dispatch('fetchChats', { latest: true })
|
dispatch('fetchChats', { latest: true })
|
||||||
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
|
setTimeout(() => dispatch('setNotificationsSilence', false), 10000)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
startPolling()
|
startPolling()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user mutes
|
// Get user mutes
|
||||||
store.dispatch('fetchMutes')
|
dispatch('fetchMutes')
|
||||||
|
|
||||||
store.dispatch('setLayoutWidth', windowWidth())
|
dispatch('setLayoutWidth', windowWidth())
|
||||||
store.dispatch('setLayoutHeight', windowHeight())
|
dispatch('setLayoutHeight', windowHeight())
|
||||||
|
|
||||||
// Fetch our friends
|
// Fetch our friends
|
||||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||||
|
|
|
@ -108,6 +108,11 @@ const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
||||||
const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
||||||
|
|
||||||
|
const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
|
||||||
|
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
|
||||||
|
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
|
||||||
|
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
|
||||||
|
|
||||||
const oldfetch = window.fetch
|
const oldfetch = window.fetch
|
||||||
|
|
||||||
const fetch = (url, options) => {
|
const fetch = (url, options) => {
|
||||||
|
@ -1668,6 +1673,94 @@ const setReportState = ({ id, state, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADMIN STUFF // EXPERIMENTAL
|
||||||
|
const fetchInstanceDBConfig = ({ credentials }) => {
|
||||||
|
return fetch(PLEROMA_ADMIN_CONFIG_URL, {
|
||||||
|
headers: authHeaders(credentials)
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchInstanceConfigDescriptions = ({ credentials }) => {
|
||||||
|
return fetch(PLEROMA_ADMIN_DESCRIPTIONS_URL, {
|
||||||
|
headers: authHeaders(credentials)
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAvailableFrontends = ({ credentials }) => {
|
||||||
|
return fetch(PLEROMA_ADMIN_FRONTENDS_URL, {
|
||||||
|
headers: authHeaders(credentials)
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushInstanceDBConfig = ({ credentials, payload }) => {
|
||||||
|
return fetch(PLEROMA_ADMIN_CONFIG_URL, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authHeaders(credentials)
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const installFrontend = ({ credentials, payload }) => {
|
||||||
|
return fetch(PLEROMA_ADMIN_FRONTENDS_INSTALL_URL, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authHeaders(credentials)
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const apiService = {
|
const apiService = {
|
||||||
verifyCredentials,
|
verifyCredentials,
|
||||||
fetchTimeline,
|
fetchTimeline,
|
||||||
|
@ -1781,7 +1874,12 @@ const apiService = {
|
||||||
postAnnouncement,
|
postAnnouncement,
|
||||||
editAnnouncement,
|
editAnnouncement,
|
||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
adminFetchAnnouncements
|
adminFetchAnnouncements,
|
||||||
|
fetchInstanceDBConfig,
|
||||||
|
fetchInstanceConfigDescriptions,
|
||||||
|
fetchAvailableFrontends,
|
||||||
|
pushInstanceDBConfig,
|
||||||
|
installFrontend
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// TODO this func might as well take the entire file and use its mimetype
|
// TODO this func might as well take the entire file and use its mimetype
|
||||||
// or the entire service could be just mimetype service that only operates
|
// or the entire service could be just mimetype service that only operates
|
||||||
// on mimetypes and not files. Currently the naming is confusing.
|
// on mimetypes and not files. Currently the naming is confusing.
|
||||||
const fileType = mimetype => {
|
export const fileType = mimetype => {
|
||||||
if (mimetype.match(/flash/)) {
|
if (mimetype.match(/flash/)) {
|
||||||
return 'flash'
|
return 'flash'
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,25 @@ const fileType = mimetype => {
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileMatchesSomeType = (types, file) =>
|
export const fileTypeExt = url => {
|
||||||
|
if (url.match(/\.(png|jpe?g|gif|webp|avif)$/)) {
|
||||||
|
return 'image'
|
||||||
|
}
|
||||||
|
if (url.match(/\.(ogv|mp4|webm|mov)$/)) {
|
||||||
|
return 'video'
|
||||||
|
}
|
||||||
|
if (url.match(/\.(it|s3m|mod|umx|mp3|aac|m4a|flac|alac|ogg|oga|opus|wav|ape|midi?)$/)) {
|
||||||
|
return 'audio'
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileMatchesSomeType = (types, file) =>
|
||||||
types.some(type => fileType(file.mimetype) === type)
|
types.some(type => fileType(file.mimetype) === type)
|
||||||
|
|
||||||
const fileTypeService = {
|
const fileTypeService = {
|
||||||
fileType,
|
fileType,
|
||||||
|
fileTypeExt,
|
||||||
fileMatchesSomeType
|
fileMatchesSomeType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue