forked from tsai/budibase
120 changed files with 2871 additions and 922 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 = { |
export const TableNames = { |
||||
USERS: "ta_users", |
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> |
<script> |
||||
import { onMount, getContext } from "svelte" |
import { onMount, getContext } from "svelte" |
||||
|
|
||||
const { API, screenStore, routeStore, DataProvider, styleable } = getContext( |
|
||||
"sdk" |
|
||||
) |
|
||||
const component = getContext("component") |
|
||||
|
|
||||
export let table |
export let table |
||||
|
|
||||
|
const { |
||||
|
API, |
||||
|
screenStore, |
||||
|
routeStore, |
||||
|
Provider, |
||||
|
styleable, |
||||
|
ActionTypes, |
||||
|
} = getContext("sdk") |
||||
|
const component = getContext("component") |
||||
let headers = [] |
let headers = [] |
||||
let row |
let row |
||||
|
|
||||
async function fetchFirstRow() { |
const fetchFirstRow = async tableId => { |
||||
const rows = await API.fetchTableData(table) |
const rows = await API.fetchTableData(tableId) |
||||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId: table } |
return Array.isArray(rows) && rows.length ? rows[0] : { tableId } |
||||
} |
} |
||||
|
|
||||
async function fetchData() { |
const fetchData = async (rowId, tableId) => { |
||||
if (!table) { |
if (!tableId) { |
||||
return |
return |
||||
} |
} |
||||
|
|
||||
const pathParts = window.location.pathname.split("/") |
const pathParts = window.location.pathname.split("/") |
||||
const routeParamId = $routeStore.routeParams.id |
|
||||
|
|
||||
// if srcdoc, then we assume this is the builder preview |
// if srcdoc, then we assume this is the builder preview |
||||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) { |
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && tableId) { |
||||
row = await fetchFirstRow() |
row = await fetchFirstRow(tableId) |
||||
} else if (routeParamId) { |
} else if (rowId) { |
||||
row = await API.fetchRow({ tableId: table, rowId: routeParamId }) |
row = await API.fetchRow({ tableId, rowId }) |
||||
} else { |
} else { |
||||
throw new Error("Row ID was not supplied to RowDetail") |
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> |
</script> |
||||
|
|
||||
{#if row} |
{#if row} |
||||
<div use:styleable={$component.styles}> |
<Provider data={row} {actions}> |
||||
<DataProvider {row}> |
<div use:styleable={$component.styles}> |
||||
<slot /> |
<slot /> |
||||
</DataProvider> |
</div> |
||||
</div> |
</Provider> |
||||
{/if} |
{/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> |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue