feat(webui): required & optional attributes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
TODO: honor distinction in EntryLists as well
This commit is contained in:
parent
75faa28ff3
commit
0811d9ccd8
2 changed files with 126 additions and 42 deletions
|
@ -182,19 +182,38 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCorrectlyTagged() {
|
async function fetchCorrectlyTagged() {
|
||||||
const attributes = (
|
if (!$entity?.attr[`~${ATTR_OF}`]?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allAttributes = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
($entity?.attr[`~${ATTR_OF}`] ?? []).map((e) => api.addressToComponents(e.entity))
|
($entity?.attr[`~${ATTR_OF}`] ?? []).map(async (e) => {
|
||||||
|
return { address: e.entity, components: await api.addressToComponents(e.entity) };
|
||||||
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filter((ac) => ac.t == 'Attribute')
|
.filter((ac) => ac.components.t == 'Attribute')
|
||||||
.map((ac) => ac.c)
|
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|
||||||
|
const attributeRequiredQuery = await api.query(
|
||||||
|
Query.matches(
|
||||||
|
allAttributes.map((ac) => `@${ac.address}`),
|
||||||
|
'TYPE_REQUIRED',
|
||||||
|
Any
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const requiredAttributes = allAttributes
|
||||||
|
.filter((ac) => {
|
||||||
|
return attributeRequiredQuery.getObject(ac.address).attr['TYPE_REQUIRED']?.length;
|
||||||
|
})
|
||||||
|
.map((ac) => ac.components.c as string);
|
||||||
|
|
||||||
const attributeQuery = await api.query(
|
const attributeQuery = await api.query(
|
||||||
Query.matches(
|
Query.matches(
|
||||||
tagged.map((t) => `@${t.entity}`),
|
tagged.map((t) => `@${t.entity}`),
|
||||||
attributes,
|
requiredAttributes,
|
||||||
Any
|
Any
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -204,7 +223,7 @@
|
||||||
|
|
||||||
for (const element of tagged) {
|
for (const element of tagged) {
|
||||||
const entity = attributeQuery.getObject(element.entity);
|
const entity = attributeQuery.getObject(element.entity);
|
||||||
if (attributes.every((attr) => entity.attr[attr])) {
|
if (requiredAttributes.every((attr) => entity.attr[attr])) {
|
||||||
correctlyTagged = [...correctlyTagged, element.entity];
|
correctlyTagged = [...correctlyTagged, element.entity];
|
||||||
} else {
|
} else {
|
||||||
incorrectlyTagged = [...incorrectlyTagged, element.entity];
|
incorrectlyTagged = [...incorrectlyTagged, element.entity];
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
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';
|
||||||
import type { UpObject, UpEntry } from '@upnd/upend';
|
import { type UpObject, type UpEntry, Query } from '@upnd/upend';
|
||||||
|
import { Any } from '@upnd/upend/query';
|
||||||
import type { Readable } from 'svelte/store';
|
import type { Readable } from 'svelte/store';
|
||||||
import { ATTR_OF } from '@upnd/upend/constants';
|
import { ATTR_OF } from '@upnd/upend/constants';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
@ -18,7 +19,33 @@
|
||||||
|
|
||||||
$: if (adding && typeSelector) typeSelector.focus();
|
$: if (adding && typeSelector) typeSelector.focus();
|
||||||
|
|
||||||
$: typeEntries = $entity?.attr[`~${ATTR_OF}`] || [];
|
let types: Array<{ address: string; entry: UpEntry; required: UpEntry | undefined }> = [];
|
||||||
|
$: updateTypes($entity?.attr[`~${ATTR_OF}`] || []);
|
||||||
|
async function updateTypes(entries: UpEntry[]) {
|
||||||
|
types = [];
|
||||||
|
const query = await api.query(
|
||||||
|
Query.matches(
|
||||||
|
entries.flatMap((e) => [`@${e.address}`, `@${e.entity}`]),
|
||||||
|
Any,
|
||||||
|
Any
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
types = entries
|
||||||
|
.map((entry) => ({
|
||||||
|
address: entry.entity,
|
||||||
|
entry,
|
||||||
|
required: query.getObject(entry.address)?.attr['TYPE_REQUIRED']?.[0]
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aLabel = query.getObject(a.address)?.identify().join('/');
|
||||||
|
const bLabel = query.getObject(b.address)?.identify().join('/');
|
||||||
|
if (aLabel && bLabel) {
|
||||||
|
return aLabel.localeCompare(bLabel);
|
||||||
|
}
|
||||||
|
return a.address.localeCompare(b.address);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function add(ev: CustomEvent<SelectorValue>) {
|
async function add(ev: CustomEvent<SelectorValue>) {
|
||||||
if (!$entity || ev.detail.t !== 'Attribute') {
|
if (!$entity || ev.detail.t !== 'Attribute') {
|
||||||
|
@ -51,10 +78,26 @@
|
||||||
dispatch('change');
|
dispatch('change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setRequired(entry: UpEntry, required: boolean) {
|
||||||
|
if (required) {
|
||||||
|
await api.putEntry({
|
||||||
|
entity: entry.address,
|
||||||
|
attribute: 'TYPE_REQUIRED',
|
||||||
|
value: { t: 'Null', c: null }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const requiredAddress = types.find((t) => t.entry === entry)?.required?.address;
|
||||||
|
if (requiredAddress) {
|
||||||
|
await api.deleteEntry(requiredAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch('change');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if typeEntries.length || $entity?.attr['~IN']?.length}
|
{#if types.length || $entity?.attr['~IN']?.length}
|
||||||
<LabelBorder hide={typeEntries.length === 0}>
|
<LabelBorder hide={types.length === 0}>
|
||||||
<span slot="header">{$i18n.t('Type Attributes')}</span>
|
<span slot="header">{$i18n.t('Type Attributes')}</span>
|
||||||
{#if adding}
|
{#if adding}
|
||||||
<div class="selector">
|
<div class="selector">
|
||||||
|
@ -70,41 +113,34 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<ul class="attributes">
|
<div class="attributes">
|
||||||
{#each typeEntries as typeEntry}
|
{#each types as type}
|
||||||
<li class="attribute">
|
<div class="label">
|
||||||
<div class="label">
|
<UpObjectDisplay address={type.address} link />
|
||||||
<UpObjectDisplay address={typeEntry.entity} link />
|
</div>
|
||||||
</div>
|
<button
|
||||||
<div class="controls">
|
class:required={type.required}
|
||||||
<IconButton name="x-circle" on:click={() => remove(typeEntry)} />
|
on:click={() => setRequired(type.entry, !type.required)}
|
||||||
</div>
|
>
|
||||||
</li>
|
{type.required ? $i18n.t('Required') : $i18n.t('Optional')}
|
||||||
|
</button>
|
||||||
|
<div class="controls">
|
||||||
|
<IconButton name="x-circle" on:click={() => remove(type.entry)} />
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<li class="no-attributes">
|
<div class="no-attributes">
|
||||||
{$i18n.t('No attributes assigned to this type.')}
|
{$i18n.t('No attributes assigned to this type.')}
|
||||||
</li>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
<div class="add-button">
|
||||||
<div class="add-button">
|
<IconButton outline small name="plus-circle" on:click={() => (adding = true)} />
|
||||||
<IconButton outline small name="plus-circle" on:click={() => (adding = true)} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LabelBorder>
|
</LabelBorder>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.attributes {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attribute {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
@ -114,6 +150,41 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attributes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, auto));
|
||||||
|
gap: 0.25em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
&.required {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
grid-column: 1 / span 3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.selector {
|
.selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
@ -122,10 +193,4 @@
|
||||||
.no-attributes {
|
.no-attributes {
|
||||||
opacity: 0.66;
|
opacity: 0.66;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue