Add WIP draft of linked records UI allowing single linked record selection
This commit is contained in:
parent
4073f354c8
commit
e4ac832c32
|
@ -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",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue