Merge branch 'fix/relationship-picker-filtering' of github.com:Budibase/budibase into fix/relationship-picker-filtering
This commit is contained in:
commit
e8017e8fab
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.13.14",
|
"version": "2.13.15",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1090,17 +1090,18 @@ export const removeBindings = (obj, replacement = "Invalid binding") => {
|
||||||
* When converting from readable to runtime it can sometimes add too many square brackets,
|
* When converting from readable to runtime it can sometimes add too many square brackets,
|
||||||
* this makes sure that doesn't happen.
|
* this makes sure that doesn't happen.
|
||||||
*/
|
*/
|
||||||
const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => {
|
const shouldReplaceBinding = (currentValue, from, convertTo, binding) => {
|
||||||
if (!currentValue?.includes(convertFrom)) {
|
if (!currentValue?.includes(from)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (convertTo === "readableBinding") {
|
if (convertTo === "readableBinding") {
|
||||||
return true
|
// Dont replace if the value already matches the readable binding
|
||||||
|
return currentValue.indexOf(binding.readableBinding) === -1
|
||||||
}
|
}
|
||||||
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
||||||
// this makes sure it is detected
|
// this makes sure it is detected
|
||||||
const noSpaces = currentValue.replace(/\s+/g, "")
|
const noSpaces = currentValue.replace(/\s+/g, "")
|
||||||
const fromNoSpaces = convertFrom.replace(/\s+/g, "")
|
const fromNoSpaces = from.replace(/\s+/g, "")
|
||||||
const invalids = [
|
const invalids = [
|
||||||
`[${fromNoSpaces}]`,
|
`[${fromNoSpaces}]`,
|
||||||
`"${fromNoSpaces}"`,
|
`"${fromNoSpaces}"`,
|
||||||
|
@ -1152,8 +1153,11 @@ const bindingReplacement = (
|
||||||
// in the search, working from longest to shortest so always use best match first
|
// in the search, working from longest to shortest so always use best match first
|
||||||
let searchString = newBoundValue
|
let searchString = newBoundValue
|
||||||
for (let from of convertFromProps) {
|
for (let from of convertFromProps) {
|
||||||
if (isJS || shouldReplaceBinding(newBoundValue, from, convertTo)) {
|
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
||||||
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
if (
|
||||||
|
isJS ||
|
||||||
|
shouldReplaceBinding(newBoundValue, from, convertTo, binding)
|
||||||
|
) {
|
||||||
let idx
|
let idx
|
||||||
do {
|
do {
|
||||||
// see if any instances of this binding exist in the search string
|
// see if any instances of this binding exist in the search string
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { expect, describe, it, vi } from "vitest"
|
||||||
|
import {
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
} from "../dataBinding"
|
||||||
|
|
||||||
|
vi.mock("@budibase/frontend-core")
|
||||||
|
vi.mock("builderStore/componentUtils")
|
||||||
|
vi.mock("builderStore/store")
|
||||||
|
vi.mock("builderStore/store/theme")
|
||||||
|
vi.mock("builderStore/store/temporal")
|
||||||
|
|
||||||
|
describe("runtimeToReadableBinding", () => {
|
||||||
|
const bindableProperties = [
|
||||||
|
{
|
||||||
|
category: "Current User",
|
||||||
|
icon: "User",
|
||||||
|
providerId: "user",
|
||||||
|
readableBinding: "Current User.firstName",
|
||||||
|
runtimeBinding: "[user].[firstName]",
|
||||||
|
type: "context",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Bindings",
|
||||||
|
icon: "Brackets",
|
||||||
|
readableBinding: "Binding.count",
|
||||||
|
runtimeBinding: "count",
|
||||||
|
type: "context",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
it("should convert a runtime binding to a readable one", () => {
|
||||||
|
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ count }}.`
|
||||||
|
expect(
|
||||||
|
runtimeToReadableBinding(
|
||||||
|
bindableProperties,
|
||||||
|
textWithBindings,
|
||||||
|
"readableBinding"
|
||||||
|
)
|
||||||
|
).toEqual(
|
||||||
|
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not convert to readable binding if it is already readable", () => {
|
||||||
|
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ Binding.count }}.`
|
||||||
|
expect(
|
||||||
|
runtimeToReadableBinding(
|
||||||
|
bindableProperties,
|
||||||
|
textWithBindings,
|
||||||
|
"readableBinding"
|
||||||
|
)
|
||||||
|
).toEqual(
|
||||||
|
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("readableToRuntimeBinding", () => {
|
||||||
|
const bindableProperties = [
|
||||||
|
{
|
||||||
|
category: "Current User",
|
||||||
|
icon: "User",
|
||||||
|
providerId: "user",
|
||||||
|
readableBinding: "Current User.firstName",
|
||||||
|
runtimeBinding: "[user].[firstName]",
|
||||||
|
type: "context",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Bindings",
|
||||||
|
icon: "Brackets",
|
||||||
|
readableBinding: "Binding.count",
|
||||||
|
runtimeBinding: "count",
|
||||||
|
type: "context",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
it("should convert a readable binding to a runtime one", () => {
|
||||||
|
const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
|
||||||
|
expect(
|
||||||
|
readableToRuntimeBinding(
|
||||||
|
bindableProperties,
|
||||||
|
textWithBindings,
|
||||||
|
"runtimeBinding"
|
||||||
|
)
|
||||||
|
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, isActive, params } from "@roxi/routify"
|
import { goto, isActive, params } from "@roxi/routify"
|
||||||
|
import { Layout } from "@budibase/bbui"
|
||||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||||
import {
|
import {
|
||||||
database,
|
database,
|
||||||
|
@ -21,8 +22,11 @@
|
||||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import { userSelectedResourceMap } from "builderStore"
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
import { enrichDatasources } from "./datasourceUtils"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let openDataSources = []
|
export let searchTerm
|
||||||
|
let toggledDatasources = {}
|
||||||
|
|
||||||
$: enrichedDataSources = enrichDatasources(
|
$: enrichedDataSources = enrichDatasources(
|
||||||
$datasources,
|
$datasources,
|
||||||
|
@ -32,52 +36,9 @@
|
||||||
$queries,
|
$queries,
|
||||||
$views,
|
$views,
|
||||||
$viewsV2,
|
$viewsV2,
|
||||||
openDataSources
|
toggledDatasources,
|
||||||
|
searchTerm
|
||||||
)
|
)
|
||||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
|
||||||
$: {
|
|
||||||
// Ensure the open datasource is always actually open
|
|
||||||
if (openDataSource) {
|
|
||||||
openNode(openDataSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enrichDatasources = (
|
|
||||||
datasources,
|
|
||||||
params,
|
|
||||||
isActive,
|
|
||||||
tables,
|
|
||||||
queries,
|
|
||||||
views,
|
|
||||||
viewsV2,
|
|
||||||
openDataSources
|
|
||||||
) => {
|
|
||||||
if (!datasources?.list?.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return datasources.list.map(datasource => {
|
|
||||||
const selected =
|
|
||||||
isActive("./datasource") &&
|
|
||||||
datasources.selectedDatasourceId === datasource._id
|
|
||||||
const open = openDataSources.includes(datasource._id)
|
|
||||||
const containsSelected = containsActiveEntity(
|
|
||||||
datasource,
|
|
||||||
params,
|
|
||||||
isActive,
|
|
||||||
tables,
|
|
||||||
queries,
|
|
||||||
views,
|
|
||||||
viewsV2
|
|
||||||
)
|
|
||||||
const onlySource = datasources.list.length === 1
|
|
||||||
return {
|
|
||||||
...datasource,
|
|
||||||
selected,
|
|
||||||
containsSelected,
|
|
||||||
open: selected || open || containsSelected || onlySource,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectDatasource(datasource) {
|
function selectDatasource(datasource) {
|
||||||
openNode(datasource)
|
openNode(datasource)
|
||||||
|
@ -91,102 +52,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeNode(datasource) {
|
|
||||||
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openNode(datasource) {
|
function openNode(datasource) {
|
||||||
if (!openDataSources.includes(datasource._id)) {
|
toggledDatasources[datasource._id] = true
|
||||||
openDataSources = [...openDataSources, datasource._id]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNode(datasource) {
|
function toggleNode(datasource) {
|
||||||
const isOpen = openDataSources.includes(datasource._id)
|
toggledDatasources[datasource._id] = !datasource.open
|
||||||
if (isOpen) {
|
|
||||||
closeNode(datasource)
|
|
||||||
} else {
|
|
||||||
openNode(datasource)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const containsActiveEntity = (
|
const appUsersTableName = "App users"
|
||||||
datasource,
|
$: showAppUsersTable =
|
||||||
params,
|
!searchTerm ||
|
||||||
isActive,
|
appUsersTableName.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
|
||||||
tables,
|
|
||||||
queries,
|
|
||||||
views,
|
|
||||||
viewsV2
|
|
||||||
) => {
|
|
||||||
// Check for being on a datasource page
|
|
||||||
if (params.datasourceId === datasource._id) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for hardcoded datasource edge cases
|
onMount(() => {
|
||||||
if (
|
if ($tables.selected) {
|
||||||
isActive("./datasource/bb_internal") &&
|
toggledDatasources[$tables.selected.sourceId] = true
|
||||||
datasource._id === "bb_internal"
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isActive("./datasource/datasource_internal_bb_default") &&
|
|
||||||
datasource._id === "datasource_internal_bb_default"
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Check for a matching query
|
$: showNoResults =
|
||||||
if (params.queryId) {
|
searchTerm && !showAppUsersTable && !enrichedDataSources.find(ds => ds.show)
|
||||||
const query = queries.list?.find(q => q._id === params.queryId)
|
|
||||||
return datasource._id === query?.datasourceId
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no entities it can't contain anything
|
|
||||||
if (!datasource.entities) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a list of table options
|
|
||||||
let options = datasource.entities
|
|
||||||
if (!Array.isArray(options)) {
|
|
||||||
options = Object.values(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a matching table
|
|
||||||
if (params.tableId) {
|
|
||||||
const selectedTable = tables.selected?._id
|
|
||||||
return options.find(x => x._id === selectedTable) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a matching view
|
|
||||||
const selectedView = views.selected?.name
|
|
||||||
const viewTable = options.find(table => {
|
|
||||||
return table.views?.[selectedView] != null
|
|
||||||
})
|
|
||||||
if (viewTable) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a matching viewV2
|
|
||||||
const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId)
|
|
||||||
return viewV2Table != null
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $database?._id}
|
{#if $database?._id}
|
||||||
<div class="hierarchy-items-container">
|
<div class="hierarchy-items-container">
|
||||||
<NavItem
|
{#if showAppUsersTable}
|
||||||
icon="UserGroup"
|
<NavItem
|
||||||
text="App users"
|
icon="UserGroup"
|
||||||
selected={$isActive("./table/:tableId") &&
|
text={appUsersTableName}
|
||||||
$tables.selected?._id === TableNames.USERS}
|
selected={$isActive("./table/:tableId") &&
|
||||||
on:click={() => selectTable(TableNames.USERS)}
|
$tables.selected?._id === TableNames.USERS}
|
||||||
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
on:click={() => selectTable(TableNames.USERS)}
|
||||||
/>
|
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||||
{#each enrichedDataSources as datasource}
|
/>
|
||||||
|
{/if}
|
||||||
|
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
|
||||||
<NavItem
|
<NavItem
|
||||||
border
|
border
|
||||||
text={datasource.name}
|
text={datasource.name}
|
||||||
|
@ -210,8 +111,8 @@
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
||||||
{#if datasource.open}
|
{#if datasource.open}
|
||||||
<TableNavigator sourceId={datasource._id} {selectTable} />
|
<TableNavigator tables={datasource.tables} {selectTable} />
|
||||||
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
{#each datasource.queries as query}
|
||||||
<NavItem
|
<NavItem
|
||||||
indentLevel={1}
|
indentLevel={1}
|
||||||
icon="SQLQuery"
|
icon="SQLQuery"
|
||||||
|
@ -228,6 +129,13 @@
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if showNoResults}
|
||||||
|
<Layout paddingY="none" paddingX="L">
|
||||||
|
<div class="no-results">
|
||||||
|
There aren't any datasources matching that name
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -240,4 +148,8 @@
|
||||||
place-items: center;
|
place-items: center;
|
||||||
flex: 0 0 24px;
|
flex: 0 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { TableNames } from "constants"
|
||||||
|
|
||||||
|
const showDatasourceOpen = ({
|
||||||
|
selected,
|
||||||
|
containsSelected,
|
||||||
|
dsToggledStatus,
|
||||||
|
searchTerm,
|
||||||
|
onlyOneSource,
|
||||||
|
}) => {
|
||||||
|
// We want to display all the ds expanded while filtering ds
|
||||||
|
if (searchTerm) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the toggle status has been a value
|
||||||
|
if (dsToggledStatus !== undefined) {
|
||||||
|
return dsToggledStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlyOneSource) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected || containsSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
const containsActiveEntity = (
|
||||||
|
datasource,
|
||||||
|
params,
|
||||||
|
isActive,
|
||||||
|
tables,
|
||||||
|
queries,
|
||||||
|
views,
|
||||||
|
viewsV2
|
||||||
|
) => {
|
||||||
|
// Check for being on a datasource page
|
||||||
|
if (params.datasourceId === datasource._id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for hardcoded datasource edge cases
|
||||||
|
if (
|
||||||
|
isActive("./datasource/bb_internal") &&
|
||||||
|
datasource._id === "bb_internal"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isActive("./datasource/datasource_internal_bb_default") &&
|
||||||
|
datasource._id === "datasource_internal_bb_default"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching query
|
||||||
|
if (params.queryId) {
|
||||||
|
const query = queries.list?.find(q => q._id === params.queryId)
|
||||||
|
return datasource._id === query?.datasourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no entities it can't contain anything
|
||||||
|
if (!datasource.entities) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of table options
|
||||||
|
let options = datasource.entities
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
options = Object.values(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching table
|
||||||
|
if (params.tableId) {
|
||||||
|
const selectedTable = tables.selected?._id
|
||||||
|
return options.find(x => x._id === selectedTable) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching view
|
||||||
|
const selectedView = views.selected?.name
|
||||||
|
const viewTable = options.find(table => {
|
||||||
|
return table.views?.[selectedView] != null
|
||||||
|
})
|
||||||
|
if (viewTable) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching viewV2
|
||||||
|
const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId)
|
||||||
|
return viewV2Table != null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enrichDatasources = (
|
||||||
|
datasources,
|
||||||
|
params,
|
||||||
|
isActive,
|
||||||
|
tables,
|
||||||
|
queries,
|
||||||
|
views,
|
||||||
|
viewsV2,
|
||||||
|
toggledDatasources,
|
||||||
|
searchTerm
|
||||||
|
) => {
|
||||||
|
if (!datasources?.list?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlySource = datasources.list.length === 1
|
||||||
|
return datasources.list.map(datasource => {
|
||||||
|
const selected =
|
||||||
|
isActive("./datasource") &&
|
||||||
|
datasources.selectedDatasourceId === datasource._id
|
||||||
|
const containsSelected = containsActiveEntity(
|
||||||
|
datasource,
|
||||||
|
params,
|
||||||
|
isActive,
|
||||||
|
tables,
|
||||||
|
queries,
|
||||||
|
views,
|
||||||
|
viewsV2
|
||||||
|
)
|
||||||
|
|
||||||
|
const dsTables = tables.list.filter(
|
||||||
|
table =>
|
||||||
|
table.sourceId === datasource._id && table._id !== TableNames.USERS
|
||||||
|
)
|
||||||
|
const dsQueries = queries.list.filter(
|
||||||
|
query => query.datasourceId === datasource._id
|
||||||
|
)
|
||||||
|
|
||||||
|
const open = showDatasourceOpen({
|
||||||
|
selected,
|
||||||
|
containsSelected,
|
||||||
|
dsToggledStatus: toggledDatasources[datasource._id],
|
||||||
|
searchTerm,
|
||||||
|
onlyOneSource: onlySource,
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleDsQueries = dsQueries.filter(
|
||||||
|
q =>
|
||||||
|
!searchTerm ||
|
||||||
|
q.name?.toLowerCase()?.indexOf(searchTerm.toLowerCase()) > -1
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleDsTables = dsTables
|
||||||
|
.map(t => ({
|
||||||
|
...t,
|
||||||
|
views: !searchTerm
|
||||||
|
? t.views
|
||||||
|
: Object.keys(t.views || {})
|
||||||
|
.filter(
|
||||||
|
viewName =>
|
||||||
|
viewName.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(acc, viewName) => ({ ...acc, [viewName]: t.views[viewName] }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
table =>
|
||||||
|
!searchTerm ||
|
||||||
|
table.name?.toLowerCase()?.indexOf(searchTerm.toLowerCase()) > -1 ||
|
||||||
|
Object.keys(table.views).length
|
||||||
|
)
|
||||||
|
|
||||||
|
const show = !!(
|
||||||
|
!searchTerm ||
|
||||||
|
visibleDsQueries.length ||
|
||||||
|
visibleDsTables.length
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...datasource,
|
||||||
|
selected,
|
||||||
|
containsSelected,
|
||||||
|
open,
|
||||||
|
queries: visibleDsQueries,
|
||||||
|
tables: visibleDsTables,
|
||||||
|
show,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { enrichDatasources } from "../datasourceUtils"
|
||||||
|
|
||||||
|
describe("datasourceUtils", () => {
|
||||||
|
describe("enrichDatasources", () => {
|
||||||
|
it.each([
|
||||||
|
["undefined", undefined],
|
||||||
|
["undefined list", {}],
|
||||||
|
["empty list", { list: [] }],
|
||||||
|
])("%s datasources will return an empty list", datasources => {
|
||||||
|
const result = enrichDatasources(datasources)
|
||||||
|
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("filtering", () => {
|
||||||
|
const internalTables = {
|
||||||
|
_id: "datasource_internal_bb_default",
|
||||||
|
name: "Sample Data",
|
||||||
|
}
|
||||||
|
|
||||||
|
const pgDatasource = {
|
||||||
|
_id: "pg_ds",
|
||||||
|
name: "PostgreSQL local",
|
||||||
|
}
|
||||||
|
|
||||||
|
const mysqlDatasource = {
|
||||||
|
_id: "mysql_ds",
|
||||||
|
name: "My SQL local",
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = [
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
_id: "ta_bb_employee",
|
||||||
|
name: "Employees",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_expenses",
|
||||||
|
name: "Expenses",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_expenses_2",
|
||||||
|
name: "Expenses 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_inventory",
|
||||||
|
name: "Inventory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_jobs",
|
||||||
|
name: "Jobs",
|
||||||
|
},
|
||||||
|
].map(t => ({
|
||||||
|
...t,
|
||||||
|
sourceId: internalTables._id,
|
||||||
|
})),
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
_id: "pg_ds-external_inventory",
|
||||||
|
name: "External Inventory",
|
||||||
|
views: {
|
||||||
|
"External Inventory first view": {
|
||||||
|
name: "External Inventory first view",
|
||||||
|
id: "pg_ds_view_1",
|
||||||
|
},
|
||||||
|
"External Inventory second view": {
|
||||||
|
name: "External Inventory second view",
|
||||||
|
id: "pg_ds_view_2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "pg_ds-another_table",
|
||||||
|
name: "Another table",
|
||||||
|
views: {
|
||||||
|
view1: {
|
||||||
|
id: "pg_ds-another_table-view1",
|
||||||
|
name: "view1",
|
||||||
|
},
|
||||||
|
["View 2"]: {
|
||||||
|
id: "pg_ds-another_table-view2",
|
||||||
|
name: "View 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "pg_ds_table2",
|
||||||
|
name: "table2",
|
||||||
|
views: {
|
||||||
|
"new 2": {
|
||||||
|
name: "new 2",
|
||||||
|
id: "pg_ds_table2_new_2",
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
name: "new",
|
||||||
|
id: "pg_ds_table2_new_",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].map(t => ({
|
||||||
|
...t,
|
||||||
|
sourceId: pgDatasource._id,
|
||||||
|
})),
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
_id: "mysql_ds-mysql_table",
|
||||||
|
name: "MySQL table",
|
||||||
|
},
|
||||||
|
].map(t => ({
|
||||||
|
...t,
|
||||||
|
sourceId: mysqlDatasource._id,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
const datasources = {
|
||||||
|
list: [internalTables, pgDatasource, mysqlDatasource],
|
||||||
|
}
|
||||||
|
const isActive = vi.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
it("without a search term, all datasources are returned", () => {
|
||||||
|
const searchTerm = ""
|
||||||
|
|
||||||
|
const result = enrichDatasources(
|
||||||
|
datasources,
|
||||||
|
{},
|
||||||
|
isActive,
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{},
|
||||||
|
searchTerm
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
datasources.list.map(d =>
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: d._id,
|
||||||
|
show: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given a valid search term, all tables are correctly filtered", () => {
|
||||||
|
const searchTerm = "ex"
|
||||||
|
|
||||||
|
const result = enrichDatasources(
|
||||||
|
datasources,
|
||||||
|
{},
|
||||||
|
isActive,
|
||||||
|
{ list: tables },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{},
|
||||||
|
searchTerm
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: internalTables._id,
|
||||||
|
show: true,
|
||||||
|
tables: [
|
||||||
|
expect.objectContaining({ _id: "ta_bb_expenses" }),
|
||||||
|
expect.objectContaining({ _id: "ta_bb_expenses_2" }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: pgDatasource._id,
|
||||||
|
show: true,
|
||||||
|
tables: [
|
||||||
|
expect.objectContaining({ _id: "pg_ds-external_inventory" }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: mysqlDatasource._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given a non matching search term, all entities are empty", () => {
|
||||||
|
const searchTerm = "non matching"
|
||||||
|
|
||||||
|
const result = enrichDatasources(
|
||||||
|
datasources,
|
||||||
|
{},
|
||||||
|
isActive,
|
||||||
|
{ list: tables },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{},
|
||||||
|
searchTerm
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: internalTables._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: pgDatasource._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: mysqlDatasource._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables, views, viewsV2, database } from "stores/backend"
|
import {
|
||||||
|
tables as tablesStore,
|
||||||
|
views,
|
||||||
|
viewsV2,
|
||||||
|
database,
|
||||||
|
} from "stores/backend"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
||||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||||
|
@ -7,14 +12,10 @@
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
import { userSelectedResourceMap } from "builderStore"
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
|
||||||
export let sourceId
|
export let tables
|
||||||
export let selectTable
|
export let selectTable
|
||||||
|
|
||||||
$: sortedTables = $tables.list
|
$: sortedTables = tables.sort(alphabetical)
|
||||||
.filter(
|
|
||||||
table => table.sourceId === sourceId && table._id !== TableNames.USERS
|
|
||||||
)
|
|
||||||
.sort(alphabetical)
|
|
||||||
|
|
||||||
const alphabetical = (a, b) => {
|
const alphabetical = (a, b) => {
|
||||||
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
|
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
|
||||||
text={table.name}
|
text={table.name}
|
||||||
selected={$isActive("./table/:tableId") &&
|
selected={$isActive("./table/:tableId") &&
|
||||||
$tables.selected?._id === table._id}
|
$tablesStore.selected?._id === table._id}
|
||||||
on:click={() => selectTable(table._id)}
|
on:click={() => selectTable(table._id)}
|
||||||
selectedBy={$userSelectedResourceMap[table._id]}
|
selectedBy={$userSelectedResourceMap[table._id]}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script>
|
||||||
|
import { tick } from "svelte"
|
||||||
|
import { Icon, Body } from "@budibase/bbui"
|
||||||
|
import { keyUtils } from "helpers/keyUtils"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let placeholder
|
||||||
|
export let value
|
||||||
|
export let onAdd
|
||||||
|
export let search
|
||||||
|
|
||||||
|
let searchInput
|
||||||
|
|
||||||
|
const openSearch = async () => {
|
||||||
|
search = true
|
||||||
|
await tick()
|
||||||
|
searchInput.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSearch = async () => {
|
||||||
|
search = false
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = e => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddButton = () => {
|
||||||
|
if (search) {
|
||||||
|
closeSearch()
|
||||||
|
} else {
|
||||||
|
onAdd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeyDown} />
|
||||||
|
|
||||||
|
<div class="header" class:search>
|
||||||
|
<input
|
||||||
|
readonly={!search}
|
||||||
|
bind:value
|
||||||
|
bind:this={searchInput}
|
||||||
|
class="searchBox"
|
||||||
|
class:hide={!search}
|
||||||
|
{placeholder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="title" class:hide={search}>
|
||||||
|
<Body size="S">{title}</Body>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
on:click={openSearch}
|
||||||
|
on:keydown={keyUtils.handleEnter(openSearch)}
|
||||||
|
class="searchButton"
|
||||||
|
class:hide={search}
|
||||||
|
>
|
||||||
|
<Icon size="S" name="Search" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
on:click={handleAddButton}
|
||||||
|
on:keydown={keyUtils.handleEnter(handleAddButton)}
|
||||||
|
class="addButton"
|
||||||
|
class:rotate={search}
|
||||||
|
>
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search {
|
||||||
|
transition: height 300ms ease-out;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-direction: row;
|
||||||
|
position: relative;
|
||||||
|
height: 50px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: border-bottom 130ms ease-out;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: var(--spectrum-alias-font-size-default);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.searchBox:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.searchBox::placeholder {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.searchButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 300ms ease-out;
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -189,6 +189,7 @@
|
||||||
flex: 0 0 20px;
|
flex: 0 0 20px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
order: 0;
|
order: 0;
|
||||||
|
transition: transform 100ms linear;
|
||||||
}
|
}
|
||||||
.icon.arrow.absolute {
|
.icon.arrow.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let onClickCloseButton
|
export let onClickCloseButton
|
||||||
export let borderLeft = false
|
export let borderLeft = false
|
||||||
export let borderRight = false
|
export let borderRight = false
|
||||||
|
export let borderBottomHeader = true
|
||||||
export let wide = false
|
export let wide = false
|
||||||
export let extraWide = false
|
export let extraWide = false
|
||||||
export let closeButtonIcon = "Close"
|
export let closeButtonIcon = "Close"
|
||||||
|
@ -26,7 +27,11 @@
|
||||||
class:borderLeft
|
class:borderLeft
|
||||||
class:borderRight
|
class:borderRight
|
||||||
>
|
>
|
||||||
<div class="header" class:custom={customHeaderContent}>
|
<div
|
||||||
|
class="header"
|
||||||
|
class:custom={customHeaderContent}
|
||||||
|
class:borderBottom={borderBottomHeader}
|
||||||
|
>
|
||||||
{#if showBackButton}
|
{#if showBackButton}
|
||||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -94,9 +99,11 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 var(--spacing-l);
|
padding: 0 var(--spacing-l);
|
||||||
border-bottom: var(--border-light);
|
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
.header.borderBottom {
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
function handleEnter(fnc) {
|
||||||
|
return e => e.key === "Enter" && fnc()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const keyUtils = {
|
||||||
|
handleEnter,
|
||||||
|
}
|
|
@ -1,9 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Layout } from "@budibase/bbui"
|
import { Layout } from "@budibase/bbui"
|
||||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import { isActive, redirect, goto, params } from "@roxi/routify"
|
import { isActive, redirect, goto, params } from "@roxi/routify"
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
|
|
||||||
|
let searchValue
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// If we ever don't have any data other than the users table, prompt the
|
// If we ever don't have any data other than the users table, prompt the
|
||||||
|
@ -18,10 +21,17 @@
|
||||||
<!-- routify:options index=1 -->
|
<!-- routify:options index=1 -->
|
||||||
<div class="data">
|
<div class="data">
|
||||||
{#if !$isActive("./new")}
|
{#if !$isActive("./new")}
|
||||||
<Panel title="Sources" borderRight>
|
<Panel borderRight borderBottomHeader={false}>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<span class="panel-title-content" slot="panel-title-content">
|
||||||
<Button cta on:click={() => $goto("./new")}>Add source</Button>
|
<NavHeader
|
||||||
<DatasourceNavigator />
|
title="Sources"
|
||||||
|
placeholder="Search for sources"
|
||||||
|
bind:value={searchValue}
|
||||||
|
onAdd={() => $goto("./new")}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<Layout paddingX="L" paddingY="none" gap="S">
|
||||||
|
<DatasourceNavigator searchTerm={searchValue} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</Panel>
|
</Panel>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -51,4 +61,8 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-title-content {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Layout, Body } from "@budibase/bbui"
|
import { Layout } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
store,
|
store,
|
||||||
sortedScreens,
|
sortedScreens,
|
||||||
|
@ -9,13 +9,14 @@
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import RoleIndicator from "./RoleIndicator.svelte"
|
import RoleIndicator from "./RoleIndicator.svelte"
|
||||||
import DropdownMenu from "./DropdownMenu.svelte"
|
import DropdownMenu from "./DropdownMenu.svelte"
|
||||||
import { onMount, tick } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
|
|
||||||
let search = false
|
let search = false
|
||||||
let resizing = false
|
let resizing = false
|
||||||
let searchValue = ""
|
let searchValue = ""
|
||||||
let searchInput
|
|
||||||
let container
|
let container
|
||||||
let screensContainer
|
let screensContainer
|
||||||
let scrolling = false
|
let scrolling = false
|
||||||
|
@ -26,10 +27,9 @@
|
||||||
|
|
||||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
$: search ? openSearch() : closeSearch()
|
||||||
|
|
||||||
const openSearch = async () => {
|
const openSearch = async () => {
|
||||||
search = true
|
|
||||||
await tick()
|
|
||||||
searchInput.focus()
|
|
||||||
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||||
previousHeight = $screensHeight
|
previousHeight = $screensHeight
|
||||||
$screensHeight = "calc(100% + 1px)"
|
$screensHeight = "calc(100% + 1px)"
|
||||||
|
@ -42,8 +42,6 @@
|
||||||
previousHeight = null
|
previousHeight = null
|
||||||
await sleep(300)
|
await sleep(300)
|
||||||
}
|
}
|
||||||
search = false
|
|
||||||
searchValue = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilteredScreens = (screens, search) => {
|
const getFilteredScreens = (screens, search) => {
|
||||||
|
@ -52,20 +50,6 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddButton = () => {
|
|
||||||
if (search) {
|
|
||||||
closeSearch()
|
|
||||||
} else {
|
|
||||||
$goto("../new")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = e => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
closeSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScroll = e => {
|
const handleScroll = e => {
|
||||||
scrolling = e.target.scrollTop !== 0
|
scrolling = e.target.scrollTop !== 0
|
||||||
}
|
}
|
||||||
|
@ -105,7 +89,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={onKeyDown} />
|
<svelte:window />
|
||||||
<div
|
<div
|
||||||
class="screens"
|
class="screens"
|
||||||
class:search
|
class:search
|
||||||
|
@ -114,26 +98,13 @@
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
>
|
>
|
||||||
<div class="header" class:scrolling>
|
<div class="header" class:scrolling>
|
||||||
<input
|
<NavHeader
|
||||||
readonly={!search}
|
title="Screens"
|
||||||
bind:value={searchValue}
|
|
||||||
bind:this={searchInput}
|
|
||||||
class="input"
|
|
||||||
placeholder="Search for screens"
|
placeholder="Search for screens"
|
||||||
|
bind:value={searchValue}
|
||||||
|
bind:search
|
||||||
|
onAdd={() => $goto("../new")}
|
||||||
/>
|
/>
|
||||||
<div class="title" class:hide={search}>
|
|
||||||
<Body size="S">Screens</Body>
|
|
||||||
</div>
|
|
||||||
<div on:click={openSearch} class="searchButton" class:hide={search}>
|
|
||||||
<Icon size="S" name="Search" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
on:click={handleAddButton}
|
|
||||||
class="addButton"
|
|
||||||
class:closeButton={search}
|
|
||||||
>
|
|
||||||
<Icon name="Add" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div on:scroll={handleScroll} bind:this={screensContainer} class="content">
|
<div on:scroll={handleScroll} bind:this={screensContainer} class="content">
|
||||||
{#if filteredScreens?.length}
|
{#if filteredScreens?.length}
|
||||||
|
@ -177,9 +148,9 @@
|
||||||
min-height: 147px;
|
min-height: 147px;
|
||||||
max-height: calc(100% - 147px);
|
max-height: calc(100% - 147px);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: height 300ms ease-out;
|
||||||
}
|
}
|
||||||
.screens.search {
|
.screens.search {
|
||||||
transition: height 300ms ease-out;
|
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
.screens.resizing {
|
.screens.resizing {
|
||||||
|
@ -202,37 +173,6 @@
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
position: absolute;
|
|
||||||
color: var(--ink);
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: var(--spectrum-alias-font-size-default);
|
|
||||||
width: 260px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.input::placeholder {
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
.screens.search input {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex: 1;
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -245,34 +185,6 @@
|
||||||
padding-right: 8px !important;
|
padding-right: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchButton {
|
|
||||||
color: var(--grey-7);
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 10px;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.searchButton:hover {
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton {
|
|
||||||
color: var(--grey-7);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 300ms ease-out;
|
|
||||||
}
|
|
||||||
.addButton:hover {
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.closeButton {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Body } from "@budibase/bbui"
|
|
||||||
import { apps, sideBarCollapsed } from "stores/portal"
|
import { apps, sideBarCollapsed } from "stores/portal"
|
||||||
import { params, goto } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import { tick } from "svelte"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
|
|
||||||
let searchInput
|
|
||||||
let searchString
|
let searchString
|
||||||
let searching = false
|
|
||||||
|
|
||||||
$: filteredApps = $apps
|
$: filteredApps = $apps
|
||||||
.filter(app => {
|
.filter(app => {
|
||||||
|
@ -21,39 +18,16 @@
|
||||||
const lowerB = b.name.toLowerCase()
|
const lowerB = b.name.toLowerCase()
|
||||||
return lowerA > lowerB ? 1 : -1
|
return lowerA > lowerB ? 1 : -1
|
||||||
})
|
})
|
||||||
|
|
||||||
const startSearching = async () => {
|
|
||||||
searching = true
|
|
||||||
searchString = ""
|
|
||||||
await tick()
|
|
||||||
searchInput.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopSearching = () => {
|
|
||||||
searching = false
|
|
||||||
searchString = ""
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="side-bar" class:collapsed={$sideBarCollapsed}>
|
<div class="side-bar" class:collapsed={$sideBarCollapsed}>
|
||||||
<div class="side-bar-controls">
|
<div class="side-bar-controls">
|
||||||
{#if searching}
|
<NavHeader
|
||||||
<input
|
title="Apps"
|
||||||
bind:this={searchInput}
|
placeholder="Search for apps"
|
||||||
bind:value={searchString}
|
bind:value={searchString}
|
||||||
placeholder="Search for apps"
|
onAdd={() => $goto("./create")}
|
||||||
/>
|
/>
|
||||||
{:else}
|
|
||||||
<Body size="S">Apps</Body>
|
|
||||||
<Icon name="Search" size="S" hoverable on:click={startSearching} />
|
|
||||||
{/if}
|
|
||||||
<div class="rotational" class:rotated={searching}>
|
|
||||||
<Icon
|
|
||||||
name="Add"
|
|
||||||
hoverable
|
|
||||||
on:click={searching ? stopSearching : () => $goto("./create")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="side-bar-nav">
|
<div class="side-bar-nav">
|
||||||
<NavItem
|
<NavItem
|
||||||
|
@ -103,44 +77,13 @@
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
padding: 0 var(--spacing-l);
|
padding: 0 var(--spacing-l);
|
||||||
}
|
}
|
||||||
.side-bar-controls :global(.spectrum-Body),
|
|
||||||
.side-bar-controls input {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.side-bar-controls :global(.spectrum-Icon) {
|
.side-bar-controls :global(.spectrum-Icon) {
|
||||||
color: var(--spectrum-global-color-gray-700);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
max-width: none;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
color: var(--spectrum-global-color-gray-800);
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 0;
|
|
||||||
transition: border 130ms ease-out;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
background: inherit;
|
|
||||||
}
|
|
||||||
input::placeholder {
|
|
||||||
color: var(--spectrum-global-color-gray-700);
|
|
||||||
transition: color 130ms ease-out;
|
|
||||||
}
|
|
||||||
input:hover::placeholder {
|
|
||||||
color: var(--spectrum-global-color-gray-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-bar-nav {
|
.side-bar-nav {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.rotational {
|
|
||||||
transition: transform 130ms ease-out;
|
|
||||||
}
|
|
||||||
div.rotational.rotated {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -426,7 +426,7 @@ describe.each([
|
||||||
const saved = (await loadRow(id, table._id!)).body
|
const saved = (await loadRow(id, table._id!)).body
|
||||||
|
|
||||||
expect(saved.stringUndefined).toBe(undefined)
|
expect(saved.stringUndefined).toBe(undefined)
|
||||||
expect(saved.stringNull).toBe("")
|
expect(saved.stringNull).toBe(null)
|
||||||
expect(saved.stringString).toBe("i am a string")
|
expect(saved.stringString).toBe("i am a string")
|
||||||
expect(saved.numberEmptyString).toBe(null)
|
expect(saved.numberEmptyString).toBe(null)
|
||||||
expect(saved.numberNull).toBe(null)
|
expect(saved.numberNull).toBe(null)
|
||||||
|
|
|
@ -46,23 +46,23 @@ export const TYPE_TRANSFORM_MAP: any = {
|
||||||
parse: parseArrayString,
|
parse: parseArrayString,
|
||||||
},
|
},
|
||||||
[FieldTypes.STRING]: {
|
[FieldTypes.STRING]: {
|
||||||
"": "",
|
"": null,
|
||||||
[null]: "",
|
[null]: null,
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
[FieldTypes.BARCODEQR]: {
|
[FieldTypes.BARCODEQR]: {
|
||||||
"": "",
|
"": null,
|
||||||
[null]: "",
|
[null]: null,
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
[FieldTypes.FORMULA]: {
|
[FieldTypes.FORMULA]: {
|
||||||
"": "",
|
"": null,
|
||||||
[null]: "",
|
[null]: null,
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
[FieldTypes.LONGFORM]: {
|
[FieldTypes.LONGFORM]: {
|
||||||
"": "",
|
"": null,
|
||||||
[null]: "",
|
[null]: null,
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
[FieldTypes.NUMBER]: {
|
[FieldTypes.NUMBER]: {
|
||||||
|
@ -71,6 +71,11 @@ export const TYPE_TRANSFORM_MAP: any = {
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
parse: n => parseFloat(n),
|
parse: n => parseFloat(n),
|
||||||
},
|
},
|
||||||
|
[FieldTypes.BIGINT]: {
|
||||||
|
"": null,
|
||||||
|
[null]: null,
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
[FieldTypes.DATETIME]: {
|
[FieldTypes.DATETIME]: {
|
||||||
"": null,
|
"": null,
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
|
|
Loading…
Reference in New Issue