Skip to content

Commit

Permalink
Merge 53f95b2 into e2979b0
Browse files Browse the repository at this point in the history
  • Loading branch information
ghys committed Dec 10, 2021
2 parents e2979b0 + 53f95b2 commit c685ca5
Show file tree
Hide file tree
Showing 13 changed files with 841 additions and 227 deletions.
Expand Up @@ -11,8 +11,11 @@ import defineValueStorageBlocks from './blocks-valuestorage'
import defineEphemerisBlocks from './blocks-ephemeris'
import defineScriptsBlocks from './blocks-scripts'
import definePeristenceBlocks from './blocks-persistence'
import { defineLibraries } from './libraries'

export default function (f7, data) {
import Blockly from 'blockly'

export default function (f7, libraryDefinitions, data) {
defineDictionaryBlocks(f7)
defineDateOffsetsBlocks(f7)
defineItemBlocks(f7)
Expand All @@ -26,4 +29,5 @@ export default function (f7, data) {
defineEphemerisBlocks(f7)
defineScriptsBlocks(f7)
definePeristenceBlocks(f7)
defineLibraries(libraryDefinitions)
}
140 changes: 140 additions & 0 deletions bundles/org.openhab.ui/web/src/assets/definitions/blockly/libraries.js
@@ -0,0 +1,140 @@
import Blockly from 'blockly'
import { addOSGiService } from './utils'

const generateCodeForBlock = (block) => {
const blockTypeId = block.openhab.blockTypeId
const definition = block.openhab.definition
const library = block.openhab.library
const codeComponent = (definition.slots && definition.slots.code && definition.slots.code[0]) ? definition.slots.code[0] : null

const context = {
fields: {},
inputs: {},
statements: {},
utilities: {},
uniqueIdentifiers: {}
}

const provideUtility = (utilityName) => {
let utilityCode = [`function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}() { /* error! */ }`]
if (library.slots.utilities) {
const utilityComponent = library.slots.utilities.find(c => c.config && c.config.name === utilityName)
if (!utilityComponent) {
} else {
switch (utilityComponent.component) {
case 'UtilityFrameworkService':
return addOSGiService(utilityName, utilityComponent.config.serviceClass)
case 'UtilityJavaType':
utilityCode = `var ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_} = Java.type('${utilityComponent.config.javaClass}');`
break
default:
utilityCode = utilityComponent.config.code.replace('{{name}}', Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_)
// process additional utilities if referenced in the function code
while (/(\{\{[A-Za-z0-9_]+\}\})/gm.test(utilityCode)) {
const match = /(\{\{[A-Za-z0-9_:]+\}\})/gm.exec(utilityCode)
const referencedUtility = match[0].replace('{{', '').replace('}}', '')
if (!context.utilities[referencedUtility]) {
// BEWARE - risk of infinite recursion
context.utilities[referencedUtility] = provideUtility(referencedUtility)
}
utilityCode = utilityCode.replace(match[0], context.utilities[referencedUtility])
}
}
}
}

return Blockly.JavaScript.provideFunction_(utilityName, utilityCode.split('\n'))
}

const processPlaceholder = (code, placeholder) => {
const placeholderFields = placeholder.replace('{{', '').replace('}}', '').split(':').map((f) => f.trim())
if (placeholderFields.length >= 2) {
const [placeholderType, placeholderName, placeholderOption] = placeholderFields
switch (placeholderType) {
case 'field':
context.fields[placeholderName] = block.getFieldValue(placeholderName)
return code.replace(placeholder, context.fields[placeholderName])
case 'input':
const order = placeholderOption ? Blockly.JavaScript['ORDER_' + placeholderOption.replace('ORDER_', '')] : Blockly.JavaScript.ORDER_NONE
context.inputs[placeholderName] = Blockly.JavaScript.valueToCode(block, placeholderName, order)
return code.replace(placeholder, context.inputs[placeholderName])
case 'utility':
if (!context.utilities[placeholderName]) {
context.utilities[placeholderName] = provideUtility(placeholderName)
}
return code.replace(placeholder, context.utilities[placeholderName])
case 'temp_name':
if (!context.uniqueIdentifiers[placeholderName]) {
const realm = placeholderOption ? Blockly.Variables[placeholderOption] : Blockly.Variables.NAME_TYPE
context.uniqueIdentifiers[placeholderName] = Blockly.JavaScript.variableDB_.getDistinctName(placeholderName, realm)
}
return code.replace(placeholder, context.uniqueIdentifiers[placeholderName])
case 'statements':
if (!context.statements[placeholderName]) {
context.statements[placeholderName] = Blockly.JavaScript.statementToCode(block, placeholderName)
}
return code.replace(placeholder, context.statements[placeholderName].replace(/^ {2}/, '').trim())
default:
return code.replace(placeholder, `/* Invalid placeholder type ${placeholderType}! */`)
}
}
}

if (!codeComponent || !codeComponent.config || !codeComponent.config.template) {
if (block.outputConnection) {
return [`/* missing implementation for value block ${blockTypeId} */`, Blockly.JavaScript.ORDER_NONE]
} else {
return `/* missing implementation for statement block ${blockTypeId} */\n`
}
}

const template = codeComponent.config.template
let code = template

while (/(\{\{[A-Za-z0-9_:]+\}\})/gm.test(code)) {
const match = /(\{\{[A-Za-z0-9_:]+\}\})/gm.exec(code)
code = processPlaceholder(code, match[0])
}

if (block.outputConnection) {
const order = codeComponent.config.order ? Blockly.JavaScript[codeComponent.config.order] : Blockly.JavaScript.ORDER_NONE
return [code, order]
} else {
return code
}
}

export const defineLibraryToolboxCategory = (library, f7) => (workspace) => {
let category = []
if (library && library.slots && library.slots.blocks) {
library.slots.blocks.forEach((blockTypeDefinition) => {
const xml = `<block type="${library.uid}_${blockTypeDefinition.config.type}" />`
const block = Blockly.Xml.textToDom(xml)
category.push(block)
})
}

return category
}

export const defineLibraries = (libraryDefinitions) => {
libraryDefinitions.forEach((library) => {
if (library.slots && library.slots.blocks) {
library.slots.blocks.forEach((block) => {
const blockTypeId = library.uid + '_' + block.config.type
Blockly.Blocks[blockTypeId] = {
init: function () {
this.jsonInit(block.config)
},
openhab: {
blockTypeId: blockTypeId,
library: library,
definition: block
}
}

Blockly.JavaScript[blockTypeId] = generateCodeForBlock
})
}
})
}
12 changes: 10 additions & 2 deletions bundles/org.openhab.ui/web/src/components/app.vue
Expand Up @@ -69,8 +69,9 @@
</ul>
</li>

<f7-list-item link="/developer/" :title="$t('sidebar.developerTools')" panel-close
:class="{ currentsection: currentUrl.indexOf('/developer/') >= 0 && currentUrl.indexOf('/developer/widgets') < 0 && currentUrl.indexOf('/developer/api-explorer') < 0 }">
<f7-list-item link="/developer/" :title="$t('sidebar.developerTools')" panel-close :animate="false"
:class="{ currentsection: currentUrl.indexOf('/developer/') >= 0 && currentUrl.indexOf('/developer/widgets') < 0 &&
currentUrl.indexOf('/developer/blocks') < 0 && currentUrl.indexOf('/developer/api-explorer') < 0 }">
<f7-icon slot="media" ios="f7:exclamationmark_shield_fill" aurora="f7:exclamationmark_shield_fill" md="material:extension" color="gray" />
</f7-list-item>
<li v-if="showDeveloperSubmenu">
Expand All @@ -79,10 +80,17 @@
:class="{ currentsection: currentUrl.indexOf('/developer/widgets') >= 0 }">
<f7-icon slot="media" f7="rectangle_on_rectangle_angled" color="gray" />
</f7-list-item>
<f7-list-item v-if="$store.getters.apiEndpoint('ui')" link="/developer/blocks/" title="Block Libraries" view=".view-main" panel-close :animate="false" no-chevron
:class="{ currentsection: currentUrl.indexOf('/developer/blocks') >= 0 }">
<f7-icon slot="media" f7="ticket" color="gray" />
</f7-list-item>
<f7-list-item link="/developer/api-explorer" title="API Explorer" view=".view-main" panel-close :animate="false" no-chevron
:class="{ currentsection: currentUrl.indexOf('/developer/api-explorer') >= 0 }">
<f7-icon slot="media" f7="burn" color="gray" />
</f7-list-item>
<!-- <f7-list-item link="" @click="$f7.emit('toggleDeveloperSidebar')" title="Sidebar" view=".view-main" panel-close :animate="false" no-chevron>
<f7-icon slot="media" :f7="$store.state.developerSidebar ? 'wrench_fill' : 'wrench'" color="gray" />
</f7-list-item> -->
</ul>
</li>
</f7-list>
Expand Down
Expand Up @@ -261,7 +261,7 @@ export default {
closeOnUnfocus: false,
completeSingle: self.mode && self.mode.indexOf('yaml') > 0,
hint (cm, option) {
if (self.mode.indexOf('application/vnd.openhab.uicomponent') === 0) {
if (self.mode.indexOf('application/vnd.openhab.uicomponent') === 0 && self.mode.indexOf('block') < 0) {
return componentsHint(cm, option, self.mode)
} else if (self.mode === 'application/vnd.openhab.rule+yaml') {
return rulesHint(cm, option, self.mode)
Expand Down
Expand Up @@ -235,6 +235,9 @@
<f7-list-button href="/developer/widgets/add" color="blue" :animate="false">
Create widget
</f7-list-button>
<f7-list-button href="/developer/blocks/add" color="blue" :animate="false">
Create block library
</f7-list-button>
</f7-list>
</f7-block>
</div>
Expand Down
13 changes: 13 additions & 0 deletions bundles/org.openhab.ui/web/src/js/routes.js
Expand Up @@ -49,6 +49,8 @@ const SchedulePage = () => import(/* webpackChunkName: "admin-schedule" */ '../p
const DeveloperToolsPage = () => import(/* webpackChunkName: "admin-devtools" */ '../pages/developer/developer-tools.vue')
const WidgetsListPage = () => import(/* webpackChunkName: "admin-devtools" */ '../pages/developer/widgets/widget-list.vue')
const WidgetEditPage = () => import(/* webpackChunkName: "admin-devtools" */ '../pages/developer/widgets/widget-edit.vue')
const BlocksListPage = () => import(/* webpackChunkName: "admin-devtools" */ '../pages/developer/blocks/blocks-list.vue')
const BlocksEditPage = () => import(/* webpackChunkName: "blockly-editor" */ '../pages/developer/blocks/blocks-edit.vue')
const ApiExplorerPage = () => import(/* webpackChunkName: "admin-devtools" */ '../pages/developer/api-explorer.vue')

const SetupWizardPage = () => import(/* webpackChunkName: "setup-wizard" */ '../pages/wizards/setup-wizard.vue')
Expand Down Expand Up @@ -268,6 +270,17 @@ export default [
}
]
},
{
path: 'blocks/',
async: loadAsync(BlocksListPage),
routes: [
{
path: ':uid',
beforeLeave: checkDirtyBeforeLeave,
async: loadAsync(BlocksEditPage, (routeTo) => (routeTo.params.uid === 'add') ? { createMode: true } : {})
}
]
},
{
path: 'add-items-dsl',
async: loadAsync(ItemsAddFromTextualDefinition)
Expand Down
@@ -0,0 +1,105 @@
<template>
<div class="block-preview" ref="blockPreview">
<code v-if="definitionError" class="definition-error text-color-red" ref="blockPreview">
{{ definitionError }}
</code>
<f7-menu style="position: absolute; right: 20px; top: 20px">
<f7-menu-item style="margin-left: auto" :text="currentBlock" dropdown>
<f7-menu-dropdown right>
<f7-menu-dropdown-item
v-for="block in blocksDefinition.slots.blocks" :key="block.config.type"
@click="displayCurrentBlock(block)" href="#" :text="block.config.type" />
</f7-menu-dropdown>
</f7-menu-item>
</f7-menu>
</div>
</template>

<style lang="stylus">
.block-preview
position absolute
top 0
left 0
right 0
bottom 0
.definition-error
position absolute
top calc(2rem + var(--f7-menu-item-height))
left 1rem
right 1rem
z-index 10000
.current-block-select
position absolute
top 20
right 20
z-index 10000
.blocklySvg
background-color inherit !important
.blocklyMainBackground
stroke inherit
.blocklyDropDownDiv
z-index 9000
</style>

<script>
import Blockly from 'blockly'
import Vue from 'vue'
Vue.config.ignoredElements = ['field', 'block', 'category', 'xml', 'mutation', 'value', 'sep']
export default {
props: ['blocksDefinition'],
data () {
return {
workspace: null,
definitionError: null,
currentBlock: null
}
},
mounted () {
this.initWorkspace()
this.defineBlocks()
},
watch: {
blocksDefinition () {
this.defineBlocks()
}
},
methods: {
initWorkspace () {
this.workspace = Blockly.inject(this.$refs.blockPreview, {
theme: (this.$f7.data.themeOptions.dark === 'dark') ? 'dark' : undefined,
trashcan: false,
readOnly: false
})
},
defineBlocks () {
try {
this.definitionError = null
if (this.blocksDefinition && this.blocksDefinition.slots && this.blocksDefinition.slots.blocks) {
this.blocksDefinition.slots.blocks.forEach((block) => {
Blockly.Blocks[block.config.type] = {
init: function () {
// const blockJson = JSON.stringify(block.config)
this.jsonInit(block.config)
}
}
})
this.displayCurrentBlock()
}
} catch (e) {
this.definitionError = e.toString()
}
},
displayCurrentBlock (block) {
if (block) this.currentBlock = block.config.type
if (!this.currentBlock) this.currentBlock = this.blocksDefinition.slots.blocks[0].config.type
let xml = '<xml>'
xml += `<block type="${this.currentBlock}" deletable="false"></block>`
xml += '</xml>'
this.workspace.clear()
Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xml), this.workspace)
}
}
}
</script>

0 comments on commit c685ca5

Please sign in to comment.