Add runtime CSS customization for webapps

This commit is contained in:
Jože Fortun
2023-06-27 14:48:48 +03:00
committed by Mumbi Francis
parent a97401996e
commit 037c53d22b
18 changed files with 348 additions and 14 deletions
+4 -1
View File
@@ -14,7 +14,7 @@
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
<title><%= FLAVOUR %></title>
</head>
<body>
<body id="admin">
<noscript>
<strong>
JavaScript is disabled in your browser!
@@ -26,6 +26,9 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<style>
@import url("<%= BASE_URL %>custom.css");
</style>
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
</body>
</html>
@@ -0,0 +1,136 @@
<template>
<b-card
class="shadow-sm"
header-bg-variant="white"
footer-bg-variant="white"
>
<template #header>
<h3 class="m-0">
{{ $t('title') }}
</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"
>
<c-ace-editor
v-model="modalCSSInput"
lang="css"
height="500px"
font-size="14px"
show-line-numbers
:border="false"
:show-popout="false"
/>
</b-modal>
<template #footer>
<c-submit-button
:disabled="!canManage"
:processing="processing"
:success="success"
class="float-right mt-2"
@submit="onSubmit"
/>
</template>
</b-card>
</template>
<script>
import CSubmitButton from 'corteza-webapp-admin/src/components/CSubmitButton'
import { components } from '@cortezaproject/corteza-vue'
const { CAceEditor } = components
export default {
name: 'CUIEditorCustomCSS',
i18nOptions: {
namespaces: 'ui.settings',
keyPrefix: 'editor.custom-css',
},
components: {
CSubmitButton,
CAceEditor,
},
props: {
settings: {
type: Object,
required: true,
},
processing: {
type: Boolean,
value: false,
},
success: {
type: Boolean,
value: false,
},
canManage: {
type: Boolean,
required: true,
},
},
data () {
return {
customCSS: '',
modalCSSInput: undefined,
showEditorModal: false,
}
},
watch: {
settings: {
immediate: true,
handler (settings) {
this.customCSS = settings['ui.custom-css'] || ''
},
},
},
methods: {
onSubmit () {
this.$emit('submit', { 'ui.custom-css': this.customCSS })
},
openEditorModal () {
this.modalCSSInput = this.customCSS
this.showEditorModal = true
},
saveCustomCSSInput () {
this.customCSS = this.modalCSSInput
},
resetCustomCSSInput () {
this.modalCSSInput = undefined
},
},
}
</script>
+23 -7
View File
@@ -11,13 +11,22 @@
:can-manage="canManage"
/>
<c-ui-custom-css
:settings="settings"
:processing="customCSS.processing"
:success="customCSS.success"
:can-manage="canManage"
class="mt-3"
@submit="onSubmit($event, 'customCSS')"
/>
<c-ui-topbar-settings
:settings="settings"
:processing="topbar.processing"
:success="topbar.success"
:can-manage="canManage"
class="mt-3"
@submit="onTopbarSubmit"
@submit="onSubmit($event, 'topbar')"
/>
</b-container>
</template>
@@ -25,6 +34,7 @@
<script>
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CUILogoEditor from 'corteza-webapp-admin/src/components/Settings/UI/CUILogoEditor'
import CUICustomCSS from 'corteza-webapp-admin/src/components/Settings/UI/CUICustomCSS.vue'
import CUITopbarSettings from 'corteza-webapp-admin/src/components/Settings/UI/CUITopbarSettings'
import { mapGetters } from 'vuex'
@@ -38,6 +48,7 @@ export default {
components: {
'c-ui-logo-editor': CUILogoEditor,
'c-ui-custom-css': CUICustomCSS,
'c-ui-topbar-settings': CUITopbarSettings,
},
@@ -53,6 +64,11 @@ export default {
processing: false,
success: false,
},
customCSS: {
processing: false,
success: false,
},
}
},
@@ -85,8 +101,8 @@ export default {
})
},
onTopbarSubmit (settings) {
this.topbar.processing = true
onSubmit (settings, type) {
this[type].processing = true
const values = Object.entries(settings).map(([name, value]) => {
return { name, value }
@@ -94,13 +110,13 @@ export default {
this.$SystemAPI.settingsUpdate({ values })
.then(() => {
this.animateSuccess('topbar')
this.toastSuccess(this.$t('notification:settings.compose.update.success'))
this.animateSuccess(type)
this.toastSuccess(this.$t('notification:settings.ui.update.success'))
this.$Settings.fetch()
})
.catch(this.toastErrorHandler(this.$t('notification:settings.compose.update.error')))
.catch(this.toastErrorHandler(this.$t('notification:settings.ui.update.error')))
.finally(() => {
this.topbar.processing = false
this[type].processing = false
})
},
},
+4 -1
View File
@@ -14,7 +14,7 @@
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
<title><%= FLAVOUR %></title>
</head>
<body>
<body id="compose">
<noscript>
<strong>
JavaScript is disabled in your browser!
@@ -26,6 +26,9 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<style>
@import url("<%= BASE_URL %>custom.css");
</style>
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
</body>
</html>
+4 -1
View File
@@ -13,7 +13,7 @@
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
<title><%= FLAVOUR %></title>
</head>
<body>
<body id="discovery">
<noscript>
<strong>
JavaScript is disabled in your browser!
@@ -25,6 +25,9 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<style>
@import url("<%= BASE_URL %>custom.css");
</style>
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
</body>
</html>
+4 -1
View File
@@ -14,7 +14,7 @@
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
<title><%= FLAVOUR %></title>
</head>
<body>
<body id="one">
<noscript>
<strong>
JavaScript is disabled in your browser!
@@ -26,6 +26,9 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<style>
@import url("<%= BASE_URL %>custom.css");
</style>
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
</body>
</html>
+4 -1
View File
@@ -13,7 +13,7 @@
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
<title><%= FLAVOUR %></title>
</head>
<body>
<body id="privacy">
<noscript>
<strong>
JavaScript is disabled in your browser!
@@ -25,6 +25,9 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<style>
@import url("<%= BASE_URL %>custom.css");
</style>
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
</body>
</html>
+4 -1
View File
@@ -13,7 +13,7 @@
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
<title><%= FLAVOUR %></title>
</head>
<body>
<body id="reporter">
<noscript>
<strong>
JavaScript is disabled in your browser!
@@ -25,6 +25,9 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<style>
@import url("<%= BASE_URL %>custom.css");
</style>
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
</body>
</html>
+4 -1
View File
@@ -23,7 +23,7 @@
}
</style>
</head>
<body>
<body id="workflow">
<noscript>
<strong>
JavaScript is disabled in your browser!
@@ -35,6 +35,9 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<style>
@import url("<%= BASE_URL %>custom.css");
</style>
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
</body>
</html>
+1
View File
@@ -52,6 +52,7 @@
"vue-echarts": "^6.2.3",
"vue-select": "^3.4.0",
"vue-swatches": "^1.0.4",
"vue2-ace-editor": "^0.0.15",
"vuedraggable": "^2.23.0",
"vuex": "^3.6.2",
"websocket-extensions": "^0.1.4",
+1
View File
@@ -18,6 +18,7 @@ export {
CRichTextInput,
CInputCheckbox,
CInputColorPicker,
CAceEditor,
} from './input'
export {
+127
View File
@@ -0,0 +1,127 @@
<template>
<div
class="position-relative"
>
<ace-editor
v-model="editorValue"
:lang="lang"
:mode="lang"
theme="chrome"
width="100%"
:height="height"
:class="{ 'border': border }"
v-on="$listeners"
@init="editorInit"
/>
<b-button
v-if="showPopout"
variant="link"
class="popout position-absolute px-2 py-1 mr-3"
@click="$emit('open')"
>
<font-awesome-icon
:icon="['fas', 'expand-alt']"
/>
</b-button>
</div>
</template>
<script>
import AceEditor from 'vue2-ace-editor'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faExpandAlt} from '@fortawesome/free-solid-svg-icons'
library.add(faExpandAlt)
export default {
components: {
AceEditor,
},
props: {
value: {
type: String,
default: '',
},
lang: {
type: String,
default: 'text',
},
height: {
type: String,
default: '80',
},
showLineNumbers: {
type: Boolean,
default: false,
},
fontSize: {
type: String,
default: '14px',
},
border: {
type: Boolean,
default: true,
},
showPopout: {
type: Boolean,
default: false,
},
},
computed: {
editorValue: {
get () {
return this.value
},
set (value = '') {
this.$emit('update:value', value)
},
},
},
methods: {
editorInit (editor) {
require('brace/mode/text')
require('brace/mode/html')
require('brace/mode/css')
require('brace/mode/javascript')
require('brace/theme/chrome')
editor.setOptions({
tabSize: 2,
fontSize: this.fontSize,
wrap: true,
indentedSoftWrap: false,
showLineNumbers: this.showLineNumbers,
showGutter: this.showLineNumbers,
displayIndentGuides: this.lang !== 'text',
useWorker: false,
})
},
},
}
</script>
<style lang="scss" scoped>
.border {
background-color: #FFFFFF;
border: 2px solid #E4E9EF;
border-radius: 0.25rem;
}
.popout {
z-index: 7;
bottom: 0;
right: 0;
}
</style>
+1
View File
@@ -7,3 +7,4 @@ export { default as CInputSearch } from './CInputSearch.vue'
export { default as CRichTextInput } from './CRichTextInput/index.vue'
export { default as CInputCheckbox } from './CInputCheckbox.vue'
export { default as CInputColorPicker } from './CInputColorPicker.vue'
export { default as CAceEditor } from './CAceEditor.vue'
+12
View File
@@ -2475,6 +2475,11 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
integrity sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q==
braces@^2.3.1, braces@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
@@ -11362,6 +11367,13 @@ vue-tour@^2.0.0:
jump.js "^1.0.2"
vue "^2.6.12"
vue2-ace-editor@^0.0.15:
version "0.0.15"
resolved "https://registry.yarnpkg.com/vue2-ace-editor/-/vue2-ace-editor-0.0.15.tgz#569b208e54ae771ae1edd3b8902ac42f0edc74e3"
integrity sha512-e3TR9OGXc71cGpvYcW068lNpRcFt3+OONCC81oxHL/0vwl/V3OgqnNMw2/RRolgQkO/CA5AjqVHWmANWKOtNnQ==
dependencies:
brace "^0.11.0"
vue@^2.6.12:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
@@ -43,6 +43,7 @@ label:
loadMore: Load more
loading: Loading
saveAndClose: Save and close
cancel: Cancel
submit: Submit
delete: Delete
undelete: Undelete
@@ -21,6 +21,11 @@ editor:
uploader:
instructions: 'Click or drop favicon file here to upload'
uploading: 'Uploading favicon'
custom-css:
title: Custom CSS
modal:
editor: Custom CSS editor
topbar:
title: Topbar
+11
View File
@@ -12,6 +12,8 @@ import (
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
@@ -24,6 +26,7 @@ type (
webappBaseUrl string
discoveryApiBaseUrl string
sentryUrl string
settings *types.AppSettings
}
)
@@ -68,6 +71,7 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt
webappBaseUrl: httpSrvOpt.BaseUrl,
discoveryApiBaseUrl: discoveryApiBaseUrl,
sentryUrl: webappSentryUrl,
settings: service.CurrentSettings,
})
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[app], fs))
}
@@ -80,6 +84,7 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt
webappBaseUrl: httpSrvOpt.BaseUrl,
discoveryApiBaseUrl: discoveryApiBaseUrl,
sentryUrl: webappSentryUrl,
settings: service.CurrentSettings,
})
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[""], fs))
}
@@ -139,6 +144,12 @@ func serveConfig(r chi.Router, config webappConfig) {
_, _ = fmt.Fprintf(w, line, "SentryDSN", config.sentryUrl)
}
})
r.Get(options.CleanBase(config.appUrl, "custom.css"), func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/css")
_, _ = fmt.Fprint(w, config.settings.UI.CustomCSS)
})
}
// Reads and modifies index HTML for the webapp
+2
View File
@@ -248,6 +248,8 @@ type (
Disabled bool `json:"disabled"`
} `kv:"sidebar,final" json:"sidebar"`
CustomCSS string `kv:"custom-css" json:"customCSS"`
Topbar struct {
HideAppSelector bool `json:"hideAppSelector"`
HideHelp bool `json:"hideHelp"`