1
0
Fork 0
mirror of synced 2024-10-01 09:38:55 +13:00

bidirectional linked records

This commit is contained in:
Martin McKeaveney 2020-06-22 21:30:23 +01:00
parent 099e394270
commit 1b1b804bbd
12 changed files with 235 additions and 87 deletions

View file

@ -4,7 +4,8 @@
import api from "builderStore/api" import api from "builderStore/api"
export let modelId export let modelId
export let linkedRecords export let linkName
export let linked = []
let records = [] let records = []
@ -19,16 +20,16 @@
}) })
function linkRecord(record) { function linkRecord(record) {
linkedRecords.push(record) linked.push(record._id)
} }
</script> </script>
<section> <section>
<h3>{linkName}</h3>
{#each records as record} {#each records as record}
<div class="linked-record" on:click={linkRecord}> <div class="linked-record" on:click={() => linkRecord(record)}>
<h3>{record.name}</h3>
<div class="fields"> <div class="fields">
{#each Object.keys(record) as key} {#each Object.keys(record).slice(0, 2) as key}
<div class="field"> <div class="field">
<span>{key}</span> <span>{key}</span>
<p>{record[key]}</p> <p>{record[key]}</p>
@ -40,21 +41,24 @@
</section> </section>
<style> <style>
section { h3 {
background: var(--grey); font-size: 20px;
padding: 20px;
} }
.fields { .fields {
padding: 20px; padding: 15px;
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px; grid-gap: 20px;
background: var(--white); background: var(--grey);
border: 1px solid var(--grey); border: 1px solid var(--grey);
border-radius: 5px; border-radius: 5px;
} }
.field:hover {
cursor: pointer;
}
.field span { .field span {
color: var(--ink-lighter); color: var(--ink-lighter);
font-size: 12px; font-size: 12px;
@ -64,5 +68,7 @@
color: var(--ink); color: var(--ink);
font-size: 14px; font-size: 14px;
word-break: break-word; word-break: break-word;
font-weight: 500;
margin-top: 4px;
} }
</style> </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 { Button } from "@budibase/bbui"
import Select from "components/common/Select.svelte" import Select from "components/common/Select.svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte";
import TablePagination from "./TablePagination.svelte" import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals" import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import * as api from "./api" import * as api from "./api"
@ -41,6 +42,7 @@
let headers = [] let headers = []
let views = [] let views = []
let currentPage = 0 let currentPage = 0
let search
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
@ -91,6 +93,10 @@
</span> </span>
</Button> </Button>
</div> </div>
<div class="search">
<i class="ri-search-line"></i>
<input placeholder="Search" class="budibase__input" bind:value={search} />
</div>
<table class="uk-table"> <table class="uk-table">
<thead> <thead>
<tr> <tr>
@ -130,7 +136,13 @@
</div> </div>
</td> </td>
{#each headers as header} {#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} {/each}
</tr> </tr>
{/each} {/each}

View file

@ -15,7 +15,7 @@ export async function createDatabase(appname, instanceName) {
} }
export async function deleteRecord(record, instanceId) { 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) const response = await api.delete(DELETE_RECORDS_URL)
return response return response
} }

View file

