forked from tsai/budibase
10 changed files with 282 additions and 243 deletions
@ -1,120 +1,71 @@ |
|||
<script> |
|||
import { writable, get as svelteGet } from "svelte/store" |
|||
import { |
|||
notifications, |
|||
Input, |
|||
Modal, |
|||
ModalContent, |
|||
Body, |
|||
} from "@budibase/bbui" |
|||
import { notifications, Input, 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" |
|||
import { APP_NAME_REGEX } from "constants" |
|||
|
|||
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" |
|||
), |
|||
} |
|||
import { createValidationStore } from "helpers/validation/yup" |
|||
import * as appValidation from "helpers/validation/yup/app" |
|||
|
|||
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 |
|||
} |
|||
} |
|||
const values = writable({ name: "", url: null }) |
|||
const validation = createValidationStore() |
|||
$: validation.check($values) |
|||
|
|||
onMount(async () => { |
|||
await hostingStore.actions.fetchDeployedApps() |
|||
const existingAppNames = svelteGet(hostingStore).deployedAppNames |
|||
validator.name = string() |
|||
.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() |
|||
) |
|||
} |
|||
) |
|||
$values.name = app.name |
|||
$values.url = app.url |
|||
setupValidation() |
|||
}) |
|||
|
|||
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) |
|||
const setupValidation = async () => { |
|||
await hostingStore.actions.fetchDeployedApps() |
|||
const apps = svelteGet(hostingStore).deployedApps |
|||
appValidation.name(validation, { apps, currentApp: app }) |
|||
appValidation.url(validation, { apps, currentApp: app }) |
|||
// init validation |
|||
validation.check($values) |
|||
} |
|||
|
|||
async function updateApp() { |
|||
try { |
|||
// Update App |
|||
await apps.update(app.instance._id, { name: $values.name.trim() }) |
|||
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 |
|||
// auto add slash to url |
|||
$: { |
|||
if ($values.url && !$values.url.startsWith("/")) { |
|||
$values.url = `/${$values.url}` |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}> |
|||
<ModalContent |
|||
title={"Edit app"} |
|||
confirmText={"Save"} |
|||
onConfirm={updateApp} |
|||
disabled={!(valid && dirty)} |
|||
> |
|||
<Body size="S">Update the name of your app.</Body> |
|||
<Input |
|||
bind:value={$values.name} |
|||
error={$touched.name && $errors.name} |
|||
on:blur={() => ($touched.name = true)} |
|||
on:change={() => (dirty = true)} |
|||
label="Name" |
|||
/> |
|||
</ModalContent> |
|||
</Modal> |
|||
<ModalContent |
|||
title={"Edit app"} |
|||
confirmText={"Save"} |
|||
onConfirm={updateApp} |
|||
disabled={!$validation.valid} |
|||
> |
|||
<Body size="S">Update the name of your app.</Body> |
|||
<Input |
|||
bind:value={$values.name} |
|||
error={$validation.touched.name && $validation.errors.name} |
|||
on:blur={() => ($validation.touched.name = true)} |
|||
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> |
|||
|
|||
@ -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