<commit_before>﻿import * as React from "react";
import * as moment from "moment"
import * as numbro from "numbro"
import { Router, Route, Redirect } from "react-router"
import * as QueryString from "query-string"
import * as Navigator from "./Navigator"
import { Dic } from './Globals'
import { ajaxGet, ajaxPost } from './Services';

import {
    QueryDescription, QueryCountRequest, QueryRequest, QueryEntitiesRequest, FindOptions, 
    FindOptionsParsed, FilterOption, FilterOptionParsed, OrderOptionParsed, CountOptionsParsed,
    QueryToken, ColumnDescription, ColumnOption, ColumnOptionParsed, Pagination, ResultColumn,
    ResultTable, ResultRow, OrderOption, SubTokensOptions, toQueryToken, isList, ColumnOptionsMode, FilterRequest, ModalFindOptions
} from './FindOptions';

import { PaginationMode, OrderType, FilterOperation, FilterType, UniqueType, QueryTokenMessage } from './Signum.Entities.DynamicQuery';

import { Entity, Lite, toLite, liteKey, parseLite, EntityControlMessage, isLite, isEntityPack, isEntity, External } from './Signum.Entities';
import { TypeEntity, QueryEntity } from './Signum.Entities.Basics';

import {
    Type, IType, EntityKind, QueryKey, getQueryNiceName, getQueryKey, isQueryDefined, TypeReference,
    getTypeInfo, getTypeInfos, getEnumInfo, toMomentFormat, toNumbroFormat, PseudoType, EntityData,
    TypeInfo, PropertyRoute
} from './Reflection';

import SearchModal from './SearchControl/SearchModal';
import EntityLink from './SearchControl/EntityLink';
import SearchControlLoaded from './SearchControl/SearchControlLoaded';
import { ImportRoute } from "./AsyncImport";


export const querySettings: { [queryKey: string]: QuerySettings } = {};

export function start(options: { routes: JSX.Element[] }) {
    options.routes.push(<ImportRoute path="~/find/:queryName" onImportModule={() => _import("./SearchControl/SearchPage")} />);
}

export function addSettings(...settings: QuerySettings[]) {
    settings.forEach(s=> Dic.addOrThrow(querySettings, getQueryKey(s.queryName), s));
}

export function getQuerySettings(queryName: PseudoType | QueryKey): QuerySettings {
    return querySettings[getQueryKey(queryName)];
}

export const isFindableEvent: Array<(queryKey: string) => boolean> = [];

export function isFindable(queryName: PseudoType | QueryKey): boolean {

    if (!isQueryDefined(queryName))
        return false;

    const queryKey = getQueryKey(queryName);

    return isFindableEvent.every(f=> f(queryKey));
}

export function find(findOptions: FindOptions, modalOptions?:ModalFindOptions): Promise<Lite<Entity> | undefined>;
export function find<T extends Entity>(type: Type<T>, modalOptions?: ModalFindOptions): Promise<Lite<T> | undefined>;
export function find(obj: FindOptions | Type<any>, modalOptions?: ModalFindOptions): Promise<Lite<Entity> | undefined> {

    const fo = (obj as FindOptions).queryName ? obj as FindOptions :
        { queryName: obj as Type<any> } as FindOptions;

    return _import<{ default: typeof SearchModal }>("./SearchControl/SearchModal")
        .then(a => a.default.open(fo, modalOptions));
}

export function findMany(findOptions: FindOptions, modalOptions?: ModalFindOptions): Promise<Lite<Entity>[] | undefined>;
export function findMany<T extends Entity>(type: Type<T>, modalOptions?: ModalFindOptions): Promise<Lite<T>[] | undefined>;
export function findMany(findOptions: FindOptions | Type<any>, modalOptions?: ModalFindOptions): Promise<Lite<Entity>[] | undefined> {

    const fo = (findOptions as FindOptions).queryName ? findOptions as FindOptions :
        { queryName: findOptions as Type<any> } as FindOptions;

    return _import<{ default: typeof SearchModal }>("./SearchControl/SearchModal")
        .then(a => a.default.openMany(fo, modalOptions));
}

