Add support for dark mode in Corteza studio
This commit is contained in:
parent
58691c11e6
commit
0095663f60
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
@ -55,6 +55,11 @@ export default (options = {}) => {
|
||||
this.$i18n.i18next.changeLanguage(user.meta.preferredLanguage)
|
||||
}
|
||||
|
||||
// switch the webapp theme based on user preference
|
||||
if (user.meta.theme) {
|
||||
document.getElementsByTagName('html')[0].setAttribute('data-color-mode', user.meta.theme)
|
||||
}
|
||||
|
||||
// ref to vue is needed inside compose helper
|
||||
// load and register bundle and list of client/server scripts
|
||||
const bundleLoaderOpt = {
|
||||
|
||||
@ -19,25 +19,52 @@
|
||||
<a :href="installSassDocs">{{ $t('installSassDocs') }}</a>
|
||||
</div>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
v-for="key in Object.keys(brandingVariables)"
|
||||
:key="key"
|
||||
md="6"
|
||||
cols="12"
|
||||
<b-tabs
|
||||
data-test-id="theme-tabs"
|
||||
nav-wrapper-class="bg-white white border-bottom rounded-0"
|
||||
card
|
||||
>
|
||||
<b-tab
|
||||
v-for="theme in themes"
|
||||
:key="theme.id"
|
||||
:title="$t(`tabs.${theme.id}`)"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t(`brandVariables.${key}`)"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-color-picker
|
||||
v-model="brandingVariables[key]"
|
||||
:data-test-id="`input-${key}-color`"
|
||||
:translations="colorTranslations"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col
|
||||
v-for="(key, index) in themeInputs"
|
||||
:key="key"
|
||||
md="6"
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t(`theme.values.${key}`)"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-color-picker
|
||||
ref="picker"
|
||||
v-model="theme.values[key]"
|
||||
:data-test-id="`input-${key}-color`"
|
||||
:translations="{
|
||||
modalTitle: $t('colorPicker'),
|
||||
cancelBtnLabel: $t('general:label.cancel'),
|
||||
saveBtnLabel: $t('general:label.saveAndClose')
|
||||
}"
|
||||
>
|
||||
<template v-slot:footer>
|
||||
<b-button
|
||||
variant="outline-primary"
|
||||
class="mr-auto"
|
||||
@click="resetColor(key, index, theme.id)"
|
||||
>
|
||||
{{ $t('label.default') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</c-input-color-picker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
@ -92,7 +119,22 @@ export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
brandingVariables: {
|
||||
themeInputs: [
|
||||
'white',
|
||||
'black',
|
||||
'primary',
|
||||
'secondary',
|
||||
'success',
|
||||
'warning',
|
||||
'danger',
|
||||
'light',
|
||||
'extra-light',
|
||||
'dark',
|
||||
'tertiary',
|
||||
'gray-200',
|
||||
'body-bg',
|
||||
],
|
||||
lightModeVariables: {
|
||||
'white': '#FFFFFF',
|
||||
'black': '#162425',
|
||||
'primary': '#0B344E',
|
||||
@ -107,10 +149,22 @@ export default {
|
||||
'gray-200': '#F9FAFB',
|
||||
'body-bg': '#F9FAFB',
|
||||
},
|
||||
colorTranslations: {
|
||||
modalTitle: this.$t('colorPicker'),
|
||||
saveBtnLabel: this.$t('general:label.saveAndClose'),
|
||||
darkModeVariables: {
|
||||
'white': '#162425',
|
||||
'black': '#FFFFFF',
|
||||
'primary': '#43AA8B',
|
||||
'secondary': '#E4E9EF',
|
||||
'success': '#E2A046',
|
||||
'warning': '#758D9B',
|
||||
'danger': '#E54122',
|
||||
'light': '#5E727E',
|
||||
'extra-light': '#F3F5F7',
|
||||
'dark': '#0B344E',
|
||||
'tertiary': '#F9FAFB',
|
||||
'gray-200': '#162425',
|
||||
'body-bg': '#162425',
|
||||
},
|
||||
themes: [],
|
||||
}
|
||||
},
|
||||
|
||||
@ -129,8 +183,24 @@ export default {
|
||||
settings: {
|
||||
immediate: true,
|
||||
handler (settings) {
|
||||
if (settings['ui.studio.branding-sass']) {
|
||||
this.brandingVariables = JSON.parse(settings['ui.studio.branding-sass'])
|
||||
if (settings['ui.studio.themes']) {
|
||||
this.themes = settings['ui.studio.themes'].map(theme => {
|
||||
theme.values = JSON.parse(theme.values)
|
||||
return theme
|
||||
})
|
||||
} else {
|
||||
this.themes = [
|
||||
{
|
||||
id: 'light',
|
||||
title: this.$t('light'),
|
||||
values: this.lightModeVariables,
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
title: this.$t('dark'),
|
||||
values: this.darkModeVariables,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -138,8 +208,20 @@ export default {
|
||||
|
||||
methods: {
|
||||
onSubmit () {
|
||||
this.$emit('submit', { 'ui.studio.branding-sass': JSON.stringify(this.brandingVariables) })
|
||||
this.$emit('submit', { 'ui.studio.themes': this.themes.map(theme => {
|
||||
theme.values = JSON.stringify(theme.values)
|
||||
return theme
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
resetColor (key, index, mode) {
|
||||
this.themes.forEach(theme => {
|
||||
theme.values[key] = mode === 'light' ? this.lightModeVariables[key] : this.darkModeVariables[key]
|
||||
})
|
||||
this.$refs.picker[index].closeMenu()
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -12,39 +12,51 @@
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<c-ace-editor
|
||||
v-model="customCSS"
|
||||
lang="css"
|
||||
height="300px"
|
||||
font-size="14px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="true"
|
||||
@open="openEditorModal"
|
||||
/>
|
||||
|
||||
<b-modal
|
||||
id="custom-css-editor"
|
||||
v-model="showEditorModal"
|
||||
:title="$t('modal.editor')"
|
||||
cancel-variant="link"
|
||||
size="lg"
|
||||
:ok-title="$t('general:label.saveAndClose')"
|
||||
:cancel-title="$t('general:label.cancel')"
|
||||
body-class="p-0"
|
||||
@ok="saveCustomCSSInput"
|
||||
@hidden="resetCustomCSSInput"
|
||||
<b-tabs
|
||||
data-test-id="theme-tabs"
|
||||
nav-wrapper-class="bg-white white border-bottom rounded-0"
|
||||
card
|
||||
>
|
||||
<c-ace-editor
|
||||
v-model="modalCSSInput"
|
||||
lang="scss"
|
||||
height="500px"
|
||||
font-size="14px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="false"
|
||||
/>
|
||||
</b-modal>
|
||||
<b-tab
|
||||
v-for="theme in themes"
|
||||
:key="theme.id"
|
||||
:title="$t(`tabs.${theme.id}`)"
|
||||
>
|
||||
<c-ace-editor
|
||||
v-model="theme.values"
|
||||
lang="css"
|
||||
height="300px"
|
||||
font-size="14px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="true"
|
||||
@open="openEditorModal(theme.id)"
|
||||
/>
|
||||
|
||||
<b-modal
|
||||
id="custom-css-editor"
|
||||
v-model="theme.showEditorModal"
|
||||
:title="$t('modal.editor')"
|
||||
cancel-variant="link"
|
||||
size="lg"
|
||||
:ok-title="$t('general:label.saveAndClose')"
|
||||
:cancel-title="$t('general:label.cancel')"
|
||||
body-class="p-0"
|
||||
@ok="saveCustomCSSInput(theme.id)"
|
||||
@hidden="resetCustomCSSInput(theme.id)"
|
||||
>
|
||||
<c-ace-editor
|
||||
v-model="theme.modalValue"
|
||||
lang="scss"
|
||||
height="500px"
|
||||
font-size="14px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="false"
|
||||
/>
|
||||
</b-modal>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
@ -99,9 +111,29 @@ export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
customCSS: '',
|
||||
modalCSSInput: undefined,
|
||||
showEditorModal: false,
|
||||
themes: [
|
||||
{
|
||||
'id': 'general',
|
||||
'title': 'General',
|
||||
'values': '',
|
||||
'modalValue': undefined,
|
||||
showEditorModal: false,
|
||||
},
|
||||
{
|
||||
'id': 'light',
|
||||
'title': 'Light mode',
|
||||
'values': '',
|
||||
'modalValue': undefined,
|
||||
showEditorModal: false,
|
||||
},
|
||||
{
|
||||
'id': 'dark',
|
||||
'title': 'Dark mode',
|
||||
'values': '',
|
||||
'modalValue': undefined,
|
||||
showEditorModal: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
@ -109,27 +141,57 @@ export default {
|
||||
settings: {
|
||||
immediate: true,
|
||||
handler (settings) {
|
||||
this.customCSS = settings['ui.custom-css'] || ''
|
||||
if (settings['ui.studio.custom-css']) {
|
||||
this.themes = settings['ui.studio.custom-css'].map((theme) => {
|
||||
return {
|
||||
id: theme.id,
|
||||
title: theme.title,
|
||||
values: theme.values,
|
||||
modalValue: undefined,
|
||||
showEditorModal: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit () {
|
||||
this.$emit('submit', { 'ui.custom-css': this.customCSS })
|
||||
this.$emit('submit', {
|
||||
'ui.studio.custom-css': this.themes.map((theme) => {
|
||||
return {
|
||||
id: theme.id,
|
||||
title: theme.title,
|
||||
values: theme.values,
|
||||
}
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
openEditorModal () {
|
||||
this.modalCSSInput = this.customCSS
|
||||
this.showEditorModal = true
|
||||
openEditorModal (themeId) {
|
||||
this.themes.forEach((theme) => {
|
||||
if (theme.id === themeId) {
|
||||
theme.modalValue = theme.values
|
||||
theme.showEditorModal = true
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
saveCustomCSSInput () {
|
||||
this.customCSS = this.modalCSSInput
|
||||
saveCustomCSSInput (themeId) {
|
||||
this.themes.forEach((theme) => {
|
||||
if (theme.id === themeId) {
|
||||
theme.values = theme.modalValue
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
resetCustomCSSInput () {
|
||||
this.modalCSSInput = undefined
|
||||
resetCustomCSSInput (themeId) {
|
||||
this.themes.forEach((theme) => {
|
||||
if (theme.id === themeId) {
|
||||
theme.modalValue = undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@ -139,8 +139,8 @@ export default {
|
||||
.finally(() => {
|
||||
this[type].processing = false
|
||||
|
||||
// Refresh the page if branding or custom CSS was updated
|
||||
if (type === 'branding' || type === 'customCSS') {
|
||||
// Refresh the page if branding variables is updated and sass installed or custom CSS was updated
|
||||
if ((type === 'branding' && this.settings['ui.studio.sass-installed']) || type === 'customCSS') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-color-mode="light" lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
@ -1442,6 +1442,7 @@ export default class System {
|
||||
handle,
|
||||
kind,
|
||||
labels,
|
||||
meta,
|
||||
updatedAt,
|
||||
} = (a as KV) || {}
|
||||
if (!userID) {
|
||||
@ -1463,6 +1464,7 @@ export default class System {
|
||||
handle,
|
||||
kind,
|
||||
labels,
|
||||
meta,
|
||||
updatedAt,
|
||||
}
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
|
||||
@ -15,6 +15,7 @@ interface UserMeta {
|
||||
avatarKind?: string;
|
||||
avatarColor?: string;
|
||||
avatarBgColor?: string;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
interface SecurityPolicy {
|
||||
@ -46,6 +47,7 @@ export class User {
|
||||
avatarKind: '',
|
||||
avatarColor: '',
|
||||
avatarBgColor: '',
|
||||
theme: '',
|
||||
}
|
||||
|
||||
public canGrant = false
|
||||
|
||||
@ -170,6 +170,16 @@
|
||||
>
|
||||
{{ labels.userSettingsChangePassword }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown
|
||||
id="theme-dropleft"
|
||||
dropleft
|
||||
text="Theme"
|
||||
variant="outline-link"
|
||||
class="ml-2"
|
||||
>
|
||||
<b-dropdown-item @click="saveThemeMode('light')">Light</b-dropdown-item>
|
||||
<b-dropdown-item @click="saveThemeMode('dark')">Dark</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
<b-dropdown-divider />
|
||||
<b-dropdown-item
|
||||
data-test-id="dropdown-profile-logout"
|
||||
@ -185,7 +195,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faMoon, faSun} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faMoon, faSun)
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isThemeDropdownVisible: false,
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
sidebarPinned: {
|
||||
type: Boolean,
|
||||
@ -261,7 +282,35 @@ export default {
|
||||
avatarExists () {
|
||||
return this.$auth.user.meta.avatarID !== "0" && this.$auth.user.meta.avatarID
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$root.$on('bv::dropdown::show', bvEvent => {
|
||||
if(bvEvent.componentId === 'theme-dropleft') {
|
||||
this.isThemeDropdownVisible = true;
|
||||
}
|
||||
})
|
||||
this.$root.$on('bv::dropdown::hide', bvEvent => {
|
||||
if(bvEvent.componentId === 'theme-dropleft') {
|
||||
this.isThemeDropdownVisible = false;
|
||||
}
|
||||
if(this.isThemeDropdownVisible) {
|
||||
bvEvent.preventDefault()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveThemeMode (mode) {
|
||||
this.$auth.user.meta.theme = mode
|
||||
|
||||
this.$SystemAPI.userUpdate(this.$auth.user)
|
||||
.then(() => {
|
||||
document.getElementsByTagName('html')[0].setAttribute('data-color-mode', mode)
|
||||
})
|
||||
.catch(console.error)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ interface OAuth2TokenResponse {
|
||||
email?: string;
|
||||
preferred_language?: string;
|
||||
avatarID?: string;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
interface PluginOpts {
|
||||
@ -583,6 +584,9 @@ export class Auth {
|
||||
|
||||
u.meta.avatarID = oa2tkn.avatarID
|
||||
|
||||
// theme
|
||||
u.meta.theme = oa2tkn.theme
|
||||
|
||||
this[accessToken] = oa2tkn.access_token
|
||||
this[user] = u
|
||||
|
||||
|
||||
@ -27,25 +27,35 @@ editor:
|
||||
colorPicker: Choose a color
|
||||
sassNotInstalled: No dart-sass binaries were detected. For guidance on how to set it up, refer to
|
||||
installSassDocs: docs here
|
||||
brandVariables:
|
||||
white: White
|
||||
black: Black
|
||||
primary: Primary
|
||||
secondary: Secondary
|
||||
success: Success
|
||||
warning: Warning
|
||||
danger: Danger
|
||||
label:
|
||||
default: Default
|
||||
tabs:
|
||||
light: Light
|
||||
extra-light: Extra light
|
||||
dark: Dark
|
||||
tertiary: Tertiary
|
||||
gray-200: Gray
|
||||
body-bg: Body background
|
||||
dark: Dark
|
||||
theme:
|
||||
values:
|
||||
white: White
|
||||
black: Black
|
||||
primary: Primary
|
||||
secondary: Secondary
|
||||
success: Success
|
||||
warning: Warning
|
||||
danger: Danger
|
||||
light: Light
|
||||
extra-light: Extra light
|
||||
dark: Dark
|
||||
tertiary: Tertiary
|
||||
gray-200: Gray
|
||||
body-bg: Body background
|
||||
|
||||
custom-css:
|
||||
title: Custom CSS
|
||||
tabs:
|
||||
general: General
|
||||
light: Light
|
||||
dark: Dark
|
||||
modal:
|
||||
editor: Custom CSS editor
|
||||
editor: Custom CSS editor
|
||||
|
||||
topbar:
|
||||
title: Topbar
|
||||
|
||||
@ -34,6 +34,7 @@ import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/pkg/provision"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sass"
|
||||
"github.com/cortezaproject/corteza/server/pkg/scheduler"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sentry"
|
||||
"github.com/cortezaproject/corteza/server/pkg/valuestore"
|
||||
@ -379,6 +380,7 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) {
|
||||
RBAC: app.Opt.RBAC,
|
||||
Limit: app.Opt.Limit,
|
||||
Attachment: app.Opt.Attachment,
|
||||
Webapps: app.Opt.Webapp,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
@ -555,8 +557,15 @@ func (app *CortezaApp) Activate(ctx context.Context) (err error) {
|
||||
updateDiscoverySettings(app.Opt.Discovery, service.CurrentSettings)
|
||||
updateLocaleSettings(app.Opt.Locale)
|
||||
|
||||
updateSassInstallSettings(ctx, app.Log)
|
||||
|
||||
app.AuthService.Watch(ctx)
|
||||
|
||||
//Generate CSS for webapps
|
||||
if err = service.GenerateCSS(sysService.CurrentSettings, app.Opt.Webapp.ScssDirPath); err != nil {
|
||||
return fmt.Errorf("could not generate css for webapps: %w", err)
|
||||
}
|
||||
|
||||
// messagebus reloader and consumer listeners
|
||||
if app.Opt.Messagebus.Enabled {
|
||||
|
||||
@ -957,6 +966,20 @@ func updateSmtpSettings(log *zap.Logger, current *types.AppSettings) {
|
||||
setupSmtpDialer(log, current.SMTP.Servers...)
|
||||
}
|
||||
|
||||
func updateSassInstallSettings(ctx context.Context, log *zap.Logger) {
|
||||
var sassInstalled bool
|
||||
|
||||
if sass.DartSassTranspiler(log) != nil {
|
||||
sassInstalled = true
|
||||
}
|
||||
|
||||
// update dart-sass installed setting
|
||||
err := updateSetting(ctx, "ui.studio.sass-installed", sassInstalled)
|
||||
if err != nil {
|
||||
log.Warn("failed to set ui.studio.sass-installed setting", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func setupSmtpDialer(log *zap.Logger, servers ...types.SmtpServers) {
|
||||
if len(servers) == 0 {
|
||||
log.Warn("no SMTP servers found, email sending will be disabled")
|
||||
|
||||
@ -57,7 +57,7 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
return
|
||||
}
|
||||
|
||||
r.Route(options.CleanBase(ho.WebappBaseUrl), webapp.MakeWebappServer(app.Log, ho, app.Opt.Auth, app.Opt.Discovery, app.Opt.Sentry, app.Opt.Webapp))
|
||||
r.Route(options.CleanBase(ho.WebappBaseUrl), webapp.MakeWebappServer(app.Log, ho, app.Opt.Auth, app.Opt.Discovery, app.Opt.Sentry))
|
||||
|
||||
app.Log.Info(
|
||||
"client web applications enabled",
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
// Do not forget to update getting-started/theming.md!
|
||||
:root {
|
||||
// Custom variable values only support SassScript inside `#{}`.
|
||||
@each $color, $value in $colors {
|
||||
--#{$color}: #{$value};
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
--#{$color}: #{$value};
|
||||
}
|
||||
|
||||
@each $bp, $value in $grid-breakpoints {
|
||||
--breakpoint-#{$bp}: #{$value};
|
||||
}
|
||||
|
||||
@each $variable, $value in $corteza-specific {
|
||||
--#{$variable}: #{$value};
|
||||
}
|
||||
|
||||
// Use `inspect` for lists so that quoted items keep the quotes.
|
||||
// See https://github.com/sass/sass/issues/2383#issuecomment-336349172
|
||||
--font-family-sans-serif: #{inspect($font-family-sans-serif)};
|
||||
--font-family-monospace: #{inspect($font-family-monospace)};
|
||||
}
|
||||
@ -49,4 +49,4 @@
|
||||
.alert-#{$color} {
|
||||
@include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level));
|
||||
}
|
||||
}
|
||||
}
|
||||
47
server/assets/src/scss/root/root.scss
Normal file
47
server/assets/src/scss/root/root.scss
Normal file
@ -0,0 +1,47 @@
|
||||
// Do not forget to update getting-started/theming.md!
|
||||
@if $theme-mode == "light" {
|
||||
/* stylelint-disable */
|
||||
// This is used within corteza-webapp-compose/src/lib/block/Calendar/feedLoader/
|
||||
// to determine default event colors
|
||||
:export {
|
||||
primary: $primary;
|
||||
danger: $danger;
|
||||
secondary: $secondary;
|
||||
}
|
||||
|
||||
/* stylelint-enable */
|
||||
:root,
|
||||
[data-color-mode="light"] {
|
||||
// Custom variable values only support SassScript inside `#{}`.
|
||||
@each $color, $value in $colors {
|
||||
--#{$color}: #{$value};
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
--#{$color}: #{$value};
|
||||
}
|
||||
|
||||
@each $bp, $value in $grid-breakpoints {
|
||||
--breakpoint-#{$bp}: #{$value};
|
||||
}
|
||||
|
||||
@each $variable, $value in $corteza-specific {
|
||||
--#{$variable}: #{$value};
|
||||
}
|
||||
|
||||
// Use `inspect` for lists so that quoted items keep the quotes.
|
||||
// See https://github.com/sass/sass/issues/2383#issuecomment-336349172
|
||||
--font-family-sans-serif: #{inspect($font-family-sans-serif)};
|
||||
--font-family-monospace: #{inspect($font-family-monospace)};
|
||||
}
|
||||
} @else {
|
||||
[data-color-mode="#{$theme-mode}"] {
|
||||
@each $color, $value in $theme-colors {
|
||||
--#{$color}: #{$value};
|
||||
}
|
||||
|
||||
@each $variable, $value in $corteza-specific {
|
||||
--#{$variable}: #{$value};
|
||||
}
|
||||
}
|
||||
}
|
||||
1717
server/assets/src/scss/theme/theme-specific.scss
Normal file
1717
server/assets/src/scss/theme/theme-specific.scss
Normal file
File diff suppressed because it is too large
Load Diff
@ -132,13 +132,3 @@ $corteza-specific: map-merge(
|
||||
),
|
||||
$corteza-specific
|
||||
);
|
||||
|
||||
/* stylelint-disable */
|
||||
// This is used within corteza-webapp-compose/src/lib/block/Calendar/feedLoader/
|
||||
// to determine default event colors
|
||||
:export {
|
||||
primary: $primary;
|
||||
danger: $danger;
|
||||
secondary: $secondary;
|
||||
}
|
||||
/* stylelint-enable */
|
||||
@ -1399,3 +1399,110 @@ $rfs-breakpoint-unit-cache: unit($rfs-breakpoint);
|
||||
@include deprecate("The `float-none` mixin", "v4.3.0", "v5");
|
||||
}
|
||||
|
||||
//bootstrap-vue
|
||||
$b-toast-background-opacity: alpha($toast-background-color) !default;
|
||||
@mixin bv-custom-range-validation-state($state, $color) {
|
||||
.input-group .custom-range {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
border-color: $color;
|
||||
|
||||
&:focus {
|
||||
border-color: $color;
|
||||
box-shadow: 0 0 0 $input-focus-width rgba($color, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-range {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
// Pseudo-elements must be split across multiple rulesets to have an affect
|
||||
&:focus {
|
||||
&::-webkit-slider-thumb {
|
||||
box-shadow: 0 0 0 1px $body-bg, 0 0 0 $input-btn-focus-width lighten($color, 35%);
|
||||
}
|
||||
&::-moz-range-thumb {
|
||||
box-shadow: 0 0 0 1px $body-bg, 0 0 0 $input-btn-focus-width lighten($color, 35%);
|
||||
}
|
||||
&::-ms-thumb {
|
||||
box-shadow: 0 0 0 1px $body-bg, 0 0 0 $input-btn-focus-width lighten($color, 35%);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
background-color: $color;
|
||||
background-image: none;
|
||||
|
||||
&:active {
|
||||
background-color: lighten($color, 35%);
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background-color: rgba($color, 0.35);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background-color: $color;
|
||||
background-image: none;
|
||||
|
||||
&:active {
|
||||
background-color: lighten($color, 35%);
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
background: rgba($color, 0.35);
|
||||
}
|
||||
|
||||
~ .#{$state}-feedback,
|
||||
~ .#{$state}-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
background-color: $color;
|
||||
background-image: none;
|
||||
|
||||
&:active {
|
||||
background-color: lighten($color, 35%);
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
&::-ms-track-lower {
|
||||
background: rgba($color, 0.35);
|
||||
}
|
||||
&::-ms-track-upper {
|
||||
background: rgba($color, 0.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@mixin b-toast-variant($background, $border, $color) {
|
||||
// Based on alert-variant mixin
|
||||
.toast {
|
||||
background-color: rgba(lighten($background, 5%), $b-toast-background-opacity);
|
||||
border-color: rgba($border, $b-toast-background-opacity);
|
||||
color: $color;
|
||||
|
||||
.toast-header {
|
||||
color: $color;
|
||||
background-color: rgba($background, $b-toast-background-opacity);
|
||||
border-bottom-color: rgba($border, $b-toast-background-opacity);
|
||||
}
|
||||
|
||||
// .toast-body[href] {
|
||||
// color: darken($color, 10%);
|
||||
// }
|
||||
}
|
||||
|
||||
&.b-toast-solid {
|
||||
.toast {
|
||||
background-color: rgba(lighten($background, 5%), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -445,6 +445,9 @@ func (h AuthHandlers) handleTokenRequest(req *request.AuthReq, client *types.Aut
|
||||
response["avatarID"] = strconv.FormatUint(user.Meta.AvatarID, 10)
|
||||
}
|
||||
|
||||
//include user's theme
|
||||
response["theme"] = user.Meta.Theme
|
||||
|
||||
// in case client is configured with "openid" scope,
|
||||
// we'll add "id_token" with all required (by OIDC) details encoded
|
||||
if strings.Contains(client.Scope, "openid") {
|
||||
|
||||
@ -41,6 +41,7 @@ func Run(ctx context.Context, log *zap.Logger, s store.Storer, provisionOpt opti
|
||||
func() error { return defaultAuthClient(ctx, log.Named("auth.clients"), s, authOpt) },
|
||||
func() error { return addAuthSuperUsers(ctx, log.Named("auth.super-users"), s, authOpt) },
|
||||
func() error { return invalidateDedupRules(ctx, log.Named("compose.deduplication"), s) },
|
||||
func() error { return updateWebappTheme(ctx, log.Named("webapp.themes"), s) },
|
||||
}
|
||||
|
||||
for _, fn := range ffn {
|
||||
|
||||
97
server/pkg/provision/stylesheet.go
Normal file
97
server/pkg/provision/stylesheet.go
Normal file
@ -0,0 +1,97 @@
|
||||
package provision
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func updateWebappTheme(ctx context.Context, log *zap.Logger, s store.Storer) (err error) {
|
||||
vv, _, err := store.SearchSettingValues(ctx, s, types.SettingsFilter{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldCustomCSS := vv.FindByName("ui.custom-css")
|
||||
oldBranding := vv.FindByName("ui.studio.branding-sass")
|
||||
|
||||
provisionTheme := func(name string, oldValue *types.SettingValue, themeIDs ...string) (err error) {
|
||||
oldValueStr, err := strconv.Unquote(oldValue.Value.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var themes []types.Theme
|
||||
for _, themeID := range themeIDs {
|
||||
title := []rune(themeID)
|
||||
title[0] = unicode.ToUpper(title[0])
|
||||
|
||||
if len(themeIDs) > 2 {
|
||||
if themeID == "general" {
|
||||
themes = append(themes, types.Theme{
|
||||
ID: themeID,
|
||||
Title: string(title),
|
||||
Values: oldValueStr,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
themes = append(themes, types.Theme{
|
||||
ID: themeID,
|
||||
Title: string(title),
|
||||
Values: "",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
themes = append(themes, types.Theme{
|
||||
ID: themeID,
|
||||
Title: string(title),
|
||||
Values: oldValueStr,
|
||||
})
|
||||
}
|
||||
|
||||
value, err := json.Marshal(themes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newThemeSetting := &types.SettingValue{
|
||||
Name: name,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
err = store.CreateSettingValue(ctx, s, newThemeSetting)
|
||||
if err != nil {
|
||||
log.Error("failed to provision webapp themes", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// delete old custom css from the database
|
||||
err = store.DeleteSettingValue(ctx, s, oldValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// provision custom CSS
|
||||
if !oldCustomCSS.IsNull() {
|
||||
err = provisionTheme("ui.studio.custom-css", oldCustomCSS, "general", "light", "dark")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// provision branding sass
|
||||
if !oldBranding.IsNull() {
|
||||
err = provisionTheme("ui.studio.themes", oldBranding, "light", "dark")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -42,20 +42,44 @@ func DefaultCSS(log *zap.Logger, customCSS string) string {
|
||||
|
||||
processedCSS := strBuilder.String()
|
||||
|
||||
StylesheetCache.Set(
|
||||
map[string]string{
|
||||
"css": processedCSS,
|
||||
},
|
||||
)
|
||||
StylesheetCache.Set("default-theme", processedCSS)
|
||||
|
||||
return processedCSS
|
||||
}
|
||||
|
||||
func Transpiler(log *zap.Logger, brandingSass, customCSS, sassDirPath string, transpiler *godartsass.Transpiler) error {
|
||||
var stringsBuilder strings.Builder
|
||||
func Transpile(transpiler *godartsass.Transpiler, log *zap.Logger, themeID, themeSASS, customCSS, sassDirPath string) (err error) {
|
||||
// process root section
|
||||
err = processSass(transpiler, log, "root", themeID, themeSASS, customCSS, sassDirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if brandingSass != "" {
|
||||
err := jsonToSass(brandingSass, &stringsBuilder)
|
||||
// process main section
|
||||
err = processSass(transpiler, log, "main", themeID, themeSASS, customCSS, sassDirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//process theme section
|
||||
err = processSass(transpiler, log, "theme", themeID, themeSASS, customCSS, sassDirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processSass(transpiler *godartsass.Transpiler, log *zap.Logger, section, themeID, themeSASS, customCSS, sassDirPath string) (err error) {
|
||||
var (
|
||||
stringsBuilder strings.Builder
|
||||
isCustomCssSyntaxValid bool
|
||||
)
|
||||
|
||||
// add theme-mode variable to the top of the section
|
||||
stringsBuilder.WriteString(fmt.Sprintf("$theme-mode: %s;\n", themeID))
|
||||
|
||||
if themeSASS != "" {
|
||||
err = jsonToSass(themeSASS, &stringsBuilder)
|
||||
if err != nil {
|
||||
log.Error("failed to unmarshal branding sass variables", zap.Error(err))
|
||||
return err
|
||||
@ -63,20 +87,34 @@ func Transpiler(log *zap.Logger, brandingSass, customCSS, sassDirPath string, tr
|
||||
}
|
||||
|
||||
if customCSS != "" {
|
||||
// Get SASS variables from the custom CSS editor and give them precedence over branding variables
|
||||
customVariables := sassVariablesPattern.FindAllString(customCSS, -1)
|
||||
|
||||
for _, customVariable := range customVariables {
|
||||
stringsBuilder.WriteString(fmt.Sprintf("%s \n", customVariable))
|
||||
_, err = transpileSass(transpiler, customCSS)
|
||||
if err != nil {
|
||||
log.Error("sass compilation for custom css failed", zap.Error(err))
|
||||
} else {
|
||||
isCustomCssSyntaxValid = true
|
||||
// Get SASS variables from the custom CSS editor and give them precedence over branding variables
|
||||
customVariables := sassVariablesPattern.FindAllString(customCSS, -1)
|
||||
for _, customVariable := range customVariables {
|
||||
stringsBuilder.WriteString(fmt.Sprintf("%s \n", customVariable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get Boostrap, bootstrap-vue and custom variables sass content
|
||||
mainSass, err := readSassFiles(log, "scss")
|
||||
//Save branding sass variables to cache
|
||||
sassVariables, err := readSassFiles(log, "scss/variables")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stringsBuilder.WriteString(mainSass)
|
||||
stringsBuilder.WriteString(sassVariables)
|
||||
StylesheetCache.Set("sass", stringsBuilder.String())
|
||||
|
||||
// start processing a section
|
||||
sassSection, err := readSassFiles(log, path.Join("scss", section))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stringsBuilder.WriteString(StylesheetCache.Get("sass"))
|
||||
stringsBuilder.WriteString(sassSection)
|
||||
|
||||
// when a user provides sets WEBAPP_SCSS_DIR_PATH environment variable
|
||||
if sassDirPath != "" {
|
||||
@ -87,33 +125,49 @@ func Transpiler(log *zap.Logger, brandingSass, customCSS, sassDirPath string, tr
|
||||
stringsBuilder.WriteString(customSass)
|
||||
}
|
||||
|
||||
if customCSS != "" {
|
||||
if customCSS != "" && isCustomCssSyntaxValid {
|
||||
//Custom CSS editor selector block
|
||||
selectorBlock := sassVariablesPattern.ReplaceAllString(customCSS, "")
|
||||
stringsBuilder.WriteString(selectorBlock)
|
||||
}
|
||||
|
||||
// compute sass content to CSS
|
||||
args := godartsass.Args{
|
||||
Source: stringsBuilder.String(),
|
||||
}
|
||||
execute, err := transpiler.Execute(args)
|
||||
transpiledCss, err := transpileSass(transpiler, stringsBuilder.String())
|
||||
if err != nil {
|
||||
log.Error("sass compilation failure", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// save computed css to Stylesheet in-memory cache
|
||||
StylesheetCache.Set(
|
||||
map[string]string{
|
||||
"css": execute.CSS,
|
||||
},
|
||||
)
|
||||
// in case of sass error in custom css sass compilation,
|
||||
// use compiled css from branding sass and append custom css value to it
|
||||
if !isCustomCssSyntaxValid {
|
||||
stringsBuilder.Reset()
|
||||
stringsBuilder.WriteString(transpiledCss)
|
||||
stringsBuilder.WriteString(customCSS)
|
||||
|
||||
// append un-compiled custom css content to transpiled css
|
||||
transpiledCss = stringsBuilder.String()
|
||||
}
|
||||
|
||||
//save the transpiled css to stylesheet cache
|
||||
sectionKey := fmt.Sprintf("%s-%s", section, themeID)
|
||||
StylesheetCache.Set(sectionKey, transpiledCss)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DartSass(log *zap.Logger) *godartsass.Transpiler {
|
||||
// transpileSass computes sass to css by the transpiler
|
||||
func transpileSass(transpiler *godartsass.Transpiler, sass string) (string, error) {
|
||||
args := godartsass.Args{
|
||||
Source: sass,
|
||||
}
|
||||
execute, err := transpiler.Execute(args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return execute.CSS, nil
|
||||
}
|
||||
|
||||
func DartSassTranspiler(log *zap.Logger) *godartsass.Transpiler {
|
||||
transpiler, err := godartsass.Start(godartsass.Options{
|
||||
DartSassEmbeddedFilename: "sass",
|
||||
})
|
||||
|
||||
@ -13,10 +13,11 @@ func newStylesheetCache() *stylesheetCache {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *stylesheetCache) Set(value map[string]string) {
|
||||
func (c *stylesheetCache) Set(key, value string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.stylesheet = value
|
||||
|
||||
c.stylesheet[key] = value
|
||||
}
|
||||
|
||||
func (c *stylesheetCache) Get(key string) string {
|
||||
@ -30,7 +31,17 @@ func (c *stylesheetCache) Get(key string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *stylesheetCache) Empty() bool {
|
||||
func (c *stylesheetCache) Keys() (keys []string) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
for k := range c.stylesheet {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *stylesheetCache) IsEmpty() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ package webapp
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -27,7 +26,6 @@ type (
|
||||
webappBaseUrl string
|
||||
discoveryApiBaseUrl string
|
||||
sentryUrl string
|
||||
scssDirPath string
|
||||
settings *types.AppSettings
|
||||
}
|
||||
)
|
||||
@ -36,7 +34,7 @@ var (
|
||||
baseHrefMatcher = regexp.MustCompile(`<base\s+href="?.+?"?\s*\/?>`)
|
||||
)
|
||||
|
||||
func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt options.AuthOpt, discoveryOpt options.DiscoveryOpt, sentryOpt options.SentryOpt, webappOpt options.WebappOpt) func(r chi.Router) {
|
||||
func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt options.AuthOpt, discoveryOpt options.DiscoveryOpt, sentryOpt options.SentryOpt) func(r chi.Router) {
|
||||
var (
|
||||
apiBaseUrl = options.CleanBase(httpSrvOpt.BaseUrl, httpSrvOpt.ApiBaseUrl)
|
||||
webappSentryUrl = sentryOpt.WebappDSN
|
||||
@ -74,7 +72,6 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt
|
||||
discoveryApiBaseUrl: discoveryApiBaseUrl,
|
||||
sentryUrl: webappSentryUrl,
|
||||
settings: service.CurrentSettings,
|
||||
scssDirPath: webappOpt.ScssDirPath,
|
||||
})
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[app], fs))
|
||||
}
|
||||
@ -88,7 +85,6 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt
|
||||
discoveryApiBaseUrl: discoveryApiBaseUrl,
|
||||
sentryUrl: webappSentryUrl,
|
||||
settings: service.CurrentSettings,
|
||||
scssDirPath: webappOpt.ScssDirPath,
|
||||
})
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[""], fs))
|
||||
}
|
||||
@ -152,12 +148,7 @@ func serveConfig(r chi.Router, config webappConfig) {
|
||||
r.Get(options.CleanBase(config.appUrl, "custom.css"), func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/css")
|
||||
|
||||
ctx := auth.SetIdentityToContext(r.Context(), auth.ServiceUser())
|
||||
|
||||
stylesheet, err := service.GenerateCSS(ctx, config.settings.UI.Studio.BrandingSASS, config.settings.UI.CustomCSS, config.scssDirPath)
|
||||
if err != nil {
|
||||
logger.Default().Error("failed to generate CSS", zap.Error(err))
|
||||
}
|
||||
stylesheet := service.FetchCSS()
|
||||
|
||||
_, _ = fmt.Fprint(w, stylesheet)
|
||||
})
|
||||
|
||||
@ -572,6 +572,10 @@ endpoints:
|
||||
name: labels
|
||||
title: Labels
|
||||
parser: label.ParseStrings
|
||||
- name: meta
|
||||
type: "*types.UserMeta"
|
||||
title: Additional user info
|
||||
parser: types.ParseUserMeta
|
||||
- { type: "*time.Time", name: updatedAt, required: false, title: Last update (or creation) date }
|
||||
- name: partialUpdate
|
||||
method: PATCH
|
||||
|
||||
@ -176,6 +176,11 @@ type (
|
||||
// Labels
|
||||
Labels map[string]string
|
||||
|
||||
// Meta POST parameter
|
||||
//
|
||||
// Additional user info
|
||||
Meta *types.UserMeta
|
||||
|
||||
// UpdatedAt POST parameter
|
||||
//
|
||||
// Last update (or creation) date
|
||||
@ -780,6 +785,7 @@ func (r UserUpdate) Auditable() map[string]interface{} {
|
||||
"handle": r.Handle,
|
||||
"kind": r.Kind,
|
||||
"labels": r.Labels,
|
||||
"meta": r.Meta,
|
||||
"updatedAt": r.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@ -814,6 +820,11 @@ func (r UserUpdate) GetLabels() map[string]string {
|
||||
return r.Labels
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r UserUpdate) GetMeta() *types.UserMeta {
|
||||
return r.Meta
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r UserUpdate) GetUpdatedAt() *time.Time {
|
||||
return r.UpdatedAt
|
||||
@ -880,6 +891,18 @@ func (r *UserUpdate) Fill(req *http.Request) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.MultipartForm.Value["meta[]"]; ok {
|
||||
r.Meta, err = types.ParseUserMeta(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if val, ok := req.MultipartForm.Value["meta"]; ok {
|
||||
r.Meta, err = types.ParseUserMeta(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.MultipartForm.Value["updatedAt"]; ok && len(val) > 0 {
|
||||
r.UpdatedAt, err = payload.ParseISODatePtrWithErr(val[0])
|
||||
if err != nil {
|
||||
@ -936,6 +959,18 @@ func (r *UserUpdate) Fill(req *http.Request) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.Form["meta[]"]; ok {
|
||||
r.Meta, err = types.ParseUserMeta(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if val, ok := req.Form["meta"]; ok {
|
||||
r.Meta, err = types.ParseUserMeta(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.Form["updatedAt"]; ok && len(val) > 0 {
|
||||
r.UpdatedAt, err = payload.ParseISODatePtrWithErr(val[0])
|
||||
if err != nil {
|
||||
|
||||
@ -153,6 +153,7 @@ func (ctrl User) Update(ctx context.Context, r *request.UserUpdate) (interface{}
|
||||
Handle: r.Handle,
|
||||
Kind: r.Kind,
|
||||
Labels: r.Labels,
|
||||
Meta: r.Meta,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ type (
|
||||
RBAC options.RbacOpt
|
||||
Limit options.LimitOpt
|
||||
Attachment options.AttachmentOpt
|
||||
Webapps options.WebappOpt
|
||||
}
|
||||
|
||||
eventDispatcher interface {
|
||||
@ -153,7 +154,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock
|
||||
|
||||
DefaultAccessControl = AccessControl(s)
|
||||
|
||||
DefaultSettings = Settings(ctx, DefaultStore, DefaultLogger, DefaultAccessControl, DefaultActionlog, CurrentSettings)
|
||||
DefaultSettings = Settings(ctx, DefaultStore, DefaultLogger, DefaultAccessControl, DefaultActionlog, CurrentSettings, c.Webapps)
|
||||
|
||||
DefaultDalConnection = Connection(ctx, dal.Service(), c.DB)
|
||||
|
||||
|
||||
@ -8,11 +8,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sass"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"go.uber.org/zap"
|
||||
@ -26,6 +26,7 @@ type (
|
||||
accessControl accessController
|
||||
logger *zap.Logger
|
||||
m sync.RWMutex
|
||||
webappsConf options.WebappOpt
|
||||
|
||||
// Holds reference to the "current" settings that
|
||||
// are used by the services
|
||||
@ -49,13 +50,14 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func Settings(ctx context.Context, s store.SettingValues, log *zap.Logger, ac accessController, al actionlog.Recorder, current interface{}) *settings {
|
||||
func Settings(ctx context.Context, s store.SettingValues, log *zap.Logger, ac accessController, al actionlog.Recorder, current interface{}, webappsConf options.WebappOpt) *settings {
|
||||
svc := &settings{
|
||||
actionlog: al,
|
||||
store: s,
|
||||
accessControl: ac,
|
||||
logger: log.Named("settings"),
|
||||
current: current,
|
||||
webappsConf: webappsConf,
|
||||
listeners: make([]registeredSettingsListener, 0, 2),
|
||||
update: make(chan types.SettingValueSet, 0),
|
||||
}
|
||||
@ -245,9 +247,16 @@ func (svc *settings) BulkSet(ctx context.Context, vv types.SettingValueSet) (err
|
||||
}
|
||||
|
||||
for _, v := range vv {
|
||||
// if branding-sass or custom-css is updated, empty stylesheet cache
|
||||
if v.Name == "ui.studio.branding-sass" || v.Name == "ui.custom-css" {
|
||||
sass.StylesheetCache.Set(map[string]string{})
|
||||
// if any of webapps stylesheet settings is updated, we need to recompute and update affected theme css
|
||||
if v.Name == "ui.studio.themes" || v.Name == "ui.studio.custom-css" {
|
||||
var compStyles *types.SettingValue
|
||||
if v.Name == "ui.studio.themes" {
|
||||
compStyles = current.FindByName("ui.studio.custom-css")
|
||||
} else {
|
||||
compStyles = current.FindByName("ui.studio.themes")
|
||||
}
|
||||
|
||||
updateCSS(v, current.FindByName(v.Name), compStyles, v.Name, svc.webappsConf.ScssDirPath, svc.logger)
|
||||
}
|
||||
|
||||
svc.logChange(ctx, v)
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sass"
|
||||
"github.com/cortezaproject/corteza/server/pkg/str"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -17,48 +18,160 @@ import (
|
||||
// If dart isn't installed on the host machine, customCustom css will continue to function, but without sass support.
|
||||
//
|
||||
// In case of an error, it will return default css and log out the error
|
||||
func GenerateCSS(ctx context.Context, brandingSass, customCSS, sassDirPath string) (string, error) {
|
||||
func GenerateCSS(settings *types.AppSettings, sassDirPath string) (err error) {
|
||||
var (
|
||||
log = logger.Default()
|
||||
transpiler = sass.DartSass(log)
|
||||
log = logger.Default()
|
||||
studio = settings.UI.Studio
|
||||
customCSSMap = make(map[string]string)
|
||||
)
|
||||
|
||||
//check if cache has already compiled css value
|
||||
if !sass.StylesheetCache.Empty() {
|
||||
return sass.StylesheetCache.Get("css"), nil
|
||||
for _, customCSS := range studio.CustomCSS {
|
||||
customCSSMap[customCSS.ID] = customCSS.Values
|
||||
}
|
||||
|
||||
sass.DefaultCSS(log, customCSSMap["general"])
|
||||
|
||||
if studio.Themes == nil && studio.CustomCSS == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if dart sass is not installed, or when the sass transpiler creation and startup process fails.
|
||||
// return contents from default css
|
||||
if transpiler == nil {
|
||||
updateSassInstalledSetting(ctx, log, false)
|
||||
return sass.DefaultCSS(log, customCSS), nil
|
||||
if !studio.SassInstalled {
|
||||
return
|
||||
}
|
||||
|
||||
updateSassInstalledSetting(ctx, log, true)
|
||||
transpiler := sass.DartSassTranspiler(log)
|
||||
|
||||
// transpile sass to css
|
||||
err := sass.Transpiler(log, brandingSass, customCSS, sassDirPath, transpiler)
|
||||
// transpile sass to css for each theme
|
||||
if studio.Themes != nil {
|
||||
for _, theme := range studio.Themes {
|
||||
if studio.CustomCSS == nil {
|
||||
err := sass.Transpile(transpiler, log, theme.ID, theme.Values, "", sassDirPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return sass.DefaultCSS(log, customCSS), err
|
||||
customCSS := processCustomCSS(theme.ID, customCSSMap)
|
||||
// transpile sass to css
|
||||
err := sass.Transpile(transpiler, log, theme.ID, theme.Values, customCSS, sassDirPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if studio.CustomCSS != nil {
|
||||
for key := range customCSSMap {
|
||||
if key == "general" {
|
||||
continue
|
||||
}
|
||||
|
||||
customCSS := processCustomCSS(key, customCSSMap)
|
||||
// transpile sass to css
|
||||
err := sass.Transpile(transpiler, log, key, "", customCSS, sassDirPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sass.StylesheetCache.Get("css"), nil
|
||||
return
|
||||
}
|
||||
|
||||
func updateSassInstalledSetting(ctx context.Context, log *zap.Logger, installed bool) {
|
||||
sv := &types.SettingValue{
|
||||
Name: "ui.studio.sass-installed",
|
||||
}
|
||||
// processCustomCSS, processes CustomCSS input and gives priority to theme specific customCSS
|
||||
func processCustomCSS(themeID string, customCSSMap map[string]string) (customCSS string) {
|
||||
var stringsBuilder strings.Builder
|
||||
|
||||
err := sv.SetSetting(installed)
|
||||
if err != nil {
|
||||
log.Warn("failed to set ui.studio.sass-installed setting", zap.Error(err))
|
||||
}
|
||||
stringsBuilder.WriteString(customCSSMap["general"])
|
||||
stringsBuilder.WriteString("\n")
|
||||
stringsBuilder.WriteString(customCSSMap[themeID])
|
||||
|
||||
err = DefaultSettings.Set(ctx, sv)
|
||||
if err != nil {
|
||||
log.Warn("failed to set ui.studio.sass-installed setting", zap.Error(err))
|
||||
}
|
||||
return stringsBuilder.String()
|
||||
}
|
||||
|
||||
// updateCSS, updates theme css when ui.studio.themes or ui.studio.custom-css settings are updated
|
||||
func updateCSS(current, old, compStyles *types.SettingValue, name, sassDirPath string, log *zap.Logger) {
|
||||
transpiler := sass.DartSassTranspiler(log)
|
||||
|
||||
complimentaryStylesMap := themeMap(compStyles)
|
||||
oldThemesMap := themeMap(old)
|
||||
currentThemesMap := themeMap(current)
|
||||
|
||||
transpileSASS := func(themeID, themeSASS string, themeCustomCSS map[string]string) {
|
||||
customCSS := processCustomCSS(themeID, themeCustomCSS)
|
||||
err := sass.Transpile(transpiler, log, themeID, themeSASS, customCSS, sassDirPath)
|
||||
if err != nil {
|
||||
log.Error("failed to transpile sass to css", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
for key := range currentThemesMap {
|
||||
if str.HashStringSHA256(oldThemesMap[key]) == str.HashStringSHA256(currentThemesMap[key]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if name == "ui.studio.themes" {
|
||||
transpileSASS(key, currentThemesMap[key], complimentaryStylesMap)
|
||||
continue
|
||||
}
|
||||
|
||||
if key == "general" {
|
||||
if complimentaryStylesMap == nil {
|
||||
themeID := "light"
|
||||
transpileSASS(themeID, complimentaryStylesMap[themeID], currentThemesMap)
|
||||
continue
|
||||
}
|
||||
|
||||
for themeID := range complimentaryStylesMap {
|
||||
transpileSASS(themeID, complimentaryStylesMap[themeID], currentThemesMap)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
transpileSASS(key, complimentaryStylesMap[key], currentThemesMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func themeMap(settingsValue *types.SettingValue) (themeMap map[string]string) {
|
||||
var themes []types.Theme
|
||||
|
||||
if settingsValue == nil {
|
||||
return themeMap
|
||||
}
|
||||
|
||||
_ = settingsValue.Value.Unmarshal(&themes)
|
||||
|
||||
themeMap = make(map[string]string)
|
||||
for _, theme := range themes {
|
||||
themeMap[theme.ID] = theme.Values
|
||||
}
|
||||
|
||||
return themeMap
|
||||
}
|
||||
|
||||
func FetchCSS() string {
|
||||
var stringsBuilder strings.Builder
|
||||
|
||||
if sass.StylesheetCache.Get("root-light") == "" {
|
||||
return sass.StylesheetCache.Get("default-theme")
|
||||
}
|
||||
|
||||
// root css section
|
||||
stringsBuilder.WriteString(sass.StylesheetCache.Get("root-light"))
|
||||
stringsBuilder.WriteString("\n")
|
||||
stringsBuilder.WriteString(sass.StylesheetCache.Get("root-dark"))
|
||||
|
||||
stringsBuilder.WriteString("\n")
|
||||
|
||||
//theme css section
|
||||
stringsBuilder.WriteString(sass.StylesheetCache.Get("theme-dark"))
|
||||
stringsBuilder.WriteString("\n")
|
||||
|
||||
// body css section
|
||||
stringsBuilder.WriteString(sass.StylesheetCache.Get("main-light"))
|
||||
stringsBuilder.WriteString("\n")
|
||||
|
||||
return stringsBuilder.String()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,7 @@ type (
|
||||
AvatarBgColor string `json:"avatarBgColor,omitempty"`
|
||||
|
||||
PreferredLanguage string `json:"preferredLanguage"`
|
||||
Theme string `json:"theme"`
|
||||
|
||||
// User's security policy settings
|
||||
SecurityPolicy struct {
|
||||
@ -163,3 +164,13 @@ func (u *User) Clone() *User {
|
||||
|
||||
func (meta *UserMeta) Scan(src any) error { return sql.ParseJSON(src, meta) }
|
||||
func (meta *UserMeta) Value() (driver.Value, error) { return json.Marshal(meta) }
|
||||
|
||||
func ParseUserMeta(ss []string) (p *UserMeta, err error) {
|
||||
p = &UserMeta{}
|
||||
|
||||
if len(ss) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return p, json.Unmarshal([]byte(ss[0]), p)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user