Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions libs/shared/src/lib/models/people.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** Model for a person returned by Common Services users query */
export interface People {
userid: string;
firstname?: string;
lastname?: string;
emailaddress?: string;
}

/** Variables for people search */
export interface SearchPeopleVars {
filter?: any;
first?: number;
skip?: number;
}

/** Search people GraphQL response */
export interface SearchPeopleQueryResponse {
users: People[];
}

/** Get people by id GraphQL response */
export interface GetPeopleByIdResponse {
users: People[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { gql } from 'apollo-angular';

/**
* Search people in Common Services
*/
export const SEARCH_PEOPLE = gql`
query SearchPeople($filter: JSON, $first: Int, $skip: Int) {
users(
limitItems: $first
offset: $skip
sortBy: { field: "firstname", direction: "ASC" }
filter: $filter
) {
userid
firstname
lastname
emailaddress
}
}
`;

/**
* Fetch people by userid
*/
export const GET_PEOPLE_BY_ID = gql`
query GetPeopleById($ids: [String!]) {
users(filter: { userid_in: $ids }) {
userid
firstname
lastname
emailaddress
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<ui-graphql-select
valueField="userid"
textField="userid"
[query]="query"
[formControl]="control"
[selectedElements]="initialSelection"
(searchChange)="onSearchChange($event)"
[filterable]="true"
[isSurveyQuestion]="true"
[searchDebounce]="searchDebounce"
[placeholder]="placeholder"
[displayFormatter]="displayFormatter"
>
</ui-graphql-select>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@


Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { CommonModule } from '@angular/common';
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { GraphQLSelectModule } from '@oort-front/ui';
import { Apollo, ApolloBase, QueryRef } from 'apollo-angular';
import { Subject, takeUntil } from 'rxjs';
import { GET_PEOPLE_BY_ID, SEARCH_PEOPLE } from './graphql/queries';
import {
GetPeopleByIdResponse,
People,
SearchPeopleQueryResponse,
SearchPeopleVars,
} from '../../../models/people.model';

/** Default placeholder text */
const DEFAULT_PLACEHOLDER = 'Begin typing and select';
/** Default page size */
const ITEMS_PER_PAGE = 10;
/** Default characters to trigger a search */
const MIN_SEARCH_LENGTH = 2;
/** Debounce time for search */
const DEBOUNCE_TIME = 500;

/**
* Dropdown component to search/select a person
*/
@Component({
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
TranslateModule,
GraphQLSelectModule,
],
selector: 'shared-people-dropdown',
templateUrl: './people-dropdown.component.html',
styleUrls: ['./people-dropdown.component.scss'],
})
export class PeopleDropdownComponent implements OnInit, OnDestroy {
/** Initial selected userid */
@Input() initialSelectionID: string | null = null;
/** Emits selected userid when selection changes */
@Output() selectionChange = new EventEmitter<string | null>();

/** Backing control for selected value */
public control = new FormControl<string | null>(null);
/** GraphQLSelect query */
public query!: QueryRef<SearchPeopleQueryResponse, SearchPeopleVars>;
/** Initial selection */
public initialSelection: People[] = [];
/** CS named client */
private csClient: ApolloBase;
/** Destroy notifier */
private destroy$ = new Subject<void>();

/** Placeholder, debounce, page size, and minimum search length (configurable via People Settings) */
public placeholder = DEFAULT_PLACEHOLDER;
/** Debounce in ms before triggering search */
public searchDebounce = DEBOUNCE_TIME;
/** Page size per request */
public pageSize = ITEMS_PER_PAGE;
/** Minimum characters required to trigger search */
public minSearchLength = MIN_SEARCH_LENGTH;

/**
* Display formatter for the select
*
* @param p Person item
* @returns The visible option label
*/
public displayFormatter = (p: People): string => {
const name = [p.firstname, p.lastname].filter(Boolean).join(' ').trim();
if (p.emailaddress) {
return name ? `${name} (${p.emailaddress})` : p.emailaddress;
}
return name;
};

/**
* Component to pick users from the list of users
*
* @param apollo Apollo client
*/
constructor(private apollo: Apollo) {
this.csClient = this.apollo.use('csClient');
}

ngOnInit(): void {
// Emit selection changes
this.control.valueChanges
?.pipe(takeUntil(this.destroy$))
.subscribe((value) => {
this.selectionChange.emit(value ?? null);
});

// Load initial selection if provided
if (this.initialSelectionID) {
this.csClient
.query<GetPeopleByIdResponse>({
query: GET_PEOPLE_BY_ID,
variables: { ids: [this.initialSelectionID] },
fetchPolicy: 'no-cache',
})
.pipe(takeUntil(this.destroy$))
.subscribe(({ data }) => {
const user = (data.users ?? [])[0];
if (user) {
this.initialSelection = [user];
this.control.setValue(user.userid, { emitEvent: false });
}
});
}
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}

/**
* Handles search updates from the text input.
*
* @param searchValue New search value
*/
public onSearchChange(searchValue: string): void {
const trimmed = (searchValue || '').trim();
if (trimmed.length < this.minSearchLength) {
if (this.query) {
const emptyFilter = JSON.stringify({ OR: [{ firstname_like: '' }] });
this.query.refetch({ filter: emptyFilter, first: 1, skip: 0 });
}
return;
}
const filter = JSON.stringify({
OR: [
{ firstname_like: trimmed },
{ lastname_like: trimmed },
{ emailaddress_like: trimmed },
],
});
if (!this.query) {
this.query = this.csClient.watchQuery<
SearchPeopleQueryResponse,
SearchPeopleVars
>({
query: SEARCH_PEOPLE,
variables: { filter, first: this.pageSize, skip: 0 },
fetchPolicy: 'no-cache',
});
} else {
this.query.refetch({ filter, first: this.pageSize, skip: 0 });
}
}
}
94 changes: 94 additions & 0 deletions libs/shared/src/lib/survey/components/people.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { ComponentCollection, Serializer, SvgRegistry } from 'survey-core';
import { DomService } from '../../services/dom/dom.service';
import { Question } from '../types';
import { PeopleDropdownComponent } from './people-dropdown/people-dropdown.component';

/**
* Inits the people component.
*
* @param componentCollectionInstance ComponentCollection
* @param domService DOM service.
*/
export const init = (
componentCollectionInstance: ComponentCollection,
domService: DomService
): void => {
// Registers icon-people in the SurveyJS library
SvgRegistry.registerIconFromSvg(
'people',
'<svg xmlns="http://www.w3.org/2000/svg" height="18px" viewBox="0 0 24 24" width="18px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5C15 14.17 10.33 13 8 13zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.96 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>'
);

const component = {
name: 'people',
title: 'People',
iconName: 'icon-people',
category: 'Custom Questions',
questionJSON: {
name: 'people',
type: 'dropdown',
placeholder: 'Begin typing and select',
optionsCaption: 'Begin typing and select',
choices: [] as any[],
},
onInit: (): void => {
Serializer.addProperty('people', {
name: 'placeholder',
category: 'People Settings',
visibleIndex: 3,
});
Serializer.addProperty('people', {
name: 'minSearchCharactersLength:number',
category: 'People Settings',
visibleIndex: 4,
});
Serializer.addProperty('people', {
name: 'pageSize:number',
category: 'People Settings',
visibleIndex: 5,
});
},
onAfterRender: (question: Question, el: HTMLElement) => {
const defaultDropdown = el.querySelector('kendo-combobox')?.parentElement;
if (defaultDropdown) {
defaultDropdown.style.display = 'none';
}

const peopleDropdown = domService.appendComponentToBody(
PeopleDropdownComponent,
el
);
const instance: PeopleDropdownComponent = peopleDropdown.instance;

instance.placeholder =
(question as any).placeholder || 'Begin typing and select';
instance.searchDebounce = (question as any).searchDebounce || 500;
instance.minSearchLength = (question as any).minSearchLength || 2;
if (
typeof (question as any).pageSize === 'number' &&
(question as any).pageSize > 0
) {
instance.pageSize = (question as any).pageSize;
}
if (question.value) instance.initialSelectionID = question.value as any;

instance.selectionChange.subscribe((userid: string | null) => {
(question as any).value = userid ?? null;
});

if ((question as any).isReadOnly) {
instance.control.disable();
}

question.registerFunctionOnPropertyValueChanged(
'readOnly',
(value: boolean) => {
if (value) instance.control.disable();
else instance.control.enable();
}
);
},
};

componentCollectionInstance.add(component);
};
3 changes: 3 additions & 0 deletions libs/shared/src/lib/survey/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as OwnerComponent from './components/owner';
import * as ResourceComponent from './components/resource';
import * as ResourcesComponent from './components/resources';
import * as UsersComponent from './components/users';
import * as PeopleComponent from './components/people';
import * as CsApiDocsProperties from './global-properties/cs-api-docs';
import * as OtherProperties from './global-properties/others';
import * as CommentWidget from './widgets/comment-widget';
Expand Down Expand Up @@ -44,6 +45,7 @@ const CUSTOM_COMPONENTS = [
'resources',
'owner',
'users',
'people',
'geospatial',
'editor',
];
Expand Down Expand Up @@ -129,6 +131,7 @@ export const initCustomSurvey = (
);
OwnerComponent.init(apollo, ComponentCollection.Instance);
UsersComponent.init(ComponentCollection.Instance, domService);
PeopleComponent.init(ComponentCollection.Instance, domService);
GeospatialComponent.init(domService, ComponentCollection.Instance);
EditorComponent.init(injector, ComponentCollection.Instance);
}
Expand Down
Loading