Simplify app layout and add option to delete apps
This commit is contained in:
parent
df607e78eb
commit
0b1b4d2aae
|
@ -7,6 +7,7 @@
|
|||
export let cancelText = "Cancel"
|
||||
export let onOk = undefined
|
||||
export let onCancel = undefined
|
||||
export let warning = true
|
||||
|
||||
let modal
|
||||
|
||||
|
@ -19,7 +20,13 @@
|
|||
</script>
|
||||
|
||||
<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}
|
||||
<slot />
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
|
||||
export let app
|
||||
export let exportApp
|
||||
|
||||
let appExportLoading = false
|
||||
export let deleteApp
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
|
@ -28,9 +27,12 @@
|
|||
</Link>
|
||||
<ActionMenu align="right">
|
||||
<Icon slot="control" name="More" hoverable />
|
||||
<MenuItem on:click={() => exportApp(app)} icon="Download"
|
||||
>Export</MenuItem
|
||||
>
|
||||
<MenuItem on:click={() => exportApp(app)} icon="Download">
|
||||
Export
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">
|
||||
Delete
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
</div>
|
||||
<div class="status">
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -14,7 +14,7 @@
|
|||
import { post } from "builderStore/api"
|
||||
import analytics from "analytics"
|
||||
import { onMount } from "svelte"
|
||||
import { capitalise } from "../../helpers"
|
||||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
export let template
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
export { default as Info } from "./Info.svelte"
|
||||
export { default as User } from "./User.svelte"
|
|
@ -11,18 +11,22 @@
|
|||
Page,
|
||||
notifications,
|
||||
} 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 api from "builderStore/api"
|
||||
import api, { del } from "builderStore/api"
|
||||
import analytics from "analytics"
|
||||
import { onMount } from "svelte"
|
||||
import { apps } from "stores/portal"
|
||||
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 modal
|
||||
let template
|
||||
let appToDelete
|
||||
let creationModal
|
||||
let deletionModal
|
||||
|
||||
const checkKeys = async () => {
|
||||
const response = await api.get(`/api/keys/`)
|
||||
|
@ -34,7 +38,11 @@
|
|||
|
||||
const initiateAppImport = () => {
|
||||
template = { fromFile: true }
|
||||
modal.show()
|
||||
creationModal.show()
|
||||
}
|
||||
|
||||
const openApp = app => {
|
||||
$goto(`../../app/${app._id}`)
|
||||
}
|
||||
|
||||
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(() => {
|
||||
checkKeys()
|
||||
apps.load()
|
||||
|
@ -63,7 +85,7 @@
|
|||
<Heading>Apps</Heading>
|
||||
<ButtonGroup>
|
||||
<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>
|
||||
</div>
|
||||
<div class="filter">
|
||||
|
@ -86,24 +108,42 @@
|
|||
</ActionGroup>
|
||||
</div>
|
||||
{#if $apps.length}
|
||||
{#if layout === "grid"}
|
||||
<AppGridView {exportApp} />
|
||||
{:else}
|
||||
<AppTableView {exportApp} />
|
||||
{/if}
|
||||
<div
|
||||
class:appGrid={layout === "grid"}
|
||||
class:appTable={layout === "table"}
|
||||
>
|
||||
{#each $apps as app, idx}
|
||||
<svelte:component
|
||||
this={layout === "grid" ? AppCard : AppRow}
|
||||
{app}
|
||||
{openApp}
|
||||
{exportApp}
|
||||
{deleteApp}
|
||||
last={idx === $apps.length - 1}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div>No apps.</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</Page>
|
||||
<Modal
|
||||
bind:this={modal}
|
||||
bind:this={creationModal}
|
||||
padding={false}
|
||||
width="600px"
|
||||
on:hide={() => (template = null)}
|
||||
>
|
||||
<CreateAppModal {template} />
|
||||
</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>
|
||||
.title,
|
||||
|
@ -117,4 +157,30 @@
|
|||
.select {
|
||||
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>
|
||||
|
|
Loading…
Reference in New Issue