Compare commits
1 Commits
main
...
feat/selec
Author | SHA1 | Date |
---|---|---|
Tomáš Mládek | 48a398f4f2 |
|
@ -38,10 +38,6 @@ export type IValue =
|
||||||
| {
|
| {
|
||||||
t: "Null";
|
t: "Null";
|
||||||
c: null;
|
c: null;
|
||||||
}
|
|
||||||
| {
|
|
||||||
t: "Invalid";
|
|
||||||
c: null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InvariantEntry {
|
export interface InvariantEntry {
|
||||||
|
|
|
@ -31,8 +31,7 @@
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<Selector
|
<Selector
|
||||||
bind:this={selector}
|
bind:this={selector}
|
||||||
type="value"
|
types={["Address", "NewAddress", "Attribute"]}
|
||||||
valueTypes={["Address"]}
|
|
||||||
on:input={(ev) => {
|
on:input={(ev) => {
|
||||||
dispatch("input", ev.detail);
|
dispatch("input", ev.detail);
|
||||||
editable = false;
|
editable = false;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import UpObjectDisplay from "./display/UpObject.svelte";
|
import UpObjectDisplay from "./display/UpObject.svelte";
|
||||||
import Selector from "./utils/Selector.svelte";
|
import Selector, { type SelectorValue } from "./utils/Selector.svelte";
|
||||||
import IconButton from "./utils/IconButton.svelte";
|
import IconButton from "./utils/IconButton.svelte";
|
||||||
import type { IValue } from "@upnd/upend/types";
|
|
||||||
import { i18n } from "../i18n";
|
import { i18n } from "../i18n";
|
||||||
import LabelBorder from "./utils/LabelBorder.svelte";
|
import LabelBorder from "./utils/LabelBorder.svelte";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
@ -22,13 +21,11 @@
|
||||||
|
|
||||||
$: if (adding && selector) selector.focus();
|
$: if (adding && selector) selector.focus();
|
||||||
|
|
||||||
let entityToAdd: IValue | undefined;
|
async function add(ev: CustomEvent<SelectorValue>) {
|
||||||
async function add() {
|
if (ev.detail.t !== "Address") {
|
||||||
if (!entityToAdd) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch("add", entityToAdd.c as string);
|
dispatch("add", ev.detail.c);
|
||||||
entityToAdd = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(address: string) {
|
async function remove(address: string) {
|
||||||
|
@ -45,9 +42,7 @@
|
||||||
<div class="selector">
|
<div class="selector">
|
||||||
<Selector
|
<Selector
|
||||||
bind:this={selector}
|
bind:this={selector}
|
||||||
type="value"
|
types={["Address", "NewAddress"]}
|
||||||
valueTypes={["Address"]}
|
|
||||||
bind:value={entityToAdd}
|
|
||||||
on:input={add}
|
on:input={add}
|
||||||
on:focus={(ev) => {
|
on:focus={(ev) => {
|
||||||
if (!ev.detail) adding = false;
|
if (!ev.detail) adding = false;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import UpObjectDisplay from "./display/UpObject.svelte";
|
import UpObjectDisplay from "./display/UpObject.svelte";
|
||||||
import Selector from "./utils/Selector.svelte";
|
import Selector, { type SelectorValue } from "./utils/Selector.svelte";
|
||||||
import IconButton from "./utils/IconButton.svelte";
|
import IconButton from "./utils/IconButton.svelte";
|
||||||
import api from "../lib/api";
|
import api from "../lib/api";
|
||||||
import { i18n } from "../i18n";
|
import { i18n } from "../i18n";
|
||||||
|
@ -20,14 +20,15 @@
|
||||||
|
|
||||||
$: typeEntries = $entity?.attr[`~${ATTR_OF}`] || [];
|
$: typeEntries = $entity?.attr[`~${ATTR_OF}`] || [];
|
||||||
|
|
||||||
let attributeToAdd: string;
|
async function add(ev: CustomEvent<SelectorValue>) {
|
||||||
async function add() {
|
if (ev.detail.t !== "Attribute") {
|
||||||
if (!attributeToAdd) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await api.putEntry({
|
await api.putEntry({
|
||||||
entity: {
|
entity: {
|
||||||
t: "Attribute",
|
t: "Attribute",
|
||||||
c: attributeToAdd,
|
c: ev.detail.name,
|
||||||
},
|
},
|
||||||
attribute: ATTR_OF,
|
attribute: ATTR_OF,
|
||||||
value: { t: "Address", c: $entity.address },
|
value: { t: "Address", c: $entity.address },
|
||||||
|
@ -57,8 +58,7 @@
|
||||||
<div class="selector">
|
<div class="selector">
|
||||||
<Selector
|
<Selector
|
||||||
bind:this={typeSelector}
|
bind:this={typeSelector}
|
||||||
type="attribute"
|
types={["Attribute", "NewAttribute"]}
|
||||||
bind:attribute={attributeToAdd}
|
|
||||||
on:input={add}
|
on:input={add}
|
||||||
placeholder={$i18n.t("Assign an attribute to this type...")}
|
placeholder={$i18n.t("Assign an attribute to this type...")}
|
||||||
on:focus={(ev) => {
|
on:focus={(ev) => {
|
||||||
|
|
|
@ -367,9 +367,8 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#key currentAnnotation}
|
{#key currentAnnotation}
|
||||||
<Selector
|
<Selector
|
||||||
type="value"
|
types={["String", "Address"]}
|
||||||
valueTypes={["String", "Address"]}
|
initial={currentAnnotation.data}
|
||||||
value={currentAnnotation.data}
|
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
on:input={(ev) => {
|
on:input={(ev) => {
|
||||||
currentAnnotation.update({ data: ev.detail });
|
currentAnnotation.update({ data: ev.detail });
|
||||||
|
|
|
@ -1,30 +1,49 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Link, useLocation, useNavigate } from "svelte-navigator";
|
import { Link, useNavigate } from "svelte-navigator";
|
||||||
import { useMatch } from "svelte-navigator";
|
// import { useMatch } from "svelte-navigator";
|
||||||
import { addEmitter } from "../AddModal.svelte";
|
import { addEmitter } from "../AddModal.svelte";
|
||||||
import Icon from "../utils/Icon.svelte";
|
import Icon from "../utils/Icon.svelte";
|
||||||
import Input from "../utils/Input.svelte";
|
|
||||||
import { jobsEmitter } from "./Jobs.svelte";
|
import { jobsEmitter } from "./Jobs.svelte";
|
||||||
import api from "../../lib/api";
|
import api from "../../lib/api";
|
||||||
|
import Selector, { type SelectorValue } from "../utils/Selector.svelte";
|
||||||
|
import { i18n } from "../../i18n";
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
// const location = useLocation();
|
||||||
|
|
||||||
const searchMatch = useMatch("/search/:query");
|
// const searchMatch = useMatch("/search/:query");
|
||||||
|
|
||||||
let searchQuery = $searchMatch?.params.query
|
// let searchQuery = $searchMatch?.params.query
|
||||||
? decodeURIComponent($searchMatch?.params.query)
|
// ? decodeURIComponent($searchMatch?.params.query)
|
||||||
: "";
|
// : "";
|
||||||
$: if (!$location.pathname.includes("search")) searchQuery = "";
|
// $: if (!$location.pathname.includes("search")) searchQuery = "";
|
||||||
function onInput(event: CustomEvent<string>) {
|
|
||||||
searchQuery = event.detail;
|
|
||||||
|
|
||||||
if (searchQuery.length > 0) {
|
let selector: Selector;
|
||||||
navigate(`/search/${encodeURIComponent(searchQuery)}`, {
|
|
||||||
replace: $location.pathname.includes("search"),
|
async function onInput(event: CustomEvent<SelectorValue>) {
|
||||||
});
|
const value = event.detail;
|
||||||
} else {
|
switch (value.t) {
|
||||||
navigate("/");
|
case "Address":
|
||||||
|
navigate(`/browse/${value.c}`);
|
||||||
|
break;
|
||||||
|
case "Attribute":
|
||||||
|
{
|
||||||
|
const attributeAddress = await api.componentsToAddress({
|
||||||
|
t: "Attribute",
|
||||||
|
c: value.name,
|
||||||
|
});
|
||||||
|
navigate(`/browse/${attributeAddress}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
selector.reset();
|
||||||
|
|
||||||
|
// searchQuery = event.detail;
|
||||||
|
|
||||||
|
// if (searchQuery.length > 0) {
|
||||||
|
// navigate(`/search/${encodeURIComponent(searchQuery)}`, {
|
||||||
|
// replace: $location.pathname.includes("search"),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileInput: HTMLInputElement;
|
let fileInput: HTMLInputElement;
|
||||||
|
@ -48,13 +67,14 @@
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<Input
|
<Selector
|
||||||
placeholder="Search or add..."
|
types={["Address", "NewAddress", "Attribute"]}
|
||||||
value={searchQuery}
|
placeholder={$i18n.t("Search or add")}
|
||||||
on:input={onInput}
|
on:input={onInput}
|
||||||
|
bind:this={selector}
|
||||||
>
|
>
|
||||||
<Icon name="search" slot="prefix" />
|
<Icon name="search" slot="prefix" />
|
||||||
</Input>
|
</Selector>
|
||||||
</div>
|
</div>
|
||||||
<button class="button" on:click={() => fileInput.click()}>
|
<button class="button" on:click={() => fileInput.click()}>
|
||||||
<Icon name="upload" />
|
<Icon name="upload" />
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Selector from "./Selector.svelte";
|
import Selector, {
|
||||||
|
type SELECTOR_TYPE,
|
||||||
|
type SelectorValue,
|
||||||
|
} from "./Selector.svelte";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import type { IValue } from "@upnd/upend/types";
|
import type { IValue } from "@upnd/upend/types";
|
||||||
import IconButton from "./IconButton.svelte";
|
import IconButton from "./IconButton.svelte";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let value: IValue | undefined = undefined;
|
export let value: IValue | undefined = undefined;
|
||||||
let newValue: IValue = value;
|
export let types: SELECTOR_TYPE[] | undefined = undefined;
|
||||||
|
let newValue: SelectorValue = value;
|
||||||
|
|
||||||
let editing = false;
|
let editing = false;
|
||||||
|
|
||||||
|
@ -16,6 +20,11 @@
|
||||||
|
|
||||||
$: if (editing && selector) selector.focus();
|
$: if (editing && selector) selector.focus();
|
||||||
$: if (!focus && !hover) editing = false;
|
$: if (!focus && !hover) editing = false;
|
||||||
|
|
||||||
|
function onInput(ev: CustomEvent<SelectorValue>) {
|
||||||
|
newValue = ev.detail;
|
||||||
|
selector.focus();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -35,11 +44,10 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Selector
|
<Selector
|
||||||
type="value"
|
{types}
|
||||||
bind:value={newValue}
|
|
||||||
bind:this={selector}
|
bind:this={selector}
|
||||||
on:focus={(ev) => (focus = ev.detail)}
|
on:focus={(ev) => (focus = ev.detail)}
|
||||||
on:input={() => selector.focus()}
|
on:input={onInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -1,70 +1,154 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export type SELECTOR_TYPE =
|
||||||
|
| "Address"
|
||||||
|
| "NewAddress"
|
||||||
|
| "Attribute"
|
||||||
|
| "NewAttribute"
|
||||||
|
| "String"
|
||||||
|
| "Number"
|
||||||
|
| "Null";
|
||||||
|
|
||||||
|
export type SelectorValue = {
|
||||||
|
t: SELECTOR_TYPE;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
t: "Address";
|
||||||
|
c: Address;
|
||||||
|
labels?: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
t: "Attribute";
|
||||||
|
name: string;
|
||||||
|
labels?: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
t: "String";
|
||||||
|
c: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
t: "Number";
|
||||||
|
c: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
t: "Null";
|
||||||
|
c: null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SelectorOption =
|
||||||
|
| SelectorValue
|
||||||
|
| { t: "NewAddress"; c: string }
|
||||||
|
| { t: "NewAttribute"; name: string; label: string };
|
||||||
|
|
||||||
|
export async function selectorValueAsValue(
|
||||||
|
value: SelectorValue,
|
||||||
|
): Promise<IValue> {
|
||||||
|
switch (value.t) {
|
||||||
|
case "Address":
|
||||||
|
return {
|
||||||
|
t: "Address",
|
||||||
|
c: value.c,
|
||||||
|
};
|
||||||
|
case "Attribute":
|
||||||
|
return {
|
||||||
|
t: "Address",
|
||||||
|
c: await api.componentsToAddress({ t: "Attribute", c: value.name }),
|
||||||
|
};
|
||||||
|
case "String":
|
||||||
|
return {
|
||||||
|
t: "String",
|
||||||
|
c: value.c,
|
||||||
|
};
|
||||||
|
case "Number":
|
||||||
|
return {
|
||||||
|
t: "Number",
|
||||||
|
c: value.c,
|
||||||
|
};
|
||||||
|
case "Null":
|
||||||
|
return {
|
||||||
|
t: "Null",
|
||||||
|
c: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import type { UpListing } from "@upnd/upend";
|
import type { UpListing } from "@upnd/upend";
|
||||||
import type { IValue, VALUE_TYPE } from "@upnd/upend/types";
|
import type { Address, IValue } from "@upnd/upend/types";
|
||||||
import { baseSearchOnce, createLabelled } from "../../util/search";
|
import { baseSearchOnce, createLabelled } from "../../util/search";
|
||||||
import UpObject from "../display/UpObject.svelte";
|
import UpObject from "../display/UpObject.svelte";
|
||||||
import IconButton from "./IconButton.svelte";
|
import IconButton from "./IconButton.svelte";
|
||||||
import Input from "./Input.svelte";
|
import Input from "./Input.svelte";
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
import { matchSorter } from "match-sorter";
|
import { matchSorter } from "match-sorter";
|
||||||
import api from "../../lib/api";
|
import api from "../../lib/api";
|
||||||
import { ATTR_LABEL } from "@upnd/upend/constants";
|
import { ATTR_LABEL } from "@upnd/upend/constants";
|
||||||
import { i18n } from "../../i18n";
|
import { i18n } from "../../i18n";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
const dbg = debug("kestrel:Selector");
|
const dbg = debug("kestrel:Selector");
|
||||||
|
|
||||||
const MAX_OPTIONS = 25;
|
export let MAX_OPTIONS = 25;
|
||||||
|
|
||||||
export let type: "attribute" | "value";
|
export let types: SELECTOR_TYPE[] = [
|
||||||
|
"Address",
|
||||||
|
"NewAddress",
|
||||||
|
"Attribute",
|
||||||
|
"String",
|
||||||
|
"Number",
|
||||||
|
];
|
||||||
export let attributeOptions: string[] | undefined = undefined;
|
export let attributeOptions: string[] | undefined = undefined;
|
||||||
export let valueTypes: VALUE_TYPE[] | undefined = undefined;
|
|
||||||
export let placeholder = "";
|
export let placeholder = "";
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
export let attribute: string | undefined = undefined;
|
export let initial: SelectorValue | undefined = undefined;
|
||||||
export let value: IValue | undefined = undefined;
|
let inputValue = "";
|
||||||
|
$: {
|
||||||
if (type == "attribute") {
|
if (initial) {
|
||||||
value = {
|
switch (initial.t) {
|
||||||
c: attribute,
|
case "Address":
|
||||||
t: "String",
|
case "String":
|
||||||
};
|
inputValue = initial.c;
|
||||||
}
|
break;
|
||||||
let inputValue = String(value?.c || "");
|
case "Attribute":
|
||||||
$: if (value === undefined) inputValue = undefined;
|
inputValue = initial.name;
|
||||||
|
break;
|
||||||
function onInput(ev: CustomEvent<string>) {
|
case "Number":
|
||||||
if (type == "attribute") {
|
inputValue = String(initial.c);
|
||||||
attribute = ev.detail;
|
break;
|
||||||
} else {
|
}
|
||||||
value = {
|
|
||||||
t: "String",
|
|
||||||
c: ev.detail,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectorOption {
|
let current:
|
||||||
attribute?: {
|
| (SelectorOption & { t: "Address" | "Attribute" | "String" | "Number" })
|
||||||
labels: string[];
|
| undefined = undefined;
|
||||||
name: string;
|
|
||||||
};
|
export function reset() {
|
||||||
value?: IValue;
|
inputValue = "";
|
||||||
labelToCreate?: string;
|
current = undefined;
|
||||||
|
dispatch("input", current);
|
||||||
}
|
}
|
||||||
|
|
||||||
let options: SelectorOption[] = [];
|
let options: SelectorOption[] = [];
|
||||||
let searchResult: UpListing;
|
let searchResult: UpListing;
|
||||||
const updateOptions = debounce(async (query: string, doSearch: boolean) => {
|
const updateOptions = debounce(async (query: string, doSearch: boolean) => {
|
||||||
switch (type) {
|
options = [];
|
||||||
case "attribute": {
|
|
||||||
const allAttributes = await api.fetchAllAttributes();
|
if (query.length === 0 && !types.includes("Attribute")) {
|
||||||
const attributes = attributeOptions
|
return;
|
||||||
? allAttributes.filter((attr) => attributeOptions.includes(attr.name))
|
}
|
||||||
: allAttributes;
|
|
||||||
options = attributes
|
if (types.includes("Attribute")) {
|
||||||
|
const allAttributes = await api.fetchAllAttributes();
|
||||||
|
const attributes = attributeOptions
|
||||||
|
? allAttributes.filter((attr) => attributeOptions.includes(attr.name))
|
||||||
|
: allAttributes;
|
||||||
|
options.push(
|
||||||
|
...attributes
|
||||||
.filter(
|
.filter(
|
||||||
(attr) =>
|
(attr) =>
|
||||||
attr.name.toLowerCase().includes(query.toLowerCase()) ||
|
attr.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
@ -72,110 +156,96 @@
|
||||||
label.toLowerCase().includes(query.toLowerCase()),
|
label.toLowerCase().includes(query.toLowerCase()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map((attribute) => {
|
.map(
|
||||||
return {
|
(attribute) =>
|
||||||
attribute,
|
({
|
||||||
};
|
t: "Attribute",
|
||||||
});
|
...attribute,
|
||||||
const attributeToCreate = inputValue
|
}) as SelectorOption,
|
||||||
.toUpperCase()
|
),
|
||||||
.replaceAll(/[^A-Z0-9]/g, "_");
|
);
|
||||||
if (
|
|
||||||
!attributeOptions &&
|
const attributeToCreate = query
|
||||||
inputValue &&
|
.toUpperCase()
|
||||||
!allAttributes.map((attr) => attr.name).includes(attributeToCreate)
|
.replaceAll(/[^A-Z0-9]/g, "_");
|
||||||
) {
|
if (
|
||||||
options.push({
|
!attributeOptions &&
|
||||||
attribute: {
|
query &&
|
||||||
labels: [],
|
!allAttributes.map((attr) => attr.name).includes(attributeToCreate) &&
|
||||||
name: attributeToCreate,
|
types.includes("NewAttribute")
|
||||||
},
|
) {
|
||||||
labelToCreate: inputValue,
|
options.push({
|
||||||
});
|
t: "NewAttribute",
|
||||||
}
|
name: attributeToCreate,
|
||||||
options = options;
|
label: query,
|
||||||
break;
|
});
|
||||||
}
|
}
|
||||||
case "value": {
|
options = options;
|
||||||
options = [];
|
}
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (types.includes("Number")) {
|
||||||
return;
|
const number = parseFloat(query);
|
||||||
}
|
if (!Number.isNaN(number)) {
|
||||||
|
options.push({
|
||||||
if (valueTypes === undefined || valueTypes.includes("Number")) {
|
t: "Number",
|
||||||
const number = parseFloat(query);
|
c: number,
|
||||||
if (!Number.isNaN(number)) {
|
});
|
||||||
options.push({
|
|
||||||
value: {
|
|
||||||
t: "Number",
|
|
||||||
c: number,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueTypes === undefined || valueTypes.includes("String")) {
|
|
||||||
options.push({
|
|
||||||
value: {
|
|
||||||
t: "String",
|
|
||||||
c: query,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueTypes === undefined || valueTypes.includes("Address")) {
|
|
||||||
if (doSearch) {
|
|
||||||
searchResult = await baseSearchOnce(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
let exactHits = Object.entries(addressToLabels)
|
|
||||||
.filter(([_, labels]) =>
|
|
||||||
labels
|
|
||||||
.map((l) => l.toLowerCase())
|
|
||||||
.includes(inputValue.toLowerCase()),
|
|
||||||
)
|
|
||||||
.map(([addr, _]) => addr);
|
|
||||||
|
|
||||||
if (exactHits.length) {
|
|
||||||
exactHits.forEach((addr) =>
|
|
||||||
options.push({
|
|
||||||
value: {
|
|
||||||
t: "Address",
|
|
||||||
c: addr,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
options.push({
|
|
||||||
labelToCreate: inputValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const validOptions = searchResult.entries
|
|
||||||
.filter((e) => e.attribute === ATTR_LABEL)
|
|
||||||
.filter((e) => !exactHits.includes(e.entity));
|
|
||||||
|
|
||||||
const sortedOptions = matchSorter(validOptions, inputValue, {
|
|
||||||
keys: ["value.c"],
|
|
||||||
});
|
|
||||||
|
|
||||||
options.push(
|
|
||||||
...sortedOptions.map((e) => {
|
|
||||||
return {
|
|
||||||
value: {
|
|
||||||
t: "Address",
|
|
||||||
c: e.entity,
|
|
||||||
},
|
|
||||||
} as SelectorOption;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
options = options;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (types.includes("String") && query.length) {
|
||||||
|
options.push({
|
||||||
|
t: "String",
|
||||||
|
c: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (types.includes("Address")) {
|
||||||
|
if (doSearch) {
|
||||||
|
searchResult = await baseSearchOnce(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
let exactHits = Object.entries(addressToLabels)
|
||||||
|
.filter(([_, labels]) =>
|
||||||
|
labels.map((l) => l.toLowerCase()).includes(query.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map(([addr, _]) => addr);
|
||||||
|
|
||||||
|
if (exactHits.length) {
|
||||||
|
exactHits.forEach((addr) =>
|
||||||
|
options.push({
|
||||||
|
t: "Address",
|
||||||
|
c: addr,
|
||||||
|
labels: addressToLabels[addr],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (query.length && types.includes("NewAddress")) {
|
||||||
|
options.push({
|
||||||
|
t: "NewAddress",
|
||||||
|
c: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const validOptions = searchResult.entries
|
||||||
|
.filter((e) => e.attribute === ATTR_LABEL)
|
||||||
|
.filter((e) => !exactHits.includes(e.entity));
|
||||||
|
|
||||||
|
const sortedOptions = matchSorter(validOptions, inputValue, {
|
||||||
|
keys: ["value.c"],
|
||||||
|
});
|
||||||
|
|
||||||
|
options.push(
|
||||||
|
...sortedOptions.map((e) => {
|
||||||
|
return {
|
||||||
|
t: "Address",
|
||||||
|
c: e.entity,
|
||||||
|
labels: [e.value.c],
|
||||||
|
} as SelectorOption;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options;
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
$: dbg("Options: %O", options);
|
$: dbg("Options: %O", options);
|
||||||
|
@ -194,38 +264,62 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function set(option: SelectorOption) {
|
async function set(option: SelectorOption) {
|
||||||
switch (type) {
|
dbg("Setting option %O", option);
|
||||||
case "attribute":
|
|
||||||
if (option.labelToCreate) {
|
switch (option.t) {
|
||||||
// First, "create" attribute, getting its address.
|
case "Address":
|
||||||
const [_, address] = await api.putEntry({
|
inputValue = option.c;
|
||||||
entity: { t: "Attribute", c: option.attribute.name },
|
current = option;
|
||||||
});
|
|
||||||
// Second, label it.
|
|
||||||
await api.putEntityAttribute(address, ATTR_LABEL, {
|
|
||||||
t: "String",
|
|
||||||
c: option.labelToCreate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
attribute = option.attribute.name;
|
|
||||||
inputValue = option.attribute.name;
|
|
||||||
break;
|
break;
|
||||||
case "value":
|
case "NewAddress":
|
||||||
if (!option.labelToCreate) {
|
{
|
||||||
value = option.value;
|
const addr = await createLabelled(option.c);
|
||||||
inputValue = String(option.value.c);
|
inputValue = addr;
|
||||||
} else {
|
current = {
|
||||||
const addr = await createLabelled(option.labelToCreate);
|
|
||||||
value = {
|
|
||||||
t: "Address",
|
t: "Address",
|
||||||
c: addr,
|
c: addr,
|
||||||
|
labels: [option.c],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "Attribute":
|
||||||
|
inputValue = option.name;
|
||||||
|
current = option;
|
||||||
|
break;
|
||||||
|
case "NewAttribute":
|
||||||
|
inputValue = option.name;
|
||||||
|
|
||||||
|
{
|
||||||
|
const address = await api.componentsToAddress({
|
||||||
|
t: "Attribute",
|
||||||
|
c: option.name,
|
||||||
|
});
|
||||||
|
await api.putEntityAttribute(address, ATTR_LABEL, {
|
||||||
|
t: "String",
|
||||||
|
c: option.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
current = {
|
||||||
|
t: "Attribute",
|
||||||
|
name: option.name,
|
||||||
|
labels: [option.label],
|
||||||
};
|
};
|
||||||
inputValue = addr;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "String":
|
||||||
|
inputValue = option.c;
|
||||||
|
current = option;
|
||||||
|
break;
|
||||||
|
case "Number":
|
||||||
|
inputValue = String(option.c);
|
||||||
|
current = option;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
dbg("Setting value to %O", value);
|
|
||||||
dispatch("input", value);
|
dbg("Result set value: %O", current);
|
||||||
|
dispatch("input", current);
|
||||||
|
|
||||||
options = [];
|
options = [];
|
||||||
optionFocusIndex = -1;
|
optionFocusIndex = -1;
|
||||||
hover = false;
|
hover = false;
|
||||||
|
@ -278,7 +372,7 @@
|
||||||
|
|
||||||
let input: Input;
|
let input: Input;
|
||||||
export function focus() {
|
export function focus() {
|
||||||
dbg("Focusing input");
|
// dbg("Focusing input");
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,10 +386,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="selector">
|
<div class="selector">
|
||||||
{#if value?.t == "Address" && inputValue}
|
{#if current?.t === "Address" && inputValue.length > 0}
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<UpObject link address={String(value.c)} />
|
<UpObject link address={String(current.c)} />
|
||||||
</div>
|
</div>
|
||||||
<IconButton name="x" on:click={() => (inputValue = "")} />
|
<IconButton name="x" on:click={() => (inputValue = "")} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -303,12 +397,13 @@
|
||||||
<Input
|
<Input
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
bind:value={inputValue}
|
bind:value={inputValue}
|
||||||
on:input={onInput}
|
|
||||||
on:focusChange={(ev) => (inputFocused = ev.detail)}
|
on:focusChange={(ev) => (inputFocused = ev.detail)}
|
||||||
on:keydown={handleArrowKeys}
|
on:keydown={handleArrowKeys}
|
||||||
{disabled}
|
{disabled}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
/>
|
>
|
||||||
|
<slot name="prefix" slot="prefix" />
|
||||||
|
</Input>
|
||||||
{/if}
|
{/if}
|
||||||
<ul
|
<ul
|
||||||
class="options"
|
class="options"
|
||||||
|
@ -333,36 +428,35 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if option.attribute}
|
{#if option.t === "Address"}
|
||||||
{#if option.labelToCreate}
|
{@const address = option.c}
|
||||||
<div class="content">{option.labelToCreate}</div>
|
<UpObject
|
||||||
<div class="type">Create attribute ({option.attribute.name})</div>
|
{address}
|
||||||
{:else if option.attribute.labels.length}
|
labels={option.labels}
|
||||||
|
on:resolved={(ev) => onAddressResolved(address, ev)}
|
||||||
|
/>
|
||||||
|
{:else if option.t === "NewAddress"}
|
||||||
|
<div class="content">{option.c}</div>
|
||||||
|
<div class="type">{$i18n.t("Create object")}</div>
|
||||||
|
{:else if option.t === "Attribute"}
|
||||||
|
{#if option.labels.length}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#each option.attribute.labels as label}
|
{#each option.labels as label}
|
||||||
<div class="label">{label}</div>
|
<div class="label">{label}</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="type">{option.attribute.name}</div>
|
<div class="type">{option.name}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{option.attribute.name}
|
{option.name}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if option.value}
|
{:else if option.t === "NewAttribute"}
|
||||||
{#if option.value.t == "Address"}
|
<div class="content">{option.label}</div>
|
||||||
<UpObject
|
<div class="type">{$i18n.t("Create attribute")} ({option.name})</div>
|
||||||
address={String(option.value.c)}
|
{:else}
|
||||||
on:resolved={(ev) =>
|
<div class="type">{option.t}</div>
|
||||||
onAddressResolved(String(option.value.c), ev)}
|
<div class="content">{option.c}</div>
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="type">{option.value.t}</div>
|
|
||||||
<div class="content">{option.value.c}</div>
|
|
||||||
{/if}
|
|
||||||
{:else if option.labelToCreate}
|
|
||||||
<div class="type">{$i18n.t("Add object")}</div>
|
|
||||||
<div class="content">{option.labelToCreate}</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { readable, type Readable } from "svelte/store";
|
import { readable, type Readable } from "svelte/store";
|
||||||
import type { UpListing } from "@upnd/upend";
|
import type { UpListing } from "@upnd/upend";
|
||||||
import type { Address, IValue } from "@upnd/upend/types";
|
import type { Address } from "@upnd/upend/types";
|
||||||
import { query } from "../../lib/entity";
|
import { query } from "../../lib/entity";
|
||||||
import UpObject from "../display/UpObject.svelte";
|
import UpObject from "../display/UpObject.svelte";
|
||||||
import UpObjectCard from "../display/UpObjectCard.svelte";
|
import UpObjectCard from "../display/UpObjectCard.svelte";
|
||||||
import { ATTR_LABEL } from "@upnd/upend/constants";
|
import { ATTR_LABEL } from "@upnd/upend/constants";
|
||||||
import { i18n } from "../../i18n";
|
import { i18n } from "../../i18n";
|
||||||
import IconButton from "../utils/IconButton.svelte";
|
import IconButton from "../utils/IconButton.svelte";
|
||||||
import Selector from "../utils/Selector.svelte";
|
import Selector, { type SelectorValue } from "../utils/Selector.svelte";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import type { WidgetChange } from "src/types/base";
|
import type { WidgetChange } from "src/types/base";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
|
@ -131,7 +131,7 @@
|
||||||
|
|
||||||
$: if (adding && addSelector) addSelector.focus();
|
$: if (adding && addSelector) addSelector.focus();
|
||||||
|
|
||||||
function addEntity(ev: CustomEvent<IValue>) {
|
function addEntity(ev: CustomEvent<SelectorValue>) {
|
||||||
dbg("Adding entity", ev.detail);
|
dbg("Adding entity", ev.detail);
|
||||||
const addAddress = ev.detail?.t == "Address" ? ev.detail.c : undefined;
|
const addAddress = ev.detail?.t == "Address" ? ev.detail.c : undefined;
|
||||||
if (!addAddress) return;
|
if (!addAddress) return;
|
||||||
|
@ -224,8 +224,7 @@
|
||||||
<Selector
|
<Selector
|
||||||
bind:this={addSelector}
|
bind:this={addSelector}
|
||||||
placeholder={$i18n.t("Search database or paste an URL")}
|
placeholder={$i18n.t("Search database or paste an URL")}
|
||||||
type="value"
|
types={["Address", "NewAddress"]}
|
||||||
valueTypes={["Address"]}
|
|
||||||
on:input={addEntity}
|
on:input={addEntity}
|
||||||
on:focus={(ev) => {
|
on:focus={(ev) => {
|
||||||
if (!ev.detail) {
|
if (!ev.detail) {
|
||||||
|
|
|
@ -7,8 +7,10 @@
|
||||||
import type { WidgetChange, AttributeUpdate } from "../../types/base";
|
import type { WidgetChange, AttributeUpdate } from "../../types/base";
|
||||||
import type { UpEntry, UpListing } from "@upnd/upend";
|
import type { UpEntry, UpListing } from "@upnd/upend";
|
||||||
import IconButton from "../utils/IconButton.svelte";
|
import IconButton from "../utils/IconButton.svelte";
|
||||||
import Selector from "../utils/Selector.svelte";
|
import Selector, {
|
||||||
import type { IValue } from "@upnd/upend/types";
|
selectorValueAsValue,
|
||||||
|
type SelectorValue,
|
||||||
|
} from "../utils/Selector.svelte";
|
||||||
import Editable from "../utils/Editable.svelte";
|
import Editable from "../utils/Editable.svelte";
|
||||||
import { query } from "../../lib/entity";
|
import { query } from "../../lib/entity";
|
||||||
import { type Readable, readable } from "svelte/store";
|
import { type Readable, readable } from "svelte/store";
|
||||||
|
@ -28,7 +30,6 @@
|
||||||
|
|
||||||
export let entries: UpEntry[];
|
export let entries: UpEntry[];
|
||||||
export let attributes: string[] | undefined = undefined;
|
export let attributes: string[] | undefined = undefined;
|
||||||
export let attributeOptions: string[] | undefined = undefined;
|
|
||||||
|
|
||||||
// Display
|
// Display
|
||||||
$: displayColumns = (columns || "entity, attribute, value")
|
$: displayColumns = (columns || "entity, attribute, value")
|
||||||
|
@ -62,16 +63,16 @@
|
||||||
let addFocus = false;
|
let addFocus = false;
|
||||||
let newAttrSelector: Selector;
|
let newAttrSelector: Selector;
|
||||||
let newEntryAttribute = "";
|
let newEntryAttribute = "";
|
||||||
let newEntryValue: IValue | undefined;
|
let newEntryValue: SelectorValue | undefined;
|
||||||
|
|
||||||
$: if (adding && newAttrSelector) newAttrSelector.focus();
|
$: if (adding && newAttrSelector) newAttrSelector.focus();
|
||||||
$: if (!addFocus && !addHover) adding = false;
|
$: if (!addFocus && !addHover) adding = false;
|
||||||
|
|
||||||
async function addEntry(attribute: string, value: IValue) {
|
async function addEntry(attribute: string, value: SelectorValue) {
|
||||||
dispatch("change", {
|
dispatch("change", {
|
||||||
type: "create",
|
type: "create",
|
||||||
attribute,
|
attribute,
|
||||||
value,
|
value: await selectorValueAsValue(value),
|
||||||
} as WidgetChange);
|
} as WidgetChange);
|
||||||
newEntryAttribute = "";
|
newEntryAttribute = "";
|
||||||
newEntryValue = undefined;
|
newEntryValue = undefined;
|
||||||
|
@ -84,13 +85,13 @@
|
||||||
async function updateEntry(
|
async function updateEntry(
|
||||||
address: string,
|
address: string,
|
||||||
attribute: string,
|
attribute: string,
|
||||||
value: IValue,
|
value: SelectorValue,
|
||||||
) {
|
) {
|
||||||
dispatch("change", {
|
dispatch("change", {
|
||||||
type: "update",
|
type: "update",
|
||||||
address,
|
address,
|
||||||
attribute,
|
attribute,
|
||||||
value,
|
value: await selectorValueAsValue(value),
|
||||||
} as AttributeUpdate);
|
} as AttributeUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,9 +387,8 @@
|
||||||
{#if column == ATTR_COL}
|
{#if column == ATTR_COL}
|
||||||
<div class="cell mark-attribute">
|
<div class="cell mark-attribute">
|
||||||
<Selector
|
<Selector
|
||||||
type="attribute"
|
types={["Attribute", "NewAttribute"]}
|
||||||
bind:attribute={newEntryAttribute}
|
on:input={(ev) => (newEntryAttribute = ev.detail.name)}
|
||||||
{attributeOptions}
|
|
||||||
on:focus={(ev) => (addFocus = ev.detail)}
|
on:focus={(ev) => (addFocus = ev.detail)}
|
||||||
bind:this={newAttrSelector}
|
bind:this={newAttrSelector}
|
||||||
/>
|
/>
|
||||||
|
@ -396,8 +396,7 @@
|
||||||
{:else if column === VALUE_COL}
|
{:else if column === VALUE_COL}
|
||||||
<div class="cell mark-value">
|
<div class="cell mark-value">
|
||||||
<Selector
|
<Selector
|
||||||
type="value"
|
on:input={(ev) => (newEntryValue = ev.detail)}
|
||||||
bind:value={newEntryValue}
|
|
||||||
on:focus={(ev) => (addFocus = ev.detail)}
|
on:focus={(ev) => (addFocus = ev.detail)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,33 +24,49 @@ type Story = StoryObj<Selector>;
|
||||||
|
|
||||||
export const Attribute: Story = {
|
export const Attribute: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "attribute",
|
types: ["Attribute", "NewAttribute"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AllValues: Story = {
|
export const AllValues: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "value",
|
types: [
|
||||||
|
"Attribute",
|
||||||
|
"NewAttribute",
|
||||||
|
"Address",
|
||||||
|
"NewAddress",
|
||||||
|
"String",
|
||||||
|
"Number",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Entities: Story = {
|
export const Entities: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "value",
|
types: ["Address", "NewAddress"],
|
||||||
valueTypes: ["Address"],
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExistingEntities: Story = {
|
||||||
|
args: {
|
||||||
|
types: ["Address"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Strings: Story = {
|
export const Strings: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "value",
|
types: ["String"],
|
||||||
valueTypes: ["String"],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Numbers: Story = {
|
export const Numbers: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "value",
|
types: ["Number"],
|
||||||
valueTypes: ["Number"],
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultipleValues: Story = {
|
||||||
|
args: {
|
||||||
|
types: ["String", "Number"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import CombineColumn from "../components/CombineColumn.svelte";
|
import CombineColumn from "../components/CombineColumn.svelte";
|
||||||
import GroupColumn from "../components/GroupColumn.svelte";
|
import GroupColumn from "../components/GroupColumn.svelte";
|
||||||
import SurfaceColumn from "../components/SurfaceColumn.svelte";
|
import SurfaceColumn from "../components/SurfaceColumn.svelte";
|
||||||
|
import type { SelectorValue } from "src/components/utils/Selector.svelte";
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
@ -16,7 +17,10 @@
|
||||||
let identities: string[] = [];
|
let identities: string[] = [];
|
||||||
$: addresses = $params.addresses.split(",");
|
$: addresses = $params.addresses.split(",");
|
||||||
|
|
||||||
function add(address: string) {
|
function add(value: SelectorValue) {
|
||||||
|
if (value.t !== "Address") return;
|
||||||
|
const address = value.c;
|
||||||
|
|
||||||
let _addresses = addresses.concat();
|
let _addresses = addresses.concat();
|
||||||
_addresses.push(address);
|
_addresses.push(address);
|
||||||
navigate(`/browse/${_addresses.join(",")}`);
|
navigate(`/browse/${_addresses.join(",")}`);
|
||||||
|
@ -148,7 +152,7 @@
|
||||||
{#key addresses}
|
{#key addresses}
|
||||||
<div class="column" data-index="add">
|
<div class="column" data-index="add">
|
||||||
<BrowseAdd
|
<BrowseAdd
|
||||||
on:input={(ev) => add(ev.detail.c)}
|
on:input={(ev) => add(ev.detail)}
|
||||||
on:editable={() => scrollToVisible("add")}
|
on:editable={() => scrollToVisible("add")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue