initial admin settings prototype (WIP)

This commit is contained in:
Henry Jameson 2023-03-14 21:50:43 +02:00
parent 9632b77786
commit 4d23d31fec
25 changed files with 760 additions and 31 deletions

View File

@ -0,0 +1,68 @@
import Modal from 'src/components/modal/modal.vue'
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
newImporter,
newExporter
} from 'src/services/export_import/export_import.js'
import {
faTimes,
faFileUpload,
faFileDownload,
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
import {
faWindowMinimize
} from '@fortawesome/free-regular-svg-icons'
library.add(
faTimes,
faWindowMinimize,
faFileUpload,
faFileDownload,
faChevronDown
)
const AdminModal = {
data () {
return {}
},
components: {
Modal,
Popover,
Checkbox,
AdminModalContent: getResettableAsyncComponent(
() => import('./admin_modal_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
delay: 0
}
)
},
methods: {
closeModal () {
this.$store.dispatch('closeAdminModal')
},
peekModal () {
this.$store.dispatch('togglePeekAdminModal')
}
},
computed: {
modalActivated () {
return this.$store.state.interface.adminModalState !== 'hidden'
},
modalOpenedOnce () {
return this.$store.state.interface.adminModalLoaded
},
modalPeeked () {
return this.$store.state.interface.adminModalState === 'minimized'
}
}
}
export default AdminModal

View File

@ -0,0 +1,80 @@
@import "src/variables";
.admin-modal {
overflow: hidden;
.setting-list,
.option-list {
list-style-type: none;
padding-left: 2em;
li {
margin-bottom: 0.5em;
}
.suboptions {
margin-top: 0.3em;
}
}
.admin-modal-panel {
overflow: hidden;
transition: transform;
transition-timing-function: ease-in-out;
transition-duration: 300ms;
width: 1000px;
max-width: 90vw;
height: 90vh;
@media all and (max-width: 800px) {
max-width: 100vw;
height: 100%;
}
>.panel-body {
height: 100%;
overflow-y: hidden;
.btn {
min-height: 2em;
min-width: 10em;
padding: 0 2em;
}
}
}
.admin-footer {
display: flex;
>* {
margin-right: 0.5em;
}
.extra-content {
display: flex;
flex-grow: 1;
}
}
&.peek {
.admin-modal-panel {
/* Explanation:
* Modal is positioned vertically centered.
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
* + 100% - we move modal completely off-screen, it's top boundary touches
* bottom of the screen
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
*/
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
@media all and (max-width: 800px) {
/* For mobile, the modal takes 100% of the available screen.
This ensures the minimized modal is always 50px above the browser bottom
bar regardless of whether or not it is visible.
*/
transform: translateY(calc(100% - 50px));
}
}
}
}

View File

@ -0,0 +1,121 @@
<template>
<Modal
:is-open="modalActivated"
class="admin-modal"
:class="{ peek: modalPeeked }"
:no-background="modalPeeked"
>
<div class="admin-modal-panel panel">
<div class="panel-heading">
<span class="title">
{{ $t('admin.settings') }}
</span>
<transition name="fade">
<div
v-if="currentSaveStateNotice"
class="alert"
:class="{ transparent: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}"
@click.prevent
>
{{ currentSaveStateNotice.error ? $t('admin.saving_err') : $t('settings.saving_ok') }}
</div>
</transition>
<button
class="btn button-default"
:title="$t('general.peek')"
@click="peekModal"
>
<FAIcon
:icon="['far', 'window-minimize']"
fixed-width
/>
</button>
<button
class="btn button-default"
:title="$t('general.close')"
@click="closeModal"
>
<FAIcon
icon="times"
fixed-width
/>
</button>
</div>
<div class="panel-body">
<AdminModalContent v-if="modalOpenedOnce" />
</div>
<div class="panel-footer admin-footer">
<Popover
class="export"
trigger="click"
placement="top"
:offset="{ y: 5, x: 5 }"
:bound-to="{ x: 'container' }"
remove-padding
>
<template #trigger>
<button
class="btn button-default"
:title="$t('general.close')"
>
<span>{{ $t("admin.file_export_import.backup_restore") }}</span>
{{ ' ' }}
<FAIcon
icon="chevron-down"
/>
</button>
</template>
<template #content="{close}">
<div class="dropdown-menu">
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="backup"
@click="close"
>
<FAIcon
icon="file-download"
fixed-width
/><span>{{ $t("admin.file_export_import.backup_settings") }}</span>
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="backupWithTheme"
@click="close"
>
<FAIcon
icon="file-download"
fixed-width
/><span>{{ $t("admin.file_export_import.backup_settings_theme") }}</span>
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="restore"
@click="close"
>
<FAIcon
icon="file-upload"
fixed-width
/><span>{{ $t("admin.file_export_import.restore_settings") }}</span>
</button>
</div>
</template>
</Popover>
<Checkbox
:model-value="!!expertLevel"
@update:modelValue="expertLevel = Number($event)"
>
{{ $t("admin.expert_mode") }}
</Checkbox>
<span
id="unscrolled-content"
class="extra-content"
/>
</div>
</div>
</Modal>
</template>
<script src="./admin_modal.js"></script>
<style src="./admin_modal.scss" lang="scss"></style>

View File

@ -0,0 +1,88 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import DataImportExportTab from './tabs/data_import_export_tab.vue'
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
import NotificationsTab from './tabs/notifications_tab.vue'
import FilteringTab from './tabs/filtering_tab.vue'
import SecurityTab from './tabs/security_tab/security_tab.vue'
import ProfileTab from './tabs/profile_tab.vue'
import GeneralTab from './tabs/general_tab.vue'
import VersionTab from './tabs/version_tab.vue'
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faWrench,
faUser,
faFilter,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
} from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench,
faUser,
faFilter,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
)
const AdminModalContent = {
components: {
TabSwitcher,
DataImportExportTab,
MutesAndBlocksTab,
NotificationsTab,
FilteringTab,
SecurityTab,
ProfileTab,
GeneralTab,
VersionTab,
ThemeTab
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
open () {
return this.$store.state.interface.AdminModalState !== 'hidden'
},
bodyLock () {
return this.$store.state.interface.AdminModalState === 'visible'
}
},
methods: {
onOpen () {
const targetTab = this.$store.state.interface.AdminModalTargetTab
// 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 Admin is opened
// it doesn't force it.
this.$store.dispatch('clearAdminModalTargetTab')
}
},
mounted () {
this.onOpen()
},
watch: {
open: function (value) {
if (value) this.onOpen()
}
}
}
export default AdminModalContent