export function exploreWindowsOpen(findOptions: FindOptions, e: React.MouseEvent<any>) {
    if (e.ctrlKey || e.button == 1)
        window.open(findOptionsPath(findOptions));
    else
        explore(findOptions).done();
}

export function explore(findOptions: FindOptions, modalOptions?: ModalFindOptions): Promise<void> {

    return _import<{ default: typeof SearchModal }>("./SearchControl/SearchModal")
        .then(a => a.default.explore(findOptions, modalOptions));
}

export function findOptionsPath(fo: FindOptions, extra?: any): string {

    const query = findOptionsPathQuery(fo, extra);

    return Navigator.history.createHref({ pathname: "~/find/" + getQueryKey(fo.queryName), search: QueryString.stringify(query) });
}

export function findOptionsPathQuery(fo: FindOptions, extra?: any): any {
    fo = expandParentColumn(fo);

    const query = {
        columnMode: !fo.columnOptionsMode || fo.columnOptionsMode == "Add" as ColumnOptionsMode ? undefined : fo.columnOptionsMode,
        create: fo.create,
        navigate: fo.navigate,
        searchOnLoad: fo.searchOnLoad,
        showFilterButton: fo.showFilterButton,
        showFilters: fo.showFilters,
        showFooter: fo.showFooter,
        showHeader: fo.showHeader,
        allowChangeColumns: fo.allowChangeColumns,
        paginationMode: fo.pagination && fo.pagination.mode,
        elementsPerPage: fo.pagination && fo.pagination.elementsPerPage,
        currentPage: fo.pagination && fo.pagination.currentPage,
        ...extra
    };

    Encoder.encodeFilters(query, fo.filterOptions);
    Encoder.encodeOrders(query, fo.orderOptions);
    Encoder.encodeColumns(query, fo.columnOptions);

    return query;
}

export function getTypeNiceName(tr: TypeReference) {

    const niceName = tr.typeNiceName ||
        getTypeInfos(tr)
            .map(ti => ti == undefined ? getSimpleTypeNiceName(tr.name) : (ti.niceName || ti.name))
            .joinComma(External.CollectionMessage.Or.niceToString());

    return tr.isCollection ? QueryTokenMessage.ListOf0.niceToString(niceName) : niceName;
}

export function getSimpleTypeNiceName(name: string) {

    switch (name) {
        case "string":
        case "guid":
            return QueryTokenMessage.Text.niceToString();
        case "datetime": return QueryTokenMessage.DateTime.niceToString();
        case "number": return QueryTokenMessage.Number.niceToString();
        case "decimal": return QueryTokenMessage.DecimalNumber.niceToString();
        case "boolean": return QueryTokenMessage.Check.niceToString();
    }

    return name;
}


export function parseFindOptionsPath(queryName: PseudoType | QueryKey, query: any): FindOptions {
    
    const result = {
        queryName: queryName,
        filterOptions: Decoder.decodeFilters(query),
        orderOptions: Decoder.decodeOrders(query),
        columnOptions: Decoder.decodeColumns(query),
        columnOptionsMode: query.columnMode == undefined ? "Add" : query.columnMode,
        create: parseBoolean(query.create),
        navigate: parseBoolean(query.navigate),
        searchOnLoad: parseBoolean(query.searchOnLoad),
        showFilterButton: parseBoolean(query.showFilterButton),
        showFilters: parseBoolean(query.showFilters),
        showFooter: parseBoolean(query.showFooter),
        showHeader: parseBoolean(query.showHeader),
        pagination: query.paginationMode && {
            mode: query.paginationMode,
            elementsPerPage: query.elementsPerPage,
            currentPage: query.currentPage,
        } as Pagination,
    } as FindOptions;

    return result;
}

