Simplify app layout and add option to delete apps

This commit is contained in:
Andrew Kingston 2021-05-10 12:28:54 +01:00
parent df607e78eb
commit 0b1b4d2aae
11 changed files with 174 additions and 382 deletions

View File

@ -7,6 +7,7 @@
export let cancelText = "Cancel" export let cancelText = "Cancel"
export let onOk = undefined export let onOk = undefined
export let onCancel = undefined export let onCancel = undefined
export let warning = true
let modal let modal
@ -19,7 +20,13 @@
</script> </script>
<Modal bind:this={modal} on:hide={onCancel}> <Modal bind:this={modal} on:hide={onCancel}>
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red> <ModalContent
onConfirm={onOk}
{title}
confirmText={okText}
{cancelText}
{warning}
>
<Body size="S"> <Body size="S">
{body} {body}
<slot /> <slot />

View File

@ -13,8 +13,7 @@
export let app export let app
export let exportApp export let exportApp
export let deleteApp
let appExportLoading = false
</script> </script>
<div class="wrapper"> <div class="wrapper">
@ -28,9 +27,12 @@
</Link> </Link>
<ActionMenu align="right"> <ActionMenu align="right">
<Icon slot="control" name="More" hoverable /> <Icon slot="control" name="More" hoverable />
<MenuItem on:click={() => exportApp(app)} icon="Download" <MenuItem on:click={() => exportApp(app)} icon="Download">
>Export</MenuItem Export
> </MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">
Delete
</MenuItem>
</ActionMenu> </ActionMenu>
</div> </div>
<div class="status"> <div class="status">

View File

