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

View File

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

View File

@ -104,7 +104,7 @@
</InlineAlert>
</div>
{/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} />
</div>
</ConfirmDialog>

View File

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

View File

@ -1,7 +1,5 @@
<script>
import { goto } from "@roxi/routify"
import TableNavItem from "./TableNavItem/TableNavItem.svelte"
import ViewNavItem from "./ViewNavItem/ViewNavItem.svelte"
export let tables
export let selectTable
@ -16,18 +14,5 @@
<div class="hierarchy-items-container">
{#each sortedTables as table, idx}
<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}
</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 * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
import ViewNavBar from "./_components/ViewNavBar.svelte"
$: tableId = $tables.selectedTableId
$: builderStore.selectResource(tableId)
@ -20,4 +21,17 @@
onDestroy(stopSyncing)
</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>