mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
111 changed files with 2833 additions and 894 deletions
@ -1,13 +0,0 @@ |
|||
import { Screen } from "./utils/Screen" |
|||
|
|||
export default { |
|||
name: `New Row (Empty)`, |
|||
create: () => createScreen(), |
|||
} |
|||
|
|||
const createScreen = () => { |
|||
return new Screen() |
|||
.component("@budibase/standard-components/newrow") |
|||
.table("") |
|||
.json() |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
import { Screen } from "./utils/Screen" |
|||
|
|||
export default { |
|||
name: `Row Detail (Empty)`, |
|||
create: () => createScreen(), |
|||
} |
|||
|
|||
const createScreen = () => { |
|||
return new Screen() |
|||
.component("@budibase/standard-components/rowdetail") |
|||
.table("") |
|||
.json() |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="attachment" /> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="boolean" /> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="datetime" /> |
|||
@ -0,0 +1,37 @@ |
|||
<script> |
|||
import { Select, Label } from "@budibase/bbui" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { getDataProviderComponents } from "builderStore/dataBinding" |
|||
|
|||
export let parameters |
|||
|
|||
$: dataProviders = getDataProviderComponents( |
|||
$currentAsset.props, |
|||
$store.selectedComponentId |
|||
) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Label size="m" color="dark">Form</Label> |
|||
<Select secondary bind:value={parameters.componentId}> |
|||
<option value="" /> |
|||
{#if dataProviders} |
|||
{#each dataProviders as component} |
|||
<option value={component._id}>{component._instanceName}</option> |
|||
{/each} |
|||
{/if} |
|||
</Select> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: baseline; |
|||
} |
|||
|
|||
.root :global(> div) { |
|||
flex: 1; |
|||
margin-left: var(--spacing-l); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,38 @@ |
|||
<script> |
|||
import { Select, Label } from "@budibase/bbui" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { getActionProviderComponents } from "builderStore/dataBinding" |
|||
|
|||
export let parameters |
|||
|
|||
$: actionProviders = getActionProviderComponents( |
|||
$currentAsset.props, |
|||
$store.selectedComponentId, |
|||
"ValidateForm" |
|||
) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Label size="m" color="dark">Form</Label> |
|||
<Select secondary bind:value={parameters.componentId}> |
|||
<option value="" /> |
|||
{#if actionProviders} |
|||
{#each actionProviders as component} |
|||
<option value={component._id}>{component._instanceName}</option> |
|||
{/each} |
|||
{/if} |
|||
</Select> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: baseline; |
|||
} |
|||
|
|||
.root :global(> div) { |
|||
flex: 1; |
|||
margin-left: var(--spacing-l); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,59 @@ |
|||
<script> |
|||
import { DataList } from "@budibase/bbui" |
|||
import { |
|||
getDatasourceForProvider, |
|||
getSchemaForDatasource, |
|||
} from "builderStore/dataBinding" |
|||
import { currentAsset } from "builderStore" |
|||
import { findClosestMatchingComponent } from "builderStore/storeUtils" |
|||
|
|||
export let componentInstance |
|||
export let value |
|||
export let onChange |
|||
export let type |
|||
|
|||
$: form = findClosestMatchingComponent( |
|||
$currentAsset.props, |
|||
componentInstance._id, |
|||
component => component._component === "@budibase/standard-components/form" |
|||
) |
|||
$: datasource = getDatasourceForProvider(form) |
|||
$: schema = getSchemaForDatasource(datasource, true).schema |
|||
$: options = getOptions(schema, type) |
|||
|
|||
const getOptions = (schema, fieldType) => { |
|||
let entries = Object.entries(schema ?? {}) |
|||
if (fieldType) { |
|||
entries = entries.filter(entry => entry[1].type === fieldType) |
|||
} |
|||
return entries.map(entry => entry[0]) |
|||
} |
|||
|
|||
const handleBlur = () => onChange(value) |
|||
</script> |
|||
|
|||
<div> |
|||
<DataList |
|||
editable |
|||
secondary |
|||
extraThin |
|||
on:blur={handleBlur} |
|||
on:change |
|||
bind:value> |
|||
<option value="" /> |
|||
{#each options as option} |
|||
<option value={option}>{option}</option> |
|||
{/each} |
|||
</DataList> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
flex: 1 1 auto; |
|||
display: flex; |
|||
flex-direction: row; |
|||
} |
|||
div :global(> div) { |
|||
flex: 1 1 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="longform" /> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FieldSelect from "./FieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FieldSelect {...$$props} multiselect /> |
|||
@ -1,5 +0,0 @@ |
|||
<script> |
|||
import TableViewFieldSelect from "./TableViewFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<TableViewFieldSelect {...$$props} multiselect /> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="number" /> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="options" /> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="link" /> |
|||
@ -0,0 +1,7 @@ |
|||
<script> |
|||
import DatasourceSelect from "./DatasourceSelect.svelte" |
|||
|
|||
const otherSources = [{ name: "Custom", label: "Custom" }] |
|||
</script> |
|||
|
|||
<DatasourceSelect on:change {...$$props} {otherSources} /> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import FormFieldSelect from "./FormFieldSelect.svelte" |
|||
</script> |
|||
|
|||
<FormFieldSelect {...$$props} type="string" /> |
|||
@ -1,15 +0,0 @@ |
|||
<script> |
|||
import { getContext, setContext } from "svelte" |
|||
import { createDataStore } from "../store" |
|||
|
|||
export let row |
|||
|
|||
// Clone and create new data context for this component tree |
|||
const dataContext = getContext("data") |
|||
const component = getContext("component") |
|||
const newData = createDataStore($dataContext) |
|||
setContext("data", newData) |
|||
$: newData.actions.addContext(row, $component.id) |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,55 @@ |
|||
<script> |
|||
import { getContext, setContext, onMount } from "svelte" |
|||
import { datasourceStore, createContextStore } from "../store" |
|||
import { ActionTypes } from "../constants" |
|||
import { generate } from "shortid" |
|||
|
|||
export let data |
|||
export let actions |
|||
export let key |
|||
|
|||
// Clone and create new data context for this component tree |
|||
const context = getContext("context") |
|||
const component = getContext("component") |
|||
const newContext = createContextStore(context) |
|||
setContext("context", newContext) |
|||
|
|||
$: providerKey = key || $component.id |
|||
|
|||
// Add data context |
|||
$: newContext.actions.provideData(providerKey, data) |
|||
|
|||
// Instance ID is unique to each instance of a provider |
|||
let instanceId |
|||
|
|||
// Add actions context |
|||
$: { |
|||
if (instanceId) { |
|||
actions?.forEach(({ type, callback, metadata }) => { |
|||
newContext.actions.provideAction(providerKey, type, callback) |
|||
|
|||
// Register any "refresh datasource" actions with a singleton store |
|||
// so we can easily refresh data at all levels for any datasource |
|||
if (type === ActionTypes.RefreshDatasource) { |
|||
const { datasource } = metadata || {} |
|||
datasourceStore.actions.registerDatasource( |
|||
datasource, |
|||
instanceId, |
|||
callback |
|||
) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
onMount(() => { |
|||
// Generate a permanent unique ID for this component and use it to register |
|||
// any datasource actions |
|||
instanceId = generate() |
|||
|
|||
// Unregister all datasource instances when unmounting this provider |
|||
return () => datasourceStore.actions.unregisterInstance(instanceId) |
|||
}) |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -1,3 +1,8 @@ |
|||
export const TableNames = { |
|||
USERS: "ta_users", |
|||
} |
|||
|
|||
export const ActionTypes = { |
|||
ValidateForm: "ValidateForm", |
|||
RefreshDatasource: "RefreshDatasource", |
|||
} |
|||
|
|||
@ -1,21 +0,0 @@ |
|||
import { writable } from "svelte/store" |
|||
|
|||
const createBindingStore = () => { |
|||
const store = writable({}) |
|||
|
|||
const setBindableValue = (value, componentId) => { |
|||
store.update(state => { |
|||
if (componentId) { |
|||
state[componentId] = value |
|||
} |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
subscribe: store.subscribe, |
|||
actions: { setBindableValue }, |
|||
} |
|||
} |
|||
|
|||
export const bindingStore = createBindingStore() |
|||
@ -0,0 +1,42 @@ |
|||
import { writable, derived } from "svelte/store" |
|||
|
|||
export const createContextStore = oldContext => { |
|||
const newContext = writable({}) |
|||
const contexts = oldContext ? [oldContext, newContext] : [newContext] |
|||
const totalContext = derived(contexts, $contexts => { |
|||
return $contexts.reduce((total, context) => ({ ...total, ...context }), {}) |
|||
}) |
|||
|
|||
// Adds a data context layer to the tree
|
|||
const provideData = (providerId, data) => { |
|||
if (!providerId || data === undefined) { |
|||
return |
|||
} |
|||
newContext.update(state => { |
|||
state[providerId] = data |
|||
|
|||
// Keep track of the closest component ID so we can later hydrate a "data" prop.
|
|||
// This is only required for legacy bindings that used "data" rather than a
|
|||
// component ID.
|
|||
state.closestComponentId = providerId |
|||
|
|||
return state |
|||
}) |
|||
} |
|||
|
|||
// Adds an action context layer to the tree
|
|||
const provideAction = (providerId, actionType, callback) => { |
|||
if (!providerId || !actionType) { |
|||
return |
|||
} |
|||
newContext.update(state => { |
|||
state[`${providerId}_${actionType}`] = callback |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
subscribe: totalContext.subscribe, |
|||
actions: { provideData, provideAction }, |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
import { writable } from "svelte/store" |
|||
import { cloneDeep } from "lodash/fp" |
|||
|
|||
export const createDataStore = existingContext => { |
|||
const store = writable({ ...existingContext }) |
|||
|
|||
// Adds a context layer to the data context tree
|
|||
const addContext = (row, componentId) => { |
|||
store.update(state => { |
|||
if (componentId) { |
|||
state[componentId] = row |
|||
state[`${componentId}_draft`] = cloneDeep(row) |
|||
state.closestComponentId = componentId |
|||
} |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
subscribe: store.subscribe, |
|||
update: store.update, |
|||
actions: { addContext }, |
|||
} |
|||
} |
|||
|
|||
export const dataStore = createDataStore() |
|||
@ -0,0 +1,84 @@ |
|||
import { writable, get } from "svelte/store" |
|||
import { notificationStore } from "./notification" |
|||
|
|||
export const createDatasourceStore = () => { |
|||
const store = writable([]) |
|||
|
|||
// Registers a new datasource instance
|
|||
const registerDatasource = (datasource, instanceId, refresh) => { |
|||
if (!datasource || !instanceId || !refresh) { |
|||
return |
|||
} |
|||
|
|||
// Create a list of all relevant datasource IDs which would require that
|
|||
// this datasource is refreshed
|
|||
let datasourceIds = [] |
|||
|
|||
// Extract table ID
|
|||
if (datasource.type === "table") { |
|||
if (datasource.tableId) { |
|||
datasourceIds.push(datasource.tableId) |
|||
} |
|||
} |
|||
|
|||
// Extract both table IDs from both sides of the relationship
|
|||
else if (datasource.type === "link") { |
|||
if (datasource.rowTableId) { |
|||
datasourceIds.push(datasource.rowTableId) |
|||
} |
|||
if (datasource.tableId) { |
|||
datasourceIds.push(datasource.tableId) |
|||
} |
|||
} |
|||
|
|||
// Extract the datasource ID (not the query ID) for queries
|
|||
else if (datasource.type === "query") { |
|||
if (datasource.datasourceId) { |
|||
datasourceIds.push(datasource.datasourceId) |
|||
} |
|||
} |
|||
|
|||
// Store configs for each relevant datasource ID
|
|||
if (datasourceIds.length) { |
|||
store.update(state => { |
|||
datasourceIds.forEach(id => { |
|||
state.push({ |
|||
datasourceId: id, |
|||
instanceId, |
|||
refresh, |
|||
}) |
|||
}) |
|||
return state |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// Removes all registered datasource instances belonging to a particular
|
|||
// instance ID
|
|||
const unregisterInstance = instanceId => { |
|||
store.update(state => { |
|||
return state.filter(instance => instance.instanceId !== instanceId) |
|||
}) |
|||
} |
|||
|
|||
// Invalidates a specific datasource ID by refreshing all instances
|
|||
// which depend on data from that datasource
|
|||
const invalidateDatasource = datasourceId => { |
|||
const relatedInstances = get(store).filter(instance => { |
|||
return instance.datasourceId === datasourceId |
|||
}) |
|||
if (relatedInstances?.length) { |
|||
notificationStore.blockNotifications(1000) |
|||
} |
|||
relatedInstances?.forEach(instance => { |
|||
instance.refresh() |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
subscribe: store.subscribe, |
|||
actions: { registerDatasource, unregisterInstance, invalidateDatasource }, |
|||
} |
|||
} |
|||
|
|||
export const datasourceStore = createDatasourceStore() |
|||
@ -0,0 +1,12 @@ |
|||
export const hashString = str => { |
|||
if (!str) { |
|||
return 0 |
|||
} |
|||
let hash = 0 |
|||
for (let i = 0; i < str.length; i++) { |
|||
let char = str.charCodeAt(i) |
|||
hash = (hash << 5) - hash + char |
|||
hash = hash & hash // Convert to 32bit integer
|
|||
} |
|||
return hash |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
<script> |
|||
import Form from "./Form.svelte" |
|||
</script> |
|||
|
|||
<Form wide={false} /> |
|||
@ -1,5 +0,0 @@ |
|||
<script> |
|||
import Form from "./Form.svelte" |
|||
</script> |
|||
|
|||
<Form wide /> |
|||
@ -1,21 +0,0 @@ |
|||
<script> |
|||
import { DatePicker } from "@budibase/bbui" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable, setBindableValue } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let placeholder |
|||
|
|||
let value |
|||
$: setBindableValue(value, $component.id) |
|||
|
|||
function handleChange(event) { |
|||
const [fullDate] = event.detail |
|||
value = fullDate |
|||
} |
|||
</script> |
|||
|
|||
<div use:styleable={$component.styles}> |
|||
<DatePicker {placeholder} on:change={handleChange} {value} /> |
|||
</div> |
|||
@ -1,103 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { |
|||
Label, |
|||
DatePicker, |
|||
Input, |
|||
Select, |
|||
Toggle, |
|||
RichText, |
|||
} from "@budibase/bbui" |
|||
import Dropzone from "./attachments/Dropzone.svelte" |
|||
import LinkedRowSelector from "./LinkedRowSelector.svelte" |
|||
import { capitalise } from "./helpers" |
|||
|
|||
const { styleable, API } = getContext("sdk") |
|||
const component = getContext("component") |
|||
const dataContext = getContext("data") |
|||
|
|||
export let wide = false |
|||
|
|||
let row |
|||
let schema |
|||
let fields = [] |
|||
|
|||
// Fetch info about the closest data context |
|||
$: getFormData($dataContext[$dataContext.closestComponentId]) |
|||
|
|||
const getFormData = async context => { |
|||
if (context) { |
|||
const tableDefinition = await API.fetchTableDefinition(context.tableId) |
|||
schema = tableDefinition?.schema |
|||
fields = Object.keys(schema ?? {}) |
|||
|
|||
// Use the draft version for editing |
|||
row = $dataContext[`${$dataContext.closestComponentId}_draft`] |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="form-content" use:styleable={$component.styles}> |
|||
<!-- <ErrorsBox errors={$store.saveRowErrors || {}} />--> |
|||
{#each fields as field} |
|||
<div class="form-field" class:wide> |
|||
{#if !(schema[field].type === 'boolean' && !wide)} |
|||
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label> |
|||
{/if} |
|||
{#if schema[field].type === 'options'} |
|||
<Select secondary bind:value={row[field]}> |
|||
<option value="">Choose an option</option> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if schema[field].type === 'datetime'} |
|||
<DatePicker bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'boolean'} |
|||
<Toggle |
|||
text={wide ? null : capitalise(schema[field].name)} |
|||
bind:checked={row[field]} /> |
|||
{:else if schema[field].type === 'number'} |
|||
<Input type="number" bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'string'} |
|||
<Input bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'longform'} |
|||
<RichText bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'attachment'} |
|||
<Dropzone bind:files={row[field]} /> |
|||
{:else if schema[field].type === 'link'} |
|||
<LinkedRowSelector |
|||
secondary |
|||
showLabel={false} |
|||
bind:linkedRows={row[field]} |
|||
schema={schema[field]} /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
|
|||
.form-content { |
|||
display: grid; |
|||
gap: var(--spacing-xl); |
|||
width: 100%; |
|||
} |
|||
|
|||
.form-field { |
|||
display: grid; |
|||
} |
|||
.form-field.wide { |
|||
align-items: center; |
|||
grid-template-columns: 20% 1fr; |
|||
gap: var(--spacing-xl); |
|||
} |
|||
.form-field.wide :global(label) { |
|||
margin-bottom: 0; |
|||
} |
|||
</style> |
|||
@ -1,14 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable, setBindableValue } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
let value |
|||
|
|||
function onBlur() { |
|||
setBindableValue(value, $component.id) |
|||
} |
|||
</script> |
|||
|
|||
<input bind:value on:blur={onBlur} use:styleable={$component.styles} /> |
|||
@ -1,14 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
|
|||
const { DataProvider, styleable } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let table |
|||
</script> |
|||
|
|||
<div use:styleable={$component.styles}> |
|||
<DataProvider row={{ tableId: table }}> |
|||
<slot /> |
|||
</DataProvider> |
|||
</div> |
|||
@ -1,29 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { RichText } from "@budibase/bbui" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let value = "" |
|||
|
|||
// Need to determine what options we want to expose. |
|||
let options = { |
|||
modules: { |
|||
toolbar: [ |
|||
[ |
|||
{ |
|||
header: [1, 2, 3, false], |
|||
}, |
|||
], |
|||
["bold", "italic", "underline", "strike"], |
|||
], |
|||
}, |
|||
placeholder: "Type something...", |
|||
theme: "snow", |
|||
} |
|||
</script> |
|||
|
|||
<div use:styleable={$component.styles}> |
|||
<RichText bind:value {options} /> |
|||
</div> |
|||
@ -1,46 +1,57 @@ |
|||
<script> |
|||
import { onMount, getContext } from "svelte" |
|||
|
|||
const { API, screenStore, routeStore, DataProvider, styleable } = getContext( |
|||
"sdk" |
|||
) |
|||
const component = getContext("component") |
|||
|
|||
export let table |
|||
|
|||
const { |
|||
API, |
|||
screenStore, |
|||
routeStore, |
|||
Provider, |
|||
styleable, |
|||
ActionTypes, |
|||
} = getContext("sdk") |
|||
const component = getContext("component") |
|||
let headers = [] |
|||
let row |
|||
|
|||
async function fetchFirstRow() { |
|||
const rows = await API.fetchTableData(table) |
|||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId: table } |
|||
const fetchFirstRow = async tableId => { |
|||
const rows = await API.fetchTableData(tableId) |
|||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId } |
|||
} |
|||
|
|||
async function fetchData() { |
|||
if (!table) { |
|||
const fetchData = async (rowId, tableId) => { |
|||
if (!tableId) { |
|||
return |
|||
} |
|||
|
|||
const pathParts = window.location.pathname.split("/") |
|||
const routeParamId = $routeStore.routeParams.id |
|||
|
|||
// if srcdoc, then we assume this is the builder preview |
|||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) { |
|||
row = await fetchFirstRow() |
|||
} else if (routeParamId) { |
|||
row = await API.fetchRow({ tableId: table, rowId: routeParamId }) |
|||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && tableId) { |
|||
row = await fetchFirstRow(tableId) |
|||
} else if (rowId) { |
|||
row = await API.fetchRow({ tableId, rowId }) |
|||
} else { |
|||
throw new Error("Row ID was not supplied to RowDetail") |
|||
} |
|||
} |
|||
|
|||
onMount(fetchData) |
|||
$: actions = [ |
|||
{ |
|||
type: ActionTypes.RefreshDatasource, |
|||
callback: () => fetchData($routeStore.routeParams.id, table), |
|||
metadata: { datasource: { type: "table", tableId: table } }, |
|||
}, |
|||
] |
|||
|
|||
onMount(() => fetchData($routeStore.routeParams.id, table)) |
|||
</script> |
|||
|
|||
{#if row} |
|||
<div use:styleable={$component.styles}> |
|||
<DataProvider {row}> |
|||
<Provider data={row} {actions}> |
|||
<div use:styleable={$component.styles}> |
|||
<slot /> |
|||
</DataProvider> |
|||
</div> |
|||
</div> |
|||
</Provider> |
|||
{/if} |
|||
|
|||
@ -0,0 +1,34 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import Dropzone from "../attachments/Dropzone.svelte" |
|||
import { onMount } from "svelte" |
|||
|
|||
export let field |
|||
export let label |
|||
|
|||
let fieldState |
|||
let fieldApi |
|||
|
|||
// Update form value from bound value after we've mounted |
|||
let value |
|||
let mounted = false |
|||
$: mounted && fieldApi?.setValue(value) |
|||
|
|||
// Get the fields initial value after initialising |
|||
onMount(() => { |
|||
value = $fieldState?.value |
|||
mounted = true |
|||
}) |
|||
</script> |
|||
|
|||
<Field |
|||
{label} |
|||
{field} |
|||
type="attachment" |
|||
bind:fieldState |
|||
bind:fieldApi |
|||
defaultValue={[]}> |
|||
{#if mounted} |
|||
<Dropzone bind:files={value} /> |
|||
{/if} |
|||
</Field> |
|||
@ -0,0 +1,57 @@ |
|||
<script> |
|||
import "@spectrum-css/checkbox/dist/index-vars.css" |
|||
import Field from "./Field.svelte" |
|||
|
|||
export let field |
|||
export let label |
|||
export let text |
|||
|
|||
let fieldState |
|||
let fieldApi |
|||
|
|||
const onChange = event => { |
|||
fieldApi.setValue(event.target.checked) |
|||
} |
|||
</script> |
|||
|
|||
<Field |
|||
{label} |
|||
{field} |
|||
type="boolean" |
|||
bind:fieldState |
|||
bind:fieldApi |
|||
defaultValue={false}> |
|||
{#if fieldState} |
|||
<div class="spectrum-FieldGroup spectrum-FieldGroup--horizontal"> |
|||
<label class="spectrum-Checkbox" class:is-invalid={!$fieldState.valid}> |
|||
<input |
|||
checked={$fieldState.value} |
|||
on:change={onChange} |
|||
type="checkbox" |
|||
class="spectrum-Checkbox-input" |
|||
id={$fieldState.fieldId} /> |
|||
<span class="spectrum-Checkbox-box"> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark75 spectrum-Checkbox-checkmark" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark75" /> |
|||
</svg> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Dash75 spectrum-Checkbox-partialCheckmark" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Dash75" /> |
|||
</svg> |
|||
</span> |
|||
<span class="spectrum-Checkbox-label">{text || 'Checkbox'}</span> |
|||
</label> |
|||
</div> |
|||
{/if} |
|||
</Field> |
|||
|
|||
<style> |
|||
.spectrum-Checkbox { |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,141 @@ |
|||
<script> |
|||
import Flatpickr from "svelte-flatpickr" |
|||
import Field from "./Field.svelte" |
|||
import "flatpickr/dist/flatpickr.css" |
|||
import "@spectrum-css/inputgroup/dist/index-vars.css" |
|||
import { generateID } from "../helpers" |
|||
|
|||
export let field |
|||
export let label |
|||
export let placeholder |
|||
export let enableTime |
|||
|
|||
let fieldState |
|||
let fieldApi |
|||
let open = false |
|||
let flatpickr |
|||
|
|||
$: flatpickrId = `${$fieldState?.id}-${generateID()}-wrapper` |
|||
$: flatpickrOptions = { |
|||
element: `#${flatpickrId}`, |
|||
enableTime: enableTime || false, |
|||
altInput: true, |
|||
altFormat: enableTime ? "F j Y, H:i" : "F j, Y", |
|||
} |
|||
|
|||
const handleChange = event => { |
|||
const [dates] = event.detail |
|||
fieldApi.setValue(dates[0]) |
|||
} |
|||
|
|||
const clearDateOnBackspace = event => { |
|||
if (["Backspace", "Clear", "Delete"].includes(event.key)) { |
|||
fieldApi.setValue(null) |
|||
flatpickr.close() |
|||
} |
|||
} |
|||
|
|||
const onOpen = () => { |
|||
open = true |
|||
document.addEventListener("keyup", clearDateOnBackspace) |
|||
} |
|||
|
|||
const onClose = () => { |
|||
open = false |
|||
document.removeEventListener("keyup", clearDateOnBackspace) |
|||
|
|||
// Manually blur all input fields since flatpickr creates a second |
|||
// duplicate input field. |
|||
// We need to blur both because the focus styling does not get properly |
|||
// applied. |
|||
const els = document.querySelectorAll(`#${flatpickrId} input`) |
|||
els.forEach(el => el.blur()) |
|||
} |
|||
</script> |
|||
|
|||
<Field {label} {field} type="datetime" bind:fieldState bind:fieldApi> |
|||
{#if fieldState} |
|||
<Flatpickr |
|||
bind:flatpickr |
|||
value={$fieldState.value} |
|||
on:open={onOpen} |
|||
on:close={onClose} |
|||
options={flatpickrOptions} |
|||
on:change={handleChange} |
|||
element={`#${flatpickrId}`}> |
|||
<div |
|||
id={flatpickrId} |
|||
aria-disabled="false" |
|||
aria-invalid={!$fieldState.valid} |
|||
class:is-invalid={!$fieldState.valid} |
|||
class="flatpickr spectrum-InputGroup spectrum-Datepicker" |
|||
class:is-focused={open} |
|||
aria-readonly="false" |
|||
aria-required="false" |
|||
aria-haspopup="true"> |
|||
<div |
|||
on:click={flatpickr?.open} |
|||
class="spectrum-Textfield spectrum-InputGroup-textfield" |
|||
class:is-invalid={!$fieldState.valid}> |
|||
{#if !$fieldState.valid} |
|||
<svg |
|||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-icon-18-Alert" /> |
|||
</svg> |
|||
{/if} |
|||
<input |
|||
data-input |
|||
type="text" |
|||
class="spectrum-Textfield-input spectrum-InputGroup-input" |
|||
aria-invalid={!$fieldState.valid} |
|||
{placeholder} |
|||
id={$fieldState.fieldId} |
|||
value={$fieldState.value} /> |
|||
</div> |
|||
<button |
|||
type="button" |
|||
class="spectrum-Picker spectrum-InputGroup-button" |
|||
tabindex="-1" |
|||
class:is-invalid={!$fieldState.valid} |
|||
on:click={flatpickr?.open}> |
|||
<svg |
|||
class="spectrum-Icon spectrum-Icon--sizeM" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
aria-label="Calendar"> |
|||
<use xlink:href="#spectrum-icon-18-Calendar" /> |
|||
</svg> |
|||
</button> |
|||
</div> |
|||
</Flatpickr> |
|||
{#if open} |
|||
<div class="overlay" on:mousedown|self={flatpickr?.close} /> |
|||
{/if} |
|||
{/if} |
|||
</Field> |
|||
|
|||
<style> |
|||
.spectrum-Textfield-input { |
|||
pointer-events: none; |
|||
} |
|||
.spectrum-Textfield:hover { |
|||
cursor: pointer; |
|||
} |
|||
.flatpickr { |
|||
width: 100%; |
|||
overflow: hidden; |
|||
} |
|||
.flatpickr .spectrum-Textfield { |
|||
width: 100%; |
|||
} |
|||
.overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
z-index: 999; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,86 @@ |
|||
<script> |
|||
import Placeholder from "./Placeholder.svelte" |
|||
import FieldGroupFallback from "./FieldGroupFallback.svelte" |
|||
import { getContext } from "svelte" |
|||
|
|||
export let label |
|||
export let field |
|||
export let fieldState |
|||
export let fieldApi |
|||
export let fieldSchema |
|||
export let defaultValue |
|||
export let type |
|||
|
|||
// Get contexts |
|||
const formContext = getContext("form") |
|||
const fieldGroupContext = getContext("fieldGroup") |
|||
const { styleable } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
// Register field with form |
|||
const formApi = formContext?.formApi |
|||
const labelPosition = fieldGroupContext?.labelPosition || "above" |
|||
const formField = formApi?.registerField(field, defaultValue) |
|||
|
|||
// Expose field properties to parent component |
|||
fieldState = formField?.fieldState |
|||
fieldApi = formField?.fieldApi |
|||
fieldSchema = formField?.fieldSchema |
|||
|
|||
// Extract label position from field group context |
|||
$: labelPositionClass = |
|||
labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}` |
|||
</script> |
|||
|
|||
<FieldGroupFallback> |
|||
<div class="spectrum-Form-item" use:styleable={$component.styles}> |
|||
<label |
|||
for={$fieldState?.fieldId} |
|||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}> |
|||
{label || ''} |
|||
</label> |
|||
<div class="spectrum-Form-itemField"> |
|||
{#if !formContext} |
|||
<Placeholder>Form components need to be wrapped in a Form</Placeholder> |
|||
{:else if !fieldState} |
|||
<Placeholder> |
|||
Add the Field setting to start using your component |
|||
</Placeholder> |
|||
{:else if fieldSchema?.type && fieldSchema?.type !== type} |
|||
<Placeholder> |
|||
This Field setting is the wrong data type for this component |
|||
</Placeholder> |
|||
{:else} |
|||
<slot /> |
|||
{#if $fieldState.error} |
|||
<div class="error">{$fieldState.error}</div> |
|||
{/if} |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</FieldGroupFallback> |
|||
|
|||
<style> |
|||
label { |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.spectrum-Form-itemField { |
|||
position: relative; |
|||
width: 100%; |
|||
} |
|||
|
|||
.error { |
|||
color: var( |
|||
--spectrum-semantic-negative-color-default, |
|||
var(--spectrum-global-color-red-500) |
|||
); |
|||
font-size: var(--spectrum-global-dimension-font-size-75); |
|||
margin-top: var(--spectrum-global-dimension-size-75); |
|||
} |
|||
|
|||
.spectrum-FieldLabel--right, |
|||
.spectrum-FieldLabel--left { |
|||
padding-right: var(--spectrum-global-dimension-size-200); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,27 @@ |
|||
<script> |
|||
import { getContext, setContext } from "svelte" |
|||
|
|||
export let labelPosition = "above" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const component = getContext("component") |
|||
setContext("fieldGroup", { labelPosition }) |
|||
</script> |
|||
|
|||
<div class="wrapper" use:styleable={$component.styles}> |
|||
<div |
|||
class="spectrum-Form" |
|||
class:spectrum-Form--labelsAbove={labelPosition === 'above'}> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.wrapper { |
|||
width: 100%; |
|||
position: relative; |
|||
} |
|||
.spectrum-Form { |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,14 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
|
|||
const fieldGroupContext = getContext("fieldGroup") |
|||
const labelPosition = fieldGroupContext?.labelPosition || "above" |
|||
</script> |
|||
|
|||
{#if fieldGroupContext} |
|||
<slot /> |
|||
{:else} |
|||
<div class="spectrum-Form--labelsAbove"> |
|||
<slot /> |
|||
</div> |
|||
{/if} |
|||
@ -0,0 +1,173 @@ |
|||
<script> |
|||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
|||
import { setContext, getContext, onMount } from "svelte" |
|||
import { writable, get } from "svelte/store" |
|||
import { createValidatorFromConstraints } from "./validation" |
|||
import { generateID } from "../helpers" |
|||
|
|||
export let datasource |
|||
export let theme |
|||
export let size |
|||
|
|||
const component = getContext("component") |
|||
const context = getContext("context") |
|||
const { styleable, API, Provider, ActionTypes } = getContext("sdk") |
|||
|
|||
let loaded = false |
|||
let schema |
|||
let table |
|||
let fieldMap = {} |
|||
|
|||
// Checks if the closest data context matches the model for this forms |
|||
// datasource, and use it as the initial form values if so |
|||
const getInitialValues = context => { |
|||
return context && context.tableId === datasource?.tableId ? context : {} |
|||
} |
|||
|
|||
// Use the closest data context as the initial form values if it matches |
|||
const initialValues = getInitialValues( |
|||
$context[`${$context.closestComponentId}`] |
|||
) |
|||
|
|||
// Form state contains observable data about the form |
|||
const formState = writable({ values: initialValues, errors: {}, valid: true }) |
|||
|
|||
// Form API contains functions to control the form |
|||
const formApi = { |
|||
registerField: (field, defaultValue = null) => { |
|||
if (!field) { |
|||
return |
|||
} |
|||
if (fieldMap[field] != null) { |
|||
return fieldMap[field] |
|||
} |
|||
|
|||
// Create validation function based on field schema |
|||
const constraints = schema?.[field]?.constraints |
|||
const validate = createValidatorFromConstraints(constraints, field, table) |
|||
|
|||
fieldMap[field] = { |
|||
fieldState: makeFieldState(field, defaultValue), |
|||
fieldApi: makeFieldApi(field, defaultValue, validate), |
|||
fieldSchema: schema?.[field] ?? {}, |
|||
} |
|||
return fieldMap[field] |
|||
}, |
|||
validate: () => { |
|||
const fields = Object.keys(fieldMap) |
|||
fields.forEach(field => { |
|||
const { fieldApi } = fieldMap[field] |
|||
fieldApi.validate() |
|||
}) |
|||
return get(formState).valid |
|||
}, |
|||
} |
|||
|
|||
// Provide both form API and state to children |
|||
setContext("form", { formApi, formState }) |
|||
|
|||
// Action context to pass to children |
|||
$: actions = [{ type: ActionTypes.ValidateForm, callback: formApi.validate }] |
|||
|
|||
// Creates an API for a specific field |
|||
const makeFieldApi = (field, defaultValue, validate) => { |
|||
const setValue = (value, skipCheck = false) => { |
|||
const { fieldState } = fieldMap[field] |
|||
|
|||
// Skip if the value is the same |
|||
if (!skipCheck && get(fieldState).value === value) { |
|||
return |
|||
} |
|||
|
|||
const newValue = value == null ? defaultValue : value |
|||
const newError = validate ? validate(newValue) : null |
|||
const newValid = !newError |
|||
|
|||
// Update field state |
|||
fieldState.update(state => { |
|||
state.value = newValue |
|||
state.error = newError |
|||
state.valid = newValid |
|||
return state |
|||
}) |
|||
|
|||
// Update form state |
|||
formState.update(state => { |
|||
state.values = { ...state.values, [field]: newValue } |
|||
if (newError) { |
|||
state.errors = { ...state.errors, [field]: newError } |
|||
} else { |
|||
delete state.errors[field] |
|||
} |
|||
state.valid = Object.keys(state.errors).length === 0 |
|||
return state |
|||
}) |
|||
|
|||
return newValid |
|||
} |
|||
return { |
|||
setValue, |
|||
validate: () => { |
|||
const { fieldState } = fieldMap[field] |
|||
setValue(get(fieldState).value, true) |
|||
}, |
|||
} |
|||
} |
|||
|
|||
// Creates observable state data about a specific field |
|||
const makeFieldState = (field, defaultValue) => { |
|||
return writable({ |
|||
field, |
|||
fieldId: `id-${generateID()}`, |
|||
value: initialValues[field] ?? defaultValue, |
|||
error: null, |
|||
valid: true, |
|||
}) |
|||
} |
|||
|
|||
// Fetches the form schema from this form's datasource, if one exists |
|||
const fetchSchema = async () => { |
|||
if (!datasource?.tableId) { |
|||
schema = {} |
|||
table = null |
|||
} else { |
|||
table = await API.fetchTableDefinition(datasource?.tableId) |
|||
if (table) { |
|||
if (datasource?.type === "query") { |
|||
schema = {} |
|||
const params = table.parameters || [] |
|||
params.forEach(param => { |
|||
schema[param.name] = { ...param, type: "string" } |
|||
}) |
|||
} else { |
|||
schema = table.schema || {} |
|||
} |
|||
} |
|||
} |
|||
loaded = true |
|||
} |
|||
|
|||
// Load the form schema on mount |
|||
onMount(fetchSchema) |
|||
</script> |
|||
|
|||
<Provider |
|||
{actions} |
|||
data={{ ...$formState.values, tableId: datasource?.tableId }}> |
|||
<div |
|||
lang="en" |
|||
dir="ltr" |
|||
use:styleable={$component.styles} |
|||
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}> |
|||
{#if loaded} |
|||
<slot /> |
|||
{/if} |
|||
</div> |
|||
</Provider> |
|||
|
|||
<style> |
|||
div { |
|||
padding: 20px; |
|||
position: relative; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,71 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { RichText } from "@budibase/bbui" |
|||
import Field from "./Field.svelte" |
|||
|
|||
export let field |
|||
export let label |
|||
export let placeholder |
|||
|
|||
let fieldState |
|||
let fieldApi |
|||
|
|||
// Update form value from bound value after we've mounted |
|||
let value |
|||
let mounted = false |
|||
$: mounted && fieldApi?.setValue(value) |
|||
|
|||
// Get the fields initial value after initialising |
|||
onMount(() => { |
|||
value = $fieldState?.value |
|||
mounted = true |
|||
}) |
|||
|
|||
// Options for rich text component |
|||
const options = { |
|||
modules: { |
|||
toolbar: [ |
|||
[ |
|||
{ |
|||
header: [1, 2, 3, false], |
|||
}, |
|||
], |
|||
["bold", "italic", "underline", "strike"], |
|||
], |
|||
}, |
|||
placeholder: placeholder || "Type something...", |
|||
theme: "snow", |
|||
} |
|||
</script> |
|||
|
|||
<Field |
|||
{label} |
|||
{field} |
|||
type="longform" |
|||
bind:fieldState |
|||
bind:fieldApi |
|||
defaultValue=""> |
|||
{#if mounted} |
|||
<div> |
|||
<RichText bind:value {options} /> |
|||
</div> |
|||
{/if} |
|||
</Field> |
|||
|
|||
<style> |
|||
div { |
|||
background-color: white; |
|||
} |
|||
div :global(> div) { |
|||
width: auto !important; |
|||
} |
|||
div :global(.ql-snow.ql-toolbar:after, .ql-snow .ql-toolbar:after) { |
|||
display: none; |
|||
} |
|||
div :global(.ql-snow .ql-formats:after) { |
|||
display: none; |
|||
} |
|||
div :global(.ql-editor p) { |
|||
word-break: break-all; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,5 @@ |
|||
<script> |
|||
import StringField from "./StringField.svelte" |
|||
</script> |
|||
|
|||
<StringField {...$$props} type="number" /> |
|||
@ -0,0 +1,44 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import Picker from "./Picker.svelte" |
|||
|
|||
export let field |
|||
export let label |
|||
export let placeholder |
|||
|
|||
let fieldState |
|||
let fieldApi |
|||
let fieldSchema |
|||
|
|||
// Picker state |
|||
let open = false |
|||
$: options = fieldSchema?.constraints?.inclusion ?? [] |
|||
$: placeholderText = placeholder || "Choose an option" |
|||
$: isNull = $fieldState?.value == null || $fieldState?.value === "" |
|||
$: fieldText = isNull ? placeholderText : $fieldState?.value |
|||
|
|||
const selectOption = value => { |
|||
fieldApi.setValue(value) |
|||
open = false |
|||
} |
|||
</script> |
|||
|
|||
<Field |
|||
{field} |
|||
{label} |
|||
type="options" |
|||
bind:fieldState |
|||
bind:fieldApi |
|||
bind:fieldSchema> |
|||
{#if fieldState} |
|||
<Picker |
|||
bind:open |
|||
{fieldState} |
|||
{fieldText} |
|||
{options} |
|||
isPlaceholder={isNull} |
|||
placeholderOption={placeholderText} |
|||
isOptionSelected={option => option === $fieldState.value} |
|||
onSelectOption={selectOption} /> |
|||
{/if} |
|||
</Field> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue