forked from tsai/budibase
10 changed files with 282 additions and 243 deletions
@ -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("/")) { |
||||
export const hide = () => { |
$values.url = `/${$values.url}` |
||||
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={!$validation.valid} |
||||
disabled={!(valid && dirty)} |
> |
||||
> |
<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={$validation.touched.name && $validation.errors.name} |
||||
error={$touched.name && $errors.name} |
on:blur={() => ($validation.touched.name = true)} |
||||
on:blur={() => ($touched.name = true)} |
label="Name" |
||||
on:change={() => (dirty = true)} |
/> |
||||
label="Name" |
<Input |
||||
/> |
bind:value={$values.url} |
||||
</ModalContent> |
error={$validation.touched.url && $validation.errors.url} |
||||
</Modal> |
on:blur={() => ($validation.touched.url = true)} |
||||
|
label="URL" |
||||
|
placeholder={$values.name |
||||
|
? "/" + encodeURIComponent($values.name).toLowerCase() |
||||
|
: "/"} |
||||
|
/> |
||||
|
</ModalContent> |
||||
|
|||||
@ -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, |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue