Merge branch 'master' into BUDI-8084/single-attachment-column-setting
This commit is contained in:
commit
ec282189ba
|
@ -1,60 +1,54 @@
|
||||||
<script context="module">
|
|
||||||
export const directions = ["n", "ne", "e", "se", "s", "sw", "w", "nw"]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import {
|
||||||
import { fade } from "svelte/transition"
|
default as AbsTooltip,
|
||||||
|
TooltipPosition,
|
||||||
|
TooltipType,
|
||||||
|
} from "../Tooltip/AbsTooltip.svelte"
|
||||||
|
|
||||||
export let direction = "n"
|
|
||||||
export let name = "Add"
|
export let name = "Add"
|
||||||
export let hidden = false
|
export let hidden = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let hoverable = false
|
export let hoverable = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let color
|
export let color
|
||||||
|
export let hoverColor
|
||||||
export let tooltip
|
export let tooltip
|
||||||
|
export let tooltipPosition = TooltipPosition.Bottom
|
||||||
|
export let tooltipType = TooltipType.Default
|
||||||
|
export let tooltipColor
|
||||||
|
export let tooltipWrap = true
|
||||||
export let newStyles = false
|
export let newStyles = false
|
||||||
|
|
||||||
$: rotation = getRotation(direction)
|
|
||||||
|
|
||||||
let showTooltip = false
|
|
||||||
|
|
||||||
const getRotation = direction => {
|
|
||||||
return directions.indexOf(direction) * 45
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<AbsTooltip
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
text={tooltip}
|
||||||
<div
|
type={tooltipType}
|
||||||
class="icon"
|
position={tooltipPosition}
|
||||||
class:newStyles
|
color={tooltipColor}
|
||||||
on:mouseover={() => (showTooltip = true)}
|
noWrap={tooltipWrap}
|
||||||
on:focus={() => (showTooltip = true)}
|
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
|
||||||
on:click={() => (showTooltip = false)}
|
|
||||||
>
|
>
|
||||||
<svg
|
<div class="icon" class:newStyles>
|
||||||
on:click
|
<svg
|
||||||
class:hoverable
|
on:click
|
||||||
class:disabled
|
class:hoverable
|
||||||
class="spectrum-Icon spectrum-Icon--size{size}"
|
class:disabled
|
||||||
focusable="false"
|
class="spectrum-Icon spectrum-Icon--size{size}"
|
||||||
aria-hidden={hidden}
|
focusable="false"
|
||||||
aria-label={name}
|
aria-hidden={hidden}
|
||||||
style={`transform: rotate(${rotation}deg); ${
|
aria-label={name}
|
||||||
color ? `color: ${color};` : ""
|
style={`${color ? `color: ${color};` : ""} ${
|
||||||
}`}
|
hoverColor
|
||||||
>
|
? `--hover-color: ${hoverColor}`
|
||||||
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-{name}" />
|
: "--hover-color: var(--spectrum-alias-icon-color-selected-hover)"
|
||||||
</svg>
|
}`}
|
||||||
{#if tooltip && showTooltip}
|
>
|
||||||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
<use
|
||||||
<Tooltip textWrapping direction="top" text={tooltip} />
|
style="pointer-events: none;"
|
||||||
</div>
|
xlink:href="#spectrum-icon-18-{name}"
|
||||||
{/if}
|
/>
|
||||||
</div>
|
</svg>
|
||||||
|
</div>
|
||||||
|
</AbsTooltip>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -71,7 +65,7 @@
|
||||||
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
}
|
}
|
||||||
svg.hoverable:hover {
|
svg.hoverable:hover {
|
||||||
color: var(--spectrum-alias-icon-color-selected-hover) !important;
|
color: var(--hover-color) !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
svg.hoverable:active {
|
svg.hoverable:active {
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
export let text = ""
|
export let text = ""
|
||||||
export let fixed = false
|
export let fixed = false
|
||||||
export let color = null
|
export let color = null
|
||||||
|
export let noWrap = false
|
||||||
|
|
||||||
let wrapper
|
let wrapper
|
||||||
let hovered = false
|
let hovered = false
|
||||||
|
@ -105,6 +106,7 @@
|
||||||
<Portal target=".spectrum">
|
<Portal target=".spectrum">
|
||||||
<span
|
<span
|
||||||
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
|
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
|
||||||
|
class:noWrap
|
||||||
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
|
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
|
||||||
transition:fade|local={{ duration: 130 }}
|
transition:fade|local={{ duration: 130 }}
|
||||||
>
|
>
|
||||||
|
@ -118,6 +120,9 @@
|
||||||
.abs-tooltip {
|
.abs-tooltip {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
.spectrum-Tooltip.noWrap .spectrum-Tooltip-label {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
.spectrum-Tooltip {
|
.spectrum-Tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
|
|
@ -19,7 +19,7 @@ export { default as ActionMenu } from "./ActionMenu/ActionMenu.svelte"
|
||||||
export { default as Button } from "./Button/Button.svelte"
|
export { default as Button } from "./Button/Button.svelte"
|
||||||
export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.svelte"
|
export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.svelte"
|
||||||
export { default as ClearButton } from "./ClearButton/ClearButton.svelte"
|
export { default as ClearButton } from "./ClearButton/ClearButton.svelte"
|
||||||
export { default as Icon, directions } from "./Icon/Icon.svelte"
|
export { default as Icon } from "./Icon/Icon.svelte"
|
||||||
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
|
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
|
||||||
export { default as Toggle } from "./Form/Toggle.svelte"
|
export { default as Toggle } from "./Form/Toggle.svelte"
|
||||||
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
class="searchButton"
|
class="searchButton"
|
||||||
class:hide={search}
|
class:hide={search}
|
||||||
>
|
>
|
||||||
<Icon size="S" name="Search" />
|
<Icon size="S" name="Search" hoverable hoverColor="var(--ink)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
class="addButton"
|
class="addButton"
|
||||||
class:rotate={search}
|
class:rotate={search}
|
||||||
>
|
>
|
||||||
<Icon name="Add" />
|
<Icon name="Add" hoverable hoverColor="var(--ink)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let iconTooltip
|
export let iconTooltip
|
||||||
export let withArrow = false
|
export let withArrow = false
|
||||||
export let withActions = true
|
export let withActions = true
|
||||||
|
export let showActions = false
|
||||||
export let indentLevel = 0
|
export let indentLevel = 0
|
||||||
export let text
|
export let text
|
||||||
export let border = true
|
export let border = true
|
||||||
|
@ -68,10 +69,11 @@
|
||||||
class:border
|
class:border
|
||||||
class:selected
|
class:selected
|
||||||
class:withActions
|
class:withActions
|
||||||
|
class:showActions
|
||||||
|
class:actionsOpen={highlighted && withActions}
|
||||||
class:scrollable
|
class:scrollable
|
||||||
class:highlighted
|
class:highlighted
|
||||||
class:selectedBy
|
class:selectedBy
|
||||||
class:actionsOpen={highlighted && withActions}
|
|
||||||
on:dragend
|
on:dragend
|
||||||
on:dragstart
|
on:dragstart
|
||||||
on:dragover
|
on:dragover
|
||||||
|
@ -170,7 +172,8 @@
|
||||||
}
|
}
|
||||||
.nav-item:hover .actions,
|
.nav-item:hover .actions,
|
||||||
.hovering .actions,
|
.hovering .actions,
|
||||||
.nav-item.withActions.actionsOpen .actions {
|
.nav-item.withActions.actionsOpen .actions,
|
||||||
|
.nav-item.withActions.showActions .actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.nav-item-content {
|
.nav-item-content {
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import {
|
import {
|
||||||
previewStore,
|
previewStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
let appActionPopoverAnchor
|
let appActionPopoverAnchor
|
||||||
let publishing = false
|
let publishing = false
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(app => app.devId === application)
|
$: filteredApps = $appsStore.apps.filter(app => app.devId === application)
|
||||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
$: latestDeployments = $deploymentStore
|
$: latestDeployments = $deploymentStore
|
||||||
.filter(deployment => deployment.status === "SUCCESS")
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await API.unpublishApp(selectedApp.prodId)
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
await apps.load()
|
await appsStore.load()
|
||||||
notifications.send("App unpublished", {
|
notifications.send("App unpublished", {
|
||||||
type: "success",
|
type: "success",
|
||||||
icon: "GlobeStrike",
|
icon: "GlobeStrike",
|
||||||
|
@ -141,7 +141,7 @@
|
||||||
|
|
||||||
const completePublish = async () => {
|
const completePublish = async () => {
|
||||||
try {
|
try {
|
||||||
await apps.load()
|
await appsStore.load()
|
||||||
await deploymentStore.load()
|
await deploymentStore.load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error refreshing app")
|
notifications.error("Error refreshing app")
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Input, notifications } from "@budibase/bbui"
|
import { Input, notifications } from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
export let appId
|
export let appId
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
deleting = true
|
deleting = true
|
||||||
try {
|
try {
|
||||||
await API.deleteApp(appId)
|
await API.deleteApp(appId)
|
||||||
apps.load()
|
appsStore.load()
|
||||||
notifications.success("App deleted successfully")
|
notifications.success("App deleted successfully")
|
||||||
onDeleteSuccess()
|
onDeleteSuccess()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -6,10 +6,13 @@
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import AppRowContext from "./AppRowContext.svelte"
|
import AppRowContext from "./AppRowContext.svelte"
|
||||||
|
import FavouriteAppButton from "pages/builder/portal/apps/FavouriteAppButton.svelte"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let lockedAction
|
export let lockedAction
|
||||||
|
|
||||||
|
let actionsOpen = false
|
||||||
|
|
||||||
$: editing = app.sessions?.length
|
$: editing = app.sessions?.length
|
||||||
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
||||||
$: unclickable = !isBuilder && !app.deployed
|
$: unclickable = !isBuilder && !app.deployed
|
||||||
|
@ -43,8 +46,10 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="app-row"
|
class="app-row"
|
||||||
on:click={lockedAction || handleDefaultClick}
|
|
||||||
class:unclickable
|
class:unclickable
|
||||||
|
class:actionsOpen
|
||||||
|
class:favourite={app.favourite}
|
||||||
|
on:click={lockedAction || handleDefaultClick}
|
||||||
>
|
>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="app-icon">
|
<div class="app-icon">
|
||||||
|
@ -75,19 +80,35 @@
|
||||||
<Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body>
|
<Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isBuilder}
|
<div class="actions-wrap">
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
<Button size="S" secondary on:click={lockedAction || goToBuilder}>
|
{#if isBuilder}
|
||||||
Edit
|
<div class="row-action">
|
||||||
</Button>
|
<Button size="S" secondary on:click={lockedAction || goToBuilder}>
|
||||||
<AppRowContext {app} />
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="row-action">
|
||||||
|
<AppRowContext
|
||||||
|
{app}
|
||||||
|
on:open={() => {
|
||||||
|
actionsOpen = true
|
||||||
|
}}
|
||||||
|
on:close={() => {
|
||||||
|
actionsOpen = false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- this can happen if an app builder has app user access to an app -->
|
||||||
|
<Button size="S" secondary>View</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if app.deployed}
|
|
||||||
<!-- this can happen if an app builder has app user access to an app -->
|
<div class="favourite-icon">
|
||||||
<div class="app-row-actions">
|
<FavouriteAppButton {app} noWrap />
|
||||||
<Button size="S" secondary>View</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -107,6 +128,16 @@
|
||||||
border-color: var(--spectrum-global-color-gray-300);
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-row .favourite-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-row:hover .favourite-icon,
|
||||||
|
.app-row.favourite .favourite-icon,
|
||||||
|
.app-row.actionsOpen .favourite-icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.updated {
|
.updated {
|
||||||
color: var(--spectrum-global-color-gray-700);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -142,11 +173,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-row-actions {
|
.app-row-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-row:hover .app-row-actions,
|
||||||
|
.app-row.actionsOpen .app-row-actions {
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-wrap {
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: var(--spectrum-alias-item-height-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
|
|
@ -4,15 +4,69 @@
|
||||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
import ExportAppModal from "./ExportAppModal.svelte"
|
import ExportAppModal from "./ExportAppModal.svelte"
|
||||||
import DuplicateAppModal from "./DuplicateAppModal.svelte"
|
import DuplicateAppModal from "./DuplicateAppModal.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
import { licensing } from "stores/portal"
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let align = "right"
|
export let align = "right"
|
||||||
|
export let options
|
||||||
|
|
||||||
let deleteModal
|
let deleteModal
|
||||||
let exportModal
|
let exportModal
|
||||||
let duplicateModal
|
let duplicateModal
|
||||||
let exportPublishedVersion = false
|
let exportPublishedVersion = false
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
const getActions = app => {
|
||||||
|
if (!loaded) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "duplicate",
|
||||||
|
icon: "Copy",
|
||||||
|
onClick: duplicateModal.show,
|
||||||
|
body: "Duplicate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "exportDev",
|
||||||
|
icon: "Export",
|
||||||
|
onClick: () => {
|
||||||
|
exportPublishedVersion = false
|
||||||
|
exportModal.show()
|
||||||
|
},
|
||||||
|
body: "Export latest edited app",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "exportProd",
|
||||||
|
icon: "Export",
|
||||||
|
onClick: () => {
|
||||||
|
exportPublishedVersion = true
|
||||||
|
exportModal.show()
|
||||||
|
},
|
||||||
|
body: "Export latest published app",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delete",
|
||||||
|
icon: "Delete",
|
||||||
|
onClick: deleteModal.show,
|
||||||
|
body: "Delete",
|
||||||
|
},
|
||||||
|
].filter(action => {
|
||||||
|
if (action.id === "exportProd" && app.deployed !== true) {
|
||||||
|
return false
|
||||||
|
} else if (Array.isArray(options) && !options.includes(action.id)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: actions = getActions(app, loaded)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
let appLimitModal
|
let appLimitModal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -45,44 +99,10 @@
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon">
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem
|
|
||||||
icon="Copy"
|
{#each actions as action}
|
||||||
on:click={() => {
|
<MenuItem icon={action.icon} on:click={action.onClick}>
|
||||||
if ($licensing?.usageMetrics?.apps < 100) {
|
{action.body}
|
||||||
duplicateModal.show()
|
|
||||||
} else {
|
|
||||||
appLimitModal.show()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Duplicate
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="Export"
|
|
||||||
on:click={() => {
|
|
||||||
exportPublishedVersion = false
|
|
||||||
exportModal.show()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export latest edited app
|
|
||||||
</MenuItem>
|
|
||||||
{#if app.deployed}
|
|
||||||
<MenuItem
|
|
||||||
icon="Export"
|
|
||||||
on:click={() => {
|
|
||||||
exportPublishedVersion = true
|
|
||||||
exportModal.show()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export latest published app
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/if}
|
{/each}
|
||||||
<MenuItem
|
|
||||||
icon="Delete"
|
|
||||||
on:click={() => {
|
|
||||||
deleteModal.show()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</MenuItem>
|
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
Label,
|
Label,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await apps.update(app.instance._id, {
|
await appsStore.save(app.instance._id, {
|
||||||
icon: { name, color },
|
icon: { name, color },
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -9,13 +9,14 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { initialise } from "stores/builder"
|
import { initialise } from "stores/builder"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { apps, admin, auth } from "stores/portal"
|
import { appsStore, admin, auth } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
import { lowercase } from "helpers"
|
import { lowercase } from "helpers"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
|
||||||
|
@ -92,7 +93,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupValidation = async () => {
|
const setupValidation = async () => {
|
||||||
const applications = svelteGet(apps)
|
const applications = svelteGet(appsStore).apps
|
||||||
appValidation.name(validation, { apps: applications })
|
appValidation.name(validation, { apps: applications })
|
||||||
appValidation.url(validation, { apps: applications })
|
appValidation.url(validation, { apps: applications })
|
||||||
appValidation.file(validation, { template })
|
appValidation.file(validation, { template })
|
||||||
|
@ -141,6 +142,11 @@
|
||||||
// Create user
|
// Create user
|
||||||
await auth.setInitInfo({})
|
await auth.setInitInfo({})
|
||||||
|
|
||||||
|
if (!sdk.users.isBuilder($auth.user, createdApp?.appId)) {
|
||||||
|
// Refresh for access to created applications
|
||||||
|
await auth.getSelf()
|
||||||
|
}
|
||||||
|
|
||||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
creating = false
|
creating = false
|
||||||
|
|
|
@ -9,9 +9,10 @@
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore, auth } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let appId
|
export let appId
|
||||||
export let appName
|
export let appName
|
||||||
|
@ -67,8 +68,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.duplicateApp(data, appId)
|
const app = await API.duplicateApp(data, appId)
|
||||||
apps.load()
|
appsStore.load()
|
||||||
|
if (!sdk.users.isBuilder($auth.user, app?.duplicateAppId)) {
|
||||||
|
// Refresh for access to created applications
|
||||||
|
await auth.getSelf()
|
||||||
|
}
|
||||||
onDuplicateSuccess()
|
onDuplicateSuccess()
|
||||||
notifications.success("App duplicated successfully")
|
notifications.success("App duplicated successfully")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -78,7 +83,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupValidation = async () => {
|
const setupValidation = async () => {
|
||||||
const applications = get(apps)
|
const applications = get(appsStore).apps
|
||||||
appValidation.name(validation, { apps: applications })
|
appValidation.name(validation, { apps: applications })
|
||||||
appValidation.url(validation, { apps: applications })
|
appValidation.url(validation, { apps: applications })
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
Layout,
|
Layout,
|
||||||
Label,
|
Label,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupValidation = async () => {
|
const setupValidation = async () => {
|
||||||
const applications = svelteGet(apps)
|
const applications = svelteGet(appsStore).apps
|
||||||
appValidation.name(validation, {
|
appValidation.name(validation, {
|
||||||
apps: applications,
|
apps: applications,
|
||||||
currentApp: {
|
currentApp: {
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
async function updateApp() {
|
async function updateApp() {
|
||||||
try {
|
try {
|
||||||
await apps.update(app.appId, {
|
await appsStore.save(app.appId, {
|
||||||
name: $values.name?.trim(),
|
name: $values.name?.trim(),
|
||||||
url: $values.url?.trim(),
|
url: $values.url?.trim(),
|
||||||
icon: {
|
icon: {
|
||||||
|
|
|
@ -22,6 +22,7 @@ body {
|
||||||
--grey-7: var(--spectrum-global-color-gray-700);
|
--grey-7: var(--spectrum-global-color-gray-700);
|
||||||
--grey-8: var(--spectrum-global-color-gray-800);
|
--grey-8: var(--spectrum-global-color-gray-800);
|
||||||
--grey-9: var(--spectrum-global-color-gray-900);
|
--grey-9: var(--spectrum-global-color-gray-900);
|
||||||
|
--spectrum-global-color-yellow-1000: #d8b500;
|
||||||
|
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background-color: var(--background-alt);
|
background-color: var(--background-alt);
|
||||||
|
|
|
@ -15,7 +15,14 @@
|
||||||
FancySelect,
|
FancySelect,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { builderStore, appStore, roles } from "stores/builder"
|
import { builderStore, appStore, roles } from "stores/builder"
|
||||||
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
import {
|
||||||
|
groups,
|
||||||
|
licensing,
|
||||||
|
appsStore,
|
||||||
|
users,
|
||||||
|
auth,
|
||||||
|
admin,
|
||||||
|
} from "stores/portal"
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
Constants,
|
Constants,
|
||||||
|
@ -54,7 +61,7 @@
|
||||||
|
|
||||||
let inviteFailureResponse = ""
|
let inviteFailureResponse = ""
|
||||||
$: validEmail = emailValidator(email) === true
|
$: validEmail = emailValidator(email) === true
|
||||||
$: prodAppId = apps.getProdAppID($appStore.appId)
|
$: prodAppId = appsStore.getProdAppID($appStore.appId)
|
||||||
$: promptInvite = showInvite(
|
$: promptInvite = showInvite(
|
||||||
filteredInvites,
|
filteredInvites,
|
||||||
filteredUsers,
|
filteredUsers,
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
userStore,
|
userStore,
|
||||||
deploymentStore,
|
deploymentStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { auth, apps } from "stores/portal"
|
import { auth, appsStore } from "stores/portal"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
const pkg = await API.fetchAppPackage(application)
|
const pkg = await API.fetchAppPackage(application)
|
||||||
await initialise(pkg)
|
await initialise(pkg)
|
||||||
|
|
||||||
await apps.load()
|
await appsStore.load()
|
||||||
await deploymentStore.load()
|
await deploymentStore.load()
|
||||||
|
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { getContext, onDestroy, onMount } from "svelte"
|
import { getContext, onDestroy, onMount } from "svelte"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { auth, licensing, admin, apps } from "stores/portal"
|
import { auth, licensing, admin, appsStore } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
let status = null
|
let status = null
|
||||||
let timeRange = null
|
let timeRange = null
|
||||||
let loaded = false
|
let loaded = false
|
||||||
$: app = $apps.find(app => $appStore.appId?.includes(app.appId))
|
$: app = $appsStore.apps.find(app => $appStore.appId?.includes(app.appId))
|
||||||
$: licensePlan = $auth.user?.license?.plan
|
$: licensePlan = $auth.user?.license?.plan
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchLogs(automationId, status, page, timeRange)
|
$: fetchLogs(automationId, status, page, timeRange)
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
|
|
||||||
async function save({ detail }) {
|
async function save({ detail }) {
|
||||||
try {
|
try {
|
||||||
await apps.update($appStore.appId, {
|
await appsStore.save($appStore.appId, {
|
||||||
automations: {
|
automations: {
|
||||||
chainAutomations: detail,
|
chainAutomations: detail,
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import { appStore } from "stores/builder"
|
import { appStore } from "stores/builder"
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(app => app.devId == $appStore.appId)
|
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
||||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
$: appUrl = `${window.origin}/embed${app?.url}`
|
$: appUrl = `${window.origin}/embed${app?.url}`
|
||||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
Modal,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import { appStore } from "stores/builder"
|
import { appStore } from "stores/builder"
|
||||||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||||
import ImportAppModal from "components/start/ImportAppModal.svelte"
|
import ImportAppModal from "components/start/ImportAppModal.svelte"
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(app => app.devId === $appStore.appId)
|
$: filteredApps = $appsStore.apps.filter(app => app.devId === $appStore.appId)
|
||||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,13 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import { appStore, initialise } from "stores/builder"
|
import { appStore, initialise } from "stores/builder"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
let updatingModal
|
let updatingModal
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(app => app.devId == $appStore.appId)
|
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
||||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,14 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { apps, organisation, auth, groups, licensing } from "stores/portal"
|
import {
|
||||||
|
appsStore,
|
||||||
|
organisation,
|
||||||
|
auth,
|
||||||
|
groups,
|
||||||
|
licensing,
|
||||||
|
enrichedApps,
|
||||||
|
} from "stores/portal"
|
||||||
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"
|
||||||
|
@ -31,7 +38,9 @@
|
||||||
$: userGroups = $groups.filter(group =>
|
$: userGroups = $groups.filter(group =>
|
||||||
group.users.find(user => user._id === $auth.user?._id)
|
group.users.find(user => user._id === $auth.user?._id)
|
||||||
)
|
)
|
||||||
$: publishedApps = $apps.filter(app => app.status === AppStatus.DEPLOYED)
|
$: publishedApps = $enrichedApps.filter(
|
||||||
|
app => app.status === AppStatus.DEPLOYED
|
||||||
|
)
|
||||||
$: userApps = getUserApps(publishedApps, userGroups, $auth.user)
|
$: userApps = getUserApps(publishedApps, userGroups, $auth.user)
|
||||||
|
|
||||||
function getUserApps(publishedApps, userGroups, user) {
|
function getUserApps(publishedApps, userGroups, user) {
|
||||||
|
@ -46,12 +55,12 @@
|
||||||
return userGroups.find(group => {
|
return userGroups.find(group => {
|
||||||
return groups.actions
|
return groups.actions
|
||||||
.getGroupAppIds(group)
|
.getGroupAppIds(group)
|
||||||
.map(role => apps.extractAppId(role))
|
.map(role => appsStore.extractAppId(role))
|
||||||
.includes(app.appId)
|
.includes(app.appId)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Object.keys($auth.user?.roles)
|
return Object.keys($auth.user?.roles)
|
||||||
.map(x => apps.extractAppId(x))
|
.map(x => appsStore.extractAppId(x))
|
||||||
.includes(app.appId)
|
.includes(app.appId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -76,7 +85,7 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
await apps.load()
|
await appsStore.load()
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error loading apps")
|
notifications.error("Error loading apps")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, redirect, goto, url } from "@roxi/routify"
|
import { isActive, redirect, goto, url } from "@roxi/routify"
|
||||||
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
||||||
import { organisation, auth, menu, apps } from "stores/portal"
|
import { organisation, auth, menu, appsStore } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
||||||
import MobileMenu from "./_components/MobileMenu.svelte"
|
import MobileMenu from "./_components/MobileMenu.svelte"
|
||||||
|
@ -16,7 +16,8 @@
|
||||||
let activeTab = "Apps"
|
let activeTab = "Apps"
|
||||||
|
|
||||||
$: $url(), updateActiveTab($menu)
|
$: $url(), updateActiveTab($menu)
|
||||||
$: isOnboarding = !$apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
$: isOnboarding =
|
||||||
|
!$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
||||||
|
|
||||||
const updateActiveTab = menu => {
|
const updateActiveTab = menu => {
|
||||||
for (let entry of menu) {
|
for (let entry of menu) {
|
||||||
|
@ -40,7 +41,7 @@
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
// We need to load apps to know if we need to show onboarding fullscreen
|
// We need to load apps to know if we need to show onboarding fullscreen
|
||||||
await Promise.all([apps.load(), organisation.init()])
|
await Promise.all([appsStore.load(), organisation.init()])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting org config")
|
notifications.error("Error getting org config")
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
Divider,
|
Divider,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { licensing, users, apps, auditLogs } from "stores/portal"
|
import { licensing, users, appsStore, auditLogs } from "stores/portal"
|
||||||
import LockedFeature from "../../_components/LockedFeature.svelte"
|
import LockedFeature from "../../_components/LockedFeature.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { onMount, setContext } from "svelte"
|
import { onMount, setContext } from "svelte"
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
enrich(parseEventObject($auditLogs.events), selectedEvents, "id"),
|
enrich(parseEventObject($auditLogs.events), selectedEvents, "id"),
|
||||||
"id"
|
"id"
|
||||||
)
|
)
|
||||||
$: sortedApps = sort(enrich($apps, selectedApps, "appId"), "name")
|
$: sortedApps = sort(enrich($appsStore.apps, selectedApps, "appId"), "name")
|
||||||
|
|
||||||
const debounce = value => {
|
const debounce = value => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, TooltipPosition, TooltipType } from "@budibase/bbui"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
export let size = "S"
|
||||||
|
export let position = TooltipPosition.Top
|
||||||
|
export let noWrap = false
|
||||||
|
export let hoverColor = "var(--ink)"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
name={app?.favourite ? "Star" : "StarOutline"}
|
||||||
|
hoverable
|
||||||
|
color={app?.favourite ? "var(--spectrum-global-color-yellow-1000)" : null}
|
||||||
|
tooltip={app?.favourite ? "Remove from favourites" : "Add to favourites"}
|
||||||
|
tooltipType={TooltipType.Info}
|
||||||
|
tooltipPosition={position}
|
||||||
|
tooltipWrap={noWrap}
|
||||||
|
{hoverColor}
|
||||||
|
{size}
|
||||||
|
on:click={async e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const userAppFavourites = new Set([...($auth.user.appFavourites || [])])
|
||||||
|
let processedAppIds = []
|
||||||
|
|
||||||
|
if ($auth.user.appFavourites && app?.appId) {
|
||||||
|
if (userAppFavourites.has(app.appId)) {
|
||||||
|
userAppFavourites.delete(app.appId)
|
||||||
|
} else {
|
||||||
|
userAppFavourites.add(app.appId)
|
||||||
|
}
|
||||||
|
processedAppIds = [...userAppFavourites]
|
||||||
|
} else {
|
||||||
|
processedAppIds = [app.appId]
|
||||||
|
}
|
||||||
|
|
||||||
|
await auth.updateSelf({
|
||||||
|
appFavourites: processedAppIds,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={!app}
|
||||||
|
/>
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { params, redirect } from "@roxi/routify"
|
import { params, redirect } from "@roxi/routify"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
|
|
||||||
$: app = $apps.find(app => app.appId === $params.appId)
|
$: app = $appsStore.apps.find(app => app.appId === $params.appId)
|
||||||
$: {
|
$: {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
$redirect("../")
|
$redirect("../")
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import { params, goto } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import { apps, auth, sideBarCollapsed } from "stores/portal"
|
import { auth, sideBarCollapsed, enrichedApps } from "stores/portal"
|
||||||
import { Link, Body, ActionButton } from "@budibase/bbui"
|
import AppRowContext from "components/start/AppRowContext.svelte"
|
||||||
|
import FavouriteAppButton from "../FavouriteAppButton.svelte"
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
TooltipPosition,
|
||||||
|
TooltipType,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import ErrorSVG from "./ErrorSVG.svelte"
|
import ErrorSVG from "./ErrorSVG.svelte"
|
||||||
|
|
||||||
$: app = $apps.find(app => app.appId === $params.appId)
|
$: app = $enrichedApps.find(app => app.appId === $params.appId)
|
||||||
$: iframeUrl = getIframeURL(app)
|
$: iframeUrl = getIframeURL(app)
|
||||||
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
||||||
|
|
||||||
|
@ -30,42 +39,63 @@
|
||||||
$: fetchScreens(app?.devId)
|
$: fetchScreens(app?.devId)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{#if $sideBarCollapsed}
|
{#if $sideBarCollapsed}
|
||||||
<ActionButton
|
<div class="headerButton" on:click={() => sideBarCollapsed.set(false)}>
|
||||||
quiet
|
<Icon
|
||||||
icon="Rail"
|
name={"Rail"}
|
||||||
on:click={() => sideBarCollapsed.set(false)}
|
hoverable
|
||||||
>
|
tooltip="Expand"
|
||||||
Menu
|
tooltipPosition={TooltipPosition.Right}
|
||||||
</ActionButton>
|
tooltipType={TooltipType.Info}
|
||||||
|
hoverColor={"var(--ink)"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ActionButton
|
<div class="headerButton" on:click={() => sideBarCollapsed.set(true)}>
|
||||||
quiet
|
<Icon
|
||||||
icon="RailRightOpen"
|
name={"RailRightOpen"}
|
||||||
on:click={() => sideBarCollapsed.set(true)}
|
hoverable
|
||||||
>
|
tooltip="Collapse"
|
||||||
Collapse
|
tooltipType={TooltipType.Info}
|
||||||
</ActionButton>
|
tooltipPosition={TooltipPosition.Top}
|
||||||
|
hoverColor={"var(--ink)"}
|
||||||
|
size="S"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if isBuilder}
|
{#if isBuilder}
|
||||||
<ActionButton
|
<Button
|
||||||
quiet
|
size="M"
|
||||||
icon="Edit"
|
secondary
|
||||||
on:click={() => $goto(`/builder/app/${app.devId}`)}
|
on:click={() => $goto(`/builder/app/${app.devId}`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</ActionButton>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<ActionButton
|
<div class="headerButton">
|
||||||
disabled={noScreens}
|
<FavouriteAppButton {app} />
|
||||||
quiet
|
</div>
|
||||||
icon="LinkOut"
|
<div class="headerButton" on:click={() => window.open(iframeUrl, "_blank")}>
|
||||||
on:click={() => window.open(iframeUrl, "_blank")}
|
<Icon
|
||||||
>
|
name="LinkOut"
|
||||||
Fullscreen
|
disabled={noScreens}
|
||||||
</ActionButton>
|
hoverable
|
||||||
|
tooltip="Open in new tab"
|
||||||
|
tooltipType={TooltipType.Info}
|
||||||
|
tooltipPosition={TooltipPosition.Top}
|
||||||
|
hoverColor={"var(--ink)"}
|
||||||
|
size="S"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AppRowContext
|
||||||
|
{app}
|
||||||
|
options={["duplicate", "delete", "exportDev", "exportProd"]}
|
||||||
|
align="left"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if noScreens}
|
{#if noScreens}
|
||||||
<div class="noScreens">
|
<div class="noScreens">
|
||||||
|
@ -83,6 +113,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.headerButton {
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -96,7 +135,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xl);
|
||||||
flex: 0 0 50px;
|
flex: 0 0 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import { apps, sideBarCollapsed, auth } from "stores/portal"
|
import { sideBarCollapsed, enrichedApps, auth } from "stores/portal"
|
||||||
import { params, goto } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import NavHeader from "components/common/NavHeader.svelte"
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
import AppRowContext from "components/start/AppRowContext.svelte"
|
import AppRowContext from "components/start/AppRowContext.svelte"
|
||||||
import { AppStatus } from "constants"
|
import FavouriteAppButton from "../FavouriteAppButton.svelte"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let searchString
|
let searchString
|
||||||
let opened
|
let opened
|
||||||
|
|
||||||
$: filteredApps = $apps
|
$: filteredApps = $enrichedApps.filter(app => {
|
||||||
.filter(app => {
|
return (
|
||||||
return (
|
!searchString ||
|
||||||
!searchString ||
|
app.name.toLowerCase().includes(searchString.toLowerCase())
|
||||||
app.name.toLowerCase().includes(searchString.toLowerCase())
|
)
|
||||||
)
|
})
|
||||||
})
|
|
||||||
.map(app => {
|
|
||||||
return {
|
|
||||||
...app,
|
|
||||||
deployed: app.status === AppStatus.DEPLOYED,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
const lowerA = a.name.toLowerCase()
|
|
||||||
const lowerB = b.name.toLowerCase()
|
|
||||||
return lowerA > lowerB ? 1 : -1
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="side-bar" class:collapsed={$sideBarCollapsed}>
|
<div class="side-bar" class:collapsed={$sideBarCollapsed}>
|
||||||
|
@ -47,27 +35,40 @@
|
||||||
selected={!$params.appId}
|
selected={!$params.appId}
|
||||||
/>
|
/>
|
||||||
{#each filteredApps as app}
|
{#each filteredApps as app}
|
||||||
<NavItem
|
<span
|
||||||
text={app.name}
|
class="side-bar-app-entry"
|
||||||
icon={app.icon?.name || "Apps"}
|
class:favourite={app.favourite}
|
||||||
iconColor={app.icon?.color}
|
class:actionsOpen={opened == app.appId}
|
||||||
selected={$params.appId === app.appId}
|
|
||||||
highlighted={opened == app.appId}
|
|
||||||
on:click={() => $goto(`./${app.appId}`)}
|
|
||||||
>
|
>
|
||||||
{#if sdk.users.isBuilder($auth.user, app?.devId)}
|
<NavItem
|
||||||
<AppRowContext
|
text={app.name}
|
||||||
{app}
|
icon={app.icon?.name || "Apps"}
|
||||||
align="left"
|
iconColor={app.icon?.color}
|
||||||
on:open={() => {
|
selected={$params.appId === app.appId}
|
||||||
opened = app.appId
|
highlighted={opened == app.appId}
|
||||||
}}
|
on:click={() => $goto(`./${app.appId}`)}
|
||||||
on:close={() => {
|
withActions
|
||||||
opened = null
|
showActions
|
||||||
}}
|
>
|
||||||
/>
|
<div class="app-entry-actions">
|
||||||
{/if}
|
{#if sdk.users.isBuilder($auth.user, app?.devId)}
|
||||||
</NavItem>
|
<AppRowContext
|
||||||
|
{app}
|
||||||
|
align="left"
|
||||||
|
on:open={() => {
|
||||||
|
opened = app.appId
|
||||||
|
}}
|
||||||
|
on:close={() => {
|
||||||
|
opened = null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="favourite-icon">
|
||||||
|
<FavouriteAppButton {app} size="XS" />
|
||||||
|
</div>
|
||||||
|
</NavItem>
|
||||||
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,4 +111,23 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-bar-app-entry :global(.nav-item-content .actions) {
|
||||||
|
width: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-app-entry:hover .app-entry-actions,
|
||||||
|
.side-bar-app-entry:hover .favourite-icon,
|
||||||
|
.side-bar-app-entry.favourite .favourite-icon,
|
||||||
|
.side-bar-app-entry.actionsOpen .app-entry-actions,
|
||||||
|
.side-bar-app-entry.actionsOpen .favourite-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-app-entry .app-entry-actions,
|
||||||
|
.side-bar-app-entry .favourite-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
admin,
|
admin,
|
||||||
apps,
|
appsStore,
|
||||||
templates,
|
templates,
|
||||||
licensing,
|
licensing,
|
||||||
groups,
|
groups,
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
import PortalSideBar from "./_components/PortalSideBar.svelte"
|
import PortalSideBar from "./_components/PortalSideBar.svelte"
|
||||||
|
|
||||||
// Don't block loading if we've already hydrated state
|
// Don't block loading if we've already hydrated state
|
||||||
let loaded = !!$apps?.length
|
let loaded = !!$appsStore.apps?.length
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -34,7 +34,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to new app page if no apps exists
|
// Go to new app page if no apps exists
|
||||||
if (!$apps.length && sdk.users.hasBuilderPermissions($auth.user)) {
|
if (
|
||||||
|
!$appsStore.apps.length &&
|
||||||
|
sdk.users.hasBuilderPermissions($auth.user)
|
||||||
|
) {
|
||||||
$redirect("./onboarding")
|
$redirect("./onboarding")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -46,7 +49,7 @@
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
{#if $apps.length > 0}
|
{#if $appsStore.apps.length > 0}
|
||||||
<PortalSideBar />
|
<PortalSideBar />
|
||||||
{/if}
|
{/if}
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
import { apps, templates, licensing } from "stores/portal"
|
import { appsStore, templates, licensing } from "stores/portal"
|
||||||
import { Breadcrumbs, Breadcrumb, Header } from "components/portal/page"
|
import { Breadcrumbs, Breadcrumb, Header } from "components/portal/page"
|
||||||
|
|
||||||
let template
|
let template
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$apps.length}
|
{#if !$appsStore.apps.length}
|
||||||
<FirstAppOnboarding />
|
<FirstAppOnboarding />
|
||||||
{:else}
|
{:else}
|
||||||
<Page>
|
<Page>
|
||||||
|
|
|
@ -19,13 +19,18 @@
|
||||||
import { automationStore, initialise } from "stores/builder"
|
import { automationStore, initialise } from "stores/builder"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { apps, auth, admin, licensing, environment } from "stores/portal"
|
import {
|
||||||
|
appsStore,
|
||||||
|
auth,
|
||||||
|
admin,
|
||||||
|
licensing,
|
||||||
|
environment,
|
||||||
|
enrichedApps,
|
||||||
|
} from "stores/portal"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import AppRow from "components/start/AppRow.svelte"
|
import AppRow from "components/start/AppRow.svelte"
|
||||||
import { AppStatus } from "constants"
|
|
||||||
import Logo from "assets/bb-space-man.svg"
|
import Logo from "assets/bb-space-man.svg"
|
||||||
|
|
||||||
let sortBy = "name"
|
|
||||||
let template
|
let template
|
||||||
let creationModal
|
let creationModal
|
||||||
let appLimitModal
|
let appLimitModal
|
||||||
|
@ -33,56 +38,27 @@
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let creatingFromTemplate = false
|
let creatingFromTemplate = false
|
||||||
let automationErrors
|
let automationErrors
|
||||||
let accessFilterList = null
|
|
||||||
|
|
||||||
$: welcomeHeader = `Welcome ${$auth?.user?.firstName || "back"}`
|
$: welcomeHeader = `Welcome ${$auth?.user?.firstName || "back"}`
|
||||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
$: filteredApps = filterApps($enrichedApps, searchTerm)
|
||||||
$: filteredApps = enrichedApps.filter(
|
$: automationErrors = getAutomationErrors(filteredApps || [])
|
||||||
app =>
|
|
||||||
(searchTerm
|
|
||||||
? app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
: true) &&
|
|
||||||
(accessFilterList !== null
|
|
||||||
? accessFilterList?.includes(
|
|
||||||
`${app?.type}_${app?.tenantId}_${app?.appId}`
|
|
||||||
)
|
|
||||||
: true)
|
|
||||||
)
|
|
||||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
|
||||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
|
const filterApps = (apps, searchTerm) => {
|
||||||
|
return apps?.filter(app => {
|
||||||
|
const query = searchTerm?.trim()?.replace(/\s/g, "")
|
||||||
|
if (query) {
|
||||||
|
return app?.name?.toLowerCase().includes(query.toLowerCase())
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const usersLimitLockAction = $licensing?.errUserLimit
|
const usersLimitLockAction = $licensing?.errUserLimit
|
||||||
? () => accountLockedModal.show()
|
? () => accountLockedModal.show()
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
|
||||||
const enrichedApps = apps.map(app => ({
|
|
||||||
...app,
|
|
||||||
deployed: app.status === AppStatus.DEPLOYED,
|
|
||||||
lockedYou: app.lockedBy && 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 === "updated") {
|
|
||||||
return enrichedApps.sort((a, b) => {
|
|
||||||
const aUpdated = a.updatedAt || "9999"
|
|
||||||
const bUpdated = b.updatedAt || "9999"
|
|
||||||
return aUpdated < bUpdated ? 1 : -1
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return enrichedApps.sort((a, b) => {
|
|
||||||
return a.name?.toLowerCase() < b.name?.toLowerCase() ? -1 : 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAutomationErrors = apps => {
|
const getAutomationErrors = apps => {
|
||||||
const automationErrors = {}
|
const automationErrors = {}
|
||||||
for (let app of apps) {
|
for (let app of apps) {
|
||||||
|
@ -117,7 +93,7 @@
|
||||||
const initiateAppCreation = async () => {
|
const initiateAppCreation = async () => {
|
||||||
if ($licensing?.usageMetrics?.apps >= 100) {
|
if ($licensing?.usageMetrics?.apps >= 100) {
|
||||||
appLimitModal.show()
|
appLimitModal.show()
|
||||||
} else if ($apps?.length) {
|
} else if ($appsStore.apps?.length) {
|
||||||
$goto("/builder/portal/apps/create")
|
$goto("/builder/portal/apps/create")
|
||||||
} else {
|
} else {
|
||||||
template = null
|
template = null
|
||||||
|
@ -136,7 +112,7 @@
|
||||||
const templateKey = template.key.split("/")[1]
|
const templateKey = template.key.split("/")[1]
|
||||||
|
|
||||||
let appName = templateKey.replace(/-/g, " ")
|
let appName = templateKey.replace(/-/g, " ")
|
||||||
const appsWithSameName = $apps.filter(app =>
|
const appsWithSameName = $appsStore.apps.filter(app =>
|
||||||
app.name?.startsWith(appName)
|
app.name?.startsWith(appName)
|
||||||
)
|
)
|
||||||
appName = `${appName} ${appsWithSameName.length + 1}`
|
appName = `${appName} ${appsWithSameName.length + 1}`
|
||||||
|
@ -217,7 +193,7 @@
|
||||||
: "View error"}
|
: "View error"}
|
||||||
on:dismiss={async () => {
|
on:dismiss={async () => {
|
||||||
await automationStore.actions.clearLogErrors({ appId })
|
await automationStore.actions.clearLogErrors({ appId })
|
||||||
await apps.load()
|
await appsStore.load()
|
||||||
}}
|
}}
|
||||||
message={automationErrorMessage(appId)}
|
message={automationErrorMessage(appId)}
|
||||||
/>
|
/>
|
||||||
|
@ -233,7 +209,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if enrichedApps.length}
|
{#if $appsStore.apps.length}
|
||||||
<Layout noPadding gap="L">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{#if $auth.user && sdk.users.canCreateApps($auth.user)}
|
{#if $auth.user && sdk.users.canCreateApps($auth.user)}
|
||||||
|
@ -245,7 +221,7 @@
|
||||||
>
|
>
|
||||||
Create new app
|
Create new app
|
||||||
</Button>
|
</Button>
|
||||||
{#if $apps?.length > 0 && !$admin.offlineMode}
|
{#if $appsStore.apps?.length > 0 && !$admin.offlineMode}
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
secondary
|
secondary
|
||||||
|
@ -255,7 +231,7 @@
|
||||||
View templates
|
View templates
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !$apps?.length}
|
{#if !$appsStore.apps?.length}
|
||||||
<Button
|
<Button
|
||||||
size="L"
|
size="L"
|
||||||
quiet
|
quiet
|
||||||
|
@ -267,11 +243,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if enrichedApps.length > 1}
|
{#if $appsStore.apps.length > 1}
|
||||||
<div class="app-actions">
|
<div class="app-actions">
|
||||||
<Select
|
<Select
|
||||||
autoWidth
|
autoWidth
|
||||||
bind:value={sortBy}
|
value={$appsStore.sortBy}
|
||||||
|
on:change={e => {
|
||||||
|
appsStore.updateSort(e.detail)
|
||||||
|
}}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
options={[
|
options={[
|
||||||
{ label: "Sort by name", value: "name" },
|
{ label: "Sort by name", value: "name" },
|
||||||
|
@ -279,7 +258,17 @@
|
||||||
{ label: "Sort by status", value: "status" },
|
{ label: "Sort by status", value: "status" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Search placeholder="Search" bind:value={searchTerm} />
|
<Search
|
||||||
|
placeholder="Search"
|
||||||
|
on:input={e => {
|
||||||
|
searchTerm = e.target.value
|
||||||
|
}}
|
||||||
|
on:change={e => {
|
||||||
|
if (!e.detail) {
|
||||||
|
searchTerm = null
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { Breadcrumb, Breadcrumbs } from "components/portal/page"
|
import { Breadcrumb, Breadcrumbs } from "components/portal/page"
|
||||||
import { roles } from "stores/builder"
|
import { roles } from "stores/builder"
|
||||||
import { apps, auth, groups } from "stores/portal"
|
import { appsStore, auth, groups } from "stores/portal"
|
||||||
import { onMount, setContext } from "svelte"
|
import { onMount, setContext } from "svelte"
|
||||||
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
|
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
|
||||||
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
|
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
|
||||||
|
@ -51,17 +51,17 @@
|
||||||
$: isScimGroup = group?.scimInfo?.isSync
|
$: isScimGroup = group?.scimInfo?.isSync
|
||||||
$: isAdmin = sdk.users.isAdmin($auth.user)
|
$: isAdmin = sdk.users.isAdmin($auth.user)
|
||||||
$: readonly = !isAdmin || isScimGroup
|
$: readonly = !isAdmin || isScimGroup
|
||||||
$: groupApps = $apps
|
$: groupApps = $appsStore.apps
|
||||||
.filter(app =>
|
.filter(app =>
|
||||||
groups.actions
|
groups.actions
|
||||||
.getGroupAppIds(group)
|
.getGroupAppIds(group)
|
||||||
.includes(apps.getProdAppID(app.devId))
|
.includes(appsStore.getProdAppID(app.devId))
|
||||||
)
|
)
|
||||||
.map(app => ({
|
.map(app => ({
|
||||||
...app,
|
...app,
|
||||||
role: group?.builder?.apps.includes(apps.getProdAppID(app.devId))
|
role: group?.builder?.apps.includes(appsStore.getProdAppID(app.devId))
|
||||||
? Constants.Roles.CREATOR
|
? Constants.Roles.CREATOR
|
||||||
: group?.roles?.[apps.getProdAppID(app.devId)],
|
: group?.roles?.[appsStore.getProdAppID(app.devId)],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeApp = async app => {
|
const removeApp = async app => {
|
||||||
await groups.actions.removeApp(groupId, apps.getProdAppID(app.devId))
|
await groups.actions.removeApp(groupId, appsStore.getProdAppID(app.devId))
|
||||||
}
|
}
|
||||||
setContext("roles", {
|
setContext("roles", {
|
||||||
updateRole: () => {},
|
updateRole: () => {},
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { keepOpen, Body, ModalContent, Select } from "@budibase/bbui"
|
import { keepOpen, Body, ModalContent, Select } from "@budibase/bbui"
|
||||||
import { apps, groups } from "stores/portal"
|
import { appsStore, groups } from "stores/portal"
|
||||||
import { roles } from "stores/builder"
|
import { roles } from "stores/builder"
|
||||||
import RoleSelect from "components/common/RoleSelect.svelte"
|
import RoleSelect from "components/common/RoleSelect.svelte"
|
||||||
|
|
||||||
export let group
|
export let group
|
||||||
|
|
||||||
$: appOptions = $apps.map(app => ({
|
$: appOptions = $appsStore.apps.map(app => ({
|
||||||
label: app.name,
|
label: app.name,
|
||||||
value: app,
|
value: app,
|
||||||
}))
|
}))
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
let selectingRole = false
|
let selectingRole = false
|
||||||
|
|
||||||
async function appSelected() {
|
async function appSelected() {
|
||||||
const prodAppId = apps.getProdAppID(selectedApp.devId)
|
const prodAppId = appsStore.getProdAppID(selectedApp.devId)
|
||||||
if (!selectingRole) {
|
if (!selectingRole) {
|
||||||
selectingRole = true
|
selectingRole = true
|
||||||
await roles.fetchByAppId(prodAppId)
|
await roles.fetchByAppId(prodAppId)
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount, setContext } from "svelte"
|
import { onMount, setContext } from "svelte"
|
||||||
import { users, auth, groups, apps, licensing } from "stores/portal"
|
import { users, auth, groups, appsStore, licensing } from "stores/portal"
|
||||||
import { roles } from "stores/builder"
|
import { roles } from "stores/builder"
|
||||||
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
||||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
||||||
$: nameLabel = getNameLabel(user)
|
$: nameLabel = getNameLabel(user)
|
||||||
$: filteredGroups = getFilteredGroups(internalGroups, searchTerm)
|
$: filteredGroups = getFilteredGroups(internalGroups, searchTerm)
|
||||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
$: availableApps = getAvailableApps($appsStore.apps, privileged, user?.roles)
|
||||||
$: userGroups = $groups.filter(x => {
|
$: userGroups = $groups.filter(x => {
|
||||||
return x.users?.find(y => {
|
return x.users?.find(y => {
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
|
@ -111,12 +111,12 @@
|
||||||
availableApps = availableApps.filter(x => {
|
availableApps = availableApps.filter(x => {
|
||||||
let roleKeys = Object.keys(roles || {})
|
let roleKeys = Object.keys(roles || {})
|
||||||
return roleKeys.concat(user?.builder?.apps).find(y => {
|
return roleKeys.concat(user?.builder?.apps).find(y => {
|
||||||
return x.appId === apps.extractAppId(y)
|
return x.appId === appsStore.extractAppId(y)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return availableApps.map(app => {
|
return availableApps.map(app => {
|
||||||
const prodAppId = apps.getProdAppID(app.devId)
|
const prodAppId = appsStore.getProdAppID(app.devId)
|
||||||
return {
|
return {
|
||||||
name: app.name,
|
name: app.name,
|
||||||
devId: app.devId,
|
devId: app.devId,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { apps } from "stores/portal"
|
import { appsStore } from "stores/portal"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
const getCount = () => {
|
const getCount = () => {
|
||||||
if (priviliged) {
|
if (priviliged) {
|
||||||
return $apps.length
|
return $appsStore.apps.length
|
||||||
} else {
|
} else {
|
||||||
return sdk.users.hasAppBuilderPermissions(row)
|
return sdk.users.hasAppBuilderPermissions(row)
|
||||||
? row?.builder?.apps?.length +
|
? row?.builder?.apps?.length +
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import BudiStore from "./BudiStore"
|
import BudiStore from "../BudiStore"
|
||||||
|
|
||||||
export const INITIAL_APP_META_STATE = {
|
export const INITIAL_APP_META_STATE = {
|
||||||
appId: "",
|
appId: "",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { createBuilderWebsocket } from "./websocket.js"
|
import { createBuilderWebsocket } from "./websocket.js"
|
||||||
import { BuilderSocketEvent } from "@budibase/shared-core"
|
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
import BudiStore from "./BudiStore"
|
import BudiStore from "../BudiStore.js"
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
|
|
||||||
export const INITIAL_BUILDER_STATE = {
|
export const INITIAL_BUILDER_STATE = {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
DB_TYPE_INTERNAL,
|
DB_TYPE_INTERNAL,
|
||||||
DB_TYPE_EXTERNAL,
|
DB_TYPE_EXTERNAL,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import BudiStore from "./BudiStore"
|
import BudiStore from "../BudiStore"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { previewStore } from "stores/builder"
|
import { previewStore } from "stores/builder"
|
||||||
import BudiStore from "./BudiStore"
|
import BudiStore from "../BudiStore"
|
||||||
|
|
||||||
export const INITIAL_HOVER_STATE = {
|
export const INITIAL_HOVER_STATE = {
|
||||||
componentId: null,
|
componentId: null,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { derived, get } from "svelte/store"
|
import { derived, get } from "svelte/store"
|
||||||
import { componentStore } from "stores/builder"
|
import { componentStore } from "stores/builder"
|
||||||
import BudiStore from "./BudiStore"
|
import BudiStore from "../BudiStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
export const INITIAL_LAYOUT_STATE = {
|
export const INITIAL_LAYOUT_STATE = {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { appStore } from "stores/builder"
|
import { appStore } from "stores/builder"
|
||||||
import BudiStore from "./BudiStore"
|
import BudiStore from "../BudiStore"
|
||||||
|
|
||||||
export const INITIAL_NAVIGATION_STATE = {
|
export const INITIAL_NAVIGATION_STATE = {
|
||||||
navigation: "Top",
|
navigation: "Top",
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { createHistoryStore } from "stores/builder/history"
|
import { createHistoryStore } from "stores/builder/history"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import BudiStore from "./BudiStore"
|
import BudiStore from "../BudiStore"
|
||||||
|
|
||||||
export const INITIAL_SCREENS_STATE = {
|
export const INITIAL_SCREENS_STATE = {
|
||||||
screens: [],
|
screens: [],
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
tables,
|
tables,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { auth, apps } from "stores/portal"
|
import { auth, appsStore } from "stores/portal"
|
||||||
import { screenStore } from "./screens"
|
import { screenStore } from "./screens"
|
||||||
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -70,7 +70,7 @@ export const createBuilderWebsocket = appId => {
|
||||||
socket.onOther(
|
socket.onOther(
|
||||||
BuilderSocketEvent.AppPublishChange,
|
BuilderSocketEvent.AppPublishChange,
|
||||||
async ({ user, published }) => {
|
async ({ user, published }) => {
|
||||||
await apps.load()
|
await appsStore.load()
|
||||||
if (published) {
|
if (published) {
|
||||||
await deploymentStore.load()
|
await deploymentStore.load()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,61 @@
|
||||||
import { writable } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { auth } from "./auth"
|
||||||
|
import BudiStore from "../BudiStore" // move this
|
||||||
|
|
||||||
// properties that should always come from the dev app, not the deployed
|
// properties that should always come from the dev app, not the deployed
|
||||||
const DEV_PROPS = ["updatedBy", "updatedAt"]
|
const DEV_PROPS = ["updatedBy", "updatedAt"]
|
||||||
|
|
||||||
const extractAppId = id => {
|
export const INITIAL_APPS_STATE = {
|
||||||
const split = id?.split("_") || []
|
apps: [],
|
||||||
return split.length ? split[split.length - 1] : null
|
sortBy: "name",
|
||||||
}
|
}
|
||||||
|
|
||||||
const getProdAppID = appId => {
|
export class AppsStore extends BudiStore {
|
||||||
if (!appId) {
|
constructor() {
|
||||||
return appId
|
super({ ...INITIAL_APPS_STATE })
|
||||||
}
|
|
||||||
let rest,
|
|
||||||
separator = ""
|
|
||||||
if (appId.startsWith("app_dev")) {
|
|
||||||
// split to take off the app_dev element, then join it together incase any other app_ exist
|
|
||||||
const split = appId.split("app_dev")
|
|
||||||
split.shift()
|
|
||||||
rest = split.join("app_dev")
|
|
||||||
} else if (!appId.startsWith("app")) {
|
|
||||||
rest = appId
|
|
||||||
separator = "_"
|
|
||||||
} else {
|
|
||||||
return appId
|
|
||||||
}
|
|
||||||
return `app${separator}${rest}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAppStore() {
|
this.extractAppId = this.extractAppId.bind(this)
|
||||||
const store = writable([])
|
this.getProdAppID = this.getProdAppID.bind(this)
|
||||||
|
this.updateSort = this.updateSort.bind(this)
|
||||||
|
this.load = this.load.bind(this)
|
||||||
|
this.save = this.save.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
extractAppId(id) {
|
||||||
|
const split = id?.split("_") || []
|
||||||
|
return split.length ? split[split.length - 1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
getProdAppID(appId) {
|
||||||
|
if (!appId) {
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
let rest,
|
||||||
|
separator = ""
|
||||||
|
if (appId.startsWith("app_dev")) {
|
||||||
|
// split to take off the app_dev element, then join it together incase any other app_ exist
|
||||||
|
const split = appId.split("app_dev")
|
||||||
|
split.shift()
|
||||||
|
rest = split.join("app_dev")
|
||||||
|
} else if (!appId.startsWith("app")) {
|
||||||
|
rest = appId
|
||||||
|
separator = "_"
|
||||||
|
} else {
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
return `app${separator}${rest}`
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSort(sortBy) {
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
sortBy,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
const json = await API.getApps()
|
const json = await API.getApps()
|
||||||
if (Array.isArray(json)) {
|
if (Array.isArray(json)) {
|
||||||
// Merge apps into one sensible list
|
// Merge apps into one sensible list
|
||||||
|
@ -43,7 +65,7 @@ export function createAppStore() {
|
||||||
|
|
||||||
// First append all dev app version
|
// First append all dev app version
|
||||||
devApps.forEach(app => {
|
devApps.forEach(app => {
|
||||||
const id = extractAppId(app.appId)
|
const id = this.extractAppId(app.appId)
|
||||||
appMap[id] = {
|
appMap[id] = {
|
||||||
...app,
|
...app,
|
||||||
devId: app.appId,
|
devId: app.appId,
|
||||||
|
@ -53,7 +75,7 @@ export function createAppStore() {
|
||||||
|
|
||||||
// Then merge with all prod app versions
|
// Then merge with all prod app versions
|
||||||
deployedApps.forEach(app => {
|
deployedApps.forEach(app => {
|
||||||
const id = extractAppId(app.appId)
|
const id = this.extractAppId(app.appId)
|
||||||
|
|
||||||
// Skip any deployed apps which don't have a dev counterpart
|
// Skip any deployed apps which don't have a dev counterpart
|
||||||
if (!appMap[id]) {
|
if (!appMap[id]) {
|
||||||
|
@ -81,39 +103,80 @@ export function createAppStore() {
|
||||||
// Transform into an array and clean up
|
// Transform into an array and clean up
|
||||||
const apps = Object.values(appMap)
|
const apps = Object.values(appMap)
|
||||||
apps.forEach(app => {
|
apps.forEach(app => {
|
||||||
app.appId = extractAppId(app.devId)
|
app.appId = this.extractAppId(app.devId)
|
||||||
delete app._id
|
delete app._id
|
||||||
delete app._rev
|
delete app._rev
|
||||||
})
|
})
|
||||||
store.set(apps)
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
apps,
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
store.set([])
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
apps: [],
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update(appId, value) {
|
async save(appId, value) {
|
||||||
await API.saveAppMetadata({
|
await API.saveAppMetadata({
|
||||||
appId,
|
appId,
|
||||||
metadata: value,
|
metadata: value,
|
||||||
})
|
})
|
||||||
store.update(state => {
|
this.update(state => {
|
||||||
const updatedAppIndex = state.findIndex(app => app.instance._id === appId)
|
const updatedAppIndex = state.apps.findIndex(
|
||||||
|
app => app.instance._id === appId
|
||||||
|
)
|
||||||
if (updatedAppIndex !== -1) {
|
if (updatedAppIndex !== -1) {
|
||||||
let updatedApp = state[updatedAppIndex]
|
let updatedApp = state.apps[updatedAppIndex]
|
||||||
updatedApp = { ...updatedApp, ...value }
|
updatedApp = { ...updatedApp, ...value }
|
||||||
state.apps = state.splice(updatedAppIndex, 1, updatedApp)
|
state.apps = state.apps.splice(updatedAppIndex, 1, updatedApp)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
load,
|
|
||||||
update,
|
|
||||||
extractAppId,
|
|
||||||
getProdAppID,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apps = createAppStore()
|
export const appsStore = new AppsStore()
|
||||||
|
|
||||||
|
// Centralise any logic that enriches the apps list
|
||||||
|
export const enrichedApps = derived([appsStore, auth], ([$store, $auth]) => {
|
||||||
|
const enrichedApps = $store.apps
|
||||||
|
? $store.apps.map(app => ({
|
||||||
|
...app,
|
||||||
|
deployed: app.status === AppStatus.DEPLOYED,
|
||||||
|
lockedYou: app.lockedBy && app.lockedBy.email === $auth.user?.email,
|
||||||
|
lockedOther: app.lockedBy && app.lockedBy.email !== $auth.user?.email,
|
||||||
|
favourite: $auth?.user.appFavourites?.includes(app.appId),
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
|
||||||
|
if ($store.sortBy === "status") {
|
||||||
|
return enrichedApps.sort((a, b) => {
|
||||||
|
if (a.favourite === b.favourite) {
|
||||||
|
if (a.status === b.status) {
|
||||||
|
return a.name?.toLowerCase() < b.name?.toLowerCase() ? -1 : 1
|
||||||
|
}
|
||||||
|
return a.status === AppStatus.DEPLOYED ? -1 : 1
|
||||||
|
}
|
||||||
|
return a.favourite ? -1 : 1
|
||||||
|
})
|
||||||
|
} else if ($store.sortBy === "updated") {
|
||||||
|
return enrichedApps?.sort((a, b) => {
|
||||||
|
if (a.favourite === b.favourite) {
|
||||||
|
const aUpdated = a.updatedAt || "9999"
|
||||||
|
const bUpdated = b.updatedAt || "9999"
|
||||||
|
return aUpdated < bUpdated ? 1 : -1
|
||||||
|
}
|
||||||
|
return a.favourite ? -1 : 1
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return enrichedApps?.sort((a, b) => {
|
||||||
|
if (a.favourite === b.favourite) {
|
||||||
|
return a.name?.toLowerCase() < b.name?.toLowerCase() ? -1 : 1
|
||||||
|
}
|
||||||
|
return a.favourite ? -1 : 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { writable } from "svelte/store"
|
||||||
export { organisation } from "./organisation"
|
export { organisation } from "./organisation"
|
||||||
export { users } from "./users"
|
export { users } from "./users"
|
||||||
export { admin } from "./admin"
|
export { admin } from "./admin"
|
||||||
export { apps } from "./apps"
|
export { appsStore, enrichedApps } from "./apps"
|
||||||
export { email } from "./email"
|
export { email } from "./email"
|
||||||
export { auth } from "./auth"
|
export { auth } from "./auth"
|
||||||
export { oidc } from "./oidc"
|
export { oidc } from "./oidc"
|
||||||
|
|
|
@ -682,7 +682,10 @@ export async function duplicateApp(
|
||||||
// Build a new request
|
// Build a new request
|
||||||
const createRequest = {
|
const createRequest = {
|
||||||
roleId: ctx.roleId,
|
roleId: ctx.roleId,
|
||||||
user: ctx.user,
|
user: {
|
||||||
|
...ctx.user,
|
||||||
|
_id: dbCore.getGlobalIDFromUserMetadataID(ctx.user._id || ""),
|
||||||
|
},
|
||||||
request: {
|
request: {
|
||||||
body: createRequestBody,
|
body: createRequestBody,
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface UpdateSelfRequest {
|
||||||
password?: string
|
password?: string
|
||||||
forceResetPassword?: boolean
|
forceResetPassword?: boolean
|
||||||
onboardedAt?: string
|
onboardedAt?: string
|
||||||
|
appFavourites?: string[]
|
||||||
tours?: Record<string, Date>
|
tours?: Record<string, Date>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ export interface User extends Document {
|
||||||
onboardedAt?: string
|
onboardedAt?: string
|
||||||
tours?: Record<string, Date>
|
tours?: Record<string, Date>
|
||||||
scimInfo?: { isSync: true } & Record<string, any>
|
scimInfo?: { isSync: true } & Record<string, any>
|
||||||
|
appFavourites?: string[]
|
||||||
ssoId?: string
|
ssoId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,12 @@ import {
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { groups } from "@budibase/pro"
|
import { groups } from "@budibase/pro"
|
||||||
import { UpdateSelfRequest, UpdateSelfResponse, UserCtx } from "@budibase/types"
|
import {
|
||||||
|
UpdateSelfRequest,
|
||||||
|
UpdateSelfResponse,
|
||||||
|
User,
|
||||||
|
UserCtx,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
const { newid } = utils
|
const { newid } = utils
|
||||||
|
|
||||||
|
@ -105,16 +110,63 @@ export async function getSelf(ctx: any) {
|
||||||
addSessionAttributesToUser(ctx)
|
addSessionAttributesToUser(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const syncAppFavourites = async (processedAppIds: string[]) => {
|
||||||
|
if (processedAppIds.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const apps = await fetchAppsByIds(processedAppIds)
|
||||||
|
return apps?.reduce((acc: string[], app) => {
|
||||||
|
const id = app.appId.replace(dbCore.APP_DEV_PREFIX, "")
|
||||||
|
if (processedAppIds.includes(id)) {
|
||||||
|
acc.push(id)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchAppsByIds = async (processedAppIds: string[]) => {
|
||||||
|
return await dbCore.getAppsByIDs(
|
||||||
|
processedAppIds.map(appId => `${dbCore.APP_DEV_PREFIX}${appId}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processUserAppFavourites = async (
|
||||||
|
user: User,
|
||||||
|
update: UpdateSelfRequest
|
||||||
|
) => {
|
||||||
|
if (!("appFavourites" in update)) {
|
||||||
|
// Ignore requests without an explicit update to favourites.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAppFavourites = user.appFavourites || []
|
||||||
|
const requestAppFavourites = new Set(update.appFavourites || [])
|
||||||
|
const containsAll = userAppFavourites.every(v => requestAppFavourites.has(v))
|
||||||
|
|
||||||
|
if (containsAll && requestAppFavourites.size === userAppFavourites.length) {
|
||||||
|
// Ignore request if the outcome will have no change
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the request by purging apps that no longer exist.
|
||||||
|
const syncedAppFavourites = await syncAppFavourites([...requestAppFavourites])
|
||||||
|
return syncedAppFavourites
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateSelf(
|
export async function updateSelf(
|
||||||
ctx: UserCtx<UpdateSelfRequest, UpdateSelfResponse>
|
ctx: UserCtx<UpdateSelfRequest, UpdateSelfResponse>
|
||||||
) {
|
) {
|
||||||
const update = ctx.request.body
|
const update = ctx.request.body
|
||||||
|
|
||||||
let user = await userSdk.db.getUser(ctx.user._id!)
|
let user = await userSdk.db.getUser(ctx.user._id!)
|
||||||
|
const updatedAppFavourites = await processUserAppFavourites(user, update)
|
||||||
|
|
||||||
user = {
|
user = {
|
||||||
...user,
|
...user,
|
||||||
...update,
|
...update,
|
||||||
|
...(updatedAppFavourites ? { appFavourites: updatedAppFavourites } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
user = await userSdk.db.save(user, { requirePassword: false })
|
user = await userSdk.db.save(user, { requirePassword: false })
|
||||||
|
|
||||||
if (update.password) {
|
if (update.password) {
|
||||||
|
|
|
@ -26,6 +26,7 @@ export const buildSelfSaveValidation = () => {
|
||||||
firstName: OPTIONAL_STRING,
|
firstName: OPTIONAL_STRING,
|
||||||
lastName: OPTIONAL_STRING,
|
lastName: OPTIONAL_STRING,
|
||||||
onboardedAt: Joi.string().optional(),
|
onboardedAt: Joi.string().optional(),
|
||||||
|
appFavourites: Joi.array().optional(),
|
||||||
tours: Joi.object().optional(),
|
tours: Joi.object().optional(),
|
||||||
}
|
}
|
||||||
return auth.joiValidator.body(Joi.object(schema).required().unknown(false))
|
return auth.joiValidator.body(Joi.object(schema).required().unknown(false))
|
||||||
|
|
Loading…
Reference in New Issue