Add WIP draft of linked records UI allowing single linked record selection

This commit is contained in:
Andrew Kingston 2020-09-29 18:27:35 +01:00
parent 4073f354c8
commit e4ac832c32
8 changed files with 86 additions and 158 deletions

View File

@ -63,7 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.37.1", "@budibase/bbui": "^1.39.0",
"@budibase/client": "^0.1.21", "@budibase/client": "^0.1.21",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",

View File

@ -91,7 +91,7 @@
{#each headers as header} {#each headers as header}
<td> <td>
{#if schema[header].type === 'link'} {#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} /> {JSON.stringify(row[header])}
{:else if schema[header].type === 'attachment'} {:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} /> <AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if} {:else}{getOr('', header, row)}{/if}

View File

@ -31,6 +31,9 @@
} }
let originalName = field.name let originalName = field.name
$: modelOptions = $backendUiStore.models.filter(
model => model._id !== $backendUiStore.draftModel._id
)
async function saveColumn() { async function saveColumn() {
backendUiStore.update(state => { backendUiStore.update(state => {
@ -67,23 +70,27 @@
</Select> </Select>
<Toggle <Toggle
bind:checked={field.constraints.presence.allowEmpty} checked={!field.constraints.presence.allowEmpty}
on:change={e => (field.constraints.presence.allowEmpty = !e.target.checked)}
thin thin
text="Required" /> text="Required" />
{#if field.type === 'string' && field.constraints} {#if field.type === 'string'}
<Input <Input
thin thin
type="number" type="number"
label="Max Length" label="Max Length"
bind:value={field.constraints.length.maximum} /> bind:value={field.constraints.length.maximum} />
<ValuesList label="Categories" bind:values={field.constraints.inclusion} /> {:else if field.type === 'options'}
{:else if field.type === 'datetime' && field.constraints} <ValuesList
label="Options (one per line)"
bind:values={field.constraints.inclusion} />
{:else if field.type === 'datetime'}
<DatePicker <DatePicker
label="Earliest" label="Earliest"
bind:value={field.constraints.datetime.earliest} /> bind:value={field.constraints.datetime.earliest} />
<DatePicker label="Latest" bind:value={field.constraints.datetime.latest} /> <DatePicker label="Latest" bind:value={field.constraints.datetime.latest} />
{:else if field.type === 'number' && field.constraints} {:else if field.type === 'number'}
<Input <Input
thin thin
type="number" type="number"
@ -95,17 +102,16 @@
label="Max Value" label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} /> bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'} {:else if field.type === 'link'}
<div class="field"> <Select label="Table" thin secondary bind:value={field.modelId}>
<label>Link</label>
<Select bind:value={field.modelId}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each $backendUiStore.models as model} {#each modelOptions as model}
{#if model._id !== $backendUiStore.draftModel._id}
<option value={model._id}>{model.name}</option> <option value={model._id}>{model.name}</option>
{/if}
{/each} {/each}
</Select> </Select>
</div> <Input
label={`Column Name in Other Table`}
thin
bind:value={field.fieldName} />
{/if} {/if}
<footer> <footer>
<Button secondary on:click={onClosed}>Cancel</Button> <Button secondary on:click={onClosed}>Cancel</Button>

View File

@ -47,9 +47,8 @@
<div> <div>
{#if meta.type === 'link'} {#if meta.type === 'link'}
<LinkedRecordSelector <LinkedRecordSelector
bind:linked={record[key]} bind:linkedRecords={record[key]}
linkName={meta.name} schema={meta} />
modelId={meta.modelId} />
{:else} {:else}
<RecordFieldControl {meta} bind:value={record[key]} /> <RecordFieldControl {meta} bind:value={record[key]} />
{/if} {/if}

View File

@ -1,64 +1,40 @@
<script> <script>
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui" import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte" import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../../helpers"
export let meta export let meta
export let value = meta.type === "boolean" ? false : "" export let value = meta.type === "boolean" ? false : ""
export let originalValue
let isSelect = const type = determineInputType(meta)
meta.type === "string" && const label = capitalise(meta.name)
meta.constraints &&
meta.constraints.inclusion &&
meta.constraints.inclusion.length > 0
let type = determineInputType(meta)
function determineInputType(meta) { function determineInputType(meta) {
if (meta.type === "datetime") return "date" if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number" if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox" if (meta.type === "boolean") return "checkbox"
if (meta.type === "attachment") return "file" if (meta.type === "attachment") return "file"
if (isSelect) return "select" if (meta.type === "options") return "select"
return "text" return "text"
} }
</script> </script>
{#if type === 'select'} {#if type === 'select'}
<Select <Select thin secondary {label} data-cy="{meta.name}-select" bind:value>
thin
secondary
label={meta.name}
data-cy="{meta.name}-select"
bind:value>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each meta.constraints.inclusion as opt} {#each meta.constraints.inclusion as opt}
<option value={opt}>{opt}</option> <option value={opt}>{opt}</option>
{/each} {/each}
</Select> </Select>
{:else if type === 'date'} {:else if type === 'date'}
<DatePicker label={meta.name} bind:value /> <DatePicker {label} bind:value />
{:else if type === 'file'} {:else if type === 'file'}
<div> <div>
<Label extraSmall grey forAttr={'dropzone-label'}>{meta.name}</Label> <Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label>
<Dropzone bind:files={value} /> <Dropzone bind:files={value} />
</div> </div>
{:else if type === 'checkbox'} {:else if type === 'checkbox'}
<Toggle text={meta.name} bind:checked={value} data-cy="{meta.name}-input" /> <Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
{:else} {:else}
<Input thin label={meta.name} data-cy="{meta.name}-input" {type} bind:value /> <Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
{/if} {/if}
<style>
.checkbox {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.checkbox :global(label) {
margin-bottom: 0;
margin-right: var(--spacing-xs);
}
</style>

View File

@ -2,37 +2,28 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
import { Select, Label } from "@budibase/bbui"
import { capitalise } from "../../helpers"
export let modelId export let schema
export let linkName export let linkedRecords = []
export let linked = []
let records = [] let records = []
let model = {}
let linkedRecords = new Set(linked) $: label = capitalise(schema.name)
$: linkedModelId = schema.modelId
$: linked = [...linkedRecords] $: linkedModel = $backendUiStore.models.find(
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name] model => model._id === linkedModelId
$: schema = $backendUiStore.selectedModel.schema )
async function fetchRecords() { async function fetchRecords() {
const FETCH_RECORDS_URL = `/api/${modelId}/records` const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
const response = await api.get(FETCH_RECORDS_URL) const response = await api.get(FETCH_RECORDS_URL)
const modelResponse = await api.get(`/api/models/${modelId}`)
model = await modelResponse.json()
records = await response.json() records = await response.json()
} }
function linkRecord(id) { function getPrettyName(record) {
if (linkedRecords.has(id)) { return record[linkedModel.primaryDisplay || "_id"]
linkedRecords.delete(id)
} else {
linkedRecords.add(id)
}
linkedRecords = linkedRecords
} }
onMount(() => { onMount(() => {
@ -40,63 +31,18 @@
}) })
</script> </script>
<section> {#if linkedModel.primaryDisplay == null}
<header> <Label extraSmall grey>{label}</Label>
<h3>{linkName}</h3> <Label small black>
</header> Please choose a primary display column for the
<b>{linkedModel.name}</b>
table.
</Label>
{:else}
<Select thin secondary bind:value={linkedRecords[0]} {label}>
<option value="">Choose an option</option>
{#each records as record} {#each records as record}
<div class="linked-record" on:click={() => linkRecord(record._id)}> <option value={record._id}>{getPrettyName(record)}</option>
<div class="fields" class:selected={linkedRecords.has(record._id)}>
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
<div class="field">
<span>{model.schema[key].name}</span>
<p>{record[key]}</p>
</div>
{/each} {/each}
</div> </Select>
</div> {/if}
{/each}
</section>
<style>
.fields.selected {
background: var(--grey-2);
border: var(--purple) 1px solid;
}
h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: var(--ink);
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
transition: 0.5s all;
margin-bottom: 8px;
}
.fields:hover {
cursor: pointer;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

View File

@ -1,6 +1,6 @@
export const FIELDS = { export const FIELDS = {
STRING: { STRING: {
name: "Plain Text", name: "Text",
icon: "ri-text", icon: "ri-text",
type: "string", type: "string",
constraints: { constraints: {
@ -9,6 +9,16 @@ export const FIELDS = {
presence: { allowEmpty: true }, presence: { allowEmpty: true },
}, },
}, },
OPTIONS: {
name: "Options",
icon: "ri-list-check-2",
type: "options",
constraints: {
type: "string",
presence: { allowEmpty: true },
inclusion: [],
},
},
NUMBER: { NUMBER: {
name: "Number", name: "Number",
icon: "ri-number-1", icon: "ri-number-1",
@ -28,15 +38,6 @@ export const FIELDS = {
presence: { allowEmpty: true }, presence: { allowEmpty: true },
}, },
}, },
// OPTIONS: {
// name: "Options",
// icon: "ri-list-check-2",
// type: "options",
// constraints: {
// type: "string",
// presence: { allowEmpty: true },
// },
// },
DATETIME: { DATETIME: {
name: "Date/Time", name: "Date/Time",
icon: "ri-calendar-event-fill", icon: "ri-calendar-event-fill",
@ -60,15 +61,15 @@ export const FIELDS = {
presence: { allowEmpty: true }, presence: { allowEmpty: true },
}, },
}, },
// LINKED_FIELDS: { LINK: {
// name: "Linked Fields", name: "Relationship",
// icon: "ri-link", icon: "ri-link",
// type: "link", type: "link",
// modelId: null, constraints: {
// constraints: { type: "array",
// type: "array", presence: { allowEmpty: true },
// }, },
// }, },
} }
export const FILE_TYPES = { export const FILE_TYPES = {

View File

@ -709,10 +709,10 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/bbui@^1.37.1": "@budibase/bbui@^1.39.0":
version "1.37.1" version "1.39.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.37.1.tgz#a4d5aa85ee36c8013173e28879f1e8c8421f017c" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.39.0.tgz#7d3e259a60a0b4602f3d2da3452679d91a591fdd"
integrity sha512-xdwwdqkpEjVYn1L38W7jR1mHkvf+xIoktoOm90LmmocJirzNo+flT4zxM2E7a3C3Bfs1l9NwEd6bqSuuxEF9Zg== integrity sha512-9IL5Lw488sdYCa9mjHHdrap11VqW6wQHNcNTL8fFHaWNzummtlaUlVMScs9cunYgsR/L4NCgH0zSFdP0RnrUqw==
dependencies: dependencies:
sirv-cli "^0.4.6" sirv-cli "^0.4.6"
svelte-flatpickr "^2.4.0" svelte-flatpickr "^2.4.0"