Compare commits

...

4 Commits

Author SHA1 Message Date
Tomáš Mládek 7e9d4349af feat(webui): upload to groups via EntityList
ci/woodpecker/push/woodpecker Pipeline was successful Details
(finishes #21)
2024-04-21 22:03:17 +02:00
Tomáš Mládek 426c584215 feat(webui): AddModal allows upload directly to groups
(addresses #21)
2024-04-21 22:03:17 +02:00
Tomáš Mládek 1118a5cfeb refactor(webui): typed Selector events 2024-04-21 22:03:17 +02:00
Tomáš Mládek e9dd4d1383 fix(webui): don't show editable label in UpObjectCard 2024-04-21 21:19:44 +02:00
10 changed files with 117 additions and 38 deletions

View File

@ -1,10 +1,12 @@
<script context="module" lang="ts">
import mitt from 'mitt';
import type { Address } from '@upnd/upend/types';
export type AddEvents = {
choose: void;
files: File[];
urls: string[];
destination: Address;
};
export const addEmitter = mitt<AddEvents>();
</script>
@ -18,18 +20,22 @@
import { i18n } from '$lib/i18n';
import { selected } from '$lib/components/EntitySelect.svelte';
import Modal from '$lib/components/layout/Modal.svelte';
import Selector, { type SelectorValue } from '$lib/components/utils/Selector.svelte';
import { ATTR_IN } from '@upnd/upend/constants';
let files: File[] = [];
let URLs: string[] = [];
let uploading = false;
let abortController: AbortController | undefined;
let destination: Address | undefined;
let progress: Record<string, number> = {};
let totalProgress: number | undefined;
let filesElement: HTMLDivElement;
$: visible = files.length + URLs.length > 0;
$: visible = files.length + URLs.length > 0 || destination;
addEmitter.on('files', (ev) => {
ev.forEach((file) => {
@ -40,6 +46,18 @@
});
});
addEmitter.on('destination', (ev) => {
destination = ev;
});
function onDestinationSelected(ev: CustomEvent<SelectorValue | undefined>) {
if (ev.detail?.t === 'Address') {
destination = ev.detail.c;
} else {
destination = undefined;
}
}
async function upload() {
uploading = true;
@ -59,6 +77,16 @@
},
timeout: -1
});
if (destination) {
await api.putEntry({
entity: address,
attribute: ATTR_IN,
value: {
t: 'Address',
c: destination
}
});
}
addresses.push(address);
if (!uploading) {
@ -91,6 +119,7 @@
URLs = [];
progress = {};
uploading = false;
destination = undefined;
}
function onKeydown(event: KeyboardEvent) {
@ -154,9 +183,20 @@
{/if}
</div>
<div class="controls">
<IconButton small disabled={uploading} name="upload" on:click={upload}>
{$i18n.t('Upload')}
</IconButton>
<div class="controls-destination">
<div class="label"><Icon plain name="download" /> {$i18n.t('Destination')}</div>
<Selector
initial={destination ? { t: 'Address', c: destination } : undefined}
types={['Address', 'NewAddress']}
placeholder={$i18n.t('Choose automatically') || ''}
on:input={onDestinationSelected}
/>
</div>
<div class="controls-submit">
<IconButton small disabled={uploading} name="upload" on:click={upload}>
{$i18n.t('Upload')}
</IconButton>
</div>
</div>
{#if uploading}
<div class="progress">
@ -260,8 +300,23 @@
.controls {
display: flex;
justify-content: center;
align-items: center;
font-size: 3em;
margin-top: 0.5rem;
gap: 1rem;
}
.controls-destination {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 1rem;
flex-grow: 3;
}
.controls-submit {
margin: 0 1rem;
}
.progress {

View File

@ -26,8 +26,8 @@
$: if (adding && selector) selector.focus();
async function add(ev: CustomEvent<SelectorValue>) {
if (ev.detail.t !== 'Address') {
async function add(ev: CustomEvent<SelectorValue | undefined>) {
if (ev.detail?.t !== 'Address') {
return;
}
dispatch('add', ev.detail.c);

View File

@ -50,8 +50,8 @@
});
}
async function add(ev: CustomEvent<SelectorValue>) {
if (!$entity || ev.detail.t !== 'Attribute') {
async function add(ev: CustomEvent<SelectorValue | undefined>) {
if (!$entity || ev.detail?.t !== 'Attribute') {
return;
}

View File

@ -229,9 +229,9 @@
resizeObserver.observe(viewEl as any);
});
async function onSelectorInput(ev: CustomEvent<SelectorValue>) {
async function onSelectorInput(ev: CustomEvent<SelectorValue | undefined>) {
const value = ev.detail;
if (value.t !== 'Address') return;
if (value?.t !== 'Address') return;
const address = value.c;
const [xValue, yValue] = selectorCoords as any;
@ -261,7 +261,7 @@
types={['Attribute', 'NewAttribute']}
initial={x ? { t: 'Attribute', name: x } : undefined}
on:input={(ev) => {
if (ev.detail.t === 'Attribute') x = ev.detail.name;
if (ev.detail?.t === 'Attribute') x = ev.detail.name;
}}
/>
<div class="value">
@ -277,7 +277,7 @@
types={['Attribute', 'NewAttribute']}
initial={y ? { t: 'Attribute', name: y } : undefined}
on:input={(ev) => {
if (ev.detail.t === 'Attribute') y = ev.detail.name;
if (ev.detail?.t === 'Attribute') y = ev.detail.name;
}}
/>
<div class="value">

View File

@ -22,7 +22,7 @@
</div>
{/if}
<div class="label">
<UpObject {address} {labels} {banner} {select} on:resolved />
<UpObject {address} {labels} {banner} {select} link on:resolved />
</div>
</div>
</UpLink>

View File

@ -28,7 +28,7 @@
lastSearched = lastSearched.slice(0, 10);
}
async function onInput(event: CustomEvent<SelectorValue>) {
async function onInput(event: CustomEvent<SelectorValue | undefined>) {
const value = event.detail;
if (!value) return;

View File

@ -20,7 +20,7 @@
$: if (editing && selector) selector.focus();
$: if (!focus && !hover) editing = false;
function onInput(ev: CustomEvent<SelectorValue>) {
function onInput(ev: CustomEvent<SelectorValue | undefined>) {
newValue = ev.detail;
selector.focus();
}

View File

@ -93,7 +93,10 @@
import debug from 'debug';
import Spinner from './Spinner.svelte';
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher<{
input: SelectorValue | undefined;
focus: boolean;
}>();
const dbg = debug('kestrel:Selector');
let selectorEl: HTMLElement;

View File

@ -11,6 +11,7 @@
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
import { createEventDispatcher } from 'svelte';
import type { WidgetChange } from '$lib/types/base';
import { addEmitter } from '$lib/components/AddModal.svelte';
import debug from 'debug';
const dispatch = createEventDispatcher();
const dbg = debug(`kestrel:EntityList`);
@ -123,7 +124,7 @@
$: if (adding && addSelector) addSelector.focus();
function addEntity(ev: CustomEvent<SelectorValue>) {
function addEntity(ev: CustomEvent<SelectorValue | undefined>) {
dbg('Adding entity', ev.detail);
const addAddress = ev.detail?.t == 'Address' ? ev.detail.c : undefined;
if (!addAddress) return;
@ -202,26 +203,39 @@
{#if address}
<div class="add">
{#if adding}
<Selector
bind:this={addSelector}
placeholder={$i18n.t('Search database or paste an URL') || ''}
types={['Address', 'NewAddress']}
on:input={addEntity}
on:focus={(ev) => {
if (!ev.detail) {
adding = false;
}
}}
/>
<div class="main">
<Selector
bind:this={addSelector}
placeholder={$i18n.t('Add or create an entry') || ''}
types={['Address', 'NewAddress']}
on:input={addEntity}
on:focus={(ev) => {
if (!ev.detail) {
adding = false;
}
}}
/>
</div>
{:else}
<IconButton
name="plus-circle"
outline
subdued
on:click={() => {
adding = true;
}}
/>
<div class="main">
<IconButton
name="plus-circle"
outline
subdued
on:click={() => {
adding = true;
}}
/>
</div>
{#if address}
<IconButton
outline
subdued
name="upload"
title={$i18n.t('Upload a file') || ''}
on:click={() => addEmitter.emit('destination', address || '')}
/>
{/if}
{/if}
</div>
{/if}
@ -318,7 +332,13 @@
.add {
display: flex;
flex-direction: column;
gap: 0.5em;
& .main {
display: flex;
flex-direction: column;
flex-grow: 1;
}
}
.entitylist.style-grid .add {

View File

@ -364,7 +364,8 @@
<div class="cell mark-attribute">
<Selector
types={['Attribute', 'NewAttribute']}
on:input={(ev) => (newEntryAttribute = ev.detail?.name)}
on:input={(ev) =>
(newEntryAttribute = ev.detail?.t === 'Attribute' ? ev.detail?.name : '')}
on:focus={(ev) => (addFocus = ev.detail)}
keepFocusOnSet
bind:this={newAttrSelector}