Merge branch 'refactor/autocomplete' into dev
This commit is contained in:
@@ -2,59 +2,114 @@
|
|||||||
import { t } from '../translations.svelte.js'
|
import { t } from '../translations.svelte.js'
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
getCandidates = async text => { conole.log('no handler for getCandidates('+text+')'); return {};},
|
getCandidates = dummyGetCandidates,
|
||||||
onSelect = text => []
|
onCommit = dummyOnCommit,
|
||||||
|
onSelect = dummyOnSelect,
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const ignore = ['Escape','Tab','ArrowUp','ArrowLeft','ArrowRight']
|
const ignore = ['ArrowLeft','ArrowRight'];
|
||||||
let options = $state({});
|
let candidate = $state({ display : '' });
|
||||||
let text = $state('')
|
let selected = $state([]);
|
||||||
|
let candidates = $derived(getCandidates(candidate.display));
|
||||||
|
|
||||||
|
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 }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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(${JSON.stringify(candidate)}) not overridden!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dummyOnSelect(candidate){
|
||||||
|
console.warn(`${candidate.display} selected, but onSelect not overridden!`)
|
||||||
|
}
|
||||||
|
|
||||||
async function ondblclick(evt){
|
async function ondblclick(evt){
|
||||||
const select = evt.target;
|
const select = evt.target;
|
||||||
const key = select.value;
|
const idx = select.value;
|
||||||
text = options[key];
|
candidate = candidates[idx];
|
||||||
let result = {};
|
candidates = [];
|
||||||
result[key] = text;
|
selected = [];
|
||||||
options = {};
|
console.log(candidate);
|
||||||
text = '';
|
onSelect(candidate);
|
||||||
onSelect(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onkeyup(evt){
|
async function onkeyup(ev){
|
||||||
const select = evt.target;
|
if (ignore.includes(ev.key)) return;
|
||||||
const key = evt.key;
|
if (ev.key == 'ArrowDown'){
|
||||||
if (ignore.includes(key)) return;
|
ev.preventDefault();
|
||||||
if (key == 'ArrowDown'){
|
selected = selected.length < 1 ? [0] : [selected[0]+1]
|
||||||
if (select.selectedIndex == 0) select.selectedIndex=1;
|
if (selected[0] >= candidates.length) selected = [0];
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (key == 'Enter'){
|
if (ev.key == 'ArrowUp'){
|
||||||
ondblclick(evt);
|
ev.preventDefault();
|
||||||
return;
|
selected = selected.length < 1 ? [-1] : [selected[0]-1]
|
||||||
|
if (selected[0] < 0) selected = [candidates.length-1];
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (key == 'Backspace'){
|
if (ev.key == 'Enter'|| ev.key == 'Tab'){
|
||||||
text = text.substring(0,text.length-1)
|
ev.preventDefault();
|
||||||
} else if (key.length<2){
|
if (selected.length>0) {
|
||||||
text += evt.key
|
candidate = candidates[selected[0]];
|
||||||
|
candidates = [];
|
||||||
|
selected = [];
|
||||||
|
onSelect(candidate);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ev.key == 'Enter') {
|
||||||
|
candidates = [];
|
||||||
|
selected = [];
|
||||||
|
onCommit(candidate);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
options = await getCandidates(text);
|
if (ev.key == 'Escape'){
|
||||||
await tick();
|
ev.preventDefault();
|
||||||
for (let o of select.getElementsByTagName('option')) o.selected = false;
|
candidates = [];
|
||||||
|
selected = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = await getCandidates(candidate.display);
|
||||||
|
if (selected>candidates.length) selected = candidates.length;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
select{
|
div { position : relative }
|
||||||
min-width: 200px;
|
select { position : absolute; top: 30px; left: 3px; }
|
||||||
}
|
|
||||||
|
select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; }
|
||||||
|
option:checked { background: orange; color: black; }
|
||||||
</style>
|
</style>
|
||||||
{#if options}
|
|
||||||
<select size={Object.keys(options).length<2?2:Object.keys(options).length+1} {onkeyup} {ondblclick} width="40">
|
<div>
|
||||||
<option>{text}</option>
|
<input type="text" bind:value={candidate.display} {onkeyup} />
|
||||||
{#each Object.entries(options) as [val,caption]}
|
{#if candidates && candidates.length > 0}
|
||||||
<option value={val}>{caption}</option>
|
<select bind:value={selected} {ondblclick} multiple tabindex="-1">
|
||||||
{/each}
|
{#each candidates as candidate,i}
|
||||||
</select>
|
<option value={i}>{candidate.display}</option>
|
||||||
{/if}
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { api } from '../urls.svelte';
|
import { api, get, post } from '../urls.svelte';
|
||||||
import { t } from '../translations.svelte';
|
import { t } from '../translations.svelte';
|
||||||
|
import { error, yikes } from '../warn.svelte';
|
||||||
|
|
||||||
import Autocomplete from './Autocomplete.svelte';
|
import Autocomplete from './Autocomplete.svelte';
|
||||||
import PermissionSelector from './PermissionSelector.svelte';
|
import PermissionSelector from './PermissionSelector.svelte';
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
});
|
});
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
var json = await resp.json();
|
var json = await resp.json();
|
||||||
return Object.fromEntries(Object.values(json).map(user => [user.id,user.name]));
|
return Object.values(json).map(user => { return {...user,display:user.name}; });
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -35,16 +36,10 @@
|
|||||||
|
|
||||||
async function loadPermissions(){
|
async function loadPermissions(){
|
||||||
const url = api('task/permissions');
|
const url = api('task/permissions');
|
||||||
const resp = await fetch(url,{credentials: 'include'});
|
const resp = await get(url);
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
permissions = await resp.json();
|
permissions = await resp.json();
|
||||||
} else {
|
} else error(resp);
|
||||||
message = await resp.text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelect(entry){
|
|
||||||
addMember(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(loadPermissions);
|
onMount(loadPermissions);
|
||||||
@@ -66,7 +61,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{t('add_object',{object:t('member')})}</td>
|
<td>{t('add_object',{object:t('member')})}</td>
|
||||||
<td>
|
<td>
|
||||||
<Autocomplete {getCandidates} {onSelect} />
|
<Autocomplete {getCandidates} onSelect={addMember} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -16,9 +16,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSelect(entry){
|
function onSelect(entry){
|
||||||
for (let [k,v] of Object.entries(entry)){
|
users[entry.id] = entry;
|
||||||
users[k] = {name:v,id:k};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let sortedUsers = $derived.by(() => Object.values(users).sort((a, b) => a.name.localeCompare(b.name)));
|
let sortedUsers = $derived.by(() => Object.values(users).sort((a, b) => a.name.localeCompare(b.name)));
|
||||||
|
|||||||
@@ -37,9 +37,7 @@
|
|||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
yikes();
|
yikes();
|
||||||
const input = await resp.json();
|
const input = await resp.json();
|
||||||
return Object.fromEntries(
|
return Object.values(input).map(user => {return {...user,display:user.name}});
|
||||||
Object.entries(input).map(([key, value]) => [key, value.name])
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
error(resp);
|
error(resp);
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -22,9 +22,7 @@
|
|||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
yikes();
|
yikes();
|
||||||
const input = await resp.json();
|
const input = await resp.json();
|
||||||
return Object.fromEntries(
|
return Object.values(input).map(user => { return {...user, display: user.name}});
|
||||||
Object.entries(input).map(([key, value]) => [key, value.name])
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
error(resp);
|
error(resp);
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
let members = $state([]);
|
let members = $state([]);
|
||||||
|
|
||||||
function addMember(member){
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,8 @@
|
|||||||
let new_state = $state({code:null,name:null})
|
let new_state = $state({code:null,name:null})
|
||||||
let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]);
|
let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]);
|
||||||
|
|
||||||
async function addMember(entry){
|
async function addMember(user){
|
||||||
const ids = Object.keys(entry);
|
return await update({new_member:+user.id});
|
||||||
if (ids) update({new_member:+ids.pop()});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addState(){
|
async function addState(){
|
||||||
@@ -67,21 +66,6 @@
|
|||||||
update({drop_member:member.user.id});
|
update({drop_member:member.user.id});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCandidates(text){
|
|
||||||
const url = api('user/search');
|
|
||||||
const resp = await fetch(url,{
|
|
||||||
credentials : 'include',
|
|
||||||
method : 'POST',
|
|
||||||
body : text
|
|
||||||
});
|
|
||||||
if (resp.ok){
|
|
||||||
var json = await resp.json();
|
|
||||||
return Object.fromEntries(Object.values(json).map(user => [user.id,user.name]));
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCreate(evt){
|
function handleCreate(evt){
|
||||||
let json = JSON.parse(evt.data);
|
let json = JSON.parse(evt.data);
|
||||||
json.event = 'create';
|
json.event = 'create';
|
||||||
@@ -242,7 +226,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="em">{t('members')}</div>
|
<div class="em">{t('members')}</div>
|
||||||
<div class="em">
|
<div class="em">
|
||||||
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} {getCandidates} />
|
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} />
|
||||||
</div>
|
</div>
|
||||||
{#if project.allowed_states}
|
{#if project.allowed_states}
|
||||||
{#each Object.keys(project.allowed_states) as key,idx}
|
{#each Object.keys(project.allowed_states) as key,idx}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
let router = useTinyRouter();
|
let router = useTinyRouter();
|
||||||
|
|
||||||
function addMember(member){
|
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){
|
function flat(json){
|
||||||
@@ -44,10 +45,9 @@
|
|||||||
|
|
||||||
async function getCandidates(text){
|
async function getCandidates(text){
|
||||||
const origin = parent_task ? parent_task.members : project.members;
|
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()))
|
.filter(member => member.user.name.toLowerCase().includes(text.toLowerCase()))
|
||||||
.map(member => [member.user.id,member.user.name]);
|
.map(member => {return { ...member.user,display:member.user.name}});
|
||||||
return Object.fromEntries(candidates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(){
|
async function load(){
|
||||||
|
|||||||
@@ -37,9 +37,8 @@
|
|||||||
router.navigate(`/task/${id}/add_subtask`);
|
router.navigate(`/task/${id}/add_subtask`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addMember(entry){
|
async function addMember(newMember){
|
||||||
const ids = Object.keys(entry);
|
return await update({new_member:+newMember.id});
|
||||||
if (ids) update({new_member:+ids.pop()});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addTime(){
|
async function addTime(){
|
||||||
@@ -64,10 +63,10 @@
|
|||||||
|
|
||||||
async function getCandidates(text){
|
async function getCandidates(text){
|
||||||
const origin = task.parent ? task.parent.members : project.members;
|
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()))
|
.filter(member => member.user.name.toLowerCase().includes(text.toLowerCase()))
|
||||||
.map(member => [member.user.id,member.user.name]);
|
.map(member => { return {...member.user,display:member.user.name}});
|
||||||
return Object.fromEntries(candidates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoKanban(){
|
function gotoKanban(){
|
||||||
@@ -177,6 +176,8 @@
|
|||||||
});
|
});
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
yikes();
|
yikes();
|
||||||
|
let json = await resp.json();
|
||||||
|
if (json.members) task.members = json.members;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
error(resp);
|
error(resp);
|
||||||
|
|||||||
@@ -22,9 +22,8 @@
|
|||||||
|
|
||||||
async function addMember(entry){
|
async function addMember(entry){
|
||||||
let newMembers = JSON.parse(JSON.stringify(page.members));
|
let newMembers = JSON.parse(JSON.stringify(page.members));
|
||||||
for (var id of Object.keys(entry)){
|
let id = entry.id;
|
||||||
if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} };
|
if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} };
|
||||||
}
|
|
||||||
return patch({members:newMembers});
|
return patch({members:newMembers});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@
|
|||||||
});
|
});
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
var json = await resp.json();
|
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 {...user,display:user.name}});
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user