Add WIP draft of linked records UI allowing single linked record selection
This commit is contained in:
parent
e798fca33b
commit
089ecf5609
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.37.1",
|
||||
"@budibase/bbui": "^1.39.0",
|
||||
"@budibase/client": "^0.1.21",
|
||||
"@budibase/colorpicker": "^1.0.1",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
{#each headers as header}
|
||||
<td>
|
||||
{#if schema[header].type === 'link'}
|
||||
<LinkedRecord field={schema[header]} ids={row[header]} />
|
||||
{JSON.stringify(row[header])}
|
||||
{:else if schema[header].type === 'attachment'}
|
||||
<AttachmentList files={row[header] || []} />
|
||||
{:else}{getOr('', header, row)}{/if}
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
}
|
||||
|
||||
let originalName = field.name
|
||||
$: modelOptions = $backendUiStore.models.filter(
|
||||
model => model._id !== $backendUiStore.draftModel._id
|
||||
)
|
||||
|
||||
async function saveColumn() {
|
||||
backendUiStore.update(state => {
|
||||
|
@ -67,23 +70,27 @@
|
|||
</Select>
|
||||
|
||||
<Toggle
|
||||
bind:checked={field.constraints.presence.allowEmpty}
|
||||
checked={!field.constraints.presence.allowEmpty}
|
||||
on:change={e => (field.constraints.presence.allowEmpty = !e.target.checked)}
|
||||
thin
|
||||
text="Required" />
|
||||
|
||||
{#if field.type === 'string' && field.constraints}
|
||||
{#if field.type === 'string'}
|
||||
<Input
|
||||
thin
|
||||
type="number"
|
||||
label="Max Length"
|
||||
bind:value={field.constraints.length.maximum} />
|
||||
<ValuesList label="Categories" bind:values={field.constraints.inclusion} />
|
||||
{:else if field.type === 'datetime' && field.constraints}
|
||||
{:else if field.type === 'options'}
|
||||
<ValuesList
|
||||
label="Options (one per line)"
|
||||
bind:values={field.constraints.inclusion} />
|
||||
{:else if field.type === 'datetime'}
|
||||
<DatePicker
|
||||
label="Earliest"
|
||||
bind:value={field.constraints.datetime.earliest} />
|
||||
<DatePicker label="Latest" bind:value={field.constraints.datetime.latest} />
|
||||
{:else if field.type === 'number' && field.constraints}
|
||||
{:else if field.type === 'number'}
|
||||
<Input
|
||||
thin
|
||||
type="number"
|
||||
|
@ -95,17 +102,16 @@
|
|||
label="Max Value"
|
||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
||||
{:else if field.type === 'link'}
|
||||
<div class="field">
|
||||
<label>Link</label>
|
||||
<Select bind:value={field.modelId}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
{#if model._id !== $backendUiStore.draftModel._id}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
<Select label="Table" thin secondary bind:value={field.modelId}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each modelOptions as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<Input
|
||||
label={`Column Name in Other Table`}
|
||||
thin
|
||||
bind:value={field.fieldName} />
|
||||
{/if}
|
||||
<footer>
|
||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||
|
|
|
@ -47,9 +47,8 @@
|
|||
<div>
|
||||
{#if meta.type === 'link'}
|
||||
<LinkedRecordSelector
|
||||
bind:linked={record[key]}
|
||||
linkName={meta.name}
|
||||
modelId={meta.modelId} />
|
||||
bind:linkedRecords={record[key]}
|
||||
schema={meta} />
|
||||
{:else}
|
||||
<RecordFieldControl {meta} bind:value={record[key]} />
|
||||
{/if}
|
||||
|
|
|
@ -1,64 +1,40 @@
|
|||
<script>
|
||||
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
import { capitalise } from "../../../../helpers"
|
||||
|
||||
export let meta
|
||||
export let value = meta.type === "boolean" ? false : ""
|
||||
export let originalValue
|
||||
|
||||
let isSelect =
|
||||
meta.type === "string" &&
|
||||
meta.constraints &&
|
||||
meta.constraints.inclusion &&
|
||||
meta.constraints.inclusion.length > 0
|
||||
|
||||
let type = determineInputType(meta)
|
||||
const type = determineInputType(meta)
|
||||
const label = capitalise(meta.name)
|
||||
|
||||
function determineInputType(meta) {
|
||||
if (meta.type === "datetime") return "date"
|
||||
if (meta.type === "number") return "number"
|
||||
if (meta.type === "boolean") return "checkbox"
|
||||
if (meta.type === "attachment") return "file"
|
||||
if (isSelect) return "select"
|
||||
|
||||
if (meta.type === "options") return "select"
|
||||
return "text"
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if type === 'select'}
|
||||
<Select
|
||||
thin
|
||||
secondary
|
||||
label={meta.name}
|
||||
data-cy="{meta.name}-select"
|
||||
bind:value>
|
||||
<Select thin secondary {label} data-cy="{meta.name}-select" bind:value>
|
||||
<option value="">Choose an option</option>
|
||||
{#each meta.constraints.inclusion as opt}
|
||||
<option value={opt}>{opt}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if type === 'date'}
|
||||
<DatePicker label={meta.name} bind:value />
|
||||
<DatePicker {label} bind:value />
|
||||
{:else if type === 'file'}
|
||||
<div>
|
||||
<Label extraSmall grey forAttr={'dropzone-label'}>{meta.name}</Label>
|
||||
<Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label>
|
||||
<Dropzone bind:files={value} />
|
||||
</div>
|
||||
{: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}
|
||||
<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}
|
||||
|
||||
<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>
|
||||
|
|
|
@ -2,37 +2,28 @@
|
|||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { capitalise } from "../../helpers"
|
||||
|
||||
export let modelId
|
||||
export let linkName
|
||||
export let linked = []
|
||||
export let schema
|
||||
export let linkedRecords = []
|
||||
|
||||
let records = []
|
||||
let model = {}
|
||||
|
||||
let linkedRecords = new Set(linked)
|
||||
|
||||
$: linked = [...linkedRecords]
|
||||
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name]
|
||||
$: schema = $backendUiStore.selectedModel.schema
|
||||
$: label = capitalise(schema.name)
|
||||
$: linkedModelId = schema.modelId
|
||||
$: linkedModel = $backendUiStore.models.find(
|
||||
model => model._id === linkedModelId
|
||||
)
|
||||
|
||||
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 modelResponse = await api.get(`/api/models/${modelId}`)
|
||||
|
||||
model = await modelResponse.json()
|
||||
records = await response.json()
|
||||
}
|
||||
|
||||
function linkRecord(id) {
|
||||
if (linkedRecords.has(id)) {
|
||||
linkedRecords.delete(id)
|
||||
} else {
|
||||
linkedRecords.add(id)
|
||||
}
|
||||
|
||||
linkedRecords = linkedRecords
|
||||
function getPrettyName(record) {
|
||||
return record[linkedModel.primaryDisplay || "_id"]
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
@ -40,63 +31,18 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<h3>{linkName}</h3>
|
||||
</header>
|
||||
{#each records as record}
|
||||
<div class="linked-record" on:click={() => linkRecord(record._id)}>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
{/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>
|
||||
{#if linkedModel.primaryDisplay == null}
|
||||
<Label extraSmall grey>{label}</Label>
|
||||
<Label small black>
|
||||
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}
|
||||
<option value={record._id}>{getPrettyName(record)}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export const FIELDS = {
|
||||
STRING: {
|
||||
name: "Plain Text",
|
||||
name: "Text",
|
||||
icon: "ri-text",
|
||||
type: "string",
|
||||
constraints: {
|
||||
|
@ -9,6 +9,16 @@ export const FIELDS = {
|
|||
presence: { allowEmpty: true },
|
||||
},
|
||||
},
|
||||
OPTIONS: {
|
||||
name: "Options",
|
||||
icon: "ri-list-check-2",
|
||||
type: "options",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: { allowEmpty: true },
|
||||
inclusion: [],
|
||||
},
|
||||
},
|
||||
NUMBER: {
|
||||
name: "Number",
|
||||
icon: "ri-number-1",
|
||||
|
@ -28,15 +38,6 @@ export const FIELDS = {
|
|||
presence: { allowEmpty: true },
|
||||
},
|
||||
},
|
||||
// OPTIONS: {
|
||||
// name: "Options",
|
||||
// icon: "ri-list-check-2",
|
||||
// type: "options",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: { allowEmpty: true },
|
||||
// },
|
||||
// },
|
||||
DATETIME: {
|
||||
name: "Date/Time",
|
||||
icon: "ri-calendar-event-fill",
|
||||
|
@ -60,15 +61,15 @@ export const FIELDS = {
|
|||
presence: { allowEmpty: true },
|
||||
},
|
||||
},
|
||||
// LINKED_FIELDS: {
|
||||
// name: "Linked Fields",
|
||||
// icon: "ri-link",
|
||||
// type: "link",
|
||||
// modelId: null,
|
||||
// constraints: {
|
||||
// type: "array",
|
||||
// },
|
||||
// },
|
||||
LINK: {
|
||||
name: "Relationship",
|
||||
icon: "ri-link",
|
||||
type: "link",
|
||||
constraints: {
|
||||
type: "array",
|
||||
presence: { allowEmpty: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const FILE_TYPES = {
|
||||
|
|
|
@ -709,10 +709,10 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/bbui@^1.37.1":
|
||||
version "1.37.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.37.1.tgz#a4d5aa85ee36c8013173e28879f1e8c8421f017c"
|
||||
integrity sha512-xdwwdqkpEjVYn1L38W7jR1mHkvf+xIoktoOm90LmmocJirzNo+flT4zxM2E7a3C3Bfs1l9NwEd6bqSuuxEF9Zg==
|
||||
"@budibase/bbui@^1.39.0":
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.39.0.tgz#7d3e259a60a0b4602f3d2da3452679d91a591fdd"
|
||||
integrity sha512-9IL5Lw488sdYCa9mjHHdrap11VqW6wQHNcNTL8fFHaWNzummtlaUlVMScs9cunYgsR/L4NCgH0zSFdP0RnrUqw==
|
||||
dependencies:
|
||||
sirv-cli "^0.4.6"
|
||||
svelte-flatpickr "^2.4.0"
|
||||
|
|
Loading…
Reference in New Issue