forked from tsai/budibase
167 changed files with 5549 additions and 1334 deletions
@ -0,0 +1,51 @@ |
|||
const fs = require("fs") |
|||
const { execSync } = require("child_process") |
|||
const path = require("path") |
|||
|
|||
const IMAGES = { |
|||
worker: "budibase/worker", |
|||
apps: "budibase/apps", |
|||
proxy: "envoyproxy/envoy:v1.16-latest", |
|||
minio: "minio/minio", |
|||
couch: "ibmcom/couchdb3", |
|||
curl: "curlimages/curl", |
|||
redis: "redis", |
|||
watchtower: "containrrr/watchtower" |
|||
} |
|||
|
|||
const FILES = { |
|||
COMPOSE: "docker-compose.yaml", |
|||
ENVOY: "envoy.yaml", |
|||
PROPERTIES: "hosting.properties" |
|||
} |
|||
|
|||
const OUTPUT_DIR = path.join(__dirname, "../", "bb-airgapped") |
|||
|
|||
function copyFile(file) { |
|||
fs.copyFileSync( |
|||
path.join(__dirname, "../", "../", file), |
|||
path.join(OUTPUT_DIR, file) |
|||
) |
|||
} |
|||
|
|||
// create output dir
|
|||
console.log(`Creating ${OUTPUT_DIR} for build..`) |
|||
fs.rmdirSync(OUTPUT_DIR, { recursive: true }) |
|||
fs.mkdirSync(OUTPUT_DIR) |
|||
|
|||
// package images into tar files
|
|||
for (let image in IMAGES) { |
|||
console.log(`Creating tar for ${image}..`) |
|||
execSync(`docker save ${IMAGES[image]} -o ${OUTPUT_DIR}/${image}.tar`) |
|||
} |
|||
|
|||
// copy config files
|
|||
copyFile(FILES.COMPOSE) |
|||
copyFile(FILES.ENVOY) |
|||
copyFile(FILES.PROPERTIES) |
|||
|
|||
// compress
|
|||
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`) |
|||
|
|||
// clean up
|
|||
fs.rmdirSync(OUTPUT_DIR, { recursive: true }) |
|||
@ -0,0 +1,54 @@ |
|||
<script> |
|||
import { ActionButton, Modal, notifications } from "@budibase/bbui" |
|||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte" |
|||
import { datasources, tables } from "../../../../stores/backend" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let table |
|||
const dispatch = createEventDispatcher() |
|||
|
|||
$: plusTables = datasource?.plus |
|||
? Object.values(datasource?.entities || {}) |
|||
: [] |
|||
$: datasource = $datasources.list.find( |
|||
source => source._id === table?.sourceId |
|||
) |
|||
|
|||
let modal |
|||
|
|||
async function saveRelationship() { |
|||
try { |
|||
// Create datasource |
|||
await datasources.save(datasource) |
|||
notifications.success(`Relationship information saved.`) |
|||
const tableList = await tables.fetch() |
|||
await tables.select(tableList.find(tbl => tbl._id === table._id)) |
|||
dispatch("updatecolumns") |
|||
} catch (err) { |
|||
notifications.error(`Error saving relationship info: ${err}`) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
{#if table.sourceId} |
|||
<div> |
|||
<ActionButton |
|||
icon="DataCorrelated" |
|||
primary |
|||
size="S" |
|||
quiet |
|||
on:click={modal.show} |
|||
> |
|||
Define existing relationship |
|||
</ActionButton> |
|||
</div> |
|||
<Modal bind:this={modal}> |
|||
<CreateEditRelationship |
|||
{datasource} |
|||
save={saveRelationship} |
|||
close={modal.hide} |
|||
{plusTables} |
|||
selectedFromTable={table} |
|||
/> |
|||
</Modal> |
|||
{/if} |
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { ActionButton, Modal } from "@budibase/bbui" |
|||
import ImportModal from "../modals/ImportModal.svelte" |
|||
|
|||
export let tableId |
|||
|
|||
let modal |
|||
</script> |
|||
|
|||
<ActionButton icon="DataUpload" size="S" quiet on:click={modal.show}> |
|||
Import |
|||
</ActionButton> |
|||
<Modal bind:this={modal}> |
|||
<ImportModal {tableId} on:updaterows /> |
|||
</Modal> |
|||
@ -0,0 +1,43 @@ |
|||
<script> |
|||
import { ModalContent, Label, notifications, Body } from "@budibase/bbui" |
|||
import TableDataImport from "../../TableNavigator/TableDataImport.svelte" |
|||
import api from "builderStore/api" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let tableId |
|||
let dataImport |
|||
|
|||
$: valid = dataImport?.csvString != null && dataImport?.valid |
|||
|
|||
async function importData() { |
|||
const response = await api.post(`/api/tables/${tableId}/import`, { |
|||
dataImport, |
|||
}) |
|||
if (response.status !== 200) { |
|||
const error = await response.text() |
|||
notifications.error(`Unable to import data - ${error}`) |
|||
} else { |
|||
notifications.success("Rows successfully imported.") |
|||
} |
|||
dispatch("updaterows") |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Import Data" |
|||
confirmText="Import" |
|||
onConfirm={importData} |
|||
disabled={!valid} |
|||
> |
|||
<Body |
|||
>Import rows to an existing table from a CSV. Only columns from the CSV |
|||
which exist in the table will be imported.</Body |
|||
> |
|||
<Label grey extraSmall>CSV to import</Label> |
|||
<TableDataImport bind:dataImport bind:existingTableId={tableId} /> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
</style> |
|||
@ -1,15 +0,0 @@ |
|||
<script> |
|||
import { Input } from "@budibase/bbui" |
|||
import { isJSBinding } from "@budibase/string-templates" |
|||
|
|||
export let value |
|||
|
|||
$: isJS = isJSBinding(value) |
|||
</script> |
|||
|
|||
<Input |
|||
{...$$props} |
|||
value={isJS ? "(JavaScript function)" : value} |
|||
readonly={isJS} |
|||
on:change |
|||
/> |
|||
@ -0,0 +1,45 @@ |
|||
<script> |
|||
import { ModalContent, Body, Input } from "@budibase/bbui" |
|||
import { tables, datasources } from "stores/backend" |
|||
import { goto } from "@roxi/routify" |
|||
|
|||
export let datasource |
|||
|
|||
let name = "" |
|||
$: valid = name && name.length > 0 && !datasource?.entities[name] |
|||
$: error = |
|||
name && datasource?.entities[name] ? "Table name already in use." : null |
|||
|
|||
function buildDefaultTable(tableName, datasourceId) { |
|||
return { |
|||
name: tableName, |
|||
type: "external", |
|||
primary: ["id"], |
|||
sourceId: datasourceId, |
|||
schema: { |
|||
id: { |
|||
autocolumn: true, |
|||
type: "number", |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
async function saveTable() { |
|||
const table = await tables.save(buildDefaultTable(name, datasource._id)) |
|||
await datasources.fetch() |
|||
$goto(`../../table/${table._id}`) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Create new table" |
|||
confirmText="Create" |
|||
onConfirm={saveTable} |
|||
disabled={!valid} |
|||
> |
|||
<Body |
|||
>Provide a name for your new table; you can add columns once it is created.</Body |
|||
> |
|||
<Input label="Table Name" bind:error bind:value={name} /> |
|||
</ModalContent> |
|||
@ -1,43 +0,0 @@ |
|||
<script> |
|||
import { ModalContent, Select, Body } from "@budibase/bbui" |
|||
import { tables } from "stores/backend" |
|||
|
|||
export let datasource |
|||
export let plusTables |
|||
export let save |
|||
|
|||
async function saveDisplayColumns() { |
|||
// be explicit about copying over |
|||
for (let table of plusTables) { |
|||
datasource.entities[table.name].primaryDisplay = table.primaryDisplay |
|||
} |
|||
save() |
|||
await tables.fetch() |
|||
} |
|||
|
|||
function getColumnOptions(table) { |
|||
if (!table || !table.schema) { |
|||
return [] |
|||
} |
|||
return Object.entries(table.schema) |
|||
.filter(field => field[1].type !== "link") |
|||
.map(([fieldName]) => fieldName) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Edit display columns" |
|||
confirmText="Save" |
|||
onConfirm={saveDisplayColumns} |
|||
> |
|||
<Body |
|||
>Select the columns that will be shown when displaying relationships.</Body |
|||
> |
|||
{#each plusTables as table} |
|||
<Select |
|||
label={table.name} |
|||
options={getColumnOptions(table)} |
|||
bind:value={table.primaryDisplay} |
|||
/> |
|||
{/each} |
|||
</ModalContent> |
|||
@ -0,0 +1,12 @@ |
|||
<script> |
|||
import { getContext, setContext } from "svelte" |
|||
|
|||
const component = getContext("component") |
|||
|
|||
// We need to set a block context to know we're inside a block, but also |
|||
// to be able to reference the actual component ID of the block from |
|||
// any depth |
|||
setContext("block", { id: $component.id }) |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,35 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { generate } from "shortid" |
|||
import Component from "components/Component.svelte" |
|||
|
|||
export let type |
|||
export let props |
|||
export let styles |
|||
export let context |
|||
|
|||
// ID is only exposed as a prop so that it can be bound to from parent |
|||
// block components |
|||
export let id |
|||
|
|||
const block = getContext("block") |
|||
const rand = generate() |
|||
|
|||
// Create a fake component instance so that we can use the core Component |
|||
// to render this part of the block, taking advantage of binding enrichment |
|||
$: id = `${block.id}-${context ?? rand}` |
|||
$: instance = { |
|||
_component: `@budibase/standard-components/${type}`, |
|||
_id: id, |
|||
_styles: { |
|||
normal: { |
|||
...styles, |
|||
}, |
|||
}, |
|||
...props, |
|||
} |
|||
</script> |
|||
|
|||
<Component {instance} isBlock> |
|||
<slot /> |
|||
</Component> |
|||
@ -0,0 +1,273 @@ |
|||
<script> |
|||
import { onMount, getContext } from "svelte" |
|||
import Block from "components/Block.svelte" |
|||
import BlockComponent from "components/BlockComponent.svelte" |
|||
import { Heading } from "@budibase/bbui" |
|||
import { makePropSafe as safe } from "@budibase/string-templates" |
|||
|
|||
export let title |
|||
export let dataSource |
|||
export let searchColumns |
|||
export let filter |
|||
export let sortColumn |
|||
export let sortOrder |
|||
export let paginate |
|||
export let limit |
|||
export let showTitleButton |
|||
export let titleButtonText |
|||
export let titleButtonURL |
|||
export let titleButtonPeek |
|||
export let cardTitle |
|||
export let cardSubtitle |
|||
export let cardDescription |
|||
export let cardImageURL |
|||
export let linkCardTitle |
|||
export let cardURL |
|||
export let cardPeek |
|||
export let cardHorizontal |
|||
export let showCardButton |
|||
export let cardButtonText |
|||
export let cardButtonOnClick |
|||
export let linkColumn |
|||
|
|||
const { API, styleable } = getContext("sdk") |
|||
const context = getContext("context") |
|||
const component = getContext("component") |
|||
const schemaComponentMap = { |
|||
string: "stringfield", |
|||
options: "optionsfield", |
|||
number: "numberfield", |
|||
datetime: "datetimefield", |
|||
boolean: "booleanfield", |
|||
} |
|||
|
|||
let formId |
|||
let dataProviderId |
|||
let repeaterId |
|||
let schema |
|||
|
|||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema) |
|||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId) |
|||
$: cardWidth = cardHorizontal ? 420 : 300 |
|||
$: fullCardURL = buildFullCardUrl( |
|||
linkCardTitle, |
|||
cardURL, |
|||
repeaterId, |
|||
linkColumn |
|||
) |
|||
$: titleButtonAction = [ |
|||
{ |
|||
"##eventHandlerType": "Navigate To", |
|||
parameters: { |
|||
peek: titleButtonPeek, |
|||
url: titleButtonURL, |
|||
}, |
|||
}, |
|||
] |
|||
|
|||
// Enrich the default filter with the specified search fields |
|||
const enrichFilter = (filter, columns, formId) => { |
|||
let enrichedFilter = [...(filter || [])] |
|||
columns?.forEach(column => { |
|||
enrichedFilter.push({ |
|||
field: column.name, |
|||
operator: column.type === "string" ? "string" : "equal", |
|||
type: "string", |
|||
valueType: "Binding", |
|||
value: `{{ [${formId}].[${column.name}] }}`, |
|||
}) |
|||
}) |
|||
return enrichedFilter |
|||
} |
|||
|
|||
// Determine data types for search fields and only use those that are valid |
|||
const enrichSearchColumns = (searchColumns, schema) => { |
|||
let enrichedColumns = [] |
|||
searchColumns?.forEach(column => { |
|||
const schemaType = schema?.[column]?.type |
|||
const componentType = schemaComponentMap[schemaType] |
|||
if (componentType) { |
|||
enrichedColumns.push({ |
|||
name: column, |
|||
componentType, |
|||
type: schemaType, |
|||
}) |
|||
} |
|||
}) |
|||
return enrichedColumns.slice(0, 3) |
|||
} |
|||
|
|||
// Builds a full details page URL for the card title |
|||
const buildFullCardUrl = (link, url, repeaterId, linkColumn) => { |
|||
if (!link || !url || !repeaterId) { |
|||
return null |
|||
} |
|||
const col = linkColumn || "_id" |
|||
const split = url.split("/:") |
|||
return `${split[0]}/{{ ${safe(repeaterId)}.${safe(col)} }}` |
|||
} |
|||
|
|||
// Load the datasource schema on mount so we can determine column types |
|||
onMount(async () => { |
|||
if (dataSource) { |
|||
schema = await API.fetchDatasourceSchema(dataSource) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<Block> |
|||
<div class="card-list" use:styleable={$component.styles}> |
|||
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}> |
|||
{#if title || enrichedSearchColumns?.length || showTitleButton} |
|||
<div class="header" class:mobile={$context.device.mobile}> |
|||
<div class="title"> |
|||
<Heading>{title || ""}</Heading> |
|||
</div> |
|||
<div class="controls"> |
|||
{#if enrichedSearchColumns?.length} |
|||
<div |
|||
class="search" |
|||
style="--cols:{enrichedSearchColumns?.length}" |
|||
> |
|||
{#each enrichedSearchColumns as column} |
|||
<BlockComponent |
|||
type={column.componentType} |
|||
props={{ |
|||
field: column.name, |
|||
placeholder: column.name, |
|||
text: column.name, |
|||
autoWidth: true, |
|||
}} |
|||
/> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
{#if showTitleButton} |
|||
<BlockComponent |
|||
type="button" |
|||
props={{ |
|||
onClick: titleButtonAction, |
|||
text: titleButtonText, |
|||
type: "cta", |
|||
}} |
|||
/> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
<BlockComponent |
|||
type="dataprovider" |
|||
bind:id={dataProviderId} |
|||
props={{ |
|||
dataSource, |
|||
filter: enrichedFilter, |
|||
sortColumn, |
|||
sortOrder, |
|||
paginate, |
|||
limit, |
|||
}} |
|||
> |
|||
<BlockComponent |
|||
type="repeater" |
|||
bind:id={repeaterId} |
|||
context="repeater" |
|||
props={{ |
|||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`, |
|||
direction: "row", |
|||
hAlign: "stretch", |
|||
vAlign: "top", |
|||
gap: "M", |
|||
noRowsMessage: "No rows found", |
|||
}} |
|||
styles={{ |
|||
display: "grid", |
|||
"grid-template-columns": `repeat(auto-fill, minmax(${cardWidth}px, 1fr))`, |
|||
}} |
|||
> |
|||
<BlockComponent |
|||
type="spectrumcard" |
|||
props={{ |
|||
title: cardTitle, |
|||
subtitle: cardSubtitle, |
|||
description: cardDescription, |
|||
imageURL: cardImageURL, |
|||
horizontal: cardHorizontal, |
|||
showButton: showCardButton, |
|||
buttonText: cardButtonText, |
|||
buttonOnClick: cardButtonOnClick, |
|||
linkURL: fullCardURL, |
|||
linkPeek: cardPeek, |
|||
}} |
|||
styles={{ |
|||
width: "auto", |
|||
}} |
|||
/> |
|||
</BlockComponent> |
|||
</BlockComponent> |
|||
</BlockComponent> |
|||
</div> |
|||
</Block> |
|||
|
|||
<style> |
|||
.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
gap: 20px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.title { |
|||
overflow: hidden; |
|||
} |
|||
.title :global(.spectrum-Heading) { |
|||
flex: 1 1 auto; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.controls { |
|||
flex: 0 1 auto; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
gap: 20px; |
|||
} |
|||
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) { |
|||
width: 100%; |
|||
} |
|||
|
|||
.search { |
|||
flex: 0 1 auto; |
|||
gap: 10px; |
|||
max-width: 100%; |
|||
display: grid; |
|||
grid-template-columns: repeat(var(--cols), minmax(120px, 200px)); |
|||
} |
|||
.search :global(.spectrum-InputGroup) { |
|||
min-width: 0; |
|||
} |
|||
|
|||
/* Mobile styles */ |
|||
.mobile { |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
.mobile .controls { |
|||
flex-direction: column-reverse; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
.mobile .search { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
position: relative; |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,61 @@ |
|||
<script> |
|||
import BlockComponent from "components/BlockComponent.svelte" |
|||
import Block from "components/Block.svelte" |
|||
import Placeholder from "components/app/Placeholder.svelte" |
|||
import { getContext } from "svelte" |
|||
import { makePropSafe as safe } from "@budibase/string-templates" |
|||
|
|||
export let dataSource |
|||
export let filter |
|||
export let sortColumn |
|||
export let sortOrder |
|||
export let limit |
|||
export let paginate |
|||
export let noRowsMessage |
|||
export let direction |
|||
export let hAlign |
|||
export let vAlign |
|||
export let gap |
|||
|
|||
let providerId |
|||
|
|||
const component = getContext("component") |
|||
const { styleable } = getContext("sdk") |
|||
</script> |
|||
|
|||
<Block> |
|||
<div use:styleable={$component.styles}> |
|||
<BlockComponent |
|||
type="dataprovider" |
|||
context="provider" |
|||
bind:id={providerId} |
|||
props={{ |
|||
dataSource, |
|||
filter, |
|||
sortColumn, |
|||
sortOrder, |
|||
limit, |
|||
paginate, |
|||
}} |
|||
> |
|||
{#if $component.empty} |
|||
<Placeholder text={$component.name} /> |
|||
{:else} |
|||
<BlockComponent |
|||
type="repeater" |
|||
context="repeater" |
|||
props={{ |
|||
dataProvider: `{{ literal ${safe(providerId)} }}`, |
|||
noRowsMessage, |
|||
direction, |
|||
hAlign, |
|||
vAlign, |
|||
gap, |
|||
}} |
|||
> |
|||
<slot /> |
|||
</BlockComponent> |
|||
{/if} |
|||
</BlockComponent> |
|||
</div> |
|||
</Block> |
|||
@ -0,0 +1,230 @@ |
|||
<script> |
|||
import { onMount, getContext } from "svelte" |
|||
import Block from "components/Block.svelte" |
|||
import BlockComponent from "components/BlockComponent.svelte" |
|||
import { Heading } from "@budibase/bbui" |
|||
import { makePropSafe as safe } from "@budibase/string-templates" |
|||
|
|||
export let title |
|||
export let dataSource |
|||
export let searchColumns |
|||
export let filter |
|||
export let sortColumn |
|||
export let sortOrder |
|||
export let paginate |
|||
export let tableColumns |
|||
export let showAutoColumns |
|||
export let rowCount |
|||
export let quiet |
|||
export let size |
|||
export let linkRows |
|||
export let linkURL |
|||
export let linkColumn |
|||
export let linkPeek |
|||
export let showTitleButton |
|||
export let titleButtonText |
|||
export let titleButtonURL |
|||
export let titleButtonPeek |
|||
|
|||
const { API, styleable } = getContext("sdk") |
|||
const context = getContext("context") |
|||
const component = getContext("component") |
|||
const schemaComponentMap = { |
|||
string: "stringfield", |
|||
options: "optionsfield", |
|||
number: "numberfield", |
|||
datetime: "datetimefield", |
|||
boolean: "booleanfield", |
|||
} |
|||
|
|||
let formId |
|||
let dataProviderId |
|||
let schema |
|||
|
|||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema) |
|||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId) |
|||
$: titleButtonAction = [ |
|||
{ |
|||
"##eventHandlerType": "Navigate To", |
|||
parameters: { |
|||
peek: titleButtonPeek, |
|||
url: titleButtonURL, |
|||
}, |
|||
}, |
|||
] |
|||
|
|||
// Enrich the default filter with the specified search fields |
|||
const enrichFilter = (filter, columns, formId) => { |
|||
let enrichedFilter = [...(filter || [])] |
|||
columns?.forEach(column => { |
|||
enrichedFilter.push({ |
|||
field: column.name, |
|||
operator: column.type === "string" ? "string" : "equal", |
|||
type: "string", |
|||
valueType: "Binding", |
|||
value: `{{ ${safe(formId)}.${safe(column.name)} }}`, |
|||
}) |
|||
}) |
|||
return enrichedFilter |
|||
} |
|||
|
|||
// Determine data types for search fields and only use those that are valid |
|||
const enrichSearchColumns = (searchColumns, schema) => { |
|||
let enrichedColumns = [] |
|||
searchColumns?.forEach(column => { |
|||
const schemaType = schema?.[column]?.type |
|||
const componentType = schemaComponentMap[schemaType] |
|||
if (componentType) { |
|||
enrichedColumns.push({ |
|||
name: column, |
|||
componentType, |
|||
type: schemaType, |
|||
}) |
|||
} |
|||
}) |
|||
return enrichedColumns.slice(0, 3) |
|||
} |
|||
|
|||
// Load the datasource schema on mount so we can determine column types |
|||
onMount(async () => { |
|||
if (dataSource) { |
|||
schema = await API.fetchDatasourceSchema(dataSource) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<Block> |
|||
<div class={size} use:styleable={$component.styles}> |
|||
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}> |
|||
{#if title || enrichedSearchColumns?.length || showTitleButton} |
|||
<div class="header" class:mobile={$context.device.mobile}> |
|||
<div class="title"> |
|||
<Heading>{title || ""}</Heading> |
|||
</div> |
|||
<div class="controls"> |
|||
{#if enrichedSearchColumns?.length} |
|||
<div |
|||
class="search" |
|||
style="--cols:{enrichedSearchColumns?.length}" |
|||
> |
|||
{#each enrichedSearchColumns as column} |
|||
<BlockComponent |
|||
type={column.componentType} |
|||
props={{ |
|||
field: column.name, |
|||
placeholder: column.name, |
|||
text: column.name, |
|||
autoWidth: true, |
|||
}} |
|||
/> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
{#if showTitleButton} |
|||
<BlockComponent |
|||
type="button" |
|||
props={{ |
|||
onClick: titleButtonAction, |
|||
text: titleButtonText, |
|||
type: "cta", |
|||
}} |
|||
/> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
<BlockComponent |
|||
type="dataprovider" |
|||
bind:id={dataProviderId} |
|||
props={{ |
|||
dataSource, |
|||
filter: enrichedFilter, |
|||
sortColumn, |
|||
sortOrder, |
|||
paginate, |
|||
limit: rowCount, |
|||
}} |
|||
> |
|||
<BlockComponent |
|||
type="table" |
|||
props={{ |
|||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`, |
|||
columns: tableColumns, |
|||
showAutoColumns, |
|||
rowCount, |
|||
quiet, |
|||
size, |
|||
linkRows, |
|||
linkURL, |
|||
linkColumn, |
|||
linkPeek, |
|||
}} |
|||
/> |
|||
</BlockComponent> |
|||
</BlockComponent> |
|||
</div> |
|||
</Block> |
|||
|
|||
<style> |
|||
.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
gap: 20px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.title { |
|||
overflow: hidden; |
|||
} |
|||
.title :global(.spectrum-Heading) { |
|||
flex: 1 1 auto; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.controls { |
|||
flex: 0 1 auto; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
gap: 20px; |
|||
} |
|||
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) { |
|||
width: 100%; |
|||
} |
|||
|
|||
.search { |
|||
flex: 0 1 auto; |
|||
gap: 10px; |
|||
max-width: 100%; |
|||
display: grid; |
|||
grid-template-columns: repeat(var(--cols), minmax(120px, 200px)); |
|||
} |
|||
.search :global(.spectrum-InputGroup) { |
|||
min-width: 0; |
|||
} |
|||
|
|||
/* Mobile styles */ |
|||
.mobile { |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
.mobile .controls { |
|||
flex-direction: column-reverse; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
.mobile .search { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
position: relative; |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,3 @@ |
|||
export { default as tableblock } from "./TableBlock.svelte" |
|||
export { default as cardsblock } from "./CardsBlock.svelte" |
|||
export { default as repeaterblock } from "./RepeaterBlock.svelte" |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue