improvements to linked records
This commit is contained in:
parent
6adb2a72e0
commit
afcc80ab9d
|
@ -58,7 +58,7 @@ export const getBackendUiStore = () => {
|
||||||
state.tabs.SETUP_PANEL = "SETUP"
|
state.tabs.SETUP_PANEL = "SETUP"
|
||||||
return state
|
return state
|
||||||
}),
|
}),
|
||||||
save: async ({ instanceId, model }) => {
|
save: async ({ model }) => {
|
||||||
const updatedModel = cloneDeep(model)
|
const updatedModel = cloneDeep(model)
|
||||||
|
|
||||||
// TODO: refactor
|
// TODO: refactor
|
||||||
|
@ -70,7 +70,7 @@ export const getBackendUiStore = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SAVE_MODEL_URL = `/api/${instanceId}/models`
|
const SAVE_MODEL_URL = `/api/models`
|
||||||
const response = await api.post(SAVE_MODEL_URL, updatedModel)
|
const response = await api.post(SAVE_MODEL_URL, updatedModel)
|
||||||
const savedModel = await response.json()
|
const savedModel = await response.json()
|
||||||
|
|
||||||
|
@ -86,6 +86,8 @@ export const getBackendUiStore = () => {
|
||||||
state.models = state.models
|
state.models = state.models
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: fetch models
|
||||||
|
|
||||||
store.actions.models.select(savedModel)
|
store.actions.models.select(savedModel)
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,8 +9,13 @@
|
||||||
|
|
||||||
let records = []
|
let records = []
|
||||||
|
|
||||||
|
let linkedRecords = new Set(linked)
|
||||||
|
|
||||||
|
$: linked = [...linkedRecords]
|
||||||
|
$: FIELDS_TO_HIDE = ["modelId", "type", "_id", "_rev", $backendUiStore.selectedModel.name]
|
||||||
|
|
||||||
async function fetchRecords() {
|
async function fetchRecords() {
|
||||||
const FETCH_RECORDS_URL = `/api/${$backendUiStore.selectedDatabase._id}/${modelId}/records`
|
const FETCH_RECORDS_URL = `/api/${modelId}/records`
|
||||||
const response = await api.get(FETCH_RECORDS_URL)
|
const response = await api.get(FETCH_RECORDS_URL)
|
||||||
records = await response.json()
|
records = await response.json()
|
||||||
}
|
}
|
||||||
|
@ -19,17 +24,25 @@
|
||||||
fetchRecords()
|
fetchRecords()
|
||||||
})
|
})
|
||||||
|
|
||||||
function linkRecord(record) {
|
function linkRecord(id) {
|
||||||
linked.push(record._id)
|
if (linkedRecords.has(id)) {
|
||||||
|
linkedRecords.delete(id);
|
||||||
|
} else {
|
||||||
|
linkedRecords.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedRecords = linkedRecords
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
<header>
|
||||||
<h3>{linkName}</h3>
|
<h3>{linkName}</h3>
|
||||||
|
</header>
|
||||||
{#each records as record}
|
{#each records as record}
|
||||||
<div class="linked-record" on:click={() => linkRecord(record)}>
|
<div class="linked-record" on:click={() => linkRecord(record._id)}>
|
||||||
<div class="fields">
|
<div class="fields" class:selected={linkedRecords.has(record._id)}>
|
||||||
{#each Object.keys(record).slice(0, 2) as key}
|
{#each Object.keys(record).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span>{key}</span>
|
<span>{key}</span>
|
||||||
<p>{record[key]}</p>
|
<p>{record[key]}</p>
|
||||||
|
@ -41,8 +54,15 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.fields.selected {
|
||||||
|
background: var(--light-grey);
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
|
@ -50,12 +70,14 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
background: var(--grey);
|
background: var(--white);
|
||||||
border: 1px solid var(--grey);
|
border: 1px solid var(--grey);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
transition: 0.5s all;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field:hover {
|
.fields:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,36 +4,45 @@
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
|
||||||
export let ids = []
|
export let ids = []
|
||||||
export let header
|
export let header
|
||||||
|
|
||||||
let records = []
|
let records = []
|
||||||
let open = false
|
let open = false
|
||||||
|
|
||||||
|
$: FIELDS_TO_HIDE = ["modelId", "type", "_id", "_rev", $backendUiStore.selectedModel.name]
|
||||||
|
|
||||||
async function fetchRecords() {
|
async function fetchRecords() {
|
||||||
const FETCH_RECORDS_URL = `/api/${$backendUiStore.selectedDatabase._id}/records/search`
|
const response = await api.post("/api/records/search", {
|
||||||
const response = await api.post(FETCH_RECORDS_URL, {
|
keys: ids
|
||||||
keys: ids,
|
|
||||||
})
|
})
|
||||||
records = await response.json()
|
records = await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
$: ids && fetchRecords()
|
$: ids && fetchRecords()
|
||||||
|
|
||||||
|
function toggleOpen() {
|
||||||
|
open = !open
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchRecords()
|
fetchRecords()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<a on:click={() => (open = !open)}>{records.length}</a>
|
<a on:click={toggleOpen}>{records.length}</a>
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="popover" transition:fade>
|
<div class="popover" transition:fade>
|
||||||
|
<header>
|
||||||
<h3>{header}</h3>
|
<h3>{header}</h3>
|
||||||
|
<i class="ri-close-circle-fill" on:click={toggleOpen} />
|
||||||
|
</header>
|
||||||
{#each records as record}
|
{#each records as record}
|
||||||
<div class="linked-record">
|
<div class="linked-record">
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each Object.keys(record).slice(0, 2) as key}
|
{#each Object.keys(record).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span>{key}</span>
|
<span>{key}</span>
|
||||||
<p>{record[key]}</p>
|
<p>{record[key]}</p>
|
||||||
|
@ -47,11 +56,29 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--ink-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
i:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
|
width: 500px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 15%;
|
right: 15%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
@ -61,6 +88,8 @@
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
|
@ -71,10 +100,7 @@
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border: 1px solid var(--grey);
|
border: 1px solid var(--grey);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.field:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.field span {
|
.field span {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
import { notifier } from "@beyonk/svelte-notifications"
|
import { notifier } from "@beyonk/svelte-notifications"
|
||||||
import { compose, map, get, flatten } from "lodash/fp"
|
import { compose, map, get, flatten } from "lodash/fp"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import { Button } from "@budibase/bbui"
|
||||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||||
import Select from "components/common/Select.svelte"
|
import Select from "components/common/Select.svelte"
|
||||||
import RecordFieldControl from "./RecordFieldControl.svelte"
|
import RecordFieldControl from "./RecordFieldControl.svelte"
|
||||||
|
@ -68,13 +68,19 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
<header>
|
||||||
|
<i class="ri-file-user-fill" />
|
||||||
<h4 class="budibase__title--4">Create / Edit Record</h4>
|
<h4 class="budibase__title--4">Create / Edit Record</h4>
|
||||||
|
</header>
|
||||||
<ErrorsBox {errors} />
|
<ErrorsBox {errors} />
|
||||||
<form on:submit|preventDefault class="uk-form-stacked">
|
<form on:submit|preventDefault class="uk-form-stacked">
|
||||||
{#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 bind:linked={record[key]} linkName={key} modelId={meta.modelId} />
|
<LinkedRecordSelector
|
||||||
|
bind:linked={record[key]}
|
||||||
|
linkName={key}
|
||||||
|
modelId={meta.modelId} />
|
||||||
{:else}
|
{:else}
|
||||||
<RecordFieldControl
|
<RecordFieldControl
|
||||||
type={determineInputType(meta)}
|
type={determineInputType(meta)}
|
||||||
|
@ -87,14 +93,44 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
<ActionButton on:click={saveRecord}>Save</ActionButton>
|
<Button attention on:click={saveRecord}>Save</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 5px;
|
||||||
|
grid-template-columns: 40px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--secondary);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--ink);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
|
|
|
@ -51,3 +51,16 @@
|
||||||
on:input={handleInput}
|
on:input={handleInput}
|
||||||
on:change={handleInput} />
|
on:change={handleInput} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
color: var(--dark-grey);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -40,7 +40,7 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const DELETE_MODEL_URL = `/api/${instanceId}/models/${model._id}/${model._rev}`
|
const DELETE_MODEL_URL = `/api/models/${model._id}/${model._rev}`
|
||||||
const response = await api.delete(DELETE_MODEL_URL)
|
const response = await api.delete(DELETE_MODEL_URL)
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
state.selectedView = null
|
state.selectedView = null
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import { store, backendUiStore } from "builderStore"
|
|
||||||
import HierarchyRow from "./HierarchyRow.svelte"
|
|
||||||
import NavItem from "./NavItem.svelte"
|
|
||||||
import getIcon from "components/common/icon"
|
|
||||||
import api from "builderStore/api"
|
|
||||||
import {
|
|
||||||
CreateEditModelModal,
|
|
||||||
CreateEditViewModal,
|
|
||||||
} from "components/database/ModelDataTable/modals"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
|
||||||
|
|
||||||
function editModel() {
|
|
||||||
open(
|
|
||||||
CreateEditModelModal,
|
|
||||||
{
|
|
||||||
model: node,
|
|
||||||
onClosed: close,
|
|
||||||
},
|
|
||||||
{ styleContent: { padding: "0" } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function newModel() {
|
|
||||||
open(
|
|
||||||
CreateEditModelModal,
|
|
||||||
{
|
|
||||||
onClosed: close,
|
|
||||||
},
|
|
||||||
{ styleContent: { padding: "0" } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function newView() {
|
|
||||||
open(
|
|
||||||
CreateEditViewModal,
|
|
||||||
{
|
|
||||||
onClosed: close,
|
|
||||||
},
|
|
||||||
{ styleContent: { padding: "0" } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectModel(model) {
|
|
||||||
backendUiStore.update(state => {
|
|
||||||
state.selectedModel = model
|
|
||||||
state.selectedView = `all_${model._id}`
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteModel(modelToDelete) {
|
|
||||||
const DELETE_MODEL_URL = `/api/models/${node._id}/${node._rev}`
|
|
||||||
const response = await api.delete(DELETE_MODEL_URL)
|
|
||||||
backendUiStore.update(state => {
|
|
||||||
state.models = state.models.filter(
|
|
||||||
model => model._id !== modelToDelete._id
|
|
||||||
)
|
|
||||||
state.selectedView = {}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectView(view) {
|
|
||||||
backendUiStore.update(state => {
|
|
||||||
state.selectedView = view.name
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="items-root">
|
|
||||||
<div class="hierarchy">
|
|
||||||
<div class="components-list-container">
|
|
||||||
<div class="nav-group-header">
|
|
||||||
<div class="hierarchy-title">Models</div>
|
|
||||||
<div class="uk-inline">
|
|
||||||
<i class="ri-add-line hoverable" on:click={newModel} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hierarchy-items-container">
|
|
||||||
{#each $backendUiStore.models as model}
|
|
||||||
<HierarchyRow onSelect={selectModel} node={model} type="model" />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hierarchy">
|
|
||||||
<div class="components-list-container">
|
|
||||||
<div class="nav-group-header">
|
|
||||||
<div class="hierarchy-title">Views</div>
|
|
||||||
<div class="uk-inline">
|
|
||||||
<i class="ri-add-line hoverable" on:click={newView} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hierarchy-items-container">
|
|
||||||
{#each $backendUiStore.views as view}
|
|
||||||
<HierarchyRow onSelect={selectView} node={view} type="view" />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.items-root {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-group-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px 20px 10px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-title {
|
|
||||||
align-items: center;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-items-container {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -85,42 +85,10 @@
|
||||||
|
|
||||||
dimensions = {top, left}
|
dimensions = {top, left}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
function openColorpicker(event) {
|
function onColorChange(color) {
|
||||||
if (colorPreview) {
|
value = color.detail;
|
||||||
const {
|
dispatch("change", color.detail)
|
||||||
top: spaceAbove,
|
|
||||||
width,
|
|
||||||
bottom,
|
|
||||||
right,
|
|
||||||
left: spaceLeft,
|
|
||||||
} = colorPreview.getBoundingClientRect()
|
|
||||||
const { innerHeight, innerWidth } = window
|
|
||||||
|
|
||||||
const { offsetLeft, offsetTop } = colorPreview
|
|
||||||
//get the scrollTop value for all scrollable parent elements
|
|
||||||
let scrollTop = parentNodes.reduce(
|
|
||||||
(scrollAcc, el) => (scrollAcc += el.scrollTop),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
const spaceBelow = innerHeight - spaceAbove - previewHeight
|
|
||||||
const top =
|
|
||||||
spaceAbove > spaceBelow
|
|
||||||
? offsetTop - pickerHeight - scrollTop
|
|
||||||
: offsetTop + previewHeight - scrollTop
|
|
||||||
|
|
||||||
//TOO: Testing and Scroll Awareness for x Scroll
|
|
||||||
const spaceRight = innerWidth - spaceLeft + previewWidth
|
|
||||||
const left =
|
|
||||||
spaceRight > spaceLeft
|
|
||||||
? offsetLeft + previewWidth + pickerWidth
|
|
||||||
: offsetLeft - pickerWidth
|
|
||||||
|
|
||||||
dimensions = { top, left }
|
|
||||||
|
|
||||||
open = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -142,13 +110,9 @@
|
||||||
<span>×</span>
|
<span>×</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
|
||||||
<div class="color-preview preview-error" style={errorPreviewStyle}>
|
|
||||||
<span>×</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.color-preview-container{
|
.color-preview-container{
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
Loading…
Reference in New Issue