diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index 0cf23b5..7ff64bc 100644 --- a/src/components/ProgressBar.tsx +++ b/src/components/ProgressBar.tsx @@ -11,6 +11,8 @@ const ProgressWrapper = styled.div` const ProgressLabel = styled(Label)` display: inline-block; + text-overflow: ellipsis; + direction: rtl; overflow: hidden; width: 10rem; margin-right: 1rem; diff --git a/src/components/form/fields/ConnectedSelect.tsx b/src/components/form/fields/ConnectedSelect.tsx index 31b1d62..04c2474 100644 --- a/src/components/form/fields/ConnectedSelect.tsx +++ b/src/components/form/fields/ConnectedSelect.tsx @@ -9,6 +9,7 @@ import { compose } from 'redux'; import { default as metaDataResolver, MetaDataProps } from '../../../services/metaDataResolver'; import { RDF_TYPE } from '../../../constants/global'; import verifyResponse from '../../../services/verifyResponse'; +import { branch, renderNothing } from 'recompose'; interface OwnProps extends SelectProps, CollectionEditViewProps { onChange: (value: string, property: Property) => void; @@ -52,5 +53,6 @@ const SelectField: SFC = ({ name, selected, metadata, onChange, shownAsMu export default compose>( withRouter, metaDataResolver(QUERY_COLLECTION_EDIT_VIEW), + branch((props: Props) => props.metadata.loading, renderNothing), verifyResponse('metadata', 'dataSetMetadata') )(SelectField); diff --git a/src/components/routes/Entry.tsx b/src/components/routes/Entry.tsx index a7dd54a..49f2882 100644 --- a/src/components/routes/Entry.tsx +++ b/src/components/routes/Entry.tsx @@ -60,7 +60,7 @@ const dataResolver = compose>( verifyResponse('metadata', 'dataSetMetadata'), graphqlWithProps(QUERY_ENTRY_VALUES), renderLoader(), - verifyResponse('data', 'dataSetMetadata.collection') + verifyResponse('data', 'dataSets') ); export default dataResolver(Entry); diff --git a/src/components/routes/FacetConfig.tsx b/src/components/routes/FacetConfig.tsx index 53a1a00..bc86b85 100644 --- a/src/components/routes/FacetConfig.tsx +++ b/src/components/routes/FacetConfig.tsx @@ -16,6 +16,10 @@ import graphToState from '../../services/graphToState'; import { RootState } from '../../reducers/rootReducer'; import { NormalizedFacetConfig } from '../../typings'; import verifyResponse from '../../services/verifyResponse'; +import { ChildProps } from 'react-apollo'; +import graphql from 'react-apollo/graphql'; +import { FacetConfig } from '../../typings/schema'; +import gql from 'graphql-tag'; interface StateProps { normalizedFacets: NormalizedFacetConfig[]; @@ -26,17 +30,40 @@ type FullProps = MetaDataProps & RouteComponentProps<{ dataSet: string; collection: string }> & FormWrapperProps; +interface GraphIndexConfig { + facet: FacetConfig[]; + fullText?: { + caption: ''; + fields: { path: string }[]; + }; +} + +type GraphProps = ChildProps; + const Section = styled.div` width: 100%; padding-bottom: 3rem; `; -const FacetConfig: SFC = props => { - const onSubmit = () => { - const facetconfig = denormalizeFacets(props.normalizedFacets); - console.groupCollapsed('sending facet config:'); - console.log(facetconfig); - console.groupEnd(); +const FacetConfig: SFC = props => { + const onSubmit = async () => { + const { dataSetId, collection } = props.metadata.dataSetMetadata!; + + try { + const facet = denormalizeFacets(props.normalizedFacets); + const indexConfig: GraphIndexConfig = { + facet, + fullText: { + caption: '', + fields: [] + } // TODO: Implement a full text configurable option for the facets + }; + + await props.mutate!({ variables: { dataSet: dataSetId, collectionUri: collection!.uri, indexConfig } }); + alert(`The facet configuration for collection ${collection!.collectionId} has been updated`); + } catch (e) { + alert(e); // TODO: Make this fancy, I'd suggest to maybe add an optional error to NormalizedFacetConfig, add a scrollTo and style the selectBox accordingly + } }; return ( @@ -50,6 +77,14 @@ const FacetConfig: SFC = props => { ); }; +const submitFacetConfig = gql` + mutation submitFacetConfig($dataSet: ID!, $collectionUri: String!, $indexConfig: IndexConfigInput!) { + setIndexConfig(dataSet: $dataSet, collectionUri: $collectionUri, indexConfig: $indexConfig) { + __typename + } + } +`; + const mapStateToProps = (state: RootState) => ({ normalizedFacets: state.facetconfig }); @@ -59,6 +94,7 @@ export default compose>( metaDataResolver(QUERY_COLLECTION_PROPERTIES), renderLoader('metadata'), verifyResponse('metadata', 'dataSetMetadata.collection'), + graphql(submitFacetConfig), connect(mapStateToProps), graphToState('GRAPH_TO_FACETCONFIG', 'metadata', false) )(FacetConfig); diff --git a/src/components/routes/Search.tsx b/src/components/routes/Search.tsx index 2dddd00..49648c6 100644 --- a/src/components/routes/Search.tsx +++ b/src/components/routes/Search.tsx @@ -1,7 +1,7 @@ import React, { ComponentType, SFC } from 'react'; import { connect, Dispatch } from 'react-redux'; import { compose } from 'redux'; -import verifyResponse from '../../services/verifyResponse'; +import verifyResponse, { handleError } from '../../services/verifyResponse'; import { ChildProps } from '../../typings'; import { lifecycle, withProps } from 'recompose'; import { RouteComponentProps, withRouter } from 'react-router'; @@ -133,15 +133,11 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ const dataResolver = compose>( withRouter, - metaDataResolver(QUERY_COLLECTION_PROPERTIES), + metaDataResolver(QUERY_COLLECTION_PROPERTIES), // TODO: Need to think about a refetch option, it now caches configuration for facets renderLoader('metadata'), verifyResponse('metadata', 'dataSetMetadata.collection'), graphqlWithProps(QUERY_COLLECTION_VALUES), - verifyResponse( - 'data', - props => - `dataSets.${props.match.params.dataSet}.${props.metadata.dataSetMetadata!.collection!.collectionListId}` - ), + handleError('data'), connect(mapStateToProps, mapDispatchToProps), withProps(({ data, metadata }: FullProps): ExtraProps => ({ collectionValues: getCollectionValues(data, metadata) diff --git a/src/components/routes/ViewConfig.tsx b/src/components/routes/ViewConfig.tsx index 899cd58..0d2e3ef 100644 --- a/src/components/routes/ViewConfig.tsx +++ b/src/components/routes/ViewConfig.tsx @@ -35,17 +35,16 @@ const Section = styled.div` `; const ViewConfig: SFC = props => { - const onSubmit = () => { + const onSubmit = async () => { const { dataSetId, collection } = props.metadata.dataSetMetadata!; - const viewConfig = props.denormalizeTree(); - if (typeof viewConfig === 'string') { - return alert(viewConfig); // TODO: Make this fancy, I'd suggest to maybe add an optional error to NormalizedComponentConfig, add a scrollTo and style the selectBox accordingly + try { + const viewConfig = props.denormalizeTree(); + await props.mutate!({ variables: { dataSet: dataSetId, collectionUri: collection!.uri, viewConfig } }); + alert(`The collection ${collection!.collectionId} has been updated`); + } catch (e) { + alert(e); // TODO: Make this fancy, I'd suggest to maybe add an optional error to NormalizedComponentConfig, add a scrollTo and style the selectBox accordingly } - - return props.mutate!({ variables: { dataSet: dataSetId, collectionUri: collection!.uri, viewConfig } }) - .then(data => alert(`The collection ${collection!.collectionId} has been updated`)) // TODO: This also should be something fancy - .catch((err: Error) => console.error('there was an error sending the query', err)); }; return ( diff --git a/src/reducers/facetconfig.ts b/src/reducers/facetconfig.ts index d6ed365..ec45542 100644 --- a/src/reducers/facetconfig.ts +++ b/src/reducers/facetconfig.ts @@ -2,6 +2,7 @@ import { CollectionMetadata, FacetConfig } from '../typings/schema'; import { NormalizedFacetConfig, ReferencePath } from '../typings/index'; import { arrayMove } from 'react-sortable-hoc'; import { createReferencePath, mendPath } from '../services/walkPath'; +import { facetErrors } from '../services/Validation'; import { MetaDataProps } from '../services/metaDataResolver'; // state def @@ -63,19 +64,27 @@ export const getById = (id: number, state: FacetConfigReducer): NormalizedFacetC state.find(config => config.id === id); export const denormalizeFacetConfig = (config: NormalizedFacetConfig): FacetConfig => { - config = { ...config }; + const denormalizedConfig = { ...config, paths: [] as string[] }; - for (const [idx, referencePath] of config.referencePaths.entries()) { - config.paths[idx] = mendPath(referencePath); + for (const referencePath of config.referencePaths) { + const error = facetErrors(referencePath, config); + + if (error) { + throw error; + } + denormalizedConfig.paths.push(mendPath(referencePath)); } - delete config.referencePaths; - delete config.id; - return config; + delete denormalizedConfig.referencePaths; + delete denormalizedConfig.id; + if (denormalizedConfig.__typename) { + delete denormalizedConfig.__typename; + } + return denormalizedConfig; }; export const denormalizeFacets = (facetConfigs: NormalizedFacetConfig[]): FacetConfig[] => - facetConfigs.map(denormalizeFacetConfig); // TODO: make sure it returns a message or something in case of error + facetConfigs.map(denormalizeFacetConfig); // reducer const references = (payload: { facetConfig: { paths: string[] }; collectionId: string }): ReferencePath[] => { diff --git a/src/reducers/search.ts b/src/reducers/search.ts index e8eb169..8fc26c6 100644 --- a/src/reducers/search.ts +++ b/src/reducers/search.ts @@ -1,6 +1,7 @@ import { Facet, FacetConfig, FacetOption } from '../typings/schema'; import { Location } from 'history'; import * as queryString from 'querystring'; +import { EsValuePath } from '../services/EsQueryStringCreator'; export interface FullTextSearch { dataset: Readonly; @@ -75,7 +76,9 @@ const mergeFacets = (configs: FacetConfig[], facets: Facet[]): EsFilter[] => const setNewSelected = (newFilters: EsFilter[], key: string, valueName: string): void => { newFilters.forEach((newFilter, filterIdx) => { newFilter.paths.forEach(path => { - if (path === key) { + const trimmedPath = EsValuePath(path); + + if (trimmedPath === key) { newFilter.values.forEach((newValue, valueIdx) => { if (newValue.name === valueName) { const value = { ...newValue, selected: true }; @@ -104,8 +107,7 @@ export const mergeOldSelected = (newFilters: EsFilter[], location: Location): vo matches.bool.should.forEach(obj => { const key = Object.keys(obj.match)[0]; - const value = obj.match[key]; - return setNewSelected(newFilters, key.slice(0, -4), value); + return setNewSelected(newFilters, key, obj.match[key]); }); } ); diff --git a/src/reducers/viewconfig.ts b/src/reducers/viewconfig.ts index f139fbd..94b7087 100644 --- a/src/reducers/viewconfig.ts +++ b/src/reducers/viewconfig.ts @@ -135,7 +135,7 @@ const denormalizePath = (childNode: NormalizedComponentConfig): NormalizedCompon return childNode; }; -const denormalizeNode = (item: NormalizedComponentConfig, state: ViewConfigReducer): ComponentConfig | string => { +const denormalizeNode = (item: NormalizedComponentConfig, state: ViewConfigReducer): ComponentConfig => { item = denormalizePath(item); item.subComponents = []; @@ -146,12 +146,12 @@ const denormalizeNode = (item: NormalizedComponentConfig, state: ViewConfigReduc if (childNode) { const error = componentErrors(childNode); if (error) { - return error; + throw error; } const denormalizedChildNode = denormalizeNode(childNode, state); - if (denormalizedChildNode && typeof denormalizedChildNode !== 'string') { + if (denormalizedChildNode) { item.subComponents.push(denormalizedChildNode); } } @@ -168,7 +168,7 @@ const denormalizeNode = (item: NormalizedComponentConfig, state: ViewConfigReduc return item; }; -export const denormalizeTree = (state: ViewConfigReducer): ComponentConfig[] | string => { +export const denormalizeTree = (state: ViewConfigReducer): ComponentConfig[] => { let denormalizedTree: ComponentConfig[] = []; const parentNode = getNodeById(0, state)!; @@ -178,16 +178,12 @@ export const denormalizeTree = (state: ViewConfigReducer): ComponentConfig[] | s if (rootNode) { const error = componentErrors(rootNode); if (error) { - return error; + throw error; } const denormalizedRootNode = denormalizeNode(rootNode, state); if (denormalizedRootNode) { - if (typeof denormalizedRootNode === 'string') { - return denormalizedRootNode; - } - denormalizedTree.push(denormalizedRootNode); } } diff --git a/src/services/CollectionArgumentsCreator.ts b/src/services/CollectionArgumentsCreator.ts index 3f3346c..494600a 100644 --- a/src/services/CollectionArgumentsCreator.ts +++ b/src/services/CollectionArgumentsCreator.ts @@ -1,6 +1,6 @@ import queryString from 'querystring'; import { FacetConfig, IndexConfig } from '../typings/schema'; -import { EsMatches, EsQuery, setFirstPathAsString } from './EsQueryStringCreator'; +import { EsMatches, EsQuery, EsValuePath } from './EsQueryStringCreator'; import { Location } from 'history'; interface Aggs { @@ -70,7 +70,7 @@ const createAggsString = (facets: FacetConfig[], searchObj: EsQuery | null): Agg const entries = facets.entries(); for (const [idx, { paths, caption, type }] of entries) { if (type === 'MultiSelect' && (caption || type) && paths) { - const field = setFirstPathAsString(paths); + const field = EsValuePath(paths[0]); const filter = searchObj ? setFilteredSearchObj(searchObj, field) : {}; aggregations[caption || `${type}_${idx}`] = { diff --git a/src/services/EsQueryStringCreator.ts b/src/services/EsQueryStringCreator.ts index 12dca7c..d7a30e4 100644 --- a/src/services/EsQueryStringCreator.ts +++ b/src/services/EsQueryStringCreator.ts @@ -1,4 +1,5 @@ import { EsFilter, FullTextSearch } from '../reducers/search'; +import { PATH_SPLIT, splitPath } from './walkPath'; export interface EsQuery { bool: EsBool; @@ -28,12 +29,12 @@ export interface EsMatch { }; } -/** create a string from the first path and append it with the for ES needed value +/** Create path that only holds values for elasticsearch querying * - * @param {string[]} paths - * @returns {string} + * @param {string} path + * @constructor */ -export const setFirstPathAsString = (paths: string[]): string => `${paths[0]}.raw`; +export const EsValuePath = (path: string) => `${splitPath(path, true).join(PATH_SPLIT)}.raw`; /** retrieve all selected values, create a new 'match' for each of them and add them to the query * @@ -48,7 +49,7 @@ const addMatchQueries = (filters: Readonly, query: EsQuery): void => filter.values.forEach(value => { if (value.selected) { hasValues = true; - const newMatch: EsMatch = { match: { [setFirstPathAsString(filter.paths)]: value.name } }; + const newMatch: EsMatch = { match: { [EsValuePath(filter.paths[0])]: value.name } }; matches.bool.should.push(newMatch); } }); diff --git a/src/services/Validation.ts b/src/services/Validation.ts index 20a3cd3..d63dc05 100644 --- a/src/services/Validation.ts +++ b/src/services/Validation.ts @@ -1,9 +1,11 @@ -import { NormalizedComponentConfig } from '../typings/index'; -import { COMPONENTS } from '../constants/global'; +import { NormalizedComponentConfig, NormalizedFacetConfig, ReferencePath } from '../typings/index'; +import { COMPONENTS, VALUE } from '../constants/global'; + +const referenceIncomplete = (path: string[][] | undefined) => path && path[path.length - 1][1] !== VALUE; export const componentErrors = (childNode: NormalizedComponentConfig): string | null => { // has a selectpath, but is not completed - if (childNode.referencePath && childNode.referencePath[childNode.referencePath.length - 1][0] !== 'VALUE') { + if (referenceIncomplete(childNode.referencePath)) { return `You forgot to finish the path for ${childNode.name}: ${childNode.value}`; } @@ -17,3 +19,11 @@ export const componentErrors = (childNode: NormalizedComponentConfig): string | return null; }; + +export const facetErrors = (referencePath: ReferencePath, config: NormalizedFacetConfig): string | null => { + if (referenceIncomplete(referencePath)) { + return `You forgot to finish the path ${config.caption}: ${referencePath}`; + } + + return null; +}; diff --git a/src/services/verifyResponse.ts b/src/services/verifyResponse.ts index acf6036..09ea52a 100644 --- a/src/services/verifyResponse.ts +++ b/src/services/verifyResponse.ts @@ -22,7 +22,7 @@ interface SinkProps { type DataProps = { [P in K]: QueryProps }; -const handleError = , K = keyof TProps>(dataProp: K) => +export const handleError = , K = keyof TProps>(dataProp: K) => branch((props: any) => !!props[dataProp].error, renderNothing); const ensureExistence = (path: string | ((props: TProps) => string), dataProp: keyof TProps) => { diff --git a/src/services/walkPath.ts b/src/services/walkPath.ts index a5d1d98..54530e9 100644 --- a/src/services/walkPath.ts +++ b/src/services/walkPath.ts @@ -42,7 +42,7 @@ export const walkPath = (pathStr: string | undefined, formatters: FormatterConfi }; export const createReferencePath = (path: string, collectionId: string): ReferencePath => - path.length > 0 ? [[collectionId], ...(splitPath(path) as ReferencePath)] : [[collectionId]]; + path.length > 0 ? (splitPath(path) as ReferencePath) : [[collectionId]]; function isValue(obj: Value | Entity): obj is Value { return obj.hasOwnProperty(VALUE); diff --git a/src/typings/schema.ts b/src/typings/schema.ts index 3ab58dc..ed540a5 100644 --- a/src/typings/schema.ts +++ b/src/typings/schema.ts @@ -253,6 +253,7 @@ export interface FacetConfig { paths: string[]; type: FacetConfigType; caption: string | null; + __typename?: string; } export type FacetConfigType = 'MultiSelect' | 'DateRange' | 'Hierarchical';