feat(webui): ✨ rudimentary combine column
parent
6288e8faec
commit
37d5cee2ad
|
@ -6,7 +6,7 @@
|
|||
import { selected } from "./EntitySelect.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let detachUrl: string | undefined = undefined;
|
||||
export let address: string | undefined = undefined;
|
||||
export let only: boolean;
|
||||
|
||||
let detail = only;
|
||||
|
@ -23,7 +23,7 @@
|
|||
}
|
||||
|
||||
function visit() {
|
||||
window.open(normUrl(detachUrl), "_blank");
|
||||
window.open(normUrl(`/browse/${address}`), "_blank");
|
||||
}
|
||||
|
||||
let width = 460;
|
||||
|
@ -50,7 +50,7 @@
|
|||
<div class="browse-column" class:detail>
|
||||
<div class="view" style="--width: {width}px">
|
||||
<header>
|
||||
{#if detachUrl}
|
||||
{#if address}
|
||||
<IconButton name="link" on:click={() => visit()} disabled={only}>
|
||||
Detach
|
||||
</IconButton>
|
||||
|
@ -65,6 +65,14 @@
|
|||
>
|
||||
Detail
|
||||
</IconButton>
|
||||
{#if address}
|
||||
<IconButton
|
||||
name="intersect"
|
||||
on:click={() => dispatch("combine", address)}
|
||||
>
|
||||
Combine
|
||||
</IconButton>
|
||||
{/if}
|
||||
<IconButton
|
||||
name="x-circle"
|
||||
on:click={() => dispatch("close")}
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
<script lang="ts">
|
||||
import { i18n } from "../i18n";
|
||||
import EntitySetEditor from "./EntitySetEditor.svelte";
|
||||
import EntryView from "./EntryView.svelte";
|
||||
import Icon from "./utils/Icon.svelte";
|
||||
import EntityList from "./widgets/EntityList.svelte";
|
||||
import api from "../lib/api";
|
||||
import { Query } from "@upnd/upend";
|
||||
import { ATTR_IN } from "@upnd/upend/constants";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let spec: string;
|
||||
|
||||
const individualSpecs = spec.split(/(?=[+-])/);
|
||||
let includedGroups = individualSpecs
|
||||
.filter((s) => s.startsWith("+"))
|
||||
.map((s) => s.slice(1));
|
||||
let excludedGroups = individualSpecs
|
||||
.filter((s) => s.startsWith("-"))
|
||||
.map((s) => s.slice(1));
|
||||
|
||||
$: if (includedGroups.length == 0 && excludedGroups.length == 0) {
|
||||
dispatch("close");
|
||||
}
|
||||
|
||||
const combinedWidgets = [
|
||||
{
|
||||
name: "List",
|
||||
icon: "list-check",
|
||||
components: ({ entities }) => [
|
||||
{
|
||||
component: EntityList,
|
||||
props: {
|
||||
entities,
|
||||
thumbnails: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "EntityList",
|
||||
icon: "image",
|
||||
components: ({ entities }) => [
|
||||
{
|
||||
component: EntityList,
|
||||
props: {
|
||||
entities,
|
||||
thumbnails: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let resultEntities = [];
|
||||
async function updateResultEntities(
|
||||
includedGroups: string[],
|
||||
excludedGroups: string[],
|
||||
) {
|
||||
const included = includedGroups.length
|
||||
? (
|
||||
await api.query(
|
||||
new Query().matches(
|
||||
undefined,
|
||||
ATTR_IN,
|
||||
includedGroups.map((g) => `@${g}`),
|
||||
),
|
||||
)
|
||||
).objects
|
||||
: [];
|
||||
const excluded = excludedGroups.length
|
||||
? (
|
||||
await api.query(
|
||||
new Query().matches(
|
||||
undefined,
|
||||
ATTR_IN,
|
||||
excludedGroups.map((g) => `@${g}`),
|
||||
),
|
||||
)
|
||||
).objects
|
||||
: [];
|
||||
resultEntities = Object.keys(included).filter((e) => !(e in excluded));
|
||||
}
|
||||
$: updateResultEntities(includedGroups, excludedGroups);
|
||||
</script>
|
||||
|
||||
<div class="view">
|
||||
<h2>
|
||||
<Icon plain name="intersect" />
|
||||
{$i18n.t("Combine")}
|
||||
</h2>
|
||||
<div class="controls">
|
||||
<EntitySetEditor
|
||||
entities={includedGroups}
|
||||
header={$i18n.t("Include")}
|
||||
on:add={(ev) => (includedGroups = [...includedGroups, ev.detail])}
|
||||
on:remove={(ev) =>
|
||||
(includedGroups = includedGroups.filter((e) => e !== ev.detail))}
|
||||
/>
|
||||
<EntitySetEditor
|
||||
entities={excludedGroups}
|
||||
header={$i18n.t("Exclude")}
|
||||
on:add={(ev) => (excludedGroups = [...excludedGroups, ev.detail])}
|
||||
on:remove={(ev) =>
|
||||
(excludedGroups = excludedGroups.filter((e) => e !== ev.detail))}
|
||||
/>
|
||||
</div>
|
||||
<div class="entities">
|
||||
<EntryView
|
||||
title={$i18n.t("Matching entities")}
|
||||
entities={resultEntities}
|
||||
widgets={combinedWidgets}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
margin-top: -0.66em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.entities {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,115 @@
|
|||
<script lang="ts">
|
||||
import UpObjectDisplay from "./display/UpObject.svelte";
|
||||
import Selector from "./utils/Selector.svelte";
|
||||
import IconButton from "./utils/IconButton.svelte";
|
||||
import type { IValue } from "@upnd/upend/types";
|
||||
import { i18n } from "../i18n";
|
||||
import LabelBorder from "./utils/LabelBorder.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let entities: string[];
|
||||
|
||||
export let header = "";
|
||||
export let confirmRemoveMessage = $i18n.t(
|
||||
"Are you sure you want to remove this?",
|
||||
);
|
||||
export let emptyMessage = $i18n.t("Nothing to show.");
|
||||
|
||||
let adding = false;
|
||||
let selector: Selector;
|
||||
|
||||
$: if (adding && selector) selector.focus();
|
||||
|
||||
let entityToAdd: IValue | undefined;
|
||||
async function add() {
|
||||
if (!entityToAdd) {
|
||||
return;
|
||||
}
|
||||
dispatch("add", entityToAdd.c as string);
|
||||
entityToAdd = undefined;
|
||||
}
|
||||
|
||||
async function remove(address: string) {
|
||||
if (confirm(confirmRemoveMessage)) {
|
||||
dispatch("remove", address);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<LabelBorder>
|
||||
<span slot="header">{header}</span>
|
||||
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
<Selector
|
||||
bind:this={selector}
|
||||
type="value"
|
||||
valueTypes={["Address"]}
|
||||
bind:value={entityToAdd}
|
||||
on:input={add}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) adding = false;
|
||||
}}
|
||||
placeholder={$i18n.t("Choose an entity...")}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="body">
|
||||
<div class="group-list">
|
||||
{#each entities as entity}
|
||||
<div class="group">
|
||||
<UpObjectDisplay address={entity} link />
|
||||
<IconButton subdued name="x-circle" on:click={() => remove(entity)} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-groups">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !adding}
|
||||
<div class="add-button">
|
||||
<IconButton
|
||||
outline
|
||||
small
|
||||
name="folder-plus"
|
||||
on:click={() => (adding = true)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</LabelBorder>
|
||||
|
||||
<style lang="scss">
|
||||
.group-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 0.2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
|
||||
.group-list {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.selector {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.no-groups {
|
||||
opacity: 0.66;
|
||||
}
|
||||
</style>
|
|
@ -6,6 +6,7 @@
|
|||
import EntitySelect, { selected } from "../components/EntitySelect.svelte";
|
||||
import SelectedColumn from "../components/SelectedColumn.svelte";
|
||||
import Inspect from "../components/Inspect.svelte";
|
||||
import CombineColumn from "../components/CombineColumn.svelte";
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
|
||||
|
@ -65,6 +66,12 @@
|
|||
}
|
||||
$: only = addresses.length === 1 && !$selected.length;
|
||||
|
||||
function addCombine(address: string) {
|
||||
let _addresses = addresses.concat();
|
||||
_addresses.push(`+${address}`);
|
||||
navigate(`/browse/${_addresses.join(",")}`);
|
||||
}
|
||||
|
||||
$: updateTitle("Browse", identities.join(" / "));
|
||||
</script>
|
||||
|
||||
|
@ -75,16 +82,27 @@
|
|||
>
|
||||
{#each addresses as address, index}
|
||||
<div class="column" data-index={index}>
|
||||
<BrowseColumn
|
||||
detachUrl="/browse/{address}"
|
||||
{only}
|
||||
on:close={() => close(index)}
|
||||
on:resolved={(ev) => onIdentified(index, ev)}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
let:detail
|
||||
>
|
||||
<Inspect {address} {index} {detail} on:resolved on:close />
|
||||
</BrowseColumn>
|
||||
{#if ["+", "-"].some((c) => address.includes(c))}
|
||||
<BrowseColumn
|
||||
{only}
|
||||
on:close={() => close(index)}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
>
|
||||
<CombineColumn spec={address} on:close={() => close(index)} />
|
||||
</BrowseColumn>
|
||||
{:else}
|
||||
<BrowseColumn
|
||||
{address}
|
||||
{only}
|
||||
on:close={() => close(index)}
|
||||
on:resolved={(ev) => onIdentified(index, ev)}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
on:combine={() => addCombine(address)}
|
||||
let:detail
|
||||
>
|
||||
<Inspect {address} {index} {detail} on:resolved on:close />
|
||||
</BrowseColumn>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $selected.length}
|
||||
|
|
Loading…
Reference in New Issue