Add support for injecting custom code-snippets in webapps
This commit is contained in:
parent
f5e6c45d07
commit
2e8b0e96a3
@ -123,7 +123,11 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
hot: false,
|
||||
|
||||
proxy: {
|
||||
'^/custom.css': {
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
@ -28,5 +28,8 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -120,6 +120,12 @@ export default {
|
||||
icon: 'cloud',
|
||||
can: ['system/', 'dal-connections.search'],
|
||||
},
|
||||
{
|
||||
label: 'system.items.code-snippets',
|
||||
route: 'system.codesnippets',
|
||||
icon: 'file-code',
|
||||
can: ['system/', 'settings.read'],
|
||||
},
|
||||
{
|
||||
label: 'system.items.sensitivityLevel',
|
||||
route: 'system.sensitivityLevel',
|
||||
|
||||
297
client/web/admin/src/views/System/CodeSnippets/Index.vue
Normal file
297
client/web/admin/src/views/System/CodeSnippets/Index.vue
Normal file
@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<b-container
|
||||
class="pt-2 pb-3"
|
||||
>
|
||||
<c-content-header
|
||||
:title="$t('title')"
|
||||
/>
|
||||
<b-card
|
||||
body-class="p-0"
|
||||
header-class="border-bottom"
|
||||
footer-class="border-top d-flex flex-wrap flex-fill-child gap-1"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<div class="align-items-center gap-1 p-3">
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
@click="newCodeSnippet()"
|
||||
>
|
||||
{{ $t('code-snippets.add') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<b-table
|
||||
:items="providerItems"
|
||||
:fields="codeSnippetProviderFields"
|
||||
head-variant="light"
|
||||
show-empty
|
||||
hover
|
||||
class="mb-0"
|
||||
style="min-height: 50px;"
|
||||
>
|
||||
<template #cell(provider)="{ item }">
|
||||
{{ item.provider || item.tag }}
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<p
|
||||
data-test-id="no-matches"
|
||||
class="text-center text-dark"
|
||||
style="margin-top: 1vh;"
|
||||
>
|
||||
{{ $t('code-snippets.empty') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #cell(editor)="{ item }">
|
||||
<c-input-confirm
|
||||
v-if="item.delete"
|
||||
:icon="item.deleted ? ['fas', 'trash-restore'] : undefined"
|
||||
@confirmed="item.delete()"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
variant="link"
|
||||
@click="openEditor(item.editor)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'wrench']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<b-modal
|
||||
id="modal-codeSnippet"
|
||||
v-model="modal.open"
|
||||
:title="modal.title"
|
||||
scrollable
|
||||
size="lg"
|
||||
title-class="text-capitalize"
|
||||
@ok="modal.updater(modal.data)"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('code-snippets.form.provider.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input v-model="modal.data.name" />
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<h5>
|
||||
{{ $t('code-snippets.add') }}
|
||||
</h5>
|
||||
<span class="text-muted">
|
||||
{{ $t('code-snippets.form.value.description') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<c-ace-editor
|
||||
v-model="modal.data.script"
|
||||
lang="javascript"
|
||||
height="500px"
|
||||
font-size="14px"
|
||||
show-line-numbers
|
||||
:border="false"
|
||||
:show-popout="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #modal-footer="{ ok, cancel}">
|
||||
<c-input-confirm
|
||||
size="md"
|
||||
variant="danger"
|
||||
@confirmed="deleteCodeSnippet(modal.index)"
|
||||
>
|
||||
{{ $t('general:label.delete') }}
|
||||
</c-input-confirm>
|
||||
|
||||
<b-button
|
||||
variant="light"
|
||||
class="ml-auto"
|
||||
@click="cancel()"
|
||||
>
|
||||
{{ $t('general:label.cancel') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
variant="primary"
|
||||
@click="ok()"
|
||||
>
|
||||
{{ $t('general:label.saveAndClose') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
|
||||
<template #footer>
|
||||
<c-button-submit
|
||||
:disabled="!canManage"
|
||||
:processing="codeSnippet.processing"
|
||||
:success="codeSnippet.success"
|
||||
:text="$t('admin:general.label.submit')"
|
||||
class="ml-auto"
|
||||
@submit="onSubmit()"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
const { CAceEditor } = components
|
||||
|
||||
export default {
|
||||
name: 'CSystemCodeSnippetEditor',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'system.code-snippets',
|
||||
keyPrefix: 'editor',
|
||||
},
|
||||
|
||||
components: {
|
||||
CAceEditor,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
editorHelpers,
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
codeSnippets: [],
|
||||
modal: {
|
||||
open: false,
|
||||
editor: null,
|
||||
title: null,
|
||||
data: [],
|
||||
index: null,
|
||||
},
|
||||
|
||||
codeSnippet: {
|
||||
processing: false,
|
||||
success: false,
|
||||
},
|
||||
originalCodeSnippets: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
canManage: 'rbac/can',
|
||||
}),
|
||||
|
||||
codeSnippetProviderFields () {
|
||||
return [
|
||||
{ key: 'provider', label: this.$t('code-snippets.table-headers.provider'), thStyle: { width: '200px' }, tdClass: 'text-capitalize' },
|
||||
{ key: 'value', label: this.$t('code-snippets.table-headers.value'), tdClass: 'td-content-overflow' },
|
||||
{ key: 'editor', label: '', thStyle: { width: '200px' }, tdClass: 'text-right' },
|
||||
]
|
||||
},
|
||||
|
||||
providerItems () {
|
||||
return this.codeSnippets.map((s, i) => ({
|
||||
provider: s.name,
|
||||
value: s.script,
|
||||
|
||||
editor: {
|
||||
data: s,
|
||||
index: i,
|
||||
title: s.name,
|
||||
updater: (changed) => {
|
||||
this.codeSnippets[i] = changed
|
||||
},
|
||||
},
|
||||
}))
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchSettings()
|
||||
|
||||
this.originalCodeSnippets = [...this.codeSnippets]
|
||||
},
|
||||
methods: {
|
||||
openEditor ({ component, title, data, updater }) {
|
||||
this.modal.open = true
|
||||
this.modal.component = component
|
||||
this.modal.title = title
|
||||
this.modal.updater = updater
|
||||
|
||||
// deref
|
||||
this.modal.data = data
|
||||
},
|
||||
|
||||
newCodeSnippet () {
|
||||
this.openEditor({
|
||||
title: this.$t('code-snippets.add'),
|
||||
data: {
|
||||
name: '',
|
||||
script: '<' + 'script> ' + '</' + 'script>',
|
||||
},
|
||||
updater: (changed) => {
|
||||
this.codeSnippets.push(changed)
|
||||
},
|
||||
})
|
||||
},
|
||||
fetchSettings () {
|
||||
this.incLoader()
|
||||
this.$Settings.fetch()
|
||||
return this.$SystemAPI.settingsList({ prefix: 'code-snippets' })
|
||||
.then(settings => {
|
||||
if (settings && settings[0]) {
|
||||
this.codeSnippets = settings[0].value
|
||||
} else {
|
||||
this.codeSnippets = []
|
||||
}
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:settings.codeSnippet.fetch.error')))
|
||||
.finally(() => {
|
||||
this.decLoader()
|
||||
})
|
||||
},
|
||||
|
||||
settingsUpdate (action) {
|
||||
this.codeSnippet.processing = true
|
||||
this.$SystemAPI.settingsUpdate({ values: [{ name: 'code-snippets', value: this.codeSnippets }] })
|
||||
.then(() => {
|
||||
this.animateSuccess('codeSnippet')
|
||||
if (action === 'delete') {
|
||||
this.toastSuccess(this.$t('notification:settings.codeSnippet.delete.success'))
|
||||
} else {
|
||||
this.toastSuccess(this.$t('notification:settings.codeSnippet.update.success'))
|
||||
}
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:settings.codeSnippet.update.error')))
|
||||
.finally(() => {
|
||||
this.codeSnippet.processing = false
|
||||
})
|
||||
},
|
||||
onSubmit () {
|
||||
this.settingsUpdate('update')
|
||||
},
|
||||
|
||||
deleteCodeSnippet (i) {
|
||||
this.codeSnippets.splice(i, 1)
|
||||
this.settingsUpdate('delete')
|
||||
this.$bvModal.hide('modal-codeSnippet')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.td-content-overflow {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@ -68,6 +68,7 @@ export default {
|
||||
processing: false,
|
||||
success: false,
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -110,6 +110,8 @@ export default [
|
||||
r('system.connection.new', 'connection/new', 'System/Connection/Editor'),
|
||||
r('system.connection.edit', 'connection/edit/:connectionID', 'System/Connection/Editor'),
|
||||
|
||||
r('system.codesnippets', 'codesnippets', 'System/CodeSnippets/Index'),
|
||||
|
||||
combo('system', 'sensitivityLevel'),
|
||||
|
||||
combo('system', 'queue', { pkey: 'queueID' }),
|
||||
|
||||
@ -124,6 +124,10 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
|
||||
@ -28,5 +28,8 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -124,6 +124,10 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
|
||||
@ -27,5 +27,8 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -124,6 +124,10 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
|
||||
@ -28,5 +28,8 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -124,6 +124,10 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
|
||||
@ -27,5 +27,8 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -124,6 +124,10 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
|
||||
@ -27,5 +27,8 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -124,6 +124,10 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
|
||||
@ -37,5 +37,8 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
|
||||
<!-- contains cdns that will be injected at the head of the page -->
|
||||
<script src="<%= BASE_URL %>code-snippets.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -124,6 +124,10 @@ module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, t
|
||||
'^/custom.css': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
|
||||
'^/code-snippets.js': {
|
||||
target: fetchBaseUrl(),
|
||||
},
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
|
||||
@ -9,6 +9,7 @@ system:
|
||||
applications: Applications
|
||||
authclients: Auth Clients
|
||||
connections: Connections
|
||||
code-snippets: Code Snippets
|
||||
sensitivityLevel: Sensitivity Levels
|
||||
permissions: Permissions
|
||||
queues: Messaging Queues
|
||||
@ -43,4 +44,3 @@ ui:
|
||||
group: User interface
|
||||
items:
|
||||
settings: Settings
|
||||
|
||||
|
||||
@ -268,6 +268,16 @@ settings:
|
||||
update:
|
||||
success: UI settings updated
|
||||
error: UI settings update failed
|
||||
code-snippet:
|
||||
fetch:
|
||||
error: Code snippets settings fetch failed
|
||||
update:
|
||||
success: Code snippets settings updated
|
||||
error: Code snippets settings update failed
|
||||
delete:
|
||||
success: Code snippet deleted
|
||||
error: Code snippet delete failed
|
||||
|
||||
permissions:
|
||||
fetch:
|
||||
error: Permissions fetch failed
|
||||
@ -293,4 +303,4 @@ connection:
|
||||
error: Connection delete failed
|
||||
undelete:
|
||||
success: Connection restored
|
||||
error: Connection restore failed
|
||||
error: Connection restore failed
|
||||
15
locale/en/corteza-webapp-admin/system.code-snippets.yaml
Normal file
15
locale/en/corteza-webapp-admin/system.code-snippets.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
editor:
|
||||
title: Code snippets
|
||||
|
||||
code-snippets:
|
||||
add: Add code snippet
|
||||
empty: No snippets yet
|
||||
table-headers:
|
||||
provider: Provider
|
||||
value: Script value
|
||||
form:
|
||||
provider:
|
||||
label: Provider
|
||||
value:
|
||||
label: Script value
|
||||
description: Ensure that your Code snippets are within <script> tags to properly inject them into Corteza web applications.
|
||||
@ -87,3 +87,4 @@ editor:
|
||||
invalid-handle-characters: Should be at least 2 characters long. Can contain only letters, numbers, underscores and dots. Must end with letter or number
|
||||
url: URL
|
||||
new-tab: New tab
|
||||
|
||||
@ -2,14 +2,18 @@ package webapp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/system/service"
|
||||
@ -28,6 +32,8 @@ type (
|
||||
sentryUrl string
|
||||
settings *types.AppSettings
|
||||
}
|
||||
|
||||
scriptAttrs = map[string]string
|
||||
)
|
||||
|
||||
var (
|
||||
@ -152,6 +158,63 @@ func serveConfig(r chi.Router, config webappConfig) {
|
||||
|
||||
_, _ = fmt.Fprint(w, stylesheet)
|
||||
})
|
||||
|
||||
// serve code-snippets.js
|
||||
r.Get(options.CleanBase(config.appUrl, "code-snippets.js"), func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/javascript")
|
||||
|
||||
codeSnippets := service.CurrentSettings.CodeSnippets
|
||||
|
||||
snippetScripts := ""
|
||||
for _, snippet := range codeSnippets {
|
||||
snippetScripts += snippet.Script
|
||||
}
|
||||
|
||||
doc, err := html.Parse(strings.NewReader(snippetScripts))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
scriptsAttrs := traverseScriptsNode(doc)
|
||||
|
||||
//create a javascript array of objects
|
||||
snippetScriptsJson, err := json.Marshal(scriptsAttrs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
jsScripts := fmt.Sprintf(
|
||||
`
|
||||
const snippetScripts = %s;
|
||||
|
||||
if (snippetScripts !== null) {
|
||||
snippetScripts.forEach(snippetScript => {
|
||||
const scriptAttr = document.createElement("script");
|
||||
|
||||
if (snippetScript.src) {
|
||||
scriptAttr.src = snippetScript.src;
|
||||
}
|
||||
|
||||
if (snippetScript.integrity) {
|
||||
scriptAttr.integrity = snippetScript.integrity;
|
||||
}
|
||||
|
||||
if (snippetScript.crossorigin) {
|
||||
scriptAttr.crossOrigin = snippetScript.crossorigin;
|
||||
}
|
||||
|
||||
if (snippetScript.content) {
|
||||
scriptAttr.textContent = snippetScript.content;
|
||||
}
|
||||
|
||||
document.head.appendChild(scriptAttr);
|
||||
});
|
||||
}
|
||||
|
||||
`, string(snippetScriptsJson))
|
||||
|
||||
_, _ = fmt.Fprint(w, jsScripts)
|
||||
})
|
||||
}
|
||||
|
||||
// Reads and modifies index HTML for the webapp
|
||||
@ -183,3 +246,34 @@ func replaceBaseHrefPlaceholder(buf []byte, app, baseHref string) []byte {
|
||||
|
||||
return fixed
|
||||
}
|
||||
|
||||
func traverseScriptsNode(n *html.Node) []scriptAttrs {
|
||||
var scripts []scriptAttrs
|
||||
|
||||
if n.Type == html.ElementNode && n.Data == "script" {
|
||||
script := scriptAttrs{}
|
||||
for _, attr := range n.Attr {
|
||||
switch attr.Key {
|
||||
case "src":
|
||||
script["src"] = attr.Val
|
||||
case "integrity":
|
||||
script["integrity"] = attr.Val
|
||||
case "crossorigin":
|
||||
script["crossorigin"] = attr.Val
|
||||
}
|
||||
}
|
||||
|
||||
if n.FirstChild != nil {
|
||||
script["content"] = n.FirstChild.Data
|
||||
}
|
||||
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
childScripts := traverseScriptsNode(c)
|
||||
scripts = append(scripts, childScripts...)
|
||||
}
|
||||
|
||||
return scripts
|
||||
}
|
||||
|
||||
@ -238,6 +238,8 @@ type (
|
||||
// Integration gateway settings
|
||||
Apigw ApigwSettings `kv:"apigw" json:"apigw"`
|
||||
|
||||
CodeSnippets []struct{ CodeSnippet } `kv:"code-snippets" json:"codeSnippets"`
|
||||
|
||||
// UserInterface settings
|
||||
UI struct {
|
||||
MainLogo string `kv:"main-logo" json:"mainLogo"`
|
||||
@ -415,6 +417,11 @@ type (
|
||||
Values string `json:"values"`
|
||||
}
|
||||
|
||||
CodeSnippet struct {
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
SmtpServers struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port,string"`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user