Added updated UX for updating app metadata.

This commit is contained in:
Dean 2024-05-14 10:48:20 +01:00
parent fcb5d88eaf
commit 1602e97047
7 changed files with 309 additions and 265 deletions

View File

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

View File

@ -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">
<div class="hover" on:click={modal.show}>
<Icon name="Edit" {size} color="var(--spectrum-global-color-gray-600)" />
</div>
<div class="normal">
{#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={name || "Apps"} {size} {color} />
</div>
{:else}
<Icon {name} {size} {color} />
</div>
{/if}
</div>
<Modal bind:this={modal}>

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

View File

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

View File

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

View File

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

View File

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