diff --git a/.eslintrc.js b/.eslintrc.js index 882170b64..a481dad4b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,54 +1,55 @@ module.exports = { - env: { - es6: true, - node: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/recommended', - 'plugin:import/typescript', - 'prettier', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 'latest', - }, - plugins: ['@typescript-eslint', 'prettier'], - root: true, - rules: { - '@typescript-eslint/no-empty-interface': [ - 'warn', - { - allowSingleExtends: false, - }, - ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-var-requires': 'off', - 'import/first': ['warn', 'absolute-first'], - 'import/order': [ - 'warn', - { - groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], - 'newlines-between': 'always', - warnOnUnassignedImports: true, - }, - ], - 'import/newline-after-import': 'warn', - 'prettier/prettier': [ - 'error', - { - printWidth: 100, - semi: true, - singleQuote: true, - trailingComma: 'all', - }, - ], - }, - settings: { - 'import/resolver': { - 'babel-module': { allowExistingDirectories: true }, - }, - 'import/internal-regex': '^@/', - }, + env: { + es6: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'prettier', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + }, + plugins: ['@typescript-eslint', 'prettier'], + root: true, + rules: { + '@typescript-eslint/no-empty-interface': [ + 'warn', + { + allowSingleExtends: false, + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + 'import/first': ['warn', 'absolute-first'], + 'import/order': [ + 'warn', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + warnOnUnassignedImports: true, + }, + ], + 'import/newline-after-import': 'warn', + 'prettier/prettier': [ + 'error', + { + printWidth: 100, + trailingComma: 'all', + semi: true, + singleQuote: true, + useTabs: true, + }, + ], + }, + settings: { + 'import/resolver': { + 'babel-module': { allowExistingDirectories: true }, + }, + 'import/internal-regex': '^@/', + }, }; diff --git a/.gitignore b/.gitignore index b2731d18e..eac182ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ lerna-debug.log **/.DS_Store **/*.env* !**/.env.schema + +.vscode/*log diff --git a/Jenkinsfile b/Jenkinsfile index 87bbe94f8..464f4d619 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -132,6 +132,13 @@ pipeline { } stage('Build images') { + when { + anyOf { + branch 'develop' + branch 'main' + branch 'test' + } + } steps { container('docker') { sh "DOCKER_BUILDKIT=1 \ diff --git a/modules/components/.eslintrc.js b/modules/components/.eslintrc.js index a0412a2ff..80511aa09 100644 --- a/modules/components/.eslintrc.js +++ b/modules/components/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { '@emotion/pkg-renaming': 'error', '@emotion/styled-import': 'error', 'jsx-a11y/href-no-hash': 'off', + 'react/no-unknown-property': ['error', { ignore: ['css'] }], 'react/prop-types': 'off', }, settings: { diff --git a/modules/components/package-lock.json b/modules/components/package-lock.json index 4df22bb12..d875f929f 100644 --- a/modules/components/package-lock.json +++ b/modules/components/package-lock.json @@ -91,6 +91,7 @@ "storybook-router": "^0.3.3", "ts-jest": "^29.0.3", "ttypescript": "^1.5.13", + "type-fest": "^3.0.0", "typescript-transform-paths": "^3.3.1" }, "peerDependencies": { @@ -4221,6 +4222,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -22806,12 +22819,12 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.0.0.tgz", + "integrity": "sha512-MINvUN5ug9u+0hJDzSZNSnuKXI8M4F5Yvb6SQZ2CYqe7SgKXKOosEcU5R7tRgo85I6eAVBbkVF7TCvB4AUK2xQ==", "dev": true, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -27585,6 +27598,14 @@ "dev": true, "requires": { "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } } }, "ansi-html-community": { @@ -42549,9 +42570,9 @@ "dev": true }, "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.0.0.tgz", + "integrity": "sha512-MINvUN5ug9u+0hJDzSZNSnuKXI8M4F5Yvb6SQZ2CYqe7SgKXKOosEcU5R7tRgo85I6eAVBbkVF7TCvB4AUK2xQ==", "dev": true }, "type-is": { diff --git a/modules/components/package.json b/modules/components/package.json index bab6846f5..cac0e6344 100644 --- a/modules/components/package.json +++ b/modules/components/package.json @@ -71,6 +71,7 @@ "storybook-router": "^0.3.3", "ts-jest": "^29.0.3", "ttypescript": "^1.5.13", + "type-fest": "^3.0.0", "typescript-transform-paths": "^3.3.1" }, "peerDependencies": { @@ -135,9 +136,10 @@ }, "prettier": { "printWidth": 100, + "trailingComma": "all", "semi": true, "singleQuote": true, - "trailingComma": "all" + "useTabs": true }, "husky": { "hooks": { diff --git a/modules/components/src/Aggs/DatesAgg.js b/modules/components/src/Aggs/DatesAgg.js index 23be108f2..6e51dbd04 100644 --- a/modules/components/src/Aggs/DatesAgg.js +++ b/modules/components/src/Aggs/DatesAgg.js @@ -6,9 +6,9 @@ import 'react-datepicker/dist/react-datepicker.css'; import { removeSQON, replaceSQON } from '@/SQONViewer/utils'; import { withTheme } from '@/ThemeContext'; +import { emptyObj } from '@/utils/noops'; import AggsWrapper from './AggsWrapper'; -import { emptyObj } from '@/utils/noops'; const dateFromSqon = (dateString) => new Date(dateString); const toSqonDate = (date) => date.valueOf(); @@ -26,12 +26,13 @@ class DatesAgg extends React.Component { this.setState(this.initializeState(nextProps)); } - initializeState = ({ stats = {}, getActiveValue = () => null }) => { - const { field } = this.props; + initializeState = ({ getActiveValue = () => null, stats = {}, enforceStatsMax = false }) => { + const { fieldName } = this.props; const minDate = stats.min && subDays(stats.min, 1); - const maxDate = stats.max && addDays(stats.max, 1); - const startFromSqon = getActiveValue({ op: '>=', field }); - const endFromSqon = getActiveValue({ op: '<=', field }); + const statsMax = stats.max && addDays(stats.max, 1); + const maxDate = enforceStatsMax ? statsMax : Math.max(Date.now(), statsMax); + const startFromSqon = getActiveValue({ op: '>=', fieldName }); + const endFromSqon = getActiveValue({ op: '<=', fieldName }); return { minDate, @@ -43,15 +44,15 @@ class DatesAgg extends React.Component { updateSqon = () => { const { startDate, endDate } = this.state; - const { field, handleDateChange } = this.props; - if (handleDateChange && field) { + const { fieldName, handleDateChange } = this.props; + if (handleDateChange && fieldName) { const content = [ ...(startDate ? [ { op: '>=', content: { - field, + fieldName, value: toSqonDate(startOfDay(startDate)), }, }, @@ -62,7 +63,7 @@ class DatesAgg extends React.Component { { op: '<=', content: { - field, + fieldName, value: toSqonDate(endOfDay(endDate)), }, }, @@ -70,10 +71,10 @@ class DatesAgg extends React.Component { : []), ]; handleDateChange({ - field, + fieldName, value: content, generateNextSQON: (sqon) => - replaceSQON(content.length ? { op: 'and', content } : null, removeSQON(field, sqon)), + replaceSQON(content.length ? { op: 'and', content } : null, removeSQON(fieldName, sqon)), }); } }; @@ -87,7 +88,7 @@ class DatesAgg extends React.Component { collapsible = true, displayName = 'Date Range', facetView = false, - field, + fieldName, theme: { colors, components: { @@ -106,7 +107,7 @@ class DatesAgg extends React.Component { const hasData = minDate && maxDate; const dataFields = { - ...(field && { 'data-field': field }), + ...(fieldName && { 'data-fieldname': fieldName }), ...(type && { 'data-type': type }), }; @@ -120,14 +121,65 @@ class DatesAgg extends React.Component { justify-content: space-around; padding-left: 5px; - .react-datepicker__current-month, + .react-datepicker__current-month { + display: none; + } + .react-datepicker-time__header, .react-datepicker-year-header { color: ${colors?.grey?.[700]}; } + .react-datepicker__day-name, + .react-datepicker__day, + .react-datepicker__time-name { + line-height: 1.4rem; + width: 1.5rem; + } + + .react-datepicker__header__dropdown { + display: flex; + justify-content: center; + } + .react-datepicker__input-container { width: 100%; + + .react-datepicker__close-icon::after { + align-items: center; + background-color: ${colors?.grey?.[500]}; + border-radius: 30%; + display: flex; + font-size: 14px; + justify-content: center; + height: 10px; + line-height: 0; + padding: 0.1rem; + width: 10px; + } + } + + .react-datepicker__month-option--selected { + left: 10px; + } + + .react-datepicker__month-read-view, + .react-datepicker__year-read-view { + border: none; + visibility: visible !important; + /* ^^ otherwise the current becomes invisible when dropdown is displayed */ + width: fit-content; + } + + .react-datepicker__month-read-view--down-arrow, + .react-datepicker__year-read-view--down-arrow { + display: none; + } + + .react-datepicker__month-read-view--selected-month, + .react-datepicker__year-read-view--selected-year { + font-size: 0.9rem; + font-weight: bold; } .react-datepicker-wrapper input { @@ -138,32 +190,13 @@ class DatesAgg extends React.Component { padding: 6px 5px 5px 7px; width: 100%; } - - .react-datepicker__input-container .react-datepicker__close-icon::after { - align-items: center; - background-color: ${colors?.grey?.[500]}; - border-radius: 30%; - display: flex; - font-size: 14px; - justify-content: center; - height: 10px; - line-height: 0; - padding: 0.1rem; - width: 10px; - } - - .react-datepicker__day-name, - .react-datepicker__day, - .react-datepicker__time-name { - line-height: 1.4rem; - width: 1.5rem; - } `} > ) : ( diff --git a/modules/components/src/DataContext/index.tsx b/modules/components/src/DataContext/index.tsx index 1263d3095..1b67c04cc 100644 --- a/modules/components/src/DataContext/index.tsx +++ b/modules/components/src/DataContext/index.tsx @@ -1,7 +1,9 @@ import { ComponentType, createContext, ReactElement, useContext, useEffect, useState } from 'react'; +import { isEqual } from 'lodash'; import { ThemeProvider } from '@/ThemeContext'; import defaultApiFetcher from '@/utils/api'; +import { ARRANGER_API, DEBUG } from '@/utils/config'; import getComponentDisplayName from '@/utils/getComponentDisplayName'; import missingProviderHandler from '@/utils/missingProvider'; import { emptyObj } from '@/utils/noops'; @@ -23,18 +25,21 @@ export const DataContext = createContext({ * @param {string} [url] customises where requests should be made by the data fetcher. */ export const DataProvider = ({ + apiUrl = ARRANGER_API, children, customFetcher: apiFetcher = defaultApiFetcher, documentType, legacyProps, theme, - url, }: DataProviderProps): ReactElement => { const [sqon, setSQON] = useState(null); useEffect(() => { - setSQON(legacyProps?.sqon); - }, [legacyProps?.sqon]); + if (legacyProps?.sqon && !isEqual(legacyProps.sqon, sqon)) { + DEBUG && console.log('setting sqon from legacyProps'); + setSQON(legacyProps?.sqon); + } + }, [legacyProps?.sqon, sqon]); const { documentMapping, @@ -51,13 +56,15 @@ export const DataProvider = ({ const fetchData = useDataFetcher({ apiFetcher, documentType, - keyField: tableConfigs?.keyField, + keyFieldName: tableConfigs?.keyFieldName, sqon, - url, + url: apiUrl, }); const contextValues = { ...legacyProps, + apiFetcher, + apiUrl, documentMapping, downloadsConfigs, extendedMapping, @@ -83,6 +90,7 @@ export const DataProvider = ({ * @returns {DataContextInterface} data object */ export const useDataContext = ({ + apiUrl: localApiUrl, callerName, customFetcher: localFetcher, }: UseDataContextProps = emptyObj): DataContextInterface => { @@ -92,6 +100,7 @@ export const useDataContext = ({ return { ...defaultContext, + apiUrl: localApiUrl || defaultContext?.apiUrl, fetchData: localFetcher || defaultContext?.fetchData, }; }; diff --git a/modules/components/src/DataContext/types.ts b/modules/components/src/DataContext/types.ts index a93f8f25e..6add3b0a3 100644 --- a/modules/components/src/DataContext/types.ts +++ b/modules/components/src/DataContext/types.ts @@ -5,17 +5,17 @@ import SQON from 'sqon-builder'; // TODO: This legacyProps import will fail when is deprecated // Should be safe to remove afterwards, if the migration path worked out import { legacyProps } from '@/Arranger/Arranger'; -import { CustomThemeType, BaseThemeInterface } from '@/ThemeContext/types'; +import { CustomThemeType, ThemeOptions } from '@/ThemeContext/types'; export type DisplayType = 'all' | 'bits' | 'boolean' | 'bytes' | 'date' | 'list' | 'number'; export interface ColumnMappingInterface { accessor: string; canChangeShow: boolean; - displayFormat?: string; + displayFormat?: string | null; displayName?: string; displayValues?: Record; - field: string; + fieldName: string; id: string; isArray?: boolean; jsonPath?: string | null; @@ -27,13 +27,13 @@ export interface ColumnMappingInterface { export interface ColumnSortingInterface { desc: boolean; - field: string; + fieldName: string; } export interface TableConfigsInterface { columns: ColumnMappingInterface[]; defaultSorting: ColumnSortingInterface[]; - keyField: string; + keyFieldName: string; } export interface ExtendedMappingInterface { @@ -41,7 +41,7 @@ export interface ExtendedMappingInterface { displayName: string; displayType: string; displayValues: Record; - field: string; + fieldName: string; isArray: boolean; primaryKey: boolean; quickSearchEnabled: boolean; @@ -73,19 +73,21 @@ export type FetchDataFn = (options?: { queryName?: string; }) => Promise<{ total?: number; data?: any } | void>; -export interface DataProviderProps { +export interface DataProviderProps { + apiUrl: string; children?: React.ReactNode; configs?: ConfigsInterface; customFetcher?: APIFetcherFn; documentType: string; legacyProps?: typeof legacyProps; // TODO: deprecate along with - url?: string; theme?: CustomThemeType; } export type SQONType = typeof SQON | null; export interface DataContextInterface { + apiFetcher: APIFetcherFn; + apiUrl: string; documentType: string; extendedMapping: ExtendedMappingInterface[]; fetchData: FetchDataFn; @@ -97,6 +99,7 @@ export interface DataContextInterface { } export interface UseDataContextProps { + apiUrl?: string; callerName?: string; customFetcher?: FetchDataFn; } diff --git a/modules/components/src/DataTable/DataTable.js b/modules/components/src/DataTable/DataTable.js index 28c5f1775..b30a87fe8 100644 --- a/modules/components/src/DataTable/DataTable.js +++ b/modules/components/src/DataTable/DataTable.js @@ -3,10 +3,9 @@ import { isEqual } from 'lodash'; import urlJoin from 'url-join'; import { withData } from '@/DataContext'; +import { ARRANGER_API } from '@/utils/config'; import noopFn from '@/utils/noops'; -import { ARRANGER_API } from '../utils/config'; - import { Table, TableToolbar } from './'; const STORED_PROPS = { @@ -86,6 +85,7 @@ class DataTableWithToolbar extends React.Component { allowTogglingColumns = true, allowTSVExport = true, alwaysSorted = [], + apiUrl = ARRANGER_API, columnDropdownText, config, customActions = null, @@ -123,7 +123,7 @@ class DataTableWithToolbar extends React.Component { } = this.props; const { page, pageSize, sorted, total } = this.state; - const url = downloadUrl || urlJoin(ARRANGER_API, 'download'); + const url = downloadUrl || urlJoin(apiUrl, 'download'); return ( <> diff --git a/modules/components/src/DataTable/Table/style.js b/modules/components/src/DataTable/Table/style.js index ce2c0266b..9d4e25f1e 100644 --- a/modules/components/src/DataTable/Table/style.js +++ b/modules/components/src/DataTable/Table/style.js @@ -4,6 +4,7 @@ export default ({ scrollbarSize: { scrollbarWidth } } = {}) => css` &.ReactTable .rt-thead.-header { padding-right: ${scrollbarWidth}px; } + &.ReactTable { width: 100%; box-sizing: border-box; @@ -12,10 +13,12 @@ export default ({ scrollbarSize: { scrollbarWidth } } = {}) => css` overflow-x: hidden; } } + .-pageJump { border: solid 1px lightgrey; border-radius: 5px; } + .ReactTable .-pagination_button { cursor: pointer; padding-left: 10px; @@ -23,8 +26,51 @@ export default ({ scrollbarSize: { scrollbarWidth } } = {}) => css` color: grey; user-select: none; } + .ReactTable .-pagination_button.-current { background: lightgrey; color: #f0f1f6; } + + ul.list-values { + margin: 0; + padding-left: 1rem; + + > li { + line-height: 1rem; + margin-bottom: 0.3rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &.none { + list-style: none; + padding: 0; + } + + &.commas { + display: flex; + flex-wrap: wrap; + list-style: none; + padding: 0; + + > li:not(:last-of-type)::after { + content: ', '; + margin-right: 0.2rem; + } + } + + &.letters { + list-style: lower-alpha; + } + + &.numbers { + list-style: decimal; + } + + &.roman { + list-style: upper-roman; + } + } `; diff --git a/modules/components/src/DataTable/columnTypes.js b/modules/components/src/DataTable/columnTypes.js index 2cf593b80..14fde2910 100644 --- a/modules/components/src/DataTable/columnTypes.js +++ b/modules/components/src/DataTable/columnTypes.js @@ -1,4 +1,4 @@ -import React from 'react'; +import cx from 'classnames'; import { format, isValid, parseISO } from 'date-fns'; import filesize from 'filesize'; import jsonPath from 'jsonpath/jsonpath.min'; @@ -8,7 +8,7 @@ import { getSingleValue } from './utils'; const STANDARD_DATE = 'yyyy-MM-dd'; -const dateHandler = ({ value, ...props }) => { +const dateHandler = ({ value, ...props } = {}) => { switch (true) { case isNil(value): return ''; @@ -35,15 +35,33 @@ const FileSize = ({ options = {}, ...props }) => ( ); export default { - bits: ({ value, ...props }) => , - boolean: ({ value }) => (isNil(value) ? '' : `${value}`), + bits: ({ value = 0, ...props } = {}) => , + boolean: ({ value = undefined } = {}) => (isNil(value) ? '' : `${value}`), bytes: (props) => , date: dateHandler, - list: (props) => { - const values = jsonPath.query(props.original, props.column.jsonPath); - const total = values.length; - const firstValue = getSingleValue(values[0]); - return [firstValue || '', ...(total > 1 ? [
, '...'] : [])]; + list: ({ column, id, original }) => { + const valuesArr = jsonPath.query(original, column.jsonPath ?? column.fieldName)?.[0]; + const arrHasValues = Array.isArray(valuesArr) && valuesArr?.filter((v) => v).length > 0; // table shouldn't display Nulls + + if (Array.isArray(valuesArr)) { + if (column.isArray && arrHasValues) { + return ( +
    + {valuesArr.map((value, index) => ( +
  • + {value} +
  • + ))} +
+ ); + } + + const total = valuesArr.length; + const firstValue = getSingleValue(valuesArr[0]); + return [firstValue || '', ...(total > 1 ? [
, '...'] : [])]; + } + + return valuesArr; }, number: Number, }; diff --git a/modules/components/src/DataTable/utils.js b/modules/components/src/DataTable/utils.js index 811e1cb42..7006b4781 100644 --- a/modules/components/src/DataTable/utils.js +++ b/modules/components/src/DataTable/utils.js @@ -1,7 +1,8 @@ -import columnTypes from './columnTypes'; import { withProps } from 'recompose'; import { isNil, sortBy } from 'lodash'; +import columnTypes from './columnTypes'; + export function getSingleValue(data) { if (typeof data === 'object' && data) { return getSingleValue(Object.values(data)[0]); @@ -28,8 +29,8 @@ export function normalizeColumns({ return { ...column, - show: typeof column.show === 'boolean' ? column.show : true, - Cell: column.Cell || types[column.type], + show: typeof column.show === 'boolean' ? column.show : false, + Cell: column.Cell || types[column.isArray ? 'list' : column.type], hasCustomType: isNil(column.hasCustomType) ? !!(customTypes || {})[column.type] : column.hasCustomType, diff --git a/modules/components/src/SQONViewer/helpers.tsx b/modules/components/src/SQONViewer/helpers.tsx index 80e507cde..cde3540be 100644 --- a/modules/components/src/SQONViewer/helpers.tsx +++ b/modules/components/src/SQONViewer/helpers.tsx @@ -49,8 +49,8 @@ export const Bubble = ({ children, className, theme, ...props }: BubbleProps) => ); }; -export const Field = ({ children, className, ...props }: BubbleProps) => ( - +export const FieldName = ({ children, className, ...props }: BubbleProps) => ( + {children} ); @@ -81,8 +81,8 @@ export const useDataBubbles = ({ colors, components: { SQONViewer: { - SQONClear: themeSQONClearProps = emptyObj, - SQONField: themeSQONFieldProps = emptyObj, + SQONClear: { label: themeSQONClearLabel = 'Clear', ...themeSQONClearProps } = emptyObj, + SQONFieldName: themeSQONFieldNameProps = emptyObj, SQONLessOrMore: themeSQONLessOrMoreProps = emptyObj, SQONValue: { characterLimit: themeCharacterLimit = 30, @@ -101,7 +101,8 @@ export const useDataBubbles = ({ const { extendedMapping } = useDataContext({ callerName: 'SQONViewer - useDataBubbles' }); const findExtendedMappingForField = useCallback( - (wantedField: string) => extendedMapping.find((mapping) => mapping.field === wantedField), + (wantedFieldName: string) => + extendedMapping.find((mapping) => mapping.fieldName === wantedFieldName), [extendedMapping], ); @@ -125,18 +126,18 @@ export const useDataBubbles = ({ ...themeSQONClearProps, }} > - Clear + {themeSQONClearLabel} ); - const FieldCrumb = ({ field, ...fieldProps }: { field: string }) => ( - ( + - {findExtendedMappingForField(field)?.displayName || field} - + {findExtendedMappingForField(fieldName)?.displayName || fieldName} + ); const lessOrMoreClickHandler = useCallback( @@ -161,19 +162,19 @@ export const useDataBubbles = ({ const ValueCrumb = ({ css: customCSS, - field, + fieldName, nextSQON, value, ...valueProps }: { - field: string; + fieldName: string; nextSQON: GroupSQONInterface; value: any; } & ThemeCommon.CustomCSS) => { const displayValue = translateSQONValue( internalTranslateSQONValue( - (findExtendedMappingForField(field)?.type === 'date' && format(value, dateFormat)) || - (findExtendedMappingForField(field)?.displayValues || {})[value] || + (findExtendedMappingForField(fieldName)?.type === 'date' && format(value, dateFormat)) || + (findExtendedMappingForField(fieldName)?.displayValues || {})[value] || value, ), ); diff --git a/modules/components/src/SQONViewer/types.ts b/modules/components/src/SQONViewer/types.ts index 02e0b5d72..e6f49c3d0 100644 --- a/modules/components/src/SQONViewer/types.ts +++ b/modules/components/src/SQONViewer/types.ts @@ -5,8 +5,10 @@ import { GenericFn } from '@/utils/noops'; export interface SQONViewerThemeProps { EmptyMessage: ThemeCommon.NonButtonThemeProps; SQONBubble: ThemedButtonProps; - SQONClear: ThemedButtonProps; - SQONField: ThemedButtonProps; + SQONClear: { + label?: string; + } & ThemedButtonProps; + SQONFieldName: ThemedButtonProps; SQONGroup: ThemeCommon.NonButtonThemeProps; SQONLessOrMore: ThemedButtonProps; SQONOp: ThemeCommon.NonButtonThemeProps; @@ -42,7 +44,7 @@ export type ArrayFieldValue = Array | string; export type ScalarFieldValue = number; export interface FilterField { - fields: string[]; + fieldNames: string[]; value: ArrayFieldValue; } @@ -52,12 +54,12 @@ export interface FilterFieldOperator { } export interface ArrayField { - field: string; + fieldName: string; value: ArrayFieldValue; } export interface ScalarField { - field: string; + fieldName: string; value: ScalarFieldValue; } @@ -82,8 +84,8 @@ export type ValueOpTypes = ArrayFieldKeys & CombinationKeys & ScalarFieldKeys; export interface ValueContentInterface { entity?: string; - field: string; - fields?: string[]; + fieldName: string; + fieldNames?: string[]; value: any | any[]; } @@ -115,4 +117,4 @@ export interface GroupSQONInterface { // export type TFilterByWhitelist = (o?: TRawQuery, w?: Array) => TRawQuery; -// export type TRemoveSQON = (field: string, query: TGroupSQON) => TGroupSQON | void; +// export type TRemoveSQON = (fieldName: string, query: TGroupSQON) => TGroupSQON | void; diff --git a/modules/components/src/Table/DownloadButton/DownloadButton.tsx b/modules/components/src/Table/DownloadButton/DownloadButton.tsx index 4c7ead58f..8b4d99fb5 100644 --- a/modules/components/src/Table/DownloadButton/DownloadButton.tsx +++ b/modules/components/src/Table/DownloadButton/DownloadButton.tsx @@ -3,6 +3,7 @@ import { merge } from 'lodash'; import urlJoin from 'url-join'; import { TransparentButton } from '@/Button'; +import { useDataContext } from '@/DataContext'; import { SQONType } from '@/DataContext/types'; import MultiSelectDropDown from '@/DropDown/MultiSelectDropDown'; import MetaMorphicChild from '@/MetaMorphicChild'; @@ -24,8 +25,10 @@ import { ProcessedExporterDetailsInterface, DownloadButtonProps } from './types' * * Either case must follow the following pattern (all params are optional): * - * @param {string[]} columns + * @param {string[] | { fieldName }} columns * Columns passed here override the ones currently being displayed in the table. + * the format for these is always an array, which could consist of one of the following types: + * accessor strings, or objects with a "fieldName" and any other properties as functions or their desired values. * If columns is missing or `null`, the exporter will use all columns shown in the table. * Bonus: if an empty array is given, the exporter will use every (showable) column declared in the config. * @param {ExporterFn} exporterFn @@ -51,6 +54,7 @@ const DownloadButton = ({ ...customThemeProps } = emptyObj, }: DownloadButtonProps) => { + const { apiUrl = ARRANGER_API } = useDataContext(); const { allColumnsDict, currentColumnsDict, @@ -71,7 +75,7 @@ const DownloadButton = ({ DownloadButton: { customExporters: themeCustomExporters, disableRowSelection: themeDisableRowSelection, - downloadUrl: themeDownloadUrl = urlJoin(ARRANGER_API, 'download'), + downloadUrl: themeDownloadUrl = urlJoin(apiUrl, 'download'), label: themeDownloadButtonLabel = 'Download', maxRows: themeMaxRows = 100, exportSelectedRowsField = 'file_autocomplete', @@ -109,15 +113,15 @@ const DownloadButton = ({ exporterColumns, exporterDownloadUrl = downloadUrl, exporterFileName, - exporterFn, + exporterFunction, exporterLabel, exporterMaxRows = maxRows, exporterRequiresRowSelection, }: ProcessedExporterDetailsInterface) => - (exporterFn && exporterRequiresRowSelection && !hasSelectedRows) || !exporterFn + (exporterFunction && exporterRequiresRowSelection && !hasSelectedRows) || !exporterFunction ? undefined : () => - exporterFn?.( + exporterFunction?.( { files: [ { diff --git a/modules/components/src/Table/DownloadButton/helpers.ts b/modules/components/src/Table/DownloadButton/helpers.ts index c191e576b..8d560015f 100644 --- a/modules/components/src/Table/DownloadButton/helpers.ts +++ b/modules/components/src/Table/DownloadButton/helpers.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import { ColumnMappingInterface } from '@/DataContext/types'; +import { ColumnMappingInterface, ExtendedMappingInterface } from '@/DataContext/types'; import download from '@/utils/download'; import { emptyObj } from '@/utils/noops'; @@ -9,42 +9,72 @@ import { CustomExportersInput, ExporterDetailsInterface, ExporterFileInterface, - ExporterFnProps, + ExporterFunctionProps, ProcessedExporterDetailsInterface, ProcessedExporterList, } from './types'; +const useCustomisers = + (extendedColumn: ExtendedMappingInterface) => + ([customiserLabel, customiserValue]): Partial => { + console.log('extendedColumn', extendedColumn); + return ( + customiserValue && { + [customiserLabel]: + typeof customiserValue === 'function' ? customiserValue(extendedColumn) : customiserValue, + } + ); + }; export const saveTSV = async ({ fileName = '', files = [], options = {}, url = '', -}: ExporterFnProps) => +}: ExporterFunctionProps) => download({ url, method: 'POST', + ...options, params: { fileName, files: files.map( - ({ allColumnsDict, columns, exporterColumns, ...file }: ExporterFileInterface) => ({ + ({ allColumnsDict, columns, exporterColumns = null, ...file }: ExporterFileInterface) => ({ ...file, columns: exporterColumns // if the component gave you custom columns to show ? Object.values( exporterColumns.length > 0 // if they ask for any specific columns - ? exporterColumns - .map( - (fieldName) => - allColumnsDict[fieldName] || // get the column data from the extended configs - // or let the user know if the column isn't valid - console.info('Could not include a column into the file:', fieldName), - ) - .filter((column) => column) // and then, use the valid ones + ? // use them + exporterColumns.map((column) => { + switch (typeof column) { + // checking if each column is customised + case 'object': { + const extendedColumn = allColumnsDict[column.fieldName]; + const useExtendedCustomisers = useCustomisers(extendedColumn); + + return { + ...extendedColumn, + ...Object.entries(column).reduce( + (customisers, customiser: ColumnCustomiserTuple) => ({ + ...customisers, + ...useExtendedCustomisers(customiser), + }), + {}, + ), + }; + } + + // or not + case 'string': + default: + return allColumnsDict[column]; + } + }) : allColumnsDict, // else, they're asking for all the columns ) - : columns.filter((column: ColumnMappingInterface) => column.show), // no custom columns, use admin's + : columns.filter((column) => column.show), // no custom columns, use admin's }), ), - ...options, + ...options.params, }, }); @@ -62,13 +92,13 @@ const processExporter = ( item = 'saveTSV' as any as CustomExportersInput, ): ProcessedExporterDetailsInterface => (item as any) === 'saveTSV' || - item?.fn === 'saveTSV' || + item?.function === 'saveTSV' || // or if they give us a filename without giving us a function ('fileName' in item && !('fn' in item)) ? { ...(item?.columns && Array.isArray(item.columns) && { exporterColumns: item?.columns }), exporterFileName: item?.fileName || 'unnamed.tsv', - exporterFn: saveTSV, + exporterFunction: saveTSV, exporterLabel: item?.label || 'Export TSV', exporterMaxRows: item?.maxRows || 0, exporterRequiresRowSelection: item?.requiresRowSelection || false, diff --git a/modules/components/src/Table/DownloadButton/types.ts b/modules/components/src/Table/DownloadButton/types.ts index 03210d879..ef978553f 100644 --- a/modules/components/src/Table/DownloadButton/types.ts +++ b/modules/components/src/Table/DownloadButton/types.ts @@ -1,12 +1,13 @@ import { ComponentType } from 'react'; +import type { Merge } from 'type-fest'; -import { ColumnMappingInterface, SQONType } from '@/DataContext/types'; +import { ColumnMappingInterface, ExtendedMappingInterface, SQONType } from '@/DataContext/types'; import { DropDownThemeProps } from '@/DropDown/types'; import { ColumnsDictionary, FieldList } from '@/Table/types'; import { ThemeCommon } from '@/ThemeContext/types'; -import { PrefixKeys } from '@/utils/types'; +import { PrefixKeys, WithFunctionOptions } from '@/utils/types'; -export type DownloadFn = (options: { +export type DownloadFunction = (options: { url: any; params: any; method?: string; @@ -17,34 +18,46 @@ export interface ExporterFileInterface { allColumnsDict: ColumnsDictionary; columns: ColumnMappingInterface[]; documentType: string; - exporterColumns?: string[]; + exporterColumns?: FieldList | ColumnMappingInterface[] | null; fileName: string; fileType: 'tsv' | string; maxRows: number; sqon: SQONType; } -export interface ExporterFnProps { +export interface ExporterFunctionProps { fileName?: string; files: ExporterFileInterface[]; options?: Record; - selectedRows?: string[]; + selectedRows: string[]; url: string; } -export type ExporterFn = (exporter: ExporterFnProps, downloadFn?: DownloadFn) => void; +export type ExporterFunction = ( + exporter: ExporterFunctionProps, + downloadFunction?: DownloadFunction, +) => void; + +export type CustomColumnMappingInterface = WithFunctionOptions>; +// export type CustomColumnMappingInterface = WithFunctionOptions>; export interface ExporterDetailsInterface { - columns?: FieldList | null; + columns?: (string | CustomColumnMappingInterface)[] | null; downloadUrl?: string; fileName?: string; - fn?: ExporterFn; + function?: ExporterFunction; label?: ComponentType | string; maxRows?: number; requiresRowSelection?: boolean; + valueWhenEmpty?: unknown; } -export type CustomExportersInput = ExporterDetailsInterface & { fn?: ExporterFn | 'saveTSV' }; +export type CustomExportersInput = Merge< + ExporterDetailsInterface, + { + function?: ExporterFunction | 'saveTSV'; + } +>; export type ProcessedExporterDetailsInterface = PrefixKeys; diff --git a/modules/components/src/Table/HeaderRow.tsx b/modules/components/src/Table/HeaderRow.tsx index 22b30e803..ef0c7f7c3 100644 --- a/modules/components/src/Table/HeaderRow.tsx +++ b/modules/components/src/Table/HeaderRow.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/react'; -import { HeaderGroup } from '@tanstack/react-table'; +import { flexRender, HeaderGroup } from '@tanstack/react-table'; import cx from 'classnames'; import { get } from 'lodash'; @@ -96,7 +96,9 @@ const TableHeaderRow = ({ key={headerObj.id} title={label} > - {headerObj.isPlaceholder ? null : headerObj.renderHeader()} + {headerObj.isPlaceholder + ? null + : flexRender(headerObj.column.columnDef.header, headerObj.getContext())} ); })} diff --git a/modules/components/src/Table/Row.tsx b/modules/components/src/Table/Row.tsx index 4b7b43c04..bfb544717 100644 --- a/modules/components/src/Table/Row.tsx +++ b/modules/components/src/Table/Row.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/react'; -import { Row } from '@tanstack/react-table'; +import { flexRender, Row } from '@tanstack/react-table'; import cx from 'classnames'; import MetaMorphicChild from '@/MetaMorphicChild'; @@ -19,7 +19,7 @@ const TableRow = ({ padding?: string; textOverflow?: string; }; -} & Partial>>) => { +} & Partial>) => { const { colors, components: { @@ -85,7 +85,7 @@ const TableRow = ({ > {hasVisibleCells ? ( visibleCells?.map((cellObj) => { - const value = getDisplayValue(cellObj?.row?.original, cellObj.column); + const value = getDisplayValue(cellObj?.row?.original, cellObj.column.columnDef); return ( - {cellObj.renderCell()} + {flexRender(cellObj.column.columnDef.cell, cellObj.getContext())} ); }) diff --git a/modules/components/src/Table/helpers/cells.tsx b/modules/components/src/Table/helpers/cells.tsx index 9e2d530be..0b13dc36f 100644 --- a/modules/components/src/Table/helpers/cells.tsx +++ b/modules/components/src/Table/helpers/cells.tsx @@ -8,13 +8,17 @@ import { get, isNil } from 'lodash'; import dateFormatter from '@/utils/dates'; import { emptyObj } from '@/utils/noops'; +import { getSingleValue } from '.'; + export const getCellValue = ( - row = emptyObj, + row = emptyObj as unknown, { accessor = '', id = '', jsonPath = '' } = emptyObj, ): string => - jsonPath ? JSONPath({ json: row, path: jsonPath }) : get(row, (id || accessor).split('.'), ''); + jsonPath + ? JSONPath({ json: row as Record, path: jsonPath }) + : get(row, (id || accessor).split('.'), ''); -export const getDisplayValue = (row = emptyObj, column = emptyObj): string => { +export const getDisplayValue = (row = emptyObj as unknown, column = emptyObj): string => { const value = getCellValue(row, column); switch (column.type) { case 'date': @@ -34,25 +38,36 @@ const Number = (props = emptyObj) => ( {props.value?.toLocaleString('en-CA')}
); + const FileSize = ({ options = emptyObj, value = 0 }) => {filesize(value, options)}; export const defaultCellTypes = { - bits: ({ value = 0, ...props } = emptyObj) => , - boolean: ({ value } = emptyObj) => (isNil(value) ? '' : `${value}`), + bits: ({ value = 0, ...props } = {}) => , + boolean: ({ value = undefined } = {}) => (isNil(value) ? '' : `${value}`), bytes: (props = emptyObj) => , date: ({ value, ...props } = emptyObj) => dateFormatter(value, props), - list: ({ column, id, value: valuesArr, ...props } = emptyObj) => { - return Array.isArray(valuesArr) ? ( -
    - {valuesArr.map((value: ReactNode, index: number) => ( -
  • - {value} -
  • - ))} -
- ) : ( - valuesArr - ); + list: ({ column, id, value: valuesArr } = emptyObj) => { + const arrHasValues = Array.isArray(valuesArr) && valuesArr?.filter((v) => v).length > 0; // table shouldn't display Nulls + + if (Array.isArray(valuesArr)) { + if (column.isArray && arrHasValues) { + return ( +
    + {valuesArr.map((value: ReactNode, index) => ( +
  • + {value} +
  • + ))} +
+ ); + } + + const total = valuesArr.length; + const firstValue = getSingleValue(valuesArr[0]); + return [firstValue || '', ...(total > 1 ? [
, '...'] : [])]; + } + + return valuesArr; }, number: Number, }; diff --git a/modules/components/src/Table/helpers/columns.tsx b/modules/components/src/Table/helpers/columns.tsx index e357ae043..11801c1b6 100644 --- a/modules/components/src/Table/helpers/columns.tsx +++ b/modules/components/src/Table/helpers/columns.tsx @@ -1,5 +1,5 @@ import { HTMLAttributes, useEffect, useRef } from 'react'; -import { Table } from '@tanstack/react-table'; +import { createColumnHelper } from '@tanstack/react-table'; import { mergeWith } from 'lodash'; import { ColumnMappingInterface } from '@/DataContext/types'; @@ -64,16 +64,15 @@ function IndeterminateCheckbox({ export const makeTableColumns = ({ allowRowSelection, columnTypes: customColumnTypes = emptyObj, - table, total, visibleColumns = [], }: { allowRowSelection?: boolean; columnTypes?: Partial; - table: Table; total: number; visibleColumns: ColumnMappingInterface[]; }) => { + const columnHelper = createColumnHelper(); const hasData = total > 0; const columnTypes = mergeWith(customColumnTypes, defaultCellTypes, (objValue, srcValue) => ({ ...objValue, @@ -84,11 +83,11 @@ export const makeTableColumns = ({ const columnType = mergeWith( {}, columnTypes.all, - columnTypes[visibleColumn?.isArray ? 'list' : visibleColumn?.type], - columnTypes[visibleColumn?.accessor], + columnTypes[visibleColumn.isArray ? 'list' : visibleColumn.type], + columnTypes[visibleColumn.accessor], ); - return table.createDataColumn((row) => getCellValue(row, visibleColumn), { + return columnHelper.accessor((row) => getCellValue(row, visibleColumn), { ...visibleColumn, cell: ({ getValue, cell }) => { const cellType = columnType?.cellValue; @@ -131,15 +130,15 @@ export const makeTableColumns = ({ return allowRowSelection ? [ - table.createDisplayColumn({ + columnHelper.display({ id: 'select', - header: ({ instance }) => ( + header: ({ table }) => ( ), @@ -155,6 +154,7 @@ export const makeTableColumns = ({ ), }), - ].concat(tableColumns) + ...tableColumns, + ] : tableColumns; }; diff --git a/modules/components/src/Table/helpers/context.tsx b/modules/components/src/Table/helpers/context.tsx index 688fad57f..7a40c7212 100644 --- a/modules/components/src/Table/helpers/context.tsx +++ b/modules/components/src/Table/helpers/context.tsx @@ -64,7 +64,7 @@ export const TableContextProvider = ({ useThemeContext({ callerName: 'TableContextProvider' }); useEffect(() => { - if (isLoadingConfigs) { + if (tableConfigs?.columns && Object.values(allColumnsDict).length === 0) { const columns = aggregateCustomColumns(customColumns, tableConfigs?.columns); setAllColumnsDict(columnsArrayToDictionary(columns)); // these will be the default to fallback to diff --git a/modules/components/src/Table/helpers/index.ts b/modules/components/src/Table/helpers/index.ts index 9f902ead5..71eb21618 100644 --- a/modules/components/src/Table/helpers/index.ts +++ b/modules/components/src/Table/helpers/index.ts @@ -1,5 +1,14 @@ -import { useEffect, useState } from 'react'; -import { createTable, useTableInstance, ColumnDef, getCoreRowModel } from '@tanstack/react-table'; +import { ReactNode, useEffect, useState } from 'react'; +import { useReactTable, ColumnDef, getCoreRowModel } from '@tanstack/react-table'; +/* Column, + Table as ReactTable, + PaginationState, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + ColumnDef, + OnChangeFn, + flexRender, */ import { UseTableDataProps } from '@/Table/types'; import { useThemeContext } from '@/ThemeContext'; @@ -8,7 +17,13 @@ import { emptyObj } from '@/utils/noops'; import { makeTableColumns } from './columns'; import { useTableContext } from './context'; -const table = createTable(); +export const getSingleValue = (data: Record | ReactNode): ReactNode => { + if (typeof data === 'object' && data) { + return getSingleValue(Object.values(data)[0]); + } else { + return data; + } +}; export const useTableData = ({ columnTypes: customColumnTypes, @@ -39,7 +54,7 @@ export const useTableData = ({ } = useThemeContext({ callerName: 'Table - useTableData' }); const allowRowSelection = !(customDisableRowSelection || themeDisableRowSelection); - const [tableColumns, setTableColumns] = useState[]>([]); + const [tableColumns, setTableColumns] = useState[]>([]); useEffect(() => { const visibleColumns = Object.values(visibleColumnsDict); @@ -52,14 +67,13 @@ export const useTableData = ({ ...customColumnTypes, // then prioritise the ones given directly into the table // this is useful if there are multiple sibling tables with different "settings" }, - table, total, visibleColumns, }), ); }, [customColumnTypes, allowRowSelection, themeColumnTypes, visibleColumnsDict, total]); - const tableInstance = useTableInstance(table, { + const tableInstance = useReactTable({ columns: tableColumns, data: tableData, getCoreRowModel: getCoreRowModel(), diff --git a/modules/components/src/Table/types.ts b/modules/components/src/Table/types.ts index a5fbdf5f3..b348adc0a 100644 --- a/modules/components/src/Table/types.ts +++ b/modules/components/src/Table/types.ts @@ -63,14 +63,15 @@ type TableBoxModelProperties = Omit & type TableInnerBoxModelProperties = Omit; /** Table Component types */ -export type TableCellProps = Cell & { +export type TableCellProps = Cell & { column: Column & ColumnMappingInterface; value: any; }; type TableCellComponent = ReactNode | ((cell: TableCellProps) => ReactNode); -export type TableHeaderProps = Header & ColumnMappingInterface & { disabled?: boolean }; +export type TableHeaderProps = Header & + ColumnMappingInterface & { disabled?: boolean }; type TableHeaderComponent = ReactNode | ((header: TableHeaderProps) => ReactNode); diff --git a/modules/components/src/ThemeContext/types/index.ts b/modules/components/src/ThemeContext/types/index.ts index fa745d71e..6beda5f79 100644 --- a/modules/components/src/ThemeContext/types/index.ts +++ b/modules/components/src/ThemeContext/types/index.ts @@ -36,13 +36,13 @@ export type ThemeAggregatorFn = ( partial: CustomThemeType | CustomThemeType[], ) => ThemeOptions; -export interface ThemeContextInterface { +export interface ThemeContextInterface { aggregateTheme: ThemeAggregatorFn; missingProvider?: string; theme: Theme; } -export interface ThemeProviderProps { +export interface ThemeProviderProps { children?: React.ReactNode; location?: string; // helpful for troubleshooting multiple theme providers theme?: CustomThemeType; @@ -53,7 +53,7 @@ export type UseThemeContextProps = CustomThemeType & { callerName?: string; }; -export interface WithThemeProps { +export interface WithThemeProps { theme?: Theme; } diff --git a/modules/components/src/index.js b/modules/components/src/index.js index 830087c28..79a78317d 100644 --- a/modules/components/src/index.js +++ b/modules/components/src/index.js @@ -3,7 +3,7 @@ export { DataContext as ArrangerDataContext, DataProvider as ArrangerDataProvider, useDataContext as useArrangerData, - withTheme as withArrangerTheme, + withData as withArrangerData, } from './DataContext'; // TODO: Deprecate "CurrentSQON" component name as unsemantical, // remove SQONView (duplicate of CurrentSQON to produce the same log warning) @@ -22,6 +22,7 @@ export { ThemeContext as ArrangerThemeContext, ThemeProvider as ArrangerThemeProvider, useThemeContext as useArrangerTheme, + withTheme as withArrangerTheme, } from './ThemeContext'; export { default as apiFetcher, addHeaders } from './utils/api'; export { default as Query, withQuery } from './Query'; diff --git a/modules/components/src/utils/dates.ts b/modules/components/src/utils/dates.ts index 1f07a3581..4405357e8 100644 --- a/modules/components/src/utils/dates.ts +++ b/modules/components/src/utils/dates.ts @@ -3,7 +3,8 @@ import { isNil } from 'lodash'; export const STANDARD_DATE = 'yyyy-MM-dd'; -const displayFormatter = (value: string, { displayFormat = STANDARD_DATE, ...props }: any) => { +const displayFormatter = (value: string, { displayFormat, ...props }: any) => { + displayFormat ??= STANDARD_DATE; // handle `null` switch (true) { case isNil(value): return ''; diff --git a/modules/components/src/utils/types.ts b/modules/components/src/utils/types.ts index 9c46f6ed2..eaccb2057 100644 --- a/modules/components/src/utils/types.ts +++ b/modules/components/src/utils/types.ts @@ -13,3 +13,7 @@ export type RecursivePartial = { export type PrefixKeys = { [P in keyof T as `${Prefix}${Capitalize}`]: T[P]; }; + +export type WithFunctionOptions = { + [key in keyof T]: T[key] | ((input: Input) => T[key]); +}; diff --git a/modules/server/package-lock.json b/modules/server/package-lock.json index d87ac99e2..902fb0961 100644 --- a/modules/server/package-lock.json +++ b/modules/server/package-lock.json @@ -28,6 +28,7 @@ "graphql-middleware": "^6.1.33", "graphql-playground-middleware-express": "^1.7.23", "graphql-scalars": "^1.19.0", + "graphql-tools": "^8.3.8", "graphql-type-json": "^0.3.2", "jsonpath": "^1.1.1", "lodash": "^4.17.21", @@ -55,6 +56,7 @@ "@babel/preset-typescript": "^7.18.6", "@babel/register": "^7.18.9", "@types/convert-units": "^2.3.5", + "@types/graphql-fields": "^1.3.4", "@types/jest": "^29.1.2", "@types/lodash": "^4.14.186", "@types/morgan": "^1.9.3", @@ -85,6 +87,48 @@ "node": ">=6.0.0" } }, + "node_modules/@apollo/client": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.1.tgz", + "integrity": "sha512-xu5M/l7p9gT9Fx7nF3AQivp0XukjB7TM7tOd5wifIpI8RskYveL4I+rpTijzWrnqCPZabkbzJKH7WEAKdctt9w==", + "optional": true, + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/context": "^0.7.0", + "@wry/equality": "^0.5.0", + "@wry/trie": "^0.3.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.16.1", + "prop-types": "^15.7.2", + "response-iterator": "^0.2.6", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql-ws": "^5.5.5", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, "node_modules/@apollo/protobufjs": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.6.tgz", @@ -2132,6 +2176,15 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "optional": true, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3114,6 +3167,24 @@ "@types/node": "*" } }, + "node_modules/@types/graphql-fields": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/graphql-fields/-/graphql-fields-1.3.4.tgz", + "integrity": "sha512-McLJaAaqY7lk9d9y7E61iQrj0AwcEjSb8uHlPh7KgYV+XX1MSLlSt/alhd5k2BPRE8gy/f4lnkLGb5ke3iG66Q==", + "dev": true, + "dependencies": { + "graphql": "^15.3.0" + } + }, + "node_modules/@types/graphql-fields/node_modules/graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -3257,6 +3328,42 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "node_modules/@wry/context": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.0.tgz", + "integrity": "sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==", + "optional": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.3.tgz", + "integrity": "sha512-avR+UXdSrsF2v8vIqIgmeTY0UR91UT+IyablCyKe/uk22uOJ8fusKZnH9JH9e1/EtLeNJBtagNmL3eJdnOV53g==", + "optional": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.2.tgz", + "integrity": "sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==", + "optional": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -5172,6 +5279,58 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/graphql-tools": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-8.3.8.tgz", + "integrity": "sha512-syDJ6hzlqm+DoiX+v8k0kogyYfdvPlpoZ+L76OD1d4Ll0qPCeZG41NzHBUJIzWWBz+Kq942m3ilEey/xCML8LQ==", + "dependencies": { + "@graphql-tools/schema": "9.0.6", + "tslib": "^2.4.0" + }, + "optionalDependencies": { + "@apollo/client": "~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-tools/node_modules/@graphql-tools/merge": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.8.tgz", + "integrity": "sha512-L9YE8OpxSlzADcdrc4IG7/33H/iWVXTJXX2ie67cWAb5MFN2t3JBdQMa0bnBcAoOrKB7A8g2+dIp8oXTpdzxjg==", + "dependencies": { + "@graphql-tools/utils": "8.13.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-tools/node_modules/@graphql-tools/schema": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.6.tgz", + "integrity": "sha512-/aznltpnVrurfWqXB4chWtaNmBFSk9v/KEJSpvas2fnlwwS9QnzWh6Sm/hsybWesirn5J2w60LLjMrrcCd58UA==", + "dependencies": { + "@graphql-tools/merge": "8.3.8", + "@graphql-tools/utils": "8.13.1", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-tools/node_modules/@graphql-tools/utils": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.13.1.tgz", + "integrity": "sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/graphql-type-json": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", @@ -5222,6 +5381,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "optional": true, + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hpagent": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz", @@ -7400,7 +7568,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "devOptional": true }, "node_modules/js-yaml": { "version": "3.14.1", @@ -7717,6 +7885,18 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "optional": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8121,6 +8301,28 @@ "opencollective-postinstall": "index.js" } }, + "node_modules/optimism": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", + "integrity": "sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==", + "optional": true, + "dependencies": { + "@wry/context": "^0.6.0", + "@wry/trie": "^0.3.0" + } + }, + "node_modules/optimism/node_modules/@wry/context": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", + "integrity": "sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==", + "optional": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -8507,6 +8709,17 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "optional": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8575,6 +8788,12 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "optional": true + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -8747,6 +8966,15 @@ "node": ">=10" } }, + "node_modules/response-iterator": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", + "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -9128,6 +9356,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -9217,6 +9454,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -9761,6 +10010,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", + "optional": true + }, + "node_modules/zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "optional": true, + "dependencies": { + "zen-observable": "0.8.15" + } + }, "node_modules/zlib": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", @@ -9781,6 +10045,27 @@ "@jridgewell/trace-mapping": "^0.3.0" } }, + "@apollo/client": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.1.tgz", + "integrity": "sha512-xu5M/l7p9gT9Fx7nF3AQivp0XukjB7TM7tOd5wifIpI8RskYveL4I+rpTijzWrnqCPZabkbzJKH7WEAKdctt9w==", + "optional": true, + "requires": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/context": "^0.7.0", + "@wry/equality": "^0.5.0", + "@wry/trie": "^0.3.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.16.1", + "prop-types": "^15.7.2", + "response-iterator": "^0.2.6", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + } + }, "@apollo/protobufjs": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.6.tgz", @@ -11218,6 +11503,13 @@ "tslib": "^2.4.0" } }, + "@graphql-typed-document-node/core": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "optional": true, + "requires": {} + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -12018,6 +12310,23 @@ "@types/node": "*" } }, + "@types/graphql-fields": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/graphql-fields/-/graphql-fields-1.3.4.tgz", + "integrity": "sha512-McLJaAaqY7lk9d9y7E61iQrj0AwcEjSb8uHlPh7KgYV+XX1MSLlSt/alhd5k2BPRE8gy/f4lnkLGb5ke3iG66Q==", + "dev": true, + "requires": { + "graphql": "^15.3.0" + }, + "dependencies": { + "graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "dev": true + } + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -12161,6 +12470,33 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "@wry/context": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.0.tgz", + "integrity": "sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==", + "optional": true, + "requires": { + "tslib": "^2.3.0" + } + }, + "@wry/equality": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.3.tgz", + "integrity": "sha512-avR+UXdSrsF2v8vIqIgmeTY0UR91UT+IyablCyKe/uk22uOJ8fusKZnH9JH9e1/EtLeNJBtagNmL3eJdnOV53g==", + "optional": true, + "requires": { + "tslib": "^2.3.0" + } + }, + "@wry/trie": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.2.tgz", + "integrity": "sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==", + "optional": true, + "requires": { + "tslib": "^2.3.0" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -13611,6 +13947,46 @@ "tslib": "^2.1.0" } }, + "graphql-tools": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-8.3.8.tgz", + "integrity": "sha512-syDJ6hzlqm+DoiX+v8k0kogyYfdvPlpoZ+L76OD1d4Ll0qPCeZG41NzHBUJIzWWBz+Kq942m3ilEey/xCML8LQ==", + "requires": { + "@apollo/client": "~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0", + "@graphql-tools/schema": "9.0.6", + "tslib": "^2.4.0" + }, + "dependencies": { + "@graphql-tools/merge": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.8.tgz", + "integrity": "sha512-L9YE8OpxSlzADcdrc4IG7/33H/iWVXTJXX2ie67cWAb5MFN2t3JBdQMa0bnBcAoOrKB7A8g2+dIp8oXTpdzxjg==", + "requires": { + "@graphql-tools/utils": "8.13.1", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/schema": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.6.tgz", + "integrity": "sha512-/aznltpnVrurfWqXB4chWtaNmBFSk9v/KEJSpvas2fnlwwS9QnzWh6Sm/hsybWesirn5J2w60LLjMrrcCd58UA==", + "requires": { + "@graphql-tools/merge": "8.3.8", + "@graphql-tools/utils": "8.13.1", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/utils": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.13.1.tgz", + "integrity": "sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==", + "requires": { + "tslib": "^2.4.0" + } + } + } + }, "graphql-type-json": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", @@ -13644,6 +14020,15 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "optional": true, + "requires": { + "react-is": "^16.7.0" + } + }, "hpagent": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz", @@ -15240,7 +15625,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "devOptional": true }, "js-yaml": { "version": "3.14.1", @@ -15515,6 +15900,15 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "optional": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -15814,6 +16208,27 @@ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true }, + "optimism": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", + "integrity": "sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==", + "optional": true, + "requires": { + "@wry/context": "^0.6.0", + "@wry/trie": "^0.3.0" + }, + "dependencies": { + "@wry/context": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", + "integrity": "sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==", + "optional": true, + "requires": { + "tslib": "^2.3.0" + } + } + } + }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -16097,6 +16512,17 @@ "sisteransi": "^1.0.5" } }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "optional": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -16146,6 +16572,12 @@ "unpipe": "1.0.0" } }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "optional": true + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -16283,6 +16715,12 @@ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, + "response-iterator": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", + "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==", + "optional": true + }, "retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -16574,6 +17012,12 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "optional": true + }, "tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -16645,6 +17089,15 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "optional": true, + "requires": { + "tslib": "^2.1.0" + } + }, "ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -17009,6 +17462,21 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", + "optional": true + }, + "zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "optional": true, + "requires": { + "zen-observable": "0.8.15" + } + }, "zlib": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", diff --git a/modules/server/package.json b/modules/server/package.json index 671a97682..04a41bc74 100644 --- a/modules/server/package.json +++ b/modules/server/package.json @@ -51,6 +51,7 @@ "graphql-middleware": "^6.1.33", "graphql-playground-middleware-express": "^1.7.23", "graphql-scalars": "^1.19.0", + "graphql-tools": "^8.3.8", "graphql-type-json": "^0.3.2", "jsonpath": "^1.1.1", "lodash": "^4.17.21", @@ -75,6 +76,7 @@ "@babel/preset-typescript": "^7.18.6", "@babel/register": "^7.18.9", "@types/convert-units": "^2.3.5", + "@types/graphql-fields": "^1.3.4", "@types/jest": "^29.1.2", "@types/lodash": "^4.14.186", "@types/morgan": "^1.9.3", diff --git a/modules/server/src/mapping/__tests__/resolveHits/hitsToEdges.test.js b/modules/server/src/mapping/__tests__/resolveHits/hitsToEdges.test.js index 156a0ced8..21c38bcb3 100644 --- a/modules/server/src/mapping/__tests__/resolveHits/hitsToEdges.test.js +++ b/modules/server/src/mapping/__tests__/resolveHits/hitsToEdges.test.js @@ -1,16 +1,18 @@ -import hits from './mockData/wrangledHits.json'; -import nestedFields from './mockData/nestedFields.json'; -import expectedEdges from './mockData/wrangledExpectedEdges.json'; -import { hitsToEdges } from '../../resolveHits'; import Parallel from 'paralleljs'; +import { hitsToEdges } from '../../resolveHits'; + +import nestedFieldNames from './mockData/nestedFieldNames.json'; +import expectedEdges from './mockData/wrangledExpectedEdges.json'; +import hits from './mockData/wrangledHits.json'; + test('hitsToEdges should be acurate', async () => { - const edges = await hitsToEdges({ hits, nestedFields, Parallel }); + const edges = await hitsToEdges({ hits, nestedFieldNames, Parallel }); expect(edges).toEqual(expectedEdges); }); test('hitsToEdges should not block process', async () => { let complete = false; - hitsToEdges({ hits, nestedFields, Parallel }).then(() => (complete = true)); + hitsToEdges({ hits, nestedFieldNames, Parallel }).then(() => (complete = true)); expect(complete).toEqual(false); }); diff --git a/modules/server/src/mapping/__tests__/resolveHits/mockData/nestedFields.json b/modules/server/src/mapping/__tests__/resolveHits/mockData/nestedFieldNames.json similarity index 100% rename from modules/server/src/mapping/__tests__/resolveHits/mockData/nestedFields.json rename to modules/server/src/mapping/__tests__/resolveHits/mockData/nestedFieldNames.json diff --git a/modules/server/src/mapping/resolveHits.js b/modules/server/src/mapping/resolveHits.js index 6b60c54f7..b07bf5b42 100644 --- a/modules/server/src/mapping/resolveHits.js +++ b/modules/server/src/mapping/resolveHits.js @@ -1,9 +1,11 @@ import getFields from 'graphql-fields'; import { chunk } from 'lodash'; -import { buildQuery } from '../middleware'; +import { buildQuery } from '@/middleware'; + import compileFilter from './utils/compileFilter'; import esSearch from './utils/esSearch'; +import loadExtendedFields from './utils/loadExtendedFields'; const findCopyToSourceFields = (mapping, path = '', results = {}) => { Object.entries(mapping).forEach(([k, v]) => { @@ -19,12 +21,21 @@ const findCopyToSourceFields = (mapping, path = '', results = {}) => { }; export const hitsToEdges = ({ + copyToSourceFields = {}, + extendedFields = [], hits, nestedFieldNames, Parallel, - copyToSourceFields = {}, systemCores = process?.env?.SYSTEM_CORES || 2, }) => { + const extendedFieldsObj = extendedFields.reduce( + (acc, field) => ({ + ...acc, + [field.fieldName]: field, + }), + {}, + ); + /* If there's a large request, we'll trigger ludicrous mode and do some parallel map-reduce based on # of cores available. Otherwise, only one child-process @@ -40,8 +51,8 @@ export const hitsToEdges = ({ (chunk) => //Parallel.spawn output has a .then but it's not returning an actual promise new Promise((resolve) => { - new Parallel({ hits: chunk, nestedFieldNames, copyToSourceFields }) - .spawn(({ hits, nestedFieldNames, copyToSourceFields }) => { + new Parallel({ copyToSourceFields, extendedFieldsObj, hits: chunk, nestedFieldNames }) + .spawn(({ copyToSourceFields, extendedFieldsObj, hits, nestedFieldNames }) => { /* everthing inside spawn is executed in a separate thread, so we have to use good old ES5 and require for run-time dependecy bundling. @@ -77,7 +88,12 @@ export const hitsToEdges = ({ parent ? `${parent}.${fieldName}` : fieldName; const resolveNested = ({ node, nestedFieldNames, parent = '' }) => { - if (!isObject(node) || !node) return node; + if (!isObject(node) || !node) { + // Backwards compatibility for Array fields when data has not been migrated + return extendedFieldsObj?.[parent]?.isArray && !Array.isArray(node) + ? [node] + : node; + } return Object.entries(node).reduce((acc, entry) => { const fieldName = entry[0]; @@ -86,7 +102,7 @@ export const hitsToEdges = ({ // TODO: inner hits query if necessary const fullPath = joinParent(parent, fieldName); - acc[fieldName] = nestedFieldNames.includes(fullPath) + acc[fieldName] = nestedFieldNames?.includes(fullPath) ? { hits: { edges: hits.map((node) => ({ @@ -167,6 +183,7 @@ export default ({ type, Parallel, getServerSideFilter }) => let nestedFieldNames = type.nested_fields; const { esClient } = context; + const { extendedFields } = type; /** * @todo: I left this chunk here for reference, in case someone actually understands what it actually is trying to do @@ -246,10 +263,11 @@ export default ({ type, Parallel, getServerSideFilter }) => return { edges: () => hitsToEdges({ + copyToSourceFields, + extendedFields, hits, nestedFieldNames, Parallel, - copyToSourceFields, }), total: () => hits.total.value, }; diff --git a/modules/server/src/mapping/utils/loadExtendedFields.js b/modules/server/src/mapping/utils/loadExtendedFields.js index a055e281a..f990df84a 100644 --- a/modules/server/src/mapping/utils/loadExtendedFields.js +++ b/modules/server/src/mapping/utils/loadExtendedFields.js @@ -1,22 +1,11 @@ -import mapHits from './mapHits'; import { get } from 'lodash'; + import esSearch from './esSearch'; +import mapHits from './mapHits'; export default async ({ esClient, index }) => { try { - const { - hits: { total }, - } = await esSearch(esClient)({ - index, - size: 0, - _source: false, - }); - const fields = mapHits( - await esSearch(esClient)({ - index, - size: total, - }), - ); + const fields = mapHits(await esSearch(esClient)({ index })); return fields; } catch (err) { const metaData = await esSearch(esClient)({ index }); diff --git a/modules/server/src/mapping/utils/mapHits.ts b/modules/server/src/mapping/utils/mapHits.ts index 9194e8cb1..1765d941d 100644 --- a/modules/server/src/mapping/utils/mapHits.ts +++ b/modules/server/src/mapping/utils/mapHits.ts @@ -1,4 +1,4 @@ import { RequestEvent } from '@elastic/elasticsearch'; export default (esIndexResponseBody: RequestEvent['body']) => - esIndexResponseBody.hits.hits.map((hit: any) => hit._source); + esIndexResponseBody.hits.hits.map((hit: any) => hit?._source); diff --git a/modules/server/src/middleware/buildQuery/index.js b/modules/server/src/middleware/buildQuery/index.js index 08f85d11f..1c18bbbf3 100644 --- a/modules/server/src/middleware/buildQuery/index.js +++ b/modules/server/src/middleware/buildQuery/index.js @@ -292,6 +292,7 @@ export const opSwitch = ({ nestedFields, filter }) => { export default function ({ nestedFields, filters: rawFilters }) { if (Object.keys(rawFilters || {}).length === 0) return {}; + return opSwitch({ nestedFields, filter: normalizeFilters(rawFilters), diff --git a/modules/server/src/utils/dataToExportFormat.js b/modules/server/src/utils/dataToExportFormat.js index d9df6598e..a94e947a4 100644 --- a/modules/server/src/utils/dataToExportFormat.js +++ b/modules/server/src/utils/dataToExportFormat.js @@ -13,7 +13,9 @@ const getAllValue = (data) => { }; const getValue = (row, column) => { - const valueFromExtended = (value) => (column.displayValues || {})[value] || value; + const valueFromExtended = (value) => + (column.extendedDisplayValues || {})[value] || column.isArray ? value.join(';') : value; + if (column.jsonPath) { return jsonPath .query(row, column.jsonPath.split('.hits.edges[*].node.').join('[*].')) diff --git a/package.json b/package.json index aa5539b09..f111af349 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,9 @@ "prettier": { "printWidth": 100, "trailingComma": "all", - "singleQuote": true + "semi": true, + "singleQuote": true, + "useTabs": true }, "husky": { "hooks": {