@ -1,22 +0,0 @@
<script>
import AppCard from "./AppCard.svelte"
import { apps } from "stores/portal"
export let exportApp
</script>
{#if $apps.length}
<div class="appList">
{#each $apps as app}
<AppCard {exportApp} {app} />
{/each}
</div>
{/if}
<style>
.appList {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
</style>

View File

@ -0,0 +1,80 @@
<script>
import { gradient } from "actions"
import {
Heading,
Button,
Icon,
ActionMenu,
MenuItem,
Link,
} from "@budibase/bbui"
import { url } from "@roxi/routify"
export let app
export let openApp
export let exportApp
export let deleteApp
export let last
</script>
<div class="title" class:last>
<div class="preview" use:gradient />
<Link href={$url(`../../app/${app._id}`)}>
<Heading size="XS">
{app.name}
</Heading>
</Link>
</div>
<div class:last>
Edited {Math.round(Math.random() * 10 + 1)} months ago
</div>
<div class:last>
{#if Math.random() < 0.33}
<div class="status status--open" />
Open
{:else if Math.random() < 0.33}
<div class="status status--locked-other" />
Locked by Will Wheaton
{:else}
<div class="status status--locked-you" />
Locked by you
{/if}
</div>
<div class:last>
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button>
<ActionMenu align="right">
<Icon hoverable slot="control" name="More" />
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
</ActionMenu>
</div>
<style>
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.title :global(a) {
text-decoration: none;
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
.status {
height: 10px;
width: 10px;
border-radius: 50%;
}
.status--locked-you {
background-color: var(--spectrum-global-color-orange-600);
}
.status--locked-other {
background-color: var(--spectrum-global-color-red-600);
}
.status--open {
background-color: var(--spectrum-global-color-green-600);
}
</style>

View File

@ -1,106 +0,0 @@
<script>
import { apps } from "stores/portal"
import { gradient } from "actions"
import {
Heading,
Button,
Icon,
ActionMenu,
MenuItem,
Link,
} from "@budibase/bbui"
import { goto, url } from "@roxi/routify"
export let exportApp
const openApp = app => {
$goto(`../../app/${app._id}`)
}
</script>
{#if $apps.length}
<div class="appTable">
{#each $apps as app}
<div class="title">
<div class="preview" use:gradient />
<Link href={$url(`../../app/${app._id}`)}>
<Heading size="XS">
{app.name}
</Heading>
</Link>
</div>
<div>
Edited {Math.round(Math.random() * 10 + 1)} months ago
</div>
<div>
{#if Math.random() < 0.33}
<div class="status status--open" />
Open
{:else if Math.random() < 0.33}
<div class="status status--locked-other" />
Locked by Will Wheaton
{:else}
<div class="status status--locked-you" />
Locked by you
{/if}
</div>
<div>
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button>
<ActionMenu align="right">
<Icon hoverable slot="control" name="More" />
<MenuItem on:click={() => exportApp(app)} icon="Download">
Export
</MenuItem>
</ActionMenu>
</div>
{/each}
</div>
{/if}
<style>
.appTable {
display: grid;
grid-template-rows: auto;
grid-template-columns: 1fr 1fr 1fr auto;
align-items: center;
}
.appTable > div {
border-bottom: var(--border-light);
height: 70px;
display: grid;
align-items: center;
gap: var(--spacing-xl);
grid-template-columns: auto 1fr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--spacing-s);
}
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.title :global(a) {
text-decoration: none;
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
.status {
height: 10px;
width: 10px;
border-radius: 50%;
}
.status--locked-you {
background-color: var(--spectrum-global-color-orange-600);
}
.status--locked-other {
background-color: var(--spectrum-global-color-red-600);
}
.status--open {
background-color: var(--spectrum-global-color-green-600);
}
</style>

View File

@ -14,7 +14,7 @@
import { post } from "builderStore/api" import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import { capitalise } from "../../helpers" import { capitalise } from "helpers"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
export let template export let template

View File

@ -1,83 +0,0 @@
<script>
export let step, done, active
</script>
<div class="container" class:active class:done>
<div class="circle" class:active class:done>
{#if done}
<svg
width="12"
height="10"
viewBox="0 0 12 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.1212 0.319527C10.327 0.115582 10.6047 0.000803464 10.8944
4.20219e-06C11.1841 -0.00079506 11.4624 0.11245 11.6693
0.315256C11.8762 0.518062 11.9949 0.794134 11.9998 1.08379C12.0048
1.37344 11.8955 1.65339 11.6957 1.86313L5.82705 9.19893C5.72619
9.30757 5.60445 9.39475 5.46913 9.45527C5.3338 9.51578 5.18766 9.54839
5.03944 9.55113C4.89123 9.55388 4.74398 9.52671 4.60651
9.47124C4.46903 9.41578 4.34416 9.33316 4.23934 9.22833L0.350925
5.33845C0.242598 5.23751 0.155712 5.11578 0.0954499 4.98054C0.0351876
4.84529 0.00278364 4.69929 0.00017159 4.55124C-0.00244046 4.4032
0.024793 4.25615 0.0802466 4.11886C0.1357 3.98157 0.218238 3.85685
0.322937 3.75215C0.427636 3.64746 0.55235 3.56492 0.68964
3.50946C0.82693 3.45401 0.973983 3.42678 1.12203 3.42939C1.27007 3.432
1.41607 3.46441 1.55132 3.52467C1.68657 3.58493 1.80829 3.67182
1.90923 3.78014L4.98762 6.85706L10.0933 0.35187C10.1024 0.340482
10.1122 0.329679 10.1227 0.319527H10.1212Z"
fill="var(--background)"
/>
</svg>
{:else}{step}{/if}
</div>
</div>
<style>
.container::before {
content: "";
position: absolute;
top: -30px;
width: 1px;
height: 30px;
background: var(--grey-5);
}
.container:first-child::before {
display: none;
}
.container {
position: relative;
height: 45px;
display: grid;
place-items: center;
}
.container.active {
box-shadow: inset 3px 0 0 0 var(--blue);
}
.circle.active {
background: var(--blue);
color: white;
border: none;
}
.circle.done {
background: var(--grey-5);
color: white;
border: none;
}
.circle {
color: var(--grey-5);
font-size: 14px;
font-weight: 500;
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid var(--grey-5);
box-sizing: border-box;
}
</style>

View File

@ -1,121 +0,0 @@
<script>
import { Label, Heading, Input, notifications } from "@budibase/bbui"
const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
export let template
export let values
export let errors
export let touched
let blurred = { appName: false }
let file
function handleFile(evt) {
const fileArray = Array.from(evt.target.files)
if (fileArray.some(file => file.size >= FILE_SIZE_LIMIT)) {
notifications.error(
`Files cannot exceed ${
FILE_SIZE_LIMIT / BYTES_IN_MB
}MB. Please try again with smaller files.`
)
return
}
file = evt.target.files[0]
template.file = file
}
</script>
<div class="container">
{#if template?.fromFile}
<Heading size="L">Import your Web App</Heading>
{:else}
<Heading size="L">Create your Web App</Heading>
{/if}
{#if template?.fromFile}
<div class="template">
<Label extraSmall grey>Import File</Label>
<div class="dropzone">
<input
id="file-upload"
accept=".txt"
type="file"
on:change={handleFile}
/>
<label for="file-upload" class:uploaded={file}>
{#if file}{file.name}{:else}Import{/if}
</label>
</div>
</div>
{:else if template}
<div class="template">
<Label extraSmall grey>Selected Template</Label>
<Heading size="S">{template.name}</Heading>
</div>
{/if}
<Input
on:change={() => ($touched.applicationName = true)}
bind:value={$values.applicationName}
label="Web App Name"
placeholder="Enter name of your web application"
error={$touched.applicationName && $errors.applicationName}
/>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
margin-top: var(--spacing-xl);
}
.template :global(label) {
/* Fix layout due to LH 0 on heading */
margin-bottom: 16px;
}
.dropzone {
text-align: center;
display: flex;
align-items: center;
flex-direction: column;
border-radius: 10px;
transition: all 0.3s;
}
.uploaded {
color: var(--blue);
}
input[type="file"] {
display: none;
}
label {
font-family: var(--font-sans);
cursor: pointer;
font-weight: 500;
box-sizing: border-box;
overflow: hidden;
border-radius: var(--border-radius-s);
color: var(--ink);
padding: var(--spacing-m) var(--spacing-l);
transition: all 0.2s ease 0s;
display: inline-flex;
text-rendering: optimizeLegibility;
min-width: auto;
outline: none;
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
-webkit-box-align: center;
user-select: none;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 100%;
background-color: var(--grey-2);
font-size: var(--font-size-xs);
line-height: normal;
border: var(--border-transparent);
}
</style>

View File

@ -1,29 +0,0 @@
<script>
import { Select, Heading } from "@budibase/bbui"
export let values
export let errors
export let touched
</script>
<div class="container">
<Heading size="L">What's your role for this app?</Heading>
<Select
bind:value={$values.roleId}
label="Role"
options={[
{ label: "Admin", value: "ADMIN" },
{ label: "Power User", value: "POWER_USER" },
]}
getOptionLabel={option => option.label}
getOptionValue={option => option.value}
error={$errors.roleId}
/>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
}
</style>

View File

@ -1,2 +0,0 @@
export { default as Info } from "./Info.svelte"
export { default as User } from "./User.svelte"

View File

@ -11,18 +11,22 @@
Page, Page,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import AppGridView from "components/start/AppGridView.svelte"
import AppTableView from "components/start/AppTableView.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import api from "builderStore/api" import api, { del } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps } from "stores/portal" import { apps } from "stores/portal"
import download from "downloadjs" import download from "downloadjs"
import { goto } from "@roxi/routify"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AppCard from "components/start/AppCard.svelte"
import AppRow from "components/start/AppRow.svelte"
let layout = "grid" let layout = "grid"
let modal
let template let template
let appToDelete
let creationModal
let deletionModal
const checkKeys = async () => { const checkKeys = async () => {
const response = await api.get(`/api/keys/`) const response = await api.get(`/api/keys/`)
@ -34,7 +38,11 @@
const initiateAppImport = () => { const initiateAppImport = () => {
template = { fromFile: true } template = { fromFile: true }
modal.show() creationModal.show()
}
const openApp = app => {
$goto(`../../app/${app._id}`)
} }
const exportApp = app => { const exportApp = app => {
@ -51,6 +59,20 @@
} }
} }
const deleteApp = app => {
appToDelete = app
deletionModal.show()
}
const confirmDeleteApp = async () => {
if (!appToDelete) {
return
}
await del(`/api/applications/${appToDelete?._id}`)
await apps.load()
appToDelete = null
}
onMount(() => { onMount(() => {
checkKeys() checkKeys()
apps.load() apps.load()
@ -63,7 +85,7 @@
<Heading>Apps</Heading> <Heading>Apps</Heading>
<ButtonGroup> <ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import app</Button> <Button secondary on:click={initiateAppImport}>Import app</Button>
<Button cta on:click={modal.show}>Create new app</Button> <Button cta on:click={creationModal.show}>Create new app</Button>
</ButtonGroup> </ButtonGroup>
</div> </div>
<div class="filter"> <div class="filter">
@ -86,24 +108,42 @@
</ActionGroup> </ActionGroup>
</div> </div>
{#if $apps.length} {#if $apps.length}
{#if layout === "grid"} <div
<AppGridView {exportApp} /> class:appGrid={layout === "grid"}
{:else} class:appTable={layout === "table"}
<AppTableView {exportApp} /> >
{/if} {#each $apps as app, idx}
<svelte:component
this={layout === "grid" ? AppCard : AppRow}
{app}
{openApp}
{exportApp}
{deleteApp}
last={idx === $apps.length - 1}
/>
{/each}
</div>
{:else} {:else}
<div>No apps.</div> <div>No apps.</div>
{/if} {/if}
</Layout> </Layout>
</Page> </Page>
<Modal <Modal
bind:this={modal} bind:this={creationModal}
padding={false} padding={false}
width="600px" width="600px"
on:hide={() => (template = null)} on:hide={() => (template = null)}
> >
<CreateAppModal {template} /> <CreateAppModal {template} />
</Modal> </Modal>
<ConfirmDialog
bind:this={deletionModal}
title="Confirm deletion"
okText="Delete app"
onOk={confirmDeleteApp}
>
Are you sure you want to delete the app <b>{appToDelete?.name}</b>?
</ConfirmDialog>
<style> <style>
.title, .title,
@ -117,4 +157,30 @@
.select { .select {
width: 110px; width: 110px;
} }
.appGrid {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.appTable {
display: grid;
grid-template-rows: auto;
grid-template-columns: 1fr 1fr 1fr auto;
align-items: center;
}
.appTable :global(> div) {
height: 70px;
display: grid;
align-items: center;
gap: var(--spacing-xl);
grid-template-columns: auto 1fr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--spacing-s);
}
.appTable :global(> div:not(.last)) {
border-bottom: var(--border-light);
}
</style> </style>