Added updated UX for updating app metadata.
This commit is contained in:
parent
fcb5d88eaf
commit
1602e97047
7 changed files with 309 additions and 265 deletions
|
@ -155,6 +155,8 @@ export default function positionDropdown(element, opts) {
|
|||
applyXStrategy(Strategies.StartToEnd)
|
||||
} else if (align === "left-outside") {
|
||||
applyXStrategy(Strategies.EndToStart)
|
||||
} else if (align === "center") {
|
||||
applyXStrategy(Strategies.MidPoint)
|
||||
} else {
|
||||
applyXStrategy(Strategies.StartToStart)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
export let app
|
||||
export let color
|
||||
export let autoSave = false
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
@ -14,12 +15,16 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="editable-icon">
|
||||
{#if !disabled}
|
||||
<div class="hover" on:click={modal.show}>
|
||||
<Icon name="Edit" {size} color="var(--spectrum-global-color-gray-600)" />
|
||||
</div>
|
||||
<div class="normal">
|
||||
<Icon {name} {size} {color} />
|
||||
<Icon name={name || "Apps"} {size} {color} />
|
||||
</div>
|
||||
{:else}
|
||||
<Icon {name} {size} {color} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
|
217
packages/builder/src/components/common/UpdateAppForm.svelte
Normal file
217
packages/builder/src/components/common/UpdateAppForm.svelte
Normal file
|
@ -0,0 +1,217 @@
|
|||
<script>
|
||||
import { Button, Label, Icon, Input, notifications } from "@budibase/bbui"
|
||||
import { AppStatus } from "constants"
|
||||
import { appStore, initialise } from "stores/builder"
|
||||
import { appsStore } from "stores/portal"
|
||||
import { API } from "api"
|
||||
import { writable } from "svelte/store"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||
import { isEqual } from "lodash"
|
||||
|
||||
export let alignActions = "left"
|
||||
|
||||
const values = writable({})
|
||||
const validation = createValidationStore()
|
||||
|
||||
let updating = false
|
||||
let edited = false
|
||||
|
||||
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||
|
||||
$: appIdParts = app.appId ? app.appId?.split("_") : []
|
||||
$: appId = appIdParts.slice(-1)[0]
|
||||
|
||||
$: appMeta = {
|
||||
name: $appStore.name,
|
||||
url: $appStore.url,
|
||||
iconName: $appStore.icon?.name,
|
||||
iconColor: $appStore.icon?.color,
|
||||
}
|
||||
|
||||
// On app update, reset the state.
|
||||
$: if (appMeta) {
|
||||
edited = false
|
||||
const { name, url, iconName, iconColor } = appMeta
|
||||
values.set({
|
||||
name,
|
||||
url,
|
||||
iconName,
|
||||
iconColor,
|
||||
})
|
||||
|
||||
setupValidation()
|
||||
}
|
||||
|
||||
$: if (values && $values) {
|
||||
const { url } = $values || {}
|
||||
validation.check({
|
||||
...$values,
|
||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||
})
|
||||
edited = !isEqual($values, appMeta)
|
||||
}
|
||||
|
||||
const resolveAppUrl = (template, name) => {
|
||||
let parsedName
|
||||
const resolvedName = resolveAppName(null, name)
|
||||
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
||||
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
||||
return encodeURI(parsedUrl)
|
||||
}
|
||||
|
||||
const nameToUrl = appName => {
|
||||
let resolvedUrl = resolveAppUrl(null, appName)
|
||||
tidyUrl(resolvedUrl)
|
||||
}
|
||||
|
||||
const resolveAppName = (template, name) => {
|
||||
if (template && !name) {
|
||||
return template.name
|
||||
}
|
||||
return name ? name.trim() : null
|
||||
}
|
||||
|
||||
const tidyUrl = url => {
|
||||
if (url && !url.startsWith("/")) {
|
||||
url = `/${url}`
|
||||
}
|
||||
$values.url = url === "" ? null : url
|
||||
}
|
||||
|
||||
const updateIcon = e => {
|
||||
const { name, color } = e.detail
|
||||
$values.iconColor = color
|
||||
$values.iconName = name
|
||||
}
|
||||
|
||||
const setupValidation = async () => {
|
||||
const applications = $appsStore.apps
|
||||
appValidation.name(validation, {
|
||||
apps: applications,
|
||||
currentApp: {
|
||||
...app,
|
||||
appId,
|
||||
},
|
||||
})
|
||||
appValidation.url(validation, {
|
||||
apps: applications,
|
||||
currentApp: {
|
||||
...app,
|
||||
appId,
|
||||
},
|
||||
})
|
||||
// init validation
|
||||
const { url } = $values
|
||||
validation.check({
|
||||
...$values,
|
||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||
})
|
||||
}
|
||||
|
||||
async function updateApp() {
|
||||
try {
|
||||
await appsStore.save($appStore.appId, {
|
||||
name: $values.name?.trim(),
|
||||
url: $values.url?.trim(),
|
||||
icon: {
|
||||
name: $values.iconName,
|
||||
color: $values.iconColor,
|
||||
},
|
||||
})
|
||||
|
||||
await initialiseApp()
|
||||
notifications.success("App update successful")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error updating app")
|
||||
}
|
||||
}
|
||||
|
||||
const initialiseApp = async () => {
|
||||
const applicationPkg = await API.fetchAppPackage($appStore.appId)
|
||||
await initialise(applicationPkg)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form">
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Name</Label>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
on:change={nameToUrl($values.name)}
|
||||
disabled={appDeployed}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<Label size="L">URL</Label>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
on:change={tidyUrl($values.url)}
|
||||
placeholder={$values.url
|
||||
? $values.url
|
||||
: `/${resolveAppUrl(null, $values.name)}`}
|
||||
disabled={appDeployed}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<Label size="L">Icon</Label>
|
||||
<EditableIcon
|
||||
{app}
|
||||
size="XL"
|
||||
name={$values.iconName}
|
||||
color={$values.iconColor}
|
||||
on:change={updateIcon}
|
||||
disabled={appDeployed}
|
||||
/>
|
||||
</div>
|
||||
<div class="actions" class:right={alignActions === "right"}>
|
||||
{#if !appDeployed}
|
||||
<Button
|
||||
cta
|
||||
on:click={async () => {
|
||||
updating = true
|
||||
await updateApp()
|
||||
updating = false
|
||||
}}
|
||||
disabled={appDeployed || updating || !edited}>Save</Button
|
||||
>
|
||||
{:else}
|
||||
<div class="edit-info">
|
||||
<Icon size="S" name="Info" /> Unpublish your app to edit name and URL
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
.actions.right {
|
||||
justify-content: end;
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-l);
|
||||
}
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 220px;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
.edit-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
<script>
|
||||
import { Popover, Layout, Icon } from "@budibase/bbui"
|
||||
import { deploymentStore } from "stores/builder"
|
||||
import { appsStore } from "stores/portal"
|
||||
import UpdateAppForm from "./UpdateAppForm.svelte"
|
||||
|
||||
export let application
|
||||
|
||||
let formPopover
|
||||
let formPopoverAnchor
|
||||
let formPopoverOpen = false
|
||||
|
||||
$: filteredApps = $appsStore.apps.filter(app => app.devId === application)
|
||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||
$: latestDeployments = $deploymentStore
|
||||
.filter(deployment => deployment.status === "SUCCESS")
|
||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||
$: isPublished =
|
||||
selectedApp?.status === "published" && latestDeployments?.length > 0
|
||||
</script>
|
||||
|
||||
<div bind:this={formPopoverAnchor}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="app-heading"
|
||||
class:editing={formPopoverOpen}
|
||||
on:click={() => {
|
||||
formPopover.show()
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
<span class="edit-icon">
|
||||
<Icon size="S" name="Edit" color={"var(--grey-7)"} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
customZindex={998}
|
||||
bind:this={formPopover}
|
||||
align="center"
|
||||
disabled={!isPublished}
|
||||
anchor={formPopoverAnchor}
|
||||
offset={20}
|
||||
on:close={() => {
|
||||
formPopoverOpen = false
|
||||
}}
|
||||
on:open={() => {
|
||||
formPopoverOpen = true
|
||||
}}
|
||||
>
|
||||
<Layout noPadding gap="M">
|
||||
<div class="popover-content">
|
||||
<UpdateAppForm />
|
||||
</div>
|
||||
</Layout>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.popover-content {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
.app-heading {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
.edit-icon {
|
||||
display: none;
|
||||
}
|
||||
.app-heading:hover .edit-icon,
|
||||
.app-heading.editing .edit-icon {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
|
@ -8,13 +8,11 @@
|
|||
ActionButton,
|
||||
Icon,
|
||||
Link,
|
||||
Modal,
|
||||
StatusLight,
|
||||
AbsTooltip,
|
||||
} from "@budibase/bbui"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import analytics, { Events, EventSource } from "analytics"
|
||||
|
@ -37,7 +35,6 @@
|
|||
export let loaded
|
||||
|
||||
let unpublishModal
|
||||
let updateAppModal
|
||||
let revertModal
|
||||
let versionModal
|
||||
let appActionPopover
|
||||
|
@ -61,11 +58,6 @@
|
|||
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
||||
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
|
||||
|
||||
const initialiseApp = async () => {
|
||||
const applicationPkg = await API.fetchAppPackage($appStore.devId)
|
||||
await initialise(applicationPkg)
|
||||
}
|
||||
|
||||
const getLastDeployedString = deployments => {
|
||||
return deployments?.length
|
||||
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||
|
@ -247,16 +239,12 @@
|
|||
appActionPopover.hide()
|
||||
if (isPublished) {
|
||||
viewApp()
|
||||
} else {
|
||||
updateAppModal.show()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$appStore.url}
|
||||
{#if isPublished}
|
||||
<Icon size="S" name="LinkOut" />
|
||||
{:else}
|
||||
<Icon size="S" name="Edit" />
|
||||
{/if}
|
||||
</span>
|
||||
</Body>
|
||||
|
@ -330,20 +318,6 @@
|
|||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
|
||||
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
||||
<UpdateAppModal
|
||||
app={{
|
||||
name: $appStore.name,
|
||||
url: $appStore.url,
|
||||
icon: $appStore.icon,
|
||||
appId: $appStore.appId,
|
||||
}}
|
||||
onUpdateComplete={async () => {
|
||||
await initialiseApp()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<RevertModal bind:this={revertModal} />
|
||||
<VersionModal hideIcon bind:this={versionModal} />
|
||||
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
ModalContent,
|
||||
Layout,
|
||||
Label,
|
||||
} from "@budibase/bbui"
|
||||
import { appsStore } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
import EditableIcon from "../common/EditableIcon.svelte"
|
||||
|
||||
export let app
|
||||
export let onUpdateComplete
|
||||
|
||||
$: appIdParts = app.appId ? app.appId?.split("_") : []
|
||||
$: appId = appIdParts.slice(-1)[0]
|
||||
|
||||
const values = writable({
|
||||
name: app.name,
|
||||
url: app.url,
|
||||
iconName: app.icon?.name,
|
||||
iconColor: app.icon?.color,
|
||||
})
|
||||
const validation = createValidationStore()
|
||||
|
||||
$: {
|
||||
const { url } = $values
|
||||
|
||||
validation.check({
|
||||
...$values,
|
||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||
})
|
||||
}
|
||||
|
||||
const setupValidation = async () => {
|
||||
const applications = svelteGet(appsStore).apps
|
||||
appValidation.name(validation, {
|
||||
apps: applications,
|
||||
currentApp: {
|
||||
...app,
|
||||
appId,
|
||||
},
|
||||
})
|
||||
appValidation.url(validation, {
|
||||
apps: applications,
|
||||
currentApp: {
|
||||
...app,
|
||||
appId,
|
||||
},
|
||||
})
|
||||
// init validation
|
||||
const { url } = $values
|
||||
validation.check({
|
||||
...$values,
|
||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||
})
|
||||
}
|
||||
|
||||
async function updateApp() {
|
||||
try {
|
||||
await appsStore.save(app.appId, {
|
||||
name: $values.name?.trim(),
|
||||
url: $values.url?.trim(),
|
||||
icon: {
|
||||
name: $values.iconName,
|
||||
color: $values.iconColor,
|
||||
},
|
||||
})
|
||||
if (typeof onUpdateComplete == "function") {
|
||||
onUpdateComplete()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error updating app")
|
||||
}
|
||||
}
|
||||
|
||||
const resolveAppUrl = (template, name) => {
|
||||
let parsedName
|
||||
const resolvedName = resolveAppName(null, name)
|
||||
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
||||
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
||||
return encodeURI(parsedUrl)
|
||||
}
|
||||
|
||||
const resolveAppName = (template, name) => {
|
||||
if (template && !name) {
|
||||
return template.name
|
||||
}
|
||||
return name ? name.trim() : null
|
||||
}
|
||||
|
||||
const tidyUrl = url => {
|
||||
if (url && !url.startsWith("/")) {
|
||||
url = `/${url}`
|
||||
}
|
||||
$values.url = url === "" ? null : url
|
||||
}
|
||||
|
||||
const nameToUrl = appName => {
|
||||
let resolvedUrl = resolveAppUrl(null, appName)
|
||||
tidyUrl(resolvedUrl)
|
||||
}
|
||||
|
||||
const updateIcon = e => {
|
||||
const { name, color } = e.detail
|
||||
$values.iconColor = color
|
||||
$values.iconName = name
|
||||
}
|
||||
|
||||
onMount(setupValidation)
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Edit name and URL"
|
||||
confirmText="Save"
|
||||
onConfirm={updateApp}
|
||||
disabled={!$validation.valid}
|
||||
>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
on:change={nameToUrl($values.name)}
|
||||
label="Name"
|
||||
/>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Icon</Label>
|
||||
<EditableIcon
|
||||
{app}
|
||||
size="XL"
|
||||
name={$values.iconName}
|
||||
color={$values.iconColor}
|
||||
on:change={updateIcon}
|
||||
/>
|
||||
</Layout>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
on:change={tidyUrl($values.url)}
|
||||
label="URL"
|
||||
placeholder={$values.url
|
||||
? $values.url
|
||||
: `/${resolveAppUrl(null, $values.name)}`}
|
||||
/>
|
||||
</ModalContent>
|
|
@ -1,30 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Divider,
|
||||
Heading,
|
||||
Body,
|
||||
Button,
|
||||
Label,
|
||||
Modal,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { AppStatus } from "constants"
|
||||
import { appStore, initialise } from "stores/builder"
|
||||
import { appsStore } from "stores/portal"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
import { API } from "api"
|
||||
|
||||
let updatingModal
|
||||
|
||||
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||
|
||||
const initialiseApp = async () => {
|
||||
const applicationPkg = await API.fetchAppPackage($appStore.appId)
|
||||
await initialise(applicationPkg)
|
||||
}
|
||||
import { Layout, Divider, Heading, Body } from "@budibase/bbui"
|
||||
import UpdateAppForm from "components/common/UpdateAppForm.svelte"
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
|
@ -33,61 +9,5 @@
|
|||
<Body>Edit your app's name and URL</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
|
||||
<Layout noPadding gap="XXS">
|
||||
<Label size="L">Name</Label>
|
||||
<Body>{$appStore?.name}</Body>
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="XS">
|
||||
<Label size="L">Icon</Label>
|
||||
<div class="icon">
|
||||
<Icon
|
||||
size="L"
|
||||
name={$appStore?.icon?.name || "Apps"}
|
||||
color={$appStore?.icon?.color}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="XXS">
|
||||
<Label size="L">URL</Label>
|
||||
<Body>{$appStore.url}</Body>
|
||||
</Layout>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
updatingModal.show()
|
||||
}}
|
||||
disabled={appDeployed}
|
||||
tooltip={appDeployed
|
||||
? "You must unpublish your app to make changes"
|
||||
: null}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
<UpdateAppForm />
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||
<UpdateAppModal
|
||||
app={{
|
||||
name: $appStore.name,
|
||||
url: $appStore.url,
|
||||
icon: $appStore.icon,
|
||||
appId: $appStore.appId,
|
||||
}}
|
||||
onUpdateComplete={async () => {
|
||||
await initialiseApp()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue