copy over all DSL files
This commit is contained in:
parent
a2afad6106
commit
2210518104
21 changed files with 1566 additions and 0 deletions
88
app/blocks/action_email.js
Normal file
88
app/blocks/action_email.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
export default {
|
||||
type: "action_email",
|
||||
|
||||
toolbox: {
|
||||
category: 'Actions',
|
||||
label: "Send yourself an email with custom subject and body templates."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: [
|
||||
"Sends an email with given SUBJECT and BODY",
|
||||
"---------------",
|
||||
"Parameters:",
|
||||
"SUBJECT - a template for generating the email subject",
|
||||
"BODY - a template for generating the email body",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "📧 Email", 'CENTER'],
|
||||
|
||||
[ "Subject:", {
|
||||
inputValue: "SUBJECT",
|
||||
shadow: {
|
||||
type: 'text_template',
|
||||
inputs: { TEMPLATE: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '{{ vars.feed_name }} feed has a new value: {{ vars.feed_value }}'
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}],
|
||||
|
||||
[ "Body:", {
|
||||
inputValue: "BODY",
|
||||
// check: 'String',
|
||||
shadow: {
|
||||
type: 'text_template',
|
||||
inputs: { TEMPLATE: {
|
||||
shadow: {
|
||||
type: 'text_multiline',
|
||||
fields: {
|
||||
TEXT: 'Hello!\nThe {{ vars.feed_name }} feed has a new value: {{ vars.value }}\nProcessed at: {{ vars.now }}'
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
emailAction: {
|
||||
subjectTemplate: JSON.parse(generator.valueToCode(block, 'SUBJECT', 0)),
|
||||
bodyTemplate: JSON.parse(generator.valueToCode(block, 'BODY', 0))
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.emailAction
|
||||
|
||||
return {
|
||||
type: "action_email",
|
||||
inputs: {
|
||||
// TODO: regenerators need to support nested shadow blocks
|
||||
SUBJECT: helpers.expressionToBlock(payload.subjectTemplate, { shadow: 'text_template' }),
|
||||
BODY: helpers.expressionToBlock(payload.bodyTemplate, { shadow: 'text_template' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
app/blocks/action_log.js
Normal file
61
app/blocks/action_log.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
export default {
|
||||
type: "action_log",
|
||||
|
||||
toolbox: {
|
||||
category: 'Actions',
|
||||
label: "Plug in a block to see its resolved value in the action's log."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: [
|
||||
"Executes the block you plug in and reveals its final value or error message.",
|
||||
"---------------",
|
||||
"Parameters:",
|
||||
"EXPRESSION - a block you'd like to see the resolved value of"
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
lines: [
|
||||
["Log:", {
|
||||
inputValue: 'EXPRESSION',
|
||||
// check: ['expression', 'String'],
|
||||
shadow: 'text'
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
logAction: {
|
||||
line: JSON.parse(generator.valueToCode(block, 'EXPRESSION', 0) || null)
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.logAction
|
||||
|
||||
if(!payload) {
|
||||
throw new Error("No data for action_log regenerator")
|
||||
}
|
||||
|
||||
return {
|
||||
type: "action_log",
|
||||
inputs: {
|
||||
EXPRESSION: helpers.expressionToBlock(payload.line, { shadow: 'text' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
app/blocks/action_publish.js
Normal file
68
app/blocks/action_publish.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
export default {
|
||||
type: "action_publish",
|
||||
|
||||
toolbox: {
|
||||
category: 'Actions',
|
||||
label: "Publish a given value to a given feed."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: [
|
||||
"Sends a given VALUE to a given FEED.",
|
||||
"---------------",
|
||||
"Parameters:",
|
||||
"VALUE - the value to write",
|
||||
"FEED - the feed to write to",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "📈 Publish", "CENTER" ],
|
||||
|
||||
[ "...value:", {
|
||||
inputValue: "VALUE",
|
||||
// check: [ "String", "Number" ],
|
||||
shadow: 'text'
|
||||
}],
|
||||
|
||||
[ "...to:", {
|
||||
inputValue: "FEED",
|
||||
// check: "feed",
|
||||
shadow: 'feed_selector'
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
publishAction: {
|
||||
feed: JSON.parse(generator.valueToCode(block, 'FEED', 0)),
|
||||
value: JSON.parse(generator.valueToCode(block, 'VALUE', 0))
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.publishAction
|
||||
|
||||
return {
|
||||
type: "action_publish",
|
||||
inputs: {
|
||||
FEED: helpers.expressionToBlock(payload.feed, { shadow: 'feed_selector' }),
|
||||
VALUE: helpers.expressionToBlock(payload.value, { shadow: 'text' }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
app/blocks/action_root.js
Normal file
65
app/blocks/action_root.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
|
||||
export default {
|
||||
type: "action_root",
|
||||
|
||||
toolbox: {},
|
||||
connections: {},
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
extensions: [ ],
|
||||
tooltip: "Drag some statement blocks into the \"Do\" list to build a custom Action"
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "Action Commands", "CENTER" ],
|
||||
|
||||
[ "Do:", {
|
||||
inputStatement: "EXPRESSIONS",
|
||||
// check: "expression"
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
let expressionsJson, expressions = []
|
||||
|
||||
try {
|
||||
expressionsJson = generator.statementToCode(block, `EXPRESSIONS`)
|
||||
|
||||
try {
|
||||
expressions = JSON.parse(`[${expressionsJson}]`)
|
||||
} catch(e) {
|
||||
console.error("Error parsing JSON:", expressionsJson)
|
||||
console.error(e)
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Error calling statementToCode on expression statements:")
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
version: "1.0.0-beta.1",
|
||||
settings: {},
|
||||
expressions,
|
||||
}, null, 2)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const { expressions } = blockObject
|
||||
|
||||
return {
|
||||
"type": "action_root",
|
||||
"movable": false,
|
||||
"deletable": false,
|
||||
"x": 50,
|
||||
"y": 50,
|
||||
"inputs": {
|
||||
"EXPRESSIONS": helpers.arrayToStatements(expressions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
app/blocks/action_sms.js
Normal file
74
app/blocks/action_sms.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
export default {
|
||||
type: "action_sms",
|
||||
|
||||
toolbox: {
|
||||
category: 'Actions',
|
||||
label: "Send yourself a templated SMS, or text message."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: [
|
||||
"(IO+ only)",
|
||||
"Sends a text message with a given BODY template.",
|
||||
"---------------",
|
||||
"Parameters:",
|
||||
"BODY - a template for generating the SMS body",
|
||||
].join('\n')
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "📲 SMS", "CENTER" ],
|
||||
|
||||
[ "Message:", {
|
||||
inputValue: "BODY",
|
||||
// check: "String",
|
||||
shadow: {
|
||||
type: 'text_template',
|
||||
inputs: { TEMPLATE: {
|
||||
shadow: {
|
||||
type: 'text_multiline',
|
||||
fields: {
|
||||
TEXT: [
|
||||
'The {{ vars.feed_name }} feed has a new',
|
||||
'value: {{ vars.feed_value }} at {{ vars.now }}'
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
smsAction: {
|
||||
bodyTemplate: JSON.parse(generator.valueToCode(block, 'BODY', 0))
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.smsAction
|
||||
|
||||
return {
|
||||
type: "action_sms",
|
||||
inputs: {
|
||||
// TODO: regenerators need to support nested shadow blocks
|
||||
BODY: helpers.expressionToBlock(payload.bodyTemplate, { shadow: 'text_template' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
app/blocks/action_webhook.js
Normal file
97
app/blocks/action_webhook.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
export default {
|
||||
type: "action_webhook",
|
||||
|
||||
toolbox: {
|
||||
category: 'Actions',
|
||||
label: "POST a given body template to a given web URL."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: [
|
||||
"Sends an HTTP POST request to a given URL, with a BODY template using FEED data.",
|
||||
"---------------",
|
||||
"Parameters:",
|
||||
"URL - a valid web location to send a request to",
|
||||
"BODY - a JSON template to render and POST",
|
||||
"FORM_ENCODE - optionally encode as form input",
|
||||
].join('\n')
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "🔗 Webhook", "CENTER" ],
|
||||
|
||||
[ "URL:", {
|
||||
inputValue: "URL",
|
||||
// check: "String",
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: { TEXT: 'https://...' }
|
||||
}
|
||||
}],
|
||||
|
||||
[ "Form Encode?", {
|
||||
field: "FORM_ENCODE",
|
||||
checked: false
|
||||
}],
|
||||
|
||||
[ "POST Body:", {
|
||||
inputValue: "BODY",
|
||||
// check: "String",
|
||||
shadow: {
|
||||
type: 'text_template',
|
||||
inputs: {
|
||||
TEMPLATE: {
|
||||
shadow: {
|
||||
type: 'text_multiline',
|
||||
fields: {
|
||||
TEXT:
|
||||
`{
|
||||
"id": "{{ vars.feed_id }}",
|
||||
"value": "{{ vars.feed_value }}"
|
||||
}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
webhookAction: {
|
||||
url: JSON.parse(generator.valueToCode(block, 'URL', 0)),
|
||||
bodyTemplate: JSON.parse(generator.valueToCode(block, 'BODY', 0)),
|
||||
formEncoded: block.getFieldValue('FORM_ENCODE') === 'TRUE'
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.webhookAction
|
||||
|
||||
return {
|
||||
type: "action_webhook",
|
||||
inputs: {
|
||||
URL: helpers.expressionToBlock(payload.url, { shadow: 'text' }),
|
||||
BODY: helpers.expressionToBlock(payload.bodyTemplate, { shadow: 'text_multiline' }),
|
||||
},
|
||||
fields: {
|
||||
FORM_ENCODE: payload.formEncoded ? 'TRUE' : 'FALSE'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
app/blocks/example_definition.js
Normal file
109
app/blocks/example_definition.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
export default {
|
||||
// required, unique
|
||||
// identifier for this block, used all over Blockly
|
||||
type: "block_type",
|
||||
|
||||
// determines where the block shows up in the toolbox
|
||||
toolbox: {
|
||||
// category must match a category in the toolbox config
|
||||
category: 'Category Name',
|
||||
// shorthand that adds an extra toolbox label
|
||||
label: 'Helpful text that appears alongside the block in the toolbox'
|
||||
},
|
||||
|
||||
// same as Blockly JSON
|
||||
visualization: {
|
||||
inputsInline: false,
|
||||
colour: 100,
|
||||
tooltip: "",
|
||||
helpUrl: ""
|
||||
},
|
||||
|
||||
// list of extension names registered from extensions directory, or...
|
||||
extensions: [ 'populateFeedDropdown' ],
|
||||
|
||||
// object naming and defining extensions inline here
|
||||
extensions: {
|
||||
populateFeedDropdown: ({ block, data, Blockly }) => {
|
||||
/* do extension stuff */
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: consider something simpler, maybe leftOutput, topOutput, and bottomCheck?
|
||||
// Block connections
|
||||
connections: {
|
||||
// mode options:
|
||||
// - falsy or "none": no connections
|
||||
// - "value": has a side input, "output" maps to "output"
|
||||
// - statements
|
||||
// - "statement": top and bottom connections, "output" maps to "previousStatement", "next" maps to "nextStatement"
|
||||
// - "statement:first": bottom connection, "output" invalid, "next" maps to "nextStatement"
|
||||
// - "statement:last": top connection, "output" maps to "previousStatement", "next" invalid
|
||||
mode: "value",
|
||||
output: "trigger",
|
||||
},
|
||||
|
||||
// describes each line of the block, from top to bottom
|
||||
lines: [
|
||||
// STRING LINES
|
||||
"line contents", // simple text line, default alignment,
|
||||
|
||||
// ARRAY LINES
|
||||
// [ string, string ]: [ text, alignment ]
|
||||
[ "line contents", "alignment" ],
|
||||
|
||||
// [ string, object ]: [ text, { inputValue, inputStatement, field, fields } ]
|
||||
[ "line $FIELD_NAME contents", { // template string: field gets embedded where its name is referenced
|
||||
// for a single block input
|
||||
inputValue: 'INPUT_VALUE_NAME',
|
||||
// check: 'input_block_output',
|
||||
shadow: 'block_type_to_shadow', // -> { shadow: { type: 'block_type_to_shadow' }}
|
||||
shadow: {
|
||||
type: 'block_type_to_shadow',
|
||||
inputs: {}, // fill in the inputs on the shadowed block
|
||||
fields: {}, // fill in the fields on the shadowed block
|
||||
},
|
||||
|
||||
// for multiple block inputs
|
||||
inputStatement: 'INPUT_STATEMENT_NAME',
|
||||
|
||||
// for a single field input
|
||||
field: 'FIELD_NAME',
|
||||
text: 'whatever', // makes a text field
|
||||
spellcheck: true, // text field option
|
||||
checked: true, // makes a checkbox field
|
||||
options: [ // makes a dropdown field
|
||||
['user text', 'computer id'],
|
||||
// ...
|
||||
],
|
||||
|
||||
// for multiple field inputs
|
||||
// hint: use template strings (see above comment) to choose where inline fields go
|
||||
fields: {
|
||||
// each field name is a key
|
||||
FIELD_NAME: {
|
||||
// same options as above for singular field
|
||||
text: 'whatever', // makes a text field
|
||||
spellcheck: true, // text field option
|
||||
checked: true, // makes a checkbox field
|
||||
options: [ // makes a dropdown field
|
||||
['user text', 'computer id'],
|
||||
// ...
|
||||
],
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
// generators for this block type
|
||||
// these get aggregated and registered together
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
// fetch connected block: block.getInputTargetBlock('INPUT_VALUE_NAME')?.type,
|
||||
// generate connected block: generator.valueToCode(block, 'INPUT_VALUE_NAME', 0)
|
||||
// field value: block.getFieldValue('FIELD_NAME')
|
||||
|
||||
return [ {}, 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
61
app/blocks/feed_selector.js
Normal file
61
app/blocks/feed_selector.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
export default {
|
||||
type: "feed_selector",
|
||||
|
||||
toolbox: {
|
||||
category: 'Feeds'
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: 300,
|
||||
tooltip: "The last value of this feed or component, always a String"
|
||||
},
|
||||
|
||||
extensions: {
|
||||
populateFeedDropdown: ({ block, data, Blockly }) => {
|
||||
const { feedOptions } = data
|
||||
|
||||
if(!feedOptions) {
|
||||
console.error(`No feedOptions in extension data to populate dropdowns!`)
|
||||
return
|
||||
}
|
||||
|
||||
const input = block.getInput('')
|
||||
input.removeField("FEED_KEY")
|
||||
input.appendField(new Blockly.FieldDropdown(feedOptions), "FEED_KEY")
|
||||
}
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "Feed:", {
|
||||
field: "FEED_KEY",
|
||||
options: [
|
||||
[ "Loading Feeds...", "" ],
|
||||
]
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
key = block.getFieldValue('FEED_KEY'),
|
||||
payload = JSON.stringify({
|
||||
feed: { key }
|
||||
})
|
||||
|
||||
return [ payload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: blockObject => {
|
||||
const payload = blockObject.feed
|
||||
|
||||
return {
|
||||
type: "feed_selector",
|
||||
fields: {
|
||||
FEED_KEY: payload.key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
app/blocks/io_controls_if.js
Normal file
93
app/blocks/io_controls_if.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import mutator from './io_controls_if/mutator.js'
|
||||
|
||||
|
||||
export default {
|
||||
type: 'io_controls_if',
|
||||
|
||||
toolbox: {
|
||||
category: 'Logic'
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: 60,
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
[ "if", { inputValue: 'IF0', shadow: 'logic_boolean' }],
|
||||
[ "do", { inputStatement: 'THEN0' }],
|
||||
[ "else if", { inputDummy: 'ELSE_IF_LABEL' }],
|
||||
[ "else", { inputDummy: 'ELSE_LABEL' }]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
conditional: {
|
||||
ifThens: []
|
||||
}
|
||||
}
|
||||
|
||||
let index = 0
|
||||
while(block.getInput(`IF${index}`)) {
|
||||
const
|
||||
ifClause = generator.valueToCode(block, `IF${index}`, 0) || 'null',
|
||||
thenClause = generator.statementToCode(block, `THEN${index}`) || ''
|
||||
|
||||
payload.conditional.ifThens.push({
|
||||
if: JSON.parse(ifClause),
|
||||
then: JSON.parse(`[ ${thenClause} ]`),
|
||||
})
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
if(block.getInput('ELSE')) {
|
||||
const elseClause = generator.statementToCode(block, 'ELSE') || ''
|
||||
|
||||
payload.conditional.else = JSON.parse(`[${ elseClause }]`)
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (bytecode, helpers) => {
|
||||
const payload = bytecode.conditional
|
||||
|
||||
if(!payload) {
|
||||
throw new Error("No data for io_controls_if regenerator")
|
||||
}
|
||||
|
||||
const
|
||||
{ ifThens } = payload,
|
||||
inputs = {}
|
||||
|
||||
ifThens.forEach((item, index) => {
|
||||
if(item.if !== null) { inputs[`IF${index}`] = helpers.expressionToBlock(item.if, { shadow: 'logic_boolean' }) }
|
||||
if(item.then) { inputs[`THEN${index}`] = helpers.arrayToStatements(item.then) }
|
||||
})
|
||||
|
||||
if(payload.else) {
|
||||
inputs.ELSE = helpers.arrayToStatements(payload.else)
|
||||
}
|
||||
|
||||
return {
|
||||
type: "io_controls_if",
|
||||
inputs,
|
||||
extraState: {
|
||||
elseIfCount: ifThens.length-1,
|
||||
elsePresent: !!payload.else
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
app/blocks/io_controls_if/mutator.js
Normal file
163
app/blocks/io_controls_if/mutator.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import Blockly from 'blockly'
|
||||
|
||||
/** Dynamic else-if and else support */
|
||||
export default {
|
||||
elseIfCount: 0,
|
||||
elsePresent: false,
|
||||
|
||||
// helper to touch the above state and automatically fire events on change
|
||||
setProperty: function(propertyName, propertyValue, element, name) {
|
||||
const old = this[propertyName]
|
||||
if(old == propertyValue) { return }
|
||||
|
||||
this[propertyName] = propertyValue
|
||||
|
||||
Blockly.Events.fire(new Blockly.Events.BlockChange(this, element, name,
|
||||
{ [propertyName]: old }, { [propertyName]: this[propertyName] }
|
||||
))
|
||||
},
|
||||
|
||||
saveExtraState: function() {
|
||||
return { elseIfCount: this.elseIfCount, elsePresent: this.elsePresent }
|
||||
},
|
||||
|
||||
loadExtraState: function(state) {
|
||||
this.initializeElseIfs(state.elseIfCount)
|
||||
this.initializeElse(state.elsePresent)
|
||||
},
|
||||
|
||||
// dynamic else-if-then support
|
||||
initializeElseIfs: function(elseIfCount) {
|
||||
this.addElseIf(elseIfCount)
|
||||
this.insertPlusElseIfButton()
|
||||
},
|
||||
|
||||
insertPlusElseIfButton: function() {
|
||||
const elseIfLabel = this.getInput('ELSE_IF_LABEL')
|
||||
elseIfLabel.removeField('PLUS_ELSE_IF', true)
|
||||
elseIfLabel.insertFieldAt(0, this.plusField(() => this.addElseIf()), 'PLUS_ELSE_IF')
|
||||
},
|
||||
|
||||
addElseIf: function(howMany=1) {
|
||||
for(let i = 1; i <= howMany; i++) {
|
||||
this.addElseIfAt(this.elseIfCount + i)
|
||||
}
|
||||
this.setProperty('elseIfCount', this.elseIfCount+howMany)
|
||||
},
|
||||
|
||||
addElseIfAt: function(index) {
|
||||
this
|
||||
.appendValueInput(`IF${index}`)
|
||||
.appendField(this.minusField(() => this.removeElseIfAt(index)))
|
||||
.appendField(new Blockly.FieldLabel("else if"))
|
||||
this
|
||||
.appendStatementInput(`THEN${index}`)
|
||||
.appendField(new Blockly.FieldLabel("do"))
|
||||
// move them up above the last items
|
||||
this.moveInputBefore(`IF${index}`, 'ELSE_IF_LABEL')
|
||||
this.moveInputBefore(`THEN${index}`, 'ELSE_IF_LABEL')
|
||||
},
|
||||
|
||||
removeElseIfAt: function(targetIndex) {
|
||||
// detect my parent IF input and its THEN
|
||||
const
|
||||
ifName = `IF${targetIndex}`,
|
||||
thenName = `THEN${targetIndex}`,
|
||||
ifInput = this.getInput(ifName),
|
||||
thenInput = this.getInput(thenName)
|
||||
|
||||
// sever the connections
|
||||
const ifConnection = ifInput.connection
|
||||
if(ifConnection.isConnected()) { ifConnection.disconnect() }
|
||||
const thenConnection = thenInput.connection
|
||||
if(thenConnection.isConnected()) { thenConnection.disconnect() }
|
||||
|
||||
// shuffle remaining if/then connections down
|
||||
const
|
||||
inputs = this.inputList,
|
||||
nextIndex = inputs.indexOf(ifInput) + 2
|
||||
|
||||
let i = nextIndex
|
||||
for (let input; (input = inputs[i]); i++) {
|
||||
if (input.name == 'ELSE_LABEL') { break }
|
||||
|
||||
const { targetConnection } = input.connection || {}
|
||||
if (targetConnection) {
|
||||
inputs[i - 2].connection.connect(targetConnection)
|
||||
}
|
||||
}
|
||||
|
||||
// remove the last/highest index ifThen
|
||||
const indexToRemove = Math.floor((i-2)/2)
|
||||
this.removeInput(`IF${indexToRemove}`)
|
||||
this.removeInput(`THEN${indexToRemove}`)
|
||||
this.bumpNeighbours()
|
||||
|
||||
this.setProperty('elseIfCount', this.elseIfCount-1)
|
||||
},
|
||||
|
||||
// dynamic else support
|
||||
initializeElse: function(elsePresent) {
|
||||
elsePresent ? this.addElse() : this.removeElse()
|
||||
},
|
||||
|
||||
addElse: function() {
|
||||
const elseLabel = this.getInput('ELSE_LABEL')
|
||||
|
||||
// swap the plus to minus
|
||||
elseLabel.removeField('PLUS_ELSE', true)
|
||||
elseLabel.removeField('MINUS_ELSE', true)
|
||||
elseLabel.insertFieldAt(0, this.minusField(() => this.removeElse()), 'MINUS_ELSE')
|
||||
|
||||
// add the input, write its label
|
||||
if(!this.getInput('ELSE')) {
|
||||
this
|
||||
.appendStatementInput('ELSE')
|
||||
.appendField(new Blockly.FieldLabel("do"))
|
||||
}
|
||||
|
||||
this.setProperty('elsePresent', true, 'input', 'ELSE')
|
||||
},
|
||||
|
||||
removeElse: function() {
|
||||
const elseLabel = this.getInput('ELSE_LABEL')
|
||||
// swap the minus to plus
|
||||
elseLabel.removeField('MINUS_ELSE', true)
|
||||
elseLabel.removeField('PLUS_ELSE', true)
|
||||
elseLabel.insertFieldAt(0, this.plusField(() => this.addElse()), 'PLUS_ELSE')
|
||||
|
||||
const elseInput = this.getInput('ELSE')
|
||||
|
||||
// sever the connection
|
||||
if(elseInput) {
|
||||
const { connection } = elseInput
|
||||
if(connection.isConnected()) { connection.disconnect() }
|
||||
// remove the input
|
||||
this.removeInput('ELSE')
|
||||
}
|
||||
|
||||
this.setProperty('elsePresent', false)
|
||||
},
|
||||
|
||||
plusImage:
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC' +
|
||||
'9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMT' +
|
||||
'ggMTBoLTR2LTRjMC0xLjEwNC0uODk2LTItMi0ycy0yIC44OTYtMiAybC4wNzEgNGgtNC4wNz' +
|
||||
'FjLTEuMTA0IDAtMiAuODk2LTIgMnMuODk2IDIgMiAybDQuMDcxLS4wNzEtLjA3MSA0LjA3MW' +
|
||||
'MwIDEuMTA0Ljg5NiAyIDIgMnMyLS44OTYgMi0ydi00LjA3MWw0IC4wNzFjMS4xMDQgMCAyLS' +
|
||||
'44OTYgMi0ycy0uODk2LTItMi0yeiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+Cg==',
|
||||
|
||||
plusField: function(onClick) {
|
||||
return new Blockly.FieldImage(this.plusImage, 15, 15, undefined, onClick)
|
||||
},
|
||||
|
||||
minusImage:
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAw' +
|
||||
'MC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPS' +
|
||||
'JNMTggMTFoLTEyYy0xLjEwNCAwLTIgLjg5Ni0yIDJzLjg5NiAyIDIgMmgxMmMxLjEwNCAw' +
|
||||
'IDItLjg5NiAyLTJzLS44OTYtMi0yLTJ6IiBmaWxsPSJ3aGl0ZSIgLz48L3N2Zz4K',
|
||||
|
||||
minusField: function(onClick) {
|
||||
return new Blockly.FieldImage(this.minusImage, 15, 15, undefined, onClick)
|
||||
},
|
||||
}
|
||||
76
app/blocks/io_logic_compare.js
Normal file
76
app/blocks/io_logic_compare.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
export default {
|
||||
type: 'io_logic_compare',
|
||||
|
||||
toolbox: {
|
||||
category: 'Math',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
inputsInline: true,
|
||||
colour: 224,
|
||||
tooltip: [
|
||||
"Compare two numeric values in various ways.",
|
||||
"-",
|
||||
"Inputs:",
|
||||
"---------------",
|
||||
"Comparator - check equality, inequality, greater than, greater than or equal to, less than, less than or equal to?",
|
||||
"Number A - the first number",
|
||||
"Number B - the second number",
|
||||
"-",
|
||||
"Casting:",
|
||||
"---------------",
|
||||
"both inputs are coerced to floating point numbers",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
lines: [
|
||||
["", { inputValue: 'A', shadow: 'math_number' }],
|
||||
[ "", {
|
||||
field: "OP",
|
||||
options: [
|
||||
['=', 'EQ'],
|
||||
['\u2260', 'NEQ'],
|
||||
['\u200F<', 'LT'],
|
||||
['\u200F\u2264', 'LTE'],
|
||||
['\u200F>', 'GT'],
|
||||
['\u200F\u2265', 'GTE'],
|
||||
]
|
||||
}],
|
||||
["", { inputValue: 'B', shadow: 'math_number' }]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
comparator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
compare: {
|
||||
left: JSON.parse(leftExp),
|
||||
comparator: comparator?.toLowerCase() || null,
|
||||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ comparator, left, right } = blockObject.compare,
|
||||
fields = {
|
||||
OP: comparator?.toUpperCase()
|
||||
},
|
||||
inputs = {
|
||||
A: helpers.expressionToBlock(left, { shadow: 'math_number' }),
|
||||
B: helpers.expressionToBlock(right, { shadow: 'math_number' }),
|
||||
}
|
||||
|
||||
return { type: 'io_logic_compare', fields, inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
42
app/blocks/io_logic_negate.js
Normal file
42
app/blocks/io_logic_negate.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export default {
|
||||
type: 'io_logic_negate',
|
||||
|
||||
toolbox: {
|
||||
category: 'Logic',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: 60,
|
||||
},
|
||||
|
||||
lines: [
|
||||
["not", { inputValue: 'EXPRESSION', shadow: 'logic_boolean' }]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
operand = generator.valueToCode(block, 'EXPRESSION', 0) || null,
|
||||
payload = {
|
||||
negate: {
|
||||
target: JSON.parse(operand)
|
||||
}
|
||||
}
|
||||
|
||||
return [ JSON.stringify(payload), 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.negate
|
||||
|
||||
return {
|
||||
type: 'io_logic_negate',
|
||||
inputs: {
|
||||
EXPRESSION: helpers.expressionToBlock(payload.target, { shadow: 'logic_boolean' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
app/blocks/io_logic_operation.js
Normal file
59
app/blocks/io_logic_operation.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
export default {
|
||||
type: 'io_logic_operation',
|
||||
|
||||
toolbox: {
|
||||
category: 'Logic',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
inputsInline: true,
|
||||
colour: 60,
|
||||
},
|
||||
|
||||
lines: [
|
||||
["", { inputValue: 'A', shadow: 'logic_boolean' }],
|
||||
["", {
|
||||
field: 'OP',
|
||||
options: [
|
||||
['and', 'AND'],
|
||||
['or', 'OR'],
|
||||
]
|
||||
}],
|
||||
["", { inputValue: 'B', shadow: 'logic_boolean' }],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
operator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
logic: {
|
||||
left: JSON.parse(leftExp),
|
||||
comparator: operator?.toLowerCase() || null,
|
||||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ comparator, left, right } = blockObject.logic,
|
||||
fields = {
|
||||
OP: comparator?.toUpperCase()
|
||||
},
|
||||
inputs = {
|
||||
A: helpers.expressionToBlock(left, { shadow: 'logic_boolean' }),
|
||||
B: helpers.expressionToBlock(right, { shadow: 'logic_boolean' }),
|
||||
}
|
||||
|
||||
return { type: 'io_logic_operation', fields, inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/blocks/io_logic_ternary.js
Normal file
37
app/blocks/io_logic_ternary.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
export default {
|
||||
type: 'io_logic_ternary',
|
||||
|
||||
toolbox: {
|
||||
category: 'Logic',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: 60,
|
||||
},
|
||||
|
||||
lines: [
|
||||
["if", { inputValue: 'IF', shadow: 'logic_boolean' }],
|
||||
["then", { inputValue: 'THEN', shadow: 'logic_boolean' }],
|
||||
["else", { inputValue: 'ELSE', shadow: 'logic_boolean' }],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
ifLogic = generator.valueToCode(block, 'IF', 0),
|
||||
thenLogic = generator.valueToCode(block, 'THEN', 0),
|
||||
elseLogic = generator.valueToCode(block, 'ELSE', 0),
|
||||
blockPayload = JSON.stringify({
|
||||
conditional: {
|
||||
ifThens: [
|
||||
{ if: ifLogic ? JSON.parse(ifLogic) : null,
|
||||
then: thenLogic ? JSON.parse(thenLogic) : null }
|
||||
],
|
||||
else: elseLogic ? JSON.parse(elseLogic) : null
|
||||
}
|
||||
})
|
||||
|
||||
return [blockPayload, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
78
app/blocks/io_math_arithmetic.js
Normal file
78
app/blocks/io_math_arithmetic.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
export default {
|
||||
type: 'io_math_arithmetic',
|
||||
|
||||
toolbox: {
|
||||
category: 'Math',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
inputsInline: true,
|
||||
colour: 230,
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "", { inputValue: 'A', shadow: 'math_number'}],
|
||||
[ "", {
|
||||
field: 'OP',
|
||||
options: [
|
||||
['+', 'ADD'],
|
||||
['-', 'MINUS'],
|
||||
['x', 'MULTIPLY'],
|
||||
['/', 'DIVIDE'],
|
||||
['^', 'POWER'],
|
||||
]
|
||||
}],
|
||||
[ "", { inputValue: 'B', shadow: 'math_number'}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
operatorMap = {
|
||||
ADD: '+',
|
||||
MINUS: '-',
|
||||
MULTIPLY: '*',
|
||||
DIVIDE: '/',
|
||||
POWER: '^'
|
||||
},
|
||||
operator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
arithmetic: {
|
||||
left: JSON.parse(leftExp),
|
||||
operator: operator
|
||||
? operatorMap[operator]
|
||||
: null,
|
||||
right: JSON.parse(rightExp)
|
||||
}
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
payload = blockObject.arithmetic,
|
||||
operatorMap = {
|
||||
'+': 'ADD',
|
||||
'-': 'MINUS',
|
||||
'*': 'MULTIPLY',
|
||||
'/': 'DIVIDE',
|
||||
'^': 'POWER',
|
||||
},
|
||||
fields = {
|
||||
OP: operatorMap[payload.operator]
|
||||
},
|
||||
inputs = {
|
||||
A: helpers.expressionToBlock(payload.left, { shadow: 'math_number' }),
|
||||
B: helpers.expressionToBlock(payload.right, { shadow: 'math_number' }),
|
||||
}
|
||||
|
||||
return { type: 'io_math_arithmetic', fields, inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
78
app/blocks/text_compare.js
Normal file
78
app/blocks/text_compare.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
export default {
|
||||
type: 'text_compare',
|
||||
|
||||
toolbox: {
|
||||
category: 'Text',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
inputsInline: true,
|
||||
colour: 180,
|
||||
tooltip: [
|
||||
"Compare two chunks of text for equality or inequality.",
|
||||
"-",
|
||||
"Inputs:",
|
||||
"---------------",
|
||||
"Comparator - check for equality or inequality?",
|
||||
"Text A - the first string of text",
|
||||
"Text B - the second string of text",
|
||||
"-",
|
||||
"Casting:",
|
||||
"---------------",
|
||||
"both inputs are coerced to strings",
|
||||
"-",
|
||||
"Options: (not implemented)",
|
||||
"---------------",
|
||||
"Trim? - trim whitespace from the front and back of the input strings",
|
||||
"Trim front? - trim whitespace from the front of the input strings",
|
||||
"Trim back? - trim whitespace from the back of the input strings",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
lines: [
|
||||
["", { inputValue: 'A', shadow: 'text' }],
|
||||
[ "", {
|
||||
field: "OP",
|
||||
options: [
|
||||
['=', 'EQ'],
|
||||
['\u2260', 'NEQ'],
|
||||
]
|
||||
}],
|
||||
["", { inputValue: 'B', shadow: 'text' }]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
comparator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
textCompare: {
|
||||
left: JSON.parse(leftExp),
|
||||
comparator: comparator?.toLowerCase() || null,
|
||||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ comparator, left, right } = blockObject.textCompare,
|
||||
fields = {
|
||||
OP: comparator?.toUpperCase()
|
||||
},
|
||||
inputs = {
|
||||
A: helpers.expressionToBlock(left, { shadow: 'text' }),
|
||||
B: helpers.expressionToBlock(right, { shadow: 'text' }),
|
||||
}
|
||||
|
||||
return { type: 'text_compare', fields, inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
60
app/blocks/text_join.js
Normal file
60
app/blocks/text_join.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
export default {
|
||||
type: 'io_text_join',
|
||||
|
||||
toolbox: {
|
||||
category: 'Text',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
inputsInline: true,
|
||||
colour: 180,
|
||||
tooltip: [
|
||||
"Join two chunks of text into one.",
|
||||
"-",
|
||||
"Inputs:",
|
||||
"---------------",
|
||||
"Text A - the first string of text",
|
||||
"Text B - the second string of text",
|
||||
"-",
|
||||
"Casting:",
|
||||
"---------------",
|
||||
"both inputs are coerced to strings",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
lines: [
|
||||
["", { inputValue: 'A', shadow: 'text' }],
|
||||
["+", ""],
|
||||
["", { inputValue: 'B', shadow: 'text' }]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
textJoin: {
|
||||
left: JSON.parse(leftExp),
|
||||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ left, right } = blockObject.textJoin,
|
||||
inputs = {
|
||||
A: helpers.expressionToBlock(left, { shadow: 'text' }),
|
||||
B: helpers.expressionToBlock(right, { shadow: 'text' }),
|
||||
}
|
||||
|
||||
return { type: 'io_text_join', inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
60
app/blocks/text_regex.js
Normal file
60
app/blocks/text_regex.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
export default {
|
||||
type: 'text_regex',
|
||||
|
||||
toolbox: {
|
||||
category: 'Text',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: 180,
|
||||
tooltip: [
|
||||
"Check for a regex match",
|
||||
"-",
|
||||
"Inputs:",
|
||||
"---------------",
|
||||
// "Text A - the first string of text",
|
||||
// "Text B - the second string of text",
|
||||
"-",
|
||||
"Casting:",
|
||||
"---------------",
|
||||
"-",
|
||||
"Options:",
|
||||
"---------------",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
lines: [
|
||||
["Regex:", { inputValue: 'REGEX', shadow: 'text' }],
|
||||
["Matches?", { inputValue: 'TARGET', shadow: 'text' }]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
regexExp = generator.valueToCode(block, 'REGEX', 0) || null,
|
||||
targetExp = generator.valueToCode(block, 'TARGET', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
textRegex: {
|
||||
regex: JSON.parse(regexExp),
|
||||
target: JSON.parse(targetExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ regex, target } = blockObject.textRegex,
|
||||
inputs = {
|
||||
REGEX: helpers.expressionToBlock(regex, { shadow: 'text' }),
|
||||
TARGET: helpers.expressionToBlock(target, { shadow: 'text' }),
|
||||
}
|
||||
|
||||
return { type: 'text_regex', inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
68
app/blocks/text_template.js
Normal file
68
app/blocks/text_template.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
export default {
|
||||
type: 'text_template',
|
||||
|
||||
toolbox: {
|
||||
category: 'Text',
|
||||
},
|
||||
|
||||
visualization: {
|
||||
inputsInline: true,
|
||||
helpUrl: "https://shopify.github.io/liquid/basics/introduction/",
|
||||
colour: 180,
|
||||
tooltip: [
|
||||
"Render a text template. Special forms surrounded by {{ curly_braces }}",
|
||||
"will be replaced with their current value during the action run. (See",
|
||||
"'Template Variables' below for a list of available forms.)",
|
||||
"-",
|
||||
"Inputs:",
|
||||
"---------------",
|
||||
"Text - the text to render with the template engine",
|
||||
"-",
|
||||
"Template Variables:",
|
||||
"---------------",
|
||||
"{{ variables.var_name }} - get the value of a variable you have defined",
|
||||
" with name 'var_name'",
|
||||
"{{ vars.var_name }} - shorthand for same as above",
|
||||
"{{ variables['var name'] }} - get the value of a variable you have",
|
||||
" defined with name 'var name' (allows spaces in variable names",
|
||||
"{{ vars.['var name'] }} - shorthand for same as above",
|
||||
"{{ user.name }} - your user's name",
|
||||
"{{ user.username }} - your user's username",
|
||||
"{{ feeds['group_key.feed_key'].name }} - access a feed with key",
|
||||
" 'group_key.feed_key' and get its name",
|
||||
"{{ feeds[...].key }} - ...get its key",
|
||||
"{{ feeds[...].value }} - ...get its last value",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
lines: [
|
||||
["{{ %1", { inputValue: 'TEMPLATE', shadow: 'text_multiline' }],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
template = generator.valueToCode(block, 'TEMPLATE', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
textTemplate: {
|
||||
template: JSON.parse(template)
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ template } = blockObject.textTemplate,
|
||||
inputs = {
|
||||
TEMPLATE: helpers.expressionToBlock(template, { shadow: 'text' })
|
||||
}
|
||||
|
||||
return { type: 'text_template', inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
115
app/toolbox/index.js
Normal file
115
app/toolbox/index.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// build toolbox from a config and blocks that reference it
|
||||
import { compact, forEach, includes, isEmpty, isString, keyBy, map, mapValues, filter, flatMap, reduce } from 'lodash-es'
|
||||
|
||||
import { allBlockDefinitions } from '../blocks/index.js'
|
||||
|
||||
|
||||
const
|
||||
SEP = '---',
|
||||
TOOLBOX_CONFIG = [
|
||||
{ name: 'Logic', colour: 60 },
|
||||
{ name: 'Math', colour: 120 },
|
||||
{ name: 'Text', colour: 180 },
|
||||
// TODO: build our own variables category & blocks
|
||||
{ name: 'Variables', colour: 240, extras: { custom: "VARIABLE" }},
|
||||
{ name: 'Feeds', colour: 300 },
|
||||
{ name: 'Actions', colour: 360 },
|
||||
],
|
||||
|
||||
generateToolboxContents = () => map(TOOLBOX_CONFIG, category =>
|
||||
// inject other kinds of toolbox objects here
|
||||
category === SEP
|
||||
? { kind: 'sep' }
|
||||
: {
|
||||
kind: 'category',
|
||||
name: category.name,
|
||||
colour: (category.colour === 0) ? "0" : category.colour,
|
||||
...category.extras,
|
||||
contents: generateCategoryContents(category)
|
||||
}
|
||||
),
|
||||
|
||||
generateCategoryContents = ({ name }) =>
|
||||
flatMap(selectBlocksByCategoryName(name), blockToLabelAndBlock),
|
||||
|
||||
generateAllBlocks = () =>
|
||||
flatMap(allBlockDefinitions, blockToLabelAndBlock),
|
||||
|
||||
selectBlocksByCategoryName = name =>
|
||||
filter(allBlockDefinitions, def =>
|
||||
def.toolbox?.category === name || includes(def.toolbox?.categories, name)
|
||||
),
|
||||
|
||||
blockToLabelAndBlock = block => compact([
|
||||
{
|
||||
kind: 'block',
|
||||
type: block.type,
|
||||
inputs: blockToInputs(block),
|
||||
fields: blockToFields(block)
|
||||
}, block.toolbox.label
|
||||
? { kind: 'label',
|
||||
text: block.toolbox.label
|
||||
}
|
||||
: null
|
||||
]),
|
||||
|
||||
blockToInputs = ({ lines }) => {
|
||||
if(!lines) { return }
|
||||
|
||||
const inputs =
|
||||
mapValues(
|
||||
keyBy(
|
||||
filter(
|
||||
map(lines, '[1]'),
|
||||
"inputValue"),
|
||||
"inputValue"),
|
||||
shadowPropertyToInput)
|
||||
|
||||
return isEmpty(inputs) ? undefined : inputs
|
||||
},
|
||||
|
||||
blockToFields = ({ lines }) => {
|
||||
if(!lines) { return }
|
||||
// get every field that contains a "value" property
|
||||
const fields =
|
||||
reduce(
|
||||
map(
|
||||
filter(
|
||||
map(lines, '[1]'),
|
||||
"fields"),
|
||||
"fields"),
|
||||
(acc, fields) => {
|
||||
forEach(fields, (field, fieldKey) => {
|
||||
if(field.value){
|
||||
acc[fieldKey] = field.value
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// produces:
|
||||
// {
|
||||
// FIELD_NAME: field_value,
|
||||
// ...
|
||||
// }
|
||||
return isEmpty(fields) ? undefined : fields
|
||||
},
|
||||
|
||||
shadowPropertyToInput = ({ shadow }) =>
|
||||
isString(shadow) // is shorthand?
|
||||
? { shadow: { type: shadow }} // expand to full object
|
||||
: { shadow } // set as shadow value
|
||||
|
||||
export const toolbox = {
|
||||
// Use given categories, fill them with blocks that declare those categories
|
||||
kind: 'categoryToolbox',
|
||||
contents: generateToolboxContents()
|
||||
|
||||
// No categories, just a gutter full of blocks
|
||||
// kind: 'flyoutToolbox',
|
||||
// contents: generateAllBlocks()
|
||||
}
|
||||
|
||||
|
||||
export default toolbox
|
||||
14
app/workspace/workspace.json
Normal file
14
app/workspace/workspace.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"blocks": {
|
||||
"languageVersion": 0,
|
||||
"blocks": [
|
||||
{
|
||||
"type": "action_root",
|
||||
"movable": false,
|
||||
"deletable": false,
|
||||
"x": 50,
|
||||
"y": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue