Merge pull request #2168 from Budibase/develop

Develop
This commit is contained in:
Martin McKeaveney 2021-07-29 10:09:52 +01:00 committed by GitHub
commit ac3bf81f72
17 changed files with 256 additions and 24 deletions

View File

@ -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}

View File

@ -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>

View File

@ -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", () => {

View File

@ -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}

View File

@ -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;
}

View File

@ -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>

View File

@ -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,
},
]

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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,
}
}

View File

@ -6,6 +6,7 @@ export const ActionTypes = {
ValidateForm: "ValidateForm",
RefreshDatasource: "RefreshDatasource",
SetDataProviderQuery: "SetDataProviderQuery",
ClearForm: "ClearForm",
}
export const ApiVersion = "1"

View File

@ -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 = {

View File

@ -1647,7 +1647,8 @@
"hasChildren": true,
"illegalChildren": ["section"],
"actions": [
"ValidateForm"
"ValidateForm",
"ClearForm"
],
"styles": ["size"],
"settings": [

View File

@ -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)