Merge branch 'admin-tabs-2' into 'develop'

Most of the remaining admin tabs

See merge request pleroma/pleroma-fe!2187
This commit is contained in:
HJ 2025-12-19 17:21:35 +00:00
commit aa426b3d14
114 changed files with 4924 additions and 952 deletions

View file

@ -29,9 +29,9 @@ const copyPlugin = ({ inUrl, inFs }) => {
order: 'post',
sequential: true,
async handler () {
console.log(`Copying '${inFs}' to ${copyTarget}...`)
console.info(`Copying '${inFs}' to ${copyTarget}...`)
await cp(inFs, copyTarget, { recursive: true })
console.log('Done.')
console.info('Done.')
}
}
}]

View file

@ -178,7 +178,7 @@ export const buildSwPlugin = ({
order: 'post',
sequential: true,
async handler () {
console.log('Building service worker for production')
console.info('Building service worker for production')
await build(config)
}
}

View file

@ -0,0 +1,2 @@
Most of the remaining AdminFE tabs were added into Admin Dashboard
It's now possible to customize PWA Manfiest from PleromaFE

View file

@ -513,6 +513,12 @@ nav {
}
}
label {
&.-disabled {
color: var(--textFaint);
}
}
input,
textarea {
border: none;
@ -553,6 +559,10 @@ textarea {
&[disabled="disabled"],
&.disabled {
cursor: not-allowed;
color: var(--textFaint);
/* stylelint-disable-next-line declaration-no-important */
background-color: transparent !important;
}
&[type="range"] {
@ -578,6 +588,8 @@ textarea {
& + label::before {
opacity: 0.5;
}
background-color: var(--background);
}
+ label::before {
@ -677,7 +689,8 @@ option {
list-style: none;
display: grid;
grid-auto-flow: row dense;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
grid-gap: 0.5em;
li {
border: 1px solid var(--border);
@ -698,7 +711,6 @@ option {
--_roundness-right: 0;
position: relative;
flex: 1 1 auto;
}
> *:first-child,

View file

@ -1,7 +1,7 @@
<template>
<label
class="checkbox"
:class="[{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }, radio ? '-radio' : '-checkbox']"
:class="[{ ['-disabled']: disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }, radio ? '-radio' : '-checkbox']"
>
<span
v-if="!!$slots.before"
@ -123,7 +123,7 @@ export default {
.disabled {
.checkbox-indicator::before {
background-color: var(--background);
background-color: transparent;
}
}

View file

@ -1,24 +1,28 @@
<template>
<div class="font-control">
<Checkbox
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
class="font-checkbox"
:model-value="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
<i18n-t
scope="global"
keypath="settings.style.fonts.override"
tag="span"
<div class="setting-item">
<Checkbox
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
class="font-checkbox setting-control setting-label"
:model-value="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
{{ label }}
</i18n-t>
</Checkbox>
<i18n-t
scope="global"
keypath="settings.style.fonts.override"
tag="span"
>
<span>
{{ label }}
</span>
</i18n-t>
</Checkbox>
</div>
{{ ' ' }}
<div
v-if="modelValue?.family"
class="font-input"
class="font-input setting-item"
>
<label
v-if="manualEntry"
@ -69,7 +73,7 @@
</span>
<span
v-else
class="btn-group"
class="font-selector btn-group"
>
<button
class="btn button-default"
@ -132,18 +136,6 @@
<script src="./font_control.js"></script>
<style lang="scss">
.font-control {
.custom-font {
min-width: 20em;
max-width: 20em;
}
.font-input {
margin-left: 2em;
margin-top: 0.5em;
}
}
.invalid-tooltip {
margin: 0.5em 1em;
min-width: 10em;

View file

@ -3,6 +3,8 @@ import localeService from '../../services/locale/locale.service.js'
import Select from '../select/select.vue'
import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue'
import { v4 as uuidv4 } from 'uuid';
export default {
components: {
Select,
@ -26,7 +28,9 @@ export default {
languages () {
return localeService.languages
},
uniqueId () {
return uuidv4()
},
controlledLanguage: {
get: function () {
return Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]

View file

@ -1,30 +1,32 @@
<template>
<div class="interface-language-switcher">
<label>
<slot />
<ProfileSettingIndicator :is-profile="profile" />
</label>
<ul class="setting-list">
<li
v-for="index of controlledLanguage.keys()"
:key="index"
<ul class="interface-language-switcher setting-list">
<li
v-for="index of controlledLanguage.keys()"
:key="index"
class="setting-item"
>
<label
class="setting-label"
:for="uniqueId+index"
>
<label>
{{ index === 0 ? $t('settings.primary_language') : $t('settings.fallback_language', { index }, index) }}
<Select
class="language-select"
:model-value="controlledLanguage[index]"
@update:model-value="val => setLanguageAt(index, val)"
{{ index === 0 ? $t('settings.primary_language') : $t('settings.fallback_language', { index }, index) }}
</label>
<span class="setting-control btn-group">
<Select
:id="uniqueId+index"
:name="uniqueId+index"
class="language-select"
:model-value="controlledLanguage[index]"
@update:model-value="val => setLanguageAt(index, val)"
>
<option
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
>
<option
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
>
{{ lang.name }}
</option>
</Select>
</label>
{{ lang.name }}
</option>
</Select>
<button
v-if="controlledLanguage.length > 1 && index !== 0"
class="button-default btn"
@ -32,25 +34,53 @@
>
{{ $t('settings.remove_language') }}
</button>
</li>
<li>
<button
class="button-default btn"
@click="addLanguage"
>
{{ $t('settings.add_language') }}
</button>
</li>
</ul>
</div>
</span>
</li>
<li class="add-button">
<button
class="button-default btn"
@click="addLanguage"
>
{{ $t('settings.add_language') }}
</button>
</li>
</ul>
</template>
<script src="./interface_language_switcher.js"></script>
<style lang="scss">
.interface-language-switcher {
.language-select {
margin-right: 1em;
.setting-list {
.setting-item {
display: grid;
grid-template-columns: subgrid;
}
}
.add-button {
display: block;
text-align: center;
padding-bottom: 1em;
.default-button {
display: block;
width: auto;
}
}
.-mobile & {
li.setting-item {
display: flex;
flex-direction: column;
gap: 0.5em;
align-items: stretch;
border-bottom: none;
}
.add-button {
border-bottom: 1px solid var(--border);
}
}
}
</style>

View file

@ -395,7 +395,7 @@
}
form textarea {
line-height: 16px;
line-height: 1;
resize: vertical;
}

View file

@ -26,6 +26,7 @@
/* TODO fix order of styles */
label.Select {
padding: 0;
display: flex;
select {
appearance: none;
@ -33,13 +34,12 @@ label.Select {
border: none;
color: var(--text);
margin: 0;
padding: 0 2em 0 0.2em;
padding: 0 2em 0 0.5em;
font-family: var(--font);
font-size: 1em;
width: 100%;
z-index: 1;
height: 2em;
line-height: 16px;
line-height: 2;
&[multiple],
&[size] {
@ -79,7 +79,7 @@ label.Select {
position: absolute;
top: 0;
bottom: 0;
right: 5px;
right: 0.5em;
height: 100%;
width: 0.875em;
font-family: var(--font);

View file

@ -0,0 +1,39 @@
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 TupleSetting from '../helpers/tuple_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import ListSetting from '../helpers/list_setting.vue'
import MapSetting from '../helpers/map_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const AuthTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
TupleSetting,
AttachmentSetting,
GroupSetting,
ListSetting,
MapSetting
},
computed: {
...SharedComputedObject(),
LDAPEnabled () {
return this.$store.state.adminSettings.draft[':pleroma'][':ldap'][':enabled']
},
}
}
export default AuthTab

View file

@ -0,0 +1,105 @@
<template>
<div :label="$t('admin_dash.tabs.job_queues')">
<div class="setting-section">
<h3>{{ $t('admin_dash.auth.MFA') }}</h3>
<ul class="setting-list">
<li>
<h4>{{ $t('admin_dash.auth.TOTP') }}</h4>
<ul class="setting-list suboptions">
<li>
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:totp.:digits" />
</li>
<li>
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:totp.:period" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.auth.backup_codes') }}</h4>
<ul class="setting-list suboptions">
<li>
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:backup_codes.:number" />
</li>
<li>
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:backup_codes.:length" />
</li>
</ul>
</li>
<GroupSetting path=":pleroma.:instance.:multi_factor_authentication" />
</ul>
<h3>{{ $t('admin_dash.auth.OAuth') }}</h3>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:auth.:auth_template" />
</li>
<li>
<BooleanSetting path=":pleroma.:auth.:enforce_oauth_admin_scope_usage" />
</li>
<li>
<StringSetting path=":pleroma.:auth.:oauth_consumer_template" />
</li>
<li>
<ListSetting path=":pleroma.:auth.:oauth_consumer_strategies" />
</li>
<li>
<IntegerSetting path=":pleroma.:oauth2.:token_expires_in" />
</li>
<li>
<BooleanSetting path=":pleroma.:oauth2.:issue_new_refresh_token" />
</li>
<li>
<BooleanSetting path=":pleroma.:oauth2.:clean_expired_tokens" />
</li>
</ul>
<h3>{{ $t('admin_dash.auth.LDAP') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:ldap.:enabled" />
</li>
<template v-if="LDAPEnabled">
<li>
<StringSetting path=":pleroma.:ldap.:host" />
</li>
<li>
<IntegerSetting path=":pleroma.:ldap.:port" />
</li>
<li>
<BooleanSetting path=":pleroma.:ldap.:tls" />
</li>
<li>
<!-- CONFIRM old admin FE only supports ONE setting which is Verify, is that correct or should we allow more than one? -->
<MapSetting
:allow-new="false"
path=":pleroma.:ldap.:tlsopts"
/>
</li>
<li>
<BooleanSetting path=":pleroma.:ldap.:ssl" />
</li>
<li>
<!-- CONFIRM old admin FE only supports ONE setting which is Verify, is that correct or should we allow more than one? -->
<MapSetting
:allow-new="false"
path=":pleroma.:ldap.:sslopts"
/>
</li>
<li>
<StringSetting path=":pleroma.:ldap.:base" />
</li>
<li>
<StringSetting path=":pleroma.:ldap.:uid" />
</li>
<li>
<StringSetting path=":pleroma.:ldap.:cacertfile" />
</li>
<li>
<StringSetting path=":pleroma.:ldap.:mail" />
</li>
</template>
</ul>
</div>
<!-- CONFIRM admin token is present in AdminFE but missing in both data and descriptions?? -->
</div>
</template>
<script src="./auth_tab.js"></script>

View file

@ -10,6 +10,22 @@ import ModifiedIndicator from '../helpers/modified_indicator.vue'
import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
import { useInterfaceStore } from 'src/stores/interface'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faArrowsRotate,
faFolderOpen,
faServer,
faDownload
} from '@fortawesome/free-solid-svg-icons'
library.add(
faArrowsRotate,
faFolderOpen,
faDownload,
faServer
)
const EmojiTab = {
components: {
TabSwitcher,
@ -44,10 +60,12 @@ const EmojiTab = {
},
computed: {
...SharedComputedObject(),
pack () {
return this.packName !== '' ? this.knownPacks[this.packName] : undefined
},
packMeta () {
if (this.packName === '') return {}
if (this.editedMetadata[this.packName] === undefined) {
this.editedMetadata[this.packName] = clone(this.pack.pack)
}
@ -98,8 +116,6 @@ const EmojiTab = {
return Promise.reject(resp)
}
}).then(() => {
this.$refs.createPackPopover.hidePopover()
this.packName = this.newPackName
this.newPackName = ''
})
@ -205,8 +221,6 @@ const EmojiTab = {
for (const pack in this.knownRemotePacks[inst]) {
this.sortPackFiles(`${pack}@${inst}`)
}
this.$refs.remotePackPopover.hidePopover()
})
.catch(data => {
this.displayError(data)
@ -223,8 +237,6 @@ const EmojiTab = {
.then(data => data.json())
.then(resp => {
if (resp === 'ok') {
this.$refs.downloadPackPopover.hidePopover()
return this.refreshPackList()
} else {
this.displayError(resp.error)
@ -242,8 +254,6 @@ const EmojiTab = {
.then(data => data.json())
.then(resp => {
if (resp === 'ok') {
this.$refs.additionalRemotePopover.hidePopover()
return this.refreshPackList()
} else {
this.displayError(resp.error)
@ -262,8 +272,6 @@ const EmojiTab = {
.then(data => data.json())
.then(resp => {
if (resp === 'ok') {
this.$refs.additionalRemotePopover.hidePopover()
return this.refreshPackList()
} else {
this.displayError(resp.error)

View file

@ -1,10 +1,54 @@
.emoji-tab {
.btn-group .btn:not(:first-child) {
margin-left: 0.5em;
.EmojiTab {
.setting-list {
margin: 0.5em 2em;
}
.pack-info-wrapper {
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
.header-buttons {
display: flex;
flex: 1 0 auto;
justify-content: end;
align-items: end;
&:not(.btn-group) {
gap: 0.5em;
}
.header-text {
flex: 1 0 auto;
}
}
.button-default {
flex: 0 0 auto;
padding: 0.5em;
font-size: 1rem;
}
.popover-wrapper {
display: flex;
}
}
.selector-buttons,
.meta-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-left: 1em;
}
h5 {
margin-top: 1em;
margin-bottom: 0.25em;
}
h3.toolbar {
align-items: end;
}
.emoji-info-input {
@ -18,8 +62,8 @@
}
.emoji {
width: 32px;
height: 32px;
width: 2em;
height: 2em;
}
.emoji-unsaved {
@ -30,6 +74,22 @@
display: flex;
flex-wrap: wrap;
gap: 1em;
.button-unstyled {
display: flex;
}
.emoji-item,
.placeholder {
width: 2em;
height: 2em;
opacity: 0.5;
}
.placeholder {
background: var(--textFaint);
border-radius: 0.5em;
}
}
}

View file

@ -1,168 +1,75 @@
<template>
<div
class="emoji-tab"
class="EmojiTab"
:label="$t('admin_dash.tabs.emoji')"
>
<div class="setting-item">
<h2>{{ $t('admin_dash.tabs.emoji') }}</h2>
<div class="setting-section">
<h3 class="toolbar">
<span class="header-text">
{{ $t('admin_dash.emoji.emoji_packs') }}
</span>
<ul class="setting-list">
<h3>{{ $t('admin_dash.emoji.global_actions') }}</h3>
<li class="btn-group setting-item">
<span class="header-buttons btn-group">
<button
class="button button-default btn"
class="button button-default"
type="button"
:title="$t('admin_dash.emoji.reload')"
@click="reloadEmoji"
>
{{ $t('admin_dash.emoji.reload') }}
</button>
<button
class="button button-default btn"
type="button"
@click="importFromFS"
>
{{ $t('admin_dash.emoji.importFS') }}
</button>
</li>
<li class="btn-group setting-item">
<button
class="button button-default btn"
type="button"
@click="$refs.remotePackPopover.showPopover"
>
{{ $t('admin_dash.emoji.remote_packs') }}
<Popover
ref="remotePackPopover"
popover-class="emoji-tab-edit-popover popover-default"
trigger="click"
placement="bottom"
bound-to-selector=".emoji-tab"
:bound-to="{ x: 'container' }"
:offset="{ y: 5 }"
>
<template #content>
<div class="emoji-tab-popover-input">
<h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3>
<input
v-model="remotePackInstance"
class="input"
:placeholder="$t('admin_dash.emoji.remote_pack_instance')"
>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
@click="listRemotePacks"
>
{{ $t('admin_dash.emoji.do_list') }}
</button>
</div>
</template>
</Popover>
</button>
<button
class="button button-default emoji-panel-additional-actions"
@click="$refs.additionalRemotePopover.showPopover"
>
<FAIcon
icon="chevron-down"
/>
<Popover
ref="additionalRemotePopover"
popover-class="emoji-tab-edit-popover popover-default"
trigger="click"
placement="bottom"
bound-to-selector=".emoji-tab"
:bound-to="{ x: 'container' }"
:offset="{ y: 5 }"
>
<template #content>
<div class="emoji-tab-popover-input">
<h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
<input
v-model="newPackName"
:placeholder="$t('admin_dash.emoji.new_pack_name')"
class="input"
>
<h3>Import pack from URL</h3>
<input
v-model="remotePackURL"
class="input"
placeholder="Pack .zip URL"
>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
:disabled="newPackName.trim() === '' || remotePackURL.trim() === ''"
@click="downloadRemoteURLPack"
>
Import
</button>
<h3>Import pack from a file</h3>
<input
type="file"
accept="application/zip"
class="emoji-tab-popover-file input"
@change="remotePackFile = $event.target.files"
>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
:disabled="newPackName.trim() === '' || remotePackFile === null || remotePackFile.length === 0"
@click="downloadRemoteFilePack"
>
Import
</button>
</div>
</template>
</Popover>
</button>
</li>
<h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3>
<li>
<h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4>
<Select
v-model="packName"
class="form-control"
>
<option
value=""
disabled
hidden
>
{{ $t('admin_dash.emoji.emoji_pack') }}
</option>
<option
v-for="(pack, listPackName) in knownPacks"
:key="listPackName"
:label="listPackName"
>
{{ listPackName }}
</option>
</Select>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
@click="$refs.createPackPopover.showPopover"
>
{{ $t('admin_dash.emoji.create_pack') }}
<FAIcon icon="arrows-rotate" />
{{ $t('admin_dash.emoji.reload_short') }}
</button>
<Popover
ref="createPackPopover"
popover-class="emoji-tab-edit-popover popover-default"
trigger="click"
placement="bottom"
bound-to-selector=".emoji-tab"
:bound-to="{ x: 'container' }"
:offset="{ y: 5 }"
>
<template #trigger>
<button
class="button button-default"
type="button"
:title="$t('admin_dash.emoji.remote_packs')"
>
<FAIcon icon="download" />
{{ $t('admin_dash.emoji.remote_packs_short') }}
</button>
</template>
<template #content>
<div class="emoji-tab-popover-input">
<h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3>
<input
v-model="remotePackInstance"
class="input"
:placeholder="$t('admin_dash.emoji.remote_pack_instance')"
>
<button
class="button button-default emoji-tab-popover-button"
type="button"
@click="listRemotePacks"
>
{{ $t('admin_dash.emoji.do_list') }}
</button>
</div>
</template>
</Popover>
<Popover
ref="additionalRemotePopover"
popover-class="emoji-tab-edit-popover popover-default"
trigger="click"
placement="bottom"
>
<template #trigger>
<button
class="button button-default emoji-panel-additional-actions"
:title="$t('admin_dash.emoji.import_pack')"
@click="$refs.additionalRemotePopover.showPopover"
>
<FAIcon icon="folder-open" />
{{ $t('admin_dash.emoji.import_pack_short') }}
</button>
</template>
<template #content>
<div class="emoji-tab-popover-input">
<h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
@ -171,94 +78,276 @@
:placeholder="$t('admin_dash.emoji.new_pack_name')"
class="input"
>
<h3>Import pack from URL</h3>
<input
v-model="remotePackURL"
class="input"
placeholder="Pack .zip URL"
>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
@click="createEmojiPack"
:disabled="newPackName.trim() === '' || remotePackURL.trim() === ''"
@click="downloadRemoteURLPack"
>
{{ $t('admin_dash.emoji.create') }}
Import
</button>
<h3>Import pack from a file</h3>
<input
type="file"
accept="application/zip"
class="emoji-tab-popover-file input"
@change="remotePackFile = $event.target.files"
>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
:disabled="newPackName.trim() === '' || remotePackFile === null || remotePackFile.length === 0"
@click="downloadRemoteFilePack"
>
Import
</button>
</div>
</template>
</Popover>
</li>
</ul>
</span>
</h3>
<div class="setting-section">
<h4 class="toolbar">
{{ $t('admin_dash.emoji.edit_pack') }}
</h4>
<div class="setting-item selector-buttons">
<button
:disabled="!pack || pack.remote !== undefined"
class="button button-default btn"
type="button"
@click="deleteModalVisible = true"
>
{{ $t('admin_dash.emoji.delete_pack') }}
<div v-if="pack">
<div class="pack-info-wrapper">
<ul class="setting-list">
<li>
<label>
{{ $t('admin_dash.emoji.description') }}
<ConfirmModal
v-if="deleteModalVisible"
:title="$t('admin_dash.emoji.delete_title')"
:cancel-text="$t('status.delete_confirm_cancel_button')"
:confirm-text="$t('status.delete_confirm_accept_button')"
@cancelled="deleteModalVisible = false"
@accepted="deleteEmojiPack"
>
{{ $t('admin_dash.emoji.delete_confirm', [packName]) }}
</ConfirmModal>
</button>
<button
:disabled="!pack || pack.remote === undefined"
class="button button-default btn"
type="button"
@click="$refs.downloadPackPopover.showPopover"
>
{{ $t('admin_dash.emoji.download_pack') }}
<Popover
ref="downloadPackPopover"
trigger="click"
placement="bottom"
bound-to-selector=".emoji-tab"
popover-class="emoji-tab-edit-popover popover-default"
:bound-to="{ x: 'container' }"
:offset="{ y: 5 }"
>
<template #content>
<h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3>
<div>
<div>
<div class="emoji-tab-popover-input">
<label>
{{ $t('admin_dash.emoji.download_as_name') }}
<input
v-model="remotePackDownloadAs"
class="emoji-data-input input"
:placeholder="$t('admin_dash.emoji.download_as_name_full')"
>
</label>
<div
v-if="downloadWillReplaceLocal"
class="warning"
>
<em>{{ $t('admin_dash.emoji.replace_warning') }}</em>
</div>
</div>
<button
class="button button-default btn"
type="button"
@click="downloadRemotePack"
>
{{ $t('admin_dash.emoji.download') }}
</button>
</div>
</div>
</template>
</Popover>
</button>
<span class="btn-group">
<Select
v-model="packName"
class="form-control"
>
<option
value=""
disabled
hidden
>
{{ $t('admin_dash.emoji.emoji_pack') }}
</option>
<option
v-for="(pack, listPackName) in knownPacks"
:key="listPackName"
:label="listPackName"
>
{{ listPackName }}
</option>
</Select>
<Popover
ref="createPackPopover"
popover-class="emoji-tab-edit-popover popover-default"
trigger="click"
placement="bottom"
>
<template #trigger>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
>
{{ $t('admin_dash.emoji.create_pack') }}
</button>
</template>
<template #content>
<div class="emoji-tab-popover-input">
<h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
<input
v-model="newPackName"
:placeholder="$t('admin_dash.emoji.new_pack_name')"
class="input"
>
<button
class="button button-default btn emoji-tab-popover-button"
type="button"
@click="createEmojiPack"
>
{{ $t('admin_dash.emoji.create') }}
</button>
</div>
</template>
</Popover>
</span>
</div>
<h5>
{{ $t('admin_dash.emoji.metadata') }}
<ModifiedIndicator
:changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
message-key="admin_dash.emoji.emoji_changed"
/>
</h5>
<ul class="setting-list">
<li>
<label
class="setting-item"
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
>
<span class="setting-label">
<ModifiedIndicator
:changed="metaEdited('description')"
message-key="admin_dash.emoji.metadata_changed"
/>
<textarea
v-model="packMeta.description"
:disabled="pack.remote !== undefined"
class="bio resize-height input"
/>
</label>
</li>
<li>
<label>
{{ $t('admin_dash.emoji.homepage') }}
{{ $t('admin_dash.emoji.description') }}
</span>
<textarea
v-model="packMeta.description"
:disabled="!pack || pack.remote !== undefined"
height="4"
class="bio resize-height input textarea setting-control"
/>
</label>
</li>
<li>
<label
class="setting-item"
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
>
<span class="setting-label">
<ModifiedIndicator
:changed="metaEdited('homepage')"
message-key="admin_dash.emoji.metadata_changed"
/>
{{ $t('admin_dash.emoji.homepage') }}
</span>
<input
v-model="packMeta.homepage"
class="emoji-info-input input"
:disabled="pack.remote !== undefined"
>
</label>
</li>
<li>
<label>
{{ $t('admin_dash.emoji.fallback_src') }}
<input
v-model="packMeta.homepage"
class="emoji-info-input input setting-control"
:disabled="!pack || pack.remote !== undefined"
>
</label>
</li>
<li>
<label
class="setting-item"
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
>
<span class="setting-label">
<ModifiedIndicator
:changed="metaEdited('fallback-src')"
message-key="admin_dash.emoji.metadata_changed"
/>
{{ $t('admin_dash.emoji.fallback_src') }}
</span>
<input
v-model="packMeta['fallback-src']"
class="emoji-info-input input"
:disabled="pack.remote !== undefined"
>
</label>
</li>
<li>
<label>
<input
v-model="packMeta['fallback-src']"
class="emoji-info-input input setting-control"
:disabled="!pack || pack.remote !== undefined"
>
</label>
</li>
<li>
<label
class="setting-item"
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
>
<span class="setting-label">
{{ $t('admin_dash.emoji.fallback_sha256') }}
</span>
<input
v-model="packMeta['fallback-src-sha256']"
:disabled="true"
class="emoji-info-input input"
>
</label>
</li>
<li>
<input
v-model="packMeta['fallback-src-sha256']"
:disabled="!pack || pack.remote !== undefined"
class="emoji-info-input input setting-control"
>
</label>
</li>
<li>
<div class="setting-item">
<Checkbox
v-model="packMeta['share-files']"
:disabled="pack.remote !== undefined"
:disabled="!pack || pack.remote !== undefined"
class="setting-label setting-control"
>
<ModifiedIndicator
:changed="metaEdited('share-files')"
message-key="admin_dash.emoji.metadata_changed"
/>
{{ $t('admin_dash.emoji.share') }}
</Checkbox>
<ModifiedIndicator
:changed="metaEdited('share-files')"
message-key="admin_dash.emoji.metadata_changed"
/>
</li>
<li class="btn-group">
</div>
</li>
<li>
<div class="meta-buttons">
<button
v-if="pack.remote === undefined"
v-if="pack && pack.remote === undefined"
class="button button-default btn"
type="button"
@click="savePackMetadata"
@ -266,148 +355,89 @@
{{ $t('admin_dash.emoji.save_meta') }}
</button>
<button
v-if="pack.remote === undefined"
v-if="pack && pack.remote === undefined"
class="button button-default btn"
type="button"
@click="savePackMetadata"
>
{{ $t('admin_dash.emoji.revert_meta') }}
</button>
<button
v-if="pack.remote === undefined"
class="button button-default btn"
type="button"
@click="deleteModalVisible = true"
>
{{ $t('admin_dash.emoji.delete_pack') }}
<ConfirmModal
v-if="deleteModalVisible"
:title="$t('admin_dash.emoji.delete_title')"
:cancel-text="$t('status.delete_confirm_cancel_button')"
:confirm-text="$t('status.delete_confirm_accept_button')"
@cancelled="deleteModalVisible = false"
@accepted="deleteEmojiPack"
>
{{ $t('admin_dash.emoji.delete_confirm', [packName]) }}
</ConfirmModal>
</button>
<button
v-if="pack.remote !== undefined"
class="button button-default btn"
type="button"
@click="$refs.downloadPackPopover.showPopover"
>
{{ $t('admin_dash.emoji.download_pack') }}
<Popover
ref="downloadPackPopover"
trigger="click"
placement="bottom"
bound-to-selector=".emoji-tab"
popover-class="emoji-tab-edit-popover popover-default"
:bound-to="{ x: 'container' }"
:offset="{ y: 5 }"
>
<template #content>
<h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3>
<div>
<div>
<div class="emoji-tab-popover-input">
<label>
{{ $t('admin_dash.emoji.download_as_name') }}
<input
v-model="remotePackDownloadAs"
class="emoji-data-input input"
:placeholder="$t('admin_dash.emoji.download_as_name_full')"
>
</label>
<div
v-if="downloadWillReplaceLocal"
class="warning"
>
<em>{{ $t('admin_dash.emoji.replace_warning') }}</em>
</div>
</div>
<button
class="button button-default btn"
type="button"
@click="downloadRemotePack"
>
{{ $t('admin_dash.emoji.download') }}
</button>
</div>
</div>
</template>
</Popover>
</button>
</li>
</ul>
</div>
<ul class="setting-list">
<h4>
{{ $t('admin_dash.emoji.files') }}
<ModifiedIndicator
v-if="pack"
:changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
message-key="admin_dash.emoji.emoji_changed"
/>
</h4>
<div
v-if="pack"
class="emoji-list"
>
<EmojiEditingPopover
v-if="pack.remote === undefined"
placement="bottom"
new-upload
:title="$t('admin_dash.emoji.adding_new')"
:pack-name="packName"
@update-pack-files="updatePackFiles"
@display-error="displayError"
>
<template #trigger>
<FAIcon
icon="plus"
size="2x"
:title="$t('admin_dash.emoji.add_file')"
/>
</template>
</EmojiEditingPopover>
<EmojiEditingPopover
v-for="(file, shortcode) in pack.files"
ref="emojiPopovers"
:key="shortcode"
placement="top"
:title="$t(`admin_dash.emoji.${pack.remote === undefined ? 'editing' : 'copying'}`, [shortcode])"
:shortcode="shortcode"
:file="file"
:pack-name="packName"
:remote="pack.remote"
:known-local-packs="knownLocalPacks"
@update-pack-files="updatePackFiles"
@display-error="displayError"
>
<template #trigger>
<StillImage
class="emoji"
:src="emojiAddr(file)"
:title="`:${shortcode}:`"
:alt="`:${shortcode}:`"
/>
</template>
</EmojiEditingPopover>
</div>
</div>
</li>
</ul>
<h5>
{{ $t('admin_dash.emoji.files') }}
<ModifiedIndicator
:changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
message-key="admin_dash.emoji.emoji_changed"
/>
</h5>
<div
class="emoji-list setting-list"
>
<EmojiEditingPopover
v-if="pack && pack.remote === undefined"
class="emoji-item"
placement="bottom"
new-upload
:title="$t('admin_dash.emoji.adding_new')"
:pack-name="packName"
@update-pack-files="updatePackFiles"
@display-error="displayError"
>
<template #trigger>
<FAIcon
icon="plus"
size="2x"
:title="$t('admin_dash.emoji.add_file')"
/>
</template>
</EmojiEditingPopover>
<template v-if="!pack">
<div
v-for="(_, i) in new Array(20)"
:key="i"
class="placeholder"
/>
</template>
<EmojiEditingPopover
v-for="(file, shortcode) in (pack?.files || [])"
ref="emojiPopovers"
:key="shortcode"
placement="top"
:title="$t(`admin_dash.emoji.${pack?.remote === undefined ? 'editing' : 'copying'}`, [shortcode])"
:shortcode="shortcode"
:file="file"
:pack-name="packName"
:remote="pack?.remote"
:known-local-packs="knownLocalPacks"
@update-pack-files="updatePackFiles"
@display-error="displayError"
>
<template #trigger>
<StillImage
class="emoji"
:src="emojiAddr(file)"
:title="`:${shortcode}:`"
:alt="`:${shortcode}:`"
/>
</template>
</EmojiEditingPopover>
</div>
</div>
<h3>{{ $t('admin_dash.emoji.advanced') }}</h3>
<button
class="button button-default btn"
type="button"
@click="importFromFS"
>
<FAIcon icon="server" />
{{ $t('admin_dash.emoji.importFS') }}
</button>
</div>
</div>
</template>

View file

@ -0,0 +1,34 @@
import BooleanSetting from '../helpers/boolean_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 ListSetting from '../helpers/list_setting.vue'
import ListTupleSetting from '../helpers/list_tuple_setting.vue'
import MapSetting from '../helpers/map_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const FederationTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
ListSetting,
ListTupleSetting,
GroupSetting,
MapSetting
},
computed: {
...SharedComputedObject()
}
}
export default FederationTab

View file

@ -0,0 +1,68 @@
<template>
<div :label="$t('admin_dash.tabs.federation')">
<div class="setting-section">
<h3>{{ $t('admin_dash.federation.global') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:instance.:federating" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:skip_thread_containment" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:external_user_synchronization" />
</li>
</ul>
<h3>{{ $t('admin_dash.federation.restrictions') }}</h3>
<ul class="setting-list">
<li>
<MapSetting path=":pleroma.:instance.:quarantined_instances" />
</li>
<li>
<MapSetting path=":pleroma.:instance.:rejected_instances" />
</li>
</ul>
<h3>{{ $t('admin_dash.federation.activitypub') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:instance.:allow_relay" />
</li>
<li>
<BooleanSetting path=":pleroma.:activitypub.:unfollow_blocked" />
</li>
<li>
<BooleanSetting path=":pleroma.:activitypub.:outgoing_blocks" />
</li>
<li>
<BooleanSetting path=":pleroma.:activitypub.:blockers_visible" />
</li>
<li>
<IntegerSetting path=":pleroma.:activitypub.:follow_handshake_timeout" />
</li>
<li>
<IntegerSetting path=":pleroma.:activitypub.:note_replies_output_limit" />
</li>
<li>
<IntegerSetting path=":pleroma.:instance.:federation_incoming_replies_max_depth" />
</li>
<li>
<IntegerSetting path=":pleroma.:instance.:federation_reachability_timeout_days" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:allow_relay" />
</li>
<li>
<BooleanSetting path=":pleroma.:activitypub.:sign_object_fetches" />
</li>
<li>
<BooleanSetting path=":pleroma.:activitypub.:authorized_fetch_mode" />
</li>
<li>
<BooleanSetting path=":pleroma.:activitypub.:client_api_enabled" />
</li>
</ul>
</div>
</div>
</template>
<script src="./federation_tab.js"></script>

View file

@ -44,10 +44,10 @@ const FrontendsTab = {
}
},
computed: {
...SharedComputedObject(),
frontends () {
return this.$store.state.adminSettings.frontends
},
...SharedComputedObject()
}
},
methods: {
canInstall (frontend) {

View file

@ -1,8 +1,24 @@
.frontends-tab {
.FrontendsTab {
.cards-list {
padding: 0;
}
li.frontend-card {
display: flex;
margin: 0;
flex-direction: column;
}
.frontend-buttons {
margin-top: 0.5em;
display: flex;
justify-content: end;
gap: 0.5em;
flex-wrap: wrap;
flex: 1 0 auto;
align-items: end;
}
.relative {
position: relative;
}
@ -16,10 +32,26 @@
inset: 0;
}
h5 {
margin: 0;
font-size: 1.15em
}
dl {
margin-left: 1em;
}
dt {
margin-top: 0.5em;
text-overflow: ellipsis;
white-space: nowrap;
overflow-x: hidden;
}
dd {
text-overflow: ellipsis;
white-space: nowrap;
overflow-x: hidden;
max-width: 10em;
}
}

View file

@ -1,17 +1,17 @@
<template>
<div
class="frontends-tab"
class="FrontendsTab"
:label="$t('admin_dash.tabs.frontends')"
>
<div class="setting-item">
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
<div class="setting-section">
<h3>{{ $t('admin_dash.frontend.title') }}</h3>
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
<ul
v-if="adminDraft"
class="setting-list"
>
<li>
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
<h4>{{ $t('admin_dash.frontend.default_frontend') }}</h4>
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
<ul class="setting-list">
<li>
@ -38,13 +38,14 @@
v-if="working"
class="overlay"
/>
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
<h4>{{ $t('admin_dash.frontend.available_frontends') }}</h4>
<ul class="cards-list">
<li
v-for="frontend in frontends"
:key="frontend.name"
class="frontend-card"
>
<strong>{{ frontend.name }}</strong>
<h5>{{ frontend.name }}</h5>
{{ ' ' }}
<span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
<i18n-t
@ -89,7 +90,7 @@
>{{ frontend.build_url }}</a>
</dd>
</dl>
<div>
<div class="frontend-buttons">
<span class="btn-group">
<button
class="button button-default btn"

View file

@ -0,0 +1,46 @@
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 ListSetting from '../helpers/list_setting.vue'
import TupleSetting from '../helpers/tuple_setting.vue'
import MapSetting from '../helpers/map_setting.vue'
import ProxySetting from '../helpers/proxy_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { get } from 'lodash'
const HTTPTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
MapSetting,
GroupSetting,
ListSetting,
TupleSetting,
ProxySetting
},
computed: {
...SharedComputedObject(),
sslOptions () {
const desc = get(this.$store.state.adminSettings.descriptions, ':pleroma.:http.:adapter.:ssl_options.:versions')
return new Set(desc.suggestions.map(option => ({
label: option.replace(':tlsv', 'TLS v'),
value: option
})))
},
}
}
export default HTTPTab

View file

@ -0,0 +1,86 @@
<template>
<div
class="LinksTab"
:label="$t('admin_dash.tabs.http')"
>
<div class="setting-section">
<h3>{{ $t('admin_dash.http.outbound') }}</h3>
<ul class="setting-list">
<li>
<ProxySetting
hide-description
path=":pleroma.:http.:proxy_url"
/>
</li>
<li>
<BooleanSetting path=":pleroma.:http.:send_user_agent" />
</li>
<li>
<StringSetting path=":pleroma.:http.:user_agent" />
</li>
<li>
<ListSetting
override-available-options
:options="sslOptions"
path=":pleroma.:http.:adapter.:ssl_options.:versions"
/>
</li>
<li>
<GroupSetting path=":pleroma.:http.:adapter" />
</li>
</ul>
</div>
<div class="setting-section">
<h3>{{ $t('admin_dash.http.incoming') }}</h3>
<ul class="setting-list">
<h4>{{ $t('admin_dash.http.security') }}</h4>
<li>
<BooleanSetting path=":pleroma.:http_security.:enabled" />
</li>
<li>
<BooleanSetting path=":pleroma.:http_security.:sts" />
</li>
<li>
<IntegerSetting path=":pleroma.:http_security.:sts_max_age" />
</li>
<li>
<IntegerSetting path=":pleroma.:http_security.:ct_max_age" />
</li>
<li>
<StringSetting path=":pleroma.:http_security.:referrer_policy" />
</li>
<li>
<BooleanSetting path=":pleroma.:http_security.:allow_unsafe_eval" />
</li>
<li>
<StringSetting path=":pleroma.:http_security.:report_url" />
</li>
</ul>
<h3>{{ $t('admin_dash.http.web_push') }}</h3>
<p>{{ $t('admin_dash.http.web_push_description') }}</p>
<ul class="setting-list">
<li>
<StringSetting
path=":web_push_encryption.:vapid_details.:subject"
/>
</li>
<li>
<StringSetting
path=":web_push_encryption.:vapid_details.:public_key"
/>
</li>
<li>
<StringSetting
path=":web_push_encryption.:vapid_details.:private_key"
/>
</li>
</ul>
<!-- CONFIRM admin_token should go there but something is wrong with both data and description. -->
<!-- given the nature of the setting it's probably better to not expose it and deprecate it on backend side -->
</div>
</div>
</template>
<!--<style lang="scss" src="./http_tab.scss"></style>-->
<script src="./http_tab.js"></script>

View file

@ -2,18 +2,15 @@ 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 ColorSetting from '../helpers/color_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import ListSetting from '../helpers/list_setting.vue'
import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
import MapSetting from '../helpers/map_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
)
import { get } from 'lodash'
const InstanceTab = {
provide () {
@ -27,11 +24,29 @@ const InstanceTab = {
ChoiceSetting,
IntegerSetting,
StringSetting,
ColorSetting,
AttachmentSetting,
ListSetting,
PWAManifestIconsSetting,
MapSetting,
GroupSetting
},
computed: {
...SharedComputedObject()
...SharedComputedObject(),
providersOptions () {
const desc = get(this.$store.state.adminSettings.descriptions, [':pleroma', 'Pleroma.Web.Metadata', ':providers'])
return new Set(desc.suggestions.map(option => ({
label: option.replace('Pleroma.Web.Metadata.Providers.', ''),
value: option
})))
},
limitLocalContentOptions () {
const desc = get(this.$store.state.adminSettings.descriptions, [':pleroma', ':instance', ':limit_to_local_content'])
return new Set(desc.suggestions.map(option => ({
label: option !== 'false' ? this.$t('admin_dash.instance.' + option) : this.$t('general.no'),
value: option
})))
}
}
}

View file

@ -1,17 +1,13 @@
<template>
<div :label="$t('admin_dash.tabs.instance')">
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.instance') }}</h2>
<div class="setting-section">
<h3>{{ $t('admin_dash.instance.instance') }}</h3>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:instance.:name" />
</li>
<!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 -->
<li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined">
<AttachmentSetting
compact
path=":pleroma.:instance.:favicon"
/>
<li>
<StringSetting path=":pleroma.:instance.:contact_username" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:email" />
@ -22,87 +18,68 @@
<li>
<StringSetting path=":pleroma.:instance.:short_description" />
</li>
<li>
<ListSetting
force-new
ignore-suggestions
path=":pleroma.:instance.:languages"
/>
</li>
<li>
<StringSetting path=":pleroma.:instance.:status_page" />
</li>
</ul>
<h3>{{ $t('admin_dash.instance.branding') }}</h3>
<ul class="setting-list">
<!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 -->
<li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined">
<AttachmentSetting
compact
path=":pleroma.:instance.:favicon"
/>
</li>
<li>
<AttachmentSetting
compact
path=":pleroma.:instance.:instance_thumbnail"
/>
</li>
<h4>{{ $t('admin_dash.instance.pwa.manifest') }}</H4>
<li>
<PWAManifestIconsSetting path=":pleroma.:manifest.:icons" />
</li>
<li>
<ColorSetting hide-draft-buttons path=":pleroma.:manifest.:theme_color" />
</li>
<li>
<ColorSetting hide-draft-buttons path=":pleroma.:manifest.:background_color" />
</li>
<li>
<GroupSetting path=":pleroma.:manifest" />
</li>
<h4>{{ $t('admin_dash.instance.misc_brand') }}</H4>
<li>
<AttachmentSetting path=":pleroma.:instance.:background_image" />
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.registrations') }}</h2>
<h3>{{ $t('admin_dash.instance.rich_metadata') }}</h3>
<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>
<ListSetting
override-available-options
:options="providersOptions"
:path="[':pleroma','Pleroma.Web.Metadata', ':providers']"
/>
</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>
<BooleanSetting
:path="[':pleroma','Pleroma.Web.Metadata', ':unfurl_nsfw']"
/>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.access') }}</h2>
<div class="setting-section">
<h3>{{ $t('admin_dash.instance.access') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
@ -115,6 +92,8 @@
<ChoiceSetting
override-backend-description
override-backend-description-label
override-available-options
:options="limitLocalContentOptions"
path=":pleroma.:instance.:limit_to_local_content"
/>
</li>

View file

@ -0,0 +1,34 @@
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 TupleSetting from '../helpers/tuple_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import ListSetting from '../helpers/list_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const JobQueuesTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
TupleSetting,
AttachmentSetting,
GroupSetting,
ListSetting
},
computed: {
...SharedComputedObject()
}
}
export default JobQueuesTab

View file

@ -0,0 +1,157 @@
<template>
<div :label="$t('admin_dash.tabs.job_queues')">
<div class="setting-section">
<h3>{{ $t('admin_dash.job_queues.Gun.title') }}</h3>
<ul class="setting-list">
<li>
<h4>{{ $t('admin_dash.job_queues.Gun.connections_pools') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting path=":pleroma.:connections_pool.:connect_timeout" />
</li>
<li>
<IntegerSetting path=":pleroma.:connections_pool.:connection_acquisition_retries" />
</li>
<li>
<IntegerSetting path=":pleroma.:connections_pool.:connection_acquisition_wait" />
</li>
<li>
<!-- CONFIRM what is this -->
<IntegerSetting path=":pleroma.:connections_pool.:retry" />
</li>
<li>
<IntegerSetting path=":pleroma.:connections_pool.:max_connections" />
</li>
<li>
<!-- CONFIRM what is this -->
<IntegerSetting path=":pleroma.:connections_pool.:max_idle_time" />
</li>
<li>
<IntegerSetting path=":pleroma.:connections_pool.:reclaim_multiplier" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.job_queues.Gun.pools.title') }}</h4>
<ul class="setting-list">
<li>
<h5>{{ $t('admin_dash.job_queues.Gun.pools.default') }}</h5>
<ul class="setting-list suboptions">
<li>
<IntegerSetting path=":pleroma.:pools.:default.:size" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:default.:max_waiting" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:default.:recv_timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:pools.:default" />
</li>
<li>
<h5>{{ $t('admin_dash.job_queues.Gun.pools.federation') }}</h5>
<ul class="setting-list suboptions">
<li>
<IntegerSetting path=":pleroma.:pools.:federation.:size" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:federation.:max_waiting" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:federation.:recv_timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:pools.:federation" />
</li>
<li>
<!-- CONFIRM what is this? -->
<h5>{{ $t('admin_dash.job_queues.Gun.pools.rich_media') }}</h5>
<ul class="setting-list suboptions">
<li>
<IntegerSetting path=":pleroma.:pools.:rich_media.:size" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:rich_media.:max_waiting" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:rich_media.:recv_timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:pools.:rich_media" />
</li>
<li>
<h5>{{ $t('admin_dash.job_queues.Gun.pools.media') }}</h5>
<ul class="setting-list suboptions">
<li>
<IntegerSetting path=":pleroma.:pools.:media.:size" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:media.:max_waiting" />
</li>
<li>
<IntegerSetting path=":pleroma.:pools.:media.:recv_timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:pools.:media" />
</li>
</ul>
</li>
</ul>
<h3>{{ $t('admin_dash.job_queues.Hackney.title') }}</h3>
<ul class="setting-list">
<li>
<h4>{{ $t('admin_dash.job_queues.Hackney.federation') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:federation.:max_connections" />
</li>
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:federation.:timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:hackney_pools.:federation" />
</li>
<li>
<h4>{{ $t('admin_dash.job_queues.Hackney.media') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:media.:max_connections" />
</li>
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:media.:timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:hackney_pools.:media" />
</li>
<li>
<!-- CONFIRM what is this -->
<h4>{{ $t('admin_dash.job_queues.Hackney.rich_media') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:rich_media.:max_connections" />
</li>
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:rich_media.:timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:hackney_pools.:rich_media" />
</li>
<li>
<h4>{{ $t('admin_dash.job_queues.Hackney.upload') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:upload.:max_connections" />
</li>
<li>
<IntegerSetting path=":pleroma.:hackney_pools.:upload.:timeout" />
</li>
</ul>
<GroupSetting path=":pleroma.:hackney_pools.:upload" />
</li>
</ul>
</div>
</div>
</template>
<script src="./job_queues_tab.js"></script>

View file

@ -4,14 +4,6 @@ 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 = {
components: {

View file

@ -1,10 +1,10 @@
<template>
<div :label="$t('admin_dash.tabs.limits')">
<div class="setting-item">
<h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
<div class="setting-section">
<h3>{{ $t('admin_dash.limits.arbitrary_limits') }}</h3>
<ul class="setting-list">
<li>
<h3>{{ $t('admin_dash.limits.posts') }}</h3>
<h4>{{ $t('admin_dash.limits.posts') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
@ -24,7 +24,7 @@
</ul>
</li>
<li>
<h3>{{ $t('admin_dash.limits.uploads') }}</h3>
<h4>{{ $t('admin_dash.limits.uploads') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
@ -50,7 +50,7 @@
</ul>
</li>
<li>
<h3>{{ $t('admin_dash.limits.users') }}</h3>
<h4>{{ $t('admin_dash.limits.users') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
@ -74,7 +74,7 @@
/>
</li>
<li>
<h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
<h5>{{ $t('admin_dash.limits.profile_fields') }}</h5>
<ul class="setting-list">
<li>
<IntegerSetting
@ -108,7 +108,7 @@
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
<h5>{{ $t('admin_dash.limits.user_uploads') }}</h5>
<ul class="setting-list">
<li>
<IntegerSetting
@ -128,6 +128,25 @@
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.limits.other') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_report_comment_size"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_endorsed_users"
draft-mode
/>
</li>
</ul>
</li>
</ul>
</div>
</div>

View file

@ -0,0 +1,109 @@
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 ListSetting from '../helpers/list_setting.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { get } from 'lodash'
const LinksTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
GroupSetting,
ListSetting,
Checkbox
},
computed: {
classIsPresent () {
return this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][':class'] !== false
},
relIsPresent () {
return this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][':rel'] !== false
},
truncateIsPresent () {
return this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][':truncate'] !== false
},
truncateDescription () {
return get(this.$store.state.adminSettings.descriptions, [':pleroma', 'Pleroma.Formatter', ':truncate'])
},
ttlSettersOptions () {
const desc = get(this.$store.state.adminSettings.descriptions, ':pleroma.:rich_media.:ttl_setters')
return new Set(desc.suggestions.map(option => ({
label: option.replace('Pleroma.Web.RichMedia.Parser.TTL.', ''),
value: option
})))
},
parsersOptions () {
const desc = get(this.$store.state.adminSettings.descriptions, ':pleroma.:rich_media.:parsers')
return new Set(desc.suggestions.map(option => ({
label: option.replace('Pleroma.Web.RichMedia.Parsers.', ''),
value: option
})))
},
validateTLDOptions () {
return [{
label: this.$t('general.yes'),
value: true
}, {
label: this.$t('general.no'),
value: false
}, {
label: this.$t('admin_dash.links.no_scheme'),
value: ':no_scheme'
}]
},
mediaProxyEnabled () {
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][':enabled']
},
mediaInvalidationProvider () {
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][':invalidation'][':provider']
},
...SharedComputedObject()
},
methods: {
checkRel (e) {
this.$store.commit(
'updateAdminDraft',
{
path: [':pleroma','Pleroma.Formatter',':rel'],
value: e ? '' : false
}
)
},
checkClass (e) {
this.$store.commit(
'updateAdminDraft',
{
path: [':pleroma','Pleroma.Formatter',':class'],
value: e ? '' : false
}
)
},
checkTruncate (e) {
this.$store.commit(
'updateAdminDraft',
{
path: [':pleroma','Pleroma.Formatter',':truncate'],
value: e ? 20 : false
}
)
}
}
}
export default LinksTab

View file

@ -0,0 +1,45 @@
<template>
<div
class="LinksTab"
:label="$t('admin_dash.tabs.media_proxy')"
>
<div class="setting-section">
<h3>{{ $t('admin_dash.links.link_previews') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:rich_media.:enabled" />
</li>
<li>
<ListSetting
override-available-options
:options="parsersOptions"
path=":pleroma.:rich_media.:parsers"
/>
</li>
<li>
<IntegerSetting
path=":pleroma.:rich_media.:timeout"
/>
</li>
<li>
<ListSetting
override-available-options
:options="ttlSettersOptions"
path=":pleroma.:rich_media.:ttl_setters"
/>
</li>
<li>
<ListSetting
path=":pleroma.:rich_media.:ignore_tld"
ignore-suggestions
/>
</li>
<li>
<ListSetting path=":pleroma.:rich_media.:ignore_hosts" />
</li>
</ul>
</div>
</div>
</template>
<script src="./links_tab.js"></script>

View file

@ -0,0 +1,65 @@
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 ColorSetting from '../helpers/color_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const MailerTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
ColorSetting,
GroupSetting
},
computed: {
adaptersLabels () {
const prefix = 'Swoosh.Adapters.'
const descriptions = this.$store.state.adminSettings.descriptions
const options = descriptions[':pleroma']['Pleroma.Emails.Mailer'][':adapter'].suggestions
return Object.fromEntries(options.map(value => [
value, value.replace(prefix, '')
]))
},
startTLSLabels () {
return {
':always': this.$t('admin_dash.generic_enforcement.always'),
':if_available': this.$t('admin_dash.generic_enforcement.if_available'),
':never': this.$t('admin_dash.generic_enforcement.never')
}
// return Object.fromEntries(options.map(value => [
// value, value.replace(prefix, '')
// ]))
},
adapter () {
return this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Emails.Mailer'][':adapter']
},
mailerEnabled () {
return this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Emails.Mailer'][':enabled']
},
...SharedComputedObject()
},
methods: {
adapterHasKey (key) {
const descriptions = this.$store.state.adminSettings.descriptions
const mailerStuff = descriptions[':pleroma']['Pleroma.Emails.Mailer']
const adapterStuff = mailerStuff[':subgroup,' + this.adapter]
return Object.prototype.hasOwnProperty.call(adapterStuff, key)
}
}
}
export default MailerTab

View file

@ -0,0 +1,179 @@
<template>
<div :label="$t('admin_dash.tabs.mailer')">
<div class="setting-section">
<h3>{{ $t('admin_dash.mailer.styling') }}</h3>
<ul class="setting-list">
<h4>{{ $t('admin_dash.mailer.assets') }}</h4>
<li>
<StringSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':logo']" />
</li>
<h4>{{ $t('admin_dash.mailer.colors') }}</h4>
<li>
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':background_color']" />
</li>
<li>
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':content_background_color']" />
</li>
<li>
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':header_color']" />
</li>
<li>
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':text_color']" />
</li>
<li>
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':link_color']" />
</li>
<li>
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':text_muted_color']" />
</li>
<li>
<GroupSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling']" />
</li>
</ul>
<h3>{{ $t('admin_dash.mailer.adapter') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting :path="[':pleroma','Pleroma.Emails.Mailer',':enabled']" />
</li>
<template v-if="mailerEnabled">
<li>
<ChoiceSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':adapter']"
:option-label-map="adaptersLabels"
/>
<h4>{{ $t('admin_dash.mailer.auth') }}</h4>
<ul class="setting-list suboptions">
<li v-if="adapterHasKey(':api_key')">
<!-- authentication info -->
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':api_key']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':access_key')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':access_key']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':access_token')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':access_token']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':username')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':username']"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':password')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':password']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':secret')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':secret']"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':auth')">
<ChoiceSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':auth']"
:password="true"
:subgroup="adapter"
/>
</li>
<!-- server info -->
<li v-if="adapterHasKey(':relay')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':relay']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':ssl')">
<BooleanSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':ssl']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':tls')">
<ChoiceSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':tls']"
:option-label-map="startTLSLabels"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':port')">
<IntegerSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':port']"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':server_id')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':server_id']"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':region')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':region']"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':domain')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':domain']"
:subgroup="adapter"
/>
</li>
<!-- sendmail exclusive -->
<li v-if="adapterHasKey(':cmd_path')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':cmd_path']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':cmd_args')">
<StringSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':cmd_args']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':qmail')">
<BooleanSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':qmail']"
:password="true"
:subgroup="adapter"
/>
</li>
<li v-if="adapterHasKey(':retries')">
<IntegerSetting
:path="[':pleroma','Pleroma.Emails.Mailer',':retries']"
:subgroup="adapter"
/>
</li>
</ul>
</li>
</template>
</ul>
</div>
</div>
</template>
<script src="./mailer_tab.js"></script>

View file

@ -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 ListSetting from '../helpers/list_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const MediaProxyTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
GroupSetting,
ListSetting
},
computed: {
mediaProxyEnabled () {
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][':enabled']
},
mediaInvalidationProvider () {
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][':invalidation'][':provider']
},
...SharedComputedObject()
}
}
export default MediaProxyTab

View file

@ -0,0 +1,135 @@
<template>
<div :label="$t('admin_dash.tabs.media_proxy')">
<div class="setting-section">
<h3>{{ $t('admin_dash.media_proxy.basic') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:media_proxy.:enabled" />
<ul
v-if="mediaProxyEnabled"
class="setting-list suboptions"
>
<li>
<StringSetting path=":pleroma.:media_proxy.:base_url" />
</li>
<li>
<BooleanSetting path=":pleroma.:media_proxy.:proxy_opts.:redirect_on_failure" />
</li>
<li>
<ListSetting
ignore-suggestions
path=":pleroma.:media_proxy.:whitelist"
/>
</li>
</ul>
</li>
</ul>
<template v-if="mediaProxyEnabled">
<h3>{{ $t('admin_dash.media_proxy.invalidation') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:media_proxy.:invalidation.:enabled" />
<ul class="setting-list suboptions">
<li>
<ChoiceSetting
path=":pleroma.:media_proxy.:invalidation.:provider"
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
/>
</li>
<h4>{{ $t('admin_dash.media_proxy.invalidation_settings') }}</h4>
<ul class="setting-list suboptions">
<template v-if="mediaInvalidationProvider === 'Pleroma.Web.MediaProxy.Invalidation.Http'">
<li>
<StringSetting
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Http', ':method']"
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
/>
</li>
<li>
<ListSetting
ignore-suggestions
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Http', ':headers']"
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
/>
</li>
<li>
<ListSetting
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Http', ':options']"
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
/>
</li>
</template>
<template v-if="mediaInvalidationProvider === 'Pleroma.Web.MediaProxy.Invalidation.Script'">
<!-- TODO: you know the drill by now - list component -->
<li>
<StringSetting
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Script', ':script_path']"
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
/>
</li>
<li>
<StringSetting
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Script', ':url_format']"
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
/>
</li>
</template>
</ul>
</ul>
</li>
</ul>
<h3>{{ $t('admin_dash.media_proxy.limits') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
path=":pleroma.:media_proxy.:proxy_opts.:max_body_length"
/>
</li>
<li>
<IntegerSetting
path=":pleroma.:media_proxy.:proxy_opts.:max_read_duration"
/>
</li>
<li>
<GroupSetting path=":pleroma.:media_proxy.:proxy_opts" />
</li>
</ul>
<!-- TODO: add whitelist when we have list component (hehe) -->
<h3>{{ $t('admin_dash.media_proxy.thumbnails') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:media_preview_proxy.:enabled" />
<ul class="setting-list suboptions">
<li>
<IntegerSetting
parent-path=":pleroma.:media_preview_proxy.:enabled"
path=":pleroma.:media_preview_proxy.:image_quality"
/>
</li>
<li>
<IntegerSetting
parent-path=":pleroma.:media_preview_proxy.:enabled"
path=":pleroma.:media_preview_proxy.:min_content_length"
/>
</li>
<li>
<IntegerSetting
parent-path=":pleroma.:media_preview_proxy.:enabled"
path=":pleroma.:media_preview_proxy.:thumbnail_max_width"
/>
</li>
<li>
<IntegerSetting
parent-path=":pleroma.:media_preview_proxy.:enabled"
path=":pleroma.:media_preview_proxy.:thumbnail_max_height"
/>
</li>
</ul>
</li>
</ul>
</template>
</div>
</div>
</template>
<script src="./media_proxy_tab.js"></script>

View file

@ -0,0 +1,42 @@
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 ListSetting from '../helpers/list_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 MonitoringTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
GroupSetting,
ListSetting
},
computed: {
...SharedComputedObject()
},
methods: {
}
}
export default MonitoringTab

View file

@ -0,0 +1,47 @@
<template>
<div :label="$t('admin_dash.tabs.monitoring')">
<div class="setting-section">
<h3>{{ $t('admin_dash.monitoring.builtins') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting :path="[':pleroma','Pleroma.Emails.NewUsersDigestEmail',':enabled']" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:healthcheck" />
</li>
</ul>
<h3>{{ $t('admin_dash.monitoring.prometheus') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting :path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']" />
</li>
<li>
<BooleanSetting
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':auth']"
/>
</li>
<li>
<StringSetting
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':path']"
/>
</li>
<li>
<ChoiceSetting
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':format']"
/>
</li>
<li>
<ListSetting
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':ip_whitelist']"
/>
</li>
</ul>
</div>
</div>
</template>
<script src="./monitoring_tab.js"></script>

View file

@ -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 ColorSetting from '../helpers/color_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import ListSetting from '../helpers/list_setting.vue'
import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
import MapSetting from '../helpers/map_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const OtherTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
ColorSetting,
AttachmentSetting,
ListSetting,
PWAManifestIconsSetting,
MapSetting,
GroupSetting
},
computed: {
...SharedComputedObject()
}
}
export default OtherTab

View file

@ -0,0 +1,52 @@
<template>
<div :label="$t('admin_dash.tabs.other')">
<div class="setting-section">
<h3>{{ $t('admin_dash.other.uncategorized') }}</h3>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:instance.:static_dir" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:profile_directory" />
</li>
</ul>
<h3>{{ $t('admin_dash.other.reports') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:instance.:report_strip_status" />
</li>
</ul>
<h3>{{ $t('admin_dash.other.user_backup') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':purge_after_days']" />
</li>
<li>
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':limit_days']" />
</li>
<!-- CONFIRM what is this?
<li>
<StringSetting :path="[':pleroma','Pleroma.User.Backup',':dir']" />
</li>
-->
<li>
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':process_chunk_size']" />
</li>
<li>
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':timeout']" />
</li>
</ul>
<h3>{{ $t('admin_dash.other.privileges') }}</h3>
<ul class="setting-list">
<li>
<ListSetting path=":pleroma.:instance.:admin_privileges" />
</li>
<li>
<ListSetting path=":pleroma.:instance.:moderator_privileges" />
</li>
</ul>
</div>
</div>
</template>
<script src="./other_tab.js"></script>

View file

@ -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 ColorSetting from '../helpers/color_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import ListSetting from '../helpers/list_setting.vue'
import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
import MapSetting from '../helpers/map_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const PostsTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
ColorSetting,
AttachmentSetting,
ListSetting,
PWAManifestIconsSetting,
MapSetting,
GroupSetting
},
computed: {
...SharedComputedObject()
}
}
export default PostsTab

View file

@ -0,0 +1,29 @@
<template>
<div :label="$t('admin_dash.tabs.posts')">
<div class="setting-section">
<h3>{{ $t('admin_dash.posts.global') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:instances_favicons.:enabled" />
</li>
</ul>
<h3>{{ $t('admin_dash.posts.local') }}</h3>
<ul class="setting-list">
<li>
<ListSetting path=":pleroma.:instance.:allowed_post_formats" />
</li>
</ul>
<h3>{{ $t('admin_dash.posts.remote') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting path=":pleroma.:instance.:remote_post_retention_days" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:skip_thread_containment" />
</li>
</ul>
</div>
</div>
</template>
<script src="./posts_tab.js"></script>

View file

@ -0,0 +1,20 @@
import RateSetting from '../helpers/rate_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const RatesTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
RateSetting,
},
computed: {
...SharedComputedObject()
}
}
export default RatesTab

View file

@ -0,0 +1,41 @@
<template>
<div :label="$t('admin_dash.tabs.instance')">
<div class="setting-section">
<h3>{{ $t('admin_dash.rate_limit.account_confirmation_resend') }}</h3>
<ul class="setting-list">
<li>
<RateSetting path=":pleroma.:rate_limit.:account_confirmation_resend" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:ap_routes" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:app_account_creation" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:authentication" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:oauth_app_creation" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:relation_id_action" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:search" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:status_id_action" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:statuses_actions" />
</li>
<li>
<RateSetting path=":pleroma.:rate_limit.:timeline" />
</li>
</ul>
</div>
</div>
</template>
<script src="./rates_tab.js"></script>

View file

@ -0,0 +1,34 @@
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 TupleSetting from '../helpers/tuple_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import ListSetting from '../helpers/list_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const RegistrationsTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
TupleSetting,
AttachmentSetting,
GroupSetting,
ListSetting
},
computed: {
...SharedComputedObject()
}
}
export default RegistrationsTab

View file

@ -0,0 +1,182 @@
<template>
<div :label="$t('admin_dash.tabs.instance')">
<div class="setting-section">
<h3>{{ $t('admin_dash.instance.registrations') }}</h3>
<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>
<h4>{{ $t('admin_dash.instance.captcha_header') }}</h4>
<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'"
>
<h5>{{ $t('admin_dash.instance.kocaptcha') }}</h5>
<ul class="setting-list">
<li>
<StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>{{ $t('admin_dash.registrations.autofollow') }}</h3>
<ul class="setting-list">
<li>
<ListSetting
path=":pleroma.:instance.:autofollowed_nicknames"
/>
</li>
<li>
<ListSetting
path=":pleroma.:instance.:autofollowing_nicknames"
/>
</li>
</ul>
<h3>{{ $t('admin_dash.registrations.welcome.title') }}</h3>
<ul class="setting-list">
<p>{{ $t('admin_dash.registrations.welcome.description') }}</p>
<li>
<h4>{{ $t('admin_dash.registrations.welcome.direct_message') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:welcome.:direct_message.:enabled"
/>
<ul class="setting-list suboptions">
<li>
<StringSetting
path=":pleroma.:welcome.:direct_message.:sender_nickname"
parent-path=":pleroma.:welcome.:direct_message.:enabled"
/>
</li>
<li>
<StringSetting
path=":pleroma.:welcome.:direct_message.:message"
parent-path=":pleroma.:welcome.:direct_message.:enabled"
/>
</li>
</ul>
<GroupSetting path=":pleroma.:welcome.:direct_message" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.registrations.welcome.chat_message') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:welcome.:chat_message.:enabled"
/>
<ul class="setting-list suboptions">
<li>
<StringSetting
tuple
path=":pleroma.:welcome.:chat_message.:sender_nickname"
parent-path=":pleroma.:welcome.:chat_message.:enabled"
/>
</li>
<li>
<StringSetting
path=":pleroma.:welcome.:chat_message.:message"
parent-path=":pleroma.:welcome.:chat_message.:enabled"
/>
</li>
</ul>
<GroupSetting path=":pleroma.:welcome.:chat_message" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.registrations.welcome.email_message') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:welcome.:email.:enabled"
/>
<ul class="setting-list suboptions">
<li>
<TupleSetting
path=":pleroma.:welcome.:email.:sender"
parent-path=":pleroma.:welcome.:email.:enabled"
/>
</li>
<li>
<StringSetting
path=":pleroma.:welcome.:email.:subject"
parent-path=":pleroma.:welcome.:email.:enabled"
/>
</li>
<li>
<StringSetting
path=":pleroma.:welcome.:email.:html"
parent-path=":pleroma.:welcome.:email.:enabled"
/>
</li>
</ul>
<GroupSetting path=":pleroma.:welcome.:email" />
</li>
</ul>
</li>
</ul>
<h3>{{ $t('admin_dash.registrations.restrictions') }}</h3>
<ul class="setting-list">
<li>
<ListSetting
ignore-suggestions
:path="[':pleroma', 'Pleroma.User', ':email_blacklist']"
/>
</li>
</ul>
</div>
</div>
</template>
<script src="./registrations_tab.js"></script>

View file

@ -0,0 +1,46 @@
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'
const UploadsTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
data () {
return {
uploaders: [{
key: 'Pleroma.Uploaders.Local',
value: 'Pleroma.Uploaders.Local',
label: this.$t('admin_dash.uploads.local_uploader')
}, {
key: 'Pleroma.Uploaders.IPFS',
value: 'Pleroma.Uploaders.IPFS',
label: 'IPFS'
}, {
key: 'Pleroma.Uploaders.S3',
value: 'Pleroma.Uploaders.S3',
label: 'S3'
}]
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting
},
computed: {
uploader () {
return this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Upload'][':uploader']
},
...SharedComputedObject()
}
}
export default UploadsTab

View file

@ -0,0 +1,110 @@
<template>
<div :label="$t('admin_dash.tabs.uploads')">
<div class="setting-section">
<h3>{{ $t('admin_dash.uploads.upload') }}</h3>
<ul class="setting-list">
<li>
<ChoiceSetting
:path="[':pleroma','Pleroma.Upload',':uploader']"
:options="uploaders"
/>
<h4>{{ $t('admin_dash.uploads.uploader_settings') }}</h4>
<ul class="setting-list suboptions">
<template v-if="uploader === 'Pleroma.Uploaders.Local'">
<li>
<StringSetting
:path="[':pleroma','Pleroma.Uploaders.Local',':uploads']"
/>
</li>
</template>
<template v-else-if="uploader === 'Pleroma.Uploaders.IPFS'">
<li>
<StringSetting
:path="[':pleroma','Pleroma.Uploaders.IPFS',':get_gateway_url']"
/>
</li>
<li>
<StringSetting
:path="[':pleroma','Pleroma.Uploaders.IPFS',':post_gateway_url']"
/>
</li>
</template>
<template v-else-if="uploader === 'Pleroma.Uploaders.S3'">
<li>
<StringSetting
:path="[':pleroma','Pleroma.Uploaders.S3',':bucket']"
/>
</li>
<li>
<StringSetting
:path="[':pleroma','Pleroma.Uploaders.S3',':bucket_namespace']"
/>
</li>
<li>
<BooleanSetting
:path="[':pleroma','Pleroma.Uploaders.S3',':streaming_enabled']"
/>
</li>
<li>
<StringSetting
:path="[':pleroma','Pleroma.Uploaders.S3',':truncated_namespace']"
/>
</li>
</template>
<li>
<IntegerSetting
:path="[':pleroma','Pleroma.Uploaders.Uploader',':timeout']"
/>
</li>
</ul>
</li>
</ul>
<h3>{{ $t('admin_dash.uploads.attachments') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:instance.:attachment_links"
:options="uploaders"
/>
</li>
<li>
<BooleanSetting
path=":pleroma.:instance.:cleanup_attachments"
:options="uploaders"
/>
</li>
</ul>
<!-- CONFIRM how filters work -->
<h3>{{ $t('admin_dash.uploads.filenames') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
:path="[':pleroma','Pleroma.Upload',':link_name']"
:subgroup="adapter"
/>
</li>
<li>
<IntegerSetting
:path="[':pleroma','Pleroma.Upload',':filename_display_max_length']"
:subgroup="adapter"
/>
</li>
<li>
<StringSetting
:path="[':pleroma','Pleroma.Upload',':default_description']"
:subgroup="adapter"
/>
</li>
<li>
<StringSetting
:path="[':pleroma','Pleroma.Upload',':base_url']"
:subgroup="adapter"
/>
</li>
<!-- TODO: add mime-type when we have a dynamic list component -->
</ul>
</div>
</div>
</template>
<script src="./uploads_tab.js"></script>

View file

@ -1,10 +1,11 @@
<template>
<span
v-if="matchesExpertLevel"
class="AttachmentSetting"
class="AttachmentSetting setting-item"
:class="{ '-compact': compact }"
>
<label
class="setting-label"
:for="path"
:class="{ 'faint': shouldBeDisabled }"
>

View file

@ -1,9 +1,10 @@
<template>
<label
v-if="matchesExpertLevel"
class="BooleanSetting"
class="BooleanSetting setting-item"
>
<Checkbox
class="setting-control setting-label"
:model-value="visibleState"
:disabled="shouldBeDisabled"
:indeterminate="isIndeterminate"
@ -13,6 +14,12 @@
class="label"
:class="{ 'faint': shouldBeDisabled }"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
{{ ' ' }}
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel }}
</template>
@ -22,19 +29,16 @@
<slot v-else />
</span>
</Checkbox>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
v-if="backendDescriptionDescription || showDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
<slot name="description">
{{ backendDescriptionDescription + ' ' }}
</slot>
</p>
<DraftButtons />
</label>
</template>

View file

@ -9,6 +9,10 @@ export default {
},
props: {
...Setting.props,
overrideOptions: {
type: Boolean,
required: false
},
options: {
type: Array,
required: false
@ -22,7 +26,16 @@ export default {
computed: {
...Setting.computed,
realOptions () {
if (this.overrideOptions) {
return this.options
}
if (this.realSource === 'admin') {
if (
!this.backendDescriptionSuggestions?.length ||
this.backendDescriptionSuggestions?.length === 0
) {
return this.options
}
return this.backendDescriptionSuggestions.map(x => ({
key: x,
value: x,

View file

@ -1,18 +1,27 @@
<template>
<label
v-if="matchesExpertLevel"
class="ChoiceSetting"
class="ChoiceSetting setting-item"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel }}
</template>
<template v-else>
<slot />
</template>
{{ ' ' }}
<span class="setting-label">
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
{{ ' ' }}
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel }}
</template>
<template v-else>
<slot />
</template>
</span>
<Select
:model-value="realDraftMode ? draft :state"
:disabled="disabled"
class="setting-control"
:model-value="realDraftMode ? draft : state"
:disabled="shouldBeDisabled"
@update:model-value="update"
>
<option
@ -24,11 +33,6 @@
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
</option>
</Select>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
@ -40,3 +44,16 @@
</template>
<script src="./choice_setting.js"></script>
<style lang="scss">
.ChoiceSetting.setting-item {
.-mobile & {
display: block;
.setting-label {
display: block;
margin-bottom: 0.5em
}
}
}
</style>

View file

@ -0,0 +1,16 @@
import Setting from './setting.js'
import ColorInput from 'src/components/color_input/color_input.vue'
export default {
...Setting,
components: {
...Setting.components,
ColorInput
},
methods: {
...Setting.methods,
getValue (e) {
return e
}
}
}

View file

@ -0,0 +1,71 @@
<template>
<label
v-if="matchesExpertLevel"
class="ColorSetting setting-item"
>
<label
v-if="!hideLabel"
:for="path"
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
{{ ' ' }}
<ColorInput
:id="path"
class="setting-control color-setting-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions"
:model-value="realDraftMode ? draft : state"
@update:model-value="update"
/>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons v-if="!hideDraftButtons" />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label>
</template>
<script src="./color_setting.js"></script>
<style lang="scss">
.ColorSetting {
&.setting-item {
display: grid;
grid-template-areas:
"label control"
". desc"
". draft";
.setting-label {
text-align: right;
align-self: center;
}
.setting-control {
align-self: end;
}
}
.color-setting-input {
align-self: baseline;
}
}
</style>

View file

@ -2,6 +2,7 @@
<!-- TODO make it reusable -->
<template>
<span
v-if="$parent.isDirty || $parent.canHardReset"
class="DraftButtons"
>
<Popover
@ -72,12 +73,10 @@ export default {
<style lang="scss">
.DraftButtons {
display: inline-block;
display: inline-flex;
position: relative;
.button-default {
margin-left: 0.5em;
}
gap: 0.5em;
margin-top: 0.5em
}
.draft-tooltip {

View file

@ -1,13 +1,5 @@
import { isEqual } from 'lodash'
import Setting from './setting.js'
export default {
...Setting,
computed: {
...Setting.computed,
isDirty () {
return !isEqual(this.state, this.draft)
}
}
}

View file

@ -1,7 +1,7 @@
<template>
<span
v-if="matchesExpertLevel"
class="GroupSetting"
class="GroupSetting setting-item"
>
<ModifiedIndicator
:changed="isChanged"

View file

@ -0,0 +1,133 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Setting from './setting.js'
export default {
...Setting,
data () {
return {
newValue: '',
}
},
components: {
...Setting.components,
Checkbox
},
props: {
...Setting.props,
ignoreSuggestions: {
required: false,
type: Boolean
},
overrideAvailableOptions: {
required: false,
type: Boolean
},
options: {
required: false,
type: Set
},
allowNew: {
required: false,
type: Boolean,
default: true
},
forceNew: {
required: false,
type: Boolean,
default: false
}
},
computed: {
...Setting.computed,
showNew () {
if (this.forceNew) return true
if (!this.allowNew) return false
const isExpert = this.$store.state.config.expertLevel > 0
const hasBuiltins = this.builtinEntries.length > 0
if (hasBuiltins) {
return isExpert
} else {
return true
}
},
valueSet () {
return new Set(this.visibleState)
},
suggestionsSet () {
const suggestions = this.backendDescriptionSuggestions
if (suggestions) {
return new Set(suggestions)
} else {
return new Set()
}
},
extraEntries () {
if (this.ignoreSuggestions) return [...this.valueSet.values()]
if (!this.suggestionsSet) return []
return [...this.valueSet.values()].filter((x) => {
return !this.builtinEntriesValueSet.has(x)
})
},
builtinEntries () {
if (this.ignoreSuggestions) return []
if (this.overrideAvailableOptions) {
return [...this.options]
}
if (!this.suggestionsSet) return []
const builtins = [...this.suggestionsSet.values()]
return builtins.map((option) => ({
label: option,
value: option
}))
},
builtinEntriesValueSet () {
return new Set(this.builtinEntries.map(x => x.value))
}
},
methods: {
...Setting.methods,
optionPresent (option) {
return this.valueSet.has(option)
},
getValue ({ event, value, index, eventType }) {
switch (eventType) {
case 'toggle': {
this.newValue = ''
const newSet = new Set(this.valueSet.values())
if (event) {
newSet.add(value)
} else {
newSet.delete(value)
}
return [...newSet.values()]
}
case 'add': {
if (!this.newValue) return this.valueSet.values()
const res = [...this.valueSet.values(), this.newValue]
this.newValue = ''
return res
}
case 'remove': {
const pre = [...this.valueSet.values()].slice(0, index)
const post = [...this.valueSet.values()].slice(index + 1)
return [...pre, ...post]
}
case 'edit': {
const pre = [...this.valueSet.values()].slice(0, index)
const post = [...this.valueSet.values()].slice(index + 1)
const string = event.target.value
if (!string) return [...this.valueSet.values()]
return [...pre, string, ...post]
}
}
}
}
}

View file

@ -0,0 +1,92 @@
<template>
<div
v-if="matchesExpertLevel"
class="ListSetting setting-item"
>
<label
class="setting-label"
: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>
<ul class="setting-list">
<li
v-for="(item, i) in builtinEntries"
:key="i"
>
<Checkbox
:disabled="shouldBeDisabled"
:model-value="optionPresent(item.value)"
@update:model-value="event => update({ event, value: item.value, eventType: 'toggle' })"
>
{{ item.label }}
</Checkbox>
</li>
<li
v-for="(item, index) in extraEntries"
:key="index"
>
<div class="btn-group">
<input
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:value="item"
@change="e => update({ event: e, index, eventType: 'edit' })"
>
<button
class="button-default"
@click="e => update({ index, eventType: 'remove' })"
>
<FAIcon icon="times" />
</button>
</div>
</li>
<li v-if="showNew">
<div class="btn-group">
<input
v-model="newValue"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
>
<button
class="button-default"
@click="e => update({ eventType: 'add' })"
>
<FAIcon icon="plus" />
</button>
</div>
</li>
</ul>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
</div>
</template>
<script src="./list_setting.js"></script>
<style lang="scss">
.ListSetting {
.setting-list {
.checkbox {
padding: 0.5em 0;
}
}
}
</style>

View file

@ -0,0 +1,44 @@
import ListSetting from './list_setting.js'
export default {
...ListSetting,
data () {
return {
newValue: ['','']
}
},
methods: {
...ListSetting.methods,
getValue ({ event, index, eventType, tuple }) {
switch (eventType) {
case 'add': {
if (!this.newValue[0] || !this.newValue[1]) return this.visibleState
const res = [...this.visibleState, this.newValue]
this.newValue = ['', '']
return res
}
case 'remove': {
const pre = this.visibleState.slice(0, index)
const post = this.visibleState.slice(index + 1)
return [...pre, ...post]
}
case 'edit': {
const pre = this.visibleState.slice(0, index)
const post = this.visibleState.slice(index + 1)
const item = this.visibleState[index]
const string = event.target.value
if (!string) return this.visibleState
if (tuple === 0) {
return [...pre, [string, item[1]], ...post]
} else {
return [...pre, [item[0], string], ...post]
}
}
}
}
}
}

View file

@ -0,0 +1,113 @@
<template>
<div
v-if="matchesExpertLevel"
class="ListTupleSetting"
>
<label
class="setting-label"
: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>
<ul class="setting-list">
<li
v-for="(item, index) in visibleState"
:key="index"
>
<div class="btn-group">
<input
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:value="item.tuple[0]"
@change="e => update({ event: e, index, eventType: 'edit', tuple: 0 })"
>
<input
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:value="item.tuple[1]"
@change="e => update({ event: e, index, eventType: 'edit', tuple: 1 })"
>
<button
class="button-default"
@click="e => update({ index, eventType: 'remove' })"
>
<FAIcon icon="times" />
</button>
</div>
</li>
<li>
<div class="btn-group">
<input
v-model="newValue[0]"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions[0][0]"
>
<input
v-model="newValue[1]"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions[0][1]"
>
<button
class="button-default"
@click="e => update({ eventType: 'add' })"
>
<FAIcon icon="plus" />
</button>
</div>
</li>
</ul>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
</div>
</template>
<script src="./list_tuple_setting.js"></script>
<style lang="scss">
.ListTupleSetting {
.btn-group {
display: flex
}
dl {
display: inline-grid;
grid-template-columns: auto auto;
gap: 0.5em;
align-items: baseline;
dt {
display: inline;
font-weight: 800;
&::after {
content: ':'
}
}
dd {
display: inline;
margin: 0
}
}
}
</style>

View file

@ -0,0 +1,70 @@
import Setting from './setting.js'
export default {
...Setting,
props: {
...Setting.props,
allowNew: {
required: false,
type: Boolean,
default: true
}
},
data () {
return {
newValue: ['',''] // avoiding extra complexity by just using an array instead of an object
}
},
computed: {
...Setting.computed,
// state that we'll show in the UI, i.e. transforming map into list
displayState () {
return Object.entries(this.visibleState)
}
},
methods: {
...Setting.methods,
getValue ({ event, key, eventType, isKey }) {
switch (eventType) {
case 'add': {
if (key === '') return this.visibleState
const res = {...this.visibleState, ...Object.fromEntries([this.newValue])}
this.newValue = ['', '']
return res
}
case 'remove': {
// initial state for this type is empty array
if (Array.isArray(this.visibleState)) return this.visibleState
const newEntries = Object.entries(this.visibleState).filter(([k]) => k !== key)
if (newEntries.length === 0 ) return []
return Object.fromEntries(newEntries)
}
case 'edit': {
const string = event.target.value
const newEntries = Object
.entries(this.visibleState)
.map(([k, v]) => {
if (isKey) {
if (k === key) {
return [string, v]
} else {
return [k, v]
}
} else {
if (k === key) {
return [k, string]
} else {
return [k, v]
}
}
})
return Object.fromEntries(newEntries)
}
}
}
}
}

View file

@ -0,0 +1,112 @@
<template>
<div
v-if="matchesExpertLevel"
class="MapSetting setting-item"
>
<label
class="setting-label"
: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>
<ul class="setting-list">
<li
v-for="(item, i) in displayState"
:key="i"
>
<div class="btn-group">
<input
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:value="item[0]"
@change="e => update({ event: e, key: item[0], eventType: 'edit', isKey: true })"
>
<input
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:value="item[1]"
@change="e => update({ event: e, key: item[0], eventType: 'edit', isKey: false })"
>
<button
class="button-default"
@click="e => update({ key: item[0], eventType: 'remove' })"
>
<FAIcon icon="times" />
</button>
</div>
</li>
<li v-if="allowNew">
<div class="btn-group">
<input
v-model="newValue[0]"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions[0][0]"
>
<input
v-model="newValue[1]"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions[0][1]"
>
<button
class="button-default"
@click="e => update({ eventType: 'add' })"
>
<FAIcon icon="plus" />
</button>
</div>
</li>
</ul>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
</div>
</template>
<script src="./map_setting.js"></script>
<style lang="scss">
.MapSetting {
.btn-group {
display: flex
}
dl {
display: inline-grid;
grid-template-columns: auto auto;
gap: 0.5em;
dt {
display: inline;
font-weight: 800;
&::after {
content: ':'
}
}
dd {
display: inline;
margin: 0
}
}
}
</style>

View file

@ -1,10 +1,12 @@
<template>
<span
v-if="matchesExpertLevel"
class="NumberSetting"
class="NumberSetting setting-item"
>
<label
v-if="!hideLabel"
:for="path"
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
@ -18,10 +20,11 @@
{{ ' ' }}
<input
:id="path"
class="input number-input"
class="input number-input setting-control"
type="number"
:step="step || 1"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions"
:min="min || 0"
:value="realDraftMode ? draft :state"
@change="update"
@ -32,7 +35,7 @@
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<DraftButtons v-if="!hideDraftButtons" />
<p
v-if="backendDescriptionDescription"
class="setting-description"

View file

@ -0,0 +1,53 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Setting from './setting.js'
const getUrl = state => state?.tuple ? state.tuple[1] + ':' + state.tuple[2] : state
const getSocks = state => state?.tuple
export default {
...Setting,
data () {
return {
urlField: '',
socksField: false
}
},
created () {
Setting.created()
this.urlField = getUrl(this.realDraftMode ? this.draft : this.state)
this.socksField = getSocks(this.realDraftMode ? this.draft : this.state)
},
computed: {
...Setting.computed,
// state that we'll show in the UI, i.e. transforming map into list
displayState () {
if (this.visibleState?.tuple) {
return this.visibleState.tuple[1] + ':' + this.visibleState.tuple[2]
}
return this.visibleState
},
socksState () {
return getSocks(this.visibleState)
}
},
components: {
...Setting.components,
Checkbox
},
methods: {
...Setting.methods,
getValue ({ event, isProxy}) {
if (isProxy) {
this.socksField = event
} else {
this.urlField = event.target.value
}
if (this.socksField) {
return { tuple: [ ':socks5', ...this.urlField.split(':') ] }
} else {
return this.urlField
}
}
}
}

View file

@ -0,0 +1,59 @@
<template>
<label
v-if="matchesExpertLevel"
class="ProxySetting setting-item"
>
<label
v-if="!hideLabel"
:for="path"
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
{{ ' ' }}
<div class="setting-control">
<input
:id="path"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions[0]"
:value="displayState"
@change="event => update({ event })"
>
{{ ' ' }}
<Checkbox
:model-value="socksState"
:disabled="shouldBeDisabled"
:indeterminate="isIndeterminate"
@update:model-value="event => update({ event, isProxy: true})"
>
SOCKS5
{{ ' ' }}
</Checkbox>
</div>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons v-if="!hideDraftButtons" />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label>
</template>
<script src="./proxy_setting.js"></script>

View file

@ -0,0 +1,99 @@
import { clone } from 'lodash'
import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
import Setting from './setting.js'
import Select from 'src/components/select/select.vue'
import Attachment from 'src/components/attachment/attachment.vue'
import MediaUpload from 'src/components/media_upload/media_upload.vue'
export default {
...Setting,
components: {
...Setting.components,
Select,
Attachment,
MediaUpload
},
computed: {
...Setting.computed,
purposeOptions () {
return ['any','monochrome','maskable'].map(value => ({
value,
key: value,
label: this.$t('admin_dash.instance.pwa.icon.' + value)
}))
}
},
methods: {
...Setting.methods,
attachment (e) {
const path = e[':src']
if (!path) {
return {
mimetype: '',
url: ''
}
}
const url = path.includes('://') ? path : this.$store.state.instance.server + path
return {
mimetype: fileTypeExt(url),
url
}
},
setMediaFile ({ event, index }) {
this.update({
event: {
target: {
value: event.url
},
},
index,
eventType: 'edit',
field: ':src'
})
},
setPurpose ({ event, index }) {
this.update({
event: {
target: {
value: event
},
},
index,
eventType: 'edit',
field: ':purpose'
})
},
getValue ({ event, field, index, eventType }) {
switch (eventType) {
case 'add': {
const res = [...this.visibleState, {}]
return res
}
case 'remove': {
const pre = this.visibleState.slice(0, index)
const post = this.visibleState.slice(index + 1)
return [...pre, ...post]
}
case 'edit': {
const pre = this.visibleState.slice(0, index)
const post = this.visibleState.slice(index + 1)
const item = clone(this.visibleState[index])
const string = event.target.value
if (!string) {
delete item[field]
} else {
item[field] = string
}
return [...pre, item, ...post]
}
}
}
}
}

View file

@ -0,0 +1,226 @@
<template>
<div
v-if="matchesExpertLevel"
class="PWAManifestIconsSetting setting-item"
>
<label
class="pwa-label setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
{{ ' ' }}
<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="setting-control">
<ul class="item-list">
<li
v-if="visibleState.length === 0"
class="no_items"
>
{{ $t('admin_dash.instance.pwa.no_icons') }}
<button
v-if="visibleState.length === 0"
class="button-default add-button"
@click="e => update({ eventType: 'add' })"
>
<FAIcon icon="plus" />
</button>
</li>
<li
v-for="(item, index) in visibleState"
:key="index"
>
<div class="icon-element">
<div class="src-field">
<Attachment
class="src-attachment"
:compact="compact"
:attachment="attachment(item)"
size="small"
hide-description
/>
<div class="src-url">
<label for="path">{{ $t('settings.url') }}</label>
<input
:id="path"
class="input string-input"
:disabled="shouldBeDisabled"
:value="item[':src']"
@change="event => update({ event, index, eventType: 'edit', field: ':src' })"
>
</div>
<MediaUpload
ref="mediaUpload"
class="src-upload media-upload-icon"
:class="{ disabled: shouldBeDisabled }"
normal-button
:accept-types="acceptTypes"
@uploaded="event => setMediaFile({ event, index })"
/>
</div>
<dl>
<dt>{{ $t('admin_dash.instance.pwa.icon.purpose') }}</dt>
<dd>
<Select
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:model-value="item[':purpose']"
@update:model-value="event => setPurpose({ event, index })"
>
<option
v-for="(purpose, index) in purposeOptions"
:key="index"
:value="purpose.value"
>
{{ purpose.label }}
</option>
</Select>
</dd>
<dt><code>sizes</code>{{ $t('admin_dash.instance.pwa.optional') }}</dt>
<dd>
<input
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:value="item[':sizes']"
@change="e => update({ event: e, index, eventType: 'edit', field: ':sizes' })"
>
</dd>
<dt><code>type</code>{{ $t('admin_dash.instance.pwa.optional') }}</dt>
<dd>
<input
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:value="item[':type']"
@change="e => update({ event: e, index, eventType: 'edit', field: ':type' })"
>
</dd>
</dl>
<div class="buttons">
<button
v-if="index === visibleState.length - 1"
class="button-default add-button"
@click="e => update({ eventType: 'add' })"
>
<FAIcon icon="plus" />
</button>
<button
class="button-default delete-button"
@click="e => update({ index, eventType: 'remove' })"
>
<FAIcon icon="times" />
</button>
</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script src="./pwa_manifest_icons_setting.js"></script>
<style lang="scss">
div.PWAManifestIconsSetting {
margin-left: 3em;
&.setting-item {
display: grid;
grid-template-areas:
"label"
"desc"
"control"
"draft";
grid-template-rows: 2em auto 1fr;
grid-template-columns: 1fr;
}
.pwa-label.setting-label {
align-self: end;
text-align: left;
}
.buttons {
display: flex;
gap: 0.5em;
justify-content: right;
margin-top: 0.5em;
button {
line-height: 2;
}
}
ul {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fit, 12em);
grid-gap: 2em;
}
dl {
display: grid;
grid-template-columns: 1fr;
margin-top: 0.5em;
gap: 0.25em;
align-items: baseline;
dt {
font-weight: 800;
&::after {
content: ':'
}
}
dd {
margin: 0
}
}
.src-field {
display: grid;
grid-template-columns: auto;
justify-items: center;
gap: 0.5em;
.src-attachment {
width: 10em;
height: 10em;
display: block;
margin-bottom: 0.5em;
}
.src-upload {
display: block;
width: 100%;
}
.src-url {
display: flex;
flex-direction: column;
gap: 0.25em;
width: 100%;
label {
display: block;
}
}
}
}
</style>

View file

@ -0,0 +1,53 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Setting from './setting.js'
export default {
...Setting,
data () {
return {
newValue: '',
}
},
components: {
...Setting.components,
Checkbox
},
props: {
...Setting.props
},
computed: {
...Setting.computed,
isSeparate () {
// [[a1, b1], [a2, b2]] vs [a, b]
return Array.isArray(this.visibleState[0])
},
normalizedState () {
if (this.isSeparate) {
return this.visibleState.map(y => y.map(x => Number(x) || 0))
} else {
return [this.visibleState.map(x => Number(x) || 0)]
}
}
},
methods: {
...Setting.methods,
getValue ({ event, side, index, eventType }) {
if (eventType === 'edit') {
const value = Number(event.target.value)
if (Number.isNaN(value)) return this.visibleState
const newVal = [...this.normalizedState.map(x => [...x])]
newVal[side][index] = value
return newVal
}
if (eventType === 'toggleMode') {
if (event === 'split') {
return [this.normalizedState[0], this.normalizedState[0]]
} else if (event === 'join') {
return [this.normalizedState[0]]
}
}
}
}
}

View file

@ -0,0 +1,139 @@
<template>
<div
v-if="matchesExpertLevel"
class="RateSetting setting-item"
>
<label
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
{{ ' ' }}
<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="setting-control">
<table>
<tr>
<th>&nbsp;</th>
<th>
{{ $t('admin_dash.rate_limit.period') }}
</th>
<th>
{{ $t('admin_dash.rate_limit.amount') }}
</th>
</tr>
<tr>
<td v-if="isSeparate">
{{ $t('admin_dash.rate_limit.unauthenticated') }}
</td>
<td v-else>
{{ $t('admin_dash.rate_limit.rate_limit') }}
</td>
<td>
<input
class="input string-input"
type="number"
:value="normalizedState[0][0]"
@change="e => update({ event: e, index: 0, side: 0, eventType: 'edit' })"
>
</td>
<td>
<input
class="input string-input"
type="number"
:value="normalizedState[0][1]"
@change="e => update({ event: e, index: 1, side: 0, eventType: 'edit' })"
>
</td>
</tr>
<tr v-if="isSeparate">
<td>
{{ $t('admin_dash.rate_limit.authenticated') }}
</td>
<td>
<input
class="input string-input"
type="number"
:value="normalizedState[1][0]"
@change="e => update({ event: e, index: 0, side: 1, eventType: 'edit' })"
>
</td>
<td>
<input
class="input string-input"
type="number"
:value="normalizedState[1][1]"
@change="e => update({ event: e, index: 1, side: 1, eventType: 'edit' })"
>
</td>
</tr>
</table>
<Checkbox
:model-value="isSeparate"
@update:model-value="event => update({ event: event ? 'join' : 'split', eventType: 'toggleMode' })"
>
{{ $t('admin_dash.rate_limit.separate') }}
</Checkbox>
</div>
<DraftButtons />
</div>
</template>
<script src="./rate_setting.js"></script>
<style lang="scss">
.RateSetting {
&.setting-item {
display: grid;
grid-template-areas:
"label control"
"desc control"
". draft";
.setting-label {
text-align: right;
align-self: center;
}
.setting-description {
text-align: right;
}
.setting-control {
align-self: end;
}
}
table {
margin-top: 0.5em;
}
th {
font-weight: normal;
}
td {
input {
width: 15ch;
}
}
margin-bottom: 2em;
}
</style>

View file

@ -1,7 +1,7 @@
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'
import { get, set, cloneDeep, isEqual } from 'lodash'
export default {
components: {
@ -18,6 +18,22 @@ export default {
type: [String, Array],
required: false
},
showDescription: {
type: Boolean,
required: false
},
descriptionPathOverride: {
type: [String, Array],
required: false
},
suggestions: {
type: [String, Array],
required: false
},
subgroup: {
type: String,
required: false
},
disabled: {
type: Boolean,
default: false
@ -37,17 +53,27 @@ export default {
type: String,
default: undefined
},
hideDraftButtons: { // this is for the weird backend hybrid (Boolean|String or Boolean|Number) settings
required: false,
type: Boolean
},
hideLabel: {
type: Boolean
},
hideDescription: {
type: Boolean
},
swapDescriptionAndLabel: {
type: Boolean
},
backendDescriptionPath: {
type: [String, Array]
},
overrideBackendDescription: {
type: Boolean
},
overrideBackendDescriptionLabel: {
type: Boolean
type: [Boolean, String]
},
draftMode: {
type: Boolean,
@ -73,7 +99,7 @@ export default {
},
created () {
if (this.realDraftMode && (this.realSource !== 'admin' || this.path == null)) {
this.draft = this.state
this.draft = cloneDeep(this.state)
}
},
computed: {
@ -114,10 +140,13 @@ export default {
return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
},
backendDescription () {
return get(this.$store.state.adminSettings.descriptions, this.path)
return get(this.$store.state.adminSettings.descriptions, this.descriptionPath)
},
backendDescriptionLabel () {
if (this.realSource !== 'admin') return ''
if (this.overrideBackendDescriptionLabel !== '' && typeof this.overrideBackendDescriptionLabel === 'string') {
return this.overrideBackendDescriptionLabel
}
if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
return this.$t([
'admin_dash',
@ -132,6 +161,7 @@ export default {
}
},
backendDescriptionDescription () {
if (this.description) return this.description
if (this.realSource !== 'admin') return ''
if (this.hideDescription) return null
if (!this.backendDescription || this.overrideBackendDescription) {
@ -148,13 +178,20 @@ export default {
}
},
backendDescriptionSuggestions () {
return this.backendDescription?.suggestions
return this.backendDescription?.suggestions || this.suggestions
},
shouldBeDisabled () {
if (this.path == null) {
return this.disabled
}
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
let parentValue = null
if (this.parentPath !== undefined && this.realSource === 'admin') {
if (this.realDraftMode) {
parentValue = get(this.$store.state.adminSettings.draft, this.parentPath)
} else {
parentValue = get(this.configSource, this.parentPath)
}
}
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
},
configSource () {
@ -209,17 +246,29 @@ export default {
if (this.path == null) return null
return Array.isArray(this.path) ? this.path : this.path.split('.')
},
descriptionPath () {
if (this.path == null) return null
if (this.descriptionPathOverride) return this.descriptionPathOverride
const path = Array.isArray(this.path) ? this.path : this.path.split('.')
if (this.subgroup) {
return [
...path.slice(0, path.length - 1),
':subgroup,' + this.subgroup,
...path.slice(path.length - 1)
]
}
return path
},
isDirty () {
if (this.path == null) return false
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
return this.realDraftMode && !isEqual(this.draft, this.state)
}
},
canHardReset () {
return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths &&
this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths?.has(this.canonPath.join(' -> '))
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0

View file

@ -1,13 +1,20 @@
<template>
<label
<span
v-if="matchesExpertLevel"
class="StringSetting"
class="StringSetting setting-item"
>
<label
v-if="!hideLabel"
:for="path"
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
{{ ' ' }}
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
@ -16,21 +23,17 @@
</template>
<slot v-else />
</label>
{{ ' ' }}
<input
:id="path"
class="input string-input"
class="setting-control input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions"
:value="realDraftMode ? draft : state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<DraftButtons v-if="!hideDraftButtons" />
<p
v-if="backendDescriptionDescription"
class="setting-description"
@ -38,7 +41,7 @@
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label>
</span>
</template>
<script src="./string_setting.js"></script>

View file

@ -0,0 +1,16 @@
import Setting from './setting.js'
export default {
...Setting,
methods: {
...Setting.methods,
getValue ({ e, side }) {
const [a, b] = this.visibleState || []
if (side === 0) {
return { tuple: [e.target.value, b]}
} else {
return { tuple: [a, e.target.value]}
}
}
}
}

View file

@ -0,0 +1,58 @@
<template>
<span
v-if="matchesExpertLevel"
class="setting-item"
>
<label
v-if="!hideLabel"
:for="path"
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
{{ ' ' }}
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
<span class="setting-control">
<input
:id="path"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions?.[0]?.[0]"
:value="visibleState?.tuple?.[0]"
@change="e => update({ e, side: 0 })"
>
{{ ' ' }}
<input
:id="path"
class="input string-input"
:class="{ disabled: shouldBeDisabled }"
:disabled="shouldBeDisabled"
:placeholder="backendDescriptionSuggestions?.[0]?.[1]"
:value="visibleState?.tuple?.[1]"
@change="e => update({ e, side: 1 })"
>
</span>
<DraftButtons v-if="!hideDraftButtons" />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</span>
</template>
<script src="./tuple_setting.js"></script>

View file

@ -1,16 +1,20 @@
<template>
<span
v-if="matchesExpertLevel"
class="UnitSetting"
class="UnitSetting setting-item"
>
<label
:for="path"
class="size-label"
class="setting-label size-label"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
{{ ' ' }}
<slot />
</label>
{{ ' ' }}
<span class="no-break">
<span class="no-break setting-control">
<input
:id="path"
class="input number-input"
@ -38,10 +42,6 @@
</Select>
</span>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
</span>
</template>
@ -50,7 +50,7 @@
<style lang="scss">
.UnitSetting {
.no-break {
display: inline-block;
display: inline-flex;
}
.number-input {

View file

@ -159,11 +159,13 @@ export default {
const wrapperClasses = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
const contentClasses = ['tab-content']
if (props['full-width']) {
if (props['full-width'] || props['full-width'] === '') {
contentClasses.push('-full-width')
wrapperClasses.push('-full-width')
}
if (props['full-height']) {
if (props['full-height'] || props['full-width'] === '') {
contentClasses.push('-full-height')
wrapperClasses.push('-full-height')
}
return (
<div class={wrapperClasses} >

View file

@ -26,24 +26,6 @@
}
> .contents {
flex: 1 0 35em;
.tab-content {
align-self: center;
&:not(.-full-width) {
max-width: 40em;
}
&.-full-width {
align-self: stretch;
}
&.-full-height {
flex: 1;
}
}
.tab-content-label {
box-sizing: border-box;
margin: 0;
@ -60,8 +42,24 @@
flex: 1 1 auto;
height: 100%;
overflow-y: auto;
display: flex;
display: grid;
grid-template-columns: minmax(1em, 1fr) minmax(min-content, 45em) minmax(1em, 1fr);
grid-template-areas: ". content .";
flex-direction: column;
.tab-content {
grid-area: content;
&.-full-width {
grid-column: 1 / 4;
}
&.-full-height {
> * {
height: 100%;
}
}
}
}
.tab-content-wrapper {
@ -109,6 +107,10 @@
flex-shrink: 1;
}
}
.tab-slot-wrapper {
grid-template-columns: 0 minmax(min-content, 45em) 0;
}
}
@supports (container-type: inline-size) {

View file

@ -170,7 +170,7 @@ const SettingsModal = {
},
computed: {
...mapState(useInterfaceStore, {
temporaryChangesTimeoutId: store => store.temporaryChangesTimeoutId,
temporaryChangesCountdown: store => store.temporaryChangesCountdown,
currentSaveStateNotice: store => store.settings.currentSaveStateNotice,
modalActivated: store => store.settingsModalState !== 'hidden',
modalMode: store => store.settingsModalMode,

View file

@ -6,6 +6,7 @@
font-weight: 500;
margin-top: 1em;
margin-bottom: 1em;
margin-right: 1em;
}
h3 {
@ -13,6 +14,7 @@
font-weight: 500;
margin-top: 1em;
margin-bottom: 0.5em;
margin-right: 1em;
border-bottom: 1px solid var(--border);
padding-bottom: 0.25em;
box-sizing: border-box;
@ -22,36 +24,133 @@
h4 {
font-size: 1.1rem;
margin-top: 1em;
margin-right: 1em;
margin-bottom: 0.5em;
margin-left: 1em;
}
h5 {
font-size: 1rem;
margin-bottom: 0.5em;
margin-top: 0;
margin-left: 1em;
margin-bottom: 0.25em;
margin-top: 0.75em;
}
p {
line-height: 1.5;
}
.suboptions {
margin-left: 1em;
}
.sidenote {
margin-left: 5em;
padding: 0.25em 1em;
margin-top: 0.25em;
}
.setting-description {
margin-top: 0.2em;
margin-bottom: 0;
font-size: 80%;
}
.setting-item {
display: grid;
grid-template-areas:
"label control"
"label desc"
". draft";
grid-template-columns: 4fr 5fr;
column-gap: 0.5em;
align-items: baseline;
padding: 0.5em 0;
.setting-label {
grid-area: label;
text-align: right;
}
.ModifiedIndicator,
.ProfileSettingIndicator {
grid-area: indicator;
}
.setting-control {
grid-area: control;
&.textarea {
align-self: baseline;
overflow: auto;
}
&.checkbox {
display: grid;
grid-template-columns: subgrid;
.label {
grid-area: label;
text-align: right;
}
.checkbox-indicator {
grid-area: control;
}
.-mobile & {
.label {
text-align: left;
}
}
}
}
.setting-control.setting-label {
grid-column: 1 / 3;
text-align: left;
}
.setting-description {
grid-area: desc;
}
.DraftButtons {
grid-area: draft;
}
}
.vertical-tab-switcher {
height: 100%;
}
.setting-list,
.option-list {
list-style-type: none;
padding-left: 2em;
padding-left: 0;
margin: 0;
.btn:not(.dropdown-button) {
padding: 0 2em;
}
li {
margin: 1em 0;
}
.suboptions {
margin-top: 0.3em;
.btn-group {
.button-default {
flex: 0 1 auto;
}
}
&.two-column {
display: grid;
grid-template-columns: 1fr 1fr;
margin-left: 2em;
border-bottom: 1px solid var(--border);
padding-bottom: 0.5em;
margin-bottom: 1em;
.setting-item {
grid-template-columns: 3fr 1fr;
}
> li {
margin: 0;
@ -60,12 +159,6 @@
}
}
.setting-description {
margin-top: 0.2em;
margin-bottom: 2em;
font-size: 70%;
}
.settings-modal-panel {
overflow: hidden;
transition: transform;
@ -114,20 +207,39 @@
}
}
/* stylelint-disable no-descending-specificity */
.setting-item {
grid-template-columns: 1fr min-content;
column-gap: 0.5em;
padding: 1em;
align-items: center;
.setting-label {
text-align: left;
}
.checkbox {
.label {
text-align: left;
margin-left: 0;
}
}
}
ul {
padding: 0;
li:not(:first-child) {
.setting-item {
border-top: 1px solid var(--border);
}
}
}
.setting-list:not(.suboptions),
.option-list {
padding-left: 0.25em;
/* stylelint-disable no-descending-specificity */
// it makes no sense
> li {
margin: 1em 0;
line-height: 1.5em;
vertical-align: middle;
}
/* stylelint-enable no-descending-specificity */
&.two-column {
grid-template-columns: 1fr;
}

View file

@ -158,14 +158,14 @@
</div>
<teleport to="#modal">
<ConfirmModal
v-if="temporaryChangesTimeoutId"
v-if="temporaryChangesCountdown > 0"
:title="$t('settings.confirm_new_setting')"
:cancel-text="$t('settings.revert')"
:confirm-text="$t('settings.confirm')"
@cancelled="temporaryChangesRevert"
@accepted="temporaryChangesConfirm"
>
{{ $t('settings.confirm_new_question') }}
{{ $t('settings.confirm_new_question_countdown', temporaryChangesCountdown) }}
</ConfirmModal>
</teleport>
</Modal>

View file

@ -1,32 +1,61 @@
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LinksTab from './admin_tabs/links_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'
import FrontendsTab from './admin_tabs/frontends_tab.vue'
import MediaProxyTab from './admin_tabs/media_proxy_tab.vue'
import EmojiTab from './admin_tabs/emoji_tab.vue'
import UploadsTab from './admin_tabs/uploads_tab.vue'
import MailerTab from './admin_tabs/mailer_tab.vue'
import MonitoringTab from './admin_tabs/monitoring_tab.vue'
import RegistrationsTab from './admin_tabs/registrations_tab.vue'
import AuthTab from './admin_tabs/auth_tab.vue'
import HTTPTab from './admin_tabs/http_tab.vue'
import OtherTab from './admin_tabs/other_tab.vue'
import RatesTab from './admin_tabs/rates_tab.vue'
import PostsTab from './admin_tabs/posts_tab.vue'
import FederationTab from './admin_tabs/federation_tab.vue'
import JobQueuesTab from './admin_tabs/job_queues_tab.vue'
import { useInterfaceStore } from 'src/stores/interface'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faWrench,
faHand,
faChain,
faGlobe,
faLaptopCode,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
faTowerBroadcast,
faEnvelope,
faChartLine,
faDoorOpen,
faGears,
faKey,
faCircleNodes,
faUpload,
faMessage,
faEllipsis,
faGauge
} from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench,
faHand,
faChain,
faGlobe,
faLaptopCode,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
faTowerBroadcast,
faEnvelope,
faChartLine,
faDoorOpen,
faGears,
faKey,
faCircleNodes,
faUpload,
faMessage,
faEllipsis,
faGauge
)
const SettingsModalAdminContent = {
@ -34,9 +63,22 @@ const SettingsModalAdminContent = {
VerticalTabSwitcher,
InstanceTab,
LimitsTab,
RegistrationsTab,
EmojiTab,
FrontendsTab,
EmojiTab
FederationTab,
LimitsTab,
MailerTab,
UploadsTab,
MediaProxyTab,
LinksTab,
JobQueuesTab,
AuthTab,
HTTPTab,
MonitoringTab,
RatesTab,
OtherTab,
PostsTab
},
computed: {
user () {

View file

@ -1,48 +0,0 @@
.settings_tab-switcher {
height: 100%;
.setting-item {
border-bottom: 2px solid var(--border);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div,
> label {
display: block;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
}
.select-multiple {
margin-top: 0.5em;
display: flex;
flex-direction: column;
.option-list {
margin: 0;
margin-top: 0.5em;
padding-left: 0.5em;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable svg {
color: var(--cRed);
}
}
}

View file

@ -2,7 +2,7 @@
<vertical-tab-switcher
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
ref="tabSwitcher"
class="settings_tab-switcher"
class="settings-admin-content settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:render-only-focused="true"
@ -15,7 +15,7 @@
data-tab-name="nodb-notice"
>
<div :label="$t('admin_dash.tabs.nodb')">
<div class="setting-item">
<div class="setting-section">
<h2>{{ $t('admin_dash.nodb.heading') }}</h2>
<i18n-t
scope="global"
@ -48,6 +48,41 @@
>
<InstanceTab />
</div>
<div
:label="$t('admin_dash.tabs.registrations')"
icon="door-open"
data-tab-name="registrations"
>
<RegistrationsTab />
</div>
<div
:label="$t('admin_dash.tabs.auth')"
icon="key"
data-tab-name="monitoring"
>
<AuthTab />
</div>
<div
:label="$t('admin_dash.tabs.emoji')"
icon="face-smile-beam"
data-tab-name="emoji"
full-width
>
<EmojiTab />
</div>
<div
:label="$t('admin_dash.tabs.frontends')"
icon="laptop-code"
data-tab-name="frontends"
full-width
>
<FrontendsTab />
</div>
<div
v-if="adminDbLoaded"
:label="$t('admin_dash.tabs.limits')"
@ -56,24 +91,104 @@
>
<LimitsTab />
</div>
<div
:label="$t('admin_dash.tabs.frontends')"
icon="laptop-code"
data-tab-name="frontends"
v-if="adminDbLoaded"
:label="$t('admin_dash.tabs.rate_limit')"
icon="gauge"
data-tab-name="rate_limits"
>
<FrontendsTab />
<RatesTab />
</div>
<div
:label="$t('admin_dash.tabs.emoji')"
icon="face-smile-beam"
data-tab-name="emoji"
:label="$t('admin_dash.tabs.uploads')"
icon="upload"
data-tab-name="uploads"
>
<EmojiTab />
<UploadsTab />
</div>
<div
:label="$t('admin_dash.tabs.media_proxy')"
icon="tower-broadcast"
data-tab-name="media_proxy"
>
<MediaProxyTab />
</div>
<div
:label="$t('admin_dash.tabs.posts')"
icon="message"
data-tab-name="other"
>
<PostsTab />
</div>
<div
:label="$t('admin_dash.tabs.links')"
icon="chain"
data-tab-name="links"
>
<LinksTab />
</div>
<div
:label="$t('admin_dash.tabs.mailer')"
icon="envelope"
data-tab-name="mailer"
>
<MailerTab />
</div>
<div
:label="$t('admin_dash.tabs.federation')"
icon="circle-nodes"
data-tab-name="monitoring"
>
<FederationTab />
</div>
<div
:label="$t('admin_dash.tabs.http')"
icon="globe"
data-tab-name="http"
>
<HTTPTab />
</div>
<div
:label="$t('admin_dash.tabs.job_queues')"
icon="gears"
data-tab-name="job_queues"
>
<JobQueuesTab />
</div>
<div
:label="$t('admin_dash.tabs.monitoring')"
icon="chart-line"
data-tab-name="monitoring"
>
<MonitoringTab />
</div>
<div
:label="$t('admin_dash.tabs.other')"
icon="ellipsis"
data-tab-name="other"
>
<OtherTab />
</div>
</vertical-tab-switcher>
</template>
<script src="./settings_modal_admin_content.js"></script>
<style src="./settings_modal_admin_content.scss" lang="scss"></style>
<style lang="scss">
.settings-admin-content {
.setting-item {
grid-template-columns: 1fr 3fr;
}
}
</style>

View file

@ -1,50 +0,0 @@
.settings_tab-switcher {
height: 100%;
[full-height="true"] {
height: 100%
}
.setting-item {
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div,
> label {
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
}
.select-multiple {
margin-top: 1em;
display: flex;
flex-direction: column;
.option-list {
margin: 0;
margin-top: 0.5em;
padding-left: 0.5em;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable svg {
color: var(--cRed);
}
}
}

View file

@ -49,7 +49,6 @@
<AppearanceTab />
</div>
<div
:full-width="true"
:label="$t('settings.layout')"
icon="table-columns"
data-tab-name="layout"
@ -68,7 +67,6 @@
</div>
<div
:label="$t('settings.filtering')"
:full-width="true"
icon="filter"
data-tab-name="filtering"
>
@ -139,5 +137,3 @@
</template>
<script src="./settings_modal_user_content.js"></script>
<style src="./settings_modal_user_content.scss" lang="scss"></style>

View file

@ -237,7 +237,6 @@ const AppearanceTab = {
return !window.IntersectionObserver
},
instanceWallpaper () {
console.log(this.$store.state.instance.background)
this.$store.state.instance.background
},
instanceWallpaperUsed () {

View file

@ -1,4 +1,6 @@
.appearance-tab {
margin: 1em;
h3 {
border: none
}
@ -67,7 +69,7 @@
aspect-ratio: 16 / 9;
width: 16em;
}
img {
object-fit: cover;
}
@ -138,6 +140,7 @@
border: 1px solid var(--border);
margin-bottom: 0.5em;
margin-top: 0;
padding: 0.5em;
}
.palettes {

View file

@ -5,7 +5,7 @@
icon="table-columns"
>
<div
class="setting-item"
class="setting-section"
:label="$t('settings.theme')"
icon="paintbrush"
>
@ -161,13 +161,16 @@
<div class="fun-monitor-neck button-default" />
<div class="fun-monitor-display-bezel button-default">
<div class="fun-monitor-display-screen input">
<img
v-if="backgroundPreview || user.background_image || instanceWallpaper"
class="fun-monitor-display-screen-image"
:src="backgroundPreview || user.background_image || instanceWallpaper"
/>
<div v-else class="wallpaper" />
<div class="fun-monitor-display-screen-overlay input" />
<img
v-if="backgroundPreview || user.background_image || instanceWallpaper"
class="fun-monitor-display-screen-image"
:src="backgroundPreview || user.background_image || instanceWallpaper"
>
<div
v-else
class="wallpaper"
/>
<div class="fun-monitor-display-screen-overlay input" />
<div
v-if="backgroundUploading"
class="fun-monitor-display-uploading"

View file

@ -152,7 +152,6 @@ const ClutterTab = {
},
purgeExpiredFilters () {
this.muteFiltersExpired.forEach(([id]) => {
console.log(id)
delete this.muteFiltersDraftObject[id]
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
})

View file

@ -1,6 +1,6 @@
<template>
<div class="clutter-tab">
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.interface') }}</h3>
<ul class="setting-list">
<li>

View file

@ -1,14 +1,19 @@
<template>
<div :label="$t('settings.posts')">
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.general') }}</h3>
<ul class="setting-list">
<li>
<label for="default-vis">
{{ $t('settings.default_vis') }}
{{ ' ' }}
<label
class="setting-item "
for="default-vis"
>
<span class="setting-label">
<ProfileSettingIndicator :is-profile="true" />
{{ $t('settings.default_vis') }}
</span>
<ScopeSelector
class="scope-selector"
class="scope-selector setting-control"
:show-all="true"
:user-default="$store.state.profileConfig.defaultScope"
:initial-scope="$store.state.profileConfig.defaultScope"
@ -16,7 +21,6 @@
:unstyled="false"
uns
/>
<ProfileSettingIndicator :is-profile="true" />
</label>
</li>
<li>

View file

@ -3,7 +3,7 @@
class="data-import-export-tab"
:label="$t('settings.data_import_export_tab')"
>
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.import_export.title') }}</h3>
<ul class="setting-list">
<li>

View file

@ -1,4 +1,24 @@
.developer-tab {
.setting-list {
margin-left: 2em;
}
.cache-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column: 1 / 3;
column-gap: 0.5em;
margin: 0 5em;
.-mobile & {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
row-gap: 0.5em;
margin: 0;
}
}
dt {
font-weight: 900;
}

View file

@ -3,7 +3,7 @@
:label="$t('settings.developer')"
class="developer-tab"
>
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.version.title') }}</h3>
<dl class="setting-list">
<dt>{{ $t('settings.version.backend_version') }}</dt>
@ -32,22 +32,6 @@
{{ $t('settings.virtual_scrolling') }}
</BooleanSetting>
</li>
<li>
<button
class="btn button-default"
@click="clearAssetCache"
>
{{ $t('settings.clear_asset_cache') }}
</button>
</li>
<li>
<button
class="btn button-default"
@click="clearEmojiCache"
>
{{ $t('settings.clear_emoji_cache') }}
</button>
</li>
<li>
<BooleanSetting
path="themeDebug"
@ -64,6 +48,25 @@
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
<h4>{{ $t('settings.cache') }}</h4>
<li>
<div class="setting-item">
<div class="cache-buttons">
<button
class="btn button-default"
@click="clearAssetCache"
>
{{ $t('settings.clear_asset_cache') }}
</button>
<button
class="btn button-default"
@click="clearEmojiCache"
>
{{ $t('settings.clear_emoji_cache') }}
</button>
</div>
</div>
</li>
</ul>
</div>
</div>

View file

@ -217,7 +217,6 @@ const FilteringTab = {
},
purgeExpiredFilters () {
this.muteFiltersExpired.forEach(([id]) => {
console.log(id)
delete this.muteFiltersDraftObject[id]
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
})

View file

@ -1,6 +1,6 @@
<template>
<div class="filtering-tab">
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.filter.mute_filter') }}</h3>
<ul class="setting-list">
<li>
@ -14,19 +14,23 @@
</ChoiceSetting>
</li>
<li>
{{ $t('user_card.default_mute_expiration') }}
<Select
id="onMuteDefaultActionLv1"
v-model="onMuteDefaultActionLv1"
>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
<span class="setting-item">
<span class="setting-label">
{{ $t('user_card.default_mute_expiration') }}
</span>
<Select
id="onMuteDefaultActionLv1"
v-model="onMuteDefaultActionLv1"
>
{{ option.label }}
</option>
</Select>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
>
{{ option.label }}
</option>
</Select>
</span>
<ul
v-if="onMuteDefaultActionLv1 === 'temporarily'"
class="setting-list suboptions"
@ -44,19 +48,24 @@
</ul>
</li>
<li v-if="blockExpirationSupported">
{{ $t('user_card.default_block_expiration') }}
<Select
id="onBlockDefaultActionLv1"
v-model="onBlockDefaultActionLv1"
>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
<span class="setting-item">
<span class="setting-label">
{{ $t('user_card.default_block_expiration') }}
</span>
<Select
id="onBlockDefaultActionLv1"
v-model="onBlockDefaultActionLv1"
class="setting-control"
>
{{ option.label }}
</option>
</Select>
<option
v-for="option in muteBlockLv1Options"
:key="option.key"
:value="option.value"
>
{{ option.label }}
</option>
</Select>
</span>
<ul
v-if="onBlockDefaultActionLv1 === 'temporarily'"
class="setting-list suboptions"

View file

@ -1,25 +1,25 @@
<template>
<div>
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.format_and_language') }}</h3>
<ul class="setting-list">
<li>
<interface-language-switcher
v-model="language"
@update="val => language = val"
>
{{ $t('settings.interfaceLanguage') }}
</interface-language-switcher>
</li>
<li>
<interface-language-switcher
v-model="emailLanguage"
:profile="true"
@update:model-value="updateProfile()"
>
{{ $t('settings.email_language') }}
</interface-language-switcher>
</li>
<h4>{{ $t('settings.interfaceLanguage') }}</h4>
<interface-language-switcher
v-model="language"
class="lang-selector"
@update="val => language = val"
/>
<h4>
{{ $t('settings.email_language') }}
{{ ' ' }}
<ProfileSettingIndicator :is-profile="true" />
</h4>
<interface-language-switcher
v-model="emailLanguage"
class="lang-selector"
:profile="true"
@update:model-value="updateProfile()"
/>
<li>
<BooleanSetting path="useAbsoluteTimeFormat">
{{ $t('settings.absolute_time_format') }}
@ -47,26 +47,24 @@
>
{{ $t('settings.text_size') }}
</UnitSetting>
<div>
<small>
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</small>
</div>
<p class="sidenote">
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</p>
</li>
<li>
<FontControl
@ -144,7 +142,9 @@
class="setting-list"
>
<li class="select-multiple">
<span class="label">{{ $t('settings.confirm_dialogs') }}</span>
<h4 class="label">
{{ $t('settings.confirm_dialogs') }}
</h4>
<ul class="option-list">
<li>
<BooleanSetting path="modalOnRepeat">
@ -156,9 +156,10 @@
{{ $t('settings.confirm_dialogs_unfollow') }}
</BooleanSetting>
</li>
<li>
<li
v-if="!blockExpirationSupported"
>
<BooleanSetting
v-if="!blockExpirationSupported"
path="modalOnBlock"
>
{{ $t('settings.confirm_dialogs_block') }}

View file

@ -1,6 +1,6 @@
<template>
<div :label="$t('settings.layout')">
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.general') }}</h3>
<ul class="setting-list">
<li>

View file

@ -1,6 +1,6 @@
<template>
<div :label="$t('settings.notifications')">
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.notification_setting_annoyance') }}</h3>
<ul class="setting-list">
<li>
@ -12,11 +12,9 @@
<BooleanSetting path="ignoreInactionableSeen">
{{ $t('settings.notification_setting_ignore_inactionable_seen') }}
</BooleanSetting>
<div>
<small>
{{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
</small>
</div>
<p class="sidenote">
{{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
</p>
</li>
<li>
<BooleanSetting
@ -28,7 +26,7 @@
</li>
</ul>
</div>
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.notification_setting_filters') }}</h3>
<ul class="setting-list">
<li>
@ -41,7 +39,10 @@
</li>
<li>
<h4> {{ $t('settings.notification_visibility') }}</h4>
<p v-if="expertLevel > 0">
<p
v-if="expertLevel > 0"
class="sidenote"
>
{{ $t('settings.notification_setting_filters_chrome_push') }}
</p>
<ul class="setting-list two-column">
@ -241,7 +242,7 @@
<div
v-if="expertLevel > 0"
class="setting-item"
class="setting-section"
>
<h3>{{ $t('settings.notification_setting_privacy') }}</h3>
<ul class="setting-list">
@ -260,11 +261,12 @@
>
{{ $t('settings.enable_web_push_always_show') }}
</BooleanSetting>
<div :class="{ faint: !mergedConfig.webPushNotifications }">
<small>
{{ $t('settings.enable_web_push_always_show_tip') }}
</small>
</div>
<p
:class="{ faint: !mergedConfig.webPushNotifications }"
class="sidenote"
>
{{ $t('settings.enable_web_push_always_show_tip') }}
</p>
</li>
</ul>
</li>
@ -279,10 +281,12 @@
</li>
</ul>
</div>
<div class="setting-item">
<p>{{ $t('settings.notification_mutes') }}</p>
<p>{{ $t('settings.notification_blocks') }}</p>
</div>
<p class="sidenote">
<ul>
<li>{{ $t('settings.notification_mutes') }}</li>
<li>{{ $t('settings.notification_blocks') }}</li>
</ul>
</p>
</div>
</template>

View file

@ -1,5 +1,6 @@
.old-theme-tab {
min-width: var(--themeEditorMinWidth, fit-content);
margin: 1em;
.deprecation-warning {
padding: 0.5em;

View file

@ -1,6 +1,6 @@
<template>
<div class="posts-tab">
<div class="setting-item">
<div class="setting-section">
<h3>{{ $t('settings.posts_appearance') }}</h3>
<ul class="setting-list">
<li>

View file

@ -1,6 +1,6 @@
.profile-tab {
// overriding global for better look
.setting-item.profile-edit {
.setting-section.profile-edit {
margin: 0;
h2 {
@ -11,5 +11,28 @@
padding: 1.2em;
overflow: hidden;
}
}
.setting-item {
.custom-boolean-setting {
display: grid;
grid-template-columns: subgrid;
.label {
grid-area: label;
text-align: right;
}
.-mobile & {
.label {
text-align: left;
}
}
.checkbox-indicator {
grid-area: control;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more