feat: modeless entrylist editing
ci/woodpecker/push/woodpecker Pipeline failed
Details
ci/woodpecker/push/woodpecker Pipeline failed
Details
parent
959a613ea3
commit
3a34fc346c
|
@ -10,6 +10,7 @@
|
|||
components: (input: {
|
||||
entries: UpEntry[];
|
||||
group?: string;
|
||||
address?: string;
|
||||
}) => Array<WidgetComponent>;
|
||||
}
|
||||
</script>
|
||||
|
@ -29,6 +30,7 @@
|
|||
export let initialWidget: string | undefined = undefined;
|
||||
export let title: string | undefined = undefined;
|
||||
export let group: string | undefined = undefined;
|
||||
export let address: string | undefined = undefined;
|
||||
export let icon: string | undefined = undefined;
|
||||
export let highlighted = false;
|
||||
|
||||
|
@ -69,7 +71,7 @@
|
|||
$: {
|
||||
components = availableWidgets
|
||||
.find((w) => w.name === currentWidget)
|
||||
.components({ entries, group });
|
||||
.components({ entries, group, address });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { UpEntry } from "upend";
|
||||
import Spinner from "./utils/Spinner.svelte";
|
||||
import NotesEditor from "./utils/NotesEditor.svelte";
|
||||
import type { AttributeChange } from "../types/base";
|
||||
import type { WidgetChange } from "../types/base";
|
||||
import type { EntityInfo } from "upend/types";
|
||||
import IconButton from "./utils/IconButton.svelte";
|
||||
import type { BrowseContext } from "../util/browse";
|
||||
|
@ -188,7 +188,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function onChange(ev: CustomEvent<AttributeChange>) {
|
||||
async function onChange(ev: CustomEvent<WidgetChange>) {
|
||||
const change = ev.detail;
|
||||
switch (change.type) {
|
||||
case "create":
|
||||
|
@ -204,6 +204,27 @@
|
|||
case "update":
|
||||
await api.putEntityAttribute(address, change.attribute, change.value);
|
||||
break;
|
||||
case "entry-add":
|
||||
await api.putEntry({
|
||||
entity: address,
|
||||
attribute: ATTR_IN,
|
||||
value: { t: "Address", c: change.address },
|
||||
});
|
||||
break;
|
||||
case "entry-delete": {
|
||||
const inEntry = $entity?.attr[`~${ATTR_IN}`].find(
|
||||
(e) => e.entity === change.address,
|
||||
);
|
||||
if (inEntry) {
|
||||
await api.deleteEntry(inEntry.address);
|
||||
} else {
|
||||
console.warn(
|
||||
"Couldn't find IN entry for entity %s?!",
|
||||
change.address,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error("Unimplemented AttributeChange", change);
|
||||
return;
|
||||
|
@ -259,10 +280,11 @@
|
|||
{
|
||||
name: "EntityList",
|
||||
icon: "image",
|
||||
components: ({ entries }) => [
|
||||
components: ({ entries, address }) => [
|
||||
{
|
||||
component: EntityList,
|
||||
props: {
|
||||
address,
|
||||
entities: entries
|
||||
.filter((e) => e.value.t == "Address")
|
||||
.map((e) => e.value.c),
|
||||
|
@ -277,10 +299,11 @@
|
|||
{
|
||||
name: "List",
|
||||
icon: "list-check",
|
||||
components: ({ entries }) => [
|
||||
components: ({ entries, address }) => [
|
||||
{
|
||||
component: EntityList,
|
||||
props: {
|
||||
address,
|
||||
entities: entries.map((e) => e.entity),
|
||||
thumbnails: false,
|
||||
},
|
||||
|
@ -290,10 +313,11 @@
|
|||
{
|
||||
name: "EntityList",
|
||||
icon: "image",
|
||||
components: ({ entries }) => [
|
||||
components: ({ entries, address }) => [
|
||||
{
|
||||
component: EntityList,
|
||||
props: {
|
||||
address,
|
||||
entities: entries.map((e) => e.entity),
|
||||
thumbnails: true,
|
||||
},
|
||||
|
@ -366,6 +390,7 @@
|
|||
highlighted={highlightedType == typeAddr}
|
||||
title={labels.join(" | ")}
|
||||
group={typeAddr}
|
||||
{address}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
|
@ -375,6 +400,7 @@
|
|||
widgets={attributeWidgets}
|
||||
entries={currentUntypedProperties}
|
||||
on:change={onChange}
|
||||
{address}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -384,6 +410,7 @@
|
|||
widgets={linkWidgets}
|
||||
entries={currentUntypedLinks}
|
||||
on:change={onChange}
|
||||
{address}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -393,6 +420,7 @@
|
|||
widgets={taggedWidgets}
|
||||
entries={tagged}
|
||||
on:change={onChange}
|
||||
{address}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -401,6 +429,7 @@
|
|||
title={`${$i18n.t("Referred to")} (${currentBacklinks.length})`}
|
||||
entries={currentBacklinks}
|
||||
on:change={onChange}
|
||||
{address}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { readable, type Readable } from "svelte/store";
|
||||
import type { UpListing } from "upend";
|
||||
import type { Address } from "upend/types";
|
||||
import type { Address, IValue } from "upend/types";
|
||||
import { query } from "../../lib/entity";
|
||||
import UpObject from "../display/UpObject.svelte";
|
||||
import UpObjectCard from "../display/UpObjectCard.svelte";
|
||||
import { ATTR_LABEL } from "upend/constants";
|
||||
import { ATTR_IN, ATTR_LABEL } from "upend/constants";
|
||||
import { i18n } from "../../i18n";
|
||||
import Icon from "../utils/Icon.svelte";
|
||||
import IconButton from "../utils/IconButton.svelte";
|
||||
import Selector from "../utils/Selector.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { WidgetChange } from "src/types/base";
|
||||
import debug from "debug";
|
||||
const dispatch = createEventDispatcher();
|
||||
const dbg = debug(`kestrel:EntityList`);
|
||||
|
||||
export let entities: Address[];
|
||||
export let thumbnails = true;
|
||||
export let sort = true;
|
||||
export let address: Address | undefined = undefined;
|
||||
|
||||
const deduplicatedEntities = Array.from(new Set(entities));
|
||||
$: deduplicatedEntities = Array.from(new Set(entities));
|
||||
|
||||
let style: "list" | "grid" | "flex" = "grid";
|
||||
|
||||
|
@ -21,7 +29,7 @@
|
|||
$: style = !thumbnails || clientWidth < 600 ? "list" : "grid";
|
||||
|
||||
// Sorting
|
||||
let sortedEntities = [];
|
||||
let sortedEntities: Address[] = [];
|
||||
|
||||
let sortKeys: { [key: string]: string[] } = {};
|
||||
function addSortKeys(key: string, vals: string[], resort: boolean) {
|
||||
|
@ -116,6 +124,37 @@
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Adding
|
||||
let addSelector: Selector | undefined;
|
||||
let adding = false;
|
||||
|
||||
$: if (adding && addSelector) addSelector.focus();
|
||||
|
||||
function addEntity(ev: CustomEvent<IValue>) {
|
||||
dbg("Adding entity", ev.detail);
|
||||
const addAddress = ev.detail?.t == "Address" ? ev.detail.c : undefined;
|
||||
if (!addAddress) return;
|
||||
|
||||
dispatch("change", {
|
||||
type: "entry-add",
|
||||
address: addAddress,
|
||||
} as WidgetChange);
|
||||
}
|
||||
|
||||
function removeEntity(address: string) {
|
||||
if (
|
||||
confirm(
|
||||
$i18n.t("Are you sure you want to remove this entry from members?"),
|
||||
)
|
||||
) {
|
||||
dbg("Removing entity", address);
|
||||
dispatch("change", {
|
||||
type: "entry-delete",
|
||||
address,
|
||||
} as WidgetChange);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -138,21 +177,63 @@
|
|||
addSortKeys(entity, event.detail, true);
|
||||
}}
|
||||
/>
|
||||
<div class="icon">
|
||||
<IconButton
|
||||
name="trash"
|
||||
color="#dc322f"
|
||||
on:click={() => removeEntity(entity)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<UpObject
|
||||
link
|
||||
address={entity}
|
||||
labels={sortKeys[entity]}
|
||||
on:resolved={(event) => {
|
||||
addSortKeys(entity, event.detail, true);
|
||||
}}
|
||||
/>
|
||||
<div class="object">
|
||||
<UpObject
|
||||
link
|
||||
address={entity}
|
||||
labels={sortKeys[entity]}
|
||||
on:resolved={(event) => {
|
||||
addSortKeys(entity, event.detail, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<IconButton
|
||||
name="trash"
|
||||
color="#dc322f"
|
||||
on:click={() => removeEntity(entity)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="skeleton" style="text-align: center">...</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if address}
|
||||
<div class="add item">
|
||||
{#if adding}
|
||||
<Selector
|
||||
bind:this={addSelector}
|
||||
type="value"
|
||||
valueTypes={["Address"]}
|
||||
on:input={addEntity}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) {
|
||||
adding = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<IconButton
|
||||
name="plus-circle"
|
||||
outline
|
||||
subdued
|
||||
on:click={() => {
|
||||
adding = true;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="message">
|
||||
|
@ -174,6 +255,7 @@
|
|||
:global(.entitylist.style-grid .items) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
:global(.entitylist.style-flex .items) {
|
||||
|
@ -190,10 +272,60 @@
|
|||
|
||||
.item {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message {
|
||||
text-align: center;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.entitylist:not(.has-thumbnails) {
|
||||
.item {
|
||||
display: flex;
|
||||
.object {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 0;
|
||||
transition: width 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entitylist.has-thumbnails {
|
||||
.item {
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.entitylist.style-grid .add {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import Ellipsis from "../utils/Ellipsis.svelte";
|
||||
import UpObject from "../display/UpObject.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { AttributeChange, AttributeUpdate } from "../../types/base";
|
||||
import type { WidgetChange, AttributeUpdate } from "../../types/base";
|
||||
import type { UpEntry, UpListing } from "upend";
|
||||
import IconButton from "../utils/IconButton.svelte";
|
||||
import Selector from "../utils/Selector.svelte";
|
||||
|
@ -57,13 +57,13 @@
|
|||
type: "create",
|
||||
attribute,
|
||||
value,
|
||||
} as AttributeChange);
|
||||
} as WidgetChange);
|
||||
newEntryAttribute = "";
|
||||
newEntryValue = undefined;
|
||||
}
|
||||
async function removeEntry(address: string) {
|
||||
if (confirm($i18n.t("Are you sure you want to remove the property?"))) {
|
||||
dispatch("change", { type: "delete", address } as AttributeChange);
|
||||
dispatch("change", { type: "delete", address } as WidgetChange);
|
||||
}
|
||||
}
|
||||
async function updateEntry(
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import type { IValue } from "upend/types";
|
||||
|
||||
export type AttributeChange =
|
||||
export type WidgetChange =
|
||||
| AttributeCreate
|
||||
| AttributeUpdate
|
||||
| AttributeDelete;
|
||||
| AttributeDelete
|
||||
| EntryInAdd
|
||||
| EntryInDelete;
|
||||
|
||||
export interface AttributeCreate {
|
||||
type: "create";
|
||||
|
@ -21,3 +23,13 @@ export interface AttributeDelete {
|
|||
type: "delete";
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface EntryInAdd {
|
||||
type: "entry-add";
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface EntryInDelete {
|
||||
type: "entry-delete";
|
||||
address: string;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue