Share validation between app modals, add yup based validation framework, add url to app modals
This commit is contained in:
parent
3d5a3e7902
commit
bc67974996
|
@ -430,13 +430,14 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
|
||||||
// Click to fetch tables
|
// Click to fetch tables
|
||||||
if (skipFetch) {
|
if (skipFetch) {
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Skip table fetch")
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Skip table fetch")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
})
|
})
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Save and fetch tables")
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Save and fetch tables")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const breweries = data
|
const breweries = data
|
||||||
const totals = {}
|
const totals = {}
|
||||||
|
|
||||||
for (let brewery of breweries)
|
for (let brewery of breweries) {
|
||||||
{const state = brewery.state
|
const state = brewery.state
|
||||||
if (totals[state] == null)
|
if (totals[state] == null) {
|
||||||
{totals[state] = 1
|
totals[state] = 1
|
||||||
} else
|
} else {
|
||||||
{totals[state]++
|
totals[state]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const entries = Object.entries(totals)
|
const entries = Object.entries(totals)
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const breweries = data
|
const breweries = data
|
||||||
const totals = {}
|
const totals = {}
|
||||||
for (let brewery of breweries)
|
for (let brewery of breweries) {
|
||||||
{const state = brewery.state
|
const state = brewery.state
|
||||||
if (totals[state] == null)
|
if (totals[state] == null) {
|
||||||
{totals[state] = 1
|
totals[state] = 1
|
||||||
} else
|
} else {
|
||||||
{totals[state]++
|
totals[state]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stateCodes =
|
const stateCodes = {
|
||||||
{texas: "tx",
|
texas: "tx",
|
||||||
colorado: "co",
|
colorado: "co",
|
||||||
florida: "fl",
|
florida: "fl",
|
||||||
iwoa: "ia",
|
iwoa: "ia",
|
||||||
|
@ -24,7 +25,7 @@ const stateCodes =
|
||||||
ohio: "oh",
|
ohio: "oh",
|
||||||
}
|
}
|
||||||
const entries = Object.entries(totals)
|
const entries = Object.entries(totals)
|
||||||
return entries.map(([state, count]) =>
|
return entries.map(([state, count]) => {
|
||||||
{stateCodes[state.toLowerCase()]
|
stateCodes[state.toLowerCase()]
|
||||||
return { state, count, flag: "http://flags.ox3.in/svg/us/${stateCode}.svg" }
|
return { state, count, flag: "http://flags.ox3.in/svg/us/${stateCode}.svg" }
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,98 +4,45 @@
|
||||||
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||||
import { store, automationStore, hostingStore } from "builderStore"
|
import { store, automationStore, hostingStore } from "builderStore"
|
||||||
import { admin, auth } from "stores/portal"
|
import { admin, auth } from "stores/portal"
|
||||||
import { string, mixed, object } from "yup"
|
|
||||||
import api, { get, post } from "builderStore/api"
|
import api, { get, post } from "builderStore/api"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { capitalise } from "helpers"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { APP_NAME_REGEX } from "constants"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import TemplateList from "./TemplateList.svelte"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
export let inline
|
|
||||||
|
|
||||||
const values = writable({ name: null })
|
const values = writable({ name: "", url: null })
|
||||||
const errors = writable({})
|
const validation = createValidationStore()
|
||||||
const touched = writable({})
|
$: validation.check($values)
|
||||||
const validator = {
|
|
||||||
name: string()
|
|
||||||
.trim()
|
|
||||||
.required("Your application must have a name")
|
|
||||||
.matches(
|
|
||||||
APP_NAME_REGEX,
|
|
||||||
"App name must be letters, numbers and spaces only"
|
|
||||||
),
|
|
||||||
file: template?.fromFile
|
|
||||||
? mixed().required("Please choose a file to import")
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
let submitting = false
|
|
||||||
let valid = false
|
|
||||||
let initialTemplateInfo = template?.fromFile || template?.key
|
|
||||||
|
|
||||||
$: checkValidity($values, validator)
|
|
||||||
$: showTemplateSelection = !template && !initialTemplateInfo
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
await setupValidation()
|
||||||
|
})
|
||||||
|
|
||||||
|
const setupValidation = async () => {
|
||||||
await hostingStore.actions.fetchDeployedApps()
|
await hostingStore.actions.fetchDeployedApps()
|
||||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
const apps = svelteGet(hostingStore).deployedApps
|
||||||
validator.name = string()
|
appValidation.name(validation, { apps })
|
||||||
.trim()
|
appValidation.url(validation, { apps })
|
||||||
.required("Your application must have a name")
|
appValidation.file(validation, { template })
|
||||||
.matches(APP_NAME_REGEX, "App name must be letters and numbers only")
|
// init validation
|
||||||
.test(
|
validation.check($values)
|
||||||
"non-existing-app-name",
|
|
||||||
"Another app with the same name already exists",
|
|
||||||
value => {
|
|
||||||
return !existingAppNames.some(
|
|
||||||
appName => appName.toLowerCase() === value.toLowerCase()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const checkValidity = async (values, validator) => {
|
|
||||||
const obj = object().shape(validator)
|
|
||||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
|
||||||
if (template?.fromFile && values.file == null) {
|
|
||||||
valid = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 createNewApp() {
|
async function createNewApp() {
|
||||||
const templateToUse = Object.keys(template).length === 0 ? null : template
|
|
||||||
submitting = true
|
|
||||||
|
|
||||||
// Check a template exists if we are important
|
|
||||||
if (templateToUse?.fromFile && !$values.file) {
|
|
||||||
$errors.file = "Please choose a file to import"
|
|
||||||
valid = false
|
|
||||||
submitting = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create form data to create app
|
// Create form data to create app
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("name", $values.name.trim())
|
data.append("name", $values.name.trim())
|
||||||
data.append("useTemplate", templateToUse != null)
|
if ($values.url) {
|
||||||
if (templateToUse) {
|
data.append("url", $values.url.trim())
|
||||||
data.append("templateName", templateToUse.name)
|
}
|
||||||
data.append("templateKey", templateToUse.key)
|
data.append("useTemplate", template != null)
|
||||||
|
if (template) {
|
||||||
|
data.append("templateName", template.name)
|
||||||
|
data.append("templateKey", template.key)
|
||||||
data.append("templateFile", $values.file)
|
data.append("templateFile", $values.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +56,7 @@
|
||||||
analytics.captureEvent(Events.APP.CREATED, {
|
analytics.captureEvent(Events.APP.CREATED, {
|
||||||
name: $values.name,
|
name: $values.name,
|
||||||
appId: appJson.instance._id,
|
appId: appJson.instance._id,
|
||||||
templateToUse,
|
templateToUse: template,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select Correct Application/DB in prep for creating user
|
// Select Correct Application/DB in prep for creating user
|
||||||
|
@ -137,7 +84,6 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
submitting = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,60 +91,50 @@
|
||||||
template = null
|
template = null
|
||||||
await auth.setInitInfo({})
|
await auth.setInitInfo({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// auto add slash to url
|
||||||
|
$: {
|
||||||
|
if ($values.url && !$values.url.startsWith("/")) {
|
||||||
|
$values.url = `/${$values.url}`
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showTemplateSelection}
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={"Get started quickly"}
|
title={"Create your app"}
|
||||||
showConfirmButton={false}
|
|
||||||
size="L"
|
|
||||||
onConfirm={() => {
|
|
||||||
template = {}
|
|
||||||
return false
|
|
||||||
}}
|
|
||||||
showCancelButton={!inline}
|
|
||||||
showCloseIcon={!inline}
|
|
||||||
>
|
|
||||||
<TemplateList
|
|
||||||
onSelect={(selected, { useImport } = {}) => {
|
|
||||||
if (!selected) {
|
|
||||||
template = useImport ? { fromFile: true } : {}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
template = selected
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
{:else}
|
|
||||||
<ModalContent
|
|
||||||
title={"Name your app"}
|
|
||||||
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
||||||
onConfirm={createNewApp}
|
onConfirm={createNewApp}
|
||||||
onCancel={inline ? onCancel : null}
|
{onCancel}
|
||||||
cancelText={inline ? "Back" : undefined}
|
disabled={!$validation.valid}
|
||||||
showCloseIcon={!inline}
|
|
||||||
disabled={!valid}
|
|
||||||
>
|
>
|
||||||
{#if template?.fromFile}
|
{#if template?.fromFile}
|
||||||
<Dropzone
|
<Dropzone
|
||||||
error={$touched.file && $errors.file}
|
error={$validation.touched.file && $validation.errors.file}
|
||||||
gallery={false}
|
gallery={false}
|
||||||
label="File to import"
|
label="File to import"
|
||||||
value={[$values.file]}
|
value={[$values.file]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
$values.file = e.detail?.[0]
|
$values.file = e.detail?.[0]
|
||||||
$touched.file = true
|
$validation.touched.file = true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Input
|
<Input
|
||||||
bind:value={$values.name}
|
bind:value={$values.name}
|
||||||
error={$touched.name && $errors.name}
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
on:blur={() => ($touched.name = true)}
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
label="Name"
|
label="Name"
|
||||||
placeholder={$auth.user.firstName
|
placeholder={$auth.user.firstName
|
||||||
? `${$auth.user.firstName}'s app`
|
? `${$auth.user.firstName}s app`
|
||||||
: "My app"}
|
: "My app"}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.url}
|
||||||
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
label="URL"
|
||||||
|
placeholder={$values.name
|
||||||
|
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||||
|
: "/"}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
{/if}
|
|
||||||
|
|
|
@ -1,120 +1,71 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
import { writable, get as svelteGet } from "svelte/store"
|
||||||
import {
|
import { notifications, Input, ModalContent, Body } from "@budibase/bbui"
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Body,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { hostingStore } from "builderStore"
|
import { hostingStore } from "builderStore"
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
import { string, object } from "yup"
|
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { capitalise } from "helpers"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import { APP_NAME_REGEX } from "constants"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
|
||||||
const values = writable({ name: null })
|
|
||||||
const errors = writable({})
|
|
||||||
const touched = writable({})
|
|
||||||
const validator = {
|
|
||||||
name: string()
|
|
||||||
.trim()
|
|
||||||
.required("Your application must have a name")
|
|
||||||
.matches(
|
|
||||||
APP_NAME_REGEX,
|
|
||||||
"App name must be letters, numbers and spaces only"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
|
||||||
let modal
|
const values = writable({ name: "", url: null })
|
||||||
let valid = false
|
const validation = createValidationStore()
|
||||||
let dirty = false
|
$: validation.check($values)
|
||||||
$: checkValidity($values, validator)
|
|
||||||
$: {
|
|
||||||
// prevent validation by setting name to undefined without an app
|
|
||||||
if (app) {
|
|
||||||
$values.name = app?.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await hostingStore.actions.fetchDeployedApps()
|
$values.name = app.name
|
||||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
$values.url = app.url
|
||||||
validator.name = string()
|
setupValidation()
|
||||||
.trim()
|
|
||||||
.required("Your application must have a name")
|
|
||||||
.matches(
|
|
||||||
APP_NAME_REGEX,
|
|
||||||
"App name must be letters, numbers and spaces only"
|
|
||||||
)
|
|
||||||
.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 setupValidation = async () => {
|
||||||
const obj = object().shape(validator)
|
await hostingStore.actions.fetchDeployedApps()
|
||||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
const apps = svelteGet(hostingStore).deployedApps
|
||||||
try {
|
appValidation.name(validation, { apps, currentApp: app })
|
||||||
await obj.validate(values, { abortEarly: false })
|
appValidation.url(validation, { apps, currentApp: app })
|
||||||
} catch (validationErrors) {
|
// init validation
|
||||||
validationErrors.inner.forEach(error => {
|
validation.check($values)
|
||||||
$errors[error.path] = capitalise(error.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
valid = await obj.isValid(values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateApp() {
|
async function updateApp() {
|
||||||
try {
|
try {
|
||||||
// Update App
|
// Update App
|
||||||
await apps.update(app.instance._id, { name: $values.name.trim() })
|
await apps.update(app.instance._id, { name: $values.name.trim() })
|
||||||
hide()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const show = () => {
|
// auto add slash to url
|
||||||
modal.show()
|
$: {
|
||||||
|
if ($values.url && !$values.url.startsWith("/")) {
|
||||||
|
$values.url = `/${$values.url}`
|
||||||
}
|
}
|
||||||
export const hide = () => {
|
|
||||||
modal.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onShow = () => {
|
|
||||||
dirty = false
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}>
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={"Edit app"}
|
title={"Edit app"}
|
||||||
confirmText={"Save"}
|
confirmText={"Save"}
|
||||||
onConfirm={updateApp}
|
onConfirm={updateApp}
|
||||||
disabled={!(valid && dirty)}
|
disabled={!$validation.valid}
|
||||||
>
|
>
|
||||||
<Body size="S">Update the name of your app.</Body>
|
<Body size="S">Update the name of your app.</Body>
|
||||||
<Input
|
<Input
|
||||||
bind:value={$values.name}
|
bind:value={$values.name}
|
||||||
error={$touched.name && $errors.name}
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
on:blur={() => ($touched.name = true)}
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
on:change={() => (dirty = true)}
|
|
||||||
label="Name"
|
label="Name"
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.url}
|
||||||
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
label="URL"
|
||||||
|
placeholder={$values.name
|
||||||
|
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||||
|
: "/"}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
|
||||||
|
|
|
@ -36,4 +36,7 @@ export const LAYOUT_NAMES = {
|
||||||
|
|
||||||
export const BUDIBASE_INTERNAL_DB = "bb_internal"
|
export const BUDIBASE_INTERNAL_DB = "bb_internal"
|
||||||
|
|
||||||
|
// one or more word characters and whitespace
|
||||||
export const APP_NAME_REGEX = /^[\w\s]+$/
|
export const APP_NAME_REGEX = /^[\w\s]+$/
|
||||||
|
// zero or more non-whitespace characters
|
||||||
|
export const APP_URL_REGEX = /^\S*$/
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { string, mixed } from "yup"
|
||||||
|
import { APP_NAME_REGEX, APP_URL_REGEX } from "constants"
|
||||||
|
|
||||||
|
export const name = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||||
|
let existingApps = Object.values(apps)
|
||||||
|
validation.addValidator(
|
||||||
|
"name",
|
||||||
|
string()
|
||||||
|
.required("Your application must have a name")
|
||||||
|
.matches(APP_NAME_REGEX, "App name must be letters and numbers only")
|
||||||
|
.test(
|
||||||
|
"non-existing-app-name",
|
||||||
|
"Another app with the same name already exists",
|
||||||
|
value => {
|
||||||
|
if (!value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (currentApp) {
|
||||||
|
// filter out the current app if present
|
||||||
|
existingApps = existingApps
|
||||||
|
// match the id format of the current app (remove 'app_')
|
||||||
|
.map(app => ({ ...app, appId: app.appId.substring(4) }))
|
||||||
|
.filter(app => app.appId !== currentApp.appId)
|
||||||
|
}
|
||||||
|
return !existingApps
|
||||||
|
.map(app => app.name)
|
||||||
|
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const url = (validation, { apps, currentApp } = { apps: {} }) => {
|
||||||
|
let existingApps = Object.values(apps)
|
||||||
|
validation.addValidator(
|
||||||
|
"url",
|
||||||
|
string()
|
||||||
|
.nullable()
|
||||||
|
.matches(APP_URL_REGEX, "App URL must not contain spaces")
|
||||||
|
.test(
|
||||||
|
"non-existing-app-url",
|
||||||
|
"Another app with the same URL already exists",
|
||||||
|
value => {
|
||||||
|
// url is nullable
|
||||||
|
if (!value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (currentApp) {
|
||||||
|
existingApps = existingApps
|
||||||
|
// match the id format of the current app (remove 'app_')
|
||||||
|
.map(app => ({ ...app, appId: app.appId.substring(4) }))
|
||||||
|
// filter out the current app if present
|
||||||
|
.filter(app => app.appId !== currentApp.appId)
|
||||||
|
}
|
||||||
|
return !existingApps
|
||||||
|
.map(app => app.url)
|
||||||
|
.some(appUrl => appUrl.toLowerCase() === value.toLowerCase())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.test("start-with-slash", "Not a valid URL", value => {
|
||||||
|
// url is nullable
|
||||||
|
if (!value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return value.length > 1
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const file = (validation, { template } = {}) => {
|
||||||
|
const templateToUse =
|
||||||
|
template && Object.keys(template).length === 0 ? null : template
|
||||||
|
validation.addValidator(
|
||||||
|
"file",
|
||||||
|
templateToUse?.fromFile
|
||||||
|
? mixed().required("Please choose a file to import")
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { object } from "yup"
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
|
export const createValidationStore = () => {
|
||||||
|
const DEFAULT = {
|
||||||
|
errors: {},
|
||||||
|
touched: {},
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = {}
|
||||||
|
const validation = writable(DEFAULT)
|
||||||
|
|
||||||
|
const addValidator = (propertyName, propertyValidator) => {
|
||||||
|
if (!propertyValidator || !propertyName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
validator[propertyName] = propertyValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = async values => {
|
||||||
|
const obj = object().shape(validator)
|
||||||
|
// clear the previous errors
|
||||||
|
const properties = Object.keys(validator)
|
||||||
|
properties.forEach(property => (get(validation).errors[property] = null))
|
||||||
|
try {
|
||||||
|
await obj.validate(values, { abortEarly: false })
|
||||||
|
} catch (error) {
|
||||||
|
error.inner.forEach(err => {
|
||||||
|
validation.update(store => {
|
||||||
|
store.errors[err.path] = capitalise(err.message)
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid
|
||||||
|
if (properties.length) {
|
||||||
|
valid = await obj.isValid(values)
|
||||||
|
} else {
|
||||||
|
// don't say valid until validators have been loaded
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
validation.update(store => {
|
||||||
|
store.valid = valid
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: validation.subscribe,
|
||||||
|
set: validation.set,
|
||||||
|
check,
|
||||||
|
addValidator,
|
||||||
|
}
|
||||||
|
}
|
|
@ -412,6 +412,11 @@
|
||||||
>
|
>
|
||||||
<CreateAppModal {template} />
|
<CreateAppModal {template} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal app={selectedApp} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={deletionModal}
|
bind:this={deletionModal}
|
||||||
title="Confirm deletion"
|
title="Confirm deletion"
|
||||||
|
@ -438,7 +443,6 @@
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<UpdateAppModal app={selectedApp} bind:this={updatingModal} />
|
|
||||||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -78,6 +78,11 @@ class QueryRunner {
|
||||||
return this.execute()
|
return this.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for undefined response
|
||||||
|
if (!rows) {
|
||||||
|
rows = []
|
||||||
|
}
|
||||||
|
|
||||||
// needs to an array for next step
|
// needs to an array for next step
|
||||||
if (!Array.isArray(rows)) {
|
if (!Array.isArray(rows)) {
|
||||||
rows = [rows]
|
rows = [rows]
|
||||||
|
|
Loading…
Reference in New Issue