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/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",

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

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

View File

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

View File

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

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,
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;

View File

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

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 = {
DEV: "dev",
PUBLISHED: "published",
ALL: "all",
DEV: "development",
DEPLOYED: "published",
}
// fields on the user table that cannot be edited

View File

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

View File

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

View File

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

View File

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

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 { 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([])
}

View File

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

View File

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