From ef71cf3b209196d368a582ff2799a3acc2d69e73 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Mon, 16 Mar 2026 23:11:16 +0100 Subject: [PATCH 1/5] first step in refactoring autocomplete: combining input and options Signed-off-by: Stephan Richter --- frontend/src/Components/Autocomplete.svelte | 99 +++++++++++-------- .../src/Components/PermissionEditor.svelte | 2 +- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/frontend/src/Components/Autocomplete.svelte b/frontend/src/Components/Autocomplete.svelte index 08d263fc..e5cc13a8 100644 --- a/frontend/src/Components/Autocomplete.svelte +++ b/frontend/src/Components/Autocomplete.svelte @@ -2,59 +2,72 @@ import { t } from '../translations.svelte.js' import { tick } from "svelte"; + let { - getCandidates = async text => { conole.log('no handler for getCandidates('+text+')'); return {};}, - onSelect = text => [] + getCandidates = dummyGetCandidates, + onCommit = dummyOnCommit, + onSelect = dummyOnSelect, } = $props(); - const ignore = ['Escape','Tab','ArrowUp','ArrowLeft','ArrowRight'] - let options = $state({}); - let text = $state('') + const ignore = ['Escape','Tab','ArrowUp','ArrowLeft','ArrowRight']; + let candidate = $state({ display : '' }); + let candidates = $derived(getCandidates(candidate.display)); - async function ondblclick(evt){ - const select = evt.target; - const key = select.value; - text = options[key]; - let result = {}; - result[key] = text; - options = {}; - text = ''; - onSelect(result); + + async function dummyGetCandidates(text){ + console.warn(`getCandidates(${text}) not overridden!`); + if (!text) return []; + return [ + { + display : 'candidate 1', + explanation : 'candidates need to have a display field' + }, + { + display : 'candidate 2', + additional : 'other fields are optional', + more : 'and may be domain specific' + }, + { display : text } + ]; } - async function onkeyup(evt){ - const select = evt.target; - const key = evt.key; + function dummyOnCommit(candidate){ + // if Enter is pressed on the input field, this method gets called with + // either the selected candidate or + // an anonymous object with the entered text in the display field + console.warn('onCommit not overridden!'); + } + + function dummyOnSelect(candidate){ + console.warn(`${candidate.display} selected, but onSelect not overridden!`) + } + + async function onkeyup(ev){ + const select = ev.target; + const key = ev.key; if (ignore.includes(key)) return; - if (key == 'ArrowDown'){ - if (select.selectedIndex == 0) select.selectedIndex=1; - return; - } - if (key == 'Enter'){ - ondblclick(evt); - return; - } - if (key == 'Backspace'){ - text = text.substring(0,text.length-1) - } else if (key.length<2){ - text += evt.key - } - options = await getCandidates(text); - await tick(); - for (let o of select.getElementsByTagName('option')) o.selected = false; + candidates = await getCandidates(candidate.display); } -{#if options} - + +
+ + {#if candidates && candidates.length > 1} + + {/if} +
+{#if candidates} +
+{JSON.stringify(candidates,null,2)}
+
{/if} +{JSON.stringify(candidate)} diff --git a/frontend/src/Components/PermissionEditor.svelte b/frontend/src/Components/PermissionEditor.svelte index 5e53d8d5..22c15639 100644 --- a/frontend/src/Components/PermissionEditor.svelte +++ b/frontend/src/Components/PermissionEditor.svelte @@ -66,7 +66,7 @@ {t('add_object',{object:t('member')})} - + From d3e5897cd54f3dc8eaf1ae68a65fc79a42a993ea Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 17 Mar 2026 00:35:49 +0100 Subject: [PATCH 2/5] completed autocomplete box in permission editor for projects Signed-off-by: Stephan Richter --- frontend/src/Components/Autocomplete.svelte | 66 ++++++++++++++----- .../src/Components/PermissionEditor.svelte | 13 ++-- frontend/src/routes/project/View.svelte | 22 +------ 3 files changed, 58 insertions(+), 43 deletions(-) diff --git a/frontend/src/Components/Autocomplete.svelte b/frontend/src/Components/Autocomplete.svelte index e5cc13a8..a3bd1f40 100644 --- a/frontend/src/Components/Autocomplete.svelte +++ b/frontend/src/Components/Autocomplete.svelte @@ -9,11 +9,11 @@ onSelect = dummyOnSelect, } = $props(); - const ignore = ['Escape','Tab','ArrowUp','ArrowLeft','ArrowRight']; + const ignore = ['ArrowLeft','ArrowRight']; let candidate = $state({ display : '' }); + let selected = $state([]); let candidates = $derived(getCandidates(candidate.display)); - async function dummyGetCandidates(text){ console.warn(`getCandidates(${text}) not overridden!`); if (!text) return []; @@ -35,7 +35,7 @@ // if Enter is pressed on the input field, this method gets called with // either the selected candidate or // an anonymous object with the entered text in the display field - console.warn('onCommit not overridden!'); + console.warn(`onCommit(${JSON.stringify(candidate)}) not overridden!`); } function dummyOnSelect(candidate){ @@ -43,31 +43,63 @@ } async function onkeyup(ev){ - const select = ev.target; - const key = ev.key; - if (ignore.includes(key)) return; + if (ignore.includes(ev.key)) return; + if (ev.key == 'ArrowDown'){ + ev.preventDefault(); + selected = selected.length < 1 ? [0] : [selected[0]+1] + if (selected[0] >= candidates.length) selected = [0]; + return false; + } + if (ev.key == 'ArrowUp'){ + ev.preventDefault(); + selected = selected.length < 1 ? [-1] : [selected[0]-1] + if (selected[0] < 0) selected = [candidates.length-1]; + return false; + } + if (ev.key == 'Enter'|| ev.key == 'Tab'){ + ev.preventDefault(); + if (selected.length>0) { + candidate = candidates[selected[0]]; + candidates = []; + selected = []; + onSelect(candidate); + return false; + } + if (ev.key == 'Enter') { + candidates = []; + selected = []; + onCommit(candidate); + } + return false; + } + if (ev.key == 'Escape'){ + ev.preventDefault(); + candidates = []; + selected = []; + return false; + } + candidates = await getCandidates(candidate.display); + if (selected>candidates.length) selected = candidates.length; + return false; }
- {#if candidates && candidates.length > 1} - + {#each candidates as candidate,i} + {/each} {/if} -
-{#if candidates} -
-{JSON.stringify(candidates,null,2)}
-
-{/if} -{JSON.stringify(candidate)} + \ No newline at end of file diff --git a/frontend/src/Components/PermissionEditor.svelte b/frontend/src/Components/PermissionEditor.svelte index 22c15639..ec5b56e1 100644 --- a/frontend/src/Components/PermissionEditor.svelte +++ b/frontend/src/Components/PermissionEditor.svelte @@ -1,8 +1,9 @@ @@ -65,7 +61,7 @@ {t('add_object',{object:t('member')})} - + diff --git a/frontend/src/routes/poll/Edit.svelte b/frontend/src/routes/poll/Edit.svelte index 41e240b4..44d80598 100644 --- a/frontend/src/routes/poll/Edit.svelte +++ b/frontend/src/routes/poll/Edit.svelte @@ -20,7 +20,7 @@ let members = $state([]); function addMember(member){ - for (let [id,name] of Object.entries(member)) update_permissions({user_id:+id,permission:4}); + update_permissions({user_id:+member.id,permission:4}); return true; } diff --git a/frontend/src/routes/project/View.svelte b/frontend/src/routes/project/View.svelte index fc4ecac4..24659282 100644 --- a/frontend/src/routes/project/View.svelte +++ b/frontend/src/routes/project/View.svelte @@ -29,7 +29,7 @@ let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]); async function addMember(user){ - update({new_member:+user.id}); + return await update({new_member:+user.id}); } async function addState(){ diff --git a/frontend/src/routes/task/Add.svelte b/frontend/src/routes/task/Add.svelte index fddb9cd9..0e9d5bcc 100644 --- a/frontend/src/routes/task/Add.svelte +++ b/frontend/src/routes/task/Add.svelte @@ -29,7 +29,8 @@ let router = useTinyRouter(); function addMember(member){ - for (let uid of Object.keys(member)) task.members[uid] = project.members[uid]; + let uid = member.id; + task.members[uid] = project.members[uid]; } function flat(json){ @@ -44,10 +45,9 @@ async function getCandidates(text){ const origin = parent_task ? parent_task.members : project.members; - const candidates = Object.values(origin) + return Object.values(origin) .filter(member => member.user.name.toLowerCase().includes(text.toLowerCase())) - .map(member => [member.user.id,member.user.name]); - return Object.fromEntries(candidates); + .map(member => {return { id:member.user.id,display:member.user.name}}); } async function load(){ diff --git a/frontend/src/routes/task/View.svelte b/frontend/src/routes/task/View.svelte index d8077dcb..d2a001d3 100644 --- a/frontend/src/routes/task/View.svelte +++ b/frontend/src/routes/task/View.svelte @@ -37,9 +37,8 @@ router.navigate(`/task/${id}/add_subtask`); } - async function addMember(entry){ - const ids = Object.keys(entry); - if (ids) update({new_member:+ids.pop()}); + async function addMember(newMember){ + return await update({new_member:+newMember.id}); } async function addTime(){ @@ -64,10 +63,10 @@ async function getCandidates(text){ const origin = task.parent ? task.parent.members : project.members; - const candidates = Object.values(origin) + return Object.values(origin) .filter(member => member.user.name.toLowerCase().includes(text.toLowerCase())) - .map(member => [member.user.id,member.user.name]); - return Object.fromEntries(candidates); + .map(member => { return {id:member.user.id,display:member.user.name}}); + } function gotoKanban(){ @@ -177,6 +176,8 @@ }); if (resp.ok){ yikes(); + let json = await resp.json(); + if (json.members) task.members = json.members; return true; } else { error(resp); diff --git a/frontend/src/routes/wiki/View.svelte b/frontend/src/routes/wiki/View.svelte index 78d87608..eb36254a 100644 --- a/frontend/src/routes/wiki/View.svelte +++ b/frontend/src/routes/wiki/View.svelte @@ -22,9 +22,8 @@ async function addMember(entry){ let newMembers = JSON.parse(JSON.stringify(page.members)); - for (var id of Object.keys(entry)){ - if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} }; - } + let id = entry.id; + if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} }; return patch({members:newMembers}); } @@ -74,7 +73,7 @@ }); if (resp.ok){ var json = await resp.json(); - return Object.fromEntries(Object.values(json).filter(nonMember).map(user => [user.id,user.name])); + return Object.values(json).filter(nonMember).map(user => { return {id:user.id,display:user.name}}); } else { return []; } From 5c0efe573025a9cca8c59477875c29880c6a7a30 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 17 Mar 2026 01:10:54 +0100 Subject: [PATCH 4/5] overhauled occurences of AutoComplete in PermissionEditor and UserSelector, overhauled Occurences of UserSelector and PermissionEditor Signed-off-by: Stephan Richter --- frontend/src/Components/PermissionEditor.svelte | 2 +- frontend/src/Components/UserSelector.svelte | 4 +--- frontend/src/routes/bookmark/Index.svelte | 4 +--- frontend/src/routes/company/Editor.svelte | 4 +--- frontend/src/routes/task/Add.svelte | 2 +- frontend/src/routes/task/View.svelte | 2 +- frontend/src/routes/wiki/View.svelte | 2 +- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/frontend/src/Components/PermissionEditor.svelte b/frontend/src/Components/PermissionEditor.svelte index af81e957..f28dc2c8 100644 --- a/frontend/src/Components/PermissionEditor.svelte +++ b/frontend/src/Components/PermissionEditor.svelte @@ -28,7 +28,7 @@ }); if (resp.ok){ var json = await resp.json(); - return Object.values(json).map(user => { return {id:user.id,name:user.name,display:user.name}; }); + return Object.values(json).map(user => { return {...user,display:user.name}; }); } else { return []; } diff --git a/frontend/src/Components/UserSelector.svelte b/frontend/src/Components/UserSelector.svelte index f7ea63bf..836db2f3 100644 --- a/frontend/src/Components/UserSelector.svelte +++ b/frontend/src/Components/UserSelector.svelte @@ -16,9 +16,7 @@ } function onSelect(entry){ - for (let [k,v] of Object.entries(entry)){ - users[k] = {name:v,id:k}; - } + users[entry.id] = entry; } let sortedUsers = $derived.by(() => Object.values(users).sort((a, b) => a.name.localeCompare(b.name))); diff --git a/frontend/src/routes/bookmark/Index.svelte b/frontend/src/routes/bookmark/Index.svelte index e9d4290c..bcf166a3 100644 --- a/frontend/src/routes/bookmark/Index.svelte +++ b/frontend/src/routes/bookmark/Index.svelte @@ -37,9 +37,7 @@ if (resp.ok){ yikes(); const input = await resp.json(); - return Object.fromEntries( - Object.entries(input).map(([key, value]) => [key, value.name]) - ); + return Object.values(input).map(user => {return {...user,display:user.name}}); } else { error(resp); return {}; diff --git a/frontend/src/routes/company/Editor.svelte b/frontend/src/routes/company/Editor.svelte index 833c841a..8579fdba 100644 --- a/frontend/src/routes/company/Editor.svelte +++ b/frontend/src/routes/company/Editor.svelte @@ -22,9 +22,7 @@ if (resp.ok){ yikes(); const input = await resp.json(); - return Object.fromEntries( - Object.entries(input).map(([key, value]) => [key, value.name]) - ); + return Object.values(input).map(user => { return {...user, display: user.name}}); } else { error(resp); return {}; diff --git a/frontend/src/routes/task/Add.svelte b/frontend/src/routes/task/Add.svelte index 0e9d5bcc..398a0b99 100644 --- a/frontend/src/routes/task/Add.svelte +++ b/frontend/src/routes/task/Add.svelte @@ -47,7 +47,7 @@ const origin = parent_task ? parent_task.members : project.members; return Object.values(origin) .filter(member => member.user.name.toLowerCase().includes(text.toLowerCase())) - .map(member => {return { id:member.user.id,display:member.user.name}}); + .map(member => {return { ...member.user,display:member.user.name}}); } async function load(){ diff --git a/frontend/src/routes/task/View.svelte b/frontend/src/routes/task/View.svelte index d2a001d3..8714ef88 100644 --- a/frontend/src/routes/task/View.svelte +++ b/frontend/src/routes/task/View.svelte @@ -65,7 +65,7 @@ const origin = task.parent ? task.parent.members : project.members; return Object.values(origin) .filter(member => member.user.name.toLowerCase().includes(text.toLowerCase())) - .map(member => { return {id:member.user.id,display:member.user.name}}); + .map(member => { return {...member.user,display:member.user.name}}); } diff --git a/frontend/src/routes/wiki/View.svelte b/frontend/src/routes/wiki/View.svelte index eb36254a..0cba778d 100644 --- a/frontend/src/routes/wiki/View.svelte +++ b/frontend/src/routes/wiki/View.svelte @@ -73,7 +73,7 @@ }); if (resp.ok){ var json = await resp.json(); - return Object.values(json).filter(nonMember).map(user => { return {id:user.id,display:user.name}}); + return Object.values(json).filter(nonMember).map(user => { return {...user,display:user.name}}); } else { return []; } From 2fcf02441023d9d0a6117c2b48435e48956214e3 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 17 Mar 2026 08:28:06 +0100 Subject: [PATCH 5/5] implemented mouse action on dropdown Signed-off-by: Stephan Richter --- frontend/src/Components/Autocomplete.svelte | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/Components/Autocomplete.svelte b/frontend/src/Components/Autocomplete.svelte index a3bd1f40..74eae485 100644 --- a/frontend/src/Components/Autocomplete.svelte +++ b/frontend/src/Components/Autocomplete.svelte @@ -42,6 +42,16 @@ console.warn(`${candidate.display} selected, but onSelect not overridden!`) } + async function ondblclick(evt){ + const select = evt.target; + const idx = select.value; + candidate = candidates[idx]; + candidates = []; + selected = []; + console.log(candidate); + onSelect(candidate); + } + async function onkeyup(ev){ if (ignore.includes(ev.key)) return; if (ev.key == 'ArrowDown'){ @@ -96,7 +106,7 @@
{#if candidates && candidates.length > 0} - {#each candidates as candidate,i} {/each}