2021-11-11 23:37:42 +01:00
|
|
|
<script lang="ts">
|
|
|
|
import AttributeView from "./AttributeView.svelte";
|
|
|
|
import { query, useEntity } from "../lib/entity";
|
2022-03-22 21:57:00 +01:00
|
|
|
import UpObject from "./display/UpObject.svelte";
|
2021-11-11 23:37:42 +01:00
|
|
|
import { UpType } from "../lib/types";
|
2022-02-20 18:04:47 +01:00
|
|
|
import { createEventDispatcher, setContext } from "svelte";
|
2021-12-02 23:27:09 +01:00
|
|
|
import { writable } from "svelte/store";
|
2022-03-22 21:57:00 +01:00
|
|
|
import type { UpEntry } from "upend";
|
2022-01-21 15:57:53 +01:00
|
|
|
import Spinner from "./utils/Spinner.svelte";
|
2022-01-22 20:19:26 +01:00
|
|
|
import NotesEditor from "./utils/NotesEditor.svelte";
|
|
|
|
import type { AttributeChange } from "../types/base";
|
2022-01-30 16:30:14 +01:00
|
|
|
import Selector from "./utils/Selector.svelte";
|
|
|
|
import type { IValue } from "upend/types";
|
2022-01-30 16:40:48 +01:00
|
|
|
import IconButton from "./utils/IconButton.svelte";
|
2022-02-13 15:08:09 +01:00
|
|
|
import type { BrowseContext } from "../util/browse";
|
|
|
|
import { useParams } from "svelte-navigator";
|
2022-02-20 12:09:37 +01:00
|
|
|
import { GROUP_TYPE_ADDR } from "upend/constants";
|
2023-01-07 11:00:55 +01:00
|
|
|
import {
|
|
|
|
deleteEntry,
|
|
|
|
putEntityAttribute,
|
|
|
|
putEntry,
|
|
|
|
queryOnce,
|
|
|
|
} from "../lib/api";
|
2022-02-20 18:04:47 +01:00
|
|
|
import Icon from "./utils/Icon.svelte";
|
2022-03-18 21:27:24 +01:00
|
|
|
import BlobViewer from "./display/BlobViewer.svelte";
|
2022-10-25 21:47:17 +02:00
|
|
|
import { i18n } from "../i18n";
|
2023-01-07 11:00:55 +01:00
|
|
|
import EntryList from "./widgets/EntryList.svelte";
|
2022-02-20 18:04:47 +01:00
|
|
|
const dispatch = createEventDispatcher();
|
2022-02-13 15:08:09 +01:00
|
|
|
const params = useParams();
|
2021-11-11 23:37:42 +01:00
|
|
|
|
|
|
|
export let address: string;
|
2021-12-02 23:27:09 +01:00
|
|
|
export let index: number | undefined;
|
2022-02-12 23:23:04 +01:00
|
|
|
export let detail: boolean;
|
2021-11-11 23:37:42 +01:00
|
|
|
export let editable = false;
|
|
|
|
|
2022-03-25 14:11:00 +01:00
|
|
|
let blobHandled = false;
|
|
|
|
|
2021-12-02 23:27:09 +01:00
|
|
|
let indexStore = writable(index);
|
|
|
|
$: $indexStore = index;
|
|
|
|
|
2022-02-13 15:08:09 +01:00
|
|
|
let addressesStore = writable([]);
|
|
|
|
$: $addressesStore = $params.addresses?.split(",") || [];
|
|
|
|
|
|
|
|
setContext("browse", {
|
|
|
|
index: indexStore,
|
|
|
|
addresses: addressesStore,
|
|
|
|
} as BrowseContext);
|
2021-12-02 23:27:09 +01:00
|
|
|
|
2023-01-07 11:00:55 +01:00
|
|
|
$: ({ entity, entityInfo, error, revalidate } = useEntity(address));
|
2021-11-11 23:37:42 +01:00
|
|
|
|
2021-12-19 13:54:16 +01:00
|
|
|
$: allTypeAddresses = ($entity?.attr["IS"] || []).map((attr) => attr.value.c);
|
2021-11-11 23:37:42 +01:00
|
|
|
|
|
|
|
$: allTypeEntries = query(
|
2022-04-09 19:58:44 +02:00
|
|
|
`(matches (in ${allTypeAddresses.map((addr) => `@${addr}`).join(" ")}) ? ?)`
|
2021-11-11 23:37:42 +01:00
|
|
|
).result;
|
|
|
|
|
|
|
|
let allTypes: { [key: string]: UpType } = {};
|
|
|
|
$: {
|
|
|
|
allTypes = {};
|
2021-12-19 13:54:16 +01:00
|
|
|
($allTypeEntries?.entries || []).forEach((entry) => {
|
2021-11-11 23:37:42 +01:00
|
|
|
if (allTypes[entry.entity] === undefined) {
|
|
|
|
allTypes[entry.entity] = new UpType(entry.entity);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (entry.attribute) {
|
|
|
|
case "TYPE":
|
2021-12-19 13:54:16 +01:00
|
|
|
allTypes[entry.entity].name = String(entry.value.c);
|
2021-11-11 23:37:42 +01:00
|
|
|
break;
|
2022-01-28 23:27:09 +01:00
|
|
|
case "LBL":
|
|
|
|
allTypes[entry.entity].label = String(entry.value.c);
|
|
|
|
break;
|
2021-11-11 23:37:42 +01:00
|
|
|
case "TYPE_HAS":
|
2021-12-19 13:54:16 +01:00
|
|
|
allTypes[entry.entity].attributes.push(String(entry.value.c));
|
2021-11-11 23:37:42 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
allTypes = allTypes;
|
|
|
|
}
|
|
|
|
|
2021-12-19 13:54:16 +01:00
|
|
|
let typedAttributes = {} as { [key: string]: UpEntry[] };
|
|
|
|
let untypedAttributes = [] as UpEntry[];
|
2021-11-11 23:37:42 +01:00
|
|
|
|
|
|
|
$: {
|
|
|
|
typedAttributes = {};
|
|
|
|
untypedAttributes = [];
|
|
|
|
|
2021-12-19 13:54:16 +01:00
|
|
|
($entity?.attributes || []).forEach((entry) => {
|
2021-11-11 23:37:42 +01:00
|
|
|
const entryTypes = Object.entries(allTypes).filter(([_, t]) =>
|
|
|
|
t.attributes.includes(entry.attribute)
|
|
|
|
);
|
|
|
|
if (entryTypes.length > 0) {
|
|
|
|
entryTypes.forEach(([addr, _]) => {
|
|
|
|
if (typedAttributes[addr] == undefined) {
|
|
|
|
typedAttributes[addr] = [];
|
|
|
|
}
|
2021-12-19 13:54:16 +01:00
|
|
|
typedAttributes[addr].push(entry);
|
2021-11-11 23:37:42 +01:00
|
|
|
});
|
|
|
|
} else {
|
2021-12-19 13:54:16 +01:00
|
|
|
untypedAttributes.push(entry);
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
typedAttributes = typedAttributes;
|
2022-01-21 15:57:53 +01:00
|
|
|
untypedAttributes = untypedAttributes;
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
2022-01-21 15:57:53 +01:00
|
|
|
|
|
|
|
$: filteredUntypedAttributes = untypedAttributes.filter(
|
2022-03-20 11:21:36 +01:00
|
|
|
(entry) =>
|
2022-03-22 21:57:00 +01:00
|
|
|
![
|
|
|
|
"IS",
|
|
|
|
"LBL",
|
|
|
|
"NOTE",
|
|
|
|
"LAST_VISITED",
|
|
|
|
"NUM_VISITED",
|
|
|
|
"LAST_ATTRIBUTE_WIDGET",
|
|
|
|
].includes(entry.attribute)
|
2022-01-21 15:57:53 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
$: currentUntypedAttributes = editable
|
|
|
|
? untypedAttributes
|
|
|
|
: filteredUntypedAttributes;
|
2022-01-22 20:19:26 +01:00
|
|
|
|
2022-01-28 22:39:08 +01:00
|
|
|
$: currentBacklinks =
|
|
|
|
(editable
|
|
|
|
? $entity?.backlinks
|
|
|
|
: $entity?.backlinks.filter(
|
|
|
|
(entry) => !["HAS"].includes(entry.attribute)
|
|
|
|
)) || [];
|
|
|
|
|
2022-01-28 23:32:13 +01:00
|
|
|
$: groups = ($entity?.backlinks || [])
|
2022-01-28 22:39:08 +01:00
|
|
|
.filter((e) => e.attribute === "HAS")
|
2022-01-30 16:40:48 +01:00
|
|
|
.map((e) => [e.address, e.entity])
|
2022-01-30 16:30:14 +01:00
|
|
|
.sort(); // TODO
|
2022-01-28 22:39:08 +01:00
|
|
|
|
2023-01-07 11:00:55 +01:00
|
|
|
let attributesUsed: UpEntry[] = [];
|
|
|
|
$: {
|
|
|
|
if ($entityInfo?.t === "Attribute") {
|
|
|
|
queryOnce(`(matches ? "${$entityInfo.c}" ?)`).then(
|
|
|
|
(result) => (attributesUsed = result.entries)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-22 20:19:26 +01:00
|
|
|
async function onChange(ev: CustomEvent<AttributeChange>) {
|
|
|
|
const change = ev.detail;
|
|
|
|
switch (change.type) {
|
|
|
|
case "create":
|
2022-02-20 13:06:01 +01:00
|
|
|
await putEntry({
|
|
|
|
entity: address,
|
|
|
|
attribute: change.attribute,
|
|
|
|
value: change.value,
|
2022-01-22 20:19:26 +01:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
case "delete":
|
2022-02-20 13:06:01 +01:00
|
|
|
await deleteEntry(change.address);
|
2022-01-22 20:19:26 +01:00
|
|
|
break;
|
|
|
|
case "update":
|
2022-02-20 13:06:01 +01:00
|
|
|
await putEntityAttribute(address, change.attribute, change.value);
|
2022-01-22 20:19:26 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.error("Unimplemented AttributeChange", change);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
revalidate();
|
|
|
|
}
|
2022-01-30 16:30:14 +01:00
|
|
|
|
2022-02-20 18:04:47 +01:00
|
|
|
let identities = [address];
|
|
|
|
function onResolved(ev: CustomEvent<string[]>) {
|
|
|
|
identities = ev.detail;
|
|
|
|
dispatch("resolved", ev.detail);
|
|
|
|
}
|
|
|
|
|
2022-01-30 16:30:14 +01:00
|
|
|
let groupToAdd: IValue | undefined;
|
|
|
|
async function addGroup() {
|
|
|
|
if (!groupToAdd) {
|
|
|
|
return;
|
|
|
|
}
|
2022-02-20 13:06:01 +01:00
|
|
|
await putEntry([
|
|
|
|
{
|
|
|
|
entity: String(groupToAdd.c),
|
|
|
|
attribute: "HAS",
|
|
|
|
value: {
|
|
|
|
t: "Address",
|
|
|
|
c: address,
|
2022-01-30 16:30:14 +01:00
|
|
|
},
|
2022-02-20 13:06:01 +01:00
|
|
|
},
|
|
|
|
{
|
|
|
|
entity: String(groupToAdd.c),
|
|
|
|
attribute: "IS",
|
|
|
|
value: {
|
|
|
|
t: "Address",
|
|
|
|
c: GROUP_TYPE_ADDR,
|
2022-02-20 12:09:37 +01:00
|
|
|
},
|
2022-02-20 13:06:01 +01:00
|
|
|
},
|
|
|
|
]);
|
2022-01-30 16:30:14 +01:00
|
|
|
revalidate();
|
|
|
|
groupToAdd = undefined;
|
|
|
|
}
|
2022-01-30 16:40:48 +01:00
|
|
|
|
2022-02-20 18:04:47 +01:00
|
|
|
async function removeGroup(groupAddress: string) {
|
|
|
|
await deleteEntry(groupAddress);
|
2022-01-30 16:40:48 +01:00
|
|
|
revalidate();
|
|
|
|
}
|
2022-02-20 18:04:47 +01:00
|
|
|
|
|
|
|
async function deleteObject() {
|
2022-10-25 21:47:17 +02:00
|
|
|
if (confirm(`${$i18n.t("Really delete")} "${identities.join(" | ")}"?`)) {
|
2022-02-20 18:04:47 +01:00
|
|
|
await deleteEntry(address);
|
|
|
|
dispatch("close");
|
|
|
|
}
|
|
|
|
}
|
2022-03-20 11:21:36 +01:00
|
|
|
|
2022-03-22 20:35:01 +01:00
|
|
|
async function onAttributeWidgetSwitch(ev: CustomEvent<string>) {
|
|
|
|
await putEntityAttribute(address, "LAST_ATTRIBUTE_WIDGET", {
|
|
|
|
t: "String",
|
|
|
|
c: ev.detail,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-03-20 11:21:36 +01:00
|
|
|
$: entity.subscribe(async (object) => {
|
2023-01-01 16:25:40 +01:00
|
|
|
if (object && object.listing.entries.length) {
|
2022-03-20 11:21:36 +01:00
|
|
|
await putEntityAttribute(object.address, "LAST_VISITED", {
|
|
|
|
t: "Number",
|
|
|
|
c: new Date().getTime() / 1000,
|
|
|
|
});
|
|
|
|
|
|
|
|
await putEntityAttribute(object.address, "NUM_VISITED", {
|
|
|
|
t: "Number",
|
|
|
|
c: (parseInt(String(object.get("NUM_VISITED"))) || 0) + 1,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2021-11-11 23:37:42 +01:00
|
|
|
</script>
|
|
|
|
|
2022-03-25 14:11:00 +01:00
|
|
|
<div class="inspect" class:detail class:blob={blobHandled}>
|
2022-01-28 22:39:08 +01:00
|
|
|
<header>
|
|
|
|
<h2>
|
|
|
|
{#if $entity}
|
2022-03-22 21:57:00 +01:00
|
|
|
<UpObject banner {address} on:resolved={onResolved} />
|
2022-01-28 22:39:08 +01:00
|
|
|
{:else}
|
2022-02-06 12:20:56 +01:00
|
|
|
<Spinner centered />
|
2022-01-28 22:39:08 +01:00
|
|
|
{/if}
|
|
|
|
</h2>
|
2022-02-15 23:31:17 +01:00
|
|
|
</header>
|
|
|
|
<div class="main-content">
|
2022-03-25 14:11:00 +01:00
|
|
|
<div class="detail-col">
|
|
|
|
{#if groups?.length || editable}
|
|
|
|
<section class="groups labelborder">
|
2022-10-25 21:47:17 +02:00
|
|
|
<header><h3>{$i18n.t("Groups")}</h3></header>
|
2022-03-25 14:11:00 +01:00
|
|
|
<div class="content">
|
|
|
|
{#each groups as [entryAddress, address]}
|
|
|
|
<div class="group">
|
|
|
|
<UpObject {address} link />
|
|
|
|
{#if editable}
|
|
|
|
<IconButton
|
|
|
|
name="x-circle"
|
|
|
|
on:click={() => removeGroup(entryAddress)}
|
|
|
|
/>
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
{#if editable}
|
|
|
|
<div class="selector">
|
|
|
|
<Selector
|
|
|
|
type="value"
|
|
|
|
valueTypes={["Address"]}
|
|
|
|
bind:value={groupToAdd}
|
|
|
|
on:input={addGroup}
|
|
|
|
placeholder="Choose an entity..."
|
2022-02-04 23:03:05 +01:00
|
|
|
/>
|
2022-03-25 14:11:00 +01:00
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
{/if}
|
2022-10-22 15:11:36 +02:00
|
|
|
<div class="blob-viewer">
|
|
|
|
<BlobViewer
|
|
|
|
{address}
|
|
|
|
{editable}
|
|
|
|
{detail}
|
|
|
|
on:handled={(ev) => (blobHandled = ev.detail)}
|
|
|
|
/>
|
2022-03-25 14:11:00 +01:00
|
|
|
</div>
|
|
|
|
<NotesEditor {address} {editable} on:change={onChange} />
|
|
|
|
{#if !$error}
|
|
|
|
{#if Boolean($allTypeEntries)}
|
|
|
|
<div class="attributes">
|
|
|
|
{#each Object.entries(typedAttributes) as [typeAddr, entries] (typeAddr)}
|
|
|
|
<AttributeView
|
|
|
|
{entries}
|
|
|
|
type={allTypes[typeAddr]}
|
|
|
|
{editable}
|
|
|
|
on:change={onChange}
|
|
|
|
initialWidget={String($entity.get("LAST_ATTRIBUTE_WIDGET"))}
|
|
|
|
on:widgetSwitched={onAttributeWidgetSwitch}
|
2022-01-30 16:40:48 +01:00
|
|
|
/>
|
2022-03-25 14:11:00 +01:00
|
|
|
{/each}
|
|
|
|
|
|
|
|
{#if currentUntypedAttributes.length > 0 || editable}
|
|
|
|
<AttributeView
|
2022-10-25 21:47:17 +02:00
|
|
|
title={$i18n.t("Other attributes")}
|
2022-03-25 14:11:00 +01:00
|
|
|
{editable}
|
|
|
|
entries={currentUntypedAttributes}
|
|
|
|
on:change={onChange}
|
|
|
|
/>
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
{#if currentBacklinks.length > 0}
|
|
|
|
<AttributeView
|
2022-10-25 21:47:17 +02:00
|
|
|
title={`${$i18n.t("Referred to")} (${
|
|
|
|
$entity.backlinks.length
|
|
|
|
})`}
|
2022-03-25 14:11:00 +01:00
|
|
|
entries={currentBacklinks}
|
|
|
|
reverse
|
|
|
|
on:change={onChange}
|
|
|
|
/>
|
|
|
|
{/if}
|
2023-01-07 11:00:55 +01:00
|
|
|
|
|
|
|
{#if $entityInfo?.t === "Attribute"}
|
|
|
|
<EntryList
|
|
|
|
columns="entity,value"
|
|
|
|
columnWidths={["auto", "33%"]}
|
|
|
|
entries={attributesUsed}
|
|
|
|
orderByValue
|
|
|
|
/>
|
|
|
|
{/if}
|
2022-03-25 14:11:00 +01:00
|
|
|
</div>
|
2022-02-15 23:31:17 +01:00
|
|
|
|
2022-03-25 14:11:00 +01:00
|
|
|
{#if editable}
|
|
|
|
<div class="button" on:click={deleteObject}>
|
|
|
|
<Icon name="trash" />
|
|
|
|
</div>
|
2022-02-15 23:31:17 +01:00
|
|
|
{/if}
|
2022-03-25 14:11:00 +01:00
|
|
|
{:else}
|
|
|
|
<Spinner centered />
|
2022-02-20 18:04:47 +01:00
|
|
|
{/if}
|
2022-02-15 23:31:17 +01:00
|
|
|
{:else}
|
2022-03-25 14:11:00 +01:00
|
|
|
<div class="error">
|
|
|
|
{$error}
|
|
|
|
</div>
|
2022-02-15 23:31:17 +01:00
|
|
|
{/if}
|
2022-03-25 14:11:00 +01:00
|
|
|
</div>
|
2022-02-15 23:31:17 +01:00
|
|
|
</div>
|
2021-11-11 23:37:42 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
2022-01-28 22:39:08 +01:00
|
|
|
@use "./util";
|
|
|
|
|
2022-02-15 23:31:17 +01:00
|
|
|
header h2 {
|
|
|
|
margin-bottom: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.inspect,
|
|
|
|
.main-content {
|
2021-11-30 00:29:27 +01:00
|
|
|
flex: auto;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
2022-01-28 22:39:08 +01:00
|
|
|
gap: 0.5rem;
|
2022-02-15 23:31:17 +01:00
|
|
|
min-height: 0;
|
2021-11-30 00:29:27 +01:00
|
|
|
}
|
|
|
|
|
2022-01-28 23:32:13 +01:00
|
|
|
.groups {
|
2022-01-28 22:39:08 +01:00
|
|
|
margin: 0.25rem 0;
|
2022-01-30 16:30:14 +01:00
|
|
|
.content {
|
|
|
|
display: flex;
|
|
|
|
flex-wrap: wrap;
|
|
|
|
gap: 0.5rem 0.5rem;
|
|
|
|
align-items: center;
|
|
|
|
}
|
2022-01-30 16:40:48 +01:00
|
|
|
|
|
|
|
.group {
|
|
|
|
display: inline-flex;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
|
2022-01-30 16:30:14 +01:00
|
|
|
.selector {
|
|
|
|
width: 100%;
|
|
|
|
}
|
2022-01-28 22:39:08 +01:00
|
|
|
}
|
|
|
|
|
2021-11-30 00:29:27 +01:00
|
|
|
.attributes {
|
|
|
|
flex: auto;
|
|
|
|
height: 0; // https://stackoverflow.com/a/14964944
|
2022-03-15 17:22:14 +01:00
|
|
|
min-height: 12em;
|
2021-11-30 00:29:27 +01:00
|
|
|
overflow-y: auto;
|
|
|
|
}
|
|
|
|
|
2022-02-15 23:31:17 +01:00
|
|
|
@media screen and (max-height: 1080px) {
|
|
|
|
.main-content {
|
2022-02-17 17:34:45 +01:00
|
|
|
overflow-y: auto;
|
2022-02-15 23:31:17 +01:00
|
|
|
// min-height: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.attributes {
|
|
|
|
height: unset;
|
2022-03-19 23:15:51 +01:00
|
|
|
min-height: unset;
|
2022-02-15 23:31:17 +01:00
|
|
|
overflow-y: unset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-25 14:11:00 +01:00
|
|
|
.inspect.detail {
|
2022-10-22 15:11:36 +02:00
|
|
|
.main-content {
|
|
|
|
position: relative;
|
|
|
|
flex-direction: row;
|
|
|
|
justify-content: end;
|
2022-03-25 14:11:00 +01:00
|
|
|
}
|
|
|
|
|
2022-10-22 15:11:36 +02:00
|
|
|
.blob-viewer {
|
|
|
|
width: 73%;
|
|
|
|
position: absolute;
|
|
|
|
left: 1%;
|
|
|
|
top: 0;
|
2022-03-25 14:11:00 +01:00
|
|
|
}
|
|
|
|
|
2022-10-22 15:11:36 +02:00
|
|
|
.detail-col {
|
|
|
|
width: 25%;
|
2022-03-25 14:11:00 +01:00
|
|
|
}
|
|
|
|
|
2022-10-22 15:11:36 +02:00
|
|
|
&.blob {
|
2022-03-25 14:11:00 +01:00
|
|
|
.detail-col {
|
2022-10-22 15:11:36 +02:00
|
|
|
flex-grow: 0;
|
2022-03-25 14:11:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-09 19:58:44 +02:00
|
|
|
.main-content .detail-col {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
flex-grow: 1;
|
|
|
|
}
|
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
.error {
|
|
|
|
color: red;
|
|
|
|
}
|
|
|
|
</style>
|