commit
ec155c2163
|
@ -6,6 +6,7 @@
|
|||
export let id = null
|
||||
export let text = null
|
||||
export let disabled = false
|
||||
export let dataCy = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = event => {
|
||||
|
@ -15,6 +16,7 @@
|
|||
|
||||
<div class="spectrum-Switch spectrum-Switch--emphasized">
|
||||
<input
|
||||
data-cy={dataCy}
|
||||
checked={value}
|
||||
{disabled}
|
||||
on:change={onChange}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let text = null
|
||||
export let disabled = false
|
||||
export let error = null
|
||||
export let dataCy = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -18,5 +19,5 @@
|
|||
</script>
|
||||
|
||||
<Field {label} {labelPosition} {error}>
|
||||
<Switch {error} {disabled} {text} {value} on:change={onChange} />
|
||||
<Switch {dataCy} {error} {disabled} {text} {value} on:change={onChange} />
|
||||
</Field>
|
||||
|
|
|
@ -29,10 +29,10 @@ context("Create a automation", () => {
|
|||
cy.get(".setup").within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.contains("dog").click()
|
||||
cy.get("input")
|
||||
cy.get(".spectrum-Textfield-input")
|
||||
.first()
|
||||
.type("goodboy")
|
||||
cy.get("input")
|
||||
cy.get(".spectrum-Textfield-input")
|
||||
.eq(1)
|
||||
.type("11")
|
||||
})
|
||||
|
@ -41,7 +41,7 @@ context("Create a automation", () => {
|
|||
cy.contains("Save Automation").click()
|
||||
|
||||
// Activate Automation
|
||||
cy.get("[aria-label=PlayCircle]").click()
|
||||
cy.get("[data-cy=activate-automation]").click()
|
||||
})
|
||||
|
||||
it("should add row when a new row is added", () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import { database } from "stores/backend"
|
||||
import { notifications, Icon, Button, Modal, Heading } from "@budibase/bbui"
|
||||
import { notifications, Button, Modal, Heading, Toggle } from "@budibase/bbui"
|
||||
import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
|
||||
import CreateWebookModal from "../Shared/CreateWebhookModal.svelte"
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
|||
$: automationLive = automation?.live
|
||||
|
||||
function setAutomationLive(live) {
|
||||
if (automation.live === live) {
|
||||
if (automationLive === live) {
|
||||
return
|
||||
}
|
||||
automation.live = live
|
||||
|
@ -48,20 +48,11 @@
|
|||
|
||||
<div class="title">
|
||||
<Heading size="S">Setup</Heading>
|
||||
<Icon
|
||||
l
|
||||
disabled={!automationLive}
|
||||
hoverable={automationLive}
|
||||
name="PauseCircle"
|
||||
on:click={() => setAutomationLive(false)}
|
||||
/>
|
||||
<Icon
|
||||
l
|
||||
name="PlayCircle"
|
||||
disabled={automationLive}
|
||||
hoverable={!automationLive}
|
||||
data-cy="activate-automation"
|
||||
on:click={() => setAutomationLive(true)}
|
||||
<Toggle
|
||||
value={automationLive}
|
||||
on:change={() => setAutomationLive(!automationLive)}
|
||||
dataCy="activate-automation"
|
||||
text="Live"
|
||||
/>
|
||||
</div>
|
||||
{#if $automationStore.selectedBlock}
|
||||
|
|
|
@ -101,6 +101,12 @@
|
|||
conditions = conditions.filter(link => link.id !== id)
|
||||
}
|
||||
|
||||
const duplicateCondition = id => {
|
||||
const condition = conditions.find(link => link.id === id)
|
||||
const duplicate = { ...condition, id: generate() }
|
||||
conditions = [...conditions, duplicate]
|
||||
}
|
||||
|
||||
const handleFinalize = e => {
|
||||
updateConditions(e)
|
||||
dragDisabled = true
|
||||
|
@ -235,6 +241,12 @@
|
|||
bind:value={condition.referenceValue}
|
||||
/>
|
||||
{/if}
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => duplicateCondition(condition.id)}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
|
@ -273,7 +285,7 @@
|
|||
gap: var(--spacing-l);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: auto 1fr auto 1fr 1fr 1fr 1fr auto;
|
||||
grid-template-columns: auto 1fr auto 1fr 1fr 1fr 1fr auto auto;
|
||||
border-radius: var(--border-radius-s);
|
||||
transition: background-color ease-in-out 130ms;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"ClearForm"
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label small>Form</Label>
|
||||
<Select
|
||||
bind:value={parameters.componentId}
|
||||
options={actionProviders}
|
||||
getOptionLabel={x => x._instanceName}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: 60px 1fr;
|
||||
align-items: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -5,6 +5,7 @@ import ExecuteQuery from "./ExecuteQuery.svelte"
|
|||
import TriggerAutomation from "./TriggerAutomation.svelte"
|
||||
import ValidateForm from "./ValidateForm.svelte"
|
||||
import LogOut from "./LogOut.svelte"
|
||||
import ClearForm from "./ClearForm.svelte"
|
||||
|
||||
// Defines which actions are available to configure in the front end.
|
||||
// Unfortunately the "name" property is used as the identifier so please don't
|
||||
|
@ -42,4 +43,8 @@ export default [
|
|||
name: "Log Out",
|
||||
component: LogOut,
|
||||
},
|
||||
{
|
||||
name: "Clear Form",
|
||||
component: ClearForm,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
export let exportApp
|
||||
export let viewApp
|
||||
export let editApp
|
||||
export let updateApp
|
||||
export let deleteApp
|
||||
export let unpublishApp
|
||||
export let releaseLock
|
||||
|
@ -53,6 +54,9 @@
|
|||
</MenuItem>
|
||||
{/if}
|
||||
{#if !app.deployed}
|
||||
<MenuItem on:click={() => updateApp(app)} icon="Edit">
|
||||
Update
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">
|
||||
Delete
|
||||
</MenuItem>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
export let exportApp
|
||||
export let viewApp
|
||||
export let editApp
|
||||
export let updateApp
|
||||
export let deleteApp
|
||||
export let unpublishApp
|
||||
export let releaseLock
|
||||
|
@ -82,6 +83,7 @@
|
|||
</MenuItem>
|
||||
{/if}
|
||||
{#if !app.deployed}
|
||||
<MenuItem on:click={() => updateApp(app)} icon="Edit">Update</MenuItem>
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
|
||||
{/if}
|
||||
</ActionMenu>
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { hostingStore } from "builderStore"
|
||||
import { apps } from "stores/portal"
|
||||
import { string, object } from "yup"
|
||||
import { onMount } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
const values = writable({ name: null })
|
||||
const errors = writable({})
|
||||
const touched = writable({})
|
||||
const validator = {
|
||||
name: string().required("Your application must have a name"),
|
||||
}
|
||||
|
||||
export let app
|
||||
|
||||
let modal
|
||||
let valid = false
|
||||
let dirty = false
|
||||
$: checkValidity($values, validator)
|
||||
$: {
|
||||
// prevent validation by setting name to undefined without an app
|
||||
if (app) {
|
||||
$values.name = app?.name
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await hostingStore.actions.fetchDeployedApps()
|
||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
||||
validator.name = string()
|
||||
.required("Your application must have a name")
|
||||
.test(
|
||||
"non-existing-app-name",
|
||||
"Another app with the same name already exists",
|
||||
value => {
|
||||
return !existingAppNames.some(
|
||||
appName => dirty && appName.toLowerCase() === value.toLowerCase()
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const checkValidity = async (values, validator) => {
|
||||
const obj = object().shape(validator)
|
||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
||||
try {
|
||||
await obj.validate(values, { abortEarly: false })
|
||||
} catch (validationErrors) {
|
||||
validationErrors.inner.forEach(error => {
|
||||
$errors[error.path] = capitalise(error.message)
|
||||
})
|
||||
}
|
||||
valid = await obj.isValid(values)
|
||||
}
|
||||
|
||||
async function updateApp() {
|
||||
try {
|
||||
// Update App
|
||||
await apps.update(app.instance._id, $values.name)
|
||||
hide()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
modal.show()
|
||||
}
|
||||
export const hide = () => {
|
||||
modal.hide()
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
hide()
|
||||
}
|
||||
|
||||
const onShow = () => {
|
||||
dirty = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}>
|
||||
<ModalContent
|
||||
title={"Update app"}
|
||||
confirmText={"Update app"}
|
||||
onConfirm={updateApp}
|
||||
disabled={!(valid && dirty)}
|
||||
>
|
||||
<Body size="S">
|
||||
Give your new app a name, and choose which groups have access (paid plans
|
||||
only).
|
||||
</Body>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$touched.name && $errors.name}
|
||||
on:blur={() => ($touched.name = true)}
|
||||
on:change={() => (dirty = true)}
|
||||
label="Name"
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
|
@ -14,6 +14,7 @@
|
|||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
import api, { del } from "builderStore/api"
|
||||
import analytics from "analytics"
|
||||
import { onMount } from "svelte"
|
||||
|
@ -30,6 +31,7 @@
|
|||
let template
|
||||
let selectedApp
|
||||
let creationModal
|
||||
let updatingModal
|
||||
let deletionModal
|
||||
let unpublishModal
|
||||
let creatingApp = false
|
||||
|
@ -164,6 +166,11 @@
|
|||
selectedApp = null
|
||||
}
|
||||
|
||||
const updateApp = async app => {
|
||||
selectedApp = app
|
||||
updatingModal.show()
|
||||
}
|
||||
|
||||
const releaseLock = async app => {
|
||||
try {
|
||||
const response = await del(`/api/dev/${app.devId}/lock`)
|
||||
|
@ -236,6 +243,7 @@
|
|||
{editApp}
|
||||
{exportApp}
|
||||
{deleteApp}
|
||||
{updateApp}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -289,6 +297,8 @@
|
|||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
|
||||
<UpdateAppModal app={selectedApp} bind:this={updatingModal} />
|
||||
|
||||
<style>
|
||||
.title,
|
||||
.filter {
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
on:change
|
||||
{options}
|
||||
label="Role"
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionLabel={role => role.label}
|
||||
getOptionValue={role => role.value}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { get } from "builderStore/api"
|
||||
import { AppStatus } from "../../constants"
|
||||
import api from "../../builderStore/api"
|
||||
|
||||
export function createAppStore() {
|
||||
const store = writable([])
|
||||
|
@ -53,9 +54,29 @@ export function createAppStore() {
|
|||
}
|
||||
}
|
||||
|
||||
async function update(appId, name) {
|
||||
const response = await api.put(`/api/applications/${appId}`, { name })
|
||||
if (response.status === 200) {
|
||||
store.update(state => {
|
||||
const updatedAppIndex = state.findIndex(
|
||||
app => app.instance._id === appId
|
||||
)
|
||||
if (updatedAppIndex !== -1) {
|
||||
const updatedApp = state[updatedAppIndex]
|
||||
updatedApp.name = name
|
||||
state.apps = state.splice(updatedAppIndex, 1, updatedApp)
|
||||
}
|
||||
return state
|
||||
})
|
||||
} else {
|
||||
throw new Error("Error updating name")
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
load,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export const ActionTypes = {
|
|||
ValidateForm: "ValidateForm",
|
||||
RefreshDatasource: "RefreshDatasource",
|
||||
SetDataProviderQuery: "SetDataProviderQuery",
|
||||
ClearForm: "ClearForm",
|
||||
}
|
||||
|
||||
export const ApiVersion = "1"
|
||||
|
|
|
@ -86,6 +86,14 @@ const logoutHandler = async () => {
|
|||
await authStore.actions.logOut()
|
||||
}
|
||||
|
||||
const clearFormHandler = async (action, context) => {
|
||||
return await executeActionHandler(
|
||||
context,
|
||||
action.parameters.componentId,
|
||||
ActionTypes.ClearForm
|
||||
)
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
["Save Row"]: saveRowHandler,
|
||||
["Delete Row"]: deleteRowHandler,
|
||||
|
@ -95,6 +103,7 @@ const handlerMap = {
|
|||
["Validate Form"]: validateFormHandler,
|
||||
["Refresh Datasource"]: refreshDatasourceHandler,
|
||||
["Log Out"]: logoutHandler,
|
||||
["Clear Form"]: clearFormHandler,
|
||||
}
|
||||
|
||||
const confirmTextMap = {
|
||||
|
|
|
@ -1647,7 +1647,8 @@
|
|||
"hasChildren": true,
|
||||
"illegalChildren": ["section"],
|
||||
"actions": [
|
||||
"ValidateForm"
|
||||
"ValidateForm",
|
||||
"ClearForm"
|
||||
],
|
||||
"styles": ["size"],
|
||||
"settings": [
|
||||
|
|
|
@ -64,6 +64,13 @@
|
|||
})
|
||||
return get(formState).valid
|
||||
},
|
||||
clear: () => {
|
||||
const fields = Object.keys(fieldMap)
|
||||
fields.forEach(field => {
|
||||
const { fieldApi } = fieldMap[field]
|
||||
fieldApi.clearValue()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Provide both form API and state to children
|
||||
|
@ -72,6 +79,7 @@
|
|||
// Action context to pass to children
|
||||
const actions = [
|
||||
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
|
||||
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
||||
]
|
||||
|
||||
// Creates an API for a specific field
|
||||
|
@ -108,8 +116,27 @@
|
|||
|
||||
return !newError
|
||||
}
|
||||
|
||||
const clearValue = () => {
|
||||
const { fieldState } = fieldMap[field]
|
||||
const newValue = initialValues[field] ?? defaultValue
|
||||
fieldState.update(state => {
|
||||
state.value = newValue
|
||||
state.error = null
|
||||
return state
|
||||
})
|
||||
|
||||
formState.update(state => {
|
||||
state.values = { ...state.values, [field]: newValue }
|
||||
delete state.errors[field]
|
||||
state.valid = Object.keys(state.errors).length === 0
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
setValue,
|
||||
clearValue,
|
||||
validate: () => {
|
||||
const { fieldState } = fieldMap[field]
|
||||
setValue(get(fieldState).value, true)
|
||||
|
|
Loading…
Reference in New Issue