Add runtime CSS customization for webapps
This commit is contained in:
committed by
Mumbi Francis
parent
a97401996e
commit
037c53d22b
@@ -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>
|
||||
@@ -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
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
CRichTextInput,
|
||||
CInputCheckbox,
|
||||
CInputColorPicker,
|
||||
CAceEditor,
|
||||
} from './input'
|
||||
|
||||
export {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user