View File

@ -0,0 +1,56 @@
@import "src/variables";
.admin_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;
}
.number-input {
max-width: 6em;
}
}
}

View File

@ -0,0 +1,33 @@
import BooleanSetting from '../settings_modal/helpers/boolean_setting.vue'
import ChoiceSetting from '../settings_modal/helpers/choice_setting.vue'
import IntegerSetting from '../settings_modal/helpers/integer_setting.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const GeneralTab = {
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
},
computed: {
mergedConfig () {
console.log(this.$store.state)
return this.$store.state
}
},
methods: {
changeDefaultScope (value) {
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
}
}
}
export default GeneralTab

View File

@ -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')
} }
} }
} }

View File

@ -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"

View File

@ -153,9 +153,9 @@
</router-link> </router-link>
<button <button
class="button-unstyled expand-icon" class="button-unstyled expand-icon"
@click.prevent="toggleStatusExpanded"
:title="$t('tool_tip.toggle_expand')" :title="$t('tool_tip.toggle_expand')"
:aria-expanded="statusExpanded" :aria-expanded="statusExpanded"
@click.prevent="toggleStatusExpanded"
> >
<FAIcon <FAIcon
class="fa-scale-110" class="fa-scale-110"

View File

@ -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 InstanceTab = {
data () {},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting
},
computed: {
...SharedComputedObject()
}
}
export default InstanceTab

View File

