Merge 3eb22611f0
into b573e9cb2b
This commit is contained in:
commit
eceb4c5e7f
|
@ -17,11 +17,23 @@
|
|||
export let autocomplete: boolean | string | undefined = undefined
|
||||
export let helpText: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: any
|
||||
enterkey: KeyboardEvent
|
||||
}>()
|
||||
const onChange = (e: any) => {
|
||||
value = e.detail
|
||||
dispatch("change", e.detail)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
dispatch("enterkey", e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field {helpText} {label} {labelPosition} {error}>
|
||||
|
@ -42,6 +54,7 @@
|
|||
on:focus
|
||||
on:keyup
|
||||
on:keydown
|
||||
on:keydown={onKeyDown}
|
||||
>
|
||||
<slot />
|
||||
</TextField>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { contextMenuStore } from "@/stores/builder"
|
||||
import { Popover, Menu, MenuItem } from "@budibase/bbui"
|
||||
import { Menu, MenuItem, Popover } from "@budibase/bbui"
|
||||
import NewPill from "./common/NewPill.svelte"
|
||||
|
||||
let dropdown
|
||||
let anchor
|
||||
|
@ -46,6 +47,11 @@
|
|||
disabled={item.disabled}
|
||||
>
|
||||
{item.name}
|
||||
<div slot="right">
|
||||
{#if item.isNew}
|
||||
<NewPill />
|
||||
{/if}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ChooseIconModal {name} {color} on:change />
|
||||
<ChooseIconModal bind:name bind:color on:change />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
export let title: string
|
||||
export let placeholder: string
|
||||
export let value: string
|
||||
export let onAdd: () => void
|
||||
export let onAdd: (_e: Event) => void
|
||||
export let search: boolean
|
||||
|
||||
let searchInput: HTMLInputElement
|
||||
|
@ -28,11 +28,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
const handleAddButton = () => {
|
||||
const handleAddButton = (e: Event) => {
|
||||
if (search) {
|
||||
closeSearch()
|
||||
} else {
|
||||
onAdd()
|
||||
onAdd(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -64,7 +64,7 @@
|
|||
|
||||
<div
|
||||
on:click={handleAddButton}
|
||||
on:keydown={keyUtils.handleEnter(handleAddButton)}
|
||||
on:keydown={e => keyUtils.handleEnter(() => handleAddButton(e))}
|
||||
class="addButton"
|
||||
class:rotate={search}
|
||||
>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import type { UIUser } from "@budibase/types"
|
||||
|
||||
export let icon: string | null
|
||||
export let icon: string | null = null
|
||||
export let iconTooltip: string = ""
|
||||
export let withArrow: boolean = false
|
||||
export let withActions: boolean = true
|
||||
|
@ -26,6 +26,7 @@
|
|||
export let compact: boolean = false
|
||||
export let hovering: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let nonSelectable: boolean = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -73,6 +74,7 @@
|
|||
class:highlighted
|
||||
class:selectedBy
|
||||
class:disabled
|
||||
class:nonSelectable
|
||||
on:dragend
|
||||
on:dragstart
|
||||
on:dragover
|
||||
|
@ -156,6 +158,9 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.nav-item.nonSelectable {
|
||||
cursor: inherit;
|
||||
}
|
||||
.nav-item.scrollable {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
@ -173,7 +178,7 @@
|
|||
.nav-item.disabled span {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
.nav-item:hover,
|
||||
.nav-item:not(.nonSelectable):hover,
|
||||
.hovering {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { Tooltip, StatusLight } from "@budibase/bbui"
|
||||
import { StatusLight, AbsTooltip } from "@budibase/bbui"
|
||||
import { roles } from "@/stores/builder"
|
||||
import { Roles } from "@/constants/backend"
|
||||
|
||||
export let roleId: string
|
||||
|
||||
let showTooltip = false
|
||||
|
||||
$: role = $roles.find(role => role._id === roleId)
|
||||
$: color =
|
||||
role?.uiMetadata?.color || "var(--spectrum-global-color-static-magenta-400)"
|
||||
|
@ -16,43 +14,6 @@
|
|||
: `Requires ${role?.uiMetadata?.displayName || "Unknown role"} access`
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="container"
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
on:focus
|
||||
style="--color: {color};"
|
||||
>
|
||||
<AbsTooltip text={tooltip} {color}>
|
||||
<StatusLight square {color} />
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping text={tooltip} direction="right" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.tooltip {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 13px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tooltip :global(.spectrum-Tooltip) {
|
||||
background: var(--color);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
max-width: 200px;
|
||||
}
|
||||
.tooltip :global(.spectrum-Tooltip-tip) {
|
||||
border-top-color: var(--color);
|
||||
}
|
||||
</style>
|
||||
</AbsTooltip>
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
hoverable
|
||||
name="MoreSmallList"
|
||||
/>
|
||||
<div slot="icon" class="icon">
|
||||
<div slot="right" class="icon">
|
||||
<RoleIndicator roleId={screen.routing.roleId} />
|
||||
</div>
|
||||
</NavItem>
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<script lang="ts">
|
||||
import NavItem from "@/components/common/NavItem.svelte"
|
||||
import { confirm } from "@/helpers"
|
||||
import { contextMenuStore, workspaceAppStore } from "@/stores/builder"
|
||||
import { Icon, Layout, notifications } from "@budibase/bbui"
|
||||
import type { UIWorkspaceApp } from "@budibase/types"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import ScreenNavItem from "./ScreenNavItem.svelte"
|
||||
|
||||
export let workspaceApp: UIWorkspaceApp
|
||||
export let searchValue: string
|
||||
|
||||
const dispatch = createEventDispatcher<{ edit: void }>()
|
||||
|
||||
async function onDelete() {
|
||||
await confirm({
|
||||
title: "Confirm Deletion",
|
||||
body: `Deleting "${workspaceApp.name}" cannot be undone. Are you sure?`,
|
||||
okText: "Delete app",
|
||||
warning: true,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await workspaceAppStore.delete(workspaceApp._id, workspaceApp._rev)
|
||||
notifications.success(
|
||||
`App '${workspaceApp.name}' deleted successfully`
|
||||
)
|
||||
} catch (e: any) {
|
||||
let message = "Error deleting app"
|
||||
if (e.message) {
|
||||
message += ` - ${e.message}`
|
||||
}
|
||||
notifications.error(message)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const openContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: "Add",
|
||||
name: "Add screen",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
callback: () => $goto("../new?workspaceAppId=" + workspaceApp._id),
|
||||
},
|
||||
{
|
||||
icon: "Edit",
|
||||
name: "Edit",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
callback: () => {
|
||||
dispatch("edit")
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "Delete",
|
||||
name: "Delete",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
callback: onDelete,
|
||||
},
|
||||
]
|
||||
|
||||
contextMenuStore.open("projectContextMenu", items, {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}
|
||||
|
||||
$: noResultsMessage = searchValue
|
||||
? "There aren't screens matching that route"
|
||||
: ""
|
||||
</script>
|
||||
|
||||
<div class="project-app-nav-item">
|
||||
<NavItem
|
||||
on:contextmenu={openContextMenu}
|
||||
indentLevel={0}
|
||||
text={workspaceApp.name}
|
||||
showTooltip
|
||||
nonSelectable
|
||||
>
|
||||
<Icon on:click={openContextMenu} size="S" hoverable name="MoreSmallList" />
|
||||
<div slot="icon">
|
||||
<Icon name={workspaceApp.icon} size="XS" color={workspaceApp.iconColor} />
|
||||
</div>
|
||||
</NavItem>
|
||||
</div>
|
||||
|
||||
<div class="screens">
|
||||
{#each workspaceApp.screens as screen (screen._id)}
|
||||
<div class="screen">
|
||||
<ScreenNavItem {screen} />
|
||||
</div>
|
||||
{:else}
|
||||
<Layout paddingY="none" paddingX="L">
|
||||
<div class="no-results">{noResultsMessage}</div>
|
||||
</Layout>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screens {
|
||||
border-left: 2px solid var(--spectrum-global-color-gray-200);
|
||||
padding-right: 12px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.screens .screen {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.screens :global(.nav-item) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.screens :global(.nav-item-content) {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.project-app-nav-item :global(.nav-item-content .icon) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
|
@ -1,11 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { Layout } from "@budibase/bbui"
|
||||
import { sortedScreens } from "@/stores/builder"
|
||||
import ScreenNavItem from "./ScreenNavItem.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { getVerticalResizeActions } from "@/components/common/resizable"
|
||||
import NavHeader from "@/components/common/NavHeader.svelte"
|
||||
import type { Screen } from "@budibase/types"
|
||||
import { getVerticalResizeActions } from "@/components/common/resizable"
|
||||
import { contextMenuStore, sortedScreens } from "@/stores/builder"
|
||||
import { workspaceAppStore } from "@/stores/builder/workspaceApps"
|
||||
import { featureFlags } from "@/stores/portal"
|
||||
import { Layout } from "@budibase/bbui"
|
||||
import type { WorkspaceApp, Screen, UIWorkspaceApp } from "@budibase/types"
|
||||
import { goto } from "@roxi/routify"
|
||||
import WorkspaceAppModal from "../WorkspaceApp/WorkspaceAppModal.svelte"
|
||||
import WorkspaceAppNavItem from "./WorkspaceAppNavItem.svelte"
|
||||
import ScreenNavItem from "./ScreenNavItem.svelte"
|
||||
|
||||
$: workspaceAppsEnabled = $featureFlags.WORKSPACE_APPS
|
||||
|
||||
const [resizable, resizableHandle] = getVerticalResizeActions()
|
||||
|
||||
|
@ -14,7 +20,14 @@
|
|||
let screensContainer: HTMLDivElement
|
||||
let scrolling = false
|
||||
|
||||
let workspaceAppModal: WorkspaceAppModal
|
||||
let selectedWorkspaceApp: WorkspaceApp | undefined
|
||||
|
||||
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
||||
$: filteredWorkspaceApps = getFilteredWorkspaceApps(
|
||||
$workspaceAppStore.workspaceApps,
|
||||
searchValue
|
||||
)
|
||||
|
||||
const handleOpenSearch = async () => {
|
||||
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||
|
@ -32,9 +45,64 @@
|
|||
})
|
||||
}
|
||||
|
||||
const getFilteredWorkspaceApps = (
|
||||
workspaceApps: UIWorkspaceApp[],
|
||||
searchValue: string
|
||||
) => {
|
||||
if (!searchValue) {
|
||||
return workspaceApps
|
||||
}
|
||||
|
||||
const filteredProjects: UIWorkspaceApp[] = []
|
||||
for (const workspaceApp of workspaceApps) {
|
||||
filteredProjects.push({
|
||||
...workspaceApp,
|
||||
screens: getFilteredScreens(workspaceApp.screens, searchValue),
|
||||
})
|
||||
}
|
||||
return filteredProjects
|
||||
}
|
||||
|
||||
const handleScroll = (e: any) => {
|
||||
scrolling = e.target.scrollTop !== 0
|
||||
}
|
||||
|
||||
const onAdd = (e: Event) => {
|
||||
if (!workspaceAppsEnabled) {
|
||||
return $goto("../new")
|
||||
}
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: "Add app",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: () => {
|
||||
workspaceAppModal.show()
|
||||
},
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
name: "Add screen",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
callback: () => $goto("../new"),
|
||||
},
|
||||
]
|
||||
|
||||
const boundingBox = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
|
||||
contextMenuStore.open("newProject", items, {
|
||||
x: boundingBox.x,
|
||||
y: boundingBox.y + boundingBox.height,
|
||||
})
|
||||
}
|
||||
|
||||
function onEditWorkspaceApp(workspaceApp: WorkspaceApp) {
|
||||
selectedWorkspaceApp = workspaceApp
|
||||
workspaceAppModal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="screens" class:searching use:resizable>
|
||||
|
@ -44,11 +112,19 @@
|
|||
placeholder="Search for screens"
|
||||
bind:value={searchValue}
|
||||
bind:search={searching}
|
||||
onAdd={() => $goto("../new")}
|
||||
{onAdd}
|
||||
/>
|
||||
</div>
|
||||
<div on:scroll={handleScroll} bind:this={screensContainer} class="content">
|
||||
{#if filteredScreens?.length}
|
||||
{#if workspaceAppsEnabled}
|
||||
{#each filteredWorkspaceApps as workspaceApp}
|
||||
<WorkspaceAppNavItem
|
||||
{workspaceApp}
|
||||
on:edit={() => onEditWorkspaceApp(workspaceApp)}
|
||||
{searchValue}
|
||||
/>
|
||||
{/each}
|
||||
{:else if filteredScreens?.length}
|
||||
{#each filteredScreens as screen (screen._id)}
|
||||
<ScreenNavItem {screen} />
|
||||
{/each}
|
||||
|
@ -69,6 +145,12 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<WorkspaceAppModal
|
||||
bind:this={workspaceAppModal}
|
||||
workspaceApp={selectedWorkspaceApp}
|
||||
on:hide={() => (selectedWorkspaceApp = undefined)}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.screens {
|
||||
display: flex;
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
<script lang="ts">
|
||||
import EditableIcon from "@/components/common/EditableIcon.svelte"
|
||||
import { workspaceAppStore } from "@/stores/builder"
|
||||
import { Input, keepOpen, Label, Modal, ModalContent } from "@budibase/bbui"
|
||||
import type { WorkspaceApp } from "@budibase/types"
|
||||
import type { ZodType } from "zod"
|
||||
import { z } from "zod"
|
||||
|
||||
export let workspaceApp: WorkspaceApp | null = null
|
||||
|
||||
let modal: Modal
|
||||
export const show = () => modal.show()
|
||||
|
||||
let errors: Partial<Record<keyof WorkspaceApp, string>> = {}
|
||||
let data: WorkspaceApp
|
||||
|
||||
$: isNew = !workspaceApp
|
||||
|
||||
$: title = isNew ? "Create new app" : "Edit app"
|
||||
|
||||
const requiredString = (errorMessage: string) =>
|
||||
z.string({ required_error: errorMessage }).trim().min(1, errorMessage)
|
||||
|
||||
const validateWorkspaceApp = (workspaceApp: Partial<WorkspaceApp>) => {
|
||||
const validator = z.object({
|
||||
name: requiredString("Name is required.").refine(
|
||||
val =>
|
||||
!$workspaceAppStore.workspaceApps
|
||||
.filter(a => a._id !== workspaceApp._id)
|
||||
.map(a => a.name.toLowerCase())
|
||||
.includes(val.toLowerCase()),
|
||||
{
|
||||
message: "This name is already taken.",
|
||||
}
|
||||
),
|
||||
urlPrefix: requiredString("Url prefix is required.")
|
||||
.regex(/^\/\w*$/, {
|
||||
message:
|
||||
"Url must start with / and contain only alphanumeric characters.",
|
||||
})
|
||||
.refine(
|
||||
val =>
|
||||
!$workspaceAppStore.workspaceApps
|
||||
.filter(a => a._id !== workspaceApp._id)
|
||||
.map(a => a.urlPrefix.toLowerCase())
|
||||
.includes(val.toLowerCase()),
|
||||
{
|
||||
message: "This url is already taken.",
|
||||
}
|
||||
),
|
||||
icon: z.string(),
|
||||
iconColor: z.string(),
|
||||
}) satisfies ZodType<WorkspaceApp>
|
||||
|
||||
const validationResult = validator.safeParse(workspaceApp)
|
||||
errors = {}
|
||||
if (!validationResult.success) {
|
||||
errors = Object.entries(
|
||||
validationResult.error.formErrors.fieldErrors
|
||||
).reduce<Record<string, string>>((acc, [field, errors]) => {
|
||||
if (errors[0]) {
|
||||
acc[field] = errors[0]
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
return validationResult
|
||||
}
|
||||
|
||||
function onShow() {
|
||||
data = {
|
||||
_id: workspaceApp?._id,
|
||||
_rev: workspaceApp?._rev,
|
||||
name: workspaceApp?.name ?? "",
|
||||
urlPrefix: workspaceApp?.urlPrefix ?? "",
|
||||
icon: workspaceApp?.icon ?? "Monitoring",
|
||||
iconColor: workspaceApp?.iconColor ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
const validationResult = validateWorkspaceApp(data)
|
||||
if (validationResult.error) {
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
const { data: workspaceAppData } = validationResult
|
||||
|
||||
if (isNew) {
|
||||
await workspaceAppStore.add(workspaceAppData)
|
||||
} else {
|
||||
await workspaceAppStore.edit({
|
||||
...workspaceAppData,
|
||||
_id: workspaceApp!._id,
|
||||
_rev: workspaceApp!._rev,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onEnterKey() {
|
||||
const result = await onConfirm()
|
||||
if (result === keepOpen) {
|
||||
return result
|
||||
}
|
||||
|
||||
modal.hide()
|
||||
}
|
||||
|
||||
$: {
|
||||
if (data && !data.urlPrefix.startsWith("/")) {
|
||||
data.urlPrefix = `/${data.urlPrefix}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:show={onShow} on:hide>
|
||||
<ModalContent {title} {onConfirm}>
|
||||
<Input
|
||||
label="App Name"
|
||||
on:enterkey={onEnterKey}
|
||||
bind:value={data.name}
|
||||
error={errors.name}
|
||||
/>
|
||||
<Input
|
||||
label="Project url"
|
||||
on:enterkey={onEnterKey}
|
||||
bind:value={data.urlPrefix}
|
||||
error={errors.urlPrefix}
|
||||
/>
|
||||
|
||||
<Label size="L">Icon</Label>
|
||||
<EditableIcon
|
||||
bind:name={data.icon}
|
||||
color={data.iconColor || ""}
|
||||
on:change={e => {
|
||||
data.iconColor = e.detail
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
|
@ -20,6 +20,8 @@
|
|||
import { makeTableOption, makeViewOption } from "./utils"
|
||||
import type { Screen, Table, ViewV2 } from "@budibase/types"
|
||||
|
||||
export let workspaceAppId: string
|
||||
|
||||
let mode: string
|
||||
|
||||
let screenDetailsModal: Modal
|
||||
|
@ -71,7 +73,10 @@
|
|||
|
||||
const createScreen = async (screenTemplate: Screen): Promise<Screen> => {
|
||||
try {
|
||||
return await screenStore.save(screenTemplate)
|
||||
return await screenStore.save({
|
||||
...screenTemplate,
|
||||
workspaceAppId: workspaceAppId,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error creating screens")
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import { licensing } from "@/stores/portal"
|
||||
import { AutoScreenTypes } from "@/constants"
|
||||
|
||||
export let workspaceAppId
|
||||
export let onClose = null
|
||||
|
||||
let createScreenModal
|
||||
|
@ -95,7 +96,7 @@
|
|||
</CreationPage>
|
||||
</div>
|
||||
|
||||
<CreateScreenModal bind:this={createScreenModal} />
|
||||
<CreateScreenModal {workspaceAppId} bind:this={createScreenModal} />
|
||||
|
||||
<style>
|
||||
.page {
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
$: onClose = getOnClose($screenStore)
|
||||
|
||||
$: workspaceAppId =
|
||||
new URLSearchParams(window.location.search).get("workspaceAppId") ||
|
||||
"default"
|
||||
|
||||
const getOnClose = ({ screens, selectedScreenId }) => {
|
||||
if (!screens?.length) {
|
||||
return null
|
||||
|
@ -20,4 +24,4 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<NewScreen {onClose} />
|
||||
<NewScreen {workspaceAppId} {onClose} />
|
||||
|
|
|
@ -10,8 +10,9 @@ interface MenuItem {
|
|||
name: string
|
||||
keyBind: string | null
|
||||
visible: boolean
|
||||
disabled: boolean
|
||||
disabled?: boolean
|
||||
callback: () => void
|
||||
isNew?: boolean
|
||||
}
|
||||
|
||||
interface ContextMenuState {
|
||||
|
|
|
@ -37,6 +37,7 @@ import { flags } from "./flags"
|
|||
import { rowActions } from "./rowActions"
|
||||
import componentTreeNodesStore from "./componentTreeNodes"
|
||||
import { oauth2 } from "./oauth2"
|
||||
import { workspaceAppStore } from "./workspaceApps"
|
||||
|
||||
import { FetchAppPackageResponse } from "@budibase/types"
|
||||
|
||||
|
@ -79,6 +80,7 @@ export {
|
|||
screenComponentErrors,
|
||||
screenComponentErrorList,
|
||||
oauth2,
|
||||
workspaceAppStore,
|
||||
}
|
||||
|
||||
export const reset = () => {
|
||||
|
@ -121,6 +123,7 @@ export const initialise = async (pkg: FetchAppPackageResponse) => {
|
|||
themeStore.syncAppTheme(application)
|
||||
snippets.syncMetadata(application)
|
||||
screenStore.syncAppScreens(pkg)
|
||||
workspaceAppStore.syncWorkspaceApps(pkg)
|
||||
layoutStore.syncAppLayouts(pkg)
|
||||
resetBuilderHistory()
|
||||
await refreshBuilderData()
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
SaveScreenResponse,
|
||||
Screen,
|
||||
ScreenVariant,
|
||||
WithRequired,
|
||||
} from "@budibase/types"
|
||||
import { featureFlag } from "@/helpers"
|
||||
|
||||
|
@ -39,7 +40,7 @@ export const initialScreenState: ScreenState = {
|
|||
export class ScreenStore extends BudiStore<ScreenState> {
|
||||
history: HistoryStore<Screen>
|
||||
delete: (screens: Screen) => Promise<void>
|
||||
save: (screen: Screen) => Promise<Screen>
|
||||
save: (screen: WithRequired<Screen, "workspaceAppId">) => Promise<Screen>
|
||||
|
||||
constructor() {
|
||||
super(initialScreenState)
|
||||
|
@ -312,7 +313,10 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
if (result === false) {
|
||||
return
|
||||
}
|
||||
return this.save(clone)
|
||||
return this.save({
|
||||
...clone,
|
||||
workspaceAppId: clone.workspaceAppId!,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -245,7 +245,9 @@ describe("Screens store", () => {
|
|||
|
||||
expect(bb.store.screens.length).toBe(1)
|
||||
|
||||
expect(bb.store.screens[0]).toStrictEqual(newDoc)
|
||||
expect(bb.store.screens[0]).toStrictEqual({
|
||||
...newDoc,
|
||||
})
|
||||
|
||||
expect(bb.store.selectedScreenId).toBe(newDocId)
|
||||
|
||||
|
@ -808,6 +810,10 @@ describe("Screens store", () => {
|
|||
screen.name = "updated"
|
||||
}, existingDocId)
|
||||
|
||||
expect(saveSpy).toBeCalledWith({ ...original, name: "updated" })
|
||||
expect(saveSpy).toBeCalledWith({
|
||||
...original,
|
||||
workspaceAppId: "default",
|
||||
name: "updated",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import { DerivedBudiStore } from "@/stores/BudiStore"
|
||||
import {
|
||||
WorkspaceApp,
|
||||
UIWorkspaceApp,
|
||||
FetchAppPackageResponse,
|
||||
FeatureFlag,
|
||||
} from "@budibase/types"
|
||||
import { derived, Readable } from "svelte/store"
|
||||
import { screenStore } from "./screens"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { featureFlag } from "@/helpers"
|
||||
|
||||
interface WorkspaceAppStoreState {
|
||||
workspaceApps: WorkspaceApp[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
interface DerivedWorkspaceAppStoreState {
|
||||
workspaceApps: UIWorkspaceApp[]
|
||||
}
|
||||
|
||||
export class WorkspaceAppStore extends DerivedBudiStore<
|
||||
WorkspaceAppStoreState,
|
||||
DerivedWorkspaceAppStoreState
|
||||
> {
|
||||
constructor() {
|
||||
const makeDerivedStore = (store: Readable<WorkspaceAppStoreState>) => {
|
||||
return derived([store, screenStore], ([$store, $screenStore]) => {
|
||||
const workspaceApps = $store.workspaceApps.map<UIWorkspaceApp>(
|
||||
workspaceApp => {
|
||||
return {
|
||||
...workspaceApp,
|
||||
screens: $screenStore.screens.filter(
|
||||
s => s.workspaceAppId === workspaceApp._id
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return { workspaceApps }
|
||||
})
|
||||
}
|
||||
|
||||
super(
|
||||
{
|
||||
workspaceApps: [],
|
||||
loading: true,
|
||||
},
|
||||
makeDerivedStore
|
||||
)
|
||||
}
|
||||
|
||||
syncWorkspaceApps(pkg: FetchAppPackageResponse) {
|
||||
let workspaceApps = featureFlag.isEnabled(FeatureFlag.WORKSPACE_APPS)
|
||||
? pkg.workspaceApps
|
||||
: []
|
||||
this.update(state => ({
|
||||
...state,
|
||||
workspaceApps,
|
||||
loading: false,
|
||||
}))
|
||||
}
|
||||
|
||||
async add(workspaceApp: WorkspaceApp) {
|
||||
this.store.update(state => {
|
||||
state.workspaceApps.push({ ...workspaceApp, _id: Helpers.uuid() })
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async edit(workspaceApp: WorkspaceApp) {
|
||||
this.store.update(state => {
|
||||
const index = state.workspaceApps.findIndex(
|
||||
app => app._id === workspaceApp._id
|
||||
)
|
||||
if (index === -1) {
|
||||
throw new Error(`App not found with id "${workspaceApp._id}"`)
|
||||
}
|
||||
|
||||
state.workspaceApps[index] = {
|
||||
...workspaceApp,
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async delete(_id: string | undefined, _rev: string | undefined) {
|
||||
this.store.update(state => {
|
||||
state.workspaceApps = state.workspaceApps.filter(app => app._id !== _id)
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const workspaceAppStore = new WorkspaceAppStore()
|
|
@ -18,10 +18,11 @@ export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
|||
* @param screen the screen to save
|
||||
*/
|
||||
saveScreen: async screen => {
|
||||
return await API.post({
|
||||
const result = await API.post<SaveScreenRequest, SaveScreenResponse>({
|
||||
url: "/api/screens",
|
||||
body: screen,
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,4 +6,5 @@ export * from "./dataFetch"
|
|||
export * from "./datasource"
|
||||
export * from "./fields"
|
||||
export * from "./permissions"
|
||||
export * from "./workspaceApps"
|
||||
export * from "./stores"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { WorkspaceApp, Screen } from "../documents"
|
||||
|
||||
export interface UIWorkspaceApp extends WorkspaceApp {
|
||||
screens: Screen[]
|
||||
}
|
Loading…
Reference in New Issue