wip: get rid of types, new EntryVIew

feat/type-attributes
Tomáš Mládek 2023-06-16 16:30:17 +02:00
parent e428ef188f
commit e496710e20
6 changed files with 211 additions and 345 deletions

View File

@ -1,22 +1,31 @@
<script lang="ts" context="module">
export interface WidgetComponent {
component: ComponentType;
props: { [key: string]: unknown };
}
export interface Widget {
name: string;
icon?: string;
components: (entries: UpEntry[]) => Array<WidgetComponent>;
}
</script>
<script lang="ts">
import UpLink from "./display/UpLink.svelte";
import type { Component, UpType, Widget } from "../lib/types";
import EntryList from "./widgets/EntryList.svelte";
import type { UpEntry } from "upend";
import Icon from "./utils/Icon.svelte";
import IconButton from "./utils/IconButton.svelte";
import { createEventDispatcher } from "svelte";
import { createEventDispatcher, type ComponentType } from "svelte";
const dispatch = createEventDispatcher();
import { i18n } from "../i18n";
export let entries: UpEntry[];
export let type: UpType | undefined = undefined;
export let widgets: Widget[] | undefined = undefined;
export let title: string | undefined = undefined;
export let editable = false;
export let reverse = false;
export let initialWidget: string | undefined = undefined;
export let title: string | undefined = undefined;
export let icon: string | undefined = undefined;
export let highlighted = false;
export let editable = false;
let currentWidget: string | undefined;
@ -29,20 +38,17 @@
$: {
availableWidgets = [
{
name: "entrylist",
name: "Entry List",
icon: "table",
components: [
components: (entries) => [
{
component: EntryList,
props: { entries, columns: "entity, attribute, value" },
},
],
},
];
if (type?.widgetInfo.length > 0) {
availableWidgets = [...type.widgetInfo, ...availableWidgets];
}
if (widgets?.length) {
availableWidgets = [...widgets, ...availableWidgets];
}
@ -54,95 +60,80 @@
}
}
let components: Component[] = [];
let components: WidgetComponent[] = [];
$: {
components = availableWidgets.find(
(w) => w.name === currentWidget
).components;
components = availableWidgets
.find((w) => w.name === currentWidget)
.components(entries);
}
</script>
<section class="attribute-view labelborder" class:highlighted>
<section class="entry-view labelborder" class:highlighted>
<header>
<h3>
{#if !title && type?.address}
<UpLink to={{ entity: type.address }}>
{#if type.icon}
<div class="icon">
<Icon name={type.icon} />
</div>
{/if}
{#if type.name != "HIER"}
{type.label || type.name || "???"}
{:else}
{$i18n.t("Members")}
{/if}
</UpLink>
{:else}
{title || ""}
{#if icon}
<div class="icon">
<Icon name={icon} />
</div>
{/if}
{title || ""}
</h3>
{#if currentWidget && (availableWidgets.length > 1 || editable)}
<div class="views">
{#each availableWidgets as widget (widget.name)}
<IconButton
name={widget.icon || "question-diamond"}
name={widget.icon || "cube"}
title={widget.name}
active={widget.name === currentWidget}
--active-color="var(--foreground)"
on:click={() => switchWidget(widget.name)}
/>
on:click={() => switchWidget(widget.name)}>{widget.name}</IconButton
>
{/each}
</div>
{/if}
</header>
<div class="content">
{#if !reverse}
{#each components as component}
<svelte:component
this={component.component}
{...(typeof component.props === "function"
? component.props(entries)
: component.props) || {}}
{entries}
{editable}
{type}
on:change
/>
{/each}
{:else}
<!-- shut up svelte check -->
{#each components as component}
<svelte:component
this={EntryList}
columns="entity, attribute"
{entries}
this={component.component}
{...component.props || {}}
{editable}
on:change
/>
{/if}
{/each}
</div>
</section>
<style scoped lang="scss">
@use "./util";
section h3 {
transition: text-shadow 0.2s;
}
section.entry-view {
header {
margin-bottom: 0.5rem;
}
section.highlighted h3 {
text-shadow: #cb4b16 0 0 0.5em;
}
.icon {
display: inline-block;
font-size: 1.25em;
margin-top: -0.3em;
position: relative;
bottom: -2px;
}
.icon {
display: inline-block;
font-size: 1.25em;
margin-top: -0.3em;
position: relative;
bottom: -2px;
h3 {
transition: text-shadow 0.2s;
}
&.highlighted h3 {
text-shadow: #cb4b16 0 0 0.5em;
}
}
.views {
display: flex;
right: 1ex;
transform: translateY(-25%);
font-size: 18px;
}
</style>

View File

@ -1,8 +1,7 @@
<script lang="ts">
import AttributeView from "./AttributeView.svelte";
import EntryView from "./EntryView.svelte";
import { query, useEntity } from "../lib/entity";
import UpObject from "./display/UpObject.svelte";
import { UpType } from "../lib/types";
import { createEventDispatcher, setContext } from "svelte";
import { writable } from "svelte/store";
import type { UpEntry } from "upend";
@ -19,6 +18,7 @@
import { i18n } from "../i18n";
import EntryList from "./widgets/EntryList.svelte";
import api from "../lib/api";
import Gallery from "./widgets/Gallery.svelte";
const dispatch = createEventDispatcher();
const params = useParams();
@ -44,38 +44,6 @@
$: ({ entity, entityInfo, error, revalidate } = useEntity(address));
$: allTypeAddresses = ($entity?.attr["OF"] || [])
.filter((attr) => attr.value.t == "Address")
.map((attr) => attr.value.c);
$: allTypeEntries = query(
`(matches (in ${allTypeAddresses.map((addr) => `@${addr}`).join(" ")}) ? ?)`
).result;
let allTypes: { [key: string]: UpType } = {};
$: {
allTypes = {};
($allTypeEntries?.entries || []).forEach((entry) => {
if (allTypes[entry.entity] === undefined) {
allTypes[entry.entity] = new UpType(entry.entity);
}
switch (entry.attribute) {
case "TYPE":
allTypes[entry.entity].name = String(entry.value.c);
break;
case "LBL":
allTypes[entry.entity].label = String(entry.value.c);
break;
case "TYPE_HAS":
allTypes[entry.entity].attributes.push(String(entry.value.c));
break;
}
});
allTypes = allTypes;
}
let typedAttributes = {} as { [key: string]: UpEntry[] };
let untypedAttributes = [] as UpEntry[];
@ -84,8 +52,10 @@
untypedAttributes = [];
($entity?.attributes || []).forEach((entry) => {
const entryTypes = Object.entries(allTypes).filter(([_, t]) =>
t.attributes.includes(entry.attribute)
const entryTypes = Object.entries(/*allTypes*/ {}).filter(
([_, t]) =>
// t.attributes.includes(entry.attribute)
false
);
if (entryTypes.length > 0) {
entryTypes.forEach(([addr, _]) => {
@ -106,7 +76,6 @@
$: filteredUntypedAttributes = untypedAttributes.filter(
(entry) =>
![
"IS",
"LBL",
"OF",
"NOTE",
@ -120,9 +89,15 @@
? untypedAttributes
: filteredUntypedAttributes;
$: currentBacklinks = $entity?.backlinks || [];
$: currentBacklinks =
(editable
? $entity?.backlinks
: $entity?.backlinks.filter(
(entry) => !["OF"].includes(entry.attribute)
)) || [];
$: groups = [];
$: groups = ($entity?.attr["OF"] || []).map((e) => e.value.c as string);
$: tagged = $entity?.attr["~OF"] || [];
let attributesUsed: UpEntry[] = [];
$: {
@ -238,33 +213,22 @@
</div>
<NotesEditor {address} {editable} on:change={onChange} />
{#if !$error}
{#if Object.keys(allTypes).length || groups.length}
{#if groups.length}
<section class="tags labelborder">
<header><h3>{$i18n.t("Tags")}</h3></header>
<div class="content">
{#each Object.values(allTypes) as type}
{#each groups as group}
<div
class="tag type"
on:mouseenter={() => (highlightedType = type.address)}
class="tag"
on:mouseenter={() => (highlightedType = group)}
on:mouseleave={() => (highlightedType = undefined)}
>
<UpObject address={type.address} link />
<UpObject address={group} link />
{#if editable}
<IconButton name="x-circle" />
{/if}
</div>
{/each}
{#each groups as [entryAddress, address]}
<div class="tag group">
<UpObject {address} link />
{#if editable}
<IconButton
name="x-circle"
on:click={() => removeGroup(entryAddress)}
/>
{/if}
</div>
{/each}
{#if editable}
<div class="selector">
<Selector
@ -279,70 +243,97 @@
</div>
</section>
{/if}
{#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}
highlighted={highlightedType == typeAddr}
/>
{/each}
<div class="attributes">
{#each Object.entries(typedAttributes) as [typeAddr, entries] (typeAddr)}
<EntryView
{entries}
{editable}
on:change={onChange}
initialWidget={String($entity.get("LAST_ATTRIBUTE_WIDGET"))}
on:widgetSwitched={onAttributeWidgetSwitch}
highlighted={highlightedType == typeAddr}
/>
{/each}
{#if currentUntypedAttributes.length > 0 || editable}
<AttributeView
title={$i18n.t("Attributes")}
{editable}
entries={currentUntypedAttributes}
on:change={onChange}
/>
{/if}
{#if currentBacklinks.length > 0}
<AttributeView
title={`${$i18n.t("Referred to")} (${
$entity.backlinks.length
})`}
entries={currentBacklinks}
reverse
on:change={onChange}
/>
{/if}
{#if $entityInfo?.t === "Attribute"}
<div class="buttons">
<div class="button">
<Link to="/surface?x={$entityInfo.c}">
{$i18n.t("Surface view")}
</Link>
</div>
</div>
<section class="labelborder">
<header>
<h3>{$i18n.t("Used")} ({attributesUsed.length})</h3>
</header>
<EntryList
columns="entity,value"
columnWidths={["auto", "33%"]}
entries={attributesUsed}
orderByValue
/>
</section>
{/if}
</div>
{#if editable}
<div class="button" on:click={deleteObject}>
<Icon name="trash" />
</div>
{#if currentUntypedAttributes.length > 0 || editable}
<EntryView
title={$i18n.t("Attributes")}
{editable}
entries={currentUntypedAttributes}
on:change={onChange}
/>
{/if}
{:else}
<Spinner centered />
{#if currentBacklinks.length > 0}
<EntryView
title={`${$i18n.t("Referred to")} (${currentBacklinks.length})`}
entries={currentBacklinks}
on:change={onChange}
/>
{/if}
{#if tagged.length > 0}
<EntryView
title={`${$i18n.t("Links")}`}
widgets={[
{
name: "List",
components: (entries) => [
{
component: Gallery,
props: {
entities: entries.map((e) => e.entity),
thumbnails: false,
},
},
],
},
{
name: "Gallery",
components: (entries) => [
{
component: Gallery,
props: {
entities: entries.map((e) => e.entity),
thumbnails: true,
},
},
],
},
]}
entries={tagged}
on:change={onChange}
/>
{/if}
{#if $entityInfo?.t === "Attribute"}
<div class="buttons">
<div class="button">
<Link to="/surface?x={$entityInfo.c}">
{$i18n.t("Surface view")}
</Link>
</div>
</div>
<section class="labelborder">
<header>
<h3>{$i18n.t("Used")} ({attributesUsed.length})</h3>
</header>
<EntryList
columns="entity,value"
columnWidths={["auto", "33%"]}
entries={attributesUsed}
orderByValue
/>
</section>
{/if}
</div>
{#if editable}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="button" on:click={deleteObject}>
<Icon name="trash" />
</div>
{/if}
{:else}
<div class="error">

View File

@ -1,6 +1,6 @@
<script lang="ts">
import filesize from "filesize";
import { format, formatRelative, fromUnixTime, parseISO } from "date-fns";
import { formatRelative, fromUnixTime, parseISO } from "date-fns";
import Ellipsis from "../utils/Ellipsis.svelte";
import UpObject from "../display/UpObject.svelte";
import { createEventDispatcher } from "svelte";
@ -17,18 +17,17 @@
import { formatDuration } from "../../util/fragments/time";
import { i18n } from "../../i18n";
import UpLink from "../display/UpLink.svelte";
import type { UpType } from "src/lib/types";
const dispatch = createEventDispatcher();
export let columns: string | undefined = undefined;
export let header = true;
export let orderByValue = false;
export let columnWidths: string[] = [];
export let columnWidths: string[] | undefined = undefined;
export let entries: UpEntry[];
export let type: UpType | undefined = undefined;
export let editable = false;
export let attributeOptions: string[] | undefined = undefined;
// Display
$: displayColumns = (columns || "attribute, value")
@ -182,7 +181,7 @@
<col class="action-col" />
{/if}
{#each displayColumns as column, idx}
{#if columnWidths.length}
{#if columnWidths?.length}
<col
class="{column}-col"
style="width: {columnWidths[idx] || 'unset'}"
@ -300,7 +299,7 @@
<Selector
type="attribute"
bind:attribute={newEntryAttribute}
attributeOptions={type?.attributes}
attributeOptions={attributeOptions || []}
/>
</td>
{/if}

View File

@ -1,108 +0,0 @@
import type { UpEntry } from "upend";
import Gallery from "../components/widgets/Gallery.svelte";
export class UpType {
address: string | undefined;
name: string | null = null;
label: string | null = null;
attributes: string[] = [];
constructor(address?: string) {
this.address = address;
}
public get icon(): string | undefined {
return this.name ? TYPE_ICONS[this.name] : undefined;
}
public get widgetInfo(): Widget[] {
return TYPE_WIDGETS[this.name] || [];
}
}
export const UNTYPED = new UpType();
export interface Component {
component: any; // TODO
props?:
| { [key: string]: unknown }
| ((entries: UpEntry[]) => { [key: string]: unknown });
}
export interface Widget {
name: string;
icon?: string;
components: Array<Component>;
}
const TYPE_ICONS: { [key: string]: string } = {
BLOB: "package",
HIER: "folder",
};
const TYPE_WIDGETS: { [key: string]: Widget[] } = {
HIER: [
{
name: "hierarchical-listing",
icon: "folder",
components: [
{
component: Gallery,
props: (entries) => {
return {
thumbnails: false,
entities: entries
.filter((e) => e.attribute == "HAS")
.map((e) => String(e.value.c)),
};
},
},
],
},
{
name: "hierarchical-listing-gallery",
icon: "image",
components: [
{
component: Gallery,
props: (entries) => {
return {
thumbnails: true,
entities: entries
.filter((e) => e.attribute == "HAS")
.map((e) => String(e.value.c)),
};
},
},
],
},
],
KSX_TRACK_MOODS: [
{
name: "ksx-track-compass",
icon: "plus-square",
components: [
// {
// name: "Compass",
// id: "compass_tint_energy",
// props: {
// xAttrName: "KSX_TINT",
// yAttrName: "KSX_ENERGY",
// xLabel: "Lightsoft // Heavydark",
// yLabel: "Chill // Extreme",
// },
// },
// {
// name: "Compass",
// id: "compass_seriousness_materials",
// props: {
// xAttrName: "KSX_SERIOUSNESS",
// yAttrName: "KSX_MATERIALS",
// xLabel: "Dionysia // Apollonia",
// yLabel: "Natural // Reinforced",
// },
// },
],
},
],
};

View File

@ -1,10 +1,10 @@
<script lang="ts">
import EntryList from "../components/widgets/EntryList.svelte";
import Gallery from "../components/widgets/Gallery.svelte";
import type { Widget } from "src/lib/types";
import type { Widget } from "../components/EntryView.svelte";
import { Link } from "svelte-navigator";
import { UpListing } from "upend";
import AttributeView from "../components/AttributeView.svelte";
import EntryView from "../components/EntryView.svelte";
import UpObjectCard from "../components/display/UpObjectCard.svelte";
import Spinner from "../components/utils/Spinner.svelte";
import api from "../lib/api";
@ -42,9 +42,9 @@
const shortWidgets: Widget[] = [
{
name: "list-table",
name: "List",
icon: "list-ul",
components: [
components: (entries) => [
{
component: EntryList,
props: {
@ -52,21 +52,20 @@
columnWidths: ["6em"],
orderByValue: true,
header: false,
entries,
},
},
],
},
{
name: "gallery-view",
name: "Gallery",
icon: "image",
components: [
components: (entries) => [
{
component: Gallery,
props: (entries) => {
return {
entities: entries.map((e) => e.entity),
sort: false,
};
props: {
entities: entries.map((e) => e.entity),
sort: false,
},
},
],
@ -75,9 +74,9 @@
const longWidgets: Widget[] = [
{
name: "list-table",
name: "List",
icon: "list-ul",
components: [
components: (entries) => [
{
component: EntryList,
props: {
@ -85,21 +84,20 @@
columnWidths: ["13em"],
orderByValue: true,
header: false,
entries,
},
},
],
},
{
name: "gallery-view",
name: "Gallery",
icon: "image",
components: [
components: (entries) => [
{
component: Gallery,
props: (entries) => {
return {
entities: entries.map((e) => e.entity),
sort: false,
};
props: {
entities: entries.map((e) => e.entity),
sort: false,
},
},
],
@ -138,7 +136,7 @@
{#if $frequentQuery == undefined}
<Spinner centered />
{:else}
<AttributeView
<EntryView
--current-background="var(--background)"
entries={frequent}
widgets={shortWidgets}
@ -152,7 +150,7 @@
{#if $recentQuery == undefined}
<Spinner centered />
{:else}
<AttributeView
<EntryView
--current-background="var(--background)"
entries={recent}
widgets={longWidgets}
@ -168,7 +166,7 @@
{#if $latestQuery == undefined}
<Spinner centered />
{:else}
<AttributeView
<EntryView
--current-background="var(--background)"
entries={latest}
widgets={longWidgets}

View File

@ -9,10 +9,9 @@
import { baseSearch, createLabelled } from "../util/search";
import { updateTitle } from "../util/title";
import { query as queryFn } from "../lib/entity";
import AttributeView from "../components/AttributeView.svelte";
import EntryView, { type Widget } from "../components/EntryView.svelte";
import api from "../lib/api";
import Gallery from "../components/widgets/Gallery.svelte";
import type { Widget } from "src/lib/types";
import { matchSorter } from "match-sorter";
const navigate = useNavigate();
@ -44,15 +43,15 @@
let exactHits: string[] = [];
$: {
const addressesString = objects.map((e) => `@${e.entity}`).join(" ");
api.query(`(matches (in ${addressesString}) "LBL" ? )`).then(
(labelListing) => {
api
.query(`(matches (in ${addressesString}) "LBL" ? )`)
.then((labelListing) => {
exactHits = labelListing.entries
.filter(
(e) => String(e.value.c).toLowerCase() === query.toLowerCase()
)
.map((e) => e.entity);
}
);
});
}
async function create() {
@ -64,33 +63,29 @@
const searchWidgets: Widget[] = [
{
name: "list-table",
name: "List",
icon: "list-ul",
components: [
components: (entries) => [
{
component: Gallery,
props: (entries) => {
return {
entities: entries.map((e) => e.entity),
sort: false,
thumbnails: false,
};
props: {
entities: entries.map((e) => e.entity),
sort: false,
thumbnails: false,
},
},
],
},
{
name: "gallery-view",
name: "Gallery",
icon: "image",
components: [
components: (entries) => [
{
component: Gallery,
props: (entries) => {
return {
entities: entries.map((e) => e.entity),
sort: false,
thumbnails: true,
};
props: {
entities: entries.map((e) => e.entity),
sort: false,
thumbnails: true,
},
},
],
@ -123,7 +118,7 @@
<section class="objects">
{#if sortedObjects.length}
<h2>Objects</h2>
<AttributeView
<EntryView
--current-background="var(--background)"
entries={sortedObjects}
widgets={searchWidgets}