mirror of https://github.com/Budibase/budibase.git
40 changed files with 1354 additions and 190 deletions
@ -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}> |
||||
|
<slot /> |
||||
|
</Component> |
||||
@ -0,0 +1,237 @@ |
|||||
|
<script> |
||||
|
import { onMount, getContext } from "svelte" |
||||
|
import Block from "components/Block.svelte" |
||||
|
import BlockComponent from "components/BlockComponent.svelte" |
||||
|
import { Heading } from "@budibase/bbui" |
||||
|
|
||||
|
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 titleButtonOnClick |
||||
|
export let cardTitle |
||||
|
export let cardSubtitle |
||||
|
export let cardDescription |
||||
|
export let cardImageURL |
||||
|
export let cardHorizontal |
||||
|
export let showCardButton |
||||
|
export let cardButtonText |
||||
|
export let cardButtonOnClick |
||||
|
|
||||
|
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) |
||||
|
$: cardWidth = cardHorizontal ? 420 : 300 |
||||
|
|
||||
|
// 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: "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, |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
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="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: titleButtonOnClick, |
||||
|
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" |
||||
|
context="repeater" |
||||
|
props={{ |
||||
|
dataProvider: `{{ literal [${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, |
||||
|
}} |
||||
|
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,218 @@ |
|||||
|
<script> |
||||
|
import { onMount, getContext } from "svelte" |
||||
|
import Block from "components/Block.svelte" |
||||
|
import BlockComponent from "components/BlockComponent.svelte" |
||||
|
import { Heading } from "@budibase/bbui" |
||||
|
|
||||
|
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 titleButtonOnClick |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
// 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: "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, |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
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: titleButtonOnClick, |
||||
|
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 [${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,2 @@ |
|||||
|
export { default as tablewithsearch } from "./TableWithSearch.svelte" |
||||
|
export { default as cardlistwithsearch } from "./CardListWithSearch.svelte" |
||||
Loading…
Reference in new issue