Merge branch 'master' of github.com:Budibase/budibase into feature/app-updated-at

This commit is contained in:
mike12345567 2021-05-21 13:16:49 +01:00
commit a1ed0a113b
22 changed files with 463 additions and 266 deletions

View File

@ -52,7 +52,7 @@
"@spectrum-css/icon": "^3.0.1", "@spectrum-css/icon": "^3.0.1",
"@spectrum-css/illustratedmessage": "^3.0.2", "@spectrum-css/illustratedmessage": "^3.0.2",
"@spectrum-css/inputgroup": "^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/link": "^3.1.1",
"@spectrum-css/menu": "^3.0.1", "@spectrum-css/menu": "^3.0.1",
"@spectrum-css/modal": "^3.0.1", "@spectrum-css/modal": "^3.0.1",
@ -64,6 +64,7 @@
"@spectrum-css/radio": "^3.0.2", "@spectrum-css/radio": "^3.0.2",
"@spectrum-css/search": "^3.0.2", "@spectrum-css/search": "^3.0.2",
"@spectrum-css/sidenav": "^3.0.2", "@spectrum-css/sidenav": "^3.0.2",
"@spectrum-css/statuslight": "^3.0.2",
"@spectrum-css/switch": "^1.0.2", "@spectrum-css/switch": "^1.0.2",
"@spectrum-css/table": "^3.0.1", "@spectrum-css/table": "^3.0.1",
"@spectrum-css/tabs": "^3.0.1", "@spectrum-css/tabs": "^3.0.1",

View File

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

View File

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

View File

@ -1,6 +1,8 @@
<script> <script>
import "@spectrum-css/tags/dist/index-vars.css"
import Avatar from "../Avatar/Avatar.svelte" import Avatar from "../Avatar/Avatar.svelte"
import ClearButton from "../ClearButton/ClearButton.svelte" import ClearButton from "../ClearButton/ClearButton.svelte"
export let icon = "" export let icon = ""
export let avatar = "" export let avatar = ""
export let invalid = false export let invalid = false
@ -32,3 +34,10 @@
<ClearButton on:click /> <ClearButton on:click />
{/if} {/if}
</div> </div>
<style>
.spectrum-Tags-item {
margin-top: 0;
margin-bottom: 0;
}
</style>

View File

@ -52,6 +52,8 @@ export { default as TreeItem } from "./TreeView/Item.svelte"
export { default as Divider } from "./Divider/Divider.svelte" export { default as Divider } from "./Divider/Divider.svelte"
export { default as Search } from "./Form/Search.svelte" export { default as Search } from "./Form/Search.svelte"
export { default as Pagination } from "./Pagination/Pagination.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 // Typography
export { default as Body } from "./Typography/Body.svelte" export { default as Body } from "./Typography/Body.svelte"

View File

@ -141,7 +141,7 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.2.tgz#f1b13603832cbd22394f3d898af13203961f8691" resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.2.tgz#f1b13603832cbd22394f3d898af13203961f8691"
integrity sha512-O0G3Lw9gxsh8gTLQWIAKkN1O8cWhjpEUl+oR1PguIKFni72uNr2ikU9piOwy/r0gJG2Q/TVs6hAshoAAkmsSzw== integrity sha512-O0G3Lw9gxsh8gTLQWIAKkN1O8cWhjpEUl+oR1PguIKFni72uNr2ikU9piOwy/r0gJG2Q/TVs6hAshoAAkmsSzw==
"@spectrum-css/label@^2.0.9": "@spectrum-css/label@^2.0.10":
version "2.0.10" version "2.0.10"
resolved "https://registry.yarnpkg.com/@spectrum-css/label/-/label-2.0.10.tgz#2368651d7636a19385b5d300cdf6272db1916001" resolved "https://registry.yarnpkg.com/@spectrum-css/label/-/label-2.0.10.tgz#2368651d7636a19385b5d300cdf6272db1916001"
integrity sha512-xCbtEiQkZIlLdWFikuw7ifDCC21DOC/KMgVrrVJHXFc4KRQe9LTZSqmGF3tovm+CSq1adE59mYoTbojVQ9YuEQ== integrity sha512-xCbtEiQkZIlLdWFikuw7ifDCC21DOC/KMgVrrVJHXFc4KRQe9LTZSqmGF3tovm+CSq1adE59mYoTbojVQ9YuEQ==
@ -201,6 +201,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/sidenav/-/sidenav-3.0.2.tgz#9d70f408d588ee79c69857751010333671f32713" resolved "https://registry.yarnpkg.com/@spectrum-css/sidenav/-/sidenav-3.0.2.tgz#9d70f408d588ee79c69857751010333671f32713"
integrity sha512-YpIdH/F0jEICYmoduGrnkTmxwJq1kfKxEp0wOs+ZkQOsvKMv1an7nyhsfOKCQqcGNfYzJ9mJAk7/u5+vsxHa8g== 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": "@spectrum-css/switch@^1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/switch/-/switch-1.0.2.tgz#f0b4c69271964573e02b08e90998096e49e1de44" resolved "https://registry.yarnpkg.com/@spectrum-css/switch/-/switch-1.0.2.tgz#f0b4c69271964573e02b08e90998096e49e1de44"

View File

@ -9,7 +9,7 @@
const updatePassword = async () => { const updatePassword = async () => {
try { try {
await auth.updateSelf({ ...$auth.user, password }) await auth.updateSelf({ ...$auth.user, password })
notifications.success("Information updated successfully") notifications.success("Password changed successfully")
} catch (error) { } catch (error) {
notifications.error("Failed to update password") notifications.error("Failed to update password")
} }

View File

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

View File

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

View File

@ -6,51 +6,67 @@
Layout, Layout,
ActionMenu, ActionMenu,
MenuItem, MenuItem,
StatusLight,
} from "@budibase/bbui" } from "@budibase/bbui"
import { gradient } from "actions" import { gradient } from "actions"
import { auth } from "stores/portal" import { auth } from "stores/portal"
import { AppStatus } from "constants"
export let app export let app
export let exportApp export let exportApp
export let openApp export let viewApp
export let editApp
export let deleteApp export let deleteApp
export let unpublishApp
export let releaseLock export let releaseLock
export let deletable
</script> </script>
<div class="wrapper"> <div class="wrapper">
<Layout noPadding gap="XS" alignContent="start"> <Layout noPadding gap="XS" alignContent="start">
<div class="preview" use:gradient={{ seed: app.name }} /> <div class="preview" use:gradient={{ seed: app.name }} />
<div class="title"> <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"> <Heading size="XS">
{app.name} {app.name}
</Heading> </Heading>
</div> </div>
<ActionMenu align="right"> <ActionMenu align="right">
<Icon slot="control" name="More" hoverable /> <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"> <MenuItem on:click={() => exportApp(app)} icon="Download">
Export Export
</MenuItem> </MenuItem>
{#if deletable} {#if app.deployed}
<MenuItem on:click={() => deleteApp(app)} icon="Delete"> <MenuItem on:click={() => unpublishApp(app)} icon="GlobeRemove">
Delete Unpublish
</MenuItem> </MenuItem>
{/if} {/if}
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email} {#if !app.deployed}
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen"> <MenuItem on:click={() => deleteApp(app)} icon="Delete">
Release Lock Delete
</MenuItem> </MenuItem>
{/if} {/if}
</ActionMenu> </ActionMenu>
</div> </div>
<div class="status"> <div class="status">
<Body size="S"> <Body size="S">
Edited {Math.floor(1 + Math.random() * 10)} months ago Updated {Math.floor(1 + Math.random() * 10)} months ago
</Body> </Body>
{#if app.lockedBy} <StatusLight active={app.deployed} neutral={!app.deployed}>
<Icon name="LockClosed" /> {#if app.deployed}Published{:else}Unpublished{/if}
{/if} </StatusLight>
</div> </div>
</Layout> </Layout>
</div> </div>
@ -59,13 +75,17 @@
.wrapper { .wrapper {
overflow: hidden; overflow: hidden;
} }
.wrapper :global(.spectrum-StatusLight) {
padding: 0;
min-height: 0;
}
.preview { .preview {
height: 135px; height: 135px;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-m);
} }
.title,
.status { .status {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -74,12 +94,18 @@
} }
.name { .name {
text-decoration: none;
flex: 1 1 auto; flex: 1 1 auto;
width: 0; }
overflow: hidden;
text-overflow: ellipsis; .title {
margin-right: var(--spacing-m); 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) { .title :global(h1) {
overflow: hidden; overflow: hidden;

View File

@ -1,55 +1,83 @@
<script> <script>
import { gradient } from "actions" 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" import { auth } from "stores/portal"
export let app export let app
export let openApp
export let exportApp export let exportApp
export let viewApp
export let editApp
export let deleteApp export let deleteApp
export let unpublishApp
export let releaseLock export let releaseLock
export let last
export let deletable
</script> </script>
<div class="title" class:last> <div class="title">
<div class="preview" use:gradient={{ seed: app.name }} /> <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"> <Heading size="XS">
{app.name} {app.name}
</Heading> </Heading>
</div> </div>
</div> </div>
<div class:last> <div>
Edited {Math.round(Math.random() * 10 + 1)} months ago Updated {Math.round(Math.random() * 10 + 1)} months ago
</div> </div>
<div class:last> <div>
{#if app.lockedBy} <StatusLight
{#if app.lockedBy.email === $auth.user.email} positive={!app.lockedYou && !app.lockedOther}
<div class="status status--locked-you" /> notice={app.lockedYou}
negative={app.lockedOther}
>
{#if app.lockedYou}
Locked by you Locked by you
{:else} {:else if app.lockedOther}
<div class="status status--locked-other" />
Locked by {app.lockedBy.email} Locked by {app.lockedBy.email}
{/if}
{:else} {:else}
<div class="status status--open" />
Open Open
{/if} {/if}
</StatusLight>
</div> </div>
<div class:last> <div>
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button> <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"> <ActionMenu align="right">
<Icon hoverable slot="control" name="More" /> <Icon hoverable slot="control" name="More" />
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem> {#if app.deployed}
{#if deletable} <MenuItem on:click={() => viewApp(app)} icon="GlobeOutline">
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem> View published app
{/if}
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email}
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen">
Release Lock
</MenuItem> </MenuItem>
{/if} {/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> </ActionMenu>
</div> </div>
@ -61,24 +89,16 @@
} }
.name { .name {
text-decoration: none; text-decoration: none;
overflow: hidden;
}
.name :global(.spectrum-Heading) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.title :global(h1:hover) { .title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600); color: var(--spectrum-global-color-blue-600);
cursor: pointer; cursor: pointer;
transition: color 130ms ease; 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> </style>

View File

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

View File

@ -10,8 +10,9 @@ export const FrontendTypes = {
} }
export const AppStatus = { export const AppStatus = {
DEV: "dev", ALL: "all",
PUBLISHED: "published", DEV: "development",
DEPLOYED: "published",
} }
// fields on the user table that cannot be edited // fields on the user table that cannot be edited

View File

@ -17,8 +17,8 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import { gradient } from "actions" import { gradient } from "actions"
import UpdateUserInfoModal from "./_components/UpdateUserInfoModal.svelte" import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
import ChangePasswordModal from "./_components/ChangePasswordModal.svelte" import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
let loaded = false let loaded = false
let userInfoModal let userInfoModal
@ -26,12 +26,14 @@
onMount(async () => { onMount(async () => {
await organisation.init() await organisation.init()
await apps.load(AppStatus.DEV) await apps.load()
loaded = true loaded = true
}) })
$: publishedApps = $apps.filter(app => app.status === AppStatus.DEPLOYED)
</script> </script>
{#if loaded} {#if $auth.user && loaded}
<div class="container"> <div class="container">
<Page> <Page>
<div class="content"> <div class="content">
@ -71,17 +73,17 @@
</ActionMenu> </ActionMenu>
</div> </div>
<Divider /> <Divider />
{#if $apps.length} {#if publishedApps.length}
<Heading>Apps</Heading> <Heading>Apps</Heading>
<div class="group"> <div class="group">
<Layout gap="S" noPadding> <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}`}> <a class="app" target="_blank" href={`/${app.appId}`}>
<div class="preview" use:gradient={{ seed: app.name }} /> <div class="preview" use:gradient={{ seed: app.name }} />
<div class="app-info"> <div class="app-info">
<Heading size="XS">{app.name}</Heading> <Heading size="XS">{app.name}</Heading>
<Body size="S"> <Body size="S">
Edited {Math.round(Math.random() * 10 + 1)} months ago Updated {Math.round(Math.random() * 10 + 1)} months ago
</Body> </Body>
</div> </div>
<Icon name="ChevronRight" /> <Icon name="ChevronRight" />

View File

@ -13,26 +13,25 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import ConfigChecklist from "components/common/ConfigChecklist.svelte" import ConfigChecklist from "components/common/ConfigChecklist.svelte"
import { organisation, auth } from "stores/portal" import { organisation, auth } from "stores/portal"
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
import { onMount } from "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 loaded = false
let userInfoModal
let changePasswordModal
const menu = [ const menu = [
{ title: "Apps", href: "/builder/portal/apps" }, { title: "Apps", href: "/builder/portal/apps" },
{ title: "Drafts", href: "/builder/portal/drafts" },
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" }, { title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
{ title: "Groups", href: "/builder/portal/manage/groups" },
{ title: "Auth", href: "/builder/portal/manage/auth" }, { title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/email" }, { title: "Email", href: "/builder/portal/manage/email" },
{ {
title: "General", title: "Organisation",
href: "/builder/portal/settings/general", href: "/builder/portal/settings/organisation",
heading: "Settings", heading: "Settings",
}, },
{ title: "Theming", href: "/builder/portal/theming" }, { title: "Theming", href: "/builder/portal/settings/theming" },
{ title: "Account", href: "/builder/portal/account" },
] ]
onMount(async () => { onMount(async () => {
@ -48,7 +47,7 @@
}) })
</script> </script>
{#if loaded} {#if $auth.user && loaded}
<div class="container"> <div class="container">
<div class="nav"> <div class="nav">
<Layout paddingX="L" paddingY="L"> <Layout paddingX="L" paddingY="L">
@ -75,14 +74,20 @@
</div> </div>
<div class="main"> <div class="main">
<div class="toolbar"> <div class="toolbar">
<Search placeholder="Global search" /> <div />
<ActionMenu align="right"> <ActionMenu align="right">
<div slot="control" class="avatar"> <div slot="control" class="avatar">
<Avatar size="M" name="John Doe" /> <Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" /> <Icon size="XL" name="ChevronDown" />
</div> </div>
<MenuItem icon="Settings" on:click={oldSettingsModal.show}> <MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
Old settings Update user information
</MenuItem>
<MenuItem
icon="LockClosed"
on:click={() => changePasswordModal.show()}
>
Update password
</MenuItem> </MenuItem>
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}> <MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
Close developer mode Close developer mode
@ -95,8 +100,11 @@
</div> </div>
</div> </div>
</div> </div>
<Modal bind:this={oldSettingsModal} width="30%"> <Modal bind:this={userInfoModal}>
<BuilderSettingsModal /> <UpdateUserInfoModal />
</Modal>
<Modal bind:this={changePasswordModal}>
<ChangePasswordModal />
</Modal> </Modal>
{/if} {/if}

View File

@ -26,15 +26,39 @@
import { AppStatus } from "constants" import { AppStatus } from "constants"
let layout = "grid" let layout = "grid"
let appStatus = AppStatus.PUBLISHED let sortBy = "name"
let template let template
let appToDelete let selectedApp
let creationModal let creationModal
let deletionModal let deletionModal
let unpublishModal
let creatingApp = false let creatingApp = false
let loaded = 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 checkKeys = async () => {
const response = await api.get(`/api/keys/`) const response = await api.get(`/api/keys/`)
@ -60,19 +84,19 @@
creatingApp = false creatingApp = false
} }
const openApp = app => { const viewApp = app => {
if (app.lockedBy && app.lockedBy?.email !== $auth.user?.email) { const id = app.deployed ? app.prodId : app.devId
window.open(`/${id}`, "_blank")
}
const editApp = app => {
if (app.lockedOther) {
notifications.error( notifications.error(
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.` `App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
) )
return return
} }
$goto(`../../app/${app.devId}`)
if (appStatus === AppStatus.DEV) {
$goto(`../../app/${app.appId}`)
} else {
window.open(`/${app.appId}`, "_blank")
}
} }
const exportApp = app => { const exportApp = app => {
@ -82,36 +106,66 @@
app.name app.name
)}` )}`
) )
notifications.success("App export complete") notifications.success("App exported successfully")
} catch (err) { } catch (err) {
console.error(err) notifications.error(`Error exporting app: ${err}`)
notifications.error("App export failed") }
}
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 => { const deleteApp = app => {
appToDelete = app selectedApp = app
deletionModal.show() deletionModal.show()
} }
const confirmDeleteApp = async () => { const confirmDeleteApp = async () => {
if (!appToDelete) { if (!selectedApp) {
return return
} }
await del(`/api/applications/${appToDelete?.appId}`) try {
const response = await del(`/api/applications/${selectedApp?.devId}`)
if (response.status !== 200) {
const json = await response.json()
throw json.message
}
await apps.load() await apps.load()
appToDelete = null notifications.success("App deleted successfully")
notifications.success("App deleted successfully.") } catch (err) {
notifications.error(`Error deleting app: ${err}`)
}
selectedApp = null
} }
const releaseLock = async appId => { const releaseLock = async app => {
try { try {
const response = await del(`/api/dev/${appId}/lock`) const response = await del(`/api/dev/${app.devId}/lock`)
if (response.status !== 200) {
const json = await response.json() const json = await response.json()
if (response.status !== 200) throw json.message throw json.message
}
notifications.success("Lock released") await apps.load()
await apps.load(appStatus) notifications.success("Lock released successfully")
} catch (err) { } catch (err) {
notifications.error(`Error releasing lock: ${err}`) notifications.error(`Error releasing lock: ${err}`)
} }
@ -119,12 +173,13 @@
onMount(async () => { onMount(async () => {
checkKeys() checkKeys()
await apps.load(appStatus) await apps.load()
loaded = true loaded = true
}) })
</script> </script>
<Page wide> <Page wide>
{#if loaded && enrichedApps.length}
<Layout noPadding> <Layout noPadding>
<div class="title"> <div class="title">
<Heading>Apps</Heading> <Heading>Apps</Heading>
@ -136,10 +191,12 @@
<div class="filter"> <div class="filter">
<div class="select"> <div class="select">
<Select <Select
bind:value={appStatus} bind:value={sortBy}
placeholder={null}
options={[ options={[
{ label: "Published", value: AppStatus.PUBLISHED }, { label: "Sort by name", value: "name" },
{ label: "In Development", value: AppStatus.DEV }, { label: "Sort by recently updated", value: "updated" },
{ label: "Sort by status", value: "status" },
]} ]}
/> />
</div> </div>
@ -158,27 +215,26 @@
/> />
</ActionGroup> </ActionGroup>
</div> </div>
{#if loaded && $apps.length}
<div <div
class:appGrid={layout === "grid"} class:appGrid={layout === "grid"}
class:appTable={layout === "table"} class:appTable={layout === "table"}
> >
{#each $apps as app, idx (app.appId)} {#each enrichedApps as app (app.appId)}
<svelte:component <svelte:component
this={layout === "grid" ? AppCard : AppRow} this={layout === "grid" ? AppCard : AppRow}
deletable={appStatus === AppStatus.PUBLISHED}
{releaseLock} {releaseLock}
{app} {app}
{openApp} {unpublishApp}
{viewApp}
{editApp}
{exportApp} {exportApp}
{deleteApp} {deleteApp}
last={idx === $apps.length - 1}
/> />
{/each} {/each}
</div> </div>
{/if}
</Layout> </Layout>
{#if !$apps.length && !creatingApp && loaded} {/if}
{#if !enrichedApps.length && !creatingApp && loaded}
<div class="empty-wrapper"> <div class="empty-wrapper">
<Modal inline> <Modal inline>
<ModalContent <ModalContent
@ -215,7 +271,15 @@
okText="Delete app" okText="Delete app"
onOk={confirmDeleteApp} 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> </ConfirmDialog>
<style> <style>
@ -228,7 +292,7 @@
} }
.select { .select {
width: 150px; width: 190px;
} }
.appGrid { .appGrid {
@ -239,7 +303,7 @@
.appTable { .appTable {
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
grid-template-columns: 1fr 1fr 1fr auto; grid-template-columns: 1fr 1fr 1fr 1fr auto;
align-items: center; align-items: center;
} }
.appTable :global(> div) { .appTable :global(> div) {
@ -253,10 +317,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
padding: 0 var(--spacing-s); padding: 0 var(--spacing-s);
} }
.appTable :global(> div:not(.last)) { .appTable :global(> div) {
border-bottom: var(--border-light); border-bottom: var(--border-light);
} }
.empty-wrapper { .empty-wrapper {
flex: 1 1 auto; flex: 1 1 auto;
height: 100%; height: 100%;

View File

@ -66,10 +66,11 @@
<Layout> <Layout>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="M">General</Heading> <Heading size="M">Organisation</Heading>
<Body> <Body>
General is the place where you edit your organisation name, logo. You can Organisation settings is where you can edit your organisation name and
also configure your platform URL as well as turn on or off analytics. logo. You can also configure your platform URL and enable or disable
analytics.
</Body> </Body>
</Layout> </Layout>
<Divider size="S" /> <Divider size="S" />

View File

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

View File

@ -1,15 +1,49 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { get } from "builderStore/api" import { get } from "builderStore/api"
import { AppStatus } from "../../constants"
export function createAppStore() { export function createAppStore() {
const store = writable([]) const store = writable([])
async function load(status = "") { async function load() {
try { try {
const res = await get(`/api/applications?status=${status}`) const res = await get(`/api/applications?status=all`)
const json = await res.json() const json = await res.json()
if (res.ok && Array.isArray(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 { } else {
store.set([]) store.set([])
} }

View File

@ -18,9 +18,9 @@ const StaticDatabases = {
} }
const AppStatus = { const AppStatus = {
DEV: "dev", DEV: "development",
ALL: "all", ALL: "all",
DEPLOYED: "PUBLISHED", DEPLOYED: "published",
} }
const DocumentTypes = { const DocumentTypes = {

View File

@ -1,4 +1,6 @@
const { Client } = require("pg") const { Pool } = require("pg")
let pool
const SCHEMA = { const SCHEMA = {
docs: "https://node-postgres.com", docs: "https://node-postgres.com",
@ -51,31 +53,39 @@ const SCHEMA = {
class PostgresIntegration { class PostgresIntegration {
constructor(config) { constructor(config) {
this.config = config this.config = config
this.client = new Client(config) if (!pool) {
this.connect() pool = new Pool(this.config)
}
} }
async connect() { async query(sql) {
return this.client.connect() try {
this.client = await pool.connect()
return await this.client.query(sql)
} catch (err) {
throw new Error(err)
} finally {
this.client.release()
}
} }
async create({ sql }) { async create({ sql }) {
const response = await this.client.query(sql) const response = await this.query(sql)
return response.rows.length ? response.rows : [{ created: true }] return response.rows.length ? response.rows : [{ created: true }]
} }
async read({ sql }) { async read({ sql }) {
const response = await this.client.query(sql) const response = await this.query(sql)
return response.rows return response.rows
} }
async update({ sql }) { async update({ sql }) {
const response = await this.client.query(sql) const response = await this.query(sql)
return response.rows.length ? response.rows : [{ updated: true }] return response.rows.length ? response.rows : [{ updated: true }]
} }
async delete({ sql }) { async delete({ sql }) {
const response = await this.client.query(sql) const response = await this.query(sql)
return response.rows.length ? response.rows : [{ deleted: true }] return response.rows.length ? response.rows : [{ deleted: true }]
} }
} }