@ -77,7 +77,7 @@
{#each modelSchema as [key, meta]} {#each modelSchema as [key, meta]}
<div class="uk-margin"> <div class="uk-margin">
{#if meta.type === 'link'} {#if meta.type === 'link'}
<LinkedRecordSelector modelId={meta.modelId} /> <LinkedRecordSelector bind:linked={record[key]} linkName={key} modelId={meta.modelId} />
{:else} {:else}
<RecordFieldControl <RecordFieldControl
type={determineInputType(meta)} type={determineInputType(meta)}

View file

@ -1,5 +1,6 @@
<script> <script>
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import { notifier } from "@beyonk/svelte-notifications"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import * as api from "../api" import * as api from "../api"
@ -26,6 +27,7 @@
alert alert
on:click={async () => { on:click={async () => {
await api.deleteRecord(record, instanceId) await api.deleteRecord(record, instanceId)
notifier.danger("Record deleted")
backendUiStore.actions.records.delete(record) backendUiStore.actions.records.delete(record)
onClosed() onClosed()
}}> }}>

View file

@ -1,6 +1,6 @@
<script> <script>
import * as blockDefinitions from "constants/backend" import * as blockDefinitions from "constants/backend"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore";
import Block from "components/common/Block.svelte" import Block from "components/common/Block.svelte"
const HEADINGS = [ const HEADINGS = [
@ -11,11 +11,7 @@
{ {
title: "Blocks", title: "Blocks",
key: "BLOCKS", key: "BLOCKS",
}, }
{
title: "Model",
key: "MODELS",
},
] ]
let selectedTab = "FIELDS" let selectedTab = "FIELDS"

View file

@ -7,6 +7,18 @@
function addNewField(field) { function addNewField(field) {
backendUiStore.actions.models.addField(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> </script>
<section transition:fade> <section transition:fade>
@ -48,7 +60,7 @@
<p>Blocks are pre-made fields and help you build your model quicker.</p> <p>Blocks are pre-made fields and help you build your model quicker.</p>
<div class="blocks"> <div class="blocks">
{#each Object.values(MODELS) as model} {#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} {/each}
</div> </div>
</div> </div>

View file

@ -28,15 +28,15 @@ export const FIELDS = {
presence: false, presence: false,
}, },
}, },
OPTIONS: { // OPTIONS: {
name: "Options", // name: "Options",
icon: "ri-list-check-2", // icon: "ri-list-check-2",
type: "options", // type: "options",
constraints: { // constraints: {
type: "string", // type: "string",
presence: false, // presence: false,
}, // },
}, // },
DATETIME: { DATETIME: {
name: "Date/Time", name: "Date/Time",
icon: "ri-calendar-event-fill", icon: "ri-calendar-event-fill",
@ -47,24 +47,24 @@ export const FIELDS = {
presence: false, presence: false,
}, },
}, },
IMAGE: { // IMAGE: {
name: "File", // name: "File",
icon: "ri-image-line", // icon: "ri-image-line",
type: "file", // type: "file",
constraints: { // constraints: {
type: "string", // type: "string",
presence: false, // presence: false,
}, // },
}, // },
FILE: { // FILE: {
name: "Image", // name: "Image",
icon: "ri-file-line", // icon: "ri-file-line",
type: "file", // type: "file",
constraints: { // constraints: {
type: "string", // type: "string",
presence: false, // presence: false,
}, // },
}, // },
DATA_LINK: { DATA_LINK: {
name: "Data Links", name: "Data Links",
icon: "ri-link", icon: "ri-link",
@ -106,16 +106,16 @@ export const BLOCKS = {
presence: false, presence: false,
}, },
}, },
PRIORITY: { // PRIORITY: {
name: "Options", // name: "Options",
icon: "ri-list-check-2", // icon: "ri-list-check-2",
type: "options", // type: "options",
constraints: { // constraints: {
type: "string", // type: "string",
presence: false, // presence: false,
inclusion: ["low", "medium", "high"], // inclusion: ["low", "medium", "high"],
}, // },
}, // },
END_DATE: { END_DATE: {
name: "End Date", name: "End Date",
icon: "ri-calendar-event-fill", icon: "ri-calendar-event-fill",
@ -126,39 +126,29 @@ export const BLOCKS = {
presence: false, presence: false,
}, },
}, },
AVATAR: { // AVATAR: {
name: "Avatar", // name: "Avatar",
icon: "ri-image-line", // icon: "ri-image-line",
type: "image", // type: "image",
constraints: { // constraints: {
type: "string", // type: "string",
presence: false, // presence: false,
}, // },
}, // },
PDF: { // PDF: {
name: "PDF", // name: "PDF",
icon: "ri-file-line", // icon: "ri-file-line",
type: "file", // type: "file",
constraints: { // constraints: {
type: "string", // type: "string",
presence: false, // presence: false,
}, // },
}, // },
DATA_LINK: {
name: "Data Links",
icon: "ri-link",
type: "link",
modelId: null,
constraints: {
type: "array",
},
},
} }
// TODO: Needs more thought, need to come up with the constraints etc for each one
export const MODELS = { export const MODELS = {
CONTACTS: { CONTACTS: {
icon: "ri-link", icon: "ri-contacts-book-line",
name: "Contacts", name: "Contacts",
schema: { schema: {
Name: BLOCKS.NAME, Name: BLOCKS.NAME,
@ -170,7 +160,21 @@ export const MODELS = {
name: "Recipes", name: "Recipes",
schema: { schema: {
Name: BLOCKS.NAME, 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 // create the link field in the other model
const linkedModel = await db.get(schema[key].modelId) const linkedModel = await db.get(schema[key].modelId)
linkedModel.schema[modelToSave.name] = { linkedModel.schema[modelToSave.name] = {
name: modelToSave.name,
type: "link", type: "link",
modelId: modelToSave._id, modelId: modelToSave._id,
constraints: { constraints: {

View file

@ -1,6 +1,7 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const validateJs = require("validate.js") const validateJs = require("validate.js")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const { link } = require("pouchdb-adapter-memory")
exports.save = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
@ -43,6 +44,28 @@ exports.save = async function(ctx) {
const response = await db.post(record) const response = await db.post(record)
record._rev = response.rev 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 &&
ctx.eventEmitter.emit(`record:save`, { ctx.eventEmitter.emit(`record:save`, {
record, record,