Frontend update for app builders, handling when in the builder portal and don't have any app access, as well as allowing viewing of apps from the portal.

This commit is contained in:
mike12345567 2023-07-27 17:52:56 +01:00
parent 6176898ae3
commit d8f50f139e
7 changed files with 155 additions and 116 deletions

View File

@ -1,16 +1,21 @@
<script> <script>
import { Heading, Body, Button, Icon } from "@budibase/bbui" import { Heading, Body, Button, Icon } from "@budibase/bbui"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import { auth } from "stores/portal"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { UserAvatars } from "@budibase/frontend-core" import { UserAvatars } from "@budibase/frontend-core"
import { sdk } from "@budibase/shared-core"
export let app export let app
export let lockedAction export let lockedAction
$: editing = app.sessions?.length $: editing = app.sessions?.length
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
const handleDefaultClick = () => { const handleDefaultClick = () => {
if (window.innerWidth < 640) { if (!isBuilder) {
goToApp()
} else if (window.innerWidth < 640) {
goToOverview() goToOverview()
} else { } else {
goToBuilder() goToBuilder()
@ -24,6 +29,10 @@
const goToOverview = () => { const goToOverview = () => {
$goto(`../../app/${app.devId}/settings`) $goto(`../../app/${app.devId}/settings`)
} }
const goToApp = () => {
window.open(`/app/${app.name}`, "_blank")
}
</script> </script>
<div class="app-row" on:click={lockedAction || handleDefaultClick}> <div class="app-row" on:click={lockedAction || handleDefaultClick}>
@ -39,7 +48,7 @@
</div> </div>
<div class="updated"> <div class="updated">
{#if editing} {#if editing && isBuilder}
Currently editing Currently editing
<UserAvatars users={app.sessions} /> <UserAvatars users={app.sessions} />
{:else if app.updatedAt} {:else if app.updatedAt}
@ -56,14 +65,21 @@
<Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body> <Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body>
</div> </div>
<div class="app-row-actions"> {#if isBuilder}
<Button size="S" secondary on:click={lockedAction || goToOverview}> <div class="app-row-actions">
Manage <Button size="S" secondary on:click={lockedAction || goToOverview}>
</Button> Manage
<Button size="S" primary on:click={lockedAction || goToBuilder}> </Button>
Edit <Button size="S" primary on:click={lockedAction || goToBuilder}>
</Button> Edit
</div> </Button>
</div>
{:else}
<!-- this can happen if an app builder has app user access to an app -->
<div class="app-row-actions">
<Button size="S" secondary>View</Button>
</div>
{/if}
</div> </div>
<style> <style>

View File

@ -108,9 +108,9 @@
await usersFetch.refresh() await usersFetch.refresh()
filteredUsers = $usersFetch.rows.map(user => { filteredUsers = $usersFetch.rows.map(user => {
const isBuilderOrAdmin = sdk.users.isBuilderOrAdmin(user, prodAppId) const isAdminOrBuilder = sdk.users.isAdminOrBuilder(user, prodAppId)
let role = undefined let role = undefined
if (isBuilderOrAdmin) { if (isAdminOrBuilder) {
role = Constants.Roles.ADMIN role = Constants.Roles.ADMIN
} else { } else {
const appRole = Object.keys(user.roles).find(x => x === prodAppId) const appRole = Object.keys(user.roles).find(x => x === prodAppId)
@ -122,7 +122,7 @@
return { return {
...user, ...user,
role, role,
isBuilderOrAdmin, isAdminOrBuilder,
} }
}) })
} }
@ -258,7 +258,7 @@
} }
// Must exclude users who have explicit privileges // Must exclude users who have explicit privileges
const userByEmail = filteredUsers.reduce((acc, user) => { const userByEmail = filteredUsers.reduce((acc, user) => {
if (user.role || sdk.users.isBuilderOrAdmin(user, prodAppId)) { if (user.role || sdk.users.isAdminOrBuilder(user, prodAppId)) {
acc.push(user.email) acc.push(user.email)
} }
return acc return acc
@ -403,7 +403,7 @@
const role = $roles.find(role => role._id === user.role) const role = $roles.find(role => role._id === user.role)
return `This user has been given ${role?.name} access from the ${user.group} group` return `This user has been given ${role?.name} access from the ${user.group} group`
} }
if (user.isBuilderOrAdmin) { if (user.isAdminOrBuilder) {
return "This user's role grants admin access to all apps" return "This user's role grants admin access to all apps"
} }
return null return null
@ -614,7 +614,7 @@
}} }}
autoWidth autoWidth
align="right" align="right"
allowedRoles={user.isBuilderOrAdmin allowedRoles={user.isAdminOrBuilder
? [Constants.Roles.ADMIN] ? [Constants.Roles.ADMIN]
: null} : null}
/> />

View File

@ -15,7 +15,7 @@
let activeTab = "Apps" let activeTab = "Apps"
$: $url(), updateActiveTab($menu) $: $url(), updateActiveTab($menu)
$: fullscreen = !$apps.length $: fullscreen = $apps.length == null
const updateActiveTab = menu => { const updateActiveTab = menu => {
for (let entry of menu) { for (let entry of menu) {

View File

@ -1,11 +1,19 @@
<script> <script>
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { admin, apps, templates, licensing, groups } from "stores/portal" import {
admin,
apps,
templates,
licensing,
groups,
auth,
} from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { sdk } from "@budibase/shared-core"
// Don't block loading if we've already hydrated state // Don't block loading if we've already hydrated state
let loaded = $apps.length > 0 let loaded = $apps.length != null
onMount(async () => { onMount(async () => {
try { try {
@ -25,7 +33,7 @@
} }
// Go to new app page if no apps exists // Go to new app page if no apps exists
if (!$apps.length) { if (!$apps.length && sdk.users.isGlobalBuilder($auth.user)) {
$redirect("./onboarding") $redirect("./onboarding")
} }
} catch (error) { } catch (error) {

View File

@ -204,106 +204,109 @@
}) })
</script> </script>
{#if $apps.length} <Page>
<Page> <Layout noPadding gap="L">
<Layout noPadding gap="L"> {#each Object.keys(automationErrors || {}) as appId}
{#each Object.keys(automationErrors || {}) as appId} <Notification
<Notification wide
wide dismissable
dismissable action={() => goToAutomationError(appId)}
action={() => goToAutomationError(appId)} type="error"
type="error" icon="Alert"
icon="Alert" actionMessage={errorCount(automationErrors[appId]) > 1
actionMessage={errorCount(automationErrors[appId]) > 1 ? "View errors"
? "View errors" : "View error"}
: "View error"} on:dismiss={async () => {
on:dismiss={async () => { await automationStore.actions.clearLogErrors({ appId })
await automationStore.actions.clearLogErrors({ appId }) await apps.load()
await apps.load() }}
}} message={automationErrorMessage(appId)}
message={automationErrorMessage(appId)} />
/> {/each}
{/each} <div class="title">
<div class="title"> <div class="welcome">
<div class="welcome"> <Layout noPadding gap="XS">
<Layout noPadding gap="XS"> <Heading size="L">{welcomeHeader}</Heading>
<Heading size="L">{welcomeHeader}</Heading> <Body size="M">
<Body size="M"> Below you'll find the list of apps that you have access to
Manage your apps and get a head start with templates </Body>
</Body> </Layout>
</Layout>
</div>
</div> </div>
</div>
{#if enrichedApps.length} {#if enrichedApps.length}
<Layout noPadding gap="L"> <Layout noPadding gap="L">
<div class="title"> <div class="title">
{#if $auth.user && sdk.users.isGlobalBuilder($auth.user)} {#if $auth.user && sdk.users.isGlobalBuilder($auth.user)}
<div class="buttons"> <div class="buttons">
<Button
size="M"
cta
on:click={usersLimitLockAction || initiateAppCreation}
>
Create new app
</Button>
{#if $apps?.length > 0 && !$admin.offlineMode}
<Button <Button
size="M" size="M"
cta secondary
on:click={usersLimitLockAction || initiateAppCreation} on:click={usersLimitLockAction ||
$goto("/builder/portal/apps/templates")}
> >
Create new app View templates
</Button> </Button>
{#if $apps?.length > 0 && !$admin.offlineMode} {/if}
<Button {#if !$apps?.length}
size="M" <Button
secondary size="L"
on:click={usersLimitLockAction || quiet
$goto("/builder/portal/apps/templates")} secondary
> on:click={usersLimitLockAction || initiateAppImport}
View templates >
</Button> Import app
{/if} </Button>
{#if !$apps?.length} {/if}
<Button </div>
size="L" {/if}
quiet {#if enrichedApps.length > 1}
secondary <div class="app-actions">
on:click={usersLimitLockAction || initiateAppImport} <Select
> autoWidth
Import app bind:value={sortBy}
</Button> placeholder={null}
{/if} options={[
</div> { label: "Sort by name", value: "name" },
{/if} { label: "Sort by recently updated", value: "updated" },
{#if enrichedApps.length > 1} { label: "Sort by status", value: "status" },
<div class="app-actions"> ]}
<Select />
autoWidth <Search placeholder="Search" bind:value={searchTerm} />
bind:value={sortBy} </div>
placeholder={null} {/if}
options={[
{ label: "Sort by name", value: "name" },
{ label: "Sort by recently updated", value: "updated" },
{ label: "Sort by status", value: "status" },
]}
/>
<Search placeholder="Search" bind:value={searchTerm} />
</div>
{/if}
</div>
<div class="app-table">
{#each filteredApps as app (app.appId)}
<AppRow {app} lockedAction={usersLimitLockAction} />
{/each}
</div>
</Layout>
{/if}
{#if creatingFromTemplate}
<div class="empty-wrapper">
<img class="img-logo img-size" alt="logo" src={Logo} />
<p>Creating your Budibase app from your selected template...</p>
<Spinner size="10" />
</div> </div>
{/if}
</Layout> <div class="app-table">
</Page> {#each filteredApps as app (app.appId)}
{/if} <AppRow {app} lockedAction={usersLimitLockAction} />
{/each}
</div>
</Layout>
{:else}
<div class="no-apps">
<img class="spaceman" alt="spaceman" src={Logo} width="100px" />
<Body weight="700">You haven't been given access to any apps yet</Body>
</div>
{/if}
{#if creatingFromTemplate}
<div class="empty-wrapper">
<img class="img-logo img-size" alt="logo" src={Logo} />
<p>Creating your Budibase app from your selected template...</p>
<Spinner size="10" />
</div>
{/if}
</Layout>
</Page>
<Modal <Modal
bind:this={creationModal} bind:this={creationModal}
@ -371,6 +374,16 @@
height: 160px; height: 160px;
} }
.no-apps {
background-color: var(--spectrum-global-color-gray-100);
padding: calc(var(--spacing-xl) * 2);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: var(--spacing-xl);
}
@media (max-width: 1000px) { @media (max-width: 1000px) {
.img-logo { .img-logo {
display: none; display: none;

View File

@ -7,10 +7,12 @@ import { db as dbCore, users } from "@budibase/backend-core"
export function filterAppList(user: ContextUser, apps: App[]) { export function filterAppList(user: ContextUser, apps: App[]) {
let appList: string[] = [] let appList: string[] = []
const roleApps = Object.keys(user.roles || {})
if (users.hasAppBuilderPermissions(user)) { if (users.hasAppBuilderPermissions(user)) {
appList = user.builder?.apps! appList = user.builder?.apps || []
appList = appList.concat(roleApps)
} else if (!users.isAdminOrBuilder(user)) { } else if (!users.isAdminOrBuilder(user)) {
appList = Object.keys(user.roles || {}) appList = roleApps
} else { } else {
return apps return apps
} }

View File

@ -206,7 +206,7 @@ describe("/api/global/auth", () => {
const newPassword = "newpassword" const newPassword = "newpassword"
const res = await config.api.auth.updatePassword(code!, newPassword) const res = await config.api.auth.updatePassword(code!, newPassword)
user = await config.getUser(user.email) as User user = (await config.getUser(user.email)) as User
delete user.password delete user.password
expect(res.body).toEqual({ message: "password reset successfully." }) expect(res.body).toEqual({ message: "password reset successfully." })