bidirectional linked records

This commit is contained in:
Martin McKeaveney 2020-06-22 21:30:23 +01:00
parent 2ac15c6b89
commit 430ca37826
12 changed files with 235 additions and 87 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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
}

View File

@ -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)}

View File

@ -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()
}}>

View File

@ -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"

View File

@ -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>

View File

@ -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"
}
},
},
}

View File

@ -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: {

View File

@ -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,