export function mergeColumns(columnDescriptions: ColumnDescription[], mode: ColumnOptionsMode, columnOptions: ColumnOption[]): ColumnOption[] {

    switch (mode) {
        case "Add":
            return columnDescriptions.filter(cd => cd.name != "Entity").map(cd => ({ columnName: cd.name, displayName: cd.displayName }) as ColumnOption)
                .concat(columnOptions);

        case "Remove":
            return columnDescriptions.filter(cd => cd.name != "Entity" && !columnOptions.some(a => a.columnName == cd.name))
                .map(cd => ({ columnName: cd.name, displayName: cd.displayName }) as ColumnOption);

        case "Replace":
            return columnOptions;

        default: throw new Error("Unexpected column mode");
    }
}

export function smartColumns(current: ColumnOptionParsed[], ideal: ColumnDescription[]): { mode: ColumnOptionsMode; columns: ColumnOption[] } {

    const similar = (c: ColumnOptionParsed, d: ColumnDescription) =>
        c.token!.fullKey == d.name && (c.displayName == d.displayName);

    ideal = ideal.filter(a => a.name != "Entity");

    current = current.filter(a => a.token != null);

    if (current.length < ideal.length) {
        const toRemove: ColumnOption[] = [];

        let j = 0;
        for (let i = 0; i < ideal.length; i++) {
            if (j < current.length && similar(current[j], ideal[i]))
                j++;
            else
                toRemove.push({ columnName: ideal[i].name, });
        }
        if (toRemove.length + current.length == ideal.length) {
            return {
                mode: "Remove",
                columns: toRemove
            };
        }
    }
    else if (current.every((c, i) => i >= ideal.length || similar(c, ideal[i]))) {
        return {
            mode: "Add",
            columns: current.slice(ideal.length).map(c => ({ columnName: c.token!.fullKey, displayName: c.displayName }) as ColumnOption)
        };
    }
    
    return {
        mode: "Replace",
        columns: current.map(c => ({ columnName: c.token!.fullKey, displayName: c.displayName }) as ColumnOption),
    };
}

function parseBoolean(value: any): boolean | undefined {
    if (value === "true" || value === true)
        return true;

    if (value === "false" || value === false)
        return false;

    return undefined;
}

export function parseFilterOptions(filterOptions: FilterOption[], qd: QueryDescription): Promise<FilterOptionParsed[]> {

    const completer = new TokenCompleter(qd);
    filterOptions.forEach(a => completer.request(a.columnName, SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll));

    return completer.finished()
        .then(() => filterOptions.map(fo => ({
            token: completer.get(fo.columnName),
            operation: fo.operation || "EqualTo",
            value: fo.value,
            frozen: fo.frozen || false,
        }) as FilterOptionParsed))
        .then(filters => parseFilterValues(filters).then(() => filters));
}

export function setFilters(e: Entity, filterOptionsParsed: FilterOptionParsed[]) : Promise<Entity> {

    function getMemberForToken(ti: TypeInfo, fullKey: string) {
        var token = fullKey.tryAfter("Entity.") || fullKey;

        if (token.contains("."))
            return null;

        return ti.members[token];
    }

    const ti = getTypeInfo(e.Type);

    return Promise.all(filterOptionsParsed.filter(fo => fo.token && fo.operation == "EqualTo").map(fo => {

        const mi = getMemberForToken(ti, fo.token!.fullKey);

        if (mi && (e as any)[mi.name] == null) {
            const promise = Navigator.tryConvert(fo.value, mi.type);

            if (promise == null)
                return null;

            return promise.then(v => (e as any)[mi.name.firstLower()] = v);
        }

        return null;
    }).filter(p => !!p)).then(() => e);
}

