Merge pull request #1516 from Budibase/enhanced-app-list
Enhanced app list + placeholder removal
This commit is contained in:
commit
05281c83cb
|
@ -52,7 +52,7 @@
|
|||
"@spectrum-css/icon": "^3.0.1",
|
||||
"@spectrum-css/illustratedmessage": "^3.0.2",
|
||||
"@spectrum-css/inputgroup": "^3.0.2",
|
||||
"@spectrum-css/label": "^2.0.9",
|
||||
"@spectrum-css/label": "^2.0.10",
|
||||
"@spectrum-css/link": "^3.1.1",
|
||||
"@spectrum-css/menu": "^3.0.1",
|
||||
"@spectrum-css/modal": "^3.0.1",
|
||||
|
@ -64,6 +64,7 @@
|
|||
"@spectrum-css/radio": "^3.0.2",
|
||||
"@spectrum-css/search": "^3.0.2",
|
||||
"@spectrum-css/sidenav": "^3.0.2",
|
||||
"@spectrum-css/statuslight": "^3.0.2",
|
||||
"@spectrum-css/switch": "^1.0.2",
|
||||
"@spectrum-css/table": "^3.0.1",
|
||||
"@spectrum-css/tabs": "^3.0.1",
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import "@spectrum-css/label/dist/index-vars.css"
|
||||
|
||||
export let size = "M"
|
||||
export let grey = false
|
||||
export let red = false
|
||||
export let orange = false
|
||||
export let yellow = false
|
||||
export let seafoam = false
|
||||
export let active = false
|
||||
export let inactive = false
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="spectrum-Label"
|
||||
class:spectrum-Label--small={size === "S"}
|
||||
class:spectrum-Label--large={size === "L"}
|
||||
class:spectrum-Label--grey={grey}
|
||||
class:spectrum-Label--red={red}
|
||||
class:spectrum-Label--orange={orange}
|
||||
class:spectrum-Label--yellow={yellow}
|
||||
class:spectrum-Label--seafoam={seafoam}
|
||||
class:spectrum-Label--active={active}
|
||||
class:spectrum-Label--inactive={inactive}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import "@spectrum-css/statuslight"
|
||||
|
||||
export let size = "M"
|
||||
export let celery = false
|
||||
export let yellow = false
|
||||
export let fuchsia = false
|
||||
export let indigo = false
|
||||
export let seafoam = false
|
||||
export let chartreuse = false
|
||||
export let magenta = false
|
||||
export let purple = false
|
||||
export let neutral = false
|
||||
export let info = false
|
||||
export let positive = false
|
||||
export let notice = false
|
||||
export let negative = false
|
||||
export let disabled = false
|
||||
export let active = false
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="spectrum-StatusLight spectrum-StatusLight--size{size}"
|
||||
class:spectrum-StatusLight--celery={celery}
|
||||
class:spectrum-StatusLight--yellow={yellow}
|
||||
class:spectrum-StatusLight--fuchsia={fuchsia}
|
||||
class:spectrum-StatusLight--indigo={indigo}
|
||||
class:spectrum-StatusLight--seafoam={seafoam}
|
||||
class:spectrum-StatusLight--chartreuse={chartreuse}
|
||||
class:spectrum-StatusLight--magenta={magenta}
|
||||
class:spectrum-StatusLight--purple={purple}
|
||||
class:spectrum-StatusLight--neutral={neutral}
|
||||
class:spectrum-StatusLight--info={info}
|
||||
class:spectrum-StatusLight--positive={positive}
|
||||
class:spectrum-StatusLight--notice={notice}
|
||||
class:spectrum-StatusLight--negative={negative}
|
||||
class:spectrum-StatusLight--disabled={disabled}
|
||||
class:spectrum-StatusLight--active={active}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import "@spectrum-css/tags/dist/index-vars.css"
|
||||
import Avatar from "../Avatar/Avatar.svelte"
|
||||
import ClearButton from "../ClearButton/ClearButton.svelte"
|
||||
|
||||
export let icon = ""
|
||||
export let avatar = ""
|
||||
export let invalid = false
|
||||
|
@ -32,3 +34,10 @@
|
|||
<ClearButton on:click />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spectrum-Tags-item {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -52,6 +52,8 @@ export { default as TreeItem } from "./TreeView/Item.svelte"
|
|||
export { default as Divider } from "./Divider/Divider.svelte"
|
||||
export { default as Search } from "./Form/Search.svelte"
|
||||
export { default as Pagination } from "./Pagination/Pagination.svelte"
|
||||
export { default as Badge } from "./Badge/Badge.svelte"
|
||||
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||
|
||||
// Typography
|
||||
export { default as Body } from "./Typography/Body.svelte"
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.2.tgz#f1b13603832cbd22394f3d898af13203961f8691"
|
||||
integrity sha512-O0G3Lw9gxsh8gTLQWIAKkN1O8cWhjpEUl+oR1PguIKFni72uNr2ikU9piOwy/r0gJG2Q/TVs6hAshoAAkmsSzw==
|
||||
|
||||
"@spectrum-css/label@^2.0.9":
|
||||
"@spectrum-css/label@^2.0.10":
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/label/-/label-2.0.10.tgz#2368651d7636a19385b5d300cdf6272db1916001"
|
||||
integrity sha512-xCbtEiQkZIlLdWFikuw7ifDCC21DOC/KMgVrrVJHXFc4KRQe9LTZSqmGF3tovm+CSq1adE59mYoTbojVQ9YuEQ==
|
||||
|
@ -201,6 +201,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@spectrum-css/sidenav/-/sidenav-3.0.2.tgz#9d70f408d588ee79c69857751010333671f32713"
|
||||
integrity sha512-YpIdH/F0jEICYmoduGrnkTmxwJq1kfKxEp0wOs+ZkQOsvKMv1an7nyhsfOKCQqcGNfYzJ9mJAk7/u5+vsxHa8g==
|
||||
|
||||
"@spectrum-css/statuslight@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5"
|
||||
integrity sha512-xodB8g8vGJH20XmUj9ZsPlM1jHrGeRbvmVXkz0q7YvQrYAhim8pP3W+XKKZAletPFAuu8cmUOc6SWn6i4X4z6w==
|
||||
|
||||
"@spectrum-css/switch@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/switch/-/switch-1.0.2.tgz#f0b4c69271964573e02b08e90998096e49e1de44"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
const updatePassword = async () => {
|
||||
try {
|
||||
await auth.updateSelf({ ...$auth.user, password })
|
||||
notifications.success("Information updated successfully")
|
||||
notifications.success("Password changed successfully")
|
||||
} catch (error) {
|
||||
notifications.error("Failed to update password")
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import { themeStore } from "builderStore"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { capitalise } from "../../helpers"
|
||||
</script>
|
||||
|
||||
<Select
|
||||
options={$themeStore.options}
|
||||
bind:value={$themeStore.theme}
|
||||
placeholder={null}
|
||||
getOptionLabel={capitalise}
|
||||
/>
|
|
@ -1,35 +0,0 @@
|
|||
<script>
|
||||
import { Icon, Label, Modal, ModalContent } from "@budibase/bbui"
|
||||
import ThemeEditor from "./ThemeEditor.svelte"
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<div class="topnavitemright" on:click={modal.show}>
|
||||
<Icon hoverable name="ColorFill" />
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
title="Builder Theme"
|
||||
confirmText="Done"
|
||||
showCancelButton={false}
|
||||
>
|
||||
<ThemeEditor />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.topnavitemright {
|
||||
cursor: pointer;
|
||||
color: var(--grey-7);
|
||||
margin: 0 12px 0 0;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
</style>
|
|
@ -6,51 +6,67 @@
|
|||
Layout,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
StatusLight,
|
||||
} from "@budibase/bbui"
|
||||
import { gradient } from "actions"
|
||||
import { auth } from "stores/portal"
|
||||
import { AppStatus } from "constants"
|
||||
|
||||
export let app
|
||||
export let exportApp
|
||||
export let openApp
|
||||
export let viewApp
|
||||
export let editApp
|
||||
export let deleteApp
|
||||
export let unpublishApp
|
||||
export let releaseLock
|
||||
export let deletable
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<Layout noPadding gap="XS" alignContent="start">
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div class="title">
|
||||
<div class="name" on:click={() => openApp(app)}>
|
||||
{#if app.lockedBy}
|
||||
<Icon name="LockClosed" />
|
||||
{/if}
|
||||
<div class="name" on:click={() => editApp(app)}>
|
||||
<Heading size="XS">
|
||||
{app.name}
|
||||
</Heading>
|
||||
</div>
|
||||
<ActionMenu align="right">
|
||||
<Icon slot="control" name="More" hoverable />
|
||||
{#if app.deployed}
|
||||
<MenuItem on:click={() => viewApp(app)} icon="GlobeOutline">
|
||||
View published app
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{#if app.lockedYou}
|
||||
<MenuItem on:click={() => releaseLock(app)} icon="LockOpen">
|
||||
Release lock
|
||||
</MenuItem>
|
||||
{/if}
|
||||
<MenuItem on:click={() => exportApp(app)} icon="Download">
|
||||
Export
|
||||
</MenuItem>
|
||||
{#if deletable}
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">
|
||||
Delete
|
||||
{#if app.deployed}
|
||||
<MenuItem on:click={() => unpublishApp(app)} icon="GlobeRemove">
|
||||
Unpublish
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email}
|
||||
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen">
|
||||
Release Lock
|
||||
{#if !app.deployed}
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">
|
||||
Delete
|
||||
</MenuItem>
|
||||
{/if}
|
||||
</ActionMenu>
|
||||
</div>
|
||||
<div class="status">
|
||||
<Body size="S">
|
||||
Edited {Math.floor(1 + Math.random() * 10)} months ago
|
||||
Updated {Math.floor(1 + Math.random() * 10)} months ago
|
||||
</Body>
|
||||
{#if app.lockedBy}
|
||||
<Icon name="LockClosed" />
|
||||
{/if}
|
||||
<StatusLight active={app.deployed} neutral={!app.deployed}>
|
||||
{#if app.deployed}Published{:else}Unpublished{/if}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
@ -59,13 +75,17 @@
|
|||
.wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
.wrapper :global(.spectrum-StatusLight) {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
height: 135px;
|
||||
border-radius: var(--border-radius-s);
|
||||
margin-bottom: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.title,
|
||||
.status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -74,12 +94,18 @@
|
|||
}
|
||||
|
||||
.name {
|
||||
text-decoration: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.title :global(.spectrum-Icon) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.title :global(h1) {
|
||||
overflow: hidden;
|
||||
|
|
|
@ -1,55 +1,83 @@
|
|||
<script>
|
||||
import { gradient } from "actions"
|
||||
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||
import {
|
||||
Heading,
|
||||
Button,
|
||||
Icon,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
StatusLight,
|
||||
} from "@budibase/bbui"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
export let app
|
||||
export let openApp
|
||||
export let exportApp
|
||||
export let viewApp
|
||||
export let editApp
|
||||
export let deleteApp
|
||||
export let unpublishApp
|
||||
export let releaseLock
|
||||
export let last
|
||||
export let deletable
|
||||
</script>
|
||||
|
||||
<div class="title" class:last>
|
||||
<div class="title">
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div class="name" on:click={() => openApp(app)}>
|
||||
<div class="name" on:click={() => editApp(app)}>
|
||||
<Heading size="XS">
|
||||
{app.name}
|
||||
</Heading>
|
||||
</div>
|
||||
</div>
|
||||
<div class:last>
|
||||
Edited {Math.round(Math.random() * 10 + 1)} months ago
|
||||
<div>
|
||||
Updated {Math.round(Math.random() * 10 + 1)} months ago
|
||||
</div>
|
||||
<div class:last>
|
||||
{#if app.lockedBy}
|
||||
{#if app.lockedBy.email === $auth.user.email}
|
||||
<div class="status status--locked-you" />
|
||||
<div>
|
||||
<StatusLight
|
||||
positive={!app.lockedYou && !app.lockedOther}
|
||||
notice={app.lockedYou}
|
||||
negative={app.lockedOther}
|
||||
>
|
||||
{#if app.lockedYou}
|
||||
Locked by you
|
||||
{:else}
|
||||
<div class="status status--locked-other" />
|
||||
{:else if app.lockedOther}
|
||||
Locked by {app.lockedBy.email}
|
||||
{:else}
|
||||
Open
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="status status--open" />
|
||||
Open
|
||||
{/if}
|
||||
</StatusLight>
|
||||
</div>
|
||||
<div class:last>
|
||||
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button>
|
||||
<div>
|
||||
<StatusLight active={app.deployed} neutral={!app.deployed}>
|
||||
{#if app.deployed}Published{:else}Unpublished{/if}
|
||||
</StatusLight>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
disabled={app.lockedOther}
|
||||
on:click={() => editApp(app)}
|
||||
size="S"
|
||||
secondary>Open</Button
|
||||
>
|
||||
<ActionMenu align="right">
|
||||
<Icon hoverable slot="control" name="More" />
|
||||
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem>
|
||||
{#if deletable}
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
|
||||
{/if}
|
||||
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email}
|
||||
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen">
|
||||
Release Lock
|
||||
{#if app.deployed}
|
||||
<MenuItem on:click={() => viewApp(app)} icon="GlobeOutline">
|
||||
View published app
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{#if app.lockedYou}
|
||||
<MenuItem on:click={() => releaseLock(app)} icon="LockOpen">
|
||||
Release lock
|
||||
</MenuItem>
|
||||
{/if}
|
||||
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem>
|
||||
{#if app.deployed}
|
||||
<MenuItem on:click={() => unpublishApp(app)} icon="GlobeRemove">
|
||||
Unpublish
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{#if !app.deployed}
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
|
||||
{/if}
|
||||
</ActionMenu>
|
||||
</div>
|
||||
|
||||
|
@ -61,24 +89,16 @@
|
|||
}
|
||||
.name {
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.name :global(.spectrum-Heading) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.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,42 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Heading,
|
||||
Divider,
|
||||
notifications,
|
||||
ModalContent,
|
||||
Toggle,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import ThemeEditor from "components/settings/ThemeEditor.svelte"
|
||||
import analytics from "analytics"
|
||||
|
||||
$: analyticsDisabled = analytics.disabled()
|
||||
|
||||
async function save() {
|
||||
notifications.success(`Settings saved.`)
|
||||
}
|
||||
|
||||
function toggleAnalytics() {
|
||||
if (analyticsDisabled) {
|
||||
analytics.optIn()
|
||||
} else {
|
||||
analytics.optOut()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent title="Builder settings" confirmText="Save" onConfirm={save}>
|
||||
<Heading size="XS">Theme</Heading>
|
||||
<ThemeEditor />
|
||||
<Divider noMargin noGrid />
|
||||
<Heading size="XS">Analytics</Heading>
|
||||
<Body size="S">
|
||||
If you would like to send analytics that help us make budibase better,
|
||||
please let us know below.
|
||||
</Body>
|
||||
<Toggle
|
||||
text="Send Analytics To Budibase"
|
||||
value={!analyticsDisabled}
|
||||
on:change={toggleAnalytics}
|
||||
/>
|
||||
</ModalContent>
|
|
@ -10,8 +10,9 @@ export const FrontendTypes = {
|
|||
}
|
||||
|
||||
export const AppStatus = {
|
||||
DEV: "dev",
|
||||
PUBLISHED: "published",
|
||||
ALL: "all",
|
||||
DEV: "development",
|
||||
DEPLOYED: "published",
|
||||
}
|
||||
|
||||
// fields on the user table that cannot be edited
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { AppStatus } from "constants"
|
||||
import { gradient } from "actions"
|
||||
import UpdateUserInfoModal from "./_components/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "./_components/ChangePasswordModal.svelte"
|
||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
|
||||
let loaded = false
|
||||
let userInfoModal
|
||||
|
@ -26,12 +26,14 @@
|
|||
|
||||
onMount(async () => {
|
||||
await organisation.init()
|
||||
await apps.load(AppStatus.DEV)
|
||||
await apps.load()
|
||||
loaded = true
|
||||
})
|
||||
|
||||
$: publishedApps = $apps.filter(app => app.status === AppStatus.DEPLOYED)
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
{#if $auth.user && loaded}
|
||||
<div class="container">
|
||||
<Page>
|
||||
<div class="content">
|
||||
|
@ -71,17 +73,17 @@
|
|||
</ActionMenu>
|
||||
</div>
|
||||
<Divider />
|
||||
{#if $apps.length}
|
||||
{#if publishedApps.length}
|
||||
<Heading>Apps</Heading>
|
||||
<div class="group">
|
||||
<Layout gap="S" noPadding>
|
||||
{#each $apps as app, idx (app.appId)}
|
||||
{#each publishedApps as app, idx (app.appId)}
|
||||
<a class="app" target="_blank" href={`/${app.appId}`}>
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div class="app-info">
|
||||
<Heading size="XS">{app.name}</Heading>
|
||||
<Body size="S">
|
||||
Edited {Math.round(Math.random() * 10 + 1)} months ago
|
||||
Updated {Math.round(Math.random() * 10 + 1)} months ago
|
||||
</Body>
|
||||
</div>
|
||||
<Icon name="ChevronRight" />
|
||||
|
|
|
@ -13,26 +13,25 @@
|
|||
} from "@budibase/bbui"
|
||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||
import { organisation, auth } from "stores/portal"
|
||||
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
|
||||
let oldSettingsModal
|
||||
let loaded = false
|
||||
let userInfoModal
|
||||
let changePasswordModal
|
||||
|
||||
const menu = [
|
||||
{ title: "Apps", href: "/builder/portal/apps" },
|
||||
{ title: "Drafts", href: "/builder/portal/drafts" },
|
||||
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
|
||||
{ title: "Groups", href: "/builder/portal/manage/groups" },
|
||||
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||
{
|
||||
title: "General",
|
||||
href: "/builder/portal/settings/general",
|
||||
title: "Organisation",
|
||||
href: "/builder/portal/settings/organisation",
|
||||
heading: "Settings",
|
||||
},
|
||||
{ title: "Theming", href: "/builder/portal/theming" },
|
||||
{ title: "Account", href: "/builder/portal/account" },
|
||||
{ title: "Theming", href: "/builder/portal/settings/theming" },
|
||||
]
|
||||
|
||||
onMount(async () => {
|
||||
|
@ -48,7 +47,7 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
{#if $auth.user && loaded}
|
||||
<div class="container">
|
||||
<div class="nav">
|
||||
<Layout paddingX="L" paddingY="L">
|
||||
|
@ -75,14 +74,20 @@
|
|||
</div>
|
||||
<div class="main">
|
||||
<div class="toolbar">
|
||||
<Search placeholder="Global search" />
|
||||
<div />
|
||||
<ActionMenu align="right">
|
||||
<div slot="control" class="avatar">
|
||||
<Avatar size="M" name="John Doe" />
|
||||
<Icon size="XL" name="ChevronDown" />
|
||||
</div>
|
||||
<MenuItem icon="Settings" on:click={oldSettingsModal.show}>
|
||||
Old settings
|
||||
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
||||
Update user information
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="LockClosed"
|
||||
on:click={() => changePasswordModal.show()}
|
||||
>
|
||||
Update password
|
||||
</MenuItem>
|
||||
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
||||
Close developer mode
|
||||
|
@ -95,8 +100,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal bind:this={oldSettingsModal} width="30%">
|
||||
<BuilderSettingsModal />
|
||||
<Modal bind:this={userInfoModal}>
|
||||
<UpdateUserInfoModal />
|
||||
</Modal>
|
||||
<Modal bind:this={changePasswordModal}>
|
||||
<ChangePasswordModal />
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -26,15 +26,39 @@
|
|||
import { AppStatus } from "constants"
|
||||
|
||||
let layout = "grid"
|
||||
let appStatus = AppStatus.PUBLISHED
|
||||
let sortBy = "name"
|
||||
let template
|
||||
let appToDelete
|
||||
let selectedApp
|
||||
let creationModal
|
||||
let deletionModal
|
||||
let unpublishModal
|
||||
let creatingApp = false
|
||||
let loaded = false
|
||||
|
||||
$: appStatus && apps.load(appStatus)
|
||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||
|
||||
const enrichApps = (apps, user, sortBy) => {
|
||||
const enrichedApps = apps.map(app => ({
|
||||
...app,
|
||||
deployed: app.status === AppStatus.DEPLOYED,
|
||||
lockedYou: app.lockedBy?.email === user.email,
|
||||
lockedOther: app.lockedBy && app.lockedBy.email !== user.email,
|
||||
}))
|
||||
if (sortBy === "status") {
|
||||
return enrichedApps.sort((a, b) => {
|
||||
if (a.status === b.status) {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
|
||||
}
|
||||
return a.status === AppStatus.DEPLOYED ? -1 : 1
|
||||
})
|
||||
} else if (sortBy === "name") {
|
||||
return enrichedApps.sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
|
||||
})
|
||||
} else {
|
||||
return enrichedApps
|
||||
}
|
||||
}
|
||||
|
||||
const checkKeys = async () => {
|
||||
const response = await api.get(`/api/keys/`)
|
||||
|
@ -60,19 +84,19 @@
|
|||
creatingApp = false
|
||||
}
|
||||
|
||||
const openApp = app => {
|
||||
if (app.lockedBy && app.lockedBy?.email !== $auth.user?.email) {
|
||||
const viewApp = app => {
|
||||
const id = app.deployed ? app.prodId : app.devId
|
||||
window.open(`/${id}`, "_blank")
|
||||
}
|
||||
|
||||
const editApp = app => {
|
||||
if (app.lockedOther) {
|
||||
notifications.error(
|
||||
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (appStatus === AppStatus.DEV) {
|
||||
$goto(`../../app/${app.appId}`)
|
||||
} else {
|
||||
window.open(`/${app.appId}`, "_blank")
|
||||
}
|
||||
$goto(`../../app/${app.devId}`)
|
||||
}
|
||||
|
||||
const exportApp = app => {
|
||||
|
@ -82,36 +106,66 @@
|
|||
app.name
|
||||
)}`
|
||||
)
|
||||
notifications.success("App export complete")
|
||||
notifications.success("App exported successfully")
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
notifications.error("App export failed")
|
||||
notifications.error(`Error exporting app: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
const unpublishApp = app => {
|
||||
selectedApp = app
|
||||
unpublishModal.show()
|
||||
}
|
||||
|
||||
const confirmUnpublishApp = async () => {
|
||||
if (!selectedApp) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await del(`/api/applications/${selectedApp.prodId}`)
|
||||
if (response.status !== 200) {
|
||||
const json = await response.json()
|
||||
throw json.message
|
||||
}
|
||||
await apps.load()
|
||||
notifications.success("App unpublished successfully")
|
||||
} catch (err) {
|
||||
notifications.error(`Error unpublishing app: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteApp = app => {
|
||||
appToDelete = app
|
||||
selectedApp = app
|
||||
deletionModal.show()
|
||||
}
|
||||
|
||||
const confirmDeleteApp = async () => {
|
||||
if (!appToDelete) {
|
||||
if (!selectedApp) {
|
||||
return
|
||||
}
|
||||
await del(`/api/applications/${appToDelete?.appId}`)
|
||||
await apps.load()
|
||||
appToDelete = null
|
||||
notifications.success("App deleted successfully.")
|
||||
try {
|
||||
const response = await del(`/api/applications/${selectedApp?.devId}`)
|
||||
if (response.status !== 200) {
|
||||
const json = await response.json()
|
||||
throw json.message
|
||||
}
|
||||
await apps.load()
|
||||
notifications.success("App deleted successfully")
|
||||
} catch (err) {
|
||||
notifications.error(`Error deleting app: ${err}`)
|
||||
}
|
||||
selectedApp = null
|
||||
}
|
||||
|
||||
const releaseLock = async appId => {
|
||||
const releaseLock = async app => {
|
||||
try {
|
||||
const response = await del(`/api/dev/${appId}/lock`)
|
||||
const json = await response.json()
|
||||
if (response.status !== 200) throw json.message
|
||||
|
||||
notifications.success("Lock released")
|
||||
await apps.load(appStatus)
|
||||
const response = await del(`/api/dev/${app.devId}/lock`)
|
||||
if (response.status !== 200) {
|
||||
const json = await response.json()
|
||||
throw json.message
|
||||
}
|
||||
await apps.load()
|
||||
notifications.success("Lock released successfully")
|
||||
} catch (err) {
|
||||
notifications.error(`Error releasing lock: ${err}`)
|
||||
}
|
||||
|
@ -119,66 +173,68 @@
|
|||
|
||||
onMount(async () => {
|
||||
checkKeys()
|
||||
await apps.load(appStatus)
|
||||
await apps.load()
|
||||
loaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<Page wide>
|
||||
<Layout noPadding>
|
||||
<div class="title">
|
||||
<Heading>Apps</Heading>
|
||||
<ButtonGroup>
|
||||
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
||||
<Button cta on:click={initiateAppCreation}>Create new app</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<div class="select">
|
||||
<Select
|
||||
bind:value={appStatus}
|
||||
options={[
|
||||
{ label: "Published", value: AppStatus.PUBLISHED },
|
||||
{ label: "In Development", value: AppStatus.DEV },
|
||||
]}
|
||||
/>
|
||||
{#if loaded && enrichedApps.length}
|
||||
<Layout noPadding>
|
||||
<div class="title">
|
||||
<Heading>Apps</Heading>
|
||||
<ButtonGroup>
|
||||
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
||||
<Button cta on:click={initiateAppCreation}>Create new app</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<div class="select">
|
||||
<Select
|
||||
bind:value={sortBy}
|
||||
placeholder={null}
|
||||
options={[
|
||||
{ label: "Sort by name", value: "name" },
|
||||
{ label: "Sort by recently updated", value: "updated" },
|
||||
{ label: "Sort by status", value: "status" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
on:click={() => (layout = "grid")}
|
||||
selected={layout === "grid"}
|
||||
quiet
|
||||
icon="ClassicGridView"
|
||||
/>
|
||||
<ActionButton
|
||||
on:click={() => (layout = "table")}
|
||||
selected={layout === "table"}
|
||||
quiet
|
||||
icon="ViewRow"
|
||||
/>
|
||||
</ActionGroup>
|
||||
</div>
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
on:click={() => (layout = "grid")}
|
||||
selected={layout === "grid"}
|
||||
quiet
|
||||
icon="ClassicGridView"
|
||||
/>
|
||||
<ActionButton
|
||||
on:click={() => (layout = "table")}
|
||||
selected={layout === "table"}
|
||||
quiet
|
||||
icon="ViewRow"
|
||||
/>
|
||||
</ActionGroup>
|
||||
</div>
|
||||
{#if loaded && $apps.length}
|
||||
<div
|
||||
class:appGrid={layout === "grid"}
|
||||
class:appTable={layout === "table"}
|
||||
>
|
||||
{#each $apps as app, idx (app.appId)}
|
||||
{#each enrichedApps as app (app.appId)}
|
||||
<svelte:component
|
||||
this={layout === "grid" ? AppCard : AppRow}
|
||||
deletable={appStatus === AppStatus.PUBLISHED}
|
||||
{releaseLock}
|
||||
{app}
|
||||
{openApp}
|
||||
{unpublishApp}
|
||||
{viewApp}
|
||||
{editApp}
|
||||
{exportApp}
|
||||
{deleteApp}
|
||||
last={idx === $apps.length - 1}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
{#if !$apps.length && !creatingApp && loaded}
|
||||
</Layout>
|
||||
{/if}
|
||||
{#if !enrichedApps.length && !creatingApp && loaded}
|
||||
<div class="empty-wrapper">
|
||||
<Modal inline>
|
||||
<ModalContent
|
||||
|
@ -215,7 +271,15 @@
|
|||
okText="Delete app"
|
||||
onOk={confirmDeleteApp}
|
||||
>
|
||||
Are you sure you want to delete the app <b>{appToDelete?.name}</b>?
|
||||
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
bind:this={unpublishModal}
|
||||
title="Confirm unpublish"
|
||||
okText="Unpublish app"
|
||||
onOk={confirmUnpublishApp}
|
||||
>
|
||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
|
@ -228,7 +292,7 @@
|
|||
}
|
||||
|
||||
.select {
|
||||
width: 150px;
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.appGrid {
|
||||
|
@ -239,7 +303,7 @@
|
|||
.appTable {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: 1fr 1fr 1fr auto;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
||||
align-items: center;
|
||||
}
|
||||
.appTable :global(> div) {
|
||||
|
@ -253,10 +317,9 @@
|
|||
text-overflow: ellipsis;
|
||||
padding: 0 var(--spacing-s);
|
||||
}
|
||||
.appTable :global(> div:not(.last)) {
|
||||
.appTable :global(> div) {
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
|
||||
.empty-wrapper {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
|
|
|
@ -66,10 +66,11 @@
|
|||
|
||||
<Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="M">General</Heading>
|
||||
<Heading size="M">Organisation</Heading>
|
||||
<Body>
|
||||
General is the place where you edit your organisation name, logo. You can
|
||||
also configure your platform URL as well as turn on or off analytics.
|
||||
Organisation settings is where you can edit your organisation name and
|
||||
logo. You can also configure your platform URL and enable or disable
|
||||
analytics.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import { Layout, Heading, Body, Divider, Label, Select } from "@budibase/bbui"
|
||||
import { themeStore } from "builderStore"
|
||||
import { capitalise } from "helpers"
|
||||
</script>
|
||||
|
||||
<Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="M">Theming</Heading>
|
||||
<Body>Customize how Budibase looks and feels.</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Builder theme</Label>
|
||||
<Select
|
||||
options={$themeStore.options}
|
||||
bind:value={$themeStore.theme}
|
||||
placeholder={null}
|
||||
getOptionLabel={capitalise}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-m);
|
||||
}
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 33% 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,15 +1,49 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { get } from "builderStore/api"
|
||||
import { AppStatus } from "../../constants"
|
||||
|
||||
export function createAppStore() {
|
||||
const store = writable([])
|
||||
|
||||
async function load(status = "") {
|
||||
async function load() {
|
||||
try {
|
||||
const res = await get(`/api/applications?status=${status}`)
|
||||
const res = await get(`/api/applications?status=all`)
|
||||
const json = await res.json()
|
||||
if (res.ok && Array.isArray(json)) {
|
||||
store.set(json)
|
||||
// Merge apps into one sensible list
|
||||
let appMap = {}
|
||||
let devApps = json.filter(app => app.status === AppStatus.DEV)
|
||||
let deployedApps = json.filter(app => app.status === AppStatus.DEPLOYED)
|
||||
|
||||
// First append all dev app version
|
||||
devApps.forEach(app => {
|
||||
const id = app.appId.substring(8)
|
||||
appMap[id] = {
|
||||
...app,
|
||||
devId: app.appId,
|
||||
devRev: app._rev,
|
||||
}
|
||||
})
|
||||
|
||||
// Then merge with all prod app versions
|
||||
deployedApps.forEach(app => {
|
||||
const id = app.appId.substring(4)
|
||||
appMap[id] = {
|
||||
...appMap[id],
|
||||
...app,
|
||||
prodId: app.appId,
|
||||
prodRev: app._rev,
|
||||
}
|
||||
})
|
||||
|
||||
// Transform into an array and clean up
|
||||
const apps = Object.values(appMap)
|
||||
apps.forEach(app => {
|
||||
app.appId = app.devId.substring(8)
|
||||
delete app._id
|
||||
delete app._rev
|
||||
})
|
||||
store.set(apps)
|
||||
} else {
|
||||
store.set([])
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ const StaticDatabases = {
|
|||
}
|
||||
|
||||
const AppStatus = {
|
||||
DEV: "dev",
|
||||
DEV: "development",
|
||||
ALL: "all",
|
||||
DEPLOYED: "PUBLISHED",
|
||||
DEPLOYED: "published",
|
||||
}
|
||||
|
||||
const DocumentTypes = {
|
||||
|
|
Loading…
Reference in New Issue