Skip to content

Commit

Permalink
Add UserInput component (#1265)
Browse files Browse the repository at this point in the history
  • Loading branch information
duranb authored May 13, 2024
1 parent 8da1135 commit c61977f
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 162 deletions.
34 changes: 26 additions & 8 deletions src/components/model/ModelForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { getShortISOForDate } from '../../utilities/time';
import { tooltip } from '../../utilities/tooltip';
import Input from '../form/Input.svelte';
import UserInput from '../ui/Tags/UserInput.svelte';
export let initialModelDescription: string = '';
export let initialModelName: string = '';
Expand All @@ -17,6 +18,7 @@
export let modelId: number | undefined;
export let createdAt: string | undefined;
export let user: User | null;
export let users: UserId[] = [];
const dispatch = createEventDispatcher<{
createPlan: number;
Expand Down Expand Up @@ -69,6 +71,14 @@
dispatch('deleteModel');
}
function onSetOwner(event: CustomEvent<UserId[]>) {
owner = event.detail[0];
}
function onClearOwner() {
owner = null;
}
onDestroy(() => {
resetModelStores();
});
Expand Down Expand Up @@ -120,14 +130,22 @@
</Input>
<Input layout="inline">
<label for="owner">Owner</label>
<input
class="st-input w-100"
name="owner"
bind:value={owner}
use:permissionHandler={{
hasPermission: hasUpdateModelPermission,
permissionError: updateModelPermissionError,
}}
<UserInput
allowMultiple={false}
{users}
{user}
selectedUsers={owner ? [owner] : []}
use={[
[
permissionHandler,
{
hasPermission: hasUpdateModelPermission,
permissionError: updateModelPermissionError,
},
],
]}
on:create={onSetOwner}
on:delete={onClearOwner}
/>
</Input>
{#if createdAt}
Expand Down
160 changes: 38 additions & 122 deletions src/components/ui/Tags/PlanCollaboratorInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
import { createEventDispatcher } from 'svelte';
import type { User, UserId } from '../../../types/app';
import type { Plan, PlanCollaborator, PlanCollaboratorSlim, PlanSlimmer } from '../../../types/plan';
import type { PlanCollaboratorTag, Tag, TagsChangeEvent } from '../../../types/tags';
import type { ActionArray } from '../../../utilities/useActions';
import PlanCollaboratorInputRow from './PlanCollaboratorInputRow.svelte';
import TagsInput from './TagsInput.svelte';
import UserInput from './UserInput.svelte';
export let collaborators: PlanCollaboratorSlim[] = [];
export let users: UserId[] = [];
Expand All @@ -21,148 +19,66 @@
delete: string;
}>();
let inputRef: TagsInput | null;
let options: PlanCollaboratorTag[] = [];
let selected: Tag[] = [];
let groups: {
name: string;
users: UserId[];
}[] = [];
$: allowableCollaborators = users.filter(user => {
return !collaborators.find(collaborator => collaborator.collaborator === user);
});
$: userIsCollaborator = (userId: UserId) => {
return allowableCollaborators.indexOf(userId) < 0;
};
$: if (users) {
let newOptions: PlanCollaboratorTag[] = users.map(userToTag);
plans.forEach(p => {
// Filter out current plan
if (p.id !== plan.id) {
newOptions.push(planToTag(p));
}
});
// Filter out plans where owner and collaborators are already added
newOptions = newOptions.filter(tag => {
// If the tag is a user tag we don't need to filter on it
if (!tag.plan) {
return true;
}
// If owner is already a collaborator and there are no tag plan collaborators
// then this tag can be skipped
const ownerIsCollaborator = userIsCollaborator(tag.plan.owner);
if (ownerIsCollaborator && tag.plan.collaborators.length < 1) {
return false;
}
let anyTagCollaboratorsNotCollaborators = !!tag.plan.collaborators.find(
collaborator => !userIsCollaborator(collaborator.collaborator),
);
return !ownerIsCollaborator || anyTagCollaboratorsNotCollaborators;
});
options = newOptions.sort((optionA, optionB) => {
if (optionA.plan && !optionB.plan) {
return -1;
}
if (!optionA.plan && optionB.plan) {
return 1;
}
if (optionA.plan && optionB.plan) {
return optionA.plan.updated_at > optionB.plan.updated_at ? -1 : 1;
}
return optionA.name < optionB.name ? -1 : 0;
});
}
$: selected = collaborators.map(collaborator => userToTag(collaborator.collaborator));
let newGroupOptions: {
name: string;
users: UserId[];
}[] = [];
[...plans]
.sort((planA, planB) => {
return planA.updated_at > planB.updated_at ? -1 : 1;
})
.forEach(p => {
// Filter out current plan
if (p.id !== plan.id) {
newGroupOptions.push({
name: p.name,
users: [p.owner, ...p.collaborators.map(({ collaborator }) => `${collaborator}`)],
});
}
});
function getTagName(tag: PlanCollaboratorTag): string {
return tag.plan ? tag.plan.name : tag.name;
groups = newGroupOptions;
}
function compareTags(tagA: PlanCollaboratorTag, tagB: PlanCollaboratorTag): boolean {
return getTagName(tagA) === getTagName(tagB);
}
function addTag(event: CustomEvent<UserId[]>) {
const { detail: newUsers } = event;
const newCollaborators: PlanCollaborator[] = [];
function addTag(tag: PlanCollaboratorTag) {
inputRef?.closeSuggestions();
inputRef?.updatePopperPosition();
let newCollaborators: PlanCollaborator[] = [];
const tagPlan = tag.plan;
if (!tagPlan) {
// Username case
newCollaborators.push({ collaborator: tag.name, plan_id: plan.id });
} else {
// Add collaborators from plan if not already a collaborator on the target plan
const tagPlanCollaborators = tagPlan.collaborators.map(c => c.collaborator);
tagPlanCollaborators.forEach(userId => {
if (!userIsCollaborator(userId)) {
newCollaborators.push({ collaborator: userId, plan_id: plan.id });
}
});
newUsers.forEach(user => {
newCollaborators.push({ collaborator: user, plan_id: plan.id });
});
// Add plan owner if not already a collaborator in target/source plans
if (!userIsCollaborator(tagPlan.owner) && tagPlanCollaborators.indexOf(tagPlan.owner) < 0) {
newCollaborators.push({ collaborator: tagPlan.owner, plan_id: plan.id });
}
}
if (user) {
dispatch('create', newCollaborators);
allowableCollaborators = allowableCollaborators.filter(
collaborator => !newCollaborators.find(c => c.collaborator === collaborator),
);
selected = selected.concat(newCollaborators.map(c => userToTag(c.collaborator)));
}
}
function userToTag(userId: UserId): PlanCollaboratorTag {
return {
color: '#EBECEC',
created_at: '',
id: -1,
name: userId || 'Unk',
owner: '',
};
}
function planToTag(plan: PlanSlimmer): PlanCollaboratorTag {
return {
color: '#EBECEC',
created_at: '',
id: -1,
name: '',
owner: '',
plan,
};
}
async function onTagsInputChange(event: TagsChangeEvent) {
const {
detail: { tag, type },
} = event;
if (type === 'remove') {
dispatch('delete', tag.name);
}
}
</script>

<TagsInput
bind:this={inputRef}
{addTag}
ignoreCase={false}
<UserInput
placeholder="Search collaborators or plans"
creatable={false}
selectedUsers={collaborators.map(({ collaborator }) => collaborator)}
tagDisplayName="collaborator"
{compareTags}
{getTagName}
{options}
{selected}
minWidth={168}
on:change={onTagsInputChange}
let:prop={tag}
userGroups={groups}
users={allowableCollaborators}
{use}
>
<PlanCollaboratorInputRow {tag} {collaborators} />
</TagsInput>
{user}
on:create={addTag}
on:delete
/>

<style>
</style>
30 changes: 17 additions & 13 deletions src/components/ui/Tags/TagsInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
updatePopperPosition();
closeSuggestions();
};
export let allowMultiple: boolean = true;
export let createTagObject: (name: string) => Tag = (name: string) => {
return { color: generateRandomPastelColor(), created_at: '', id: -1, name, owner: '' };
};
Expand Down Expand Up @@ -239,19 +240,21 @@
<TagChip {tag} removable={!disabled} on:click={() => onTagRemove(tag)} {disabled} ariaRole="option" />
{/each}
{#if !disabled || (disabled && !selectedTags.length)}
<input
{id}
{name}
{disabled}
placeholder={disabled ? '' : placeholder}
class="st-input"
style:min-width={`${minWidth}px`}
on:mouseup={openSuggestions}
on:focus={openSuggestions}
on:keydown|stopPropagation={onKeydown}
bind:value={searchText}
bind:this={inputRef}
/>
{#if !(!allowMultiple && selectedTags.length)}
<input
{id}
{name}
{disabled}
placeholder={disabled ? '' : placeholder}
class="st-input"
style:min-width={`${minWidth}px`}
on:mouseup={openSuggestions}
on:focus={openSuggestions}
on:keydown|stopPropagation={onKeydown}
bind:value={searchText}
bind:this={inputRef}
/>
{/if}
{/if}
</div>
{#if suggestionsVisible}
Expand Down Expand Up @@ -311,6 +314,7 @@
display: flex;
gap: 8px;
max-height: 40vh;
overflow: hidden;
padding: 2px;
}
Expand Down
Loading

0 comments on commit c61977f

Please sign in to comment.