Merge branch 'settings-modal' into shigusegubu

* settings-modal:
  fixes. sorry for bad commit message i'm tired
  lint
  Move modal frame parts away from modal-content into modal, improve error handling
  moved multiChoiceProperties where it fits better
This commit is contained in:
Henry Jameson 2020-05-27 12:39:13 +03:00
commit db2d3d9c8c
16 changed files with 301 additions and 234 deletions

View file

@ -1,19 +1,17 @@
<template> <template>
<div class="error-window panel"> <div class="async-component-error">
<div class="panel-heading"> <div>
<span class="title"> <h4>
{{ $t('general.generic_error') }} {{ $t('general.generic_error') }}
</span> </h4>
</div>
<div class="panel-body">
<p> <p>
{{ $t('general.error_retry') }} {{ $t('general.error_retry') }}
</p> </p>
<button <button
class="btn" class="btn"
@click="closeAllModals" @click="retry"
> >
{{ $t('general.close') }} {{ $t('general.retry') }}
</button> </button>
</div> </div>
</div> </div>
@ -22,9 +20,7 @@
<script> <script>
export default { export default {
methods: { methods: {
closeAllModals () { retry () {
// TODO make a global hook to close all modals?
this.$store.dispatch('closeSettingsModal')
this.$emit('resetAsyncComponent') this.$emit('resetAsyncComponent')
} }
} }
@ -32,7 +28,11 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.error-window { .async-component-error {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
.btn { .btn {
margin: .5em; margin: .5em;
padding: .5em 2em; padding: .5em 2em;

View file

@ -1,16 +0,0 @@
<template>
<div class="big-spinner">
<i class="icon-spin4 animate-spin" />
</div>
</template>
<style lang="scss">
.big-spinner {
font-size: 15em;
line-height: 0;
opacity: .6;
> i {
color: white;
}
}
</style>

View file

@ -0,0 +1,29 @@
<template>
<div class="panel-loading">
<span class="loading-text">
<i class="icon-spin4 animate-spin" />
{{ $t('general.loading') }}
</span>
</div>
</template>
<style lang="scss">
@import 'src/_variables.scss';
.panel-loading {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
font-size: 2em;
color: $fallback--text;
color: var(--text, $fallback--text);
.loading-text i {
font-size: 3em;
line-height: 0;
vertical-align: middle;
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style>

View file

@ -1,6 +1,6 @@
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import BigSpinner from 'src/components/big_spinner/big_spinner.vue' import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
import ErrorWindow from 'src/components/error_window/error_window.vue' import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
import getResettableAsyncComponent from 'src/services/resettable_async_component.js' import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
const SettingsModal = { const SettingsModal = {
@ -9,13 +9,24 @@ const SettingsModal = {
SettingsModalContent: getResettableAsyncComponent( SettingsModalContent: getResettableAsyncComponent(
() => import('./settings_modal_content.vue'), () => import('./settings_modal_content.vue'),
{ {
loading: BigSpinner, loading: PanelLoading,
error: ErrorWindow, error: AsyncComponentError,
delay: 0 delay: 0
} }
) )
}, },
methods: {
closeModal () {
this.$store.dispatch('closeSettingsModal')
},
peekModal () {
this.$store.dispatch('togglePeekSettingsModal')
}
},
computed: { computed: {
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
modalActivated () { modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden' return this.$store.state.interface.settingsModalState !== 'hidden'
}, },

View file

@ -3,7 +3,7 @@
overflow: hidden; overflow: hidden;
&.peek { &.peek {
.modal-panel { .settings-modal-panel {
/* Explanation: /* Explanation:
* Modal is positioned vertically centered. * Modal is positioned vertically centered.
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen * 100vh - 100% = Distance between modal's top+bottom boundaries and screen
@ -15,4 +15,30 @@
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px)); transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
} }
} }
.settings-modal-panel {
overflow: hidden;
transition: transform;
transition-timing-function: ease-in-out;
transition-duration: 300ms;
width: 1000px;
max-width: 90vw;
height: 90vh;
@media all and (max-width: 800px) {
max-width: 100vw;
height: 100vh;
}
.panel-body {
height: 100%;
overflow-y: hidden;
.btn {
min-height: 28px;
min-width: 10em;
padding: 0 2em;
}
}
}
} }

View file

@ -5,10 +5,47 @@
:class="{ peek: modalPeeked }" :class="{ peek: modalPeeked }"
:no-background="modalPeeked" :no-background="modalPeeked"
> >
<SettingsModalContent <div class="settings-modal-panel panel">
v-if="modalActivated" <div class="panel-heading">
class="modal-panel" <span class="title">
/> {{ $t('settings.settings') }}
</span>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div
v-if="currentSaveStateNotice.error"
class="alert error"
@click.prevent
>
{{ $t('settings.saving_err') }}
</div>
<div
v-if="!currentSaveStateNotice.error"
class="alert transparent"
@click.prevent
>
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
<button
class="btn"
@click="peekModal"
>
{{ $t('general.peek') }}
</button>
<button
class="btn"
@click="closeModal"
>
{{ $t('general.close') }}
</button>
</div>
<div class="panel-body">
<SettingsModalContent v-if="modalActivated" />
</div>
</div>
</Modal> </Modal>
</template> </template>

View file

@ -25,20 +25,9 @@ const SettingsModalContent = {
ThemeTab ThemeTab
}, },
computed: { computed: {
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
isLoggedIn () { isLoggedIn () {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
} }
},
methods: {
closeModal () {
this.$store.dispatch('closeSettingsModal')
},
peekModal () {
this.$store.dispatch('togglePeekSettingsModal')
}
} }
} }

View file

@ -1,32 +1,6 @@
@import 'src/_variables.scss'; @import 'src/_variables.scss';
.settings_tab-switcher {
.settings-modal-panel {
overflow: hidden;
transition: transform;
transition-timing-function: ease-in-out;
transition-duration: 300ms;
width: 1000px;
max-width: 90vw;
height: 90vh;
@media all and (max-width: 800px) {
max-width: 100vw;
height: 100vh;
}
.settings_tab-switcher {
height: 100%; height: 100%;
}
.panel-body {
height: 100%;
overflow-y: hidden;
.btn {
min-height: 28px;
min-width: 10em;
padding: 0 2em;
}
}
.full-height { .full-height {
height: 100%; height: 100%;

View file

@ -1,42 +1,4 @@
<template> <template>
<div class="settings-modal-panel panel">
<div class="panel-heading">
<span class="title">
{{ $t('settings.settings') }}
</span>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div
v-if="currentSaveStateNotice.error"
class="alert error"
@click.prevent
>
{{ $t('settings.saving_err') }}
</div>
<div
v-if="!currentSaveStateNotice.error"
class="alert transparent"
@click.prevent
>
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
<button
class="btn"
@click="peekModal"
>
{{ $t('general.peek') }}
</button>
<button
class="btn"
@click="closeModal"
>
{{ $t('general.close') }}
</button>
</div>
<div class="panel-body">
<tab-switcher <tab-switcher
ref="tabSwitcher" ref="tabSwitcher"
class="settings_tab-switcher" class="settings_tab-switcher"
@ -78,7 +40,7 @@
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:label="$t('settings.notifications')" :label="$t('settings.notifications')"
icon="chat" icon="bell-ringing-o"
> >
<NotificationsTab /> <NotificationsTab />
</div> </div>
@ -105,8 +67,6 @@
<VersionTab /> <VersionTab />
</div> </div>
</tab-switcher> </tab-switcher>
</div>
</div>
</template> </template>
<script src="./settings_modal_content.js"></script> <script src="./settings_modal_content.js"></script>

View file

@ -1,10 +1,9 @@
import { filter, trim } from 'lodash' import { filter, trim } from 'lodash'
import { instanceDefaultProperties, defaultState as configDefaultState } from 'src/modules/config.js' import {
instanceDefaultProperties,
const multiChoiceProperties = [ multiChoiceProperties,
'postContentType', defaultState as configDefaultState
'subjectLineBehavior' } from 'src/modules/config.js'
]
const SharedComputedObject = () => ({ const SharedComputedObject = () => ({
user () { user () {

View file

@ -95,20 +95,25 @@
align-items: baseline; align-items: baseline;
width: 100%; width: 100%;
min-height: 30px; min-height: 30px;
margin-bottom: 1em;
.btn {
min-width: 1px;
flex: 0 auto;
padding: 0 1em;
}
p { p {
flex: 1; flex: 1;
margin: 0; margin: 0;
margin-right: .5em; margin-right: .5em;
} }
}
margin-bottom: 1em; .tab-header-buttons {
display: flex;
flex-direction: column;
.btn {
min-width: 1px;
flex: 0 auto;
padding: 0 1em;
margin-bottom: .5em;
}
} }
.shadow-selector { .shadow-selector {

View file

@ -126,6 +126,7 @@
> >
<div class="tab-header"> <div class="tab-header">
<p>{{ $t('settings.theme_help') }}</p> <p>{{ $t('settings.theme_help') }}</p>
<div class="tab-header-buttons">
<button <button
class="btn" class="btn"
@click="clearOpacity" @click="clearOpacity"
@ -139,6 +140,7 @@
{{ $t('settings.style.switcher.clear_all') }} {{ $t('settings.style.switcher.clear_all') }}
</button> </button>
</div> </div>
</div>
<p>{{ $t('settings.theme_help_v2_1') }}</p> <p>{{ $t('settings.theme_help_v2_1') }}</p>
<h4>{{ $t('settings.style.common_colors.main') }}</h4> <h4>{{ $t('settings.style.common_colors.main') }}</h4>
<div class="color-item"> <div class="color-item">

View file

@ -95,9 +95,12 @@ export default Vue.component('tab-switcher', {
disabled={slot.data.attrs.disabled} disabled={slot.data.attrs.disabled}
onClick={this.activateTab(index)} onClick={this.activateTab(index)}
class={classesTab.join(' ')} class={classesTab.join(' ')}
type="button"
> >
{!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)} {!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)}
<span class="text">
{slot.data.attrs.label} {slot.data.attrs.label}
</span>
</button> </button>
</div> </div>
) )
@ -110,13 +113,23 @@ export default Vue.component('tab-switcher', {
if (slot.data.attrs.fullHeight) { if (slot.data.attrs.fullHeight) {
classes.push('full-height') classes.push('full-height')
} }
const newSlot = (
<div class={classes}>
{
this.sideTabBar
? <h1 class="mobile-label">{slot.data.attrs.label}</h1>
: ''
}
{slot}
</div>
)
if (this.renderOnlyFocused) { if (this.renderOnlyFocused) {
return active return active
? <div class={classes.join(' ')}>{slot}</div> ? <div class={classes.join(' ')}>{newSlot}</div>
: <div class={classes.join(' ')}></div> : <div class={classes.join(' ')}></div>
} }
return <div class={classes.join(' ')}>{slot}</div> return <div class={classes.join(' ')}>{newSlot}</div>
}) })
return ( return (

View file

@ -10,12 +10,14 @@
&.top-tabs { &.top-tabs {
flex-direction: column; flex-direction: column;
> .tabs { > .tabs {
width: 100%; width: 100%;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
padding-top: 5px; padding-top: 5px;
flex-direction: row; flex-direction: row;
&::after, &::before { &::after, &::before {
content: ''; content: '';
flex: 1 1 auto; flex: 1 1 auto;
@ -51,34 +53,47 @@
&.side-tabs { &.side-tabs {
flex-direction: row; flex-direction: row;
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
overflow-x: auto; overflow-x: auto;
} }
> .contents { > .contents {
flex: 0 1 80%; flex: 1 1 auto;
@media all and (max-width: 800px) {
min-width: 96vw;
}
} }
> .tabs { > .tabs {
flex: 1 0 auto; flex: 0 0 auto;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
flex-direction: column; flex-direction: column;
&::after, &::before { &::after, &::before {
flex: 1 0 .5em; flex-shrink: 0;
flex-basis: .5em;
content: ''; content: '';
border-right: 1px solid; border-right: 1px solid;
border-right-color: $fallback--border; border-right-color: $fallback--border;
border-right-color: var(--border, $fallback--border); border-right-color: var(--border, $fallback--border);
} }
&::after {
flex-grow: 1;
}
&::before { &::before {
flex-grow: 0; flex-grow: 0;
} }
.tab-wrapper { .tab-wrapper {
min-width: 10em; min-width: 10em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@media all and (max-width: 800px) {
min-width: 1em;
}
&:not(.active)::after { &:not(.active)::after {
top: 0; top: 0;
right: 0; right: 0;
@ -87,6 +102,7 @@
border-right-color: $fallback--border; border-right-color: $fallback--border;
border-right-color: var(--border, $fallback--border); border-right-color: var(--border, $fallback--border);
} }
&::before { &::before {
flex: 0 0 6px; flex: 0 0 6px;
content: ''; content: '';
@ -94,6 +110,7 @@
border-right-color: $fallback--border; border-right-color: $fallback--border;
border-right-color: var(--border, $fallback--border); border-right-color: var(--border, $fallback--border);
} }
&:last-child .tab { &:last-child .tab {
margin-bottom: 0; margin-bottom: 0;
} }
@ -106,25 +123,22 @@
min-width: 1px; min-width: 1px;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
padding-left: 1em;
padding-right: calc(1em + 200px); padding-right: calc(1em + 200px);
margin-right: 6px - 200px; margin-right: calc(1em - 200px);
margin-left: 6px; margin-left: 1em;
} @media all and (max-width: 800px) {
padding-left: .25em;
.tab-wrapper { padding-right: calc(.25em + 200px);
min-width: 10em; margin-right: calc(.25em - 200px);
&:not(.active)::after { margin-left: .25em;
top: 0; .text {
right: 0; display: none
bottom: 0; }
border-right: 1px solid;
border-right-color: $fallback--border;
border-right-color: var(--border, $fallback--border);
} }
} }
} }
} }
.contents { .contents {
flex: 1 0 auto; flex: 1 0 auto;
@ -142,14 +156,14 @@
.tab { .tab {
position: relative; position: relative;
white-space: nowrap; white-space: nowrap;
padding: 6px 1em; padding: 6px 1em;
background-color: $fallback--fg;
background-color: var(--tab, $fallback--fg);
&, &:active .tab-icon { &, &:active .tab-icon {
color: $fallback--text; color: $fallback--text;
color: var(--tabText, $fallback--text); color: var(--tabText, $fallback--text);
} }
background-color: $fallback--fg;
background-color: var(--tab, $fallback--fg);
&:not(.active) { &:not(.active) {
z-index: 4; z-index: 4;
@ -173,7 +187,6 @@
} }
} }
.tabs { .tabs {
display: flex; display: flex;
position: relative; position: relative;
@ -198,4 +211,17 @@
} }
} }
} }
.mobile-label {
padding-left: .3em;
padding-bottom: .25em;
margin-top: .5em;
margin-left: .2em;
margin-bottom: .25em;
border-bottom: 1px solid var(--border, $fallback--border);
@media all and (min-width: 800px) {
display: none;
}
}
} }

View file

@ -59,8 +59,10 @@
"apply": "Apply", "apply": "Apply",
"submit": "Submit", "submit": "Submit",
"more": "More", "more": "More",
"loading": "Loading…",
"generic_error": "An error occured", "generic_error": "An error occured",
"error_retry": "Please try again", "error_retry": "Please try again",
"retry": "Try again",
"optional": "optional", "optional": "optional",
"show_more": "Show more", "show_more": "Show more",
"show_less": "Show less", "show_less": "Show less",

View file

@ -3,6 +3,16 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
const browserLocale = (window.navigator.language || 'en').split('-')[0] const browserLocale = (window.navigator.language || 'en').split('-')[0]
/* TODO this is a bit messy.
* We need to declare settings with their types and also deal with
* instance-default settings in some way, hopefully try to avoid copy-pasta
* in general.
*/
export const multiChoiceProperties = [
'postContentType',
'subjectLineBehavior'
]
export const defaultState = { export const defaultState = {
colors: {}, colors: {},
theme: undefined, theme: undefined,