Compare commits

...

41 commits

Author SHA1 Message Date
Loren Norman
b959330af6 break blockly auto-factor functionality
Some checks failed
Build Docs and Deploy to Pages / build (push) Has been cancelled
Build Docs and Deploy to Pages / Deploy (push) Has been cancelled
2025-07-29 17:01:14 -04:00
Loren Norman
f237342563 improve default feed names 2025-07-29 16:55:13 -04:00
Loren Norman
f463143199 coordinate screenshots via data-id 2025-07-29 16:46:46 -04:00
Loren Norman
02dd9b9c1c explicit exit 2025-07-29 16:32:09 -04:00
Loren Norman
296bf65816 point to cypress config, check that vite closes 2025-07-29 16:24:23 -04:00
Loren Norman
5b5f9d9c4f a bold block image export script 2025-07-29 16:11:19 -04:00
Loren Norman
552fffaaf6 set up cypress for block images 2025-07-29 16:06:46 -04:00
Loren Norman
ad3d1d8cb2 render block image tags 2025-07-29 15:22:31 -04:00
Loren Norman
e9f3f8c5c7 reify shadows if no input block present 2025-07-29 15:22:02 -04:00
Loren Norman
6ad85110eb add image exporting to vue component 2025-07-29 15:01:28 -04:00
Loren Norman
6949620141 export workspace containing all blocks 2025-07-29 14:56:43 -04:00
Loren Norman
e3dcab44e5 separate CSS from image export code 2025-07-29 14:56:10 -04:00
Loren Norman
ae0baf6908 fix root block short description 2025-07-29 14:41:32 -04:00
Loren Norman
20026c433d ignore more generated files 2025-07-29 14:41:14 -04:00
Loren Norman
a3e5f70621 name block image files 2025-07-29 14:40:53 -04:00
Loren Norman
3272fbe05f drop removed blocks, fix help urls 2025-07-25 15:48:04 -04:00
Loren Norman
702749303b remove matcher_boolean_operation (not used) 2025-07-25 15:46:26 -04:00
Loren Norman
37600d82f1 standalone regenerators, block migrations 2025-07-25 15:32:20 -04:00
Loren Norman
65fd5959bc uncategorized blocks in sidebar 2025-07-25 14:48:49 -04:00
Loren Norman
49cf69b4f4 remove empty and unimplemented sections 2025-07-25 14:05:30 -04:00
Loren Norman
03822ca3d0 bump the snapshots 2025-07-25 13:57:45 -04:00
Loren Norman
6728875106 oops missing . 2025-07-25 11:33:14 -04:00
Loren Norman
d181533dc7 change site root 2025-07-25 11:31:20 -04:00
Loren Norman
19dd399a1f override conditional inputs and weather fields 2025-07-24 18:48:47 -04:00
Loren Norman
da3a86f55d support doc overrides for inputs & fields 2025-07-24 18:48:34 -04:00
Loren Norman
31cbbfb3a4 block list: categories, links, descriptions 2025-07-24 14:01:40 -04:00
Loren Norman
dd34206376 generate a block index 2025-07-24 13:43:48 -04:00
Loren Norman
8adf43ed7b improve missing block/shadow warning 2025-07-24 12:49:12 -04:00
Loren Norman
25ec0db569 block page URLs by category not filesystem 2025-07-24 12:40:27 -04:00
Loren Norman
38a850cdd7 blocks can specify primary category 2025-07-24 11:51:42 -04:00
Loren Norman
eaaaf03b4f boolean output is also expression 2025-07-24 11:51:07 -04:00
Loren Norman
42bca1b579 add output warning, improve input check warning 2025-07-23 15:32:33 -04:00
Loren Norman
bd002ba253 ensure all block inputs/outputs are checked 2025-07-23 15:32:13 -04:00
Loren Norman
848d9d5fda contributing page 2025-07-23 14:58:39 -04:00
Loren Norman
46aec1da15 remove block workspace and initial example work 2025-07-23 14:20:26 -04:00
Loren Norman
9dec8c9b73 add guides to Getting Started 2025-07-23 14:17:57 -04:00
Loren Norman
25f061b833 drop example page, block workspace, 2025-07-23 14:17:48 -04:00
Loren Norman
fbb759bb59 missing "name" and "check" warnings 2025-07-23 13:53:52 -04:00
Loren Norman
f936b63d83 add and change some outputs/checks 2025-07-22 21:01:59 -04:00
Loren Norman
f2a533bf9c use paragraph symbol instead of P 2025-07-22 17:17:38 -04:00
Loren Norman
3d0dbe114f weather output, map nullcheck 2025-07-22 17:07:25 -04:00
63 changed files with 3371 additions and 974 deletions

2
.gitignore vendored
View file

@ -2,7 +2,9 @@
.ds_store
# Blockly exports
tmp
export
docs/block_images
# Logs
logs

View file

