bidirectional linked records
This commit is contained in:
parent
2ac15c6b89
commit
430ca37826
|
@ -4,7 +4,8 @@
|
|||
import api from "builderStore/api"
|
||||
|
||||
export let modelId
|
||||
export let linkedRecords
|
||||
export let linkName
|
||||
export let linked = []
|
||||
|
||||
let records = []
|
||||
|
||||
|
@ -19,16 +20,16 @@
|
|||
})
|
||||
|
||||
function linkRecord(record) {
|
||||
linkedRecords.push(record)
|
||||
linked.push(record._id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h3>{linkName}</h3>
|
||||
{#each records as record}
|
||||
<div class="linked-record" on:click={linkRecord}>
|
||||
<h3>{record.name}</h3>
|
||||
<div class="linked-record" on:click={() => linkRecord(record)}>
|
||||
<div class="fields">
|
||||
{#each Object.keys(record) as key}
|
||||
{#each Object.keys(record).slice(0, 2) as key}
|
||||
<div class="field">
|
||||
<span>{key}</span>
|
||||
<p>{record[key]}</p>
|
||||
|
@ -40,21 +41,24 @@
|
|||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
background: var(--grey);
|
||||
padding: 20px;
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.fields {
|
||||
padding: 20px;
|
||||
padding: 15px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-gap: 20px;
|
||||
background: var(--white);
|
||||
background: var(--grey);
|
||||
border: 1px solid var(--grey);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.field:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field span {
|
||||
color: var(--ink-lighter);
|
||||
font-size: 12px;
|
||||
|
@ -64,5 +68,7 @@
|
|||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
word-break: break-word;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
|
||||
export let ids = []
|
||||
export let header
|
||||
|
||||
let records = []
|
||||
let open = false
|
||||
|
||||
async function fetchRecords() {
|
||||
const FETCH_RECORDS_URL = `/api/${$backendUiStore.selectedDatabase._id}/records/search`
|
||||
const response = await api.post(FETCH_RECORDS_URL, {
|
||||
keys: ids,
|
||||
})
|
||||
records = await response.json()
|
||||
}
|
||||
|
||||
$: ids && fetchRecords()
|
||||
|
||||
onMount(() => {
|
||||
fetchRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<a on:click={() => (open = !open)}>{records.length}</a>
|
||||
{#if open}
|
||||
<div class="popover" transition:fade>
|
||||
<h3>{header}</h3>
|
||||
{#each records as record}
|
||||
<div class="linked-record">
|
||||
<div class="fields">
|
||||
{#each Object.keys(record).slice(0, 2) as key}
|
||||
<div class="field">
|
||||
<span>{key}</span>
|
||||
<p>{record[key]}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
a {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.popover {
|
||||
position: absolute;
|
||||
right: 15%;
|
||||
padding: 20px;
|
||||
background: var(--light-grey);
|
||||
border: 1px solid var(--grey);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.field: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>
|
|
@ -4,6 +4,7 @@
|
|||
import { Button } from "@budibase/bbui"
|
||||
import Select from "components/common/Select.svelte"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import LinkedRecord from "./LinkedRecord.svelte";
|
||||
import TablePagination from "./TablePagination.svelte"
|
||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
||||
import * as api from "./api"
|
||||
|
@ -41,6 +42,7 @@
|
|||
let headers = []
|
||||
let views = []
|
||||
let currentPage = 0
|
||||
let search
|
||||
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
|
@ -91,6 +93,10 @@
|
|||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="search">
|
||||
<i class="ri-search-line"></i>
|
||||
<input placeholder="Search" class="budibase__input" bind:value={search} />
|
||||
</div>
|
||||
<table class="uk-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -130,7 +136,13 @@
|
|||
</div>
|
||||
</td>
|
||||
{#each headers as header}
|
||||
<td>{row[header]}</td>
|
||||
<td>
|
||||
{#if Array.isArray(row[header])}
|
||||
<LinkedRecord {header} ids={row[header]} />
|
||||
{:else}
|
||||
{row[header] || 0}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
|
|
|
@ -15,7 +15,7 @@ export async function createDatabase(appname, instanceName) {
|
|||
}
|
||||
|
||||
export async function deleteRecord(record, instanceId) {
|
||||
const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}`
|
||||
const DELETE_RECORDS_URL = `/api/${instanceId}/${record.modelId}/records/${record._id}/${record._rev}`
|
||||
const response = await api.delete(DELETE_RECORDS_URL)
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
{#each modelSchema as [key, meta]}
|
||||
<div class="uk-margin">
|
||||
{#if meta.type === 'link'}
|
||||
<LinkedRecordSelector modelId={meta.modelId} />
|
||||
<LinkedRecordSelector bind:linked={record[key]} linkName={key} modelId={meta.modelId} />
|
||||
{:else}
|
||||
<RecordFieldControl
|
||||
type={determineInputType(meta)}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import * as api from "../api"
|
||||
|
||||
|
@ -26,6 +27,7 @@
|
|||
alert
|
||||
on:click={async () => {
|
||||
await api.deleteRecord(record, instanceId)
|
||||
notifier.danger("Record deleted")
|
||||
backendUiStore.actions.records.delete(record)
|
||||
onClosed()
|
||||
}}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import * as blockDefinitions from "constants/backend"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { backendUiStore } from "builderStore";
|
||||
import Block from "components/common/Block.svelte"
|
||||
|
||||
const HEADINGS = [
|
||||
|
@ -11,11 +11,7 @@
|
|||
{
|
||||
title: "Blocks",
|
||||
key: "BLOCKS",
|
||||
},
|
||||
{
|
||||
title: "Model",
|
||||
key: "MODELS",
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
let selectedTab = "FIELDS"
|
||||
|
|
|
@ -7,6 +7,18 @@
|
|||
function addNewField(field) {
|
||||
backendUiStore.actions.models.addField(field)
|
||||
}
|
||||
|
||||
function createModel(model) {
|
||||
const { schema, ...rest } = $backendUiStore.selectedModel
|
||||
|
||||
backendUiStore.actions.models.save({
|
||||
model: {
|
||||
...model,
|
||||
...rest
|
||||
},
|
||||
instanceId: $backendUiStore.selectedDatabase._id
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<section transition:fade>
|
||||
|
@ -48,7 +60,7 @@
|
|||
<p>Blocks are pre-made fields and help you build your model quicker.</p>
|
||||
<div class="blocks">
|
||||
{#each Object.values(MODELS) as model}
|
||||
<Block tertiary title={model.name} icon={model.icon} />
|
||||
<Block tertiary title={model.name} icon={model.icon} on:click={() => createModel(model)}/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,15 +28,15 @@ export const FIELDS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
OPTIONS: {
|
||||
name: "Options",
|
||||
icon: "ri-list-check-2",
|
||||
type: "options",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
// OPTIONS: {
|
||||
// name: "Options",
|
||||
// icon: "ri-list-check-2",
|
||||
// type: "options",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: false,
|
||||
// },
|
||||
// },
|
||||
DATETIME: {
|
||||
name: "Date/Time",
|
||||
icon: "ri-calendar-event-fill",
|
||||
|
@ -47,24 +47,24 @@ export const FIELDS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
IMAGE: {
|
||||
name: "File",
|
||||
icon: "ri-image-line",
|
||||
type: "file",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
FILE: {
|
||||
name: "Image",
|
||||
icon: "ri-file-line",
|
||||
type: "file",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
// IMAGE: {
|
||||
// name: "File",
|
||||
// icon: "ri-image-line",
|
||||
// type: "file",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: false,
|
||||
// },
|
||||
// },
|
||||
// FILE: {
|
||||
// name: "Image",
|
||||
// icon: "ri-file-line",
|
||||
// type: "file",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: false,
|
||||
// },
|
||||
// },
|
||||
DATA_LINK: {
|
||||
name: "Data Links",
|
||||
icon: "ri-link",
|
||||
|
@ -106,16 +106,16 @@ export const BLOCKS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
PRIORITY: {
|
||||
name: "Options",
|
||||
icon: "ri-list-check-2",
|
||||
type: "options",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: false,
|
||||
inclusion: ["low", "medium", "high"],
|
||||
},
|
||||
},
|
||||
// PRIORITY: {
|
||||
// name: "Options",
|
||||
// icon: "ri-list-check-2",
|
||||
// type: "options",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: false,
|
||||
// inclusion: ["low", "medium", "high"],
|
||||
// },
|
||||
// },
|
||||
END_DATE: {
|
||||
name: "End Date",
|
||||
icon: "ri-calendar-event-fill",
|
||||
|
@ -126,39 +126,29 @@ export const BLOCKS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
AVATAR: {
|
||||
name: "Avatar",
|
||||
icon: "ri-image-line",
|
||||
type: "image",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
PDF: {
|
||||
name: "PDF",
|
||||
icon: "ri-file-line",
|
||||
type: "file",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
DATA_LINK: {
|
||||
name: "Data Links",
|
||||
icon: "ri-link",
|
||||
type: "link",
|
||||
modelId: null,
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
// AVATAR: {
|
||||
// name: "Avatar",
|
||||
// icon: "ri-image-line",
|
||||
// type: "image",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: false,
|
||||
// },
|
||||
// },
|
||||
// PDF: {
|
||||
// name: "PDF",
|
||||
// icon: "ri-file-line",
|
||||
// type: "file",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: false,
|
||||
// },
|
||||
// },
|
||||
}
|
||||
|
||||
// TODO: Needs more thought, need to come up with the constraints etc for each one
|
||||
export const MODELS = {
|
||||
CONTACTS: {
|
||||
icon: "ri-link",
|
||||
icon: "ri-contacts-book-line",
|
||||
name: "Contacts",
|
||||
schema: {
|
||||
Name: BLOCKS.NAME,
|
||||
|
@ -170,7 +160,21 @@ export const MODELS = {
|
|||
name: "Recipes",
|
||||
schema: {
|
||||
Name: BLOCKS.NAME,
|
||||
"Phone Number": BLOCKS.PHONE_NUMBER,
|
||||
Cuisine: {
|
||||
...FIELDS.PLAIN_TEXT,
|
||||
name: "Cuisine"
|
||||
},
|
||||
},
|
||||
},
|
||||
SPORTS_TEAM: {
|
||||
icon: "ri-basketball-line",
|
||||
name: "Sports Team",
|
||||
schema: {
|
||||
Name: BLOCKS.NAME,
|
||||
Championships: {
|
||||
...FIELDS.NUMBER,
|
||||
name: "Championships"
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ exports.save = async function(ctx) {
|
|||
// create the link field in the other model
|
||||
const linkedModel = await db.get(schema[key].modelId)
|
||||
linkedModel.schema[modelToSave.name] = {
|
||||
name: modelToSave.name,
|
||||
type: "link",
|
||||
modelId: modelToSave._id,
|
||||
constraints: {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const CouchDB = require("../../db")
|
||||
const validateJs = require("validate.js")
|
||||
const newid = require("../../db/newid")
|
||||
const { link } = require("pouchdb-adapter-memory")
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
|
@ -43,6 +44,28 @@ exports.save = async function(ctx) {
|
|||
const response = await db.post(record)
|
||||
record._rev = response.rev
|
||||
|
||||
// create links in other tables
|
||||
for (let key in record) {
|
||||
// link
|
||||
if (Array.isArray(record[key])) {
|
||||
const linked = await db.allDocs({
|
||||
include_docs: true,
|
||||
keys: record[key],
|
||||
})
|
||||
|
||||
// add this record to the linked records in attached models
|
||||
const linkedDocs = linked.rows.map(row => {
|
||||
const doc = row.doc
|
||||
return {
|
||||
...doc,
|
||||
[model.name]: doc[model.name] ? [...doc[model.name], record._id] : [record._id]
|
||||
}
|
||||
})
|
||||
|
||||
await db.bulkDocs(linkedDocs)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emit(`record:save`, {
|
||||
record,
|
||||
|
|
Loading…
Reference in New Issue