commit
4157ad3026
47 changed files with 1402 additions and 225 deletions
|
|
@ -1 +1 @@
|
|||
nodejs 18.17.0
|
||||
nodejs 22.5.1
|
||||
|
|
|
|||
21
app/blocks/root/action_settings/delay_days.js
Normal file
21
app/blocks/root/action_settings/delay_days.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export default {
|
||||
type: "delay_days",
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: "1 day is the maximum delay available"
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: "delay_period",
|
||||
},
|
||||
|
||||
lines: [ "1 day" ],
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/blocks/root/action_settings/delay_hours.js
Normal file
36
app/blocks/root/action_settings/delay_hours.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
|
||||
export default {
|
||||
type: "delay_hours",
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: "Set a delay between 1 and 23 hours",
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: "delay_period",
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "%SECONDS hours", {
|
||||
field: 'SECONDS',
|
||||
options: makeOptions({ from: 1, upTo: 24, valueFunc: sec => sec*60*60 })
|
||||
// options: [
|
||||
// ['1', '3600'],
|
||||
// ['2', '7200'],
|
||||
// ...
|
||||
// ['58', '79200'],
|
||||
// ['59', '82800'],
|
||||
// ]
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/blocks/root/action_settings/delay_minutes.js
Normal file
36
app/blocks/root/action_settings/delay_minutes.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
|
||||
export default {
|
||||
type: "delay_minutes",
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: "Set a delay between 1 and 59 minutes",
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: "delay_period",
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "%SECONDS minutes", {
|
||||
field: 'SECONDS',
|
||||
options: makeOptions({ from: 1, upTo: 60, valueFunc: sec => sec*60 })
|
||||
// options: [
|
||||
// ['1', '60'],
|
||||
// ['2', '120'],
|
||||
// ...
|
||||
// ['58', '3480'],
|
||||
// ['59', '3540'],
|
||||
// ]
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
21
app/blocks/root/action_settings/delay_none.js
Normal file
21
app/blocks/root/action_settings/delay_none.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export default {
|
||||
type: "delay_none",
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: "No delay: Actions run immediately when triggered."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: "delay_period",
|
||||
},
|
||||
|
||||
lines: [ "No Delay" ],
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/blocks/root/action_settings/delay_seconds.js
Normal file
35
app/blocks/root/action_settings/delay_seconds.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
|
||||
export default {
|
||||
type: "delay_seconds",
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
tooltip: "Set a delay between 1 and 59 seconds (or 0 for no delay)",
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: "delay_period",
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "%SECONDS seconds", {
|
||||
field: 'SECONDS',
|
||||
options: makeOptions({ from: 1, upTo: 60 })
|
||||
// options: [
|
||||
// ['1', '1'],
|
||||
// ...
|
||||
// ['58', '58'],
|
||||
// ['59', '59'],
|
||||
// ]
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/blocks/root/action_settings/delay_settings.js
Normal file
51
app/blocks/root/action_settings/delay_settings.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export default {
|
||||
type: "delay_settings",
|
||||
|
||||
// same as Blockly JSON
|
||||
visualization: {
|
||||
colour: '0',
|
||||
tooltip: [
|
||||
"Causes a delay between this Action's trigger and its execution",
|
||||
"---------------",
|
||||
"Parameters:",
|
||||
"Delay - how long to delay, from 1 second to 1 day",
|
||||
"Mode - how to proceed if another action is already on delay.",
|
||||
"...'keep' will keep the existing delay and ignore new triggers",
|
||||
"...'reset' will delete the existing delay and start a new one",
|
||||
].join('\n'),
|
||||
},
|
||||
|
||||
connections: { },
|
||||
|
||||
lines: [
|
||||
["Delay Settings", "CENTER"],
|
||||
["Delay:", {
|
||||
// for a single block input
|
||||
inputValue: 'DELAY_PERIOD',
|
||||
check: 'delay_period',
|
||||
shadow: 'delay_none',
|
||||
}],
|
||||
|
||||
["and", {
|
||||
field: 'DELAY_MODE',
|
||||
options: [
|
||||
['reset', 'extend'],
|
||||
['keep', 'static'],
|
||||
],
|
||||
}],
|
||||
|
||||
"existing delays"
|
||||
],
|
||||
|
||||
// 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 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
66
app/blocks/root/action_settings/mutator.js
Normal file
66
app/blocks/root/action_settings/mutator.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
|
||||
/** Action settings on the root block */
|
||||
export default {
|
||||
delaySeconds: 0,
|
||||
delayMode: 'extend',
|
||||
|
||||
saveExtraState: function() {
|
||||
return {
|
||||
delaySeconds: this.delaySeconds,
|
||||
delayMode: this.delayMode,
|
||||
}
|
||||
},
|
||||
|
||||
loadExtraState: function({ delaySeconds, delayMode }) {
|
||||
this.delaySeconds = delaySeconds || 0
|
||||
this.delayMode = delayMode || "extend"
|
||||
},
|
||||
|
||||
flyoutBlockTypes: [ 'delay_none', 'delay_seconds', 'delay_minutes', 'delay_hours', 'delay_days' ],
|
||||
|
||||
decompose: function(workspace) {
|
||||
// initialize the top-level block for the sub-diagram
|
||||
const delaySettingsBlock = workspace.newBlock('delay_settings')
|
||||
delaySettingsBlock.initSvg()
|
||||
|
||||
// set the appropriate delay block type by the seconds
|
||||
const delayBlockType = (this.delaySeconds <= 0) ? "delay_none"
|
||||
: (this.delaySeconds < 60) ? "delay_seconds"
|
||||
: (this.delaySeconds < 3600) ? "delay_minutes"
|
||||
: (this.delaySeconds < 86400) ? "delay_hours"
|
||||
: (this.delaySeconds >= 86400) ? "delay_days"
|
||||
: "delay_none"
|
||||
|
||||
const delayPeriodBlock = workspace.newBlock(delayBlockType)
|
||||
delayPeriodBlock.initSvg()
|
||||
|
||||
if(delayBlockType !== "delay_days" && delayBlockType !== "delay_none") {
|
||||
// set its seconds field (day doesn't have one)
|
||||
delayPeriodBlock.setFieldValue(this.delaySeconds.toString(), "SECONDS")
|
||||
}
|
||||
|
||||
// connect it to the delay settings block
|
||||
const
|
||||
{ connection } = delaySettingsBlock.getInput("DELAY_PERIOD"),
|
||||
{ outputConnection } = delayPeriodBlock
|
||||
|
||||
connection.connect(outputConnection)
|
||||
connection.setShadowState({ type: 'delay_none' })
|
||||
|
||||
// set the mode field
|
||||
delaySettingsBlock.setFieldValue(this.delayMode, 'DELAY_MODE')
|
||||
|
||||
return delaySettingsBlock
|
||||
},
|
||||
|
||||
compose: function(delayBlock) {
|
||||
const
|
||||
periodInput = delayBlock.getInputTargetBlock("DELAY_PERIOD"),
|
||||
value = (periodInput?.type == "delay_days") // hard-code for day
|
||||
? "86400"
|
||||
: periodInput?.getFieldValue("SECONDS") || 0
|
||||
|
||||
this.delaySeconds = parseInt(value, 10)
|
||||
this.delayMode = delayBlock.getFieldValue("DELAY_MODE")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,32 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./action_settings/mutator.js?key=${random}`)).default
|
||||
|
||||
export default {
|
||||
type: "action_root",
|
||||
|
||||
toolbox: {},
|
||||
connections: {},
|
||||
|
||||
visualization: {
|
||||
colour: "0",
|
||||
extensions: [ ],
|
||||
tooltip: "Add Triggers to determine when this Action runs.\nAdd Actions to determine what this Action does."
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
[ "", "LEFT" ],
|
||||
[ "Triggers:", "LEFT" ],
|
||||
[ "", {
|
||||
inputStatement: "TRIGGERS",
|
||||
check: 'trigger'
|
||||
}],
|
||||
|
||||
[ "...delay this Action by %DELAY", {
|
||||
align: "LEFT",
|
||||
field: 'DELAY',
|
||||
options: [
|
||||
["no delay", "0"],
|
||||
["5s", "5"],
|
||||
["5m", "300"],
|
||||
["5h", "1800"],
|
||||
]
|
||||
}],
|
||||
|
||||
[ "...repeat Actions %DELAY_MODE", {
|
||||
field: 'DELAY_MODE',
|
||||
options: [
|
||||
["reset existing delays", "extend"],
|
||||
["are ignored", "static"],
|
||||
]
|
||||
}],
|
||||
|
||||
[ "", "LEFT" ],
|
||||
|
||||
[ "Actions:", "LEFT" ],
|
||||
[ "", {
|
||||
inputStatement: "EXPRESSIONS",
|
||||
// check: "expression"
|
||||
check: "expression"
|
||||
}],
|
||||
|
||||
[ "", "LEFT" ],
|
||||
|
|
@ -71,8 +55,8 @@ export default {
|
|||
}
|
||||
|
||||
const
|
||||
seconds = parseInt(block.getFieldValue('DELAY'), 10),
|
||||
mode = block.getFieldValue('DELAY_MODE'),
|
||||
seconds = block.delaySeconds,
|
||||
mode = block.delayMode,
|
||||
delay = (seconds > 0)
|
||||
? { seconds, mode }
|
||||
: undefined
|
||||
|
|
@ -96,9 +80,9 @@ export default {
|
|||
deletable: false,
|
||||
x: 50,
|
||||
y: 50,
|
||||
fields: {
|
||||
DELAY: (settings.delay?.seconds || 0).toString(),
|
||||
DELAY_MODE: settings.delay?.mode || 'extend'
|
||||
extraState: {
|
||||
delaySeconds: settings.delay?.seconds || 0,
|
||||
delayMode: settings.delay?.mode || 'extend'
|
||||
},
|
||||
inputs: {
|
||||
"TRIGGERS": helpers.arrayToStatements(triggers),
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import { map, range } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
type: "every_day",
|
||||
|
||||
toolbox: {
|
||||
category: 'Triggers',
|
||||
label: "Runs the Action every day on the specified hour."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Run this action daily on the specified hour."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "trigger",
|
||||
next: "trigger"
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "...every day at %HOUR", {
|
||||
align: 'LEFT',
|
||||
fields: {
|
||||
HOUR: {
|
||||
options: [
|
||||
["12 midnight", "0"],
|
||||
["1am", "1"],
|
||||
["2am", "2"],
|
||||
["3am", "3"],
|
||||
["4am", "4"],
|
||||
["5am", "5"],
|
||||
["6am", "6"],
|
||||
["7am", "7"],
|
||||
["8am", "8"],
|
||||
["9am", "9"],
|
||||
["10am", "10"],
|
||||
["11am", "11"],
|
||||
["12 noon", "12"],
|
||||
["1pm", "13"],
|
||||
["2pm", "14"],
|
||||
["3pm", "15"],
|
||||
["4pm", "16"],
|
||||
["5pm", "17"],
|
||||
["6pm", "18"],
|
||||
["7pm", "19"],
|
||||
["8pm", "20"],
|
||||
["9pm", "21"],
|
||||
["10pm", "22"],
|
||||
["11pm", "23"],
|
||||
]
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const hour = block.getFieldValue('HOUR')
|
||||
|
||||
return JSON.stringify({
|
||||
everyDay: {
|
||||
schedule: `0 ${hour} * * *`
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: blockObject => {
|
||||
const
|
||||
{ schedule } = blockObject.everyDay,
|
||||
hour = schedule.split(' ')[1]
|
||||
|
||||
return {
|
||||
type: "every_day",
|
||||
fields: {
|
||||
HOUR: hour
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { map, range } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
type: "every_hour",
|
||||
|
||||
toolbox: {
|
||||
category: 'Triggers',
|
||||
label: "Runs the Action every hour at the specified minute."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
inputsInline: true,
|
||||
colour: 30,
|
||||
tooltip: "Run this action hourly at the minute specified."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "trigger",
|
||||
next: "trigger"
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "...%FREQUENCY an hour, at minute %MINUTE", {
|
||||
align: 'LEFT',
|
||||
fields: {
|
||||
FREQUENCY: {
|
||||
options: [
|
||||
["once", "once"],
|
||||
["twice", "twice"],
|
||||
]
|
||||
},
|
||||
MINUTE: {
|
||||
options: map(map(range(60), String), idx => ([ idx, idx ]))
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
frequency = block.getFieldValue('FREQUENCY'),
|
||||
minute = block.getFieldValue('MINUTE'),
|
||||
cronMinutes = frequency === 'once'
|
||||
? minute
|
||||
: `${minute%30}/30`
|
||||
|
||||
return JSON.stringify({
|
||||
everyHour: {
|
||||
schedule: `${cronMinutes} * * * *`
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: blockObject => {
|
||||
const
|
||||
{ schedule } = blockObject.everyHour,
|
||||
cronMinutes = schedule.split(" ")[0],
|
||||
minute = cronMinutes.split('/')[0],
|
||||
frequency = cronMinutes.includes('/')
|
||||
? 'twice'
|
||||
: 'once'
|
||||
|
||||
return {
|
||||
type: "every_hour",
|
||||
fields: {
|
||||
MINUTE: minute,
|
||||
FREQUENCY: frequency,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
app/blocks/triggers/on_schedule.js
Normal file
169
app/blocks/triggers/on_schedule.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
export default {
|
||||
type: "on_schedule",
|
||||
|
||||
toolbox: {
|
||||
label: "Run this Action on a set schedule."
|
||||
},
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "A schedule to run the action, from every minute to once a year."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "trigger",
|
||||
next: "trigger"
|
||||
},
|
||||
|
||||
lines: [
|
||||
[ "Schedule", "CENTER" ],
|
||||
|
||||
[ "Months:", {
|
||||
inputValue: "MONTH",
|
||||
check: "cron_month",
|
||||
block: "all_months",
|
||||
}],
|
||||
|
||||
[ "Days:", {
|
||||
inputValue: "DAY",
|
||||
check: "cron_day",
|
||||
block: "all_days"
|
||||
}],
|
||||
|
||||
[ "Hours:", {
|
||||
inputValue: "HOUR",
|
||||
check: "cron_hour",
|
||||
block: "all_hours"
|
||||
}],
|
||||
|
||||
[ "Minutes:", {
|
||||
inputValue: "MINUTE",
|
||||
check: "cron_minute",
|
||||
block: {
|
||||
type: "one_minute",
|
||||
fields: {
|
||||
MINUTE: '15'
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
// each schedule block generates a crontab for its unit
|
||||
const
|
||||
minute = generator.valueToCode(block, 'MINUTE', 0) || "*/1",
|
||||
hour = generator.valueToCode(block, 'HOUR', 0) || "*/1",
|
||||
daysOfMonth = generator.valueToCode(block, 'DAY', 0) || "*/1",
|
||||
daysOfWeek = "*",
|
||||
month = generator.valueToCode(block, 'MONTH', 0) || "*"
|
||||
|
||||
return JSON.stringify({
|
||||
onSchedule: {
|
||||
schedule: `${minute} ${hour} ${daysOfMonth} ${month} ${daysOfWeek}`
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const EVERY_REGEX = /^(\d{1,2})(-(\d{1,2}))?\/(\d{1,2})$/m
|
||||
|
||||
const
|
||||
monthCronToBlock = monthCron => {
|
||||
if(monthCron === '*') {
|
||||
return { block: { type: 'all_months' } }
|
||||
|
||||
} else if(/^\d*$/gm.test(monthCron)) {
|
||||
return { block: { type: 'one_month', fields: { MONTH: monthCron } } }
|
||||
|
||||
} else if(/^\w{3}(,\w{3})*$/gm.test(monthCron)) {
|
||||
const
|
||||
fieldNames = monthCron.toUpperCase().split(','),
|
||||
fields = fieldNames.reduce((acc, month) => {
|
||||
acc[month] = true
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return { block: { type: 'some_months', fields }}
|
||||
|
||||
} else {
|
||||
console.warn(`Bad crontab for months: ${monthCron}`)
|
||||
}
|
||||
},
|
||||
|
||||
dayCronToBlock = dayCron => {
|
||||
if(dayCron === '*') {
|
||||
return { block: { type: 'all_days' } }
|
||||
|
||||
} else if(/^\d*$/gm.test(dayCron)) {
|
||||
return { block: { type: 'one_day', fields: { DAY: dayCron } } }
|
||||
|
||||
} else {
|
||||
throw new Error(`Bad cron string for days: ${dayCron}`)
|
||||
}
|
||||
},
|
||||
|
||||
hourCronToBlock = hourCron => {
|
||||
if(hourCron === '*') {
|
||||
return { block: { type: 'all_hours' } }
|
||||
|
||||
} else if(/^\d*$/gm.test(hourCron)) {
|
||||
return { block: { type: 'one_hour', fields: { HOUR: hourCron } } }
|
||||
|
||||
} else {
|
||||
throw new Error(`Bad cron string for hours: ${hourCron}`)
|
||||
}
|
||||
},
|
||||
|
||||
minuteCronToBlock = minuteCron => {
|
||||
if(minuteCron === '*') {
|
||||
return { block: { type: 'all_minutes' } }
|
||||
|
||||
} else if(/^\d*$/gm.test(minuteCron)) {
|
||||
return { block: { type: 'one_minute', fields: { MINUTE: minuteCron } } }
|
||||
|
||||
} else if(EVERY_REGEX.test(minuteCron)) {
|
||||
const [ skip1, START, skip2, END, FREQUENCY ] = minuteCron.match(EVERY_REGEX)
|
||||
|
||||
return { block: {
|
||||
type: 'every_minutes_between',
|
||||
fields: { START, END, FREQUENCY }
|
||||
}}
|
||||
|
||||
} else {
|
||||
throw new Error(`Bad cron string for minutes: ${minuteCron}`)
|
||||
}
|
||||
}
|
||||
|
||||
const
|
||||
// { schedule } = blockObject.onSchedule,
|
||||
{ schedule } = Object.values(blockObject)[0],
|
||||
[ minute, hour, daysOfMonth, month, daysOfWeek ] = schedule.split(' ')
|
||||
|
||||
return {
|
||||
type: "on_schedule",
|
||||
inputs: {
|
||||
MONTH: {
|
||||
...monthCronToBlock(month),
|
||||
shadow: { type: "all_months" }
|
||||
},
|
||||
DAY: {
|
||||
...dayCronToBlock(daysOfMonth),
|
||||
shadow: { type: "all_days" }
|
||||
},
|
||||
HOUR: {
|
||||
...hourCronToBlock(hour),
|
||||
shadow: { type: "all_hours" }
|
||||
},
|
||||
MINUTE: {
|
||||
...minuteCronToBlock(minute),
|
||||
shadow: { type: "one_minute", fields: { 'MINUTE': '15' }}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
app/blocks/triggers/schedule/day/all_days.js
Normal file
27
app/blocks/triggers/schedule/day/all_days.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./day_mutator.js?key=${random}`)).default
|
||||
|
||||
export default {
|
||||
type: "all_days",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs during every day of the month."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_day'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [ "Every day" ],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/blocks/triggers/schedule/day/day_mutator.js
Normal file
9
app/blocks/triggers/schedule/day/day_mutator.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
makeScheduleMutator = (await import(`../schedule_mutator.js?key=${random}`)).default
|
||||
|
||||
export default makeScheduleMutator(
|
||||
[ 'all_days', 'one_day' ],
|
||||
'day_settings',
|
||||
'all_days'
|
||||
)
|
||||
22
app/blocks/triggers/schedule/day/day_settings.js
Normal file
22
app/blocks/triggers/schedule/day/day_settings.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export default {
|
||||
type: "day_settings",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "How would you like to specify days of the month for your schedule?"
|
||||
},
|
||||
|
||||
connections: {},
|
||||
|
||||
lines: [
|
||||
[ "Day:", {
|
||||
inputValue: 'DAY_BLOCK',
|
||||
check: 'cron_day',
|
||||
shadow: 'all_days'
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
}
|
||||
}
|
||||
40
app/blocks/triggers/schedule/day/one_day.js
Normal file
40
app/blocks/triggers/schedule/day/one_day.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./day_mutator.js?key=${random}`)).default
|
||||
|
||||
|
||||
export default {
|
||||
type: "one_day",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: [
|
||||
"Runs during a particular day of the month.",
|
||||
"Remember: not all months have days after 28!"
|
||||
].join("\n")
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_day'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
["On date:", {
|
||||
field: 'DAY',
|
||||
options: makeOptions({ from: 1, upTo: 31 })
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const day = block.getFieldValue('DAY')
|
||||
|
||||
return [ day, 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/blocks/triggers/schedule/hour/all_hours.js
Normal file
28
app/blocks/triggers/schedule/hour/all_hours.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./hour_mutator.js?key=${random}`)).default
|
||||
|
||||
|
||||
export default {
|
||||
type: "all_hours",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs during all hours of the day."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_hour'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [ "Every hour" ],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/blocks/triggers/schedule/hour/hour_mutator.js
Normal file
9
app/blocks/triggers/schedule/hour/hour_mutator.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
makeScheduleMutator = (await import(`../schedule_mutator.js?key=${random}`)).default
|
||||
|
||||
export default makeScheduleMutator(
|
||||
[ 'all_hours', 'one_hour' ],
|
||||
'hour_settings',
|
||||
'all_hours'
|
||||
)
|
||||
22
app/blocks/triggers/schedule/hour/hour_settings.js
Normal file
22
app/blocks/triggers/schedule/hour/hour_settings.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export default {
|
||||
type: "hour_settings",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "How would you like to specify hours of the day for your schedule?"
|
||||
},
|
||||
|
||||
connections: {},
|
||||
|
||||
lines: [
|
||||
[ "Hour:", {
|
||||
inputValue: 'HOUR_BLOCK',
|
||||
check: 'cron_hour',
|
||||
shadow: 'all_hours'
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
}
|
||||
}
|
||||
37
app/blocks/triggers/schedule/hour/one_hour.js
Normal file
37
app/blocks/triggers/schedule/hour/one_hour.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./hour_mutator.js?key=${random}`)).default
|
||||
|
||||
|
||||
export default {
|
||||
type: "one_hour",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs during a particular hour of the day."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_hour'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
["During hour:", {
|
||||
field: 'HOUR',
|
||||
options: makeOptions({ upTo: 24 })
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const hour = block.getFieldValue('HOUR')
|
||||
|
||||
return [ hour, 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/blocks/triggers/schedule/minute/all_minutes.js
Normal file
28
app/blocks/triggers/schedule/minute/all_minutes.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./minute_mutator.js?key=${random}`)).default
|
||||
|
||||
|
||||
export default {
|
||||
type: "all_minutes",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs every minute of the hour."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_minute'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [ "Every minute" ],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/blocks/triggers/schedule/minute/every_minutes_between.js
Normal file
45
app/blocks/triggers/schedule/minute/every_minutes_between.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./minute_mutator.js?key=${random}`)).default
|
||||
|
||||
export default {
|
||||
type: "every_minutes_between",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs every X minutes, between minutes Y and Z."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_minute'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
["Every %FREQUENCY minutes between %START and %END", {
|
||||
fields: {
|
||||
FREQUENCY: { options: makeOptions({ factorsOf: 60 }) },
|
||||
|
||||
START: { options: makeOptions({ upTo: 60 }) },
|
||||
|
||||
END: { options: makeOptions({ upTo: 60, reverse: true }) }
|
||||
}
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
frequency = block.getFieldValue('FREQUENCY'),
|
||||
start = block.getFieldValue('START'),
|
||||
end = block.getFieldValue('END'),
|
||||
cronString = `${start}-${end}/${frequency}`
|
||||
|
||||
return [ cronString, 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/blocks/triggers/schedule/minute/minute_mutator.js
Normal file
9
app/blocks/triggers/schedule/minute/minute_mutator.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
makeScheduleMutator = (await import(`../schedule_mutator.js?key=${random}`)).default
|
||||
|
||||
export default makeScheduleMutator(
|
||||
[ 'all_minutes', 'one_minute', 'every_minutes_between' ],
|
||||
'minute_settings',
|
||||
'all_minutes'
|
||||
)
|
||||
22
app/blocks/triggers/schedule/minute/minute_settings.js
Normal file
22
app/blocks/triggers/schedule/minute/minute_settings.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export default {
|
||||
type: "minute_settings",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "How would you like to specify minutes of the hour for your schedule?"
|
||||
},
|
||||
|
||||
connections: {},
|
||||
|
||||
lines: [
|
||||
[ "Minute:", {
|
||||
inputValue: 'MINUTE_BLOCK',
|
||||
check: 'cron_minute',
|
||||
shadow: 'all_minutes'
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
}
|
||||
}
|
||||
36
app/blocks/triggers/schedule/minute/one_minute.js
Normal file
36
app/blocks/triggers/schedule/minute/one_minute.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./minute_mutator.js?key=${random}`)).default
|
||||
|
||||
export default {
|
||||
type: "one_minute",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs at a particular minute of the hour."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_minute'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
["At minute:", {
|
||||
field: 'MINUTE',
|
||||
options: makeOptions({ upTo: 60 })
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const minute = block.getFieldValue('MINUTE')
|
||||
|
||||
return [ minute, 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
app/blocks/triggers/schedule/month/all_months.js
Normal file
27
app/blocks/triggers/schedule/month/all_months.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./month_mutator.js?key=${random}`)).default
|
||||
|
||||
export default {
|
||||
type: "all_months",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs during all months."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_month'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [ "Every month" ],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/blocks/triggers/schedule/month/month_mutator.js
Normal file
9
app/blocks/triggers/schedule/month/month_mutator.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
makeScheduleMutator = (await import(`../schedule_mutator.js?key=${random}`)).default
|
||||
|
||||
export default makeScheduleMutator(
|
||||
[ 'all_months', 'one_month', 'some_months', ],
|
||||
'month_settings',
|
||||
'all_months'
|
||||
)
|
||||
22
app/blocks/triggers/schedule/month/month_settings.js
Normal file
22
app/blocks/triggers/schedule/month/month_settings.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export default {
|
||||
type: "month_settings",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "How would you like to specify the months portion of your schedule?"
|
||||
},
|
||||
|
||||
connections: {},
|
||||
|
||||
lines: [
|
||||
[ "Month:", {
|
||||
inputValue: 'MONTH_BLOCK',
|
||||
check: 'cron_month',
|
||||
shadow: 'all_months'
|
||||
}],
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
}
|
||||
}
|
||||
47
app/blocks/triggers/schedule/month/one_month.js
Normal file
47
app/blocks/triggers/schedule/month/one_month.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./month_mutator.js?key=${random}`)).default
|
||||
|
||||
export default {
|
||||
type: "one_month",
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Runs during a particular month."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_month'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
["", {
|
||||
field: 'MONTH',
|
||||
options: [
|
||||
[ "January", '1' ],
|
||||
[ "February", '2' ],
|
||||
[ "March", '3' ],
|
||||
[ "April", '4' ],
|
||||
[ "May", '5' ],
|
||||
[ "June", '6' ],
|
||||
[ "July", '7' ],
|
||||
[ "August", '8' ],
|
||||
[ "September", '9' ],
|
||||
[ "October", '10' ],
|
||||
[ "November", '11' ],
|
||||
[ "December", '12' ],
|
||||
]
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const month = block.getFieldValue('MONTH')
|
||||
|
||||
return [ month, 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
66
app/blocks/triggers/schedule/month/some_months.js
Normal file
66
app/blocks/triggers/schedule/month/some_months.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
const
|
||||
random = Math.random()*100000000, // busts the NodeJS file cache
|
||||
mutator = (await import(`./month_mutator.js?key=${random}`)).default
|
||||
|
||||
export default {
|
||||
type: 'some_months',
|
||||
|
||||
visualization: {
|
||||
colour: 30,
|
||||
tooltip: "Run during particular months."
|
||||
},
|
||||
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: 'cron_month'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
lines: [
|
||||
[ 'Jan: %JAN Feb: %FEB Mar: %MAR Apr: %APR', {
|
||||
align: 'CENTER',
|
||||
fields: {
|
||||
JAN: { checked: false },
|
||||
FEB: { checked: false },
|
||||
MAR: { checked: false },
|
||||
APR: { checked: false }
|
||||
},
|
||||
}],
|
||||
|
||||
[ 'May: %MAY Jun: %JUN Jul: %JUL Aug: %AUG', {
|
||||
align: 'CENTER',
|
||||
fields: {
|
||||
MAY: { checked: false },
|
||||
JUN: { checked: false },
|
||||
JUL: { checked: false },
|
||||
AUG: { checked: false },
|
||||
}
|
||||
}],
|
||||
|
||||
[ 'Sep: %SEP Oct: %OCT Nov: %NOV Dec: %DEC', {
|
||||
align: 'CENTER',
|
||||
fields: {
|
||||
SEP: { checked: false },
|
||||
OCT: { checked: false },
|
||||
NOV: { checked: false },
|
||||
DEC: { checked: false },
|
||||
}
|
||||
}]
|
||||
],
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
MONTHS = [ 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC' ],
|
||||
selectedMonths = MONTHS.reduce((months, month) => (
|
||||
block.getFieldValue(month) === "TRUE"
|
||||
? months.concat(month)
|
||||
: months
|
||||
), []),
|
||||
months = selectedMonths.join(',').toLowerCase()
|
||||
|
||||
return [ months, 0 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
139
app/blocks/triggers/schedule/schedule_mutator.js
Normal file
139
app/blocks/triggers/schedule/schedule_mutator.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
const MUTATOR_BASE = {
|
||||
loadExtraState: function () { },
|
||||
|
||||
saveExtraState: function () { },
|
||||
|
||||
flyoutBlockTypes: [],
|
||||
|
||||
decompose: function (workspace) {
|
||||
// prepare the mutator root block as usual in decompose
|
||||
const settingsBlock = workspace.newBlock(this.rootBlock) // here
|
||||
settingsBlock.initSvg()
|
||||
|
||||
this.innerInputName = settingsBlock.inputList[0]?.name
|
||||
if(!this.innerInputName) {
|
||||
throw new Error("No inner input detected for self-mutating block mutator.")
|
||||
}
|
||||
|
||||
const
|
||||
defaultType = this.defaultType, // here
|
||||
outerUnitType = this.type || defaultType,
|
||||
// create inner versions
|
||||
innerUnitInput = settingsBlock.getInput(this.innerInputName), // here
|
||||
innerUnitConnection = innerUnitInput.connection,
|
||||
innerUnitBlock = workspace.newBlock(outerUnitType),
|
||||
{ outputConnection } = innerUnitBlock
|
||||
|
||||
// copy fields from outer block to inner block
|
||||
this.copyFields(this, innerUnitBlock)
|
||||
|
||||
// initialize the block for interaction
|
||||
innerUnitBlock.initSvg()
|
||||
// attach to schedule settings
|
||||
innerUnitConnection.connect(outputConnection)
|
||||
|
||||
// remove mutators from all the inner blocks
|
||||
const innerBlocks = workspace.getFlyout().getWorkspace().getTopBlocks() || []
|
||||
innerBlocks.forEach(block => block.setMutator(null))
|
||||
|
||||
// self-removing listener callback, applied to the outer workspace,
|
||||
// that calls attachOuterBlock when the mutator is closed
|
||||
const bubbleCloseCallback = e => {
|
||||
if(e.type !== Blockly.Events.BUBBLE_OPEN) { return }
|
||||
|
||||
// mutator is closing
|
||||
if(!e.isOpen) {
|
||||
// update outer block if inner present
|
||||
if(this.newBlockType) { this.attachOuterBlock(settingsBlock) }
|
||||
// remove ourself
|
||||
this.workspace.removeChangeListener(bubbleCloseCallback)
|
||||
}
|
||||
}
|
||||
this.workspace.addChangeListener(bubbleCloseCallback)
|
||||
|
||||
// when a new block is created in this mutator, remove mutators from those blocks too
|
||||
workspace.addChangeListener(e => {
|
||||
if(e.type !== Blockly.Events.BLOCK_CREATE) { return }
|
||||
if(e.blockId === settingsBlock.id) { return }
|
||||
|
||||
const block = workspace.getBlockById(e.blockId)
|
||||
|
||||
block?.setMutator(null)
|
||||
})
|
||||
|
||||
return settingsBlock
|
||||
},
|
||||
|
||||
// compose gets called frequently, despite what the docs say
|
||||
// as a result it needs to store the changes it wants to make
|
||||
// for later processing, instead of just doing them
|
||||
compose: function (settingsBlock) {
|
||||
const newBlock = settingsBlock.getInputTargetBlock(this.innerInputName) // here
|
||||
|
||||
if(newBlock) {
|
||||
this.newBlockType = newBlock.type
|
||||
this.storeFieldData(newBlock)
|
||||
} else {
|
||||
this.newBlockType = null
|
||||
this.fieldData = {}
|
||||
}
|
||||
},
|
||||
|
||||
// replace ourself with the new block and its data
|
||||
attachOuterBlock: function(settingsBlock) {
|
||||
const
|
||||
outerUnitConnection = this.outputConnection.targetConnection,
|
||||
outerUnitBlock = this.workspace.newBlock(this.newBlockType)
|
||||
|
||||
// populate new block with data stored from the inner block
|
||||
this.restoreFieldData(outerUnitBlock)
|
||||
|
||||
// disconnect ourself
|
||||
outerUnitConnection.disconnect()
|
||||
// connect our new block and intialize its graphics
|
||||
outerUnitConnection.connect(outerUnitBlock.outputConnection)
|
||||
outerUnitBlock.initSvg()
|
||||
outerUnitBlock.render()
|
||||
|
||||
// be done with ourself
|
||||
this.dispose()
|
||||
},
|
||||
|
||||
// field data helpers
|
||||
getFieldNames: function(block) {
|
||||
return block.inputList.reduce((names, input) =>
|
||||
names.concat(input.fieldRow.map(field => field.name).filter(name => name))
|
||||
, [])
|
||||
},
|
||||
|
||||
copyFields: function(fromBlock, toBlock) {
|
||||
const fieldNames = this.getFieldNames(fromBlock)
|
||||
|
||||
// get from source, set on destination
|
||||
fieldNames.forEach(fieldName => {
|
||||
toBlock.setFieldValue(fromBlock.getFieldValue(fieldName), fieldName)
|
||||
})
|
||||
},
|
||||
|
||||
storeFieldData: function(fromBlock) {
|
||||
const fieldNames = this.getFieldNames(fromBlock)
|
||||
|
||||
this.fieldData = fieldNames.reduce((data, name) => {
|
||||
data[name] = fromBlock.getFieldValue(name)
|
||||
return data
|
||||
}, {})
|
||||
},
|
||||
|
||||
restoreFieldData: function(toBlock) {
|
||||
for (const [fieldName, fieldValue] of Object.entries(this.fieldData)) {
|
||||
toBlock.setFieldValue(fieldValue, fieldName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default (flyoutBlockTypes, rootBlock, defaultType) => {
|
||||
return {
|
||||
...MUTATOR_BASE,
|
||||
flyoutBlockTypes, rootBlock, defaultType
|
||||
}
|
||||
}
|
||||
6
app/extensions/auto_disable.js
Normal file
6
app/extensions/auto_disable.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// makes disabled blocks
|
||||
export const autoDisable = ({ block }) => {
|
||||
block.disabled = true
|
||||
}
|
||||
|
||||
export default autoDisable
|
||||
|
|
@ -7,10 +7,9 @@ export default {
|
|||
"Actions cannot fire more than once in a 5 minute period."
|
||||
],
|
||||
contents: [
|
||||
'on_schedule',
|
||||
'when_data',
|
||||
'when_data_matching',
|
||||
'when_data_matching_state',
|
||||
'every_hour',
|
||||
'every_day',
|
||||
]
|
||||
}
|
||||
|
|
|
|||
56
app/util/fields.js
Normal file
56
app/util/fields.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { filter, identity, map, range, reverse } from 'lodash-es'
|
||||
|
||||
|
||||
const makeFactorsOf = target => {
|
||||
return filter(range(target), idx => (target % idx) == 0)
|
||||
}
|
||||
|
||||
const makeUpTo = (from, target, step) => {
|
||||
return range(from, target, step)
|
||||
}
|
||||
|
||||
const stringifyOptions = (options) => {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {number} [options.factorsOf]
|
||||
* @param {number} [options.upTo]
|
||||
* @param {number} [options.from]
|
||||
* @param {number} [options.step]
|
||||
* @param {boolean} [options.reverse]
|
||||
* @param {Function} [options.valueFunc]
|
||||
* @returns {Array}
|
||||
*/
|
||||
export const makeOptions = (options = {}) => {
|
||||
let optionValues
|
||||
|
||||
if(options.factorsOf) {
|
||||
optionValues = makeFactorsOf(options.factorsOf)
|
||||
|
||||
} else if(options.upTo) {
|
||||
const
|
||||
from = options.from || 0,
|
||||
step = options.step || 1
|
||||
|
||||
optionValues = makeUpTo(from, options.upTo, step)
|
||||
}
|
||||
|
||||
if(!optionValues) {
|
||||
throw new Error(`No valid options sent to makeOptions: ${options}`)
|
||||
}
|
||||
|
||||
if(options.reverse) {
|
||||
optionValues = reverse(optionValues)
|
||||
}
|
||||
|
||||
const
|
||||
valueFunc = options.valueFunc || identity,
|
||||
mappedValues = map(optionValues, labelValue =>
|
||||
map([ labelValue, valueFunc(labelValue) ], String)
|
||||
)
|
||||
|
||||
return mappedValues
|
||||
}
|
||||
|
|
@ -11,9 +11,14 @@
|
|||
<div id="page-container">
|
||||
<div id="menu-pane">
|
||||
<button id="button-clear">Clear</button>
|
||||
|
||||
<button id="button-reload-bytecode">Reload via Bytecode</button>
|
||||
<button id="button-reload-serialized">Reload via LocalStorage</button>
|
||||
|
||||
<div id="stats-row">
|
||||
<span>Top Blocks: <span id="top-blocks">0</span></span>
|
||||
<span>Blocks: <span id="total-blocks">0</span></span>
|
||||
<span title="Regular:Flyout:Mutator">Workspaces: <span id="total-workspaces">0:0:0</span></span>
|
||||
</div>
|
||||
<pre id="bytecode-json-container"><code id="bytecode-json"></code></pre>
|
||||
<pre id="blockly-json-container"><code id="blockly-json"></code></pre>
|
||||
</div>
|
||||
|
|
|
|||
12
jsconfig.json
Normal file
12
jsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"module": "nodenext",
|
||||
"target": "es2022",
|
||||
"paths": {
|
||||
"#app/*.js": ["./app/*.js"],
|
||||
"#src/*.js": ["./src/*.js"],
|
||||
"#test/*.js": ["./test/*.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
73
package-lock.json
generated
73
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.2.0",
|
||||
"chai": "^5.1.2",
|
||||
"eslint": "^8.57.0",
|
||||
"esm-reload": "^1.0.1",
|
||||
"glob": "^10.4.2",
|
||||
|
|
@ -761,6 +762,15 @@
|
|||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
|
@ -807,6 +817,22 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
|
||||
"integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
|
@ -823,6 +849,15 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -897,11 +932,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
|
|
@ -917,6 +952,15 @@
|
|||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
|
||||
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
|
|
@ -1693,6 +1737,12 @@
|
|||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
|
||||
"integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz",
|
||||
|
|
@ -1743,9 +1793,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
|
|
@ -1904,6 +1954,15 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
||||
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -3,12 +3,18 @@
|
|||
"version": "1.0.0",
|
||||
"description": "A Blockly tooling project",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#app/*.js": "./app/*.js",
|
||||
"#src/*.js": "./src/*.js",
|
||||
"#test/*.js": "./test/*.js"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "vite --force",
|
||||
"test": "node --test",
|
||||
"test-watch": "node --test --watch",
|
||||
"lint": "eslint src/",
|
||||
"lint-export": "eslint export/",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "npm run export && vite build",
|
||||
"build-all-branches": "node build_all_branches.js",
|
||||
"preview": "npm run build && vite preview",
|
||||
|
|
@ -21,6 +27,7 @@
|
|||
"license": "",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.2.0",
|
||||
"chai": "^5.1.2",
|
||||
"eslint": "^8.57.0",
|
||||
"esm-reload": "^1.0.1",
|
||||
"glob": "^10.4.2",
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const
|
|||
if(!definition) {
|
||||
throw new Error(`No Block definition found at path: ${BLOCK_LOCATION}${path}`)
|
||||
}
|
||||
// console.log("Processing Block:", path)//, definition)
|
||||
|
||||
// ensure only supported keys are present
|
||||
const extraKeys = without(keys(definition), ...BLOCK_KEYS)
|
||||
|
||||
|
|
@ -58,7 +58,13 @@ const
|
|||
|
||||
if(definition.disabled) { return }
|
||||
|
||||
// TODO: locate block defaults
|
||||
// TODO: mechanism for Definition JSON defaults
|
||||
if(definition.connections?.mode === 'value') {
|
||||
// default input values with no output to 'expression'
|
||||
definition.connections.output = definition.connections.output || 'expression'
|
||||
}
|
||||
|
||||
// TODO: mechanism for Blockly JSON defaults
|
||||
const blockDefaults = {
|
||||
inputsInline: false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const processLine = (line) => {
|
|||
check: lineData.check
|
||||
})
|
||||
|
||||
// append inputStatement
|
||||
// append inputValue
|
||||
} else if(lineData.inputValue) {
|
||||
args.push({
|
||||
type: "input_value",
|
||||
|
|
@ -58,7 +58,7 @@ const processLine = (line) => {
|
|||
check: lineData.check
|
||||
})
|
||||
|
||||
// append a field to args
|
||||
// append a field to args
|
||||
} else if(lineData.field) {
|
||||
args.push({
|
||||
name: lineData.field,
|
||||
|
|
@ -149,7 +149,7 @@ const parseArrayLine = line => {
|
|||
}
|
||||
|
||||
if(isObject(second)) {
|
||||
const extraKeys = without(keys(second), "align", "inputDummy", "inputValue", "inputStatement", "check", "field", "fields", "type", "text", "multiline_text", "options", "shadow", "checked")
|
||||
const extraKeys = without(keys(second), "align", "inputDummy", "inputValue", "inputStatement", "check", "block", "field", "fields", "type", "text", "multiline_text", "options", "shadow", "checked")
|
||||
if(extraKeys.length) {
|
||||
throw new Error(`Unrecognized keys (${extraKeys.join(', ')}) for block line with text: "${text}"`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,30 @@
|
|||
import { isString, isFunction, map, mapValues, pickBy } from 'lodash-es'
|
||||
import { forOwn, isString, isFunction, isArray, isNumber, isNull, isObject, map, mapValues, pickBy, isRegExp } from 'lodash-es'
|
||||
import { importBlockDefinitions } from './block_importer.js'
|
||||
import renderTemplate from './template_renderer.js'
|
||||
|
||||
|
||||
const renderValue = value => {
|
||||
if(isString(value)) {
|
||||
if (isString(value)) {
|
||||
return `"${value}"`
|
||||
} else if(isFunction) {
|
||||
return value.toString().replaceAll("\n", "\n ")
|
||||
} else {
|
||||
|
||||
} else if (isRegExp(value) || isNull(value) || isNumber(value) || value === false) {
|
||||
return value
|
||||
|
||||
} else if (isFunction(value)) {
|
||||
return value.toString().replaceAll("\n", "\n ")
|
||||
|
||||
} else if (isArray(value)) {
|
||||
return `[ ${value.map(renderValue).join(", ")} ]`
|
||||
|
||||
} else if (isObject(value)) {
|
||||
const lines = []
|
||||
forOwn(value, (val, key) => {
|
||||
lines.push(`"${key}": ${renderValue(val)}`)
|
||||
})
|
||||
return `{ ${lines.join(",\n")} }`
|
||||
|
||||
} else {
|
||||
throw new Error(`Unexpected value type: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,5 @@ const allBlockMutators = {}
|
|||
/* <<-LOCAL */
|
||||
|
||||
for (const [blockName, mutatorObject] of Object.entries(allBlockMutators)) {
|
||||
if(mutatorObject.helperFunction) {
|
||||
Blockly.Extensions.registerMutator(blockName, mutatorObject, mutatorObject.helperFunction)
|
||||
} else {
|
||||
Blockly.Extensions.registerMutator(blockName, mutatorObject)
|
||||
}
|
||||
Blockly.Extensions.registerMutator(blockName, mutatorObject, mutatorObject.helperFunction, mutatorObject.flyoutBlockTypes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ const blockRegenerators = {}
|
|||
/* <<-LOCAL */
|
||||
|
||||
const BYTECODE_BLOCK_TYPE_MAP = {
|
||||
everyHour: 'every_hour',
|
||||
everyDay: 'every_day',
|
||||
onSchedule: 'on_schedule',
|
||||
everyMonth: 'on_schedule',
|
||||
everyDay: 'on_schedule',
|
||||
everyHour: 'on_schedule',
|
||||
everyMinute: 'on_schedule',
|
||||
whenData: 'when_data',
|
||||
whenDataMatching: 'when_data_matching',
|
||||
whenDataMatchStateChanged: 'when_data_matching_state',
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ const
|
|||
map(lines, '[1]'),
|
||||
"inputValue"),
|
||||
"inputValue"),
|
||||
shadowPropertyToInput)
|
||||
definitionPropsToInputs)
|
||||
|
||||
return isEmpty(inputs) ? undefined : inputs
|
||||
},
|
||||
|
|
@ -197,10 +197,34 @@ const
|
|||
return isEmpty(fields) ? undefined : fields
|
||||
},
|
||||
|
||||
shadowPropertyToInput = ({ shadow }) =>
|
||||
isString(shadow) // is shorthand?
|
||||
? { shadow: { type: shadow }} // expand to full object
|
||||
: { shadow } // set as shadow value
|
||||
definitionPropsToInputs = ({ inputValue, block, shadow }) => {
|
||||
if(!block && !shadow) {
|
||||
console.warn("Warning: no block or shadow specified for", inputValue)
|
||||
return
|
||||
}
|
||||
|
||||
if(block) {
|
||||
const
|
||||
blockJson = blockToInput(block),
|
||||
shadowJson = shadowToInput(shadow || block)
|
||||
|
||||
return {
|
||||
...blockJson,
|
||||
...shadowJson
|
||||
}
|
||||
|
||||
} else if(shadow) {
|
||||
return shadowToInput(shadow)
|
||||
}
|
||||
},
|
||||
|
||||
blockToInput = block => isString(block) // is shorthand?
|
||||
? { block: { type: block }} // expand to full object
|
||||
: { block }, // set as shadow value
|
||||
|
||||
shadowToInput = shadow => isString(shadow) // is shorthand?
|
||||
? { shadow: { type: shadow }} // expand to full object
|
||||
: { shadow } // set as shadow value
|
||||
|
||||
export default importToolbox
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ export default function ImportUserAppPlugin() {
|
|||
},
|
||||
|
||||
handleHotUpdate(ctx) {
|
||||
if (!ctx.file.includes('/app/') && !ctx.file.includes('/blockly_api.js')) {
|
||||
if(ctx.file.includes("/test/")) { return }
|
||||
|
||||
if(!ctx.file.includes('/app/') && !ctx.file.includes('/blockly_api.js')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
32
src/index.js
32
src/index.js
|
|
@ -1,4 +1,5 @@
|
|||
import Blockly from 'blockly'
|
||||
import { filter } from 'lodash-es'
|
||||
|
||||
import initialWorkspace from '../export/workspace.json'
|
||||
import { inject, jsonToWorkspace, workspaceToJson } from '../export/blockly.js'
|
||||
|
|
@ -9,10 +10,16 @@ import './index.css'
|
|||
|
||||
// wire up the internal json to the dom
|
||||
const
|
||||
topBlocksDiv = document.getElementById('top-blocks'),
|
||||
totalBlocksDiv = document.getElementById('total-blocks'),
|
||||
totalWorkspacesDiv = document.getElementById('total-workspaces'),
|
||||
blocklyJsonOutputDiv = document.getElementById('blockly-json'),
|
||||
bytecodeJsonOutputDiv = document.getElementById('bytecode-json'),
|
||||
|
||||
onJsonUpdated = bytecodeJson => {
|
||||
topBlocksDiv.innerText = workspace.getTopBlocks().length
|
||||
totalBlocksDiv.innerText = workspace.getAllBlocks().length
|
||||
|
||||
blocklyJsonOutputDiv.innerText = ``
|
||||
bytecodeJsonOutputDiv.innerText = `Bytecode is valid JSON ✅\n\n${bytecodeJson}`
|
||||
|
||||
|
|
@ -20,11 +27,14 @@ const
|
|||
const workspaceJson = JSON.stringify(jsonToWorkspace(bytecodeJson), null, 2)
|
||||
blocklyJsonOutputDiv.innerText = `Workspace JSON\n\n${workspaceJson}`
|
||||
} catch(error) {
|
||||
console.error("Workspace JSON Error:\n", error)
|
||||
console.error("JSON:\n", bytecodeJson)
|
||||
blocklyJsonOutputDiv.innerText = `Workspace JSON generation failed ❌\nYou may need the "Clear" button above.`
|
||||
}
|
||||
},
|
||||
|
||||
onJsonError = error => {
|
||||
console.error("Bytecode JSON Error:\n", error)
|
||||
bytecodeJsonOutputDiv.innerText = `Bytecode generation failed ❌\nYou may need the "Clear" button above.`
|
||||
blocklyJsonOutputDiv.innerText = `Workspace JSON not generated.`
|
||||
}
|
||||
|
|
@ -48,6 +58,17 @@ const workspace = inject('blocklyDiv', {
|
|||
|
||||
// register listeners
|
||||
|
||||
setInterval(() => {
|
||||
const
|
||||
workspaces = Blockly.Workspace.getAll(),
|
||||
total = workspaces.length,
|
||||
mutators = filter(workspaces, 'isMutator').length,
|
||||
flyouts = filter(workspaces, 'isFlyout').length,
|
||||
rest = total - mutators - flyouts
|
||||
|
||||
totalWorkspacesDiv.innerText = `${rest}:${flyouts}:${mutators}`
|
||||
|
||||
}, 1000)
|
||||
// auto-save on non-UI changes
|
||||
workspace.addChangeListener((e) => e.isUiEvent || save(workspace))
|
||||
|
||||
|
|
@ -64,8 +85,15 @@ const reloadBytecodeButton = document.getElementById('button-reload-bytecode')
|
|||
reloadBytecodeButton.addEventListener('click', () => {
|
||||
// export bytecode
|
||||
const bytecodeJson = workspaceToJson(workspace)
|
||||
// convert bytecode to workspace json
|
||||
const workspaceJson = jsonToWorkspace(bytecodeJson)
|
||||
|
||||
let workspaceJson
|
||||
try{
|
||||
// convert bytecode to workspace json
|
||||
workspaceJson = jsonToWorkspace(bytecodeJson)
|
||||
|
||||
} catch(e) {
|
||||
console.error("Failed to parse bytecode:", bytecodeJson)
|
||||
}
|
||||
|
||||
// disable events while we're working
|
||||
Blockly.Events.disable()
|
||||
|
|
|
|||
56
test/app/util/fields_test.js
Normal file
56
test/app/util/fields_test.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it } from 'node:test'
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
|
||||
describe("Fields utils", () => {
|
||||
describe("makeOptions", () => {
|
||||
it("blows up with no valid options", () => {
|
||||
assert.throws(() => makeOptions())
|
||||
})
|
||||
})
|
||||
|
||||
describe("makeOptions.upTo", () => {
|
||||
it("uses from: 0 and step: 1 if options not given", () => {
|
||||
assert.deepEqual(makeOptions({ upTo: 2 }), [["0", "0"], ["1", "1"]])
|
||||
})
|
||||
|
||||
it("uses 'from' option if given", () => {
|
||||
assert.deepEqual(makeOptions({ from: 1, upTo: 2 }), [["1", "1"]])
|
||||
})
|
||||
|
||||
it("uses 'step' option if given", () => {
|
||||
assert.deepEqual(makeOptions({ upTo: 21, step: 10 }), [
|
||||
["0", "0"], ["10", "10"], ["20", "20"]
|
||||
])
|
||||
})
|
||||
|
||||
it("uses 'valueFunc' option if given", () => {
|
||||
assert.deepEqual(makeOptions({ upTo: 3, valueFunc: (v) => v*5 }),
|
||||
[["0", "0"], ["1", "5"], ["2", "10"]]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("makeOptions.factorsOf", () => {
|
||||
it("makes options with factors", () => {
|
||||
assert.deepEqual(makeOptions({ factorsOf: 2 }), [["1", "1"]])
|
||||
assert.deepEqual(makeOptions({ factorsOf: 24 }),
|
||||
[
|
||||
[ '1', '1' ], [ '2', '2' ], [ '3', '3' ],
|
||||
[ '4', '4' ], [ '6', '6' ], [ '8', '8' ],
|
||||
[ '12', '12' ]
|
||||
]
|
||||
)
|
||||
assert.deepEqual(makeOptions({ factorsOf: 60 }),
|
||||
[
|
||||
[ '1', '1' ], [ '2', '2' ], [ '3', '3' ],
|
||||
[ '4', '4' ], [ '5', '5' ], [ '6', '6' ],
|
||||
[ '10', '10' ], [ '12', '12' ], [ '15', '15' ],
|
||||
[ '20', '20' ], [ '30', '30' ]
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue