Add initial new UI for views

This commit is contained in:
Andrew Kingston 2024-08-15 16:14:00 +01:00
parent 0109fce51d
commit fa80d99139
No known key found for this signature in database
16 changed files with 386 additions and 188 deletions

View File

@ -57,64 +57,52 @@
} }
</script> </script>
<div class="wrapper"> <Grid
<Grid {API}
{API} {darkMode}
{darkMode} datasource={gridDatasource}
datasource={gridDatasource} canAddRows={!isUsersTable}
canAddRows={!isUsersTable} canDeleteRows={!isUsersTable}
canDeleteRows={!isUsersTable} canEditRows={!isUsersTable || !$appStore.features.disableUserMetadata}
canEditRows={!isUsersTable || !$appStore.features.disableUserMetadata} canEditColumns={!isUsersTable || !$appStore.features.disableUserMetadata}
canEditColumns={!isUsersTable || !$appStore.features.disableUserMetadata} schemaOverrides={isUsersTable ? userSchemaOverrides : null}
schemaOverrides={isUsersTable ? userSchemaOverrides : null} showAvatars={false}
showAvatars={false} on:updatedatasource={handleGridTableUpdate}
on:updatedatasource={handleGridTableUpdate} isCloud={$admin.cloud}
isCloud={$admin.cloud} >
> <svelte:fragment slot="filter">
<svelte:fragment slot="filter"> {#if isUsersTable && $appStore.features.disableUserMetadata}
{#if isUsersTable && $appStore.features.disableUserMetadata} <GridUsersTableButton />
<GridUsersTableButton /> {/if}
{/if} <GridFilterButton />
<GridFilterButton /> </svelte:fragment>
</svelte:fragment> <svelte:fragment slot="controls">
<svelte:fragment slot="controls"> {#if !isUsersTable}
{#if !isUsersTable} <GridCreateViewButton />
<GridCreateViewButton /> {/if}
{/if} <GridManageAccessButton />
<GridManageAccessButton /> {#if !isUsersTable}
{#if !isUsersTable} <GridCreateAutomationButton />
<GridCreateAutomationButton /> {/if}
{/if} {#if relationshipsEnabled}
{#if relationshipsEnabled} <GridRelationshipButton />
<GridRelationshipButton /> {/if}
{/if} {#if isUsersTable}
{#if isUsersTable} <EditRolesButton />
<EditRolesButton /> {:else}
{:else} <GridImportButton />
<GridImportButton /> {/if}
{/if} <GridExportButton />
<GridExportButton /> {#if isUsersTable}
{#if isUsersTable} <GridEditUserModal />
<GridEditUserModal /> {:else}
{:else} <GridCreateEditRowModal />
<GridCreateEditRowModal /> {/if}
{/if} </svelte:fragment>
</svelte:fragment> <svelte:fragment slot="edit-column">
<svelte:fragment slot="edit-column"> <GridEditColumnModal />
<GridEditColumnModal /> </svelte:fragment>
</svelte:fragment> <svelte:fragment slot="add-column">
<svelte:fragment slot="add-column"> <GridAddColumnModal />
<GridAddColumnModal /> </svelte:fragment>
</svelte:fragment> </Grid>
</Grid>
</div>
<style>
.wrapper {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
background: var(--background);
}
</style>

View File

@ -19,34 +19,21 @@
} }
</script> </script>
<div class="wrapper"> <Grid
<Grid {API}
{API} {datasource}
{datasource} allowAddRows
allowAddRows allowDeleteRows
allowDeleteRows showAvatars={false}
showAvatars={false} on:updatedatasource={handleGridViewUpdate}
on:updatedatasource={handleGridViewUpdate} isCloud={$admin.cloud}
isCloud={$admin.cloud} allowViewReadonlyColumns={$licensing.isViewReadonlyColumnsEnabled}
allowViewReadonlyColumns={$licensing.isViewReadonlyColumnsEnabled} >
> <svelte:fragment slot="filter">
<svelte:fragment slot="filter"> <GridFilterButton />
<GridFilterButton /> </svelte:fragment>
</svelte:fragment> <svelte:fragment slot="controls">
<svelte:fragment slot="controls"> <GridCreateEditRowModal />
<GridCreateEditRowModal /> <GridManageAccessButton />
<GridManageAccessButton /> </svelte:fragment>
</svelte:fragment> </Grid>
</Grid>
</div>
<style>
.wrapper {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
background: var(--background);
overflow: hidden;
}
</style>

View File

@ -104,7 +104,7 @@
</InlineAlert> </InlineAlert>
</div> </div>
{/if} {/if}
<p class="fourthWarning">Please enter the app name below to confirm.</p> <p class="fourthWarning">Please enter the table name below to confirm.</p>
<Input bind:value={deleteTableName} placeholder={table.name} /> <Input bind:value={deleteTableName} placeholder={table.name} />
</div> </div>
</ConfirmDialog> </ConfirmDialog>

View File

@ -20,14 +20,6 @@
const getContextMenuItems = () => { const getContextMenuItems = () => {
return [ return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
{ {
icon: "Edit", icon: "Edit",
name: "Edit", name: "Edit",
@ -36,6 +28,14 @@
disabled: false, disabled: false,
callback: editModal.show, callback: editModal.show,
}, },
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
] ]
} }

View File

@ -1,7 +1,5 @@
<script> <script>
import { goto } from "@roxi/routify"
import TableNavItem from "./TableNavItem/TableNavItem.svelte" import TableNavItem from "./TableNavItem/TableNavItem.svelte"
import ViewNavItem from "./ViewNavItem/ViewNavItem.svelte"
export let tables export let tables
export let selectTable export let selectTable
@ -16,18 +14,5 @@
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each sortedTables as table, idx} {#each sortedTables as table, idx}
<TableNavItem {table} {idx} on:click={() => selectTable(table._id)} /> <TableNavItem {table} {idx} on:click={() => selectTable(table._id)} />
{#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)}
<ViewNavItem
{view}
{name}
on:click={() => {
if (view.version === 2) {
$goto(`./view/v2/${encodeURIComponent(view.id)}`)
} else {
$goto(`./view/v1/${encodeURIComponent(name)}`)
}
}}
/>
{/each}
{/each} {/each}
</div> </div>

View File

@ -1,71 +0,0 @@
<script>
import {
contextMenuStore,
views,
viewsV2,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import { isActive } from "@roxi/routify"
import { Icon } from "@budibase/bbui"
import EditViewModal from "./EditViewModal.svelte"
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
export let view
export let name
let editModal
let deleteConfirmationModal
const getContextMenuItems = () => {
return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: editModal.show,
},
]
}
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getContextMenuItems()
contextMenuStore.open(view.id, items, { x: e.clientX, y: e.clientY })
}
const isViewActive = (view, isActive, views, viewsV2) => {
return (
(isActive("./view/v1") && views.selected?.name === view.name) ||
(isActive("./view/v2") && viewsV2.selected?.id === view.id)
)
}
</script>
<NavItem
on:contextmenu={openContextMenu}
indentLevel={2}
icon="Remove"
text={name}
selected={isViewActive(view, $isActive, $views, $viewsV2)}
hovering={view.id === $contextMenuStore.id}
on:click
selectedBy={$userSelectedResourceMap[name] ||
$userSelectedResourceMap[view.id]}
>
<Icon on:click={openContextMenu} s hoverable name="MoreSmallList" />
</NavItem>
<EditViewModal {view} bind:this={editModal} />
<DeleteConfirmationModal {view} bind:this={deleteConfirmationModal} />

View File

@ -0,0 +1,24 @@
<script>
import { viewsV2, builderStore } from "stores/builder"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
$: id = $viewsV2.selectedViewId
$: builderStore.selectResource(id)
const stopSyncing = syncURLToState({
urlParam: "viewId",
stateKey: "selectedViewId",
validate: id => $viewsV2.list?.some(view => view.id === id),
update: viewsV2.select,
fallbackUrl: "../",
store: viewsV2,
routify,
decode: decodeURIComponent,
})
onDestroy(stopSyncing)
</script>
<slot />

View File

@ -0,0 +1,5 @@
<script>
import ViewV2DataTable from "components/backend/DataTable/ViewV2DataTable.svelte"
</script>
<ViewV2DataTable />

View File

@ -0,0 +1,200 @@
<script>
import {
tables,
datasources,
userSelectedResourceMap,
contextMenuStore,
} from "stores/builder"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import { Icon } from "@budibase/bbui"
import { params, url } from "@roxi/routify"
import EditViewModal from "./EditViewModal.svelte"
import DeleteViewModal from "./DeleteViewModal.svelte"
import EditTableModal from "components/backend/TableNavigator/TableNavItem/EditModal.svelte"
import DeleteTableModal from "components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte"
import { UserAvatars } from "@budibase/frontend-core"
import { tick } from "svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend"
import { TableNames } from "constants"
// Editing table
let editTableModal
let deleteTableModal
// Editing views
let editableView
let editViewModal
let deleteViewModal
$: tableId = $params.tableId
$: table = $tables.list.find(x => x._id === tableId)
$: datasource = $datasources.list.find(x => x._id === table?.sourceId)
$: tableSelectedBy = $userSelectedResourceMap[table?._id]
$: tableEditable = table?._id !== TableNames.USERS
$: activeId = $params.viewId ?? $params.tableId
$: views = Object.values(table?.views || {})
.filter(x => x.version === 2)
.slice()
.sort(alphabetical)
const openTableContextMenu = e => {
if (!tableEditable) {
return
}
e.preventDefault()
e.stopPropagation()
contextMenuStore.open(
table._id,
[
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: table?.sourceType !== DB_TYPE_EXTERNAL,
disabled: false,
callback: editTableModal?.show,
},
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteTableModal?.show,
},
],
{
x: e.clientX,
y: e.clientY,
}
)
}
const openViewContextMenu = async (e, view) => {
e.preventDefault()
e.stopPropagation()
editableView = view
await tick()
contextMenuStore.open(
view.id,
[
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: editViewModal?.show,
},
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteViewModal?.show,
},
],
{
x: e.clientX,
y: e.clientY,
}
)
}
const alphabetical = (a, b) => {
return a.name < b.name ? -1 : 1
}
</script>
<div class="view-nav-bar">
<IntegrationIcon
integrationType={datasource.source}
schema={datasource.schema}
size="24"
/>
<a
href={$url(`../${tableId}`)}
class="nav-bar-item"
class:active={tableId === activeId}
on:contextmenu={openTableContextMenu}
>
<div class="title">
{table.name}
</div>
{#if tableSelectedBy}
<UserAvatars size="XS" users={tableSelectedBy} />
{/if}
{#if tableEditable}
<Icon on:click={openTableContextMenu} s hoverable name="MoreSmallList" />
{/if}
</a>
{#each views as view (view.id)}
{@const selectedBy = $userSelectedResourceMap[view.id]}
<a
href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)}
class="nav-bar-item"
class:active={view.id === activeId}
on:contextmenu={e => openViewContextMenu(e, view)}
>
<div class="title">
{view.name}
</div>
{#if selectedBy}
<UserAvatars size="XS" users={selectedBy} />
{/if}
<Icon
on:click={e => openViewContextMenu(e, view)}
s
hoverable
name="MoreSmallList"
/>
</a>
{/each}
</div>
{#if table}
<EditTableModal {table} bind:this={editTableModal} />
<DeleteTableModal {table} bind:this={deleteTableModal} />
{/if}
{#if editableView}
<EditViewModal view={editableView} bind:this={editViewModal} />
<DeleteViewModal view={editableView} bind:this={deleteViewModal} />
{/if}
<style>
.view-nav-bar {
height: 50px;
border-bottom: var(--border-light);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 0 var(--spacing-xl);
gap: var(--spacing-m);
}
.nav-bar-item {
padding: 6px 8px;
border-radius: 4px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
transition: background 130ms ease-out, color 130ms ease-out;
color: var(--spectrum-global-color-gray-700);
}
.title {
font-size: 16px;
}
.nav-bar-item.active,
.nav-bar-item:hover {
color: var(--spectrum-global-color-gray-900);
background: var(--spectrum-global-color-gray-300);
cursor: pointer;
}
.nav-bar-item:not(.active) :global(.icon) {
display: none;
}
</style>

View File

@ -3,6 +3,7 @@
import { tables, builderStore } from "stores/builder" import { tables, builderStore } from "stores/builder"
import * as routify from "@roxi/routify" import * as routify from "@roxi/routify"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import ViewNavBar from "./_components/ViewNavBar.svelte"
$: tableId = $tables.selectedTableId $: tableId = $tables.selectedTableId
$: builderStore.selectResource(tableId) $: builderStore.selectResource(tableId)
@ -20,4 +21,17 @@
onDestroy(stopSyncing) onDestroy(stopSyncing)
</script> </script>
<slot /> <div class="wrapper">
<ViewNavBar />
<slot />
</div>
<style>
.wrapper {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
background: var(--background);
}
</style>

View File

@ -0,0 +1,19 @@
<script>
import { onMount } from "svelte"
import { views, viewsV2 } from "stores/builder"
import { redirect } from "@roxi/routify"
onMount(async () => {
if ($viewsV2.selected) {
$redirect(`./v2/${$viewsV2.selected.id}`)
} else if ($viewsV2.list?.length) {
$redirect(`./v2/${$viewsV2.list[0].id}`)
} else if ($views.selected) {
$redirect(`./${encodeURIComponent($views.selected?.name)}`)
} else if ($views.list?.length) {
$redirect(`./${encodeURIComponent($views.list[0].name)}`)
} else {
$redirect("../")
}
})
</script>

View File

@ -0,0 +1,24 @@
<script>
import { views, builderStore } from "stores/builder"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
$: name = $views.selectedViewName
$: builderStore.selectResource(name)
const stopSyncing = syncURLToState({
urlParam: "viewName",
stateKey: "selectedViewName",
validate: name => $views.list?.some(view => view.name === name),
update: views.select,
fallbackUrl: "../../",
store: views,
routify,
decode: decodeURIComponent,
})
onDestroy(stopSyncing)
</script>
<slot />

View File

@ -0,0 +1,18 @@
<script>
import ViewDataTable from "components/backend/DataTable/ViewDataTable.svelte"
import { views } from "stores/builder"
$: selectedView = $views.selected
</script>
{#if selectedView}
<ViewDataTable view={selectedView} />
{:else}<i>Create your first table to start building</i>{/if}
<style>
i {
font-size: var(--font-size-m);
color: var(--grey-4);
margin-top: 2px;
}
</style>

View File

@ -0,0 +1,5 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../")
</script>