import * as React from 'react'; import { ElementModel, ElementIri, Dictionary } from '../data/model'; import { FilterParams } from '../data/provider'; import { Element as DiagramElement, FatLinkType, FatClassModel } from '../diagram/elements'; import { DiagramView } from '../diagram/view'; import { AsyncModel } from '../editor/asyncModel'; import { EventObserver } from '../viewUtils/events'; import { ProgressBar, ProgressState } from '../widgets/progressBar'; import { SearchResults } from './searchResults'; import { WorkspaceContextTypes, WorkspaceContextWrapper, WorkspaceEventKey } from '../workspace/workspaceContext'; const DirectionInImage = require('../../../images/direction-in.png'); const DirectionOutImage = require('../../../images/direction-out.png'); export interface InstancesSearchProps { className?: string; model: AsyncModel; view: DiagramView; criteria: SearchCriteria; onCriteriaChanged: (criteria: SearchCriteria) => void; } export interface SearchCriteria { readonly text?: string; readonly elementType?: FatClassModel; readonly refElement?: DiagramElement; readonly refElementLink?: FatLinkType; readonly linkDirection?: 'in' | 'out'; } export interface State { readonly inputText?: string; readonly quering?: boolean; readonly resultId?: number; readonly error?: any; readonly items?: ReadonlyArray; readonly selection?: ReadonlySet; readonly moreItemsAvailable?: boolean; } const CLASS_NAME = 'ontodia-instances-search'; export class InstancesSearch extends React.Component { static contextTypes = WorkspaceContextTypes; readonly context: WorkspaceContextWrapper; private readonly listener = new EventObserver(); private currentRequest: FilterParams; constructor(props: InstancesSearchProps, context: any) { super(props, context); this.state = { resultId: 0, selection: new Set(), }; } render() { const ENTER_KEY_CODE = 13; const className = `${CLASS_NAME} ${this.props.className || ''}`; const progressState = ( this.state.quering ? ProgressState.loading : this.state.error ? ProgressState.error : this.state.items ? ProgressState.completed : ProgressState.none ); const searchTerm = this.state.inputText === undefined ? this.props.criteria.text : this.state.inputText; return
{this.renderCriteria()}
this.setState({inputText: e.currentTarget.value})} onKeyUp={e => { if (e.keyCode === ENTER_KEY_CODE) { this.submitCriteriaUpdate(); } }} />
{/* specify resultId as key to reset scroll position when loaded new search results */}
; } private onSelectionChanged = (newSelection: ReadonlySet) => { this.setState({selection: newSelection}); } private renderCriteria(): React.ReactElement { const {criteria = {}, view} = this.props; const criterions: React.ReactElement[] = []; if (criteria.elementType) { const classInfo = criteria.elementType; const classLabel = view.formatLabel(classInfo.label, classInfo.id); criterions.push(
{this.renderRemoveCriterionButtons(() => this.props.onCriteriaChanged( {...this.props.criteria, elementType: undefined}))} Has type {classLabel}
); } else if (criteria.refElement) { const element = criteria.refElement; const elementLabel = view.formatLabel(element.data.label.values, element.iri); const linkType = criteria.refElementLink; const linkTypeLabel = linkType ? view.formatLabel(linkType.label, linkType.id) : undefined; criterions.push(
{this.renderRemoveCriterionButtons(() => this.props.onCriteriaChanged( {...this.props.criteria, refElement: undefined, refElementLink: undefined}))} Connected to {elementLabel} {linkType && {' through '} {linkTypeLabel} {criteria.linkDirection === 'in' && {' as '} source } {criteria.linkDirection === 'out' && {' as '} target } }
); } return
{criterions}
; } private renderRemoveCriterionButtons(onClick: () => void) { return
; } private submitCriteriaUpdate() { let text = this.state.inputText === undefined ? this.props.criteria.text : this.state.inputText; text = text === '' ? undefined : text; this.props.onCriteriaChanged({...this.props.criteria, text}); } componentDidMount() { this.listener.listen(this.props.view.events, 'changeLanguage', () => this.forceUpdate()); this.queryItems(false); } componentWillReceiveProps(nextProps: InstancesSearchProps) { const languageChanged = this.currentRequest ? this.currentRequest.languageCode !== nextProps.view.getLanguage() : false; if (this.props.criteria !== nextProps.criteria || languageChanged) { this.setState({inputText: undefined}, () => this.queryItems(false)); } } componentWillUnmount() { this.listener.stopListening(); this.currentRequest = undefined; } private queryItems(loadMoreItems: boolean) { let request: FilterParams; if (loadMoreItems) { if (!this.currentRequest) { throw new Error('Cannot request more items without initial request.'); } const {offset, limit} = this.currentRequest; request = {...this.currentRequest, offset: offset + limit}; } else { request = createRequest(this.props.criteria, this.props.view.getLanguage()); } if (!(request.text || request.elementTypeId || request.refElementId || request.refElementLinkId)) { this.setState({ quering: false, error: undefined, items: undefined, selection: new Set(), moreItemsAvailable: false, }); return; } this.currentRequest = request; this.setState({ quering: true, error: undefined, moreItemsAvailable: false, }); this.props.model.dataProvider.filter(request).then(elements => { if (this.currentRequest !== request) { return; } this.processFilterData(elements); this.context.ontodiaWorkspace.triggerWorkspaceEvent(WorkspaceEventKey.searchQueryItem); }).catch(error => { if (this.currentRequest !== request) { return; } // tslint:disable-next-line:no-console console.error(error); this.setState({quering: false, error}); }); } private processFilterData(elements: Dictionary) { const requestedAdditionalItems = this.currentRequest.offset > 0; const existingIris: { [iri: string]: true } = {}; if (requestedAdditionalItems) { this.state.items.forEach(item => existingIris[item.id] = true); } const items = requestedAdditionalItems ? [...this.state.items] : []; for (const iri in elements) { if (!elements.hasOwnProperty(iri)) { continue; } if (existingIris[iri]) { continue; } items.push(elements[iri]); } const moreItemsAvailable = Object.keys(elements).length >= this.currentRequest.limit; if (requestedAdditionalItems) { this.setState({quering: false, items, error: undefined, moreItemsAvailable}); } else { this.setState({ quering: false, resultId: this.state.resultId + 1, items, selection: new Set(), error: undefined, moreItemsAvailable, }); } } } export function createRequest(criteria: SearchCriteria, language: string): FilterParams { const {text, elementType, refElement, refElementLink, linkDirection} = criteria; return { text, elementTypeId: elementType ? elementType.id : undefined, refElementId: refElement ? refElement.iri : undefined, refElementLinkId: refElementLink ? refElementLink.id : undefined, linkDirection, offset: 0, limit: 100, languageCode: language || 'en', }; }