@ -18,12 +18,14 @@ export default {
inputs: {
SUBJECT: {
description: "a template for generating the email subject",
check: "expression",
bytecodeProperty: "subjectTemplate",
shadow: singleLineTemplate,
},
BODY: {
description: "a multi-line template for generating the email body",
check: "expression",
bytecodeProperty: "bodyTemplate",
shadow: multilineLineTemplate,
}

View file

@ -16,6 +16,7 @@ export default {
inputs: {
EXPRESSION: {
description: "A Block diagram you'd like to see the resolved value and type of.",
check: "expression",
shadow: 'io_text'
}
},

View file

@ -1,62 +0,0 @@
// deprecated: use feeds/set_value
export default {
type: "action_publish",
bytecodeKey: "publishAction",
name: "Publish to Feed",
colour: "0",
description: "Sends the given value to the specified Feed.",
connections: {
mode: "statement",
output: "expression",
next: 'expression'
},
template: `
📈 Publish |CENTER
...value: %VALUE
...to: %FEED
`,
inputs: {
VALUE: {
description: "The value to write to the Feed.",
shadow: 'io_text'
},
FEED: {
description: "The Feed to write to.",
shadow: 'feed_selector'
},
},
generators: {
json: (block, generator) => {
const payload = {
publishAction: {
feed: JSON.parse(generator.valueToCode(block, 'FEED', 0) || null),
value: JSON.parse(generator.valueToCode(block, 'VALUE', 0) || null)
}
}
return JSON.stringify(payload)
}
},
regenerators: {
json: (blockObject, helpers) => {
const payload = blockObject.publishAction
// migrating to a new block
return {
type: "feed_set_value",
fields: {
FEED_KEY: payload.feed.feed.key,
},
inputs: {
VALUE: helpers.expressionToBlock(payload.value, { shadow: 'io_text' }),
}
}
}
}
}

View file

@ -23,6 +23,7 @@ export default {
inputs: {
BODY: {
description: "A template for generating the SMS body",
check: "expression",
shadow: multilineLineTemplate
}
},

View file

@ -25,6 +25,7 @@ export default {
inputs: {
URL: {
description: "A valid web location to send a POST request to.",
check: "expression",
shadow: {
type: 'io_text',
fields: { TEXT: 'https://...' }
@ -33,6 +34,7 @@ export default {
BODY: {
description: "A JSON template to render and POST",
check: "expression",
shadow: multilineLineTemplate
}
},

View file

@ -8,12 +8,6 @@ export default {
description: "Execute different block diagrams based on the outcome of conditional checks.",
colour: 60,
// "if - the first value to check for truthiness",
// "do - the commands to execute if the first check was true",
// "else if - (optional, repeating) an extra value to check for truthiness",
// "do - the commands to execute if the previous check was true",
// "else - (optional) the commands to execute if no checks were true",
connections: {
mode: "statement",
output: "expression",
@ -29,19 +23,28 @@ export default {
else %ELSE_LABEL
`,
// TODO: open a way to send raw documentation for the inputs section
// the conditional block has a lot of dynamic behavior that is harder to
// document plainly alongside the data definitions
inputs: {
IF0: {
description: "Runs the given block tree and checks whether it resolve true or false. If true, executes the 'do' branch, otherwise moves onto the next if (if present), or the final else (if present.)",
check: "expression",
shadow: 'io_logic_boolean'
},
THEN0: {
description: "The block diagram to execute when the preceding 'if' clause resolves to true",
check: "expression",
type: 'statement',
shadow: {
type:'action_log',
inputs: {
EXPRESSION: {
shadow: {
type: 'io_text',
fields: {
TEXT: 'conditional was true!'
}
}
}
}
}
},
ELSE_IF_LABEL: {
@ -53,6 +56,33 @@ export default {
}
},
docOverrides: {
inputs: `
### \`If\`
This block tree will always be run. If it resolve to \`true\`, the blocks
under the next 'do' section will be executed. Otherwise, execution moves
to the next "else if" (if present), or the final "else" (if present.)
### \`Do\`
The block diagram to execute when the preceding "if" or "else if" clause
resolves to \`true\`.
### \`Else if\`
**Optional:** "else if" only appears after clicking "+ else if", and can be
removed by clicking the "-" next to it.
Another "if" to check, only if every prior if has executed and none
resolved to \`true\`.
### \`Else\`
**Optional:** "else" only appears after clicking "+ else", and can be removed
by clicking "-" next to it.
This section will execute if all "if"s and "else-if"s have been executed and
all resolved to \`false\`.
`
},
generators: {
json: (block, generator) => {
const payload = {

View file

@ -8,6 +8,11 @@ export default {
mixins: ['replaceDropdownOptions'],
extensions: ['populateFeedDropdown'],
connections: {
mode: "value",
output: "expression",
},
template: `Get %FEED_KEY`,
fields: {

View file

@ -1,48 +0,0 @@
// deprecated: use feeds/get_value
export default {
type: "feed_selector",
bytecodeKey: "feed",
name: "Feed",
colour: 300,
description: "The last value of this feed or component, always a String",
mixins: ['replaceDropdownOptions'],
extensions: ['populateFeedDropdown'],
template: "Feed: %FEED_KEY",
fields: {
FEED_KEY: {
description: "A listing of the User's Feeds to select from.",
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
// migrating to a new block
return {
type: "feed_get_value",
fields: {
FEED_KEY: payload.key
}
}
}
}
}

View file

@ -20,6 +20,7 @@ export default {
inputs: {
VALUE: {
description: "The value to publish to the Feed.",
check: "expression",
shadow: 'io_text'
}
},

View file

@ -6,7 +6,7 @@ export default {
connections: {
mode: "value",
output: "boolean",
output: [ "expression", "boolean" ],
},
template: "%BOOL",

View file

@ -5,11 +5,17 @@ export default {
colour: 60,
description: "Swaps a truthy value to `false`, or a falsy value to `true`.",
connections: {
mode: "value",
output: "expression",
},
template: "not %EXPRESSION",
inputs: {
EXPRESSION: {
description: "Block diagram that will be resolved, then have its truthiness flipped.",
check: "expression",
shadow: 'io_logic_boolean'
}
},

View file

@ -6,16 +6,23 @@ export default {
colour: 60,
description: "Perform the specifed boolean logic operation on two operands.",
connections: {
mode: "value",
output: "expression",
},
template: `%A %OP %B`,
inputs: {
A: {
description: "A block diagram that will be resolved to a truthy/falsy value",
check: "expression",
shadow: 'io_logic_boolean'
},
B: {
description: "A block diagram that will be resolved to a truthy/falsy value",
check: "expression",
shadow: 'io_logic_boolean'
}
},

View file

@ -1,59 +0,0 @@
export default {
type: 'matcher_boolean_operation',
bytecodeKey: "matcherBooleanOperation",
name: "Compare Matcher",
colour: 60,
inputsInline: true,
description: "Perform a logic operation between the triggering Feed value and a block diagram.",
connections: { mode: 'value', output: 'matcher' },
template: `is true %OP %B`,
fields: {
OP: {
options: [
['and', 'AND'],
['or', 'OR'],
]
}
},
inputs: {
B: {
shadow: 'io_logic_boolean'
}
},
generators: {
json: (block, generator) => {
const
operator = block.getFieldValue('OP'),
rightExp = generator.valueToCode(block, 'B', 0) || null,
blockPayload = JSON.stringify({
matcherBooleanOperation: {
comparator: operator?.toLowerCase() || null,
right: JSON.parse(rightExp),
},
})
return [ blockPayload, 0 ]
}
},
regenerators: {
json: (blockObject, helpers) => {
const
{ comparator, right } = blockObject.matcherBooleanOperation,
fields = {
OP: comparator?.toUpperCase()
},
inputs = {
B: helpers.expressionToBlock(right, { shadow: 'io_logic_boolean' }),
}
return { type: 'matcher_boolean_operation', fields, inputs }
}
}
}

View file

@ -27,6 +27,7 @@ export default {
inputs: {
B: {
description: "The value to compare with the Feed value.",
check: "expression",
shadow: 'io_math_number'
}
},

View file

@ -13,6 +13,7 @@ export default {
inputs: {
B: {
description: "The string to compare with the Feed value.",
check: "expression",
shadow: 'io_text'
}
},

View file

@ -6,16 +6,23 @@ export default {
inputsInline: true,
description: "Perform the specified arithmetic operation on two specified operands.",
connections: {
mode: "value",
output: "expression",
},
template: `%A %OP %B`,
inputs: {
A: {
description: "The left side of the operation. Will be coerced to a number",
check: "expression",
shadow: 'io_math_number'
},
B: {
description: "The right side of the operation. Will be coerced to a number",
check: "expression",
shadow: 'io_math_number'
},
},

View file

@ -4,18 +4,26 @@ export default {
name: "Compare Numbers",
colour: 120,
inputsInline: true,
primaryCategory: "Math",
description: "Numerically compare two given values using the selected math operation.",
connections: {
mode: "value",
output: "expression",
},
template: `%A %OP %B`,
inputs: {
A: {
description: "The left side of the comparison. Will be coerced to a number",
check: "expression",
shadow: 'io_math_number'
},
B: {
description: "The right side of the comparison. Will be coerced to a number",
check: "expression",
shadow: 'io_math_number'
},
},

View file

@ -17,6 +17,7 @@ export default {
inputs: {
VALUE: {
check: "expression",
shadow: "io_math_number"
},

View file

@ -19,6 +19,7 @@ export default {
inputs: {
VALUE: {
check: "expression",
bytecodeProperty: "value",
shadow: 'io_math_number'
},
@ -63,7 +64,7 @@ export default {
generators: {
json: (block, generator) => {
const
value = JSON.parse(generator.valueToCode(block, 'VALUE', 0)),
value = JSON.parse(generator.valueToCode(block, 'VALUE', 0) || null),
from = JSON.parse(generator.valueToCode(block, 'FROM_RANGE', 0)),
to = JSON.parse(generator.valueToCode(block, 'TO_RANGE', 0)),
payload = { mapValue: { value, from, to }}

View file

@ -7,7 +7,7 @@ export default {
connections: {
mode: "value",
output: "number",
output: [ "expression", "number" ],
},
extensions: {

View file

@ -17,12 +17,14 @@ export default {
inputs: {
FROM: {
description: "The lower bound of the range.",
check: "expression",
bytecodeProperty: "from",
shadow: "io_math_number"
},
TO: {
description: "The upper bound of the range.",
check: "expression",
bytecodeProperty: "to",
shadow: "io_math_number"
},

View file

@ -5,16 +5,6 @@ export default {
color: 120,
description: "Round a value to the nearest whole number via round, floor, or ceiling functions",
docBlocks: [
{
type: 'io_math_number',
fields: { NUM: 1.45 }
}, {
type: 'io_text',
fields: { TEXT: "1.55" }
}
],
connections: {
mode: "value",
output: "expression",
@ -24,6 +14,7 @@ export default {
VALUE: {
description: "A value you'd like to round to a whole number. Will be coerced to a number.",
bytecodeProperty: "value",
check: "expression",
shadow: "io_math_number"
}
},

View file

@ -1,6 +1,12 @@
import weatherMixin from "./weather_mixin.js"
const
{ keyToLabel, HELP_TEXT_BY_PROP: propText } = weatherMixin,
propLines = (prefix, props) =>
props.map(prop => `${prefix}- \`${keyToLabel(prop)}\`: ${propText[prop].description}`).join("")
export default {
type: "weather",
bytecodeKey: "weather",
@ -9,6 +15,11 @@ export default {
ioPlus: true,
description: "Fetch the current or forecast weather conditions at the specified location.",
connections: {
mode: "value",
output: "expression",
},
mixins: [
'replaceDropdownOptions',
{ weatherMixin }
@ -61,14 +72,12 @@ export default {
fields: {
POWER_UP_ID: {
description: "Select a location from those defined by the Weather Power-Up",
options: [
[ "Loading locations...", "" ],
]
},
WEATHER_TIME: {
description: "Select which kind of forecast to query",
options: [
[ "Now", "current" ],
[ "In 5 minutes", "forecast_minutes_5" ],
@ -93,7 +102,6 @@ export default {
},
WEATHER_PROPERTY: {
description: "Select which metric of the forecast to use.",
label: ""
},
@ -102,6 +110,36 @@ export default {
},
},
docOverrides: {
fields: `
### \`Location\`
The list of weather locations defined in the Weather Power-Up. Select
the location you would like weather information for.
### \`Forecast\` and \`Metric\`
A list a weather forecasts to choose from. The weather metrics available
are different based on the chosen forecast.
:::details \`Now\` Metrics` + propLines(`
`, weatherMixin.CURRENT_PROPS) + `
:::
:::details \`In X minutes\` Metrics` + propLines(`
`, weatherMixin.MINUTE_PROPS) + `
:::
:::details \`In X hours\` Metrics` + propLines(`
`, weatherMixin.HOUR_PROPS) + `
:::
:::details \`In X days\` Metrics
**Some daily metrics can be narrowed to just the daytime or overnight portions.**
` + propLines(`
`, weatherMixin.DAY_PROPS) + `
:::
`
},
generators: {
json: block => {
const

View file

@ -5,7 +5,7 @@ export default {
type: "action_root",
name: "Root",
colour: "0",
description: "Add Triggers to determine when this Action runs.\nAdd Actions to determine what this Action does.",
description: "Add Triggers to determine when this Action runs. Add Actions to determine what this Action does.",
connections: {},

View file

@ -4,18 +4,26 @@ export default {
name: "Compare Text",
colour: 180,
inputsInline: true,
primaryCategory: "Logic",
description: "Compare two chunks of text for equality, inequality, or inclusion.",
connections: {
mode: "value",
output: "expression",
},
template: `%A %OP %B`,
inputs: {
A: {
description: "The left side of the comparison. Will be coerced to a string",
check: "expression",
shadow: 'io_text'
},
B: {
description: "The right side of the comparison. Will be coerced to a string",
check: "expression",
shadow: 'io_text'
},
},

View file

@ -6,16 +6,23 @@ export default {
inputsInline: true,
description: "Join two pieces of text into one.",
connections: {
mode: "value",
output: "expression",
},
template: "%A + %B",
inputs: {
A: {
description: "The first string of text",
check: "expression",
shadow: "io_text"
},
B: {
description: "The last string of text",
check: "expression",
shadow: "io_text"
},
},

View file

@ -41,10 +41,18 @@ export default {
:::
`,
connections: {
mode: "value",
output: [ "expression", "string" ],
},
template: "{{ %TEMPLATE",
inputs: {
TEMPLATE: { shadow: 'io_text_multiline' }
TEMPLATE: {
check: "expression",
shadow: 'io_text_multiline'
}
},
generators: {

View file

@ -6,7 +6,7 @@ export default {
connections: {
mode: "value",
output: "String",
output: [ "expression", "string" ],
},
template: `"%TEXT`,

View file

@ -6,10 +6,10 @@ export default {
connections: {
mode: "value",
output: "String",
output: [ "expression", "string" ],
},
template: "P %TEXT",
template: " %TEXT",
fields: {
TEXT: {

View file

@ -7,7 +7,7 @@ export default {
connections: {
mode: 'value',
output: "String",
output: "expression",
},
template: "Get variable %VAR",

View file

@ -16,6 +16,7 @@ export default {
inputs: {
VALUE: {
check: "expression",
shadow: "io_text",
}
},

View file

@ -0,0 +1,38 @@
// These are for blocks that have been replaced with new blocks, but still
// exist in the database. The block definition gets removed, but a new
// regenerator is written here to catch those legacy blocks and "port" them
// to the new block when they are loaded.
export default {
// becomes feed_set_value
action_publish: {
json: (blockObject, helpers) => {
const payload = blockObject.publishAction
return {
type: "feed_set_value",
fields: {
FEED_KEY: payload.feed.feed.key,
},
inputs: {
VALUE: helpers.expressionToBlock(payload.value, { shadow: 'io_text' }),
}
}
}
},
// becomes feed_get_value
feed_selector: {
json: blockObject => {
const payload = blockObject.feed
return {
type: "feed_get_value",
fields: {
FEED_KEY: payload.key
}
}
}
}
}

View file

@ -0,0 +1,5 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {},
});

View file

@ -0,0 +1,11 @@
describe("Block Images", () => {
it("download all block images", () => {
cy.visit("http://localhost:5173/")
cy.get("[data-id^='block-type-']").each(($el) => {
cy.wrap($el).rightclick({ force: true })
cy.contains("Save Block as PNG...").click()
}).then(blockElements => {
cy.log(`Saved ${blockElements.length} block images.`)
})
})
})

0
cypress/support/e2e.js Normal file
View file

View file

@ -13,7 +13,7 @@ export default defineConfig({
['link', { rel: 'icon', href: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧩</text></svg>" }]
],
base: "/io-actions/",
base: "/actions-docs/",
lastUpdated: true,
@ -24,14 +24,13 @@ export default defineConfig({
},
editLink: {
text: "Suggest an edit to this page",
// runs on the frontend, must be a pure function!
pattern: ({ filePath }) => {
pattern: ({ filePath, frontmatter }) => {
// special handling for block pages
if(filePath.match(/^blocks\//)) {
// docs come from the js, md is not the true source
const jsPath = filePath.replace(/.md$/, '.js')
// and we want to link to the main branch, not docs
return `https://github.com/adafruit/io-actions/edit/main/app/${jsPath}`
// block pages have their source paths in their frontmatter
return `https://github.com/adafruit/io-actions/edit/main/app/blocks/${frontmatter.definitionPath}`
}
return `https://github.com/adafruit/io-actions/edit/main/docs/${filePath}`
@ -42,13 +41,14 @@ export default defineConfig({
{ text: '🧩', link: '/sandbox' },
{ text: 'Home', link: '/' },
{ text: 'Getting Started', link: '/getting-started' },
{ text: 'The Blocks', link: '/block-index' },
{ text: 'Examples', link: '/automation-examples' }
{ text: 'Block List', link: '/blocks/index' },
{ text: 'Contributing', link: '/contributing' },
// { text: 'Examples', link: '/automation-examples' }
],
sidebar: {
// covers /blocks/* and /block-index
"/block": [ blocksSidebar ],
// block index and pages
"/blocks/": [ blocksSidebar ],
// devtools for the sandbox
"/sandbox": [

View file

@ -1,7 +0,0 @@
---
title: The Blocks
---
# The Blocks
Coming soon...

View file

@ -2,6 +2,8 @@
import { onMounted, onUnmounted } from 'vue'
import { dispose, inject } from "../blockly/blockly_app.js"
import initialWorkspace from "../blockly/workspace.json"
import { imageExportRegistryItems } from '#src/image_exporter.js'
const
{ block, blocks=[], width="100%", height="200px", toolbox=true } = defineProps(
@ -10,6 +12,30 @@
injectOptions = {},
options = {
injectOptions,
contextMenu: {
register: [ ...imageExportRegistryItems ]
},
// TODO: specify dummy extension data with the block defs
extensionData: {
feedOptions: [
["Group A Feed 1", "group-a.feed-1"],
["Group A Feed 2", "group-a.feed-2"],
["Group B Feed 1", "group-b.feed-1"],
["Group C Feed 1", "group-C.feed-1"],
],
weatherLocationOptions: [
[ "Industry City", "1" ],
[ "Varick", "2" ],
[ "Shenzhen", "3" ],
],
currentWeatherByLocation: {
1: {
current: {
cloudCover: "5.4321",
}
}
}
},
workspaceJson: block
? {
blocks: {
@ -21,8 +47,8 @@
x: 20,
y: 20
},
...blocks.map((docBlock, idx) => ({
...docBlock,
...blocks.map((otherBlock, idx) => ({
...otherBlock,
x: 180,
y: idx*30 + 20
}))

41
docs/contributing.md Normal file
View file

@ -0,0 +1,41 @@
---
title: Contributing
---
# Contributing
## Documentation Wrong or Missing?
_You can help!_
### Have a GitHub Account?
Awesome! You can suggest changes to any page in a few simple steps:
1. Scroll to the bottom of the page in question.
2. Click the _"Edit this page"_ link.
3. You will be asked to create a Fork, do so.
4. You should now see the page source, opened and editable.
5. Search for the text you'd like to change.
6. Make the desired changes.
7. Click the green _"Commit Changes..."_ button. (top-right)
8. Add a brief explanation about the purpose of your changes.
9. Click the green _"Propose Changes"_ button.
10. Congrats! You've just gotten involved improving things for everyone.
### No GitHub Account?
If you don't already have a GitHub account, but you're willing to create one,
simply head over to the [GitHub signup page](https://github.com/signup) and
do so. Once created, return to the instructions above.
If you'd rather not create a GitHub account, no worries! You can suggest any
changes via [the Adafruit IO Forums](https://forums.adafruit.com/viewforum.php?f=56)
or [support channels](https://io.adafruit.com/support).
## Want to Suggest a New Block?
We'd love to hear from you!
You can suggest new or updated blocks via
[the Adafruit IO Forums](https://forums.adafruit.com/viewforum.php?f=56) or
[support channels](https://io.adafruit.com/support).

View file

@ -4,4 +4,16 @@ title: Getting Started
# Getting Started
Coming soon...
_Welcome to the wonderful world of IoT automation using blocks, by Adafruit IO!_
::: tip New here?
Get started with our handy guide, [How to Use Blockly for Actions](https://learn.adafruit.com/how-to-use-blockly-for-actions-on-adafruit-io)
:::
Here's some project Guides that use Actions:
- [No-Code Counters and Email Reports](https://learn.adafruit.com/no-code-counters-and-email-reports-with-adafruit-io-actions)
- [IoT Door Detector](https://learn.adafruit.com/using-adafruit-io-actions-to-make-an-iot-door-detector)
- [Rain-sensing Umbrella Stand](https://learn.adafruit.com/no-code-rain-sensing-smart-desktop-umbrella-stand)
- [No-Code Barometer](https://learn.adafruit.com/dps310-analog-barometer)
We'll continue to update this section as new guides are written, check back soon!

View file

@ -5,17 +5,14 @@ layout: home
hero:
name: "IO Actions:"
text: "Block Reference"
tagline: Automation that clicks!
tagline: Automation that clicks, for <a href="https://io.adafruit.com">Adafruit IO</a>
actions:
- theme: brand
text: Getting Started
link: /getting-started.md
- theme: alt
text: The Blocks
link: /block-index.md
- theme: alt
text: Automation Examples
link: /automation-examples.md
text: Block List
link: /blocks/index.md
features:
- icon: 📚

View file

@ -1,9 +1,14 @@
import { spawn, spawnSync } from 'node:child_process'
import { copyFileSync, cpSync } from 'node:fs'
import { cleanDir, write, totalBytesWritten } from "./export_util.js"
import DefinitionSet from '#src/definitions/definition_set.js'
import { exportTo } from '#src/exporters/index.js'
const toExport = process.argv[2]
const
toExport = process.argv[2],
taskArgs = process.argv.slice(3)
if(!toExport) {
console.error(`Export Error: Missing export name!\nUsage: node export.js [export name]`)
@ -30,16 +35,59 @@ const
},
"docs": async () => {
await exporters.app("docs/blockly")
// allow option to skip image generation
const skipImages = taskArgs.includes("skipImages")
if(!skipImages) {
await exporters.blockImages()
cleanDir("docs/block_images")
cpSync("tmp/block_images/images", "docs/block_images", { recursive: true })
}
await exporters.app("docs/blockly")
cleanDir("docs/blocks")
await exportTo("docs", definitions, exportItem => {
exportItem.sidebar("blocks/_blocks_sidebar.json")
exportItem.blockPages(block => `blocks/${block.definitionPath.replace(/.js$/, '.md')}`)
exportItem.blockIndex("blocks/index.md")
exportItem.blockPages()
// exportItem.blockExamples(block => "blocks/${block.definitionPath}/examples.json")
})
},
"blockImages": async () => {
const destination = "tmp/block_images"
cleanDir(destination)
cleanDir(`${destination}/images`)
// export a special app with no toolbox, all blocks on workspace
await exportTo(destination, definitions, exportItem => {
exportItem.workspaceAllBlocks("workspace.json")
write(`${destination}/toolbox.json`, "null")
exportItem.blocks("blocks.json")
exportItem.script("blockly_app.js")
// TODO: make a DocumentExporter for generating html wrappers
copyFileSync("src/exporters/document_templates/blockly_workspace.template.html", `${destination}/index.html`)
})
// serve it
console.log('Serving workspace for screenshots...')
const viteProcess = spawn("npx", ["vite", "serve", destination])
// extract the screenshots
console.log('Generating screenshots...')
spawnSync("npx", ["cypress", "run",
"--config", `downloadsFolder=${destination}/images`,
"--config-file", `cypress/cypress.config.js`,
])
console.log('Generation complete.')
// kill the server
if(!viteProcess.kill()) {
console.log("Vite failed to exit gracefully")
process.exit(1)
}
console.log('Server closed.')
}
},
exporterNames = Object.keys(exporters)
@ -58,3 +106,6 @@ await exporter()
const elapsed = Date.now() - startTime
console.log("=======================")
console.log(`🏁 Done. Wrote ${totalBytesWritten.toFixed(3)}k in ${elapsed}ms 🏁`)
process.exit(0)

View file

@ -8,7 +8,7 @@ export const
if(fs.existsSync(dirName)) {
fs.rmSync(dirName, { recursive: true, force: true })
}
fs.mkdirSync(dirName)
fs.mkdirSync(dirName, { recursive: true, force: true })
console.log(`/${dirName}: clean`)
},

1952
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@
"devDependencies": {
"@eslint/js": "^9.2.0",
"chai": "^5.1.2",
"cypress": "^14.5.3",
"eslint": "^8.57.0",
"esm-reload": "^1.0.1",
"glob": "^10.4.2",

495
src/blockly_css.js Normal file
View file

@ -0,0 +1,495 @@
export default `
.blocklySvg {
background-color: #fff;
outline: none;
overflow: hidden; /* IE overflows by default. */
position: absolute;
display: block;
}
.blocklyWidgetDiv {
display: none;
position: absolute;
z-index: 99999; /* big value for bootstrap3 compatibility */
}
.injectionDiv {
height: 100%;
position: relative;
overflow: hidden; /* So blocks in drag surface disappear at edges */
touch-action: none;
}
.blocklyNonSelectable {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.blocklyBlockCanvas.blocklyCanvasTransitioning,
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
transition: transform .5s;
}
.blocklyTooltipDiv {
background-color: #ffffc7;
border: 1px solid #ddc;
box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);
color: #000;
display: none;
font: 9pt sans-serif;
opacity: .9;
padding: 2px;
position: absolute;
z-index: 100000; /* big value for bootstrap3 compatibility */
}
.blocklyDropDownDiv {
position: absolute;
left: 0;
top: 0;
z-index: 1000;
display: none;
border: 1px solid;
border-color: #dadce0;
background-color: #fff;
border-radius: 2px;
padding: 4px;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownContent {
max-height: 300px; /* @todo: spec for maximum height. */
overflow: auto;
overflow-x: hidden;
position: relative;
}
.blocklyDropDownArrow {
position: absolute;
left: 0;
top: 0;
width: 16px;
height: 16px;
z-index: -1;
background-color: inherit;
border-color: inherit;
}
.blocklyDropDownButton {
display: inline-block;
float: left;
padding: 0;
margin: 4px;
border-radius: 4px;
outline: none;
border: 1px solid;
transition: box-shadow .1s;
cursor: pointer;
}
.blocklyArrowTop {
border-top: 1px solid;
border-left: 1px solid;
border-top-left-radius: 4px;
border-color: inherit;
}
.blocklyArrowBottom {
border-bottom: 1px solid;
border-right: 1px solid;
border-bottom-right-radius: 4px;
border-color: inherit;
}
.blocklyResizeSE {
cursor: se-resize;
fill: #aaa;
}
.blocklyResizeSW {
cursor: sw-resize;
fill: #aaa;
}
.blocklyResizeLine {
stroke: #515A5A;
stroke-width: 1;
}
.blocklyHighlightedConnectionPath {
fill: none;
stroke: #fc3;
stroke-width: 4px;
}
.blocklyPathLight {
fill: none;
stroke-linecap: round;
stroke-width: 1;
}
.blocklySelected>.blocklyPathLight {
display: none;
}
.blocklyDraggable {
cursor: grab;
cursor: -webkit-grab;
}
.blocklyDragging {
cursor: grabbing;
cursor: -webkit-grabbing;
}
/* Changes cursor on mouse down. Not effective in Firefox because of
https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */
.blocklyDraggable:active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.blocklyDragging.blocklyDraggingDelete {
cursor: url("./handdelete.cur"), auto;
}
.blocklyDragging>.blocklyPath,
.blocklyDragging>.blocklyPathLight {
fill-opacity: .8;
stroke-opacity: .8;
}
.blocklyDragging>.blocklyPathDark {
display: none;
}
.blocklyDisabled>.blocklyPath {
fill-opacity: .5;
stroke-opacity: .5;
}
.blocklyDisabled>.blocklyPathLight,
.blocklyDisabled>.blocklyPathDark {
display: none;
}
.blocklyInsertionMarker>.blocklyPath,
.blocklyInsertionMarker>.blocklyPathLight,
.blocklyInsertionMarker>.blocklyPathDark {
fill-opacity: .2;
stroke: none;
}
.blocklyMultilineText {
font-family: monospace;
}
.blocklyNonEditableText>text {
pointer-events: none;
}
.blocklyFlyout {
position: absolute;
z-index: 20;
}
.blocklyText text {
cursor: default;
}
/*
Don't allow users to select text. It gets annoying when trying to
drag a block and selected text moves instead.
*/
.blocklySvg text {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
cursor: inherit;
}
.blocklyHidden {
display: none;
}
.blocklyFieldDropdown:not(.blocklyHidden) {
display: block;
}
.blocklyIconGroup {
cursor: default;
}
.blocklyIconGroup:not(:hover),
.blocklyIconGroupReadonly {
opacity: .6;
}
.blocklyIconShape {
fill: #00f;
stroke: #fff;
stroke-width: 1px;
}
.blocklyIconSymbol {
fill: #fff;
}
.blocklyMinimalBody {
margin: 0;
padding: 0;
}
.blocklyHtmlInput {
border: none;
border-radius: 4px;
height: 100%;
margin: 0;
outline: none;
padding: 0;
width: 100%;
text-align: center;
display: block;
box-sizing: border-box;
}
/* Remove the increase and decrease arrows on the field number editor */
input.blocklyHtmlInput[type=number]::-webkit-inner-spin-button,
input.blocklyHtmlInput[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
.blocklyMainBackground {
stroke-width: 1;
stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */
}
.blocklyMutatorBackground {
fill: #fff;
stroke: #ddd;
stroke-width: 1;
}
.blocklyFlyoutBackground {
fill: #ddd;
fill-opacity: .8;
}
.blocklyMainWorkspaceScrollbar {
z-index: 20;
}
.blocklyFlyoutScrollbar {
z-index: 30;
}
.blocklyScrollbarHorizontal,
.blocklyScrollbarVertical {
position: absolute;
outline: none;
}
.blocklyScrollbarBackground {
opacity: 0;
}
.blocklyScrollbarHandle {
fill: #ccc;
}
.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyScrollbarHandle:hover {
fill: #bbb;
}
/* Darken flyout scrollbars due to being on a grey background. */
/* By contrast, workspace scrollbars are on a white background. */
.blocklyFlyout .blocklyScrollbarHandle {
fill: #bbb;
}
.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyFlyout .blocklyScrollbarHandle:hover {
fill: #aaa;
}
.blocklyInvalidInput {
background: #faa;
}
.blocklyVerticalMarker {
stroke-width: 3px;
fill: rgba(255,255,255,.5);
pointer-events: none;
}
.blocklyComputeCanvas {
position: absolute;
width: 0;
height: 0;
}
.blocklyNoPointerEvents {
pointer-events: none;
}
.blocklyContextMenu {
border-radius: 4px;
max-height: 100%;
}
.blocklyDropdownMenu {
border-radius: 2px;
padding: 0 !important;
}
.blocklyDropdownMenu .blocklyMenuItem {
/* 28px on the left for icon or checkbox. */
padding-left: 28px;
}
/* BiDi override for the resting state. */
.blocklyDropdownMenu .blocklyMenuItemRtl {
/* Flip left/right padding for BiDi. */
padding-left: 5px;
padding-right: 28px;
}
.blocklyWidgetDiv .blocklyMenu {
background: #fff;
border: 1px solid transparent;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
font: normal 13px Arial, sans-serif;
margin: 0;
outline: none;
padding: 4px 0;
position: absolute;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
.blocklyWidgetDiv .blocklyMenu.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv .blocklyMenu {
background: inherit; /* Compatibility with gapi, reset from goog-menu */
border: inherit; /* Compatibility with gapi, reset from goog-menu */
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
outline: none;
position: relative; /* Compatibility with gapi, reset from goog-menu */
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
/* State: resting. */
.blocklyMenuItem {
border: none;
color: #000;
cursor: pointer;
list-style: none;
margin: 0;
/* 7em on the right for shortcut. */
min-width: 7em;
padding: 6px 15px;
white-space: nowrap;
}
/* State: disabled. */
.blocklyMenuItemDisabled {
color: #ccc;
cursor: inherit;
}
/* State: hover. */
.blocklyMenuItemHighlight {
background-color: rgba(0,0,0,.1);
}
/* State: selected/checked. */
.blocklyMenuItemCheckbox {
height: 16px;
position: absolute;
width: 16px;
}
.blocklyMenuItemSelected .blocklyMenuItemCheckbox {
background: url(./sprites.png) no-repeat -48px -16px;
float: left;
margin-left: -24px;
position: static; /* Scroll with the menu. */
}
.blocklyMenuItemRtl .blocklyMenuItemCheckbox {
float: right;
margin-right: -24px;
}
/* Begin .geras-renderer.classic-theme (stripped) */
.blocklyText,
.blocklyFlyoutLabelText {
font: normal 11pt sans-serif;
}
.blocklyText {
fill: #fff;
}
.blocklyNonEditableText>rect,
.blocklyEditableText>rect {
fill: #fff;
fill-opacity: .6;
stroke: none;
}
.blocklyNonEditableText>text,
.blocklyEditableText>text {
fill: #000;
}
.blocklyFlyoutLabelText {
fill: #000;
}
.blocklyText.blocklyBubbleText {
fill: #000;
}
.blocklyEditableText:not(.editing):hover>rect {
stroke: #fff;
stroke-width: 2;
}
.blocklyHtmlInput {
font-family: sans-serif;
font-weight: normal;
}
.blocklySelected>.blocklyPath {
stroke: #fc3;
stroke-width: 3px;
}
.blocklyHighlightedConnectionPath {
stroke: #fc3;
}
.blocklyReplaceable .blocklyPath {
fill-opacity: .5;
}
.blocklyReplaceable .blocklyPathLight,
.blocklyReplaceable .blocklyPathDark {
display: none;
}
.blocklyInsertionMarker>.blocklyPath {
fill-opacity: 0.2;
stroke: none;
}
.blocklyInsertionMarker>.blocklyPathLight,
.blocklyInsertionMarker>.blocklyPathDark {
fill-opacity: 0.2;
stroke: none;
}
`

View file

@ -1,9 +1,11 @@
import { capitalize, filter, isString, map, isEmpty, keyBy, mapValues, reduce, forEach, pickBy, identity } from 'lodash-es'
import { capitalize, filter, isString, isEmpty, mapValues, forEach, pickBy, identity } from 'lodash-es'
import BlockExporter from "#src/exporters/block_exporter.js"
import { niceTemplate } from '#src/util.js'
const UNCATEGORIZED_PATH = "uncategorized"
class BlockDefinition {
definitionSet = null
@ -16,8 +18,8 @@ class BlockDefinition {
name = null
description = ''
docOverrides = {}
ioPlus = false
docBlocks = null
colour = null
color = null
@ -41,7 +43,7 @@ class BlockDefinition {
disabled = false
categories = []
primaryCategory = null
getCategories() {
@ -52,6 +54,42 @@ class BlockDefinition {
: [])
}
getPrimaryCategory() {
const categories = this.getCategories()
// doesn't appear in a category
if(!categories.length) {
// why specify a primary? warn
if(this.primaryCategory) {
console.warn(`Warning [${this.type}]: No category found, but did have "primaryCategory" key: "${this.primaryCategory}"`)
}
return UNCATEGORIZED_PATH
}
const firstCategoryName = categories[0].name
// appears in multiple categories
if(categories.length > 1) {
// doesn't specify a primary! this is bad, unsure what menu and URL it will fall under, warn
if(!this.primaryCategory) {
console.warn(`Warning [${this.type}]: Multiple categories but no "primaryCategory" declaration, using "${firstCategoryName}"`)
} else {
return this.primaryCategory
}
}
return firstCategoryName
}
documentationPath() {
const
blockMdFilename = this.definitionPath.split("/").at(-1).replace(/.js$/, '.md'),
primaryCategory = this.getPrimaryCategory()
return `blocks/${primaryCategory}/${blockMdFilename}`.toLowerCase()
}
toBlocklyJSON() {
return BlockExporter.export(this)
}
@ -86,9 +124,17 @@ class BlockDefinition {
}
const
definitionPropsToInputs = ({ inputValue, block, shadow }) => {
definitionPropsToInputs = input => {
const { block, shadow, type } = input
// bail unless this is a statement or value input
// (undefined implies value, for now)
if(![undefined, 'statement', 'value'].includes(type)) {
return
}
if(!block && !shadow) {
console.warn("Warning: no block or shadow specified for", inputValue)
console.warn("Warning: no block or shadow specified for input:", input)
return
}
@ -103,7 +149,14 @@ const
}
} else if(shadow) {
return shadowToInput(shadow)
const shadowJson = shadowToInput(shadow)
return {
// also copy the shadow into a real block
// TODO: nested shadow blocks
block: shadowJson.shadow,
...shadowJson
}
}
},
@ -132,10 +185,11 @@ BlockDefinition.parseRawDefinition = function(rawBlockDefinition, definitionPath
blockDef.definitionJS = rawBlockDefinition
blockDef.type = rawBlockDefinition.type
blockDef.name = rawBlockDefinition.name
blockDef.primaryCategory = rawBlockDefinition.primaryCategory
blockDef.docOverrides = rawBlockDefinition.docOverrides
blockDef.description = rawBlockDefinition.description
? niceTemplate(rawBlockDefinition.description)
: ""
blockDef.docBlocks = rawBlockDefinition.docBlocks
blockDef.ioPlus = rawBlockDefinition.ioPlus
blockDef.tooltip = blockDef.description.split("\n")[0]
blockDef.disabled = !!rawBlockDefinition.disabled
@ -155,10 +209,19 @@ BlockDefinition.parseRawDefinition = function(rawBlockDefinition, definitionPath
// warnings on any data that's missing, ugly, etc
if(!blockDef.name) {
// if no name given, humanize the type property as a default
// TODO: make a setting for this and re-enable
// console.warn(`No "name" property provided for block: "${rawBlockDefinition.type}" (${definitionPath})`)
blockDef.name = rawBlockDefinition.type.split(/[\W_]+/).map(capitalize).join(" ").replace(/^io /i, "")
console.warn(`Warning: [${blockDef.type}] No "name" property provided, defaulted to: "${blockDef.name}"`)
}
if(!blockDef.connections) {
console.warn(`Warning: [${blockDef.type}] No "connections" property provided, no defaults.`)
}
forEach(blockDef.inputs, (input, inputName) => {
if(!input.check && input.type !== 'label') {
console.warn(`Warning: [${blockDef.type}] Input is unchecked: ${inputName} ${input.type}`)
}
})
return blockDef
}

View file

@ -60,14 +60,23 @@ DefinitionSet.load = async function(appLocation) {
enabledBlocks = reject(rawDefinitions.blocks, "definition.disabled"),
definitionSet = new DefinitionSet()
// TODO: fields
// TODO: shadows
// TODO: inputs
// TODO: process fields
// TODO: process shadows
// TODO: process inputs
// process mixins
definitionSet.mixins = rawDefinitions.mixins
// process extensions
definitionSet.extensions = rawDefinitions.extensions
// process mutators
definitionSet.mutators = rawDefinitions.mutators
// process standalone regenerators
forEach(rawDefinitions.regenerators, (regenerators, blockType) => {
definitionSet.regenerators[blockType] = regenerators
})
// process blocks
forEach(enabledBlocks, ({ definition, path }) => {
const blockDef = BlockDefinition.parseRawDefinition(definition, path, definitionSet)
definitionSet.blocks.push(blockDef)
@ -110,15 +119,24 @@ DefinitionSet.load = async function(appLocation) {
definitionSet.mutators[blockDef.type] = mutator
}
if(definitionSet.generators[blockDef.type]) {
throw new Error(`Generator already present for block: ${blockDef.type}`)
}
definitionSet.generators[blockDef.type] = blockDef.generators
if(definitionSet.regenerators[blockDef.type]) {
throw new Error(`Regenerator already present for block: ${blockDef.type}`)
}
definitionSet.regenerators[blockDef.type] = blockDef.regenerators
})
// process toolbox
forEach(rawDefinitions.toolboxes, rawToolboxDef => {
const toolboxDef = ToolboxDefinition.parseRawDefinition(rawToolboxDef, definitionSet)
definitionSet.toolboxes.push(toolboxDef)
})
// process workspace
forEach(rawDefinitions.workspaces, rawWorkspaceDef => {
const workspaceDef = WorkspaceDefinition.parseRawDefinition(rawWorkspaceDef, definitionSet)
definitionSet.workspaces.push(workspaceDef)

View file

@ -1,10 +1,10 @@
import { capitalize, trim } from 'lodash-es'
import { trim } from 'lodash-es'
import renderFields from './render_block_fields.js'
import renderInputs from './render_block_inputs.js'
const
export const
IO_PLUS_ALERT = `
::: tip :warning: IO+ Required
This Block requires an IO+ subscription to use. [Learn more about IO+](https://io.adafruit.com/plus)
@ -17,54 +17,73 @@ This Block requires an IO+ subscription to use. [Learn more about IO+](https://i
return trim(`${name} ${ioPlusBadge}`)
},
// ![alt](url "title")
renderBlockImage = ({ name, type }) => `![the ${name} block](/block_images/${type}.png "${name}")`,
renderDescription = ({ description }) => description || "No docs for this block, yet.",
renderIOPlusAlert = ({ ioPlus }) => ioPlus ? IO_PLUS_ALERT : "",
renderWorkspace = definition => {
const workspaceProps = JSON.stringify({
toolbox: false,
block: definition.toBlocklyInstanceJSON(),
blocks: definition.docBlocks || []
})
renderFieldsSection = definition => {
const fieldsMarkdown = renderFields(definition)
return `<BlocklyWorkspace v-bind='${ workspaceProps }' />`
return fieldsMarkdown
? `## Fields\n\n${ fieldsMarkdown }`
: ""
},
renderInputsSection = definition => {
const inputsMarkdown = renderInputs(definition)
return inputsMarkdown
? `## Inputs\n\n${ inputsMarkdown }`
: ""
},
renderOutput = definition => {
return capitalize(definition.connections?.output || "Unspecified")
return ''
// TODO: re-enable when we have something meanginful to show the user
// const defaultedOutput = capitalize(definition.connections?.output || "Unspecified")
// return `
// ## Output
// ${ defaultedOutput }
// `
},
renderExamples = definition => {
return "Coming soon..."
return ""
// TODO: re-enable conditionally when we have examples
// return `
// ## Examples
// Coming soon...
// `
}
export default definition =>
`---
title: "Block: ${definition.name}"
editLink: true
definitionPath: ${ definition.definitionPath }
---
# Block: ${ renderBlockTitle(definition) }
Type: \`${definition.type}\`
${ renderBlockImage(definition) }
${ renderDescription(definition) }
${ renderIOPlusAlert(definition) }
## Workspace
${ renderWorkspace(definition) }
${ renderFieldsSection(definition) }
## Fields
${ renderFields(definition) }
${ renderInputsSection(definition) }
## Inputs
${ renderInputs(definition) }
## Output
${ renderOutput(definition) }
## Examples
${ renderExamples(definition) }
`

View file

@ -1,20 +1,40 @@
import { capitalize, map, mapValues, values } from 'lodash-es'
import { capitalize, keys, map, mapValues, pickBy, values } from 'lodash-es'
import { niceTemplate } from '#src/util.js'
const
renderFields = definition => {
if(definition.docOverrides?.fields) {
return renderOverridenFields(definition)
}
const fields = values(mapValues(definition.fields, (newField, name) => {
newField.field = name
return newField
}))
if(!fields.length) { return "This block has no form fields." }
if(!fields.length) { return }
return fields.map(renderField).join("\n\n")
},
renderOverridenFields = definition => {
// warn if any inputs have descriptions that won't be rendered
const
{ fields } = definition.docOverrides,
missedFields = keys(pickBy(definition.fields, "description")).join(", ")
if(missedFields) {
console.warn(`Warning [${definition.type}]: Inputs doc is overriden, input descriptions will not be seen for: ${missedFields}`)
}
// determine if the override is a function to call
return niceTemplate(typeof fields === 'string'
? fields
: fields(definition))
},
renderField = field => {
const lines = []

View file

@ -1,12 +1,36 @@
import { capitalize, forEach, keys } from 'lodash-es'
import { capitalize, pickBy, forEach, keys } from 'lodash-es'
import { niceTemplate } from '#src/util.js'
const
renderInputs = definition => {
if(!keys(definition.inputs).length) {
return "This block has no inputs"
if(definition.docOverrides?.inputs) {
return renderOverridenInputs(definition)
}
if(!keys(definition.inputs).length) { return }
return renderEachInput(definition)
},
renderOverridenInputs = definition => {
// warn if any inputs have descriptions that won't be rendered
const
{ inputs } = definition.docOverrides,
missedInputs = keys(pickBy(definition.inputs, "description")).join(", ")
if(missedInputs) {
console.warn(`Warning [${definition.type}]: Inputs doc is overriden, input descriptions will not be seen for: ${missedInputs}`)
}
// determine if the override is a function to call
return niceTemplate(typeof inputs === 'string'
? inputs
: inputs(definition))
},
renderEachInput = definition => {
const lines = []
forEach(definition.inputs, (input, inputName) => {
if(input.type === 'label') { return }

View file

@ -0,0 +1,90 @@
import { writeFileSync } from 'fs'
import { isString, without } from 'lodash-es'
import { renderBlockImage } from '../docs/render_block.js'
export default class BlockIndexExporter {
definitionSet = null
destination = null
constructor(definitionSet, destination) {
this.definitionSet = definitionSet
this.destination = destination
}
export(givenOptions = {}) {
const
options = {
toFile: false,
filenameFunc: blockDef => blockDef.documentationPath(),
...givenOptions
},
categories = this.definitionSet.getCategories(),
categorized = [],
index = []
index.push(`---
title: "Block List"
editLink: false
---
# Block List`)
index.push(`${this.definitionSet.blocks.length} blocks and counting`)
categories.forEach(category => {
index.push(`## ${category.name}`)
index.push(
this.definitionSet.blocks.reduce((acc, def) => {
if(category.contents?.includes(def) || category.usesBlocks?.includes(def.type)) {
acc.push(definitionToIndexLines(def))
categorized.push(def)
}
return acc
}, []).join("\n")
)
})
// Special "Uncategorized" category
const uncategorized = without(this.definitionSet.blocks, ...categorized)
index.push(`## Uncategorized`)
index.push("These blocks do not appear in the toolbox directly. They may appear in gear menus, as sub-blocks, etc.")
index.push(
uncategorized.map(def => definitionToIndexLines(def)).join("\n")
)
const finalMarkdown = index.join("\n\n")
if(!options.toFile) {
return finalMarkdown
}
const filename = isString(options.toFile)
? options.toFile
: `index.md`
writeFileSync(`${this.destination}/${filename}`, finalMarkdown)
}
exportToFile = (toFile=true) => {
this.export({ toFile })
}
}
const definitionToIndexLines = def => {
const indexLines = []
// block name and link
indexLines.push(`### [${ def.name }](/${ def.documentationPath() })`)
// block short description
indexLines.push(`_${def.tooltip}_`)
// block image
indexLines.push(renderBlockImage(def))
return indexLines.join("\n")
}

View file

@ -1,6 +1,6 @@
import { mkdirSync, writeFileSync } from 'fs'
import { dirname } from 'path'
import { forEach } from 'lodash-es'
import { forEach, identity, pickBy } from 'lodash-es'
import toBlockMarkdown from "#src/docs/render_block.js"
@ -18,14 +18,13 @@ export default class BlockPageExporter {
const
options = {
toFile: false,
filenameFunc: null,
filenameFunc: blockDef => blockDef.documentationPath(),
...givenOptions
}
forEach(this.definitionSet.blocks, blockDefinition => {
const
docPath = options.filenameFunc?.(blockDefinition)
|| blockDefinition.definitionPath.replace(/.js$/, '.md'),
docPath = options.filenameFunc(blockDefinition),
fullPath = `${this.destination}/${docPath}`
mkdirSync(dirname(fullPath), { recursive: true })
@ -34,6 +33,11 @@ export default class BlockPageExporter {
}
exportToFile = (filenameFunc, toFile=true) => {
this.export({ toFile, filenameFunc })
const exportOptions = {
toFile,
// no filenameFunc if absent/falsy
...pickBy({ filenameFunc }, identity)
}
this.export(exportOptions)
}
}

View file

@ -1,15 +1,17 @@
const
// TODO: rely on project configuration for docs site location
DOCS_BLOCKS_ROOT = "https://adafruit.github.io/io-actions/blocks",
DOCS_BLOCKS_ROOT = "https://io.adafruit.com/actions-docs",
processHelp = definition => {
if (!definition.definitionPath) { return {} }
const thisBlockPredicate = definition.definitionPath.slice(0, -3)
const
// location of the markdown, without the .md extension
thisBlockPredicate = definition.documentationPath().slice(0, -3),
// build the full URL
helpUrl = `${DOCS_BLOCKS_ROOT}/${thisBlockPredicate}`
return {
helpUrl: `${DOCS_BLOCKS_ROOT}/${thisBlockPredicate}`
}
return { helpUrl }
}
export default processHelp

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Blockly: All Blocks</title>
<script type="module">
import { inject } from './blockly_app.js'
import { imageExportRegistryItems } from '#src/image_exporter.js'
inject("blocklyDiv", {
contextMenu: {
register: [ ...imageExportRegistryItems ]
},
extensionData: {
feedOptions: [
["Group\u00A0A Feed 1", "group-a.feed-1"],
["Group\u00A0A Feed 2", "group-a.feed-2"],
["Group\u00A0B Feed 1", "group-b.feed-1"],
["Group\u00A0C Feed 1", "group-C.feed-1"],
],
weatherLocationOptions: [
[ "Industry City", "1" ],
[ "Varick", "2" ],
[ "Shenzhen", "3" ],
],
currentWeatherByLocation: {
1: {
current: {
cloudCover: "5.4321",
}
}
}
}
})
</script>
<style>
#blocklyDiv {
width: 100%;
height: 100%;
position: absolute;
}
</style>
</head>
<body>
<div id="blocklyDiv"></div>
</body>
</html>

View file

@ -2,7 +2,9 @@ import BlockExporter from "./block_exporter.js"
import ToolboxExporter from "./toolbox_exporter.js"
import WorkspaceExporter from "./workspace_exporter.js"
import ScriptExporter from "./script_exporter.js"
import WorkspaceAllBlocksExporter from "./workspace_all_blocks_exporter.js"
import SidebarExporter from "./sidebar_exporter.js"
import BlockIndexExporter from "./block_index_exporter.js"
import BlockPageExporter from "./block_page_exporter.js"
@ -23,7 +25,9 @@ export const exportTo = async (destination, definitions, exportFunc) => {
blocks: new BlockExporter(definitions, destination).exportToFile,
script: new ScriptExporter(definitions, destination).exportToFile,
// docs exporters
workspaceAllBlocks: new WorkspaceAllBlocksExporter(definitions, destination).exportToFile,
sidebar: new SidebarExporter(definitions, destination).exportToFile,
blockIndex: new BlockIndexExporter(definitions, destination).exportToFile,
blockPages: new BlockPageExporter(definitions, destination).exportToFile,
}

View file

@ -13,7 +13,6 @@ const BYTECODE_BLOCK_TYPE_MAP = {
whenDataMatchStateChanged: 'when_data_matching_state',
matcherCompare: 'matcher_compare',
matcherTextCompare: 'matcher_text_compare',
matcherBooleanOperation: 'matcher_boolean_operation',
logAction: 'action_log',
conditional: 'io_controls_if',
compare: 'io_logic_compare',
@ -30,13 +29,15 @@ const BYTECODE_BLOCK_TYPE_MAP = {
negate: 'io_logic_negate',
setVariable: 'io_variables_set',
getVariable: 'io_variables_get',
feed: 'feed_selector',
getFeedValue: 'feed_get_value',
setFeedValue: 'feed_set_value',
publishAction: 'action_publish',
webhookAction: 'action_webhook',
emailAction: 'action_email',
smsAction: 'action_sms',
// removed blocks that have migration regenerators
// see: app/regenerators/migrations.js
feed: 'feed_selector',
publishAction: 'action_publish',
}
const lookupRegenerator = expressionName => {

View file

@ -25,24 +25,30 @@ export default class SidebarExporter {
collapsed: true,
items: []
}))
},
uncategorizedCategory = {
text: "Uncategorized",
collapsed: true,
items: []
}
blockSidebar.items.push(uncategorizedCategory)
forEach(this.definitionSet.blocks, blockDefinition => {
// skip disabled blocks
if(blockDefinition.disabled) { return }
const docPath = blockDefinition.definitionPath.replace(/.js$/, '.md')
const
blockSidebarPath = `/blocks/${docPath.slice(0, -3)}`,
sidebarEntry = {
text: blockDefinition.name,
link: blockSidebarPath
}
link: blockDefinition.documentationPath()
},
blockCategories = blockDefinition.getCategories()
// put into Uncategorized if no category
if(!blockCategories.length) {
uncategorizedCategory.items.push(sidebarEntry)
}
// add links to each sidebar category we're a part of
forEach(blockDefinition.getCategories(), category => {
forEach(blockCategories, category => {
// if category contains this block, add to its sidebar
const sidebarCategory = find(blockSidebar.items, { text: category.name })

View file

@ -0,0 +1,51 @@
import { writeFileSync } from 'fs'
import { isString, invokeMap } from 'lodash-es'
export default class WorkspaceExporter {
definitionSet = null
destination = null
constructor(definitionSet, destination) {
this.definitionSet = definitionSet
this.destination = destination
}
export(givenOptions = {}) {
const
options = {
toFile: false,
...givenOptions
},
allBlocks = invokeMap(this.definitionSet.blocks, "toBlocklyInstanceJSON"),
workspaceObject = {
blocks: {
languageVersion: 0,
blocks: allBlocks.map((block, index) => ({
...block,
id: `block-type-${block.type}`,
x: 50,
y: 50*index
}))
}
}
if(!options.toFile) {
return workspaceObject
}
const filename = isString(options.toFile)
? options.toFile
: `workspace_all_blocks.json`
writeFileSync(`${this.destination}/${filename}`, JSON.stringify(workspaceObject, null, 2))
}
exportToFile = (toFile=true) => {
this.export({ toFile })
}
static export() {
}
}

View file

@ -1,5 +1,7 @@
import Blockly from 'blockly'
import BLOCKLY_CSS from "#src/blockly_css.js"
// right-click menu items
export const imageExportRegistryItems = [
@ -8,18 +10,20 @@ export const imageExportRegistryItems = [
displayText: 'Save Block as SVG...',
weight: 100,
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
preconditionFn: scope => "enabled",
callback: (scope, menuOpenEvent, menuSelectEvent, location) => {
downloadBlockAsSVG(scope.block.id)
preconditionFn: () => "enabled",
callback: scope => {
const { id, type } = scope.block
downloadBlockAsSVG(id, type)
}
}, {
id: "block-png",
displayText: 'Save Block as PNG...',
weight: 100,
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
preconditionFn: scope => "enabled",
callback: (scope, menuOpenEvent, menuSelectEvent, location) => {
downloadBlockAsPNG(scope.block.id)
preconditionFn: () => "enabled",
callback: scope => {
const { id, type } = scope.block
downloadBlockAsPNG(id, type)
}
}
]
@ -75,13 +79,13 @@ const
URL.revokeObjectURL(element.href)
},
downloadBlockAsSVG = blockId => {
downloadBlockAsSVG = (blockId, blockType) => {
const svgObjectURL = blockToSVGBlob(blockId)
download(svgObjectURL, 'block.svg')
download(svgObjectURL, `${blockType}.svg`)
},
downloadBlockAsPNG = blockId => {
downloadBlockAsPNG = (blockId, blockType) => {
const
svgObjectURL = blockToSVGBlob(blockId),
img = new Image()
@ -95,504 +99,8 @@ const
// extract a png object url from the canvas and download it
const pngObjectURL = canvas.toDataURL("image/png")
download(pngObjectURL, 'block.png')
download(pngObjectURL, `${blockType}.png`)
}
img.src = svgObjectURL
}
// hardcode the CSS we actually use for now (includes geras renderer CSS)
const BLOCKLY_CSS = `
.blocklySvg {
background-color: #fff;
outline: none;
overflow: hidden; /* IE overflows by default. */
position: absolute;
display: block;
}
.blocklyWidgetDiv {
display: none;
position: absolute;
z-index: 99999; /* big value for bootstrap3 compatibility */
}
.injectionDiv {
height: 100%;
position: relative;
overflow: hidden; /* So blocks in drag surface disappear at edges */
touch-action: none;
}
.blocklyNonSelectable {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.blocklyBlockCanvas.blocklyCanvasTransitioning,
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
transition: transform .5s;
}
.blocklyTooltipDiv {
background-color: #ffffc7;
border: 1px solid #ddc;
box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);
color: #000;
display: none;
font: 9pt sans-serif;
opacity: .9;
padding: 2px;
position: absolute;
z-index: 100000; /* big value for bootstrap3 compatibility */
}
.blocklyDropDownDiv {
position: absolute;
left: 0;
top: 0;
z-index: 1000;
display: none;
border: 1px solid;
border-color: #dadce0;
background-color: #fff;
border-radius: 2px;
padding: 4px;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownContent {
max-height: 300px; /* @todo: spec for maximum height. */
overflow: auto;
overflow-x: hidden;
position: relative;
}
.blocklyDropDownArrow {
position: absolute;
left: 0;
top: 0;
width: 16px;
height: 16px;
z-index: -1;
background-color: inherit;
border-color: inherit;
}
.blocklyDropDownButton {
display: inline-block;
float: left;
padding: 0;
margin: 4px;
border-radius: 4px;
outline: none;
border: 1px solid;
transition: box-shadow .1s;
cursor: pointer;
}
.blocklyArrowTop {
border-top: 1px solid;
border-left: 1px solid;
border-top-left-radius: 4px;
border-color: inherit;
}
.blocklyArrowBottom {
border-bottom: 1px solid;
border-right: 1px solid;
border-bottom-right-radius: 4px;
border-color: inherit;
}
.blocklyResizeSE {
cursor: se-resize;
fill: #aaa;
}
.blocklyResizeSW {
cursor: sw-resize;
fill: #aaa;
}
.blocklyResizeLine {
stroke: #515A5A;
stroke-width: 1;
}
.blocklyHighlightedConnectionPath {
fill: none;
stroke: #fc3;
stroke-width: 4px;
}
.blocklyPathLight {
fill: none;
stroke-linecap: round;
stroke-width: 1;
}
.blocklySelected>.blocklyPathLight {
display: none;
}
.blocklyDraggable {
cursor: grab;
cursor: -webkit-grab;
}
.blocklyDragging {
cursor: grabbing;
cursor: -webkit-grabbing;
}
/* Changes cursor on mouse down. Not effective in Firefox because of
https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */
.blocklyDraggable:active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.blocklyDragging.blocklyDraggingDelete {
cursor: url("./handdelete.cur"), auto;
}
.blocklyDragging>.blocklyPath,
.blocklyDragging>.blocklyPathLight {
fill-opacity: .8;
stroke-opacity: .8;
}
.blocklyDragging>.blocklyPathDark {
display: none;
}
.blocklyDisabled>.blocklyPath {
fill-opacity: .5;
stroke-opacity: .5;
}
.blocklyDisabled>.blocklyPathLight,
.blocklyDisabled>.blocklyPathDark {
display: none;
}
.blocklyInsertionMarker>.blocklyPath,
.blocklyInsertionMarker>.blocklyPathLight,
.blocklyInsertionMarker>.blocklyPathDark {
fill-opacity: .2;
stroke: none;
}
.blocklyMultilineText {
font-family: monospace;
}
.blocklyNonEditableText>text {
pointer-events: none;
}
.blocklyFlyout {
position: absolute;
z-index: 20;
}
.blocklyText text {
cursor: default;
}
/*
Don't allow users to select text. It gets annoying when trying to
drag a block and selected text moves instead.
*/
.blocklySvg text {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
cursor: inherit;
}
.blocklyHidden {
display: none;
}
.blocklyFieldDropdown:not(.blocklyHidden) {
display: block;
}
.blocklyIconGroup {
cursor: default;
}
.blocklyIconGroup:not(:hover),
.blocklyIconGroupReadonly {
opacity: .6;
}
.blocklyIconShape {
fill: #00f;
stroke: #fff;
stroke-width: 1px;
}
.blocklyIconSymbol {
fill: #fff;
}
.blocklyMinimalBody {
margin: 0;
padding: 0;
}
.blocklyHtmlInput {
border: none;
border-radius: 4px;
height: 100%;
margin: 0;
outline: none;
padding: 0;
width: 100%;
text-align: center;
display: block;
box-sizing: border-box;
}
/* Remove the increase and decrease arrows on the field number editor */
input.blocklyHtmlInput[type=number]::-webkit-inner-spin-button,
input.blocklyHtmlInput[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
.blocklyMainBackground {
stroke-width: 1;
stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */
}
.blocklyMutatorBackground {
fill: #fff;
stroke: #ddd;
stroke-width: 1;
}
.blocklyFlyoutBackground {
fill: #ddd;
fill-opacity: .8;
}
.blocklyMainWorkspaceScrollbar {
z-index: 20;
}
.blocklyFlyoutScrollbar {
z-index: 30;
}
.blocklyScrollbarHorizontal,
.blocklyScrollbarVertical {
position: absolute;
outline: none;
}
.blocklyScrollbarBackground {
opacity: 0;
}
.blocklyScrollbarHandle {
fill: #ccc;
}
.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyScrollbarHandle:hover {
fill: #bbb;
}
/* Darken flyout scrollbars due to being on a grey background. */
/* By contrast, workspace scrollbars are on a white background. */
.blocklyFlyout .blocklyScrollbarHandle {
fill: #bbb;
}
.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
.blocklyFlyout .blocklyScrollbarHandle:hover {
fill: #aaa;
}
.blocklyInvalidInput {
background: #faa;
}
.blocklyVerticalMarker {
stroke-width: 3px;
fill: rgba(255,255,255,.5);
pointer-events: none;
}
.blocklyComputeCanvas {
position: absolute;
width: 0;
height: 0;
}
.blocklyNoPointerEvents {
pointer-events: none;
}
.blocklyContextMenu {
border-radius: 4px;
max-height: 100%;
}
.blocklyDropdownMenu {
border-radius: 2px;
padding: 0 !important;
}
.blocklyDropdownMenu .blocklyMenuItem {
/* 28px on the left for icon or checkbox. */
padding-left: 28px;
}
/* BiDi override for the resting state. */
.blocklyDropdownMenu .blocklyMenuItemRtl {
/* Flip left/right padding for BiDi. */
padding-left: 5px;
padding-right: 28px;
}
.blocklyWidgetDiv .blocklyMenu {
background: #fff;
border: 1px solid transparent;
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
font: normal 13px Arial, sans-serif;
margin: 0;
outline: none;
padding: 4px 0;
position: absolute;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
.blocklyWidgetDiv .blocklyMenu.blocklyFocused {
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
}
.blocklyDropDownDiv .blocklyMenu {
background: inherit; /* Compatibility with gapi, reset from goog-menu */
border: inherit; /* Compatibility with gapi, reset from goog-menu */
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
outline: none;
position: relative; /* Compatibility with gapi, reset from goog-menu */
z-index: 20000; /* Arbitrary, but some apps depend on it... */
}
/* State: resting. */
.blocklyMenuItem {
border: none;
color: #000;
cursor: pointer;
list-style: none;
margin: 0;
/* 7em on the right for shortcut. */
min-width: 7em;
padding: 6px 15px;
white-space: nowrap;
}
/* State: disabled. */
.blocklyMenuItemDisabled {
color: #ccc;
cursor: inherit;
}
/* State: hover. */
.blocklyMenuItemHighlight {
background-color: rgba(0,0,0,.1);
}
/* State: selected/checked. */
.blocklyMenuItemCheckbox {
height: 16px;
position: absolute;
width: 16px;
}
.blocklyMenuItemSelected .blocklyMenuItemCheckbox {
background: url(./sprites.png) no-repeat -48px -16px;
float: left;
margin-left: -24px;
position: static; /* Scroll with the menu. */
}
.blocklyMenuItemRtl .blocklyMenuItemCheckbox {
float: right;
margin-right: -24px;
}
/* Begin .geras-renderer.classic-theme (stripped) */
.blocklyText,
.blocklyFlyoutLabelText {
font: normal 11pt sans-serif;
}
.blocklyText {
fill: #fff;
}
.blocklyNonEditableText>rect,
.blocklyEditableText>rect {
fill: #fff;
fill-opacity: .6;
stroke: none;
}
.blocklyNonEditableText>text,
.blocklyEditableText>text {
fill: #000;
}
.blocklyFlyoutLabelText {
fill: #000;
}
.blocklyText.blocklyBubbleText {
fill: #000;
}
.blocklyEditableText:not(.editing):hover>rect {
stroke: #fff;
stroke-width: 2;
}
.blocklyHtmlInput {
font-family: sans-serif;
font-weight: normal;
}
.blocklySelected>.blocklyPath {
stroke: #fc3;
stroke-width: 3px;
}
.blocklyHighlightedConnectionPath {
stroke: #fc3;
}
.blocklyReplaceable .blocklyPath {
fill-opacity: .5;
}
.blocklyReplaceable .blocklyPathLight,
.blocklyReplaceable .blocklyPathDark {
display: none;
}
.blocklyInsertionMarker>.blocklyPath {
fill-opacity: 0.2;
stroke: none;
}
.blocklyInsertionMarker>.blocklyPathLight,
.blocklyInsertionMarker>.blocklyPathDark {
fill-opacity: 0.2;
stroke: none;
}
`

View file

@ -11,6 +11,7 @@ const
EXTENSION_LOCATION = `extensions/`,
MIXIN_LOCATION = `mixins/`,
MUTATOR_LOCATION = `mutators/`,
REGENERATOR_LOCATION = `regenerators/`,
TOOLBOX_LOCATION = `toolbox/`,
WORKSPACE_LOCATION = `workspace/`,
@ -62,6 +63,23 @@ export const DefinitionLoader = {
))
},
loadRegenerators: async (appLocation=APP_LOCATION) => {
const jsfiles = await glob(`./${appLocation}/${REGENERATOR_LOCATION}**/*.js`, { ignore: EXAMPLE_FILES })
// loads app/regenerators/*.js into object like:
// { blockType: { json: Function }}
let regenerators = {}
await Promise.all(jsfiles.map( async filePath => {
const regeneratorsFromFile = (await import(`${PROJECT_ROOT}/${filePath}`)).default
regenerators = {
...regenerators,
...regeneratorsFromFile
}
}))
return regenerators
},
loadBlocks: async (appLocation=APP_LOCATION) => {
// get the file listing
const
@ -101,6 +119,7 @@ export const DefinitionLoader = {
mutators: await DefinitionLoader.loadMutators(options.source),
mixins: await DefinitionLoader.loadMixins(options.source),
extensions: await DefinitionLoader.loadExtensions(options.source),
regenerators: await DefinitionLoader.loadRegenerators(options.source),
blocks: await DefinitionLoader.loadBlocks(options.source),
toolboxes: await DefinitionLoader.loadToolboxes(options.source),
workspaces: await DefinitionLoader.loadWorkspaces(options.source),

View file

@ -18,6 +18,7 @@ exports[`Block Snapshots > Blockly JSON > action_email 1`] = `
{
"type": "input_value",
"name": "SUBJECT",
"check": "expression",
"align": "RIGHT"
}
],
@ -26,10 +27,11 @@ exports[`Block Snapshots > Blockly JSON > action_email 1`] = `
{
"type": "input_value",
"name": "BODY",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/email"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/notifications/email"
}
`;
@ -46,45 +48,11 @@ exports[`Block Snapshots > Blockly JSON > action_log 1`] = `
{
"type": "input_value",
"name": "EXPRESSION",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/log"
}
`;
exports[`Block Snapshots > Blockly JSON > action_publish 1`] = `
{
"inputsInline": false,
"type": "action_publish",
"colour": "0",
"tooltip": "Sends the given value to the specified Feed.",
"nextStatement": "expression",
"previousStatement": "expression",
"message0": "📈 Publish %1",
"args0": [
{
"type": "input_dummy",
"align": "CENTRE"
}
],
"message1": "...value: %1",
"args1": [
{
"type": "input_value",
"name": "VALUE",
"align": "RIGHT"
}
],
"message2": "...to: %1",
"args2": [
{
"type": "input_value",
"name": "FEED",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/publish"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/utility/log"
}
`;
@ -134,7 +102,7 @@ exports[`Block Snapshots > Blockly JSON > action_root 1`] = `
}
],
"mutator": "action_root",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/root"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/root"
}
`;
@ -158,10 +126,11 @@ exports[`Block Snapshots > Blockly JSON > action_sms 1`] = `
{
"type": "input_value",
"name": "BODY",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/sms"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/notifications/sms"
}
`;
@ -185,6 +154,7 @@ exports[`Block Snapshots > Blockly JSON > action_webhook 1`] = `
{
"type": "input_value",
"name": "URL",
"check": "expression",
"align": "RIGHT"
}
],
@ -205,10 +175,11 @@ exports[`Block Snapshots > Blockly JSON > action_webhook 1`] = `
{
"type": "input_value",
"name": "BODY",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/webhook"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/notifications/webhook"
}
`;
@ -227,7 +198,7 @@ exports[`Block Snapshots > Blockly JSON > all_days 1`] = `
}
],
"mutator": "all_days",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/all_days"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_days"
}
`;
@ -246,7 +217,7 @@ exports[`Block Snapshots > Blockly JSON > all_hours 1`] = `
}
],
"mutator": "all_hours",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/all_hours"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_hours"
}
`;
@ -265,7 +236,7 @@ exports[`Block Snapshots > Blockly JSON > all_minutes 1`] = `
}
],
"mutator": "all_minutes",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/all_minutes"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_minutes"
}
`;
@ -284,7 +255,7 @@ exports[`Block Snapshots > Blockly JSON > all_months 1`] = `
}
],
"mutator": "all_months",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/all_months"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_months"
}
`;
@ -303,7 +274,7 @@ exports[`Block Snapshots > Blockly JSON > day_settings 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/day_settings"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/day_settings"
}
`;
@ -364,7 +335,7 @@ exports[`Block Snapshots > Blockly JSON > days_of_week 1`] = `
}
],
"mutator": "days_of_week",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/days_of_week"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/days_of_week"
}
`;
@ -382,7 +353,7 @@ exports[`Block Snapshots > Blockly JSON > delay_days 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_days"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_days"
}
`;
@ -498,7 +469,7 @@ exports[`Block Snapshots > Blockly JSON > delay_hours 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_hours"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_hours"
}
`;
@ -758,7 +729,7 @@ exports[`Block Snapshots > Blockly JSON > delay_minutes 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_minutes"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_minutes"
}
`;
@ -776,7 +747,7 @@ exports[`Block Snapshots > Blockly JSON > delay_none 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_none"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_none"
}
`;
@ -1036,7 +1007,7 @@ exports[`Block Snapshots > Blockly JSON > delay_seconds 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_seconds"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_seconds"
}
`;
@ -1090,7 +1061,7 @@ exports[`Block Snapshots > Blockly JSON > delay_settings 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_settings"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_settings"
}
`;
@ -1403,7 +1374,7 @@ exports[`Block Snapshots > Blockly JSON > every_days_between 1`] = `
}
],
"mutator": "every_days_between",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/every_days_between"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_days_between"
}
`;
@ -1660,7 +1631,7 @@ exports[`Block Snapshots > Blockly JSON > every_hours_between 1`] = `
}
],
"mutator": "every_hours_between",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/every_hours_between"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_hours_between"
}
`;
@ -2221,7 +2192,7 @@ exports[`Block Snapshots > Blockly JSON > every_minutes_between 1`] = `
}
],
"mutator": "every_minutes_between",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/every_minutes_between"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_minutes_between"
}
`;
@ -2374,7 +2345,7 @@ exports[`Block Snapshots > Blockly JSON > every_months_between 1`] = `
}
],
"mutator": "every_months_between",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/every_months_between"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_months_between"
}
`;
@ -2384,7 +2355,7 @@ exports[`Block Snapshots > Blockly JSON > feed_get_value 1`] = `
"type": "feed_get_value",
"colour": 300,
"tooltip": "Resolves to the last value of this feed or component, always a String",
"output": null,
"output": "expression",
"message0": "Get %1 %2",
"args0": [
{
@ -2406,39 +2377,7 @@ exports[`Block Snapshots > Blockly JSON > feed_get_value 1`] = `
"replaceDropdownOptions",
"populateFeedDropdown"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/feed/get_value"
}
`;
exports[`Block Snapshots > Blockly JSON > feed_selector 1`] = `
{
"inputsInline": false,
"type": "feed_selector",
"colour": 300,
"tooltip": "The last value of this feed or component, always a String",
"output": null,
"message0": "Feed: %1 %2",
"args0": [
{
"name": "FEED_KEY",
"type": "field_dropdown",
"options": [
[
"Loading Feeds...",
""
]
]
},
{
"type": "input_dummy",
"align": "RIGHT"
}
],
"extensions": [
"replaceDropdownOptions",
"populateFeedDropdown"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/feed/selector"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/feeds/get_value"
}
`;
@ -2465,6 +2404,7 @@ exports[`Block Snapshots > Blockly JSON > feed_set_value 1`] = `
{
"type": "input_value",
"name": "VALUE",
"check": "expression",
"align": "RIGHT"
}
],
@ -2472,7 +2412,7 @@ exports[`Block Snapshots > Blockly JSON > feed_set_value 1`] = `
"replaceDropdownOptions",
"populateFeedDropdown"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/feed/set_value"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/feeds/set_value"
}
`;
@ -2491,7 +2431,7 @@ exports[`Block Snapshots > Blockly JSON > hour_settings 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/hour_settings"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/hour_settings"
}
`;
@ -2508,6 +2448,7 @@ exports[`Block Snapshots > Blockly JSON > io_controls_if 1`] = `
{
"type": "input_value",
"name": "IF0",
"check": "expression",
"align": "RIGHT"
}
],
@ -2516,6 +2457,7 @@ exports[`Block Snapshots > Blockly JSON > io_controls_if 1`] = `
{
"type": "input_statement",
"name": "THEN0",
"check": "expression",
"align": "RIGHT"
}
],
@ -2536,7 +2478,7 @@ exports[`Block Snapshots > Blockly JSON > io_controls_if 1`] = `
}
],
"mutator": "io_controls_if",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/controls/if"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/if"
}
`;
@ -2546,7 +2488,10 @@ exports[`Block Snapshots > Blockly JSON > io_logic_boolean 1`] = `
"type": "io_logic_boolean",
"colour": 60,
"tooltip": "A true or false value.",
"output": "boolean",
"output": [
"expression",
"boolean"
],
"message0": "%1 %2",
"args0": [
{
@ -2568,7 +2513,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_boolean 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/logic/boolean"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/boolean"
}
`;
@ -2578,12 +2523,13 @@ exports[`Block Snapshots > Blockly JSON > io_logic_compare 1`] = `
"type": "io_logic_compare",
"colour": 120,
"tooltip": "Numerically compare two given values using the selected math operation.",
"output": null,
"output": "expression",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "A",
"check": "expression",
"align": "RIGHT"
}
],
@ -2622,10 +2568,11 @@ exports[`Block Snapshots > Blockly JSON > io_logic_compare 1`] = `
{
"type": "input_value",
"name": "B",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/compare"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/compare"
}
`;
@ -2635,16 +2582,17 @@ exports[`Block Snapshots > Blockly JSON > io_logic_negate 1`] = `
"type": "io_logic_negate",
"colour": 60,
"tooltip": "Swaps a truthy value to \`false\`, or a falsy value to \`true\`.",
"output": null,
"output": "expression",
"message0": "not %1",
"args0": [
{
"type": "input_value",
"name": "EXPRESSION",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/logic/negate"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/negate"
}
`;
@ -2654,12 +2602,13 @@ exports[`Block Snapshots > Blockly JSON > io_logic_operation 1`] = `
"type": "io_logic_operation",
"colour": 60,
"tooltip": "Perform the specifed boolean logic operation on two operands.",
"output": null,
"output": "expression",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "A",
"check": "expression",
"align": "RIGHT"
}
],
@ -2682,10 +2631,11 @@ exports[`Block Snapshots > Blockly JSON > io_logic_operation 1`] = `
{
"type": "input_value",
"name": "B",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/logic/operation"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/operation"
}
`;
@ -2695,12 +2645,13 @@ exports[`Block Snapshots > Blockly JSON > io_math_arithmetic 1`] = `
"type": "io_math_arithmetic",
"colour": 120,
"tooltip": "Perform the specified arithmetic operation on two specified operands.",
"output": null,
"output": "expression",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "A",
"check": "expression",
"align": "RIGHT"
}
],
@ -2735,10 +2686,11 @@ exports[`Block Snapshots > Blockly JSON > io_math_arithmetic 1`] = `
{
"type": "input_value",
"name": "B",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/arithmetic"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/arithmetic"
}
`;
@ -2754,6 +2706,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_constrain 1`] = `
{
"type": "input_value",
"name": "VALUE",
"check": "expression",
"align": "RIGHT"
}
],
@ -2766,7 +2719,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_constrain 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/constrain"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/constrain"
}
`;
@ -2776,7 +2729,10 @@ exports[`Block Snapshots > Blockly JSON > io_math_number 1`] = `
"type": "io_math_number",
"colour": 120,
"tooltip": "A numeric value, whole or decimal.",
"output": "number",
"output": [
"expression",
"number"
],
"message0": " %1 %2",
"args0": [
{
@ -2792,7 +2748,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_number 1`] = `
"extensions": [
"validateNumbers"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/number"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/number"
}
`;
@ -2826,10 +2782,11 @@ exports[`Block Snapshots > Blockly JSON > io_math_round 1`] = `
{
"type": "input_value",
"name": "VALUE",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/round"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/round"
}
`;
@ -2839,7 +2796,10 @@ exports[`Block Snapshots > Blockly JSON > io_text 1`] = `
"type": "io_text",
"colour": 180,
"tooltip": "A String of text",
"output": "String",
"output": [
"expression",
"string"
],
"message0": "\\"%1 %2",
"args0": [
{
@ -2852,7 +2812,7 @@ exports[`Block Snapshots > Blockly JSON > io_text 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/text"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/text"
}
`;
@ -2862,12 +2822,13 @@ exports[`Block Snapshots > Blockly JSON > io_text_join 1`] = `
"type": "io_text_join",
"colour": 180,
"tooltip": "Join two pieces of text into one.",
"output": null,
"output": "expression",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "A",
"check": "expression",
"align": "RIGHT"
}
],
@ -2876,10 +2837,11 @@ exports[`Block Snapshots > Blockly JSON > io_text_join 1`] = `
{
"type": "input_value",
"name": "B",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/join"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/join"
}
`;
@ -2889,8 +2851,11 @@ exports[`Block Snapshots > Blockly JSON > io_text_multiline 1`] = `
"type": "io_text_multiline",
"colour": 180,
"tooltip": "A String of longer-form text with newlines.",
"output": "String",
"message0": "P %1 %2",
"output": [
"expression",
"string"
],
"message0": "¶ %1 %2",
"args0": [
{
"name": "TEXT",
@ -2902,7 +2867,7 @@ exports[`Block Snapshots > Blockly JSON > io_text_multiline 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/text_multiline"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/text_multiline"
}
`;
@ -2912,7 +2877,7 @@ exports[`Block Snapshots > Blockly JSON > io_variables_get 1`] = `
"type": "io_variables_get",
"colour": 240,
"tooltip": "Get the value previously assigned to a variable.",
"output": "String",
"output": "expression",
"message0": "Get variable %1 %2",
"args0": [
{
@ -2924,7 +2889,7 @@ exports[`Block Snapshots > Blockly JSON > io_variables_get 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/variables/get"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/variables/get"
}
`;
@ -2945,43 +2910,11 @@ exports[`Block Snapshots > Blockly JSON > io_variables_set 1`] = `
{
"type": "input_value",
"name": "VALUE",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/variables/set"
}
`;
exports[`Block Snapshots > Blockly JSON > matcher_boolean_operation 1`] = `
{
"inputsInline": true,
"type": "matcher_boolean_operation",
"colour": 60,
"tooltip": "Perform a logic operation between the triggering Feed value and a block diagram.",
"output": "matcher",
"message0": "is true %1 %2",
"args0": [
{
"name": "OP",
"type": "field_dropdown",
"options": [
[
"and",
"AND"
],
[
"or",
"OR"
]
]
},
{
"type": "input_value",
"name": "B",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/matchers/matcher_boolean_operation"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/variables/set"
}
`;
@ -3027,10 +2960,11 @@ exports[`Block Snapshots > Blockly JSON > matcher_compare 1`] = `
{
"type": "input_value",
"name": "B",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/matchers/matcher_compare"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/matcher_compare"
}
`;
@ -3064,10 +2998,11 @@ exports[`Block Snapshots > Blockly JSON > matcher_text_compare 1`] = `
{
"type": "input_value",
"name": "B",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/matchers/matcher_text_compare"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/matcher_text_compare"
}
`;
@ -3090,6 +3025,7 @@ exports[`Block Snapshots > Blockly JSON > math_map 1`] = `
{
"type": "input_value",
"name": "VALUE",
"check": "expression",
"align": "RIGHT"
}
],
@ -3111,7 +3047,7 @@ exports[`Block Snapshots > Blockly JSON > math_map 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/map"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/map"
}
`;
@ -3127,6 +3063,7 @@ exports[`Block Snapshots > Blockly JSON > math_range 1`] = `
{
"type": "input_value",
"name": "FROM",
"check": "expression",
"align": "RIGHT"
}
],
@ -3135,6 +3072,7 @@ exports[`Block Snapshots > Blockly JSON > math_range 1`] = `
{
"type": "input_value",
"name": "TO",
"check": "expression",
"align": "RIGHT"
}
],
@ -3145,7 +3083,7 @@ exports[`Block Snapshots > Blockly JSON > math_range 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/range"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/range"
}
`;
@ -3164,7 +3102,7 @@ exports[`Block Snapshots > Blockly JSON > minute_settings 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/minute_settings"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/minute_settings"
}
`;
@ -3183,7 +3121,7 @@ exports[`Block Snapshots > Blockly JSON > month_settings 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/month_settings"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/month_settings"
}
`;
@ -3238,7 +3176,7 @@ exports[`Block Snapshots > Blockly JSON > on_schedule 1`] = `
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/on_schedule"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/on_schedule"
}
`;
@ -3387,7 +3325,7 @@ exports[`Block Snapshots > Blockly JSON > one_day 1`] = `
}
],
"mutator": "one_day",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/one_day"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_day"
}
`;
@ -3508,7 +3446,7 @@ exports[`Block Snapshots > Blockly JSON > one_hour 1`] = `
}
],
"mutator": "one_hour",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/one_hour"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_hour"
}
`;
@ -3773,7 +3711,7 @@ exports[`Block Snapshots > Blockly JSON > one_minute 1`] = `
}
],
"mutator": "one_minute",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/one_minute"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_minute"
}
`;
@ -3846,7 +3784,7 @@ exports[`Block Snapshots > Blockly JSON > one_month 1`] = `
}
],
"mutator": "one_month",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/one_month"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_month"
}
`;
@ -3939,7 +3877,7 @@ exports[`Block Snapshots > Blockly JSON > some_months 1`] = `
}
],
"mutator": "some_months",
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/some_months"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/some_months"
}
`;
@ -3949,12 +3887,13 @@ exports[`Block Snapshots > Blockly JSON > text_compare 1`] = `
"type": "text_compare",
"colour": 180,
"tooltip": "Compare two chunks of text for equality, inequality, or inclusion.",
"output": null,
"output": "expression",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "A",
"check": "expression",
"align": "RIGHT"
}
],
@ -3981,10 +3920,11 @@ exports[`Block Snapshots > Blockly JSON > text_compare 1`] = `
{
"type": "input_value",
"name": "B",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/compare"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/compare"
}
`;
@ -3994,16 +3934,20 @@ exports[`Block Snapshots > Blockly JSON > text_template 1`] = `
"type": "text_template",
"colour": 180,
"tooltip": "Render a text template.",
"output": null,
"output": [
"expression",
"string"
],
"message0": "{{ %1",
"args0": [
{
"type": "input_value",
"name": "TEMPLATE",
"check": "expression",
"align": "RIGHT"
}
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/template"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/template"
}
`;
@ -4013,7 +3957,7 @@ exports[`Block Snapshots > Blockly JSON > weather 1`] = `
"type": "weather",
"colour": 360,
"tooltip": "Fetch the current or forecast weather conditions at the specified location.",
"output": null,
"output": "expression",
"message0": "Weather %1",
"args0": [
{
@ -4156,7 +4100,7 @@ exports[`Block Snapshots > Blockly JSON > weather 1`] = `
"weatherMixin",
"prepareWeather"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/power_ups/weather"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/weather/weather"
}
`;
@ -4189,7 +4133,7 @@ exports[`Block Snapshots > Blockly JSON > when_data 1`] = `
"replaceDropdownOptions",
"populateFeedDropdown"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/when_data"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/when_data"
}
`;
@ -4224,7 +4168,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching 1`] = `
"replaceDropdownOptions",
"populateFeedDropdown"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/when_data_matching"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/when_data_matching"
}
`;
@ -4281,6 +4225,6 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching_state 1`] = `
"replaceDropdownOptions",
"populateFeedDropdown"
],
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/when_data_matching_state"
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/when_data_matching_state"
}
`;