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 = { |
|||
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> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue