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>
|
||||
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { auth } from "stores/portal"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
export let app
|
||||
export let lockedAction
|
||||
|
||||
$: editing = app.sessions?.length
|
||||
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
||||
|
||||
const handleDefaultClick = () => {
|
||||
if (window.innerWidth < 640) {
|
||||
if (!isBuilder) {
|
||||
goToApp()
|
||||
} else if (window.innerWidth < 640) {
|
||||
goToOverview()
|
||||
} else {
|
||||
goToBuilder()
|
||||
|
@ -24,6 +29,10 @@
|
|||
const goToOverview = () => {
|
||||
$goto(`../../app/${app.devId}/settings`)
|
||||
}
|
||||
|
||||
const goToApp = () => {
|
||||
window.open(`/app/${app.name}`, "_blank")
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
||||
|
@ -39,7 +48,7 @@
|
|||
</div>
|
||||
|
||||
<div class="updated">
|
||||
{#if editing}
|
||||
{#if editing && isBuilder}
|
||||
Currently editing
|
||||
<UserAvatars users={app.sessions} />
|
||||
{:else if app.updatedAt}
|
||||
|
@ -56,14 +65,21 @@
|
|||
<Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body>
|
||||
</div>
|
||||
|
||||
<div class="app-row-actions">
|
||||
<Button size="S" secondary on:click={lockedAction || goToOverview}>
|
||||
Manage
|
||||
</Button>
|
||||
<Button size="S" primary on:click={lockedAction || goToBuilder}>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{#if isBuilder}
|
||||
<div class="app-row-actions">
|
||||
<Button size="S" secondary on:click={lockedAction || goToOverview}>
|
||||
Manage
|
||||
</Button>
|
||||
<Button size="S" primary on:click={lockedAction || goToBuilder}>
|
||||
Edit
|
||||
</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>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -108,9 +108,9 @@
|
|||
await usersFetch.refresh()
|
||||
|
||||
filteredUsers = $usersFetch.rows.map(user => {
|
||||
const isBuilderOrAdmin = sdk.users.isBuilderOrAdmin(user, prodAppId)
|
||||
const isAdminOrBuilder = sdk.users.isAdminOrBuilder(user, prodAppId)
|
||||
let role = undefined
|
||||
if (isBuilderOrAdmin) {
|
||||
if (isAdminOrBuilder) {
|
||||
role = Constants.Roles.ADMIN
|
||||
} else {
|
||||
const appRole = Object.keys(user.roles).find(x => x === prodAppId)
|
||||
|
@ -122,7 +122,7 @@
|
|||
return {
|
||||
...user,
|
||||
role,
|
||||
isBuilderOrAdmin,
|
||||
isAdminOrBuilder,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -258,7 +258,7 @@
|
|||
}
|
||||
// Must exclude users who have explicit privileges
|
||||
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)
|
||||
}
|
||||
return acc
|
||||
|
@ -403,7 +403,7 @@
|
|||
const role = $roles.find(role => role._id === user.role)
|
||||
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 null
|
||||
|
@ -614,7 +614,7 @@
|
|||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isBuilderOrAdmin
|
||||
allowedRoles={user.isAdminOrBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
: null}
|
||||
/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
let activeTab = "Apps"
|
||||
|
||||
$: $url(), updateActiveTab($menu)
|
||||
$: fullscreen = !$apps.length
|
||||
$: fullscreen = $apps.length == null
|
||||
|
||||
const updateActiveTab = menu => {
|
||||
for (let entry of menu) {
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
<script>
|
||||
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 { redirect } from "@roxi/routify"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
// Don't block loading if we've already hydrated state
|
||||
let loaded = $apps.length > 0
|
||||
let loaded = $apps.length != null
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
|
@ -25,7 +33,7 @@
|
|||
}
|
||||
|
||||
// Go to new app page if no apps exists
|
||||
if (!$apps.length) {
|
||||
if (!$apps.length && sdk.users.isGlobalBuilder($auth.user)) {
|
||||
$redirect("./onboarding")
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -204,106 +204,109 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
{#if $apps.length}
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
{#each Object.keys(automationErrors || {}) as appId}
|
||||
<Notification
|
||||
wide
|
||||
dismissable
|
||||
action={() => goToAutomationError(appId)}
|
||||
type="error"
|
||||
icon="Alert"
|
||||
actionMessage={errorCount(automationErrors[appId]) > 1
|
||||
? "View errors"
|
||||
: "View error"}
|
||||
on:dismiss={async () => {
|
||||
await automationStore.actions.clearLogErrors({ appId })
|
||||
await apps.load()
|
||||
}}
|
||||
message={automationErrorMessage(appId)}
|
||||
/>
|
||||
{/each}
|
||||
<div class="title">
|
||||
<div class="welcome">
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="L">{welcomeHeader}</Heading>
|
||||
<Body size="M">
|
||||
Manage your apps and get a head start with templates
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
{#each Object.keys(automationErrors || {}) as appId}
|
||||
<Notification
|
||||
wide
|
||||
dismissable
|
||||
action={() => goToAutomationError(appId)}
|
||||
type="error"
|
||||
icon="Alert"
|
||||
actionMessage={errorCount(automationErrors[appId]) > 1
|
||||
? "View errors"
|
||||
: "View error"}
|
||||
on:dismiss={async () => {
|
||||
await automationStore.actions.clearLogErrors({ appId })
|
||||
await apps.load()
|
||||
}}
|
||||
message={automationErrorMessage(appId)}
|
||||
/>
|
||||
{/each}
|
||||
<div class="title">
|
||||
<div class="welcome">
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="L">{welcomeHeader}</Heading>
|
||||
<Body size="M">
|
||||
Below you'll find the list of apps that you have access to
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if enrichedApps.length}
|
||||
<Layout noPadding gap="L">
|
||||
<div class="title">
|
||||
{#if $auth.user && sdk.users.isGlobalBuilder($auth.user)}
|
||||
<div class="buttons">
|
||||
{#if enrichedApps.length}
|
||||
<Layout noPadding gap="L">
|
||||
<div class="title">
|
||||
{#if $auth.user && sdk.users.isGlobalBuilder($auth.user)}
|
||||
<div class="buttons">
|
||||
<Button
|
||||
size="M"
|
||||
cta
|
||||
on:click={usersLimitLockAction || initiateAppCreation}
|
||||
>
|
||||
Create new app
|
||||
</Button>
|
||||
{#if $apps?.length > 0 && !$admin.offlineMode}
|
||||
<Button
|
||||
size="M"
|
||||
cta
|
||||
on:click={usersLimitLockAction || initiateAppCreation}
|
||||
secondary
|
||||
on:click={usersLimitLockAction ||
|
||||
$goto("/builder/portal/apps/templates")}
|
||||
>
|
||||
Create new app
|
||||
View templates
|
||||
</Button>
|
||||
{#if $apps?.length > 0 && !$admin.offlineMode}
|
||||
<Button
|
||||
size="M"
|
||||
secondary
|
||||
on:click={usersLimitLockAction ||
|
||||
$goto("/builder/portal/apps/templates")}
|
||||
>
|
||||
View templates
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !$apps?.length}
|
||||
<Button
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
on:click={usersLimitLockAction || initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if enrichedApps.length > 1}
|
||||
<div class="app-actions">
|
||||
<Select
|
||||
autoWidth
|
||||
bind:value={sortBy}
|
||||
placeholder={null}
|
||||
options={[
|
||||
{ label: "Sort by name", value: "name" },
|
||||
{ label: "Sort by recently updated", value: "updated" },
|
||||
{ label: "Sort by status", value: "status" },
|
||||
]}
|
||||
/>
|
||||
<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" />
|
||||
{/if}
|
||||
{#if !$apps?.length}
|
||||
<Button
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
on:click={usersLimitLockAction || initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if enrichedApps.length > 1}
|
||||
<div class="app-actions">
|
||||
<Select
|
||||
autoWidth
|
||||
bind:value={sortBy}
|
||||
placeholder={null}
|
||||
options={[
|
||||
{ label: "Sort by name", value: "name" },
|
||||
{ label: "Sort by recently updated", value: "updated" },
|
||||
{ label: "Sort by status", value: "status" },
|
||||
]}
|
||||
/>
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</Page>
|
||||
{/if}
|
||||
|
||||
<div class="app-table">
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<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
|
||||
bind:this={creationModal}
|
||||
|
@ -371,6 +374,16 @@
|
|||
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) {
|
||||
.img-logo {
|
||||
display: none;
|
||||
|
|
|
@ -7,10 +7,12 @@ import { db as dbCore, users } from "@budibase/backend-core"
|
|||
|
||||
export function filterAppList(user: ContextUser, apps: App[]) {
|
||||
let appList: string[] = []
|
||||
const roleApps = Object.keys(user.roles || {})
|
||||
if (users.hasAppBuilderPermissions(user)) {
|
||||
appList = user.builder?.apps!
|
||||
appList = user.builder?.apps || []
|
||||
appList = appList.concat(roleApps)
|
||||
} else if (!users.isAdminOrBuilder(user)) {
|
||||
appList = Object.keys(user.roles || {})
|
||||
appList = roleApps
|
||||
} else {
|
||||
return apps
|
||||
}
|
||||
|
|
|
@ -206,7 +206,7 @@ describe("/api/global/auth", () => {
|
|||
const newPassword = "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
|
||||
|
||||
expect(res.body).toEqual({ message: "password reset successfully." })
|
||||
|
|
Loading…
Reference in New Issue