export function toFindOptions(fo: FindOptionsParsed, queryDescription: QueryDescription) {

    const pair = smartColumns(fo.columnOptions, Dic.getValues(queryDescription.columns));

    const qs = getQuerySettings(fo.queryKey);

    const defPagination = qs && qs.pagination || defaultPagination;

    function equalsPagination(p1: Pagination, p2: Pagination) {
        return p1.mode == p2.mode && p1.elementsPerPage == p2.elementsPerPage && p1.currentPage == p2.currentPage;<commit_msg>finder change fix<commit_after>﻿import * as React from "react";
import * as moment from "moment"
import * as numbro from "numbro"
import { Router, Route, Redirect } from "react-router"
import * as QueryString from "query-string"
import * as Navigator from "./Navigator"
import { Dic } from './Globals'
import { ajaxGet, ajaxPost } from './Services';

import {
    QueryDescription, QueryCountRequest, QueryRequest, QueryEntitiesRequest, FindOptions, 
    FindOptionsParsed, FilterOption, FilterOptionParsed, OrderOptionParsed, CountOptionsParsed,
    QueryToken, ColumnDescription, ColumnOption, ColumnOptionParsed, Pagination, ResultColumn,
    ResultTable, ResultRow, OrderOption, SubTokensOptions, toQueryToken, isList, ColumnOptionsMode, FilterRequest, ModalFindOptions
} from './FindOptions';

import { PaginationMode, OrderType, FilterOperation, FilterType, UniqueType, QueryTokenMessage } from './Signum.Entities.DynamicQuery';

import { Entity, Lite, toLite, liteKey, parseLite, EntityControlMessage, isLite, isEntityPack, isEntity, External } from './Signum.Entities';
import { TypeEntity, QueryEntity } from './Signum.Entities.Basics';

import {
    Type, IType, EntityKind, QueryKey, getQueryNiceName, getQueryKey, isQueryDefined, TypeReference,
    getTypeInfo, getTypeInfos, getEnumInfo, toMomentFormat, toNumbroFormat, PseudoType, EntityData,
    TypeInfo, PropertyRoute
} from './Reflection';

import SearchModal from './SearchControl/SearchModal';
import EntityLink from './SearchControl/EntityLink';
import SearchControlLoaded from './SearchControl/SearchControlLoaded';
import { ImportRoute } from "./AsyncImport";


export const querySettings: { [queryKey: string]: QuerySettings } = {};

export function start(options: { routes: JSX.Element[] }) {
    options.routes.push(<ImportRoute path="~/find/:queryName" onImportModule={() => _import("./SearchControl/SearchPage")} />);
}

export function addSettings(...settings: QuerySettings[]) {
    settings.forEach(s=> Dic.addOrThrow(querySettings, getQueryKey(s.queryName), s));
}

export function getQuerySettings(queryName: PseudoType | QueryKey): QuerySettings {
    return querySettings[getQueryKey(queryName)];
}

export const isFindableEvent: Array<(queryKey: string) => boolean> = [];

export function isFindable(queryName: PseudoType | QueryKey): boolean {

    if (!isQueryDefined(queryName))
        return false;

    const queryKey = getQueryKey(queryName);

    return isFindableEvent.every(f=> f(queryKey));
}

export function find(findOptions: FindOptions, modalOptions?:ModalFindOptions): Promise<Lite<Entity> | undefined>;
export function find<T extends Entity>(type: Type<T>, modalOptions?: ModalFindOptions): Promise<Lite<T> | undefined>;
export function find(obj: FindOptions | Type<any>, modalOptions?: ModalFindOptions): Promise<Lite<Entity> | undefined> {

    const fo = (obj as FindOptions).queryName ? obj as FindOptions :
        { queryName: obj as Type<any> } as FindOptions;

    return _import<{ default: typeof SearchModal }>("./SearchControl/SearchModal")
        .then(a => a.default.open(fo, modalOptions));
}

export function findMany(findOptions: FindOptions, modalOptions?: ModalFindOptions): Promise<Lite<Entity>[] | undefined>;
export function findMany<T extends Entity>(type: Type<T>, modalOptions?: ModalFindOptions): Promise<Lite<T>[] | undefined>;
export function findMany(findOptions: FindOptions | Type<any>, modalOptions?: ModalFindOptions): Promise<Lite<Entity>[] | undefined> {

    const fo = (findOptions as FindOptions).queryName ? findOptions as FindOptions :
        { queryName: findOptions as Type<any> } as FindOptions;

    return _import<{ default: typeof SearchModal }>("./SearchControl/SearchModal")
        .then(a => a.default.openMany(fo, modalOptions));
}

export function exploreWindowsOpen(findOptions: FindOptions, e: React.MouseEvent<any>) {
    if (e.ctrlKey || e.button == 1)
        window.open(findOptionsPath(findOptions));
    else
        explore(findOptions).done();
}

export function explore(findOptions: FindOptions, modalOptions?: ModalFindOptions): Promise<void> {

    return _import<{ default: typeof SearchModal }>("./SearchControl/SearchModal")
        .then(a => a.default.explore(findOptions, modalOptions));
}

export function findOptionsPath(fo: FindOptions, extra?: any): string {

    const query = findOptionsPathQuery(fo, extra);

    return Navigator.history.createHref({ pathname: "~/find/" + getQueryKey(fo.queryName), search: QueryString.stringify(query) });
}

export function findOptionsPathQuery(fo: FindOptions, extra?: any): any {
    fo = expandParentColumn(fo);

    const query = {
        columnMode: !fo.columnOptionsMode || fo.columnOptionsMode == "Add" as ColumnOptionsMode ? undefined : fo.columnOptionsMode,
        create: fo.create,
        navigate: fo.navigate,
        searchOnLoad: fo.searchOnLoad,
        showFilterButton: fo.showFilterButton,
        showFilters: fo.showFilters,
        showFooter: fo.showFooter,
        showHeader: fo.showHeader,
        allowChangeColumns: fo.allowChangeColumns,
        paginationMode: fo.pagination && fo.pagination.mode,
        elementsPerPage: fo.pagination && fo.pagination.elementsPerPage,
        currentPage: fo.pagination && fo.pagination.currentPage,
        ...extra
    };

    Encoder.encodeFilters(query, fo.filterOptions);
    Encoder.encodeOrders(query, fo.orderOptions);
    Encoder.encodeColumns(query, fo.columnOptions);

    return query;
}

export function getTypeNiceName(tr: TypeReference) {

    const niceName = tr.typeNiceName ||
        getTypeInfos(tr)
            .map(ti => ti == undefined ? getSimpleTypeNiceName(tr.name) : (ti.niceName || ti.name))
            .joinComma(External.CollectionMessage.Or.niceToString());

    return tr.isCollection ? QueryTokenMessage.ListOf0.niceToString(niceName) : niceName;
}

export function getSimpleTypeNiceName(name: string) {

    switch (name) {
        case "string":
        case "guid":
            return QueryTokenMessage.Text.niceToString();
        case "datetime": return QueryTokenMessage.DateTime.niceToString();
        case "number": return QueryTokenMessage.Number.niceToString();
        case "decimal": return QueryTokenMessage.DecimalNumber.niceToString();
        case "boolean": return QueryTokenMessage.Check.niceToString();
    }

    return name;
}


export function parseFindOptionsPath(queryName: PseudoType | QueryKey, query: any): FindOptions {
    
    const result = {
        queryName: queryName,
        filterOptions: Decoder.decodeFilters(query),
        orderOptions: Decoder.decodeOrders(query),
        columnOptions: Decoder.decodeColumns(query),
        columnOptionsMode: query.columnMode == undefined ? "Add" : query.columnMode,
        create: parseBoolean(query.create),
        navigate: parseBoolean(query.navigate),
        searchOnLoad: parseBoolean(query.searchOnLoad),
        showFilterButton: parseBoolean(query.showFilterButton),
        showFilters: parseBoolean(query.showFilters),
        showFooter: parseBoolean(query.showFooter),
        showHeader: parseBoolean(query.showHeader),
        pagination: query.paginationMode && {
            mode: query.paginationMode,
            elementsPerPage: query.elementsPerPage,
            currentPage: query.currentPage,
        } as Pagination,
    } as FindOptions;

    return result;
}

export function mergeColumns(columnDescriptions: ColumnDescription[], mode: ColumnOptionsMode, columnOptions: ColumnOption[]): ColumnOption[] {

    switch (mode) {
        case "Add":
            return columnDescriptions.filter(cd => cd.name != "Entity").map(cd => ({ columnName: cd.name, displayName: cd.displayName }) as ColumnOption)
                .concat(columnOptions);

        case "Remove":
            return columnDescriptions.filter(cd => cd.name != "Entity" && !columnOptions.some(a => a.columnName == cd.name))
                .map(cd => ({ columnName: cd.name, displayName: cd.displayName }) as ColumnOption);

        case "Replace":
            return columnOptions;

        default: throw new Error("Unexpected column mode");
    }
}

export function smartColumns(current: ColumnOptionParsed[], ideal: ColumnDescription[]): { mode: ColumnOptionsMode; columns: ColumnOption[] } {

    const similar = (c: ColumnOptionParsed, d: ColumnDescription) =>
        c.token!.fullKey == d.name && (c.displayName == d.displayName);

    ideal = ideal.filter(a => a.name != "Entity");

    current = current.filter(a => a.token != null);

    if (current.length < ideal.length) {
        const toRemove: ColumnOption[] = [];

        let j = 0;
        for (let i = 0; i < ideal.length; i++) {
            if (j < current.length && similar(current[j], ideal[i]))
                j++;
            else
                toRemove.push({ columnName: ideal[i].name, });
        }
        if (toRemove.length + current.length == ideal.length) {
            return {
                mode: "Remove",
                columns: toRemove
            };
        }
    }
    else if (current.every((c, i) => i >= ideal.length || similar(c, ideal[i]))) {
        return {
            mode: "Add",
            columns: current.slice(ideal.length).map(c => ({ columnName: c.token!.fullKey, displayName: c.displayName }) as ColumnOption)
        };
    }
    
    return {
        mode: "Replace",
        columns: current.map(c => ({ columnName: c.token!.fullKey, displayName: c.displayName }) as ColumnOption),
    };
}

function parseBoolean(value: any): boolean | undefined {
    if (value === "true" || value === true)
        return true;

    if (value === "false" || value === false)
        return false;

    return undefined;
}

export function parseFilterOptions(filterOptions: FilterOption[], qd: QueryDescription): Promise<FilterOptionParsed[]> {

    const completer = new TokenCompleter(qd);
    filterOptions.forEach(a => completer.request(a.columnName, SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll));

    return completer.finished()
        .then(() => filterOptions.map(fo => ({
            token: completer.get(fo.columnName),
            operation: fo.operation || "EqualTo",
            value: fo.value,
            frozen: fo.frozen || false,
        }) as FilterOptionParsed))
        .then(filters => parseFilterValues(filters).then(() => filters));
}

export function setFilters(e: Entity, filterOptionsParsed: FilterOptionParsed[]) : Promise<Entity> {

    function getMemberForToken(ti: TypeInfo, fullKey: string) {
        var token = fullKey.tryAfter("Entity.") || fullKey;

        if (token.contains("."))
            return null;

        return ti.members[token];
    }

    const ti = getTypeInfo(e.Type);

    return Promise.all(filterOptionsParsed.filter(fo => fo.token && fo.operation == "EqualTo").map(fo => {

        const mi = getMemberForToken(ti, fo.token!.fullKey);

        if (mi && (e as any)[mi.name.firstLower()] == null) {
            const promise = Navigator.tryConvert(fo.value, mi.type);

            if (promise == null)
                return null;

            return promise.then(v => (e as any)[mi.name.firstLower()] = v);
        }

        return null;
    }).filter(p => !!p)).then(() => e);
}

export function toFindOptions(fo: FindOptionsParsed, queryDescription: QueryDescription) {

    const pair = smartColumns(fo.columnOptions, Dic.getValues(queryDescription.columns));

    const qs = getQuerySettings(fo.queryKey);

    const defPagination = qs && qs.pagination || defaultPagination;

    function equalsPagination(p1: Pagination, p2: Pagination) {
        return p1.mode == p2.mode && p1.elementsPerPage == p2.elementsPerPage && p1.currentPage == p2.currentPage;
{'fraction_non_alphanumeric': 0.12520796894330447, 'fraction_numerical': 0.0010238471054989122, 'mean_word_length': 3.612750885478158, 'pattern_counts': {'":': 18, '<': 67, '<?xml version=': 0, '>': 121, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2111853', 'n_tokens_mistral': 6616, 'n_tokens_neox': 6527, 'n_words': 1543}