backlinks

This commit is contained in:
2024-08-16 17:38:26 +03:00
parent 9bbd53b586
commit 5a13ddb2ec
20 changed files with 60 additions and 851 deletions
@@ -121,6 +121,11 @@
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
>View trashed records</a
>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
>View unlinked records</a
>
{/if}
</Dropdown>
-19
View File
@@ -3,7 +3,6 @@
import Slug from "./elements/Slug.svelte";
import Reference from "./elements/Reference.svelte";
import ReferenceInline from "./elements/ReferenceInline.svelte";
import Block from "./block/Block.svelte";
import Color from "./elements/Color.svelte";
import Checkbox from "./elements/Checkbox.svelte";
import Number from "./elements/Number.svelte";
@@ -17,7 +16,6 @@
import Json from "./elements/JSON.svelte";
import Markdown from "./elements/Markdown.svelte";
import FieldHeader from "./elements/FieldHeader.svelte";
import ReferenceTable from "./elements/ReferenceTable.svelte";
import ReferenceTags from "./elements/ReferenceTags.svelte";
const formElements = {
@@ -56,14 +54,6 @@
{field}
{validationErrors}
/>
{:else if field.info.name === "reference" && field.layout === "table"}
<ReferenceTable
bind:graph
{id}
{record}
{field}
{validationErrors}
/>
{:else if field.info.name === "reference" && field.layout === "tags"}
<ReferenceTags
bind:graph
@@ -82,15 +72,6 @@
/>
{:else if field.info.name === "file"}
<File bind:graph {record} {field} {validationErrors}/>
{:else if field.info.name === "block"}
<Block
bind:graph
bind:value={data[field.name]}
{record}
{id}
{field}
{validationErrors}
/>
{:else if field.info.name === "text"}
<Text
bind:value={data[field.name]}
+26 -121
View File
@@ -1,15 +1,9 @@
<script>
import PreviewCardSmall from "./PreviewCardSmall.svelte";
import PreviewCard from "./PreviewCard.svelte";
import Icon from "../common/Icon.svelte";
import Preview from "../files/Preview.svelte";
import {getContext} from "svelte";
import {uniqBy} from "lodash";
import PreviewReference from "./previews/PreviewReference.svelte";
const channel = getContext("channel");
export let graph;
export let record;
function findEdgeField(schema, edgeField){
if(edgeField.includes(":")){
let edgeFieldAr = edgeField.split(":");
@@ -18,121 +12,32 @@
return schema.fields.find((f) => f.name === edgeField);
}
let parentEdgesByField = graph.parentEdges
.filter((edge) => edge.source !== record.id && edge.depth === 1)
.reduce((carry, edge) => {
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
let edgeField = findEdgeField(schema,edge.field);
let schemaField = edge.sourceSchema + edgeField;
let arecord = graph.records.find((n) => {
return n.id === edge.source;
});
if (!carry[schemaField]) {
carry[schemaField] = {
field: edgeField,
schema: schema,
nodes: [],
};
}
if (arecord) {
carry[schemaField].nodes.push(arecord);
carry[schemaField].nodes = uniqBy(carry[schemaField].nodes,"id");
}
return carry;
}, {});
let childrenEdgesByField = graph.edges
.filter((edge) => edge.source === record.id && edge.depth === 1)
.reduce((carry, edge) => {
let schema = channel.schemas.find((s) => s.name === record.schema);
let edgeField = findEdgeField(schema,edge.field);
// let schemaField = edge.targetSchema + edgeField;
let schemaField = edgeField.name + edge.targetSchema;
if (!carry[schemaField]) {
carry[schemaField] = {
field: edgeField,
nodes: [],
};
}
let arecord = graph.records.find((n) => {
return n.id === edge.target;
});
if (arecord) {
carry[schemaField].nodes.push(arecord);
carry[schemaField].nodes = uniqBy(carry[schemaField].nodes,"id");
}
return carry;
}, {});
let backlinks = graph.parentEdges.map(edge => {
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
let edgeField = findEdgeField(schema,edge.field);
return {
field: edgeField.label,
record: graph.records.find( record => record.id === edge.source)
}
})
</script>
{#each Object.entries(parentEdgesByField) as [fieldName, fieldData]}
<div class="lx-card mt-3">
<div class="text-center mb-3 d-flex justify-content-center align-items-center text-uppercase ">
<span>{fieldData.schema.label}</span>
<Icon icon="angle-right" width="12" height="12"/>
<span>{fieldData.field.label}</span>
</div>
<div class="d-flex justify-content-center text-center flex-wrap">
{#each fieldData.nodes as node}
{#if node._file?.path}
<div class="ms-2 mb-2" style="max-height:64px;">
<Preview record={node} size="small"/>
</div>
{:else}
<div class="ms-2 mb-2">
<PreviewCardSmall {graph} record={node}/>
</div>
{/if}
{/each}
</div>
<!-- <div class="text-center mt-3 d-block">{fieldData.field.label}</div>-->
{#each backlinks as backlink}
<div style="margin: 0 0 15px;position: relative;">
<span style="
font-size: 14px;
margin-bottom: 5px;
display: block;
position: absolute;
top: -9px;
left: 9px;
z-index: 9;
"
>In <i>{backlink.field}</i></span>
<PreviewReference
record={backlink.record}
hasDelete={false}
{graph}
/>
</div>
{/each}
{#if Object.entries(parentEdgesByField).length > 0}
<div class="text-center my-4">
<Icon icon="angles-down" width="32" height="32"/>
</div>
{/if}
<div style="max-width:400px;margin:0 auto;">
<PreviewCard {graph} record={record}/>
</div>
{#if Object.entries(childrenEdgesByField).length > 0}
<div class="text-center my-4">
<Icon icon="angles-down" width="32" height="32"/>
</div>
{/if}
{#each Object.entries(childrenEdgesByField) as [fieldName, fieldData]}
<div class="lx-card mt-3">
<div class="text-center mb-5 d-block">{fieldData.field.label}</div>
<div class="d-flex justify-content-center text-center flex-wrap">
{#each fieldData.nodes as node}
{#if fieldData.field.info.ui === "file"}
<div
class="ms-2 mb-2"
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
>
<Preview
record={node}
size="small"
showFilename={true}
/>
</div>
{:else}
<div class="ms-2 mb-2">
<PreviewCardSmall {graph} record={node}/>
</div>
{/if}
{/each}
</div>
</div>
{/each}
<style>
</style>
@@ -1,37 +0,0 @@
<script>
import BlockButtons from "./BlockButtons.svelte";
import BlockElements from "./BlockElements.svelte";
import {flip} from "svelte/animate";
import {quintOut} from 'svelte/easing';
import {getContext} from "svelte";
const channel = getContext("channel");
export let record;
export let field;
export let value = [];
export let graph;
let blockSchema = channel.schemas.find((s) => s.name === field.schema);
</script>
<div class=" ">
<div class="inline-card-wrapper">
<BlockButtons
bind:blockData={value}
{blockSchema}
/>
</div>
{#each value as blockItemData (blockItemData.id)}
<div class="block-field-wrapper" animate:flip="{{delay: 250, duration: 250, easing: quintOut}}">
<BlockElements
bind:block={blockItemData}
bind:blockData={value}
{record}
{field}
bind:graph
/>
</div>
{/each}
</div>
@@ -1,61 +0,0 @@
<script>
import Icon from "../../common/Icon.svelte";
import {insertBlock} from "./block";
export let blockId = "";
export let blockData;
export let blockSchema;
$: showOptions = false;
function createBlock(e, ui) {
e.preventDefault();
blockData = insertBlock(blockData,ui);
showOptions = false;
}
</script>
<div class="d-flex justify-content-left mb-2 ">
<button
type="button"
class:is-first={!blockId}
class=" btn btn-lg btn-link text-decoration-none block-buttons"
on:click|preventDefault={(e) => (showOptions = !showOptions)}
>
<Icon width={24} height={24} icon="circle-plus"/>
</button>
{#if showOptions}
<div class="d-flex ">
{#each blockSchema.fields as validUi}
<div class="ms-2">
<button
class="btn btn-sm btn-primary"
on:click={(e) => createBlock(e, validUi)}
>{validUi.label}
</button>
</div>
{/each}
</div>
{/if}
</div>
<style>
:global(.block-field-wrapper) {
display: flex;
flex-direction: column;
}
:global(.block-field-wrapper .block-buttons) {
visibility: hidden;
}
:global(.block-field-wrapper:hover .block-buttons) {
visibility: visible;
}
.block-buttons {
padding: 0px;
z-index: 1;
margin: 0px ;
}
</style>
@@ -1,158 +0,0 @@
<script>
import Heading from "./elements/Heading.svelte";
import Textarea from "./elements/Textarea.svelte";
import Rich from "./elements/Rich.svelte";
import Markdown from "./elements/Markdown.svelte";
import Reference from "./elements/Reference.svelte";
import Icon from "../../common/Icon.svelte";
import {insertBlock} from "./block";
import {getContext} from "svelte";
import {findIndex} from "lodash";
import File from "./elements/File.svelte";
const channel = getContext("channel");
export let record;
export let blockData;
export let field;
export let graph;
export let block;
let blockSchema = channel.schemas.find((s) => s.name === field.schema);
function createBlock(e, ui, blockId) {
e.preventDefault();
blockData = insertBlock(blockData, ui, blockId);
}
function deleteBlock(e, blockId) {
e.preventDefault();
blockData = blockData.filter(b => b.id !== blockId)
}
function upBlock(e, blockId) {
e.preventDefault();
let blockIndex = findIndex(blockData, (b) => b.id === blockId);
let tempBlock = blockData[blockIndex];
blockData[blockIndex] = blockData[blockIndex - 1];
blockData[blockIndex - 1] = tempBlock;
}
function downBlock(e, blockId) {
e.preventDefault();
let blockIndex = findIndex(blockData, (b) => b.id === blockId);
let tempBlock = blockData[blockIndex];
blockData[blockIndex] = blockData[blockIndex + 1];
blockData[blockIndex + 1] = tempBlock;
}
function blockIsFirst(blockId) {
return findIndex(blockData, (b) => b.id === blockId) === 0;
}
function blockIsLast(blockId) {
return findIndex(blockData, (b) => b.id === blockId) === blockData.length - 1;
}
</script>
<div class="card block-editor-field d-flex">
<div class="d-flex justify-content-between">
<span class="text-muted d-block fs-6 mb-1">{block.meta.label}</span>
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="ellipsis"/>
</button>
<div class="dropdown-menu">
<h6 class="dropdown-header">
Block id: <input class="form-control-plaintext" readonly value={block.id}/>
Block name: <input class="form-control-plaintext" readonly value={block.meta.name}/>
</h6>
<div>
<hr class="dropdown-divider">
</div>
<h6 class="dropdown-header">Actions</h6>
<button
class="dropdown-item"
class:d-none={blockIsFirst(block.id)}
on:click={(e) => upBlock(e, block.id)}
>Move up
</button>
<button
class="dropdown-item"
class:d-none={blockIsLast(block.id)}
on:click={(e) => downBlock(e, block.id)}
>Move down
</button>
<button
class="dropdown-item text-danger"
on:click={(e) => deleteBlock(e, block.id)}
>Delete
</button
>
<h6 class="dropdown-header">Insert after</h6>
{#each blockSchema.fields as blockField}
<button
class="dropdown-item"
on:click={(e) => createBlock(e, blockField, block.id)}
>{blockField.label}
</button
>
{/each}
</div>
</div>
</div>
{#if block.meta.info.name === "heading"}
<Heading
bind:block={block}
/>
{:else if block.meta.info.name === "textarea"}
<Textarea
bind:block={block}
/>
{:else if block.meta.info.name === "rich"}
<Rich
bind:block={block}
/>
{:else if block.meta.info.name === "markdown"}
<Markdown
bind:block={block}
/>
{:else if block.meta.info.name === "file"}
<File
{record}
{field}
bind:graph
bind:block={block}
/>
{:else if block.meta.info.name === "reference"}
<Reference
{record}
{field}
bind:graph
bind:block={block}
/>
{/if}
</div>
<style>
.block-editor-field{
margin: 10px 0;
border-color: transparent;
}
</style>
-25
View File
@@ -1,25 +0,0 @@
import {randomId} from "../../../helpers.js";
export function insertBlock(blockData, blockField, afterBlockId = null) {
if (!afterBlockId) {
return [{
meta: blockField,
id: randomId(),
value: null
}, ...blockData];
}
return blockData.reduce((carry, block) => {
carry.push(block)
if (block.id === afterBlockId) {
carry.push({
meta: blockField,
id: randomId(),
value: null
});
}
return carry;
}, []);
}
@@ -1,105 +0,0 @@
<script>
import {getContext} from "svelte";
import PreviewCard from "../../PreviewCard.svelte";
import {sortByField} from "../../../edges/sortEdges";
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
import Sortable from "../../../libs/Sortable.svelte";
import {insertEdges} from "../../elements/reference";
import BrowseModal from "../../elements/BrowseModal.svelte";
const channel = getContext("channel");
export let block;
export let record;
export let field;
export let graph;
let browseModal;
let blockFieldName = field.name + ":" + block.id;
$: references = graph.edges
.filter((edge) => edge.field === blockFieldName)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
block.meta.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === blockFieldName)
);
block.value = graph.edges.filter(edge => edge.field === blockFieldName) ?? [];
}
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, blockFieldName, references);
}
function insert(e) {
e.preventDefault();
browseModal.close();
graph = insertEdges(graph,record,e.detail.records,blockFieldName,e.detail.action);
}
</script>
<div class="mb-0">
{#if block.meta.collections.length === 1}
<button
class="btn btn-outline-primary"
on:click={(e) => openBrowseModal(e, collections[0].name)}
>
Browse
</button>
{:else}
<div class="dropdown d-inline-block">
<button
class="btn btn-outline-primary btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Browse
</button>
<ul class="dropdown-menu">
{#each collections as collection}
<li>
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
<a
class="dropdown-item"
on:click={(e) =>
openBrowseModal(e, collection.name)}
href="/">{collection.label}</a
>
</li>
{/each}
</ul>
</div>
{/if}
</div>
{#if references.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.id)}
<div class="col mb-3">
<PreviewCard
classes="h-100"
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
<BrowseModal bind:this={browseModal} on:insert={insert}/>
@@ -1,13 +0,0 @@
<script>
export let block;
</script>
<div class="mb-0">
<input
type="text"
id={block.id}
class="form-control"
bind:value={block.value}
autocomplete="off"
/>
</div>
@@ -1,15 +0,0 @@
<script>
import Codemirror from "../../../libs/CodemirrorMarkdown.svelte";
export let block;
// export let id;
</script>
<div class="mb-3">
<Codemirror bind:value={block.value} />
</div>
@@ -1,71 +0,0 @@
<script>
import {getContext} from "svelte";
import PreviewCard from "../../PreviewCard.svelte";
import {sortByField} from "../../../edges/sortEdges";
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
import Sortable from "../../../libs/Sortable.svelte";
import {insertEdges} from "../../elements/reference";
const channel = getContext("channel");
export let block;
export let record;
export let field;
export let graph;
let blockFieldName = field.name + ":" + block.id;
$: references = graph.edges
.filter((edge) => edge.field === blockFieldName)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
block.meta.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === blockFieldName)
);
block.value = graph.edges.filter(edge => edge.field === blockFieldName) ?? [];
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, blockFieldName, references);
}
function insert(e) {
e.preventDefault();
graph = insertEdges(graph,record,e.detail.records,blockFieldName,e.detail.action);
}
</script>
<div class="inline-card-wrapper">
<ReferenceInlineButtons
buttonClass="mt-2"
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.id)}
<div class="col mb-3">
<PreviewCard
classes="h-100"
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
@@ -1,10 +0,0 @@
<script>
import Tinymce from "../../../libs/Tinymce.svelte";
export let block;
let additionalConfig = {};
</script>
<div class="mb-0">
<Tinymce bind:value={block.value} {additionalConfig}/>
</div>
@@ -1,39 +0,0 @@
<script>
import {onMount} from "svelte";
export let block;
let thisEl;
function resize(e) {
let el;
if (e.target) {
el = e.target;
} else {
el = e;
}
el.style.overflow = "hidden";
el.style.height = "1px";
el.style.height = +el.scrollHeight + "px";
}
onMount(() => {
resize(thisEl);
});
</script>
<div class="mb-0">
<textarea
bind:value={block.value}
bind:this={thisEl}
on:input={resize}
id={block.id}
class="form-control"
autocomplete="off"></textarea>
</div>
<style>
textarea {
resize: none;
}
</style>
@@ -63,7 +63,6 @@
{#each references as reference (reference.id)}
<div>
<PreviewReference
classes="h-100"
record={reference}
hasDelete={true}
on:remove={removeReference}
@@ -1,170 +0,0 @@
<script>
import {getContext} from "svelte";
import {previewTitle} from "../Preview";
import {getErrorMessage} from "./errorMessage";
import {sortByField} from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import Sortable from "../../libs/Sortable.svelte";
import RenderField from "../../content/RenderField.svelte";
import Icon from "../../common/Icon.svelte";
import {insertEdges} from "./reference.js";
const channel = getContext("channel");
export let field;
export let record;
export let graph;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
let collection = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
)[0];
function removeReference(e, recordId) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.source === record.id && edge.target === recordId && edge.field === field.name)
);
}
function sendToTop(e, recordId) {
e.preventDefault();
let ref = graph.edges.find(
(edge) => edge.source === record.id && edge.target === recordId && edge.field === field.name
);
removeReference(e, recordId);
graph.edges = [ref, ...graph.edges];
}
function sendToBottom(e, recordId) {
e.preventDefault();
let ref = graph.edges.find(
(edge) => edge.source === record.id && edge.target === recordId && edge.field === field.name
);
removeReference(e, recordId);
graph.edges = [...graph.edges, ref];
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
}
function insert(e) {
e.preventDefault();
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
}
$:visibleColumns = [];
// $: visibleColumns = collection.fields
// .filter((f) => f.ui !== "tab" && !f.trashed)
// .filter((f) => {
// return collection.visible.includes(f.name);
// });
</script>
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
{errorMessage}
</div>
{/if}
<div class="inline-card-wrapper">
<ReferenceInlineButtons
buttonClass="mt-2"
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
<div class="lx-table rounded">
<table class="">
<thead class="table-light">
<tr>
<th/>
{#each visibleColumns as field}
<th
class="field-ui-{field.ui}"
scope="col"
title={field.help}
data-bs-toggle="tooltip"
data-bs-placement="top">{field.label}</th
>
{/each}
<th/>
</tr>
</thead>
<Sortable isTable={true} on:update={reorder}>
{#each references as record,index (record.id)}
<tr>
<td class="">
<div class="">
<div class="d-flex align-items-center">
<a
class="me-2 text-decoration-none text-dark fs-6"
href="{channel.lucentUrl}/records/{record.id}"
target="_blank"
>
{previewTitle(channel.schemas, record)}
</a>
</div>
</div>
</td>
{#each visibleColumns as field, index}
<td class="field-ui-{field.ui}">
<RenderField
{record}
{graph}
schema={collection}
{field}
/>
</td>
{/each}
<td>
<button
class="trash-button text-dark btn btn-sm btn-link"
on:click={(e) =>
removeReference(e, record.id)}
>
<Icon icon="trash-can"/>
</button>
{#if references.length > 30 && index > 0}
<button
title="Send item to top"
class="to-top-button text-dark btn btn-sm btn-link"
on:click={(e) =>
sendToTop(e, record.id)}
>
<Icon icon="circle-chevron-up"/>
</button>
{/if}
{#if references.length > 30 && index + 1 < references.length}
<button
title="Send item to bottom"
class="to-top-button text-dark btn btn-sm btn-link"
on:click={(e) =>
sendToBottom(e, record.id)}
>
<Icon icon="circle-chevron-down"/>
</button>
{/if}
</td>
</tr>
{/each}
</Sortable>
</table>
</div>
{/if}
@@ -12,7 +12,7 @@
name: "",
};
let graphTab = {
label: "Graph",
label: "Backlinks",
name: "_graph",
};
if (isCreateMode) {