@ -0,0 +1,35 @@
<template>
<div :label="$t('settings.general')">
<div class="setting-item">
<h2>{{ $t('admin_dash.instance') }}</h2>
<ul class="setting-list">
<li>
<StringSetting source="admin" path=":pleroma.:instance.:name">
NAME
</StringSetting>
</li>
<li>
<StringSetting source="admin" path=":pleroma.:instance.:description">
DESCRIPTION
</StringSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./instance_tab.js"></script>
<style lang="scss">
.column-settings {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.column-settings .size-label {
display: block;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
</style>

View File

@ -42,6 +42,8 @@ export default {
switch (this.source) { switch (this.source) {
case 'profile': case 'profile':
return this.$store.state.profileConfig return this.$store.state.profileConfig
case 'admin':
return this.$store.state.adminSettings.config
default: default:
return this.$store.getters.mergedConfig return this.$store.getters.mergedConfig
} }
@ -50,6 +52,8 @@ export default {
switch (this.source) { switch (this.source) {
case 'profile': case 'profile':
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v }) return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
case 'admin':
return (k, v) => console.log(this.path, k, v)
default: default:
return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
} }
@ -66,7 +70,15 @@ export default {
return this.source === 'profile' return this.source === 'profile'
}, },
isChanged () { isChanged () {
return !this.source === 'default' && this.state !== this.defaultState switch (this.source) {
case 'profile':
return false
case 'admin':
console.log(this.$store.state.adminSettings.modifiedPaths)
return this.$store.state.adminSettings.modifiedPaths.has(this.path)
default:
return this.state !== this.defaultState
}
}, },
matchesExpertLevel () { matchesExpertLevel () {
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0 return (this.expert || 0) <= this.$store.state.config.expertLevel > 0

View File

@ -0,0 +1,9 @@
import ModifiedIndicator from './modified_indicator.vue'
import Setting from './setting.js'
export default {
components: {
ModifiedIndicator
},
...Setting
}

View File

@ -0,0 +1,25 @@
<template>
<label
v-if="matchesExpertLevel"
class="StringSetting"
>
<label :for="path">
<slot />
</label>
<input
:id="path"
class="string-input"
step="1"
:disabled="disabled"
:value="state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
</label>
</template>
<script src="./boolean_setting.js"></script>

View File

@ -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,
@ -156,8 +164,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,7 +181,6 @@ 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 })
} }
} }

View File

@ -42,7 +42,8 @@
</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">
<Popover <Popover

View File

@ -0,0 +1,76 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import DataImportExportTab from './tabs/data_import_export_tab.vue'
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
import InstanceTab from './admin_tabs/instance_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faWrench,
faUser,
faFilter,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
} from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench,
faUser,
faFilter,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
)
const SettingsModalAdminContent = {
components: {
TabSwitcher,
DataImportExportTab,
MutesAndBlocksTab,
InstanceTab
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
open () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
bodyLock () {
return this.$store.state.interface.settingsModalState === 'visible'
}
},
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

View File

@ -0,0 +1,21 @@
<template>
<tab-switcher
ref="tabSwitcher"
class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:body-scroll-lock="bodyLock"
>
<div
:label="$t('settings.general')"
icon="wrench"
data-tab-name="general"
>
<InstanceTab />
</div>
</tab-switcher>
</template>
<script src="./settings_modal_admin_content.js"></script>
<style src="./settings_modal_admin_content.scss" lang="scss"></style>

View File

@ -0,0 +1,56 @@
@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;
}
.number-input {
max-width: 6em;
}
}
}

View File

@ -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>

View File

@ -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]

View File

@ -10,7 +10,7 @@ export const newUserFlags = {
...defaultState.flagStorage ...defaultState.flagStorage
} }
const serverSideStorage = { const adminSettingsStorage = {
state: { state: {
...cloneDeep(defaultState) ...cloneDeep(defaultState)
}, },
@ -40,9 +40,10 @@ const serverSideStorage = {
} }
set(config, path, convert(c.value)) set(config, path, convert(c.value))
}) })
console.log(config)
commit('updateAdminSettings', { config, modifiedPaths }) commit('updateAdminSettings', { config, modifiedPaths })
} }
} }
} }
export default serverSideStorage export default adminSettingsStorage

View File

@ -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')