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:
parent
6176898ae3
commit
d8f50f139e
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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." })
|
||||||
|
|
Loading…
Reference in New Issue