|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 105 KiB |
@ -0,0 +1,95 @@ |
|||||
|
import mustache from "mustache" |
||||
|
import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions" |
||||
|
import { generate } from "shortid" |
||||
|
|
||||
|
/** |
||||
|
* Class responsible for the traversing of the workflow definition. |
||||
|
* Workflow definitions are stored in linked lists. |
||||
|
*/ |
||||
|
export default class Workflow { |
||||
|
constructor(workflow) { |
||||
|
this.workflow = workflow |
||||
|
} |
||||
|
|
||||
|
hasTrigger() { |
||||
|
return this.workflow.definition.trigger |
||||
|
} |
||||
|
|
||||
|
addBlock(block) { |
||||
|
// Make sure to add trigger if doesn't exist
|
||||
|
if (!this.hasTrigger() && block.type === "TRIGGER") { |
||||
|
this.workflow.definition.trigger = { id: generate(), ...block } |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.workflow.definition.steps.push({ |
||||
|
id: generate(), |
||||
|
...block, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
updateBlock(updatedBlock, id) { |
||||
|
const { steps, trigger } = this.workflow.definition |
||||
|
|
||||
|
if (trigger && trigger.id === id) { |
||||
|
this.workflow.definition.trigger = null |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const stepIdx = steps.findIndex(step => step.id === id) |
||||
|
if (stepIdx < 0) throw new Error("Block not found.") |
||||
|
steps.splice(stepIdx, 1, updatedBlock) |
||||
|
} |
||||
|
|
||||
|
deleteBlock(id) { |
||||
|
const { steps, trigger } = this.workflow.definition |
||||
|
|
||||
|
if (trigger && trigger.id === id) { |
||||
|
this.workflow.definition.trigger = null |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const stepIdx = steps.findIndex(step => step.id === id) |
||||
|
if (stepIdx < 0) throw new Error("Block not found.") |
||||
|
steps.splice(stepIdx, 1) |
||||
|
} |
||||
|
|
||||
|
createUiTree() { |
||||
|
if (!this.workflow.definition) return [] |
||||
|
return Workflow.buildUiTree(this.workflow.definition) |
||||
|
} |
||||
|
|
||||
|
static buildUiTree(definition) { |
||||
|
const steps = [] |
||||
|
if (definition.trigger) steps.push(definition.trigger) |
||||
|
|
||||
|
return [...steps, ...definition.steps].map(step => { |
||||
|
// The client side display definition for the block
|
||||
|
const definition = blockDefinitions[step.type][step.actionId] |
||||
|
if (!definition) { |
||||
|
throw new Error( |
||||
|
`No block definition exists for the chosen block. Check there's an entry in the block definitions for ${step.actionId}` |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (!definition.params) { |
||||
|
throw new Error( |
||||
|
`Blocks should always have parameters. Ensure that the block definition is correct for ${step.actionId}` |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const tagline = definition.tagline || "" |
||||
|
const args = step.args || {} |
||||
|
|
||||
|
return { |
||||
|
id: step.id, |
||||
|
type: step.type, |
||||
|
params: step.params, |
||||
|
args, |
||||
|
heading: step.actionId, |
||||
|
body: mustache.render(tagline, args), |
||||
|
name: definition.name, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
import api from "../../api" |
||||
|
import Workflow from "./Workflow" |
||||
|
|
||||
|
const workflowActions = store => ({ |
||||
|
fetch: async instanceId => { |
||||
|
const WORKFLOWS_URL = `/api/${instanceId}/workflows` |
||||
|
const workflowResponse = await api.get(WORKFLOWS_URL) |
||||
|
const json = await workflowResponse.json() |
||||
|
store.update(state => { |
||||
|
state.workflows = json |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
create: async ({ instanceId, name }) => { |
||||
|
const workflow = { |
||||
|
name, |
||||
|
definition: { |
||||
|
steps: [], |
||||
|
}, |
||||
|
} |
||||
|
const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows` |
||||
|
const response = await api.post(CREATE_WORKFLOW_URL, workflow) |
||||
|
const json = await response.json() |
||||
|
store.update(state => { |
||||
|
state.workflows = state.workflows.concat(json.workflow) |
||||
|
state.currentWorkflow = new Workflow(json.workflow) |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
save: async ({ instanceId, workflow }) => { |
||||
|
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` |
||||
|
const response = await api.put(UPDATE_WORKFLOW_URL, workflow) |
||||
|
const json = await response.json() |
||||
|
store.update(state => { |
||||
|
const existingIdx = state.workflows.findIndex( |
||||
|
existing => existing._id === workflow._id |
||||
|
) |
||||
|
state.workflows.splice(existingIdx, 1, json.workflow) |
||||
|
state.workflows = state.workflows |
||||
|
state.currentWorkflow = new Workflow(json.workflow) |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
update: async ({ instanceId, workflow }) => { |
||||
|
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` |
||||
|
const response = await api.put(UPDATE_WORKFLOW_URL, workflow) |
||||
|
const json = await response.json() |
||||
|
store.update(state => { |
||||
|
const existingIdx = state.workflows.findIndex( |
||||
|
existing => existing._id === workflow._id |
||||
|
) |
||||
|
state.workflows.splice(existingIdx, 1, json.workflow) |
||||
|
state.workflows = state.workflows |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
delete: async ({ instanceId, workflow }) => { |
||||
|
const { _id, _rev } = workflow |
||||
|
const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}` |
||||
|
await api.delete(DELETE_WORKFLOW_URL) |
||||
|
|
||||
|
store.update(state => { |
||||
|
const existingIdx = state.workflows.findIndex( |
||||
|
existing => existing._id === _id |
||||
|
) |
||||
|
state.workflows.splice(existingIdx, 1) |
||||
|
state.workflows = state.workflows |
||||
|
state.currentWorkflow = null |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
select: workflow => { |
||||
|
store.update(state => { |
||||
|
state.currentWorkflow = new Workflow(workflow) |
||||
|
state.selectedWorkflowBlock = null |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
addBlockToWorkflow: block => { |
||||
|
store.update(state => { |
||||
|
state.currentWorkflow.addBlock(block) |
||||
|
state.selectedWorkflowBlock = block |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
deleteWorkflowBlock: block => { |
||||
|
store.update(state => { |
||||
|
state.currentWorkflow.deleteBlock(block.id) |
||||
|
state.selectedWorkflowBlock = null |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
export const getWorkflowStore = () => { |
||||
|
const INITIAL_WORKFLOW_STATE = { |
||||
|
workflows: [], |
||||
|
} |
||||
|
|
||||
|
const store = writable(INITIAL_WORKFLOW_STATE) |
||||
|
|
||||
|
store.actions = workflowActions(store) |
||||
|
|
||||
|
return store |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
import Workflow from "../Workflow"; |
||||
|
import TEST_WORKFLOW from "./testWorkflow"; |
||||
|
|
||||
|
const TEST_BLOCK = { |
||||
|
id: "VFWeZcIPx", |
||||
|
name: "Update UI State", |
||||
|
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>", |
||||
|
icon: "ri-refresh-line", |
||||
|
description: "Update your User Interface with some data.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
path: "string", |
||||
|
value: "longText", |
||||
|
}, |
||||
|
args: { |
||||
|
path: "foo", |
||||
|
value: "started...", |
||||
|
}, |
||||
|
actionId: "SET_STATE", |
||||
|
type: "ACTION", |
||||
|
} |
||||
|
|
||||
|
describe("Workflow Data Object", () => { |
||||
|
let workflow |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
workflow = new Workflow({ ...TEST_WORKFLOW }); |
||||
|
}); |
||||
|
|
||||
|
it("adds a workflow block to the workflow", () => { |
||||
|
workflow.addBlock(TEST_BLOCK); |
||||
|
expect(workflow.workflow.definition) |
||||
|
}) |
||||
|
|
||||
|
it("updates a workflow block with new attributes", () => { |
||||
|
const firstBlock = workflow.workflow.definition.steps[0]; |
||||
|
const updatedBlock = { |
||||
|
...firstBlock, |
||||
|
name: "UPDATED" |
||||
|
}; |
||||
|
workflow.updateBlock(updatedBlock, firstBlock.id); |
||||
|
expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock) |
||||
|
}) |
||||
|
|
||||
|
it("deletes a workflow block successfully", () => { |
||||
|
const { steps } = workflow.workflow.definition |
||||
|
const originalLength = steps.length |
||||
|
|
||||
|
const lastBlock = steps[steps.length - 1]; |
||||
|
workflow.deleteBlock(lastBlock.id); |
||||
|
expect(workflow.workflow.definition.steps.length).toBeLessThan(originalLength); |
||||
|
}) |
||||
|
|
||||
|
it("builds a tree that gets rendered in the flowchart builder", () => { |
||||
|
expect(Workflow.buildUiTree(TEST_WORKFLOW.definition)).toMatchSnapshot(); |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,49 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`Workflow Data Object builds a tree that gets rendered in the flowchart builder 1`] = ` |
||||
|
Array [ |
||||
|
Object { |
||||
|
"args": Object { |
||||
|
"time": 3000, |
||||
|
}, |
||||
|
"body": "Delay for <b>3000</b> milliseconds", |
||||
|
"heading": "DELAY", |
||||
|
"id": "zJQcZUgDS", |
||||
|
"name": "Delay", |
||||
|
"params": Object { |
||||
|
"time": "number", |
||||
|
}, |
||||
|
"type": "LOGIC", |
||||
|
}, |
||||
|
Object { |
||||
|
"args": Object { |
||||
|
"path": "foo", |
||||
|
"value": "finished", |
||||
|
}, |
||||
|
"body": "Update <b>foo</b> to <b>finished</b>", |
||||
|
"heading": "SET_STATE", |
||||
|
"id": "3RSTO7BMB", |
||||
|
"name": "Update UI State", |
||||
|
"params": Object { |
||||
|
"path": "string", |
||||
|
"value": "longText", |
||||
|
}, |
||||
|
"type": "ACTION", |
||||
|
}, |
||||
|
Object { |
||||
|
"args": Object { |
||||
|
"path": "foo", |
||||
|
"value": "started...", |
||||
|
}, |
||||
|
"body": "Update <b>foo</b> to <b>started...</b>", |
||||
|
"heading": "SET_STATE", |
||||
|
"id": "VFWeZcIPx", |
||||
|
"name": "Update UI State", |
||||
|
"params": Object { |
||||
|
"path": "string", |
||||
|
"value": "longText", |
||||
|
}, |
||||
|
"type": "ACTION", |
||||
|
}, |
||||
|
] |
||||
|
`; |
||||
@ -0,0 +1,63 @@ |
|||||
|
export default { |
||||
|
_id: "53b6148c65d1429c987e046852d11611", |
||||
|
_rev: "4-02c6659734934895812fa7be0215ee59", |
||||
|
name: "Test Workflow", |
||||
|
definition: { |
||||
|
steps: [ |
||||
|
{ |
||||
|
id: "VFWeZcIPx", |
||||
|
name: "Update UI State", |
||||
|
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>", |
||||
|
icon: "ri-refresh-line", |
||||
|
description: "Update your User Interface with some data.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
path: "string", |
||||
|
value: "longText", |
||||
|
}, |
||||
|
args: { |
||||
|
path: "foo", |
||||
|
value: "started...", |
||||
|
}, |
||||
|
actionId: "SET_STATE", |
||||
|
type: "ACTION", |
||||
|
}, |
||||
|
{ |
||||
|
id: "zJQcZUgDS", |
||||
|
name: "Delay", |
||||
|
icon: "ri-time-fill", |
||||
|
tagline: "Delay for <b>{{time}}</b> milliseconds", |
||||
|
description: "Delay the workflow until an amount of time has passed.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
time: "number", |
||||
|
}, |
||||
|
args: { |
||||
|
time: 3000, |
||||
|
}, |
||||
|
actionId: "DELAY", |
||||
|
type: "LOGIC", |
||||
|
}, |
||||
|
{ |
||||
|
id: "3RSTO7BMB", |
||||
|
name: "Update UI State", |
||||
|
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>", |
||||
|
icon: "ri-refresh-line", |
||||
|
description: "Update your User Interface with some data.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
path: "string", |
||||
|
value: "longText", |
||||
|
}, |
||||
|
args: { |
||||
|
path: "foo", |
||||
|
value: "finished", |
||||
|
}, |
||||
|
actionId: "SET_STATE", |
||||
|
type: "ACTION", |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
type: "workflow", |
||||
|
live: true, |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps" |
||||
|
import api from "./api" |
||||
|
import { generate_screen_css } from "./generate_css" |
||||
|
|
||||
|
export const selectComponent = (state, component) => { |
||||
|
const componentDef = component._component.startsWith("##") |
||||
|
? component |
||||
|
: state.components[component._component] |
||||
|
state.currentComponentInfo = makePropsSafe(componentDef, component) |
||||
|
state.currentView = "component" |
||||
|
return state |
||||
|
} |
||||
|
|
||||
|
export const getParent = (rootProps, child) => { |
||||
|
let parent |
||||
|
walkProps(rootProps, (p, breakWalk) => { |
||||
|
if ( |
||||
|
p._children && |
||||
|
(p._children.includes(child) || p._children.some(c => c._id === child)) |
||||
|
) { |
||||
|
parent = p |
||||
|
breakWalk() |
||||
|
} |
||||
|
}) |
||||
|
return parent |
||||
|
} |
||||
|
|
||||
|
export const saveCurrentPreviewItem = s => |
||||
|
s.currentFrontEndType === "page" |
||||
|
? savePage(s) |
||||
|
: saveScreenApi(s.currentPreviewItem, s) |
||||
|
|
||||
|
export const savePage = async s => { |
||||
|
const page = s.pages[s.currentPageName] |
||||
|
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, { |
||||
|
page: { componentLibraries: s.pages.componentLibraries, ...page }, |
||||
|
uiFunctions: s.currentPageFunctions, |
||||
|
screens: page._screens, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export const saveScreenApi = (screen, s) => { |
||||
|
api |
||||
|
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen) |
||||
|
.then(() => savePage(s)) |
||||
|
} |
||||
|
|
||||
|
export const renameCurrentScreen = (newname, state) => { |
||||
|
const oldname = state.currentPreviewItem.name |
||||
|
state.currentPreviewItem.name = newname |
||||
|
api.patch( |
||||
|
`/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`, |
||||
|
{ |
||||
|
oldname, |
||||
|
newname, |
||||
|
} |
||||
|
) |
||||
|
return state |
||||
|
} |
||||
|
|
||||
|
export const walkProps = (props, action, cancelToken = null) => { |
||||
|
cancelToken = cancelToken || { cancelled: false } |
||||
|
action(props, () => { |
||||
|
cancelToken.cancelled = true |
||||
|
}) |
||||
|
|
||||
|
if (props._children) { |
||||
|
for (let child of props._children) { |
||||
|
if (cancelToken.cancelled) return |
||||
|
walkProps(child, action, cancelToken) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const regenerateCssForCurrentScreen = state => { |
||||
|
state.currentPreviewItem._css = generate_screen_css([ |
||||
|
state.currentPreviewItem.props, |
||||
|
]) |
||||
|
return state |
||||
|
} |
||||
|
After Width: | Height: | Size: 419 B |
@ -1,30 +1,61 @@ |
|||||
<script> |
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
import { buildStyle } from "../../helpers.js" |
||||
export let value = "" |
export let value = "" |
||||
export let width = "" |
export let textAlign = "left" |
||||
|
export let width = "160px" |
||||
|
export let placeholder = "" |
||||
|
export let suffix = "" |
||||
|
export let onChange = val => {} |
||||
|
|
||||
let style = { width } |
let centerPlaceholder = textAlign === "center" |
||||
|
|
||||
|
let style = buildStyle({ width, textAlign }) |
||||
|
|
||||
|
function handleChange(val) { |
||||
|
value = val |
||||
|
let _value = value !== "auto" ? value + suffix : value |
||||
|
onChange(_value) |
||||
|
} |
||||
|
|
||||
|
$: displayValue = |
||||
|
suffix && value && value.endsWith(suffix) |
||||
|
? value.replace(new RegExp(`${suffix}$`), "") |
||||
|
: value || "" |
||||
</script> |
</script> |
||||
|
|
||||
<input type="text" style={`width: ${width};`} on:change bind:value /> |
<input |
||||
|
class:centerPlaceholder |
||||
|
type="text" |
||||
|
value={displayValue} |
||||
|
{placeholder} |
||||
|
{style} |
||||
|
on:change={e => handleChange(e.target.value)} /> |
||||
|
|
||||
<style> |
<style> |
||||
input { |
input { |
||||
display: block; |
/* width: 32px; */ |
||||
font-size: 14px; |
height: 32px; |
||||
font-weight: 500; |
font-size: 12px; |
||||
|
font-weight: 700; |
||||
|
margin: 0px 0px 0px 1px; |
||||
color: var(--ink); |
color: var(--ink); |
||||
|
opacity: 0.7; |
||||
|
padding: 0px 4px; |
||||
line-height: 1.3; |
line-height: 1.3; |
||||
padding: 12px; |
/* padding: 12px; */ |
||||
width: 164px; |
width: 164px; |
||||
float: right; |
|
||||
max-width: 100%; |
|
||||
box-sizing: border-box; |
box-sizing: border-box; |
||||
margin: 0; |
border: 1px solid var(--grey); |
||||
-moz-appearance: none; |
border-radius: 2px; |
||||
-webkit-appearance: none; |
outline: none; |
||||
appearance: none; |
} |
||||
background: #fff; |
|
||||
border: 1px solid var(--grey-dark); |
input::placeholder { |
||||
height: 32px; |
text-align: left; |
||||
|
} |
||||
|
|
||||
|
.centerPlaceholder::placeholder { |
||||
|
text-align: center; |
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
@ -1,74 +1,50 @@ |
|||||
<script> |
<script> |
||||
import { onMount } from "svelte" |
import { onMount } from "svelte" |
||||
|
import Input from "../Input.svelte" |
||||
|
|
||||
export let meta = [] |
export let meta = [] |
||||
export let label = "" |
export let label = "" |
||||
export let value = [0, 0, 0, 0] |
export let value = ["0", "0", "0", "0"] |
||||
export let type = "number" |
export let suffix = "" |
||||
|
|
||||
export let onChange = () => {} |
export let onChange = () => {} |
||||
|
|
||||
function handleChange(val, idx) { |
function handleChange(val, idx) { |
||||
value.splice(idx, 1, val) |
value.splice(idx, 1, val !== "auto" && suffix ? val + suffix : val) |
||||
|
|
||||
value = value |
value = value |
||||
onChange(value) |
let _value = value.map(v => |
||||
|
suffix && !v.endsWith(suffix) && v !== "auto" ? v + suffix : v |
||||
|
) |
||||
|
onChange(_value) |
||||
} |
} |
||||
|
|
||||
|
$: displayValues = |
||||
|
value && suffix |
||||
|
? value.map(v => v.replace(new RegExp(`${suffix}$`), "")) |
||||
|
: value || [] |
||||
</script> |
</script> |
||||
|
|
||||
<div class="input-container"> |
<div class="input-container"> |
||||
<div class="label">{label}</div> |
<div class="label">{label}</div> |
||||
<div class="inputs"> |
<div class="inputs-group"> |
||||
{#each meta as { placeholder }, i} |
{#each meta as m, i} |
||||
<input |
<Input |
||||
{type} |
width="37px" |
||||
placeholder={placeholder || ''} |
textAlign="center" |
||||
value={!value || value[i] === 0 ? '' : value[i]} |
placeholder={m.placeholder || ''} |
||||
on:change={e => handleChange(e.target.value || 0, i)} /> |
value={!displayValues || displayValues[i] === '0' ? '' : displayValues[i]} |
||||
|
onChange={value => handleChange(value || 0, i)} /> |
||||
{/each} |
{/each} |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
|
|
||||
<style> |
<style> |
||||
.input-container { |
|
||||
} |
|
||||
|
|
||||
.label { |
.label { |
||||
flex: 0; |
flex: 0; |
||||
} |
} |
||||
|
|
||||
.inputs { |
.inputs-group { |
||||
flex: 1; |
flex: 1; |
||||
} |
} |
||||
|
|
||||
input { |
|
||||
width: 40px; |
|
||||
height: 32px; |
|
||||
font-size: 12px; |
|
||||
font-weight: 700; |
|
||||
margin: 0px 0px 0px 1px; |
|
||||
text-align: center; |
|
||||
color: var(--ink); |
|
||||
opacity: 0.7; |
|
||||
padding: 0px 4px; |
|
||||
box-sizing: border-box; |
|
||||
border: 1px solid var(--grey); |
|
||||
border-radius: 2px; |
|
||||
outline: none; |
|
||||
float: right; |
|
||||
} |
|
||||
|
|
||||
input[type="number"]::-webkit-inner-spin-button, |
|
||||
input[type="number"]::-webkit-outer-spin-button { |
|
||||
-webkit-appearance: none; |
|
||||
-moz-appearance: none; |
|
||||
appearance: none; |
|
||||
margin: 0; |
|
||||
} |
|
||||
|
|
||||
input[type="number"] { |
|
||||
-moz-appearance: textfield; |
|
||||
} |
|
||||
|
|
||||
input::placeholder { |
|
||||
text-align: center; |
|
||||
} |
|
||||
</style> |
</style> |
||||
|
|||||
@ -0,0 +1,78 @@ |
|||||
|
<script> |
||||
|
export let tabs = [] |
||||
|
export const selectTab = tabName => { |
||||
|
selected = tabName |
||||
|
selectedIndex = tabs.indexOf(selected) |
||||
|
} |
||||
|
|
||||
|
let selected = tabs.length > 0 && tabs[0] |
||||
|
let selectedIndex = 0 |
||||
|
|
||||
|
const isSelected = tab => selected === tab |
||||
|
</script> |
||||
|
|
||||
|
<div class="root"> |
||||
|
|
||||
|
<div class="switcher"> |
||||
|
|
||||
|
{#each tabs as tab} |
||||
|
<button class:selected={selected === tab} on:click={() => selectTab(tab)}> |
||||
|
{tab} |
||||
|
</button> |
||||
|
{/each} |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<div class="panel"> |
||||
|
{#if selectedIndex === 0} |
||||
|
<slot name="0" /> |
||||
|
{:else if selectedIndex === 1} |
||||
|
<slot name="1" /> |
||||
|
{:else if selectedIndex === 2} |
||||
|
<slot name="2" /> |
||||
|
{:else if selectedIndex === 3} |
||||
|
<slot name="3" /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.root { |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 20px 20px; |
||||
|
border-left: solid 1px var(--grey); |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.switcher { |
||||
|
display: flex; |
||||
|
margin: 0px 20px 20px 0px; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.switcher > button { |
||||
|
display: inline-block; |
||||
|
border: none; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
cursor: pointer; |
||||
|
font-size: 18px; |
||||
|
font-weight: 700; |
||||
|
color: var(--ink-lighter); |
||||
|
margin-right: 20px; |
||||
|
background: none; |
||||
|
} |
||||
|
|
||||
|
.switcher > .selected { |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
.panel { |
||||
|
min-height: 0; |
||||
|
height: 100%; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
</style> |
||||
@ -1,31 +0,0 @@ |
|||||
import { isString } from "lodash/fp" |
|
||||
|
|
||||
import { |
|
||||
BB_STATE_BINDINGPATH, |
|
||||
BB_STATE_FALLBACK, |
|
||||
BB_STATE_BINDINGSOURCE, |
|
||||
isBound, |
|
||||
parseBinding, |
|
||||
} from "@budibase/client/src/state/parseBinding" |
|
||||
|
|
||||
export const isBinding = isBound |
|
||||
|
|
||||
export const setBinding = ({ path, fallback, source }, binding = {}) => { |
|
||||
if (isNonEmptyString(path)) binding[BB_STATE_BINDINGPATH] = path |
|
||||
if (isNonEmptyString(fallback)) binding[BB_STATE_FALLBACK] = fallback |
|
||||
binding[BB_STATE_BINDINGSOURCE] = source || "store" |
|
||||
return binding |
|
||||
} |
|
||||
|
|
||||
export const getBinding = val => { |
|
||||
const binding = parseBinding(val) |
|
||||
return binding |
|
||||
? binding |
|
||||
: { |
|
||||
path: "", |
|
||||
source: "store", |
|
||||
fallback: "", |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const isNonEmptyString = s => isString(s) && s.length > 0 |
|
||||
@ -0,0 +1,277 @@ |
|||||
|
<script> |
||||
|
import { MoreIcon } from "components/common/Icons" |
||||
|
import { store } from "builderStore" |
||||
|
import { getComponentDefinition } from "builderStore/store" |
||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
||||
|
import { last, cloneDeep } from "lodash/fp" |
||||
|
import UIkit from "uikit" |
||||
|
import { |
||||
|
selectComponent, |
||||
|
getParent, |
||||
|
walkProps, |
||||
|
saveCurrentPreviewItem, |
||||
|
regenerateCssForCurrentScreen, |
||||
|
} from "builderStore/storeUtils" |
||||
|
import { uuid } from "builderStore/uuid" |
||||
|
|
||||
|
export let component |
||||
|
|
||||
|
let confirmDeleteDialog |
||||
|
let dropdownEl |
||||
|
|
||||
|
$: dropdown = UIkit.dropdown(dropdownEl, { |
||||
|
mode: "click", |
||||
|
offset: 0, |
||||
|
pos: "bottom-right", |
||||
|
"delay-hide": 0, |
||||
|
animation: false, |
||||
|
}) |
||||
|
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false)) |
||||
|
$: noChildrenAllowed = |
||||
|
!component || |
||||
|
getComponentDefinition($store, component._component).children === false |
||||
|
$: noPaste = !$store.componentToPaste |
||||
|
|
||||
|
const lastPartOfName = c => (c ? last(c._component.split("/")) : "") |
||||
|
|
||||
|
const hideDropdown = () => { |
||||
|
dropdown.hide() |
||||
|
} |
||||
|
|
||||
|
const moveUpComponent = () => { |
||||
|
store.update(s => { |
||||
|
const parent = getParent(s.currentPreviewItem.props, component) |
||||
|
|
||||
|
if (parent) { |
||||
|
const currentIndex = parent._children.indexOf(component) |
||||
|
if (currentIndex === 0) return s |
||||
|
|
||||
|
const newChildren = parent._children.filter(c => c !== component) |
||||
|
newChildren.splice(currentIndex - 1, 0, component) |
||||
|
parent._children = newChildren |
||||
|
} |
||||
|
s.currentComponentInfo = component |
||||
|
saveCurrentPreviewItem(s) |
||||
|
|
||||
|
return s |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const moveDownComponent = () => { |
||||
|
store.update(s => { |
||||
|
const parent = getParent(s.currentPreviewItem.props, component) |
||||
|
|
||||
|
if (parent) { |
||||
|
const currentIndex = parent._children.indexOf(component) |
||||
|
if (currentIndex === parent._children.length - 1) return s |
||||
|
|
||||
|
const newChildren = parent._children.filter(c => c !== component) |
||||
|
newChildren.splice(currentIndex + 1, 0, component) |
||||
|
parent._children = newChildren |
||||
|
} |
||||
|
s.currentComponentInfo = component |
||||
|
saveCurrentPreviewItem(s) |
||||
|
|
||||
|
return s |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const copyComponent = () => { |
||||
|
store.update(s => { |
||||
|
const parent = getParent(s.currentPreviewItem.props, component) |
||||
|
const copiedComponent = cloneDeep(component) |
||||
|
walkProps(copiedComponent, p => { |
||||
|
p._id = uuid() |
||||
|
}) |
||||
|
parent._children = [...parent._children, copiedComponent] |
||||
|
saveCurrentPreviewItem(s) |
||||
|
s.currentComponentInfo = copiedComponent |
||||
|
regenerateCssForCurrentScreen(s) |
||||
|
return s |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const deleteComponent = () => { |
||||
|
store.update(state => { |
||||
|
const parent = getParent(state.currentPreviewItem.props, component) |
||||
|
|
||||
|
if (parent) { |
||||
|
parent._children = parent._children.filter(c => c !== component) |
||||
|
} |
||||
|
|
||||
|
saveCurrentPreviewItem(state) |
||||
|
|
||||
|
return state |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const generateNewIdsForComponent = c => |
||||
|
walkProps(c, p => { |
||||
|
p._id = uuid() |
||||
|
}) |
||||
|
|
||||
|
const storeComponentForCopy = (cut = false) => { |
||||
|
store.update(s => { |
||||
|
const copiedComponent = cloneDeep(component) |
||||
|
s.componentToPaste = copiedComponent |
||||
|
if (cut) { |
||||
|
const parent = getParent(s.currentPreviewItem.props, component._id) |
||||
|
parent._children = parent._children.filter(c => c._id !== component._id) |
||||
|
selectComponent(s, parent) |
||||
|
} |
||||
|
|
||||
|
return s |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const pasteComponent = mode => { |
||||
|
store.update(s => { |
||||
|
if (!s.componentToPaste) return s |
||||
|
|
||||
|
const componentToPaste = cloneDeep(s.componentToPaste) |
||||
|
generateNewIdsForComponent(componentToPaste) |
||||
|
delete componentToPaste._cutId |
||||
|
|
||||
|
if (mode === "inside") { |
||||
|
component._children.push(componentToPaste) |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
const parent = getParent(s.currentPreviewItem.props, component) |
||||
|
|
||||
|
const targetIndex = parent._children.indexOf(component) |
||||
|
const index = mode === "above" ? targetIndex : targetIndex + 1 |
||||
|
parent._children.splice(index, 0, cloneDeep(componentToPaste)) |
||||
|
regenerateCssForCurrentScreen(s) |
||||
|
saveCurrentPreviewItem(s) |
||||
|
selectComponent(s, componentToPaste) |
||||
|
|
||||
|
return s |
||||
|
}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="root boundary" on:click|stopPropagation={() => {}}> |
||||
|
<button> |
||||
|
<MoreIcon /> |
||||
|
</button> |
||||
|
<ul class="menu" bind:this={dropdownEl} on:click={hideDropdown}> |
||||
|
<li class="item" on:click={() => confirmDeleteDialog.show()}> |
||||
|
<i class="icon ri-delete-bin-2-line" /> |
||||
|
Delete |
||||
|
</li> |
||||
|
<li class="item" on:click={moveUpComponent}> |
||||
|
<i class="icon ri-arrow-up-line" /> |
||||
|
Move up |
||||
|
</li> |
||||
|
<li class="item" on:click={moveDownComponent}> |
||||
|
<i class="icon ri-arrow-down-line" /> |
||||
|
Move down |
||||
|
</li> |
||||
|
<li class="item" on:click={copyComponent}> |
||||
|
<i class="icon ri-repeat-one-line" /> |
||||
|
Duplicate |
||||
|
</li> |
||||
|
<li class="item" on:click={() => storeComponentForCopy(true)}> |
||||
|
<i class="icon ri-scissors-cut-line" /> |
||||
|
Cut |
||||
|
</li> |
||||
|
<li class="item" on:click={() => storeComponentForCopy(false)}> |
||||
|
<i class="icon ri-file-copy-line" /> |
||||
|
Copy |
||||
|
</li> |
||||
|
<hr class="hr-style" /> |
||||
|
<li |
||||
|
class="item" |
||||
|
class:disabled={noPaste} |
||||
|
on:click={() => pasteComponent('above')}> |
||||
|
<i class="icon ri-insert-row-top" /> |
||||
|
Paste above |
||||
|
</li> |
||||
|
<li |
||||
|
class="item" |
||||
|
class:disabled={noPaste} |
||||
|
on:click={() => pasteComponent('below')}> |
||||
|
<i class="icon ri-insert-row-bottom" /> |
||||
|
Paste below |
||||
|
</li> |
||||
|
<li |
||||
|
class="item" |
||||
|
class:disabled={noPaste || noChildrenAllowed} |
||||
|
on:click={() => pasteComponent('inside')}> |
||||
|
<i class="icon ri-insert-column-right" /> |
||||
|
Paste inside |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<ConfirmDialog |
||||
|
bind:this={confirmDeleteDialog} |
||||
|
title="Confirm Delete" |
||||
|
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`} |
||||
|
okText="Delete Component" |
||||
|
onOk={deleteComponent} /> |
||||
|
|
||||
|
<style> |
||||
|
.root { |
||||
|
overflow: hidden; |
||||
|
z-index: 9; |
||||
|
} |
||||
|
|
||||
|
.root button { |
||||
|
border-style: none; |
||||
|
border-radius: 2px; |
||||
|
padding: 5px; |
||||
|
background: transparent; |
||||
|
cursor: pointer; |
||||
|
color: var(--ink); |
||||
|
outline: none; |
||||
|
} |
||||
|
|
||||
|
.menu { |
||||
|
z-index: 100000; |
||||
|
overflow: visible; |
||||
|
padding: 12px 0px; |
||||
|
border-radius: 5px; |
||||
|
} |
||||
|
|
||||
|
.menu li { |
||||
|
border-style: none; |
||||
|
background-color: transparent; |
||||
|
list-style-type: none; |
||||
|
padding: 4px 16px; |
||||
|
margin: 0; |
||||
|
width: 100%; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.menu li:not(.disabled) { |
||||
|
cursor: pointer; |
||||
|
color: var(--ink-light); |
||||
|
} |
||||
|
|
||||
|
.menu li:not(.disabled):hover { |
||||
|
color: var(--ink); |
||||
|
background-color: var(--grey-light); |
||||
|
} |
||||
|
|
||||
|
.disabled { |
||||
|
color: var(--grey-dark); |
||||
|
cursor: default; |
||||
|
} |
||||
|
|
||||
|
.hr-style { |
||||
|
margin: 8px 0; |
||||
|
color: var(--grey-dark); |
||||
|
} |
||||
|
</style> |
||||
@ -1,73 +1,52 @@ |
|||||
<script> |
<script> |
||||
|
import { Input } from "@budibase/bbui" |
||||
import IconButton from "components/common/IconButton.svelte" |
import IconButton from "components/common/IconButton.svelte" |
||||
import PlusButton from "components/common/PlusButton.svelte" |
import PlusButton from "components/common/PlusButton.svelte" |
||||
import Select from "components/common/Select.svelte" |
import Select from "components/common/Select.svelte" |
||||
import Input from "components/common/Input.svelte" |
|
||||
import { find, map, keys, reduce, keyBy } from "lodash/fp" |
import { find, map, keys, reduce, keyBy } from "lodash/fp" |
||||
import { pipe } from "components/common/core" |
import { pipe } from "components/common/core" |
||||
import { |
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers" |
||||
EVENT_TYPE_MEMBER_NAME, |
import { store, workflowStore } from "builderStore" |
||||
allHandlers, |
|
||||
} from "components/common/eventHandlers" |
|
||||
import { store } from "builderStore" |
|
||||
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte" |
|
||||
import { ArrowDownIcon } from "components/common/Icons/" |
import { ArrowDownIcon } from "components/common/Icons/" |
||||
|
|
||||
export let parameter |
export let parameter |
||||
export let onChange |
|
||||
|
|
||||
let isOpen = false |
let isOpen = false |
||||
|
|
||||
|
const capitalize = s => { |
||||
|
if (typeof s !== "string") return "" |
||||
|
return s.charAt(0).toUpperCase() + s.slice(1) |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
<div class="handler-option"> |
<div class="handler-option"> |
||||
<span>{parameter.name}</span> |
{#if parameter.name === 'workflow'} |
||||
<div class="handler-input"> |
<span>{parameter.name}</span> |
||||
<Input on:change={onChange} value={parameter.value} /> |
{/if} |
||||
<button on:click={() => (isOpen = !isOpen)}> |
{#if parameter.name === 'workflow'} |
||||
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}> |
<Select on:change bind:value={parameter.value}> |
||||
<ArrowDownIcon size={36} /> |
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow} |
||||
</div> |
<option value={workflow._id}>{workflow.name}</option> |
||||
</button> |
{/each} |
||||
{#if isOpen} |
</Select> |
||||
<StateBindingOptions |
{:else} |
||||
onSelect={option => { |
<Input |
||||
onChange(option) |
name={parameter.name} |
||||
isOpen = false |
label={capitalize(parameter.name)} |
||||
}} /> |
on:change |
||||
{/if} |
value={parameter.value} /> |
||||
</div> |
{/if} |
||||
</div> |
</div> |
||||
|
|
||||
<style> |
<style> |
||||
button { |
|
||||
cursor: pointer; |
|
||||
outline: none; |
|
||||
border: none; |
|
||||
border-radius: 5px; |
|
||||
background: rgba(249, 249, 249, 1); |
|
||||
|
|
||||
font-size: 1.6rem; |
|
||||
font-weight: 700; |
|
||||
color: rgba(22, 48, 87, 1); |
|
||||
margin-left: 5px; |
|
||||
} |
|
||||
|
|
||||
.icon { |
|
||||
width: 24px; |
|
||||
} |
|
||||
|
|
||||
.handler-option { |
.handler-option { |
||||
display: flex; |
display: flex; |
||||
flex-direction: column; |
flex-direction: column; |
||||
} |
} |
||||
|
|
||||
.handler-input { |
|
||||
position: relative; |
|
||||
display: flex; |
|
||||
} |
|
||||
|
|
||||
span { |
span { |
||||
font-size: 13px; |
font-size: 18px; |
||||
margin-bottom: 5px; |
margin-bottom: 10px; |
||||
|
font-weight: 500; |
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
@ -0,0 +1,57 @@ |
|||||
|
<script> |
||||
|
import { store, backendUiStore } from "builderStore" |
||||
|
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte" |
||||
|
import PageLayout from "components/userInterface/PageLayout.svelte" |
||||
|
import PagesList from "components/userInterface/PagesList.svelte" |
||||
|
import NewScreen from "components/userInterface/NewScreen.svelte" |
||||
|
|
||||
|
const newScreen = () => { |
||||
|
newScreenPicker.show() |
||||
|
} |
||||
|
|
||||
|
let newScreenPicker |
||||
|
</script> |
||||
|
|
||||
|
<PagesList /> |
||||
|
|
||||
|
<button class="newscreen" on:click={newScreen}> |
||||
|
<i class="icon ri-add-circle-fill" /> |
||||
|
Create New Screen |
||||
|
</button> |
||||
|
|
||||
|
<PageLayout layout={$store.pages[$store.currentPageName]} /> |
||||
|
|
||||
|
<div class="nav-items-container"> |
||||
|
<ComponentsHierarchy screens={$store.screens} /> |
||||
|
</div> |
||||
|
|
||||
|
<NewScreen bind:this={newScreenPicker} /> |
||||
|
|
||||
|
<style> |
||||
|
.newscreen { |
||||
|
cursor: pointer; |
||||
|
border: 1px solid var(--grey-dark); |
||||
|
border-radius: 3px; |
||||
|
width: 100%; |
||||
|
padding: 8px 16px; |
||||
|
margin: 20px 0px 12px 0px; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
background: white; |
||||
|
color: var(--ink); |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
transition: all 2ms; |
||||
|
} |
||||
|
|
||||
|
.newscreen:hover { |
||||
|
background: var(--grey-light); |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
color: var(--ink); |
||||
|
font-size: 16px; |
||||
|
margin-right: 4px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,16 @@ |
|||||
|
<script> |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
|
||||
|
export let value |
||||
|
</script> |
||||
|
|
||||
|
<div class="uk-margin block-field"> |
||||
|
<div class="uk-form-controls"> |
||||
|
<select class="budibase__input" on:change {value}> |
||||
|
<option value="" /> |
||||
|
{#each $backendUiStore.models as model} |
||||
|
<option value={model._id}>{model.name}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
@ -1,36 +1,223 @@ |
|||||
<script> |
<script> |
||||
import { onMount } from "svelte" |
import { onMount, beforeUpdate } from "svelte" |
||||
|
import { buildStyle } from "../../helpers.js" |
||||
|
export let options = [] |
||||
export let value = "" |
export let value = "" |
||||
|
export let styleBindingProperty |
||||
export let onChange = value => {} |
export let onChange = value => {} |
||||
export let options = [] |
|
||||
export let initialValue = "" |
let open = null |
||||
export let styleBindingProperty = "" |
let rotate = "" |
||||
|
let select |
||||
|
let selectMenu |
||||
|
let icon |
||||
|
|
||||
|
let selectYPosition = null |
||||
|
let availableSpace = 0 |
||||
|
|
||||
|
let positionSide = "top" |
||||
|
let maxHeight = null |
||||
|
let menuHeight |
||||
|
|
||||
const handleStyleBind = value => |
const handleStyleBind = value => |
||||
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {} |
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {} |
||||
|
|
||||
$: isOptionsObject = options.every(o => typeof o === "object") |
|
||||
|
|
||||
onMount(() => { |
onMount(() => { |
||||
if (!value && !!initialValue) { |
if (select) { |
||||
value = initialValue |
select.addEventListener("keydown", addSelectKeyEvents) |
||||
|
} |
||||
|
return () => { |
||||
|
select.removeEventListener("keydown", addSelectKeyEvents) |
||||
} |
} |
||||
}) |
}) |
||||
|
|
||||
|
function checkPosition() { |
||||
|
const { bottom, top: spaceAbove } = select.getBoundingClientRect() |
||||
|
const spaceBelow = window.innerHeight - bottom |
||||
|
|
||||
|
if (spaceAbove > spaceBelow) { |
||||
|
positionSide = "bottom" |
||||
|
maxHeight = `${spaceAbove.toFixed(0) - 20}px` |
||||
|
} else { |
||||
|
positionSide = "top" |
||||
|
maxHeight = `${spaceBelow.toFixed(0) - 20}px` |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function addSelectKeyEvents(e) { |
||||
|
if (e.key === "Enter") { |
||||
|
if (!open) { |
||||
|
toggleSelect(true) |
||||
|
} |
||||
|
} else if (e.key === "Escape") { |
||||
|
if (open) { |
||||
|
toggleSelect(false) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toggleSelect(isOpen) { |
||||
|
checkPosition() |
||||
|
if (isOpen) { |
||||
|
icon.style.transform = "rotate(180deg)" |
||||
|
} else { |
||||
|
icon.style.transform = "rotate(0deg)" |
||||
|
} |
||||
|
open = isOpen |
||||
|
} |
||||
|
|
||||
|
function handleClick(val) { |
||||
|
value = val |
||||
|
onChange(value) |
||||
|
} |
||||
|
|
||||
|
$: menuStyle = buildStyle({ |
||||
|
"max-height": maxHeight, |
||||
|
"transform-origin": `center ${positionSide}`, |
||||
|
[positionSide]: "32px", |
||||
|
}) |
||||
|
|
||||
|
$: isOptionsObject = options.every(o => typeof o === "object") |
||||
|
|
||||
|
$: selectedOption = isOptionsObject |
||||
|
? options.find(o => o.value === value) |
||||
|
: {} |
||||
|
|
||||
|
$: displayLabel = |
||||
|
selectedOption && selectedOption.label ? selectedOption.label : value || "" |
||||
</script> |
</script> |
||||
|
|
||||
<select |
<div |
||||
class="uk-select uk-form-small" |
tabindex="0" |
||||
{value} |
bind:this={select} |
||||
on:change={ev => onChange(ev.target.value)}> |
class="bb-select-container" |
||||
{#if isOptionsObject} |
on:click={() => toggleSelect(!open)}> |
||||
{#each options as { value, label }} |
<div class="bb-select-anchor selected"> |
||||
<option {...handleStyleBind(value || label)} value={value || label}> |
<span>{displayLabel}</span> |
||||
{label} |
<i bind:this={icon} class="ri-arrow-down-s-fill" /> |
||||
</option> |
</div> |
||||
{/each} |
<div |
||||
{:else} |
bind:this={selectMenu} |
||||
{#each options as value} |
style={menuStyle} |
||||
<option {...handleStyleBind(value)} {value}>{value}</option> |
class="bb-select-menu" |
||||
{/each} |
class:open> |
||||
{/if} |
<ul> |
||||
</select> |
{#if isOptionsObject} |
||||
|
{#each options as { value: v, label }} |
||||
|
<li |
||||
|
{...handleStyleBind(v)} |
||||
|
on:click|self={handleClick(v)} |
||||
|
class:selected={value === v}> |
||||
|
{label} |
||||
|
</li> |
||||
|
{/each} |
||||
|
{:else} |
||||
|
{#each options as v} |
||||
|
<li |
||||
|
{...handleStyleBind(v)} |
||||
|
on:click|self={handleClick(v)} |
||||
|
class:selected={value === v}> |
||||
|
{v} |
||||
|
</li> |
||||
|
{/each} |
||||
|
{/if} |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
{#if open} |
||||
|
<div on:click|self={() => toggleSelect(false)} class="overlay" /> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
.overlay { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
bottom: 0; |
||||
|
right: 0; |
||||
|
left: 0; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
.bb-select-container { |
||||
|
position: relative; |
||||
|
outline: none; |
||||
|
width: 160px; |
||||
|
height: 32px; |
||||
|
cursor: pointer; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.bb-select-anchor { |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
padding: 5px 10px; |
||||
|
background-color: #f2f2f2; |
||||
|
border-radius: 2px; |
||||
|
border: 1px solid var(--grey-dark); |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.bb-select-anchor > span { |
||||
|
color: #565a66; |
||||
|
font-weight: 500; |
||||
|
width: 140px; |
||||
|
overflow-x: hidden; |
||||
|
} |
||||
|
|
||||
|
.bb-select-anchor > i { |
||||
|
transition: transform 0.13s ease; |
||||
|
transform-origin: center; |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.selected { |
||||
|
color: #565a66; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.bb-select-menu { |
||||
|
position: absolute; |
||||
|
display: flex; |
||||
|
box-sizing: border-box; |
||||
|
flex-direction: column; |
||||
|
opacity: 0; |
||||
|
width: 160px; |
||||
|
z-index: 2; |
||||
|
color: #808192; |
||||
|
font-weight: 500; |
||||
|
height: fit-content !important; |
||||
|
border-bottom-left-radius: 2px; |
||||
|
border-bottom-right-radius: 2px; |
||||
|
border-right: 1px solid var(--grey-dark); |
||||
|
border-left: 1px solid var(--grey-dark); |
||||
|
border-bottom: 1px solid var(--grey-dark); |
||||
|
background-color: #f2f2f2; |
||||
|
transform: scale(0); |
||||
|
transition: opacity 0.13s linear, transform 0.12s cubic-bezier(0, 0, 0.2, 1); |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.open { |
||||
|
transform: scale(1); |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
list-style-type: none; |
||||
|
margin: 0; |
||||
|
padding: 5px 0px; |
||||
|
} |
||||
|
|
||||
|
li { |
||||
|
height: auto; |
||||
|
padding: 5px 0px; |
||||
|
cursor: pointer; |
||||
|
padding-left: 10px; |
||||
|
} |
||||
|
|
||||
|
li:hover { |
||||
|
background-color: #e6e6e6; |
||||
|
} |
||||
|
</style> |
||||
|
|||||
@ -0,0 +1,33 @@ |
|||||
|
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
export let value = "" |
||||
|
export let onChange = value => {} |
||||
|
export let options = [] |
||||
|
export let initialValue = "" |
||||
|
export let styleBindingProperty = "" |
||||
|
|
||||
|
const handleStyleBind = value => |
||||
|
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {} |
||||
|
|
||||
|
$: isOptionsObject = options.every(o => typeof o === "object") |
||||
|
|
||||
|
onMount(() => { |
||||
|
if (!value && !!initialValue) { |
||||
|
value = initialValue |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<select {value} on:change={ev => onChange(ev.target.value)}> |
||||
|
{#if isOptionsObject} |
||||
|
{#each options as { value, label }} |
||||
|
<option {...handleStyleBind(value || label)} value={value || label}> |
||||
|
{label} |
||||
|
</option> |
||||
|
{/each} |
||||
|
{:else} |
||||
|
{#each options as value} |
||||
|
<option {...handleStyleBind(value)} {value}>{value}</option> |
||||
|
{/each} |
||||
|
{/if} |
||||
|
</select> |
||||
@ -1,121 +0,0 @@ |
|||||
<script> |
|
||||
import { ArrowDownIcon } from "components/common/Icons/" |
|
||||
import { store } from "builderStore" |
|
||||
import { buildStateOrigins } from "builderStore/buildStateOrigins" |
|
||||
import { isBinding, getBinding, setBinding } from "components/common/binding" |
|
||||
import StateBindingOptions from "./StateBindingOptions.svelte" |
|
||||
|
|
||||
export let onChanged = () => {} |
|
||||
export let value = "" |
|
||||
|
|
||||
let isOpen = false |
|
||||
let stateBindings = [] |
|
||||
|
|
||||
let bindingPath = "" |
|
||||
let bindingFallbackValue = "" |
|
||||
let bindingSource = "store" |
|
||||
let bindingValue = "" |
|
||||
|
|
||||
const bindValueToSource = (path, fallback, source) => { |
|
||||
if (!path) { |
|
||||
onChanged(fallback) |
|
||||
return |
|
||||
} |
|
||||
const binding = setBinding({ path, fallback, source }) |
|
||||
onChanged(binding) |
|
||||
} |
|
||||
|
|
||||
const setBindingPath = value => |
|
||||
bindValueToSource(value, bindingFallbackValue, bindingSource) |
|
||||
|
|
||||
const setBindingFallback = value => |
|
||||
bindValueToSource(bindingPath, value, bindingSource) |
|
||||
|
|
||||
const setBindingSource = source => |
|
||||
bindValueToSource(bindingPath, bindingFallbackValue, source) |
|
||||
|
|
||||
$: { |
|
||||
const binding = getBinding(value) |
|
||||
if (bindingPath !== binding.path) isOpen = false |
|
||||
bindingPath = binding.path |
|
||||
bindingValue = typeof value === "object" ? "" : value |
|
||||
bindingFallbackValue = binding.fallback || bindingValue |
|
||||
|
|
||||
const currentScreen = $store.screens.find( |
|
||||
({ name }) => name === $store.currentPreviewItem.name |
|
||||
) |
|
||||
stateBindings = currentScreen |
|
||||
? Object.keys(buildStateOrigins(currentScreen)) |
|
||||
: [] |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="cascader"> |
|
||||
<div class="input-box"> |
|
||||
<input |
|
||||
class:bold={!bindingFallbackValue && bindingPath} |
|
||||
class="uk-input uk-form-small" |
|
||||
value={bindingFallbackValue || bindingPath} |
|
||||
on:change={e => { |
|
||||
setBindingFallback(e.target.value) |
|
||||
onChanged(e.target.value) |
|
||||
}} /> |
|
||||
<button on:click={() => (isOpen = !isOpen)}> |
|
||||
<div |
|
||||
class="icon" |
|
||||
class:highlighted={bindingPath} |
|
||||
style={`transform: rotate(${isOpen ? 0 : 90}deg);`}> |
|
||||
<ArrowDownIcon size={36} /> |
|
||||
</div> |
|
||||
</button> |
|
||||
</div> |
|
||||
{#if isOpen} |
|
||||
<StateBindingOptions |
|
||||
onSelect={option => { |
|
||||
onChanged(option) |
|
||||
isOpen = false |
|
||||
}} /> |
|
||||
{/if} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.bold { |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
.highlighted { |
|
||||
color: rgba(0, 85, 255, 0.8); |
|
||||
} |
|
||||
|
|
||||
button { |
|
||||
cursor: pointer; |
|
||||
outline: none; |
|
||||
border: none; |
|
||||
border-radius: 3px; |
|
||||
font-size: 1.6rem; |
|
||||
font-weight: 700; |
|
||||
color: rgba(22, 48, 87, 1); |
|
||||
} |
|
||||
|
|
||||
.cascader { |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.input-box { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
input { |
|
||||
margin-right: 5px; |
|
||||
border: 1px solid #dbdbdb; |
|
||||
border-radius: 2px; |
|
||||
opacity: 0.5; |
|
||||
height: 40px; |
|
||||
} |
|
||||
|
|
||||
.icon { |
|
||||
width: 24px; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,63 +0,0 @@ |
|||||
<script> |
|
||||
export let onSelect = () => {} |
|
||||
|
|
||||
let options = [ |
|
||||
{ |
|
||||
name: "state", |
|
||||
description: "Front-end client state.", |
|
||||
}, |
|
||||
{ |
|
||||
name: "context", |
|
||||
description: "The component context object.", |
|
||||
}, |
|
||||
{ |
|
||||
name: "event", |
|
||||
description: "DOM event handler arguments.", |
|
||||
}, |
|
||||
] |
|
||||
</script> |
|
||||
|
|
||||
<ul class="options"> |
|
||||
{#each options as option} |
|
||||
<li on:click={() => onSelect(`${option.name}.`)}> |
|
||||
<span class="name">{option.name}</span> |
|
||||
<span class="description">{option.description}</span> |
|
||||
</li> |
|
||||
{/each} |
|
||||
</ul> |
|
||||
|
|
||||
<style> |
|
||||
.options { |
|
||||
width: 172px; |
|
||||
margin: 0; |
|
||||
position: absolute; |
|
||||
top: 35px; |
|
||||
padding: 10px; |
|
||||
z-index: 1; |
|
||||
background: rgba(249, 249, 249, 1); |
|
||||
min-height: 50px; |
|
||||
border-radius: 2px; |
|
||||
} |
|
||||
|
|
||||
.description { |
|
||||
font-size: 0.8em; |
|
||||
} |
|
||||
|
|
||||
.name { |
|
||||
color: rgba(22, 48, 87, 0.6); |
|
||||
font-size: 13px; |
|
||||
font-weight: bold; |
|
||||
text-transform: uppercase; |
|
||||
margin-top: 5px; |
|
||||
display: block; |
|
||||
} |
|
||||
|
|
||||
.name:hover { |
|
||||
cursor: pointer; |
|
||||
font-weight: 800; |
|
||||
} |
|
||||
|
|
||||
li { |
|
||||
list-style-type: none; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1 +0,0 @@ |
|||||
export { default } from "./PropertyCascader.svelte" |
|
||||
@ -0,0 +1,89 @@ |
|||||
|
<script> |
||||
|
import { store, backendUiStore, workflowStore } from "builderStore" |
||||
|
import { notifier } from "@beyonk/svelte-notifications" |
||||
|
import api from "builderStore/api" |
||||
|
import ActionButton from "components/common/ActionButton.svelte" |
||||
|
|
||||
|
export let onClosed |
||||
|
|
||||
|
let name |
||||
|
|
||||
|
$: valid = !!name |
||||
|
$: instanceId = $backendUiStore.selectedDatabase._id |
||||
|
|
||||
|
async function deleteWorkflow() { |
||||
|
await workflowStore.actions.delete({ |
||||
|
instanceId, |
||||
|
workflow: $workflowStore.currentWorkflow.workflow, |
||||
|
}) |
||||
|
onClosed() |
||||
|
notifier.danger("Workflow deleted.") |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<header> |
||||
|
<i class="ri-stackshare-line" /> |
||||
|
Delete Workflow |
||||
|
</header> |
||||
|
<div> |
||||
|
<p> |
||||
|
Are you sure you want to delete this workflow? This action can't be undone. |
||||
|
</p> |
||||
|
</div> |
||||
|
<footer> |
||||
|
<a href="https://docs.budibase.com"> |
||||
|
<i class="ri-information-line" /> |
||||
|
Learn about workflows |
||||
|
</a> |
||||
|
<ActionButton on:click={onClosed}>Cancel</ActionButton> |
||||
|
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton> |
||||
|
</footer> |
||||
|
|
||||
|
<style> |
||||
|
header { |
||||
|
font-size: 24px; |
||||
|
color: var(--font); |
||||
|
font-weight: bold; |
||||
|
padding: 30px; |
||||
|
} |
||||
|
|
||||
|
header i { |
||||
|
margin-right: 10px; |
||||
|
font-size: 20px; |
||||
|
background: var(--secondary); |
||||
|
color: var(--dark-grey); |
||||
|
padding: 8px; |
||||
|
} |
||||
|
|
||||
|
div { |
||||
|
padding: 0 30px 30px 30px; |
||||
|
} |
||||
|
|
||||
|
label { |
||||
|
font-size: 18px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
footer { |
||||
|
display: grid; |
||||
|
grid-auto-flow: column; |
||||
|
grid-gap: 5px; |
||||
|
grid-auto-columns: 3fr 1fr 1fr; |
||||
|
padding: 20px; |
||||
|
background: #fafafa; |
||||
|
border-radius: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
footer a { |
||||
|
color: var(--primary); |
||||
|
font-size: 14px; |
||||
|
vertical-align: middle; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
footer i { |
||||
|
font-size: 20px; |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,45 @@ |
|||||
|
<script> |
||||
|
import { store } from "builderStore" |
||||
|
import deepmerge from "deepmerge" |
||||
|
|
||||
|
export let value |
||||
|
|
||||
|
let pages = [] |
||||
|
let components = [] |
||||
|
let pageName |
||||
|
|
||||
|
let selectedPage |
||||
|
let selectedScreen |
||||
|
|
||||
|
$: pages = $store.pages |
||||
|
$: selectedPage = pages[pageName] |
||||
|
$: screens = selectedPage ? selectedPage._screens : [] |
||||
|
$: if (selectedPage) { |
||||
|
let result = selectedPage |
||||
|
for (screen of screens) { |
||||
|
result = deepmerge(result, screen) |
||||
|
} |
||||
|
components = result.props._children |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="uk-margin block-field"> |
||||
|
<label class="uk-form-label">Page</label> |
||||
|
<div class="uk-form-controls"> |
||||
|
<select class="budibase__input" bind:value={pageName}> |
||||
|
{#each Object.keys(pages) as page} |
||||
|
<option value={page}>{page}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
</div> |
||||
|
{#if components.length > 0} |
||||
|
<label class="uk-form-label">Component</label> |
||||
|
<div class="uk-form-controls"> |
||||
|
<select class="budibase__input" bind:value> |
||||
|
{#each components as component} |
||||
|
<option value={component._id}>{component._id}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
@ -0,0 +1,16 @@ |
|||||
|
<script> |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
|
||||
|
export let value |
||||
|
</script> |
||||
|
|
||||
|
<div class="uk-margin block-field"> |
||||
|
<div class="uk-form-controls"> |
||||
|
<select class="budibase__input" bind:value> |
||||
|
<option value="" /> |
||||
|
{#each $backendUiStore.models as model} |
||||
|
<option value={model}>{model.name}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,33 @@ |
|||||
|
<script> |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
|
||||
|
export let value |
||||
|
</script> |
||||
|
|
||||
|
<div class="uk-margin block-field"> |
||||
|
<div class="uk-form-controls"> |
||||
|
<select class="budibase__input" bind:value={value.model}> |
||||
|
{#each $backendUiStore.models as model} |
||||
|
<option value={model}>{model.name}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{#if value.model} |
||||
|
<div class="uk-margin block-field"> |
||||
|
<label class="uk-form-label fields">Fields</label> |
||||
|
{#each Object.keys(value.model.schema) as field} |
||||
|
<div class="uk-form-controls uk-margin"> |
||||
|
<label class="uk-form-label">{field}</label> |
||||
|
<input type="text" class="budibase__input" bind:value={value[field]} /> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
.fields { |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,294 @@ |
|||||
|
<script> |
||||
|
import { fade } from "svelte/transition" |
||||
|
import { onMount, getContext } from "svelte" |
||||
|
import { backendUiStore, workflowStore } from "builderStore" |
||||
|
import { notifier } from "@beyonk/svelte-notifications" |
||||
|
import api from "builderStore/api" |
||||
|
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte" |
||||
|
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte" |
||||
|
|
||||
|
const { open, close } = getContext("simple-modal") |
||||
|
|
||||
|
const ACCESS_LEVELS = [ |
||||
|
{ |
||||
|
name: "Admin", |
||||
|
key: "ADMIN", |
||||
|
}, |
||||
|
{ |
||||
|
name: "Power User", |
||||
|
key: "POWER_USER", |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
let selectedTab = "SETUP" |
||||
|
let testResult |
||||
|
|
||||
|
$: workflow = |
||||
|
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow |
||||
|
$: workflowBlock = $workflowStore.selectedWorkflowBlock |
||||
|
|
||||
|
function deleteWorkflow() { |
||||
|
open( |
||||
|
DeleteWorkflowModal, |
||||
|
{ |
||||
|
onClosed: close, |
||||
|
}, |
||||
|
{ styleContent: { padding: "0" } } |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function deleteWorkflowBlock() { |
||||
|
workflowStore.actions.deleteWorkflowBlock(workflowBlock) |
||||
|
notifier.info("Workflow block deleted.") |
||||
|
} |
||||
|
|
||||
|
function testWorkflow() { |
||||
|
testResult = "PASSED" |
||||
|
} |
||||
|
|
||||
|
async function saveWorkflow() { |
||||
|
const workflow = $workflowStore.currentWorkflow.workflow |
||||
|
await workflowStore.actions.save({ |
||||
|
instanceId: $backendUiStore.selectedDatabase._id, |
||||
|
workflow, |
||||
|
}) |
||||
|
notifier.success(`Workflow ${workflow.name} saved.`) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<section> |
||||
|
<header> |
||||
|
<span |
||||
|
class="hoverable" |
||||
|
class:selected={selectedTab === 'SETUP'} |
||||
|
on:click={() => { |
||||
|
selectedTab = 'SETUP' |
||||
|
testResult = null |
||||
|
}}> |
||||
|
Setup |
||||
|
</span> |
||||
|
{#if !workflowBlock} |
||||
|
<span |
||||
|
class="test-tab" |
||||
|
class:selected={selectedTab === 'TEST'} |
||||
|
on:click={() => (selectedTab = 'TEST')}> |
||||
|
Test |
||||
|
</span> |
||||
|
{/if} |
||||
|
</header> |
||||
|
{#if selectedTab === 'TEST'} |
||||
|
<div class="uk-margin config-item"> |
||||
|
{#if testResult} |
||||
|
<button |
||||
|
transition:fade |
||||
|
class:passed={testResult === 'PASSED'} |
||||
|
class:failed={testResult === 'FAILED'} |
||||
|
class="test-result"> |
||||
|
{testResult} |
||||
|
</button> |
||||
|
{/if} |
||||
|
<button class="workflow-button hoverable" on:click={testWorkflow}> |
||||
|
Test |
||||
|
</button> |
||||
|
</div> |
||||
|
{/if} |
||||
|
{#if selectedTab === 'SETUP'} |
||||
|
{#if workflowBlock} |
||||
|
<WorkflowBlockSetup {workflowBlock} /> |
||||
|
<div class="buttons"> |
||||
|
<button class="workflow-button hoverable" on:click={saveWorkflow}> |
||||
|
Save Workflow |
||||
|
</button> |
||||
|
<button |
||||
|
class="delete-workflow-button hoverable" |
||||
|
on:click={deleteWorkflowBlock}> |
||||
|
Delete Block |
||||
|
</button> |
||||
|
</div> |
||||
|
{:else if $workflowStore.currentWorkflow} |
||||
|
<div class="panel"> |
||||
|
<div class="panel-body"> |
||||
|
<div class="block-label">Workflow: {workflow.name}</div> |
||||
|
<div class="config-item"> |
||||
|
<label>Name</label> |
||||
|
<div class="form"> |
||||
|
<input |
||||
|
type="text" |
||||
|
class="budibase_input" |
||||
|
bind:value={workflow.name} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="config-item"> |
||||
|
<label class="uk-form-label">User Access</label> |
||||
|
<div class="access-levels"> |
||||
|
{#each ACCESS_LEVELS as { name, key }} |
||||
|
<span class="access-level"> |
||||
|
<label>{name}</label> |
||||
|
<input class="uk-checkbox" type="checkbox" /> |
||||
|
</span> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="buttons"> |
||||
|
<button class="delete-workflow-button" on:click={deleteWorkflow}> |
||||
|
Delete Workflow |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
{/if} |
||||
|
{/if} |
||||
|
</section> |
||||
|
|
||||
|
<style> |
||||
|
section { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
|
||||
|
.panel-body { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.panel { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
|
||||
|
header { |
||||
|
font-size: 20px; |
||||
|
font-weight: 700; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-bottom: 18px; |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
.selected { |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
.block-label { |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
color: var(--ink); |
||||
|
margin: 0px 0px 16px 0px; |
||||
|
} |
||||
|
|
||||
|
.config-item { |
||||
|
margin: 0px 0px 4px 0px; |
||||
|
padding: 12px; |
||||
|
background: var(--light-grey); |
||||
|
} |
||||
|
|
||||
|
.budibase_input { |
||||
|
height: 35px; |
||||
|
width: 220px; |
||||
|
border-radius: 3px; |
||||
|
border: 1px solid var(--grey-dark); |
||||
|
text-align: left; |
||||
|
color: var(--ink); |
||||
|
font-size: 14px; |
||||
|
padding-left: 12px; |
||||
|
} |
||||
|
|
||||
|
header > span { |
||||
|
color: var(--ink-lighter); |
||||
|
margin-right: 20px; |
||||
|
} |
||||
|
|
||||
|
.form { |
||||
|
margin-top: 12px; |
||||
|
} |
||||
|
|
||||
|
label { |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
.buttons { |
||||
|
position: absolute; |
||||
|
bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.delete-workflow-button { |
||||
|
cursor: pointer; |
||||
|
border: 1px solid var(--red); |
||||
|
border-radius: 3px; |
||||
|
width: 260px; |
||||
|
padding: 8px 16px; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
background: var(--white); |
||||
|
color: var(--red); |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
transition: all 2ms; |
||||
|
align-self: flex-end; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.delete-workflow-button:hover { |
||||
|
background: var(--red); |
||||
|
border: 1px solid var(--red); |
||||
|
color: var(--white); |
||||
|
} |
||||
|
|
||||
|
.workflow-button { |
||||
|
cursor: pointer; |
||||
|
border: 1px solid var(--grey-dark); |
||||
|
border-radius: 3px; |
||||
|
width: 100%; |
||||
|
padding: 8px 16px; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
background: white; |
||||
|
color: var(--ink); |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
transition: all 2ms; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.workflow-button:hover { |
||||
|
background: var(--grey-light); |
||||
|
} |
||||
|
|
||||
|
.access-level { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.access-level label { |
||||
|
font-weight: normal; |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
.test-result { |
||||
|
border: none; |
||||
|
width: 100%; |
||||
|
border-radius: 3px; |
||||
|
height: 32px; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
color: var(--white); |
||||
|
text-align: center; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.passed { |
||||
|
background: #84c991; |
||||
|
} |
||||
|
|
||||
|
.failed { |
||||
|
background: var(--red); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,97 @@ |
|||||
|
<script> |
||||
|
import { backendUiStore, store } from "builderStore" |
||||
|
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte" |
||||
|
import ModelSelector from "./ParamInputs/ModelSelector.svelte" |
||||
|
import RecordSelector from "./ParamInputs/RecordSelector.svelte" |
||||
|
|
||||
|
export let workflowBlock |
||||
|
|
||||
|
let params |
||||
|
|
||||
|
$: workflowParams = workflowBlock.params |
||||
|
? Object.entries(workflowBlock.params) |
||||
|
: [] |
||||
|
</script> |
||||
|
|
||||
|
<label class="uk-form-label">{workflowBlock.type}: {workflowBlock.name}</label> |
||||
|
{#each workflowParams as [parameter, type]} |
||||
|
<div class="block-field"> |
||||
|
<label class="uk-form-label">{parameter}</label> |
||||
|
<div class="uk-form-controls"> |
||||
|
{#if Array.isArray(type)} |
||||
|
<select |
||||
|
class="budibase_input" |
||||
|
bind:value={workflowBlock.args[parameter]}> |
||||
|
{#each type as option} |
||||
|
<option value={option}>{option}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
{:else if type === 'component'} |
||||
|
<ComponentSelector bind:value={workflowBlock.args[parameter]} /> |
||||
|
{:else if type === 'accessLevel'} |
||||
|
<select |
||||
|
class="budibase__input" |
||||
|
bind:value={workflowBlock.args[parameter]}> |
||||
|
<option value="ADMIN">Admin</option> |
||||
|
<option value="POWER_USER">Power User</option> |
||||
|
</select> |
||||
|
{:else if type === 'password'} |
||||
|
<input |
||||
|
type="password" |
||||
|
class="budibase__input" |
||||
|
bind:value={workflowBlock.args[parameter]} /> |
||||
|
{:else if type === 'number'} |
||||
|
<input |
||||
|
type="number" |
||||
|
class="budibase__input" |
||||
|
bind:value={workflowBlock.args[parameter]} /> |
||||
|
{:else if type === 'longText'} |
||||
|
<textarea |
||||
|
type="text" |
||||
|
class="budibase__input" |
||||
|
bind:value={workflowBlock.args[parameter]} /> |
||||
|
{:else if type === 'model'} |
||||
|
<ModelSelector bind:value={workflowBlock.args[parameter]} /> |
||||
|
{:else if type === 'record'} |
||||
|
<RecordSelector bind:value={workflowBlock.args[parameter]} /> |
||||
|
{:else if type === 'string'} |
||||
|
<input |
||||
|
type="text" |
||||
|
class="budibase__input" |
||||
|
bind:value={workflowBlock.args[parameter]} /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</div> |
||||
|
{/each} |
||||
|
|
||||
|
<style> |
||||
|
.block-field { |
||||
|
border-radius: 3px; |
||||
|
background: var(--grey-light); |
||||
|
padding: 12px; |
||||
|
margin: 0px 0px 4px 0px; |
||||
|
} |
||||
|
|
||||
|
.budibase_input { |
||||
|
height: 35px; |
||||
|
width: 220px; |
||||
|
border-radius: 3px; |
||||
|
border: 1px solid var(--grey-dark); |
||||
|
text-align: left; |
||||
|
color: var(--ink); |
||||
|
font-size: 14px; |
||||
|
padding-left: 12px; |
||||
|
} |
||||
|
|
||||
|
label { |
||||
|
text-transform: capitalize; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
textarea { |
||||
|
min-height: 150px; |
||||
|
font-family: inherit; |
||||
|
padding: 5px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1 @@ |
|||||
|
export { default as SetupPanel } from "./SetupPanel.svelte" |
||||
@ -0,0 +1,90 @@ |
|||||
|
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
import { workflowStore, backendUiStore } from "builderStore" |
||||
|
import { notifier } from "@beyonk/svelte-notifications" |
||||
|
import Flowchart from "./flowchart/FlowChart.svelte" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
let selectedWorkflow |
||||
|
let uiTree |
||||
|
let instanceId = $backendUiStore.selectedDatabase._id |
||||
|
|
||||
|
$: selectedWorkflow = $workflowStore.currentWorkflow |
||||
|
|
||||
|
$: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live |
||||
|
|
||||
|
$: uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : [] |
||||
|
|
||||
|
$: instanceId = $backendUiStore.selectedDatabase._id |
||||
|
|
||||
|
function onSelect(block) { |
||||
|
workflowStore.update(state => { |
||||
|
state.selectedWorkflowBlock = block |
||||
|
return state |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function setWorkflowLive(live) { |
||||
|
const { workflow } = selectedWorkflow |
||||
|
workflow.live = live |
||||
|
workflowStore.actions.save({ instanceId, workflow }) |
||||
|
if (live) { |
||||
|
notifier.info(`Workflow ${workflow.name} enabled.`) |
||||
|
} else { |
||||
|
notifier.danger(`Workflow ${workflow.name} disabled.`) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<section> |
||||
|
<Flowchart blocks={uiTree} {onSelect} /> |
||||
|
<footer> |
||||
|
{#if selectedWorkflow} |
||||
|
<button |
||||
|
class:highlighted={workflowLive} |
||||
|
class:hoverable={workflowLive} |
||||
|
class="stop-button hoverable"> |
||||
|
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} /> |
||||
|
</button> |
||||
|
<button |
||||
|
class:highlighted={!workflowLive} |
||||
|
class:hoverable={!workflowLive} |
||||
|
class="play-button hoverable" |
||||
|
on:click={() => setWorkflowLive(true)}> |
||||
|
<i class="ri-play-fill" /> |
||||
|
</button> |
||||
|
{/if} |
||||
|
</footer> |
||||
|
</section> |
||||
|
|
||||
|
<style> |
||||
|
footer { |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
right: 0; |
||||
|
display: flex; |
||||
|
align-items: flex-end; |
||||
|
} |
||||
|
|
||||
|
footer > button { |
||||
|
border-radius: 100%; |
||||
|
color: var(--white); |
||||
|
width: 76px; |
||||
|
height: 76px; |
||||
|
border: none; |
||||
|
background: #adaec4; |
||||
|
font-size: 45px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
margin-right: 24px; |
||||
|
} |
||||
|
|
||||
|
.play-button.highlighted { |
||||
|
background: var(--primary); |
||||
|
} |
||||
|
|
||||
|
.stop-button.highlighted { |
||||
|
background: var(--coral); |
||||
|
} |
||||
|
</style> |
||||
|
After Width: | Height: | Size: 241 B |
@ -0,0 +1,24 @@ |
|||||
|
<script> |
||||
|
import FlowItem from "./FlowItem.svelte" |
||||
|
import Arrow from "./Arrow.svelte" |
||||
|
|
||||
|
export let blocks = [] |
||||
|
export let onSelect |
||||
|
</script> |
||||
|
|
||||
|
<section class="canvas"> |
||||
|
{#each blocks as block, idx} |
||||
|
<FlowItem {onSelect} {block} /> |
||||
|
{#if idx !== blocks.length - 1} |
||||
|
<Arrow /> |
||||
|
{/if} |
||||
|
{/each} |
||||
|
</section> |
||||
|
|
||||
|
<style> |
||||
|
.canvas { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,73 @@ |
|||||
|
<script> |
||||
|
import { fade } from "svelte/transition" |
||||
|
|
||||
|
export let onSelect |
||||
|
export let block |
||||
|
|
||||
|
function selectBlock() { |
||||
|
onSelect(block) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div transition:fade class={`${block.type} hoverable`} on:click={selectBlock}> |
||||
|
<header> |
||||
|
{#if block.type === 'TRIGGER'} |
||||
|
<i class="ri-lightbulb-fill" /> |
||||
|
When this happens... |
||||
|
{:else if block.type === 'ACTION'} |
||||
|
<i class="ri-flashlight-fill" /> |
||||
|
Do this... |
||||
|
{:else if block.type === 'LOGIC'} |
||||
|
<i class="ri-pause-fill" /> |
||||
|
Only continue if... |
||||
|
{/if} |
||||
|
</header> |
||||
|
<hr /> |
||||
|
<p> |
||||
|
{@html block.body} |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div { |
||||
|
width: 320px; |
||||
|
padding: 20px; |
||||
|
border-radius: 5px; |
||||
|
transition: 0.3s all; |
||||
|
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08); |
||||
|
background-color: var(--font); |
||||
|
font-size: 16px; |
||||
|
color: var(--white); |
||||
|
} |
||||
|
|
||||
|
header { |
||||
|
font-size: 16px; |
||||
|
font-weight: 500; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
header i { |
||||
|
font-size: 20px; |
||||
|
margin-right: 5px; |
||||
|
} |
||||
|
|
||||
|
.ACTION { |
||||
|
background-color: var(--white); |
||||
|
color: var(--font); |
||||
|
} |
||||
|
|
||||
|
.TRIGGER { |
||||
|
background-color: var(--font); |
||||
|
color: var(--white); |
||||
|
} |
||||
|
|
||||
|
.LOGIC { |
||||
|
background-color: var(--secondary); |
||||
|
color: var(--font); |
||||
|
} |
||||
|
|
||||
|
div:hover { |
||||
|
transform: scale(1.05); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,81 @@ |
|||||
|
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
import { backendUiStore, workflowStore } from "builderStore" |
||||
|
import { WorkflowList } from "../" |
||||
|
import WorkflowBlock from "./WorkflowBlock.svelte" |
||||
|
import api from "builderStore/api" |
||||
|
import blockDefinitions from "../blockDefinitions" |
||||
|
|
||||
|
let selectedTab = "TRIGGER" |
||||
|
let definitions = [] |
||||
|
|
||||
|
$: definitions = Object.entries(blockDefinitions[selectedTab]) |
||||
|
|
||||
|
$: { |
||||
|
if ( |
||||
|
$workflowStore.currentWorkflow.hasTrigger() && |
||||
|
selectedTab === "TRIGGER" |
||||
|
) { |
||||
|
selectedTab = "ACTION" |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<section> |
||||
|
<div class="subtabs"> |
||||
|
{#if !$workflowStore.currentWorkflow.hasTrigger()} |
||||
|
<span |
||||
|
class="hoverable" |
||||
|
class:selected={'TRIGGER' === selectedTab} |
||||
|
on:click={() => (selectedTab = 'TRIGGER')}> |
||||
|
Triggers |
||||
|
</span> |
||||
|
{/if} |
||||
|
<span |
||||
|
class="hoverable" |
||||
|
class:selected={'ACTION' === selectedTab} |
||||
|
on:click={() => (selectedTab = 'ACTION')}> |
||||
|
Actions |
||||
|
</span> |
||||
|
<span |
||||
|
class="hoverable" |
||||
|
class:selected={'LOGIC' === selectedTab} |
||||
|
on:click={() => (selectedTab = 'LOGIC')}> |
||||
|
Logic |
||||
|
</span> |
||||
|
</div> |
||||
|
<div id="blocklist"> |
||||
|
{#each definitions as [actionId, blockDefinition]} |
||||
|
<WorkflowBlock {blockDefinition} {actionId} blockType={selectedTab} /> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<style> |
||||
|
.subtabs { |
||||
|
margin-top: 27px; |
||||
|
display: grid; |
||||
|
grid-gap: 5px; |
||||
|
grid-auto-flow: column; |
||||
|
grid-auto-columns: 1fr 1fr 1fr; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.subtabs span { |
||||
|
transition: 0.3s all; |
||||
|
text-align: center; |
||||
|
color: var(--dark-grey); |
||||
|
font-weight: 500; |
||||
|
padding: 10px; |
||||
|
} |
||||
|
|
||||
|
.subtabs span.selected { |
||||
|
background: var(--dark-grey); |
||||
|
color: var(--white); |
||||
|
border-radius: 2px; |
||||
|
} |
||||
|
|
||||
|
.subtabs span:not(.selected) { |
||||
|
color: var(--dark-grey); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,56 @@ |
|||||
|
<script> |
||||
|
import { workflowStore } from "builderStore" |
||||
|
|
||||
|
export let blockType |
||||
|
export let blockDefinition |
||||
|
export let actionId |
||||
|
|
||||
|
function addBlockToWorkflow() { |
||||
|
workflowStore.actions.addBlockToWorkflow({ |
||||
|
...blockDefinition, |
||||
|
args: blockDefinition.args || {}, |
||||
|
actionId, |
||||
|
type: blockType, |
||||
|
}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="workflow-block hoverable" on:click={addBlockToWorkflow}> |
||||
|
<div> |
||||
|
<i class={blockDefinition.icon} /> |
||||
|
</div> |
||||
|
<div class="workflow-text"> |
||||
|
<h4>{blockDefinition.name}</h4> |
||||
|
<p>{blockDefinition.description}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.workflow-block { |
||||
|
display: flex; |
||||
|
padding: 20px; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.workflow-text { |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
|
||||
|
i { |
||||
|
background: var(--secondary); |
||||
|
color: var(--dark-grey); |
||||
|
padding: 10px; |
||||
|
} |
||||
|
|
||||
|
h4 { |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
font-size: 12px; |
||||
|
color: var(--dark-grey); |
||||
|
margin: 0; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,89 @@ |
|||||
|
<script> |
||||
|
import { store, backendUiStore, workflowStore } from "builderStore" |
||||
|
import { notifier } from "@beyonk/svelte-notifications" |
||||
|
import api from "builderStore/api" |
||||
|
import ActionButton from "components/common/ActionButton.svelte" |
||||
|
|
||||
|
export let onClosed |
||||
|
|
||||
|
let name |
||||
|
|
||||
|
$: valid = !!name |
||||
|
$: instanceId = $backendUiStore.selectedDatabase._id |
||||
|
$: appId = $store.appId |
||||
|
|
||||
|
async function createWorkflow() { |
||||
|
await workflowStore.actions.create({ |
||||
|
name, |
||||
|
instanceId, |
||||
|
}) |
||||
|
onClosed() |
||||
|
notifier.success(`Workflow ${name} created.`) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<header> |
||||
|
<i class="ri-stackshare-line" /> |
||||
|
Create Workflow |
||||
|
</header> |
||||
|
<div> |
||||
|
<label class="uk-form-label" for="form-stacked-text">Name</label> |
||||
|
<input class="uk-input" type="text" bind:value={name} /> |
||||
|
</div> |
||||
|
<footer> |
||||
|
<a href="https://docs.budibase.com"> |
||||
|
<i class="ri-information-line" /> |
||||
|
Learn about workflows |
||||
|
</a> |
||||
|
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton> |
||||
|
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton> |
||||
|
</footer> |
||||
|
|
||||
|
<style> |
||||
|
header { |
||||
|
font-size: 24px; |
||||
|
color: var(--font); |
||||
|
font-weight: bold; |
||||
|
padding: 30px; |
||||
|
} |
||||
|
|
||||
|
header i { |
||||
|
margin-right: 10px; |
||||
|
font-size: 20px; |
||||
|
background: var(--secondary); |
||||
|
color: var(--dark-grey); |
||||
|
padding: 8px; |
||||
|
} |
||||
|
|
||||
|
div { |
||||
|
padding: 0 30px 30px 30px; |
||||
|
} |
||||
|
|
||||
|
label { |
||||
|
font-size: 18px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
footer { |
||||
|
display: grid; |
||||
|
grid-auto-flow: column; |
||||
|
grid-gap: 5px; |
||||
|
grid-auto-columns: 3fr 1fr 1fr; |
||||
|
padding: 20px; |
||||
|
background: #fafafa; |
||||
|
border-radius: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
footer a { |
||||
|
color: var(--primary); |
||||
|
font-size: 14px; |
||||
|
vertical-align: middle; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
footer i { |
||||
|
font-size: 20px; |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,125 @@ |
|||||
|
<script> |
||||
|
import Modal from "svelte-simple-modal" |
||||
|
import { notifier } from "@beyonk/svelte-notifications" |
||||
|
import { onMount, getContext } from "svelte" |
||||
|
import { backendUiStore, workflowStore } from "builderStore" |
||||
|
import api from "builderStore/api" |
||||
|
import CreateWorkflowModal from "./CreateWorkflowModal.svelte" |
||||
|
|
||||
|
const { open, close } = getContext("simple-modal") |
||||
|
|
||||
|
$: currentWorkflowId = |
||||
|
$workflowStore.currentWorkflow && |
||||
|
$workflowStore.currentWorkflow.workflow._id |
||||
|
|
||||
|
function newWorkflow() { |
||||
|
open( |
||||
|
CreateWorkflowModal, |
||||
|
{ |
||||
|
onClosed: close, |
||||
|
}, |
||||
|
{ styleContent: { padding: "0" } } |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
onMount(() => { |
||||
|
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<section> |
||||
|
<button class="new-workflow-button hoverable" on:click={newWorkflow}> |
||||
|
<i class="icon ri-add-circle-fill" /> |
||||
|
Create New Workflow |
||||
|
</button> |
||||
|
<ul> |
||||
|
{#each $workflowStore.workflows as workflow} |
||||
|
<li |
||||
|
class="workflow-item" |
||||
|
class:selected={workflow._id === currentWorkflowId} |
||||
|
on:click={() => workflowStore.actions.select(workflow)}> |
||||
|
<i class="ri-stackshare-line" class:live={workflow.live} /> |
||||
|
{workflow.name} |
||||
|
</li> |
||||
|
{/each} |
||||
|
</ul> |
||||
|
</section> |
||||
|
|
||||
|
<style> |
||||
|
section { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
i { |
||||
|
color: #adaec4; |
||||
|
} |
||||
|
|
||||
|
i:hover { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
list-style-type: none; |
||||
|
padding: 0; |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.live { |
||||
|
color: var(--primary); |
||||
|
} |
||||
|
|
||||
|
li { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.workflow-item { |
||||
|
display: flex; |
||||
|
border-radius: 3px; |
||||
|
padding-left: 12px; |
||||
|
align-items: center; |
||||
|
height: 40px; |
||||
|
margin-bottom: 4px; |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
.workflow-item i { |
||||
|
font-size: 24px; |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
|
||||
|
.workflow-item:hover { |
||||
|
cursor: pointer; |
||||
|
background: var(--grey-light); |
||||
|
} |
||||
|
|
||||
|
.workflow-item.selected { |
||||
|
background: var(--blue-light); |
||||
|
} |
||||
|
|
||||
|
.new-workflow-button { |
||||
|
cursor: pointer; |
||||
|
border: 1px solid var(--grey-dark); |
||||
|
border-radius: 3px; |
||||
|
width: 100%; |
||||
|
padding: 8px 16px; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
background: white; |
||||
|
color: var(--ink); |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
transition: all 2ms; |
||||
|
} |
||||
|
|
||||
|
.new-workflow-button:hover { |
||||
|
background: var(--grey-light); |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
color: var(--ink); |
||||
|
font-size: 16px; |
||||
|
margin-right: 4px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,55 @@ |
|||||
|
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
import { backendUiStore, workflowStore } from "builderStore" |
||||
|
import { WorkflowList, BlockList } from "./" |
||||
|
import api from "builderStore/api" |
||||
|
import blockDefinitions from "./blockDefinitions" |
||||
|
|
||||
|
let selectedTab = "WORKFLOWS" |
||||
|
let definitions = [] |
||||
|
</script> |
||||
|
|
||||
|
<header> |
||||
|
<span |
||||
|
class="hoverable workflow-header" |
||||
|
class:selected={selectedTab === 'WORKFLOWS'} |
||||
|
on:click={() => (selectedTab = 'WORKFLOWS')}> |
||||
|
Workflows |
||||
|
</span> |
||||
|
{#if $workflowStore.currentWorkflow} |
||||
|
<span |
||||
|
class="hoverable" |
||||
|
class:selected={selectedTab === 'ADD'} |
||||
|
on:click={() => (selectedTab = 'ADD')}> |
||||
|
Add |
||||
|
</span> |
||||
|
{/if} |
||||
|
</header> |
||||
|
{#if selectedTab === 'WORKFLOWS'} |
||||
|
<WorkflowList /> |
||||
|
{:else if selectedTab === 'ADD'} |
||||
|
<BlockList /> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
header { |
||||
|
font-size: 18px; |
||||
|
font-weight: 700; |
||||
|
background: none; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.workflow-header { |
||||
|
margin-right: 20px; |
||||
|
} |
||||
|
|
||||
|
span:not(.selected) { |
||||
|
color: var(--ink-lighter); |
||||
|
} |
||||
|
|
||||
|
span:not(.selected):hover { |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,170 @@ |
|||||
|
const ACTION = { |
||||
|
SET_STATE: { |
||||
|
name: "Update UI State", |
||||
|
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>", |
||||
|
icon: "ri-refresh-line", |
||||
|
description: "Update your User Interface with some data.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
path: "string", |
||||
|
value: "longText", |
||||
|
}, |
||||
|
}, |
||||
|
NAVIGATE: { |
||||
|
name: "Navigate", |
||||
|
tagline: "Navigate to <b>{{url}}</b>", |
||||
|
icon: "ri-navigation-line", |
||||
|
description: "Navigate to another page.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
url: "string", |
||||
|
}, |
||||
|
}, |
||||
|
SAVE_RECORD: { |
||||
|
name: "Save Record", |
||||
|
tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record", |
||||
|
icon: "ri-save-3-fill", |
||||
|
description: "Save a record to your database.", |
||||
|
environment: "SERVER", |
||||
|
params: { |
||||
|
record: "record", |
||||
|
}, |
||||
|
args: { |
||||
|
record: {}, |
||||
|
}, |
||||
|
}, |
||||
|
DELETE_RECORD: { |
||||
|
description: "Delete a record from your database.", |
||||
|
icon: "ri-delete-bin-line", |
||||
|
name: "Delete Record", |
||||
|
tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record", |
||||
|
environment: "SERVER", |
||||
|
params: { |
||||
|
record: "record", |
||||
|
}, |
||||
|
args: { |
||||
|
record: {}, |
||||
|
}, |
||||
|
}, |
||||
|
// FIND_RECORD: {
|
||||
|
// description: "Find a record in your database.",
|
||||
|
// tagline: "<b>Find</b> a <b>{{record.model.name}}</b> record",
|
||||
|
// icon: "ri-search-line",
|
||||
|
// name: "Find Record",
|
||||
|
// environment: "SERVER",
|
||||
|
// params: {
|
||||
|
// record: "string",
|
||||
|
// },
|
||||
|
// },
|
||||
|
CREATE_USER: { |
||||
|
description: "Create a new user.", |
||||
|
tagline: "Create user <b>{{username}}</b>", |
||||
|
icon: "ri-user-add-fill", |
||||
|
name: "Create User", |
||||
|
environment: "SERVER", |
||||
|
params: { |
||||
|
username: "string", |
||||
|
password: "password", |
||||
|
accessLevelId: "accessLevel", |
||||
|
}, |
||||
|
}, |
||||
|
SEND_EMAIL: { |
||||
|
description: "Send an email.", |
||||
|
tagline: "Send email to <b>{{to}}</b>", |
||||
|
icon: "ri-mail-open-fill", |
||||
|
name: "Send Email", |
||||
|
environment: "SERVER", |
||||
|
params: { |
||||
|
to: "string", |
||||
|
from: "string", |
||||
|
subject: "longText", |
||||
|
text: "longText", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
const TRIGGER = { |
||||
|
RECORD_SAVED: { |
||||
|
name: "Record Saved", |
||||
|
event: "record:save", |
||||
|
icon: "ri-save-line", |
||||
|
tagline: "Record is added to <b>{{model.name}}</b>", |
||||
|
description: "Save a record to your database.", |
||||
|
environment: "SERVER", |
||||
|
params: { |
||||
|
model: "model", |
||||
|
}, |
||||
|
}, |
||||
|
RECORD_DELETED: { |
||||
|
name: "Record Deleted", |
||||
|
event: "record:delete", |
||||
|
icon: "ri-delete-bin-line", |
||||
|
tagline: "Record is deleted from <b>{{model.name}}</b>", |
||||
|
description: "Fired when a record is deleted from your database.", |
||||
|
environment: "SERVER", |
||||
|
params: { |
||||
|
model: "model", |
||||
|
}, |
||||
|
}, |
||||
|
// CLICK: {
|
||||
|
// name: "Click",
|
||||
|
// icon: "ri-cursor-line",
|
||||
|
// tagline: "{{component}} is clicked",
|
||||
|
// description: "Trigger when you click on an element in the UI.",
|
||||
|
// environment: "CLIENT",
|
||||
|
// params: {
|
||||
|
// component: "component"
|
||||
|
// }
|
||||
|
// },
|
||||
|
// LOAD: {
|
||||
|
// name: "Load",
|
||||
|
// icon: "ri-loader-line",
|
||||
|
// tagline: "{{component}} is loaded",
|
||||
|
// description: "Trigger an element has finished loading.",
|
||||
|
// environment: "CLIENT",
|
||||
|
// params: {
|
||||
|
// component: "component"
|
||||
|
// }
|
||||
|
// },
|
||||
|
// INPUT: {
|
||||
|
// name: "Input",
|
||||
|
// icon: "ri-text",
|
||||
|
// tagline: "Text entered into {{component}",
|
||||
|
// description: "Trigger when you type into an input box.",
|
||||
|
// environment: "CLIENT",
|
||||
|
// params: {
|
||||
|
// component: "component"
|
||||
|
// }
|
||||
|
// },
|
||||
|
} |
||||
|
|
||||
|
const LOGIC = { |
||||
|
FILTER: { |
||||
|
name: "Filter", |
||||
|
tagline: "{{field}} <b>{{condition}}</b> {{value}}", |
||||
|
icon: "ri-git-branch-line", |
||||
|
description: "Filter any workflows which do not meet certain conditions.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
filter: "string", |
||||
|
condition: ["equals"], |
||||
|
value: "string", |
||||
|
}, |
||||
|
}, |
||||
|
DELAY: { |
||||
|
name: "Delay", |
||||
|
icon: "ri-time-fill", |
||||
|
tagline: "Delay for <b>{{time}}</b> milliseconds", |
||||
|
description: "Delay the workflow until an amount of time has passed.", |
||||
|
environment: "CLIENT", |
||||
|
params: { |
||||
|
time: "number", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
ACTION, |
||||
|
TRIGGER, |
||||
|
LOGIC, |
||||
|
} |
||||