diff --git a/package.json b/package.json index d03b9c6b1a..3be855ecd7 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@react-editor-js/client": "^2.1.0", "@react-editor-js/core": "^2.1.0", "@react-editor-js/server": "^2.1.0", + "@seedcompany/common": ">=0.13 <1", "ahooks": "^3.7.8", "body-parser": "^1.20.2", "compression": "^1.7.4", diff --git a/razzle.config.js b/razzle.config.js index 9bcdba8b68..01da9091bc 100644 --- a/razzle.config.js +++ b/razzle.config.js @@ -19,8 +19,9 @@ const modifyWebpackOptions = ({ cache: path.resolve(__dirname, 'cache/terser-webpack-plugin'), }; - // Run editorjs through babel, since the current loader doesn't understand the newer syntax. + // Run these through babel, since the current loader doesn't understand the newer syntax. options.babelRule.include.push( + require.resolve('@seedcompany/common').replace('.cjs', '.js'), require.resolve('@editorjs/editorjs').replace('.umd.js', '.mjs') ); @@ -36,6 +37,8 @@ const modifyWebpackConfig = (opts) => { config.resolve.plugins.push(new TsconfigPathsPlugin()); + config.resolve.alias['@seedcompany/common'] = '@seedcompany/common/index.js'; + const define = (key, value) => { opts.options.webpackOptions.definePluginOptions[key] = value; }; diff --git a/src/api/caching/invalidate-props.ts b/src/api/caching/invalidate-props.ts index 1654afa708..7c263e5c2c 100644 --- a/src/api/caching/invalidate-props.ts +++ b/src/api/caching/invalidate-props.ts @@ -1,10 +1,10 @@ import { ApolloCache, MutationUpdaterFunction } from '@apollo/client'; -import { compact, Many } from 'lodash'; -import { mapFromList, Nullable } from '~/common'; +import { Modifier } from '@apollo/client/cache'; +import { isNotFalsy, Many, mapValues, Nil } from '@seedcompany/common'; import { GqlObject, GqlTypeOf } from '../schema'; type PropKeys = ReadonlyArray< - Many & string>> + Many<(keyof GqlTypeOf & string) | Nil> >; /** @@ -28,13 +28,15 @@ export const invalidateProps = ( cache.modify({ id, - fields: mapFromList(compact(fields.flat()), (field) => [ - field, - (_, { DELETE }) => DELETE, - ]), + fields: mapValues.fromList( + fields.flat().filter(isNotFalsy), + () => deleteField + ).asRecord, }); }; +const deleteField: Modifier = (_, { DELETE }) => DELETE; + /** * A variant of {@link invalidateProps} that can be given directly to a * mutation's cache function. diff --git a/src/api/caching/lists/addItemToList.ts b/src/api/caching/lists/addItemToList.ts index 1e924a36a7..5889236f6c 100644 --- a/src/api/caching/lists/addItemToList.ts +++ b/src/api/caching/lists/addItemToList.ts @@ -1,5 +1,9 @@ -import { ApolloCache, MutationUpdaterFunction } from '@apollo/client'; -import { orderBy } from 'lodash'; +import { + ApolloCache, + MutationUpdaterFunction, + Reference, +} from '@apollo/client'; +import { sortBy } from '@seedcompany/common'; import { Except } from 'type-fest'; import { unwrapSecured } from '~/common'; import { modifyChangesetDiff } from '../../changesets'; @@ -81,7 +85,7 @@ export const addItemToList = return; } - let newList = [...existing.items, newItemRef]; + let newList: readonly Reference[] = [...existing.items, newItemRef]; // Sort the new item appropriately given the list's sort/order params const args = argsFromStoreFieldName(storeFieldName); @@ -90,15 +94,15 @@ export const addItemToList = defaultSortingForList(listId) ); if (sort && order) { - newList = orderBy( - newList, + newList = sortBy(newList, [ (ref) => { const field = readField(sort, ref); const fieldVal = unwrapSecured(field); - return fieldVal; + // Unsafely assume this value is sortable + return fieldVal as any; }, - order.toLowerCase() as Lowercase - ); + order.toLowerCase() as Lowercase, + ]); } return { diff --git a/src/api/caching/lists/modifyList.ts b/src/api/caching/lists/modifyList.ts index a04235a0ef..b19864675f 100644 --- a/src/api/caching/lists/modifyList.ts +++ b/src/api/caching/lists/modifyList.ts @@ -2,25 +2,25 @@ import { ApolloCache, isReference, TypePolicies } from '@apollo/client'; import type { Reference } from '@apollo/client'; import { Modifier } from '@apollo/client/cache/core/types/common'; import type { EntityStore } from '@apollo/client/cache/inmemory/entityStore'; +import { mapValues, Nil } from '@seedcompany/common'; import type { ConditionalKeys } from 'type-fest'; -import { keys, mapFromList, Nullable } from '~/common'; import type { Entity, GqlTypeOf } from '../../schema'; import type { Query } from '../../schema/schema.graphql'; import { typePolicies } from '../../schema/typePolicies'; import { PaginatedListOutput, SortableListInput } from './types'; // Only the keys of T that represent list fields. -type ListFieldKeys = ConditionalKeys>>; +type ListFieldKeys = ConditionalKeys | Nil>; type ObjectWithField = - | [existingObject: Nullable, field: ListFieldKeys>] + | [existingObject: Obj | Nil, field: ListFieldKeys>] | [ref: Reference, field: string]; export type ListIdentifier = | ListFieldKeys | ObjectWithField; -export type ListModifier = Modifier>>; +export type ListModifier = Modifier | Nil>; export interface ModifyListOptions { cache: ApolloCache; @@ -47,7 +47,7 @@ export const modifyList = ({ // @ts-expect-error assuming in memory cache with entity store const store: EntityStore | null = cache.data; const obj = store?.toObject()[id ?? 'ROOT_QUERY'] ?? {}; - const listVariations = keys(obj) + const listVariations = Object.keys(obj) .filter( (query) => query === field || @@ -71,7 +71,7 @@ export const modifyList = ({ } }); - const fields = mapFromList(listVariations, (field) => [field, modifier]); + const fields = mapValues.fromList(listVariations, () => modifier).asRecord; cache.modify({ ...(id ? { id } : {}), fields, diff --git a/src/api/changesets/changesetAware.ts b/src/api/changesets/changesetAware.ts index dc7db416c9..ada8594cde 100644 --- a/src/api/changesets/changesetAware.ts +++ b/src/api/changesets/changesetAware.ts @@ -1,5 +1,5 @@ import { Merge } from 'type-fest'; -import { ChangesetIdFragment, has, IdFragment } from '~/common'; +import { ChangesetIdFragment, IdFragment } from '~/common'; export const hasChangeset = ( data: IdFragment @@ -9,7 +9,7 @@ export const hasChangeset = ( ChangesetIdFragment, { changeset: NonNullable } > -> => has('changeset', data) && !!data.changeset; +> => 'changeset' in data && !!data.changeset; export const getChangeset = (data: IdFragment) => hasChangeset(data) ? data.changeset : undefined; diff --git a/src/api/client/ImpersonationContext.tsx b/src/api/client/ImpersonationContext.tsx index 795e15a3fd..3663ebca20 100644 --- a/src/api/client/ImpersonationContext.tsx +++ b/src/api/client/ImpersonationContext.tsx @@ -1,3 +1,4 @@ +import { Many, many } from '@seedcompany/common'; import { useLatest } from 'ahooks'; import Cookies from 'js-cookie'; import { noop, pickBy } from 'lodash'; @@ -8,7 +9,7 @@ import { useMemo, useState, } from 'react'; -import { ChildrenProp, Many, many } from '~/common'; +import { ChildrenProp } from '~/common'; import { Role } from '../schema/schema.graphql'; export interface Impersonation { diff --git a/src/api/client/links/delay.link.ts b/src/api/client/links/delay.link.ts index 4631b38d1e..698deb693c 100644 --- a/src/api/client/links/delay.link.ts +++ b/src/api/client/links/delay.link.ts @@ -1,5 +1,5 @@ import { fromPromise, RequestHandler } from '@apollo/client'; -import { sleep } from '~/common'; +import { delay } from '@seedcompany/common'; import { GQLOperations } from '../../operationsList'; let API_DEBUG = { @@ -33,5 +33,5 @@ export const delayLink: RequestHandler = (operation, forward) => { ) { return forward(operation); } - return fromPromise(sleep(currentDelay)).flatMap(() => forward(operation)); + return fromPromise(delay(currentDelay)).flatMap(() => forward(operation)); }; diff --git a/src/api/errorHandling/form-error-handling.ts b/src/api/errorHandling/form-error-handling.ts index 1a0c04a4ae..911cba73fd 100644 --- a/src/api/errorHandling/form-error-handling.ts +++ b/src/api/errorHandling/form-error-handling.ts @@ -1,5 +1,5 @@ +import { isNotFalsy, mapValues } from '@seedcompany/common'; import { FORM_ERROR, FormApi, setIn, SubmissionErrors } from 'final-form'; -import { identity, mapValues } from 'lodash'; import { Promisable } from 'type-fest'; import { ErrorMap, getErrorInfo, ValidationError } from './error.types'; @@ -68,7 +68,9 @@ const expandDotNotation = (input: Record) => // We'll just use the first human error string for each field export const renderValidationErrors = (e: ValidationError) => - expandDotNotation(mapValues(e.errors, (er) => Object.values(er)[0])); + expandDotNotation( + mapValues(e.errors, (_, er) => Object.values(er)[0]).asRecord + ); /** * These are the default handlers which are used as fallbacks @@ -109,7 +111,7 @@ export const handleFormError = async ( // get handler for each code .map((c) => mergedHandlers[c]) // remove unhandled codes - .filter(identity) + .filter(isNotFalsy) // normalize handlers to a standard function shape .map((h) => resolveHandler(h, utils)) // In order to build the next function for each handler we need to start diff --git a/src/api/schema/enumLists/enumLists.codegen.ts b/src/api/schema/enumLists/enumLists.codegen.ts index a7832983a9..22516fb659 100644 --- a/src/api/schema/enumLists/enumLists.codegen.ts +++ b/src/api/schema/enumLists/enumLists.codegen.ts @@ -1,5 +1,6 @@ +import { sortBy } from '@seedcompany/common'; import { GraphQLEnumType, GraphQLEnumValue } from 'graphql'; -import { lowerCase, sortBy } from 'lodash'; +import { lowerCase } from 'lodash'; import { titleCase } from 'title-case'; import { addExportedConst, diff --git a/src/api/schema/typeMap/typeMap.codegen.ts b/src/api/schema/typeMap/typeMap.codegen.ts index 59967d4d69..874316cc5f 100644 --- a/src/api/schema/typeMap/typeMap.codegen.ts +++ b/src/api/schema/typeMap/typeMap.codegen.ts @@ -1,5 +1,6 @@ +import { sortBy } from '@seedcompany/common'; import { GraphQLNamedType, isAbstractType, isObjectType } from 'graphql'; -import { difference, sortBy } from 'lodash'; +import { difference } from 'lodash'; import { OptionalKind, PropertySignatureStructure } from 'ts-morph'; import { getSchemaTypes } from '../codeGenUtil/gql.util'; import { tsMorphPlugin } from '../codeGenUtil/ts.util'; diff --git a/src/api/schema/typePolicies/lists/page-limit-pagination.ts b/src/api/schema/typePolicies/lists/page-limit-pagination.ts index 6b0991da76..0f606fc073 100644 --- a/src/api/schema/typePolicies/lists/page-limit-pagination.ts +++ b/src/api/schema/typePolicies/lists/page-limit-pagination.ts @@ -4,10 +4,8 @@ import { FieldPolicy, KeySpecifier, } from '@apollo/client/cache/inmemory/policies'; +import { sortBy } from '@seedcompany/common'; import { - isObject, - last, - orderBy, sortedIndexBy, sortedLastIndexBy, uniqBy, @@ -143,7 +141,8 @@ const mergeList = ( return (fieldVal as VariantFragment).key; } - return fieldVal; + // Unsafely assume this value is sortable + return fieldVal as any; }; let items: readonly Reference[]; @@ -172,11 +171,10 @@ const mergeList = ( if (sort && order && uniqueItems.length !== items.length) { // If we found duplicates, re-sort the list, because I'm not certain // we removed the right one(s). - items = orderBy( - uniqueItems, + items = sortBy(uniqueItems, [ readSecuredField(sort), - order.toLowerCase() as Lowercase - ); + order.toLowerCase() as Lowercase, + ]); } else { items = uniqueItems; } @@ -195,7 +193,7 @@ const spliceAscLists = ( // such as the same name or created time. const firstIdx = sortedLastIndexBy(existing, incoming[0]!, iteratee); // splice ending point is the first occurrence - const lastIdx = sortedIndexBy(existing, last(incoming)!, iteratee); + const lastIdx = sortedIndexBy(existing, incoming.at(-1)!, iteratee); // start adding incoming after firstIdx, // remove any items between firstIdx and lastIdx (if <= 0 this is skipped), // finish incoming, and continue on with rest of existing list. @@ -232,7 +230,7 @@ const objectToKeyArgsRecurse = (obj: Record): KeySpecifier => (keyArgs: KeySpecifier, [key, val]) => [ ...keyArgs, key, - ...(isObject(val) ? [objectToKeyArgsRecurse(val)] : []), + ...(val && typeof val === 'object' ? [objectToKeyArgsRecurse(val)] : []), ], [] ); @@ -240,7 +238,7 @@ const objectToKeyArgsRecurse = (obj: Record): KeySpecifier => const cleanEmptyObjects = (obj: Record): Record => { const res: Record = {}; for (const [key, value] of Object.entries(obj)) { - if (!isObject(value)) { + if (!(value && typeof value === 'object')) { res[key] = value; continue; } diff --git a/src/common/approach.tsx b/src/common/approach.tsx index 8ef0b43a65..22b6e1ae02 100644 --- a/src/common/approach.tsx +++ b/src/common/approach.tsx @@ -4,9 +4,9 @@ import { PlayCircleFilled, Translate, } from '@mui/icons-material'; +import { entries, mapValues } from '@seedcompany/common'; import { ReactNode } from 'react'; import { ProductApproach, ProductMethodology } from '~/api/schema.graphql'; -import { entries, mapFromList } from './array-helpers'; export const ApproachMethodologies: Record< ProductApproach, @@ -32,7 +32,7 @@ export const ApproachMethodologies: Record< export const MethodologyToApproach = entries(ApproachMethodologies).reduce( (map, [approach, methodologies]) => ({ ...map, - ...mapFromList(methodologies, (methodology) => [methodology, approach]), + ...mapValues.fromList(methodologies, () => approach).asRecord, }), {} ) as Record; diff --git a/src/common/array-helpers.ts b/src/common/array-helpers.ts index 937b995bf5..e00f316f55 100644 --- a/src/common/array-helpers.ts +++ b/src/common/array-helpers.ts @@ -1,20 +1,9 @@ -import { compact, fill, isEmpty, times } from 'lodash'; -import { Nullable } from './types'; - -export type Many = T | readonly T[]; - -export const many = (items: Many): readonly T[] => - Array.isArray(items) ? items : [items as T]; +import { Nil } from '@seedcompany/common'; +import { fill, times } from 'lodash'; export const isListNotEmpty = ( - list: Nullable -): list is readonly T[] & { 0: T } => !isEmpty(list); - -/** Converts a CSV string into a cleaned list */ -export const csv = ( - list: string, - separator = ',' -): T[] => compact(list.split(separator).map((i) => i.trim() as T)); + list: readonly T[] | Nil +): list is readonly T[] & { 0: T } => !!list && list.length > 0; /** * Returns the list if given or a list of undefined items @@ -27,47 +16,6 @@ export const listOrPlaceholders = ( ): ReadonlyArray => list ?? fill(times(placeholderCount), undefined); -/** - * Just like Object.entries except keys are strict and only pairs that exist are iterated - */ -export const entries: (o: { [Key in K]?: V }) => Array< - [K, V] -> = Object.entries as any; - -/** - * Just like Object.keys except keys are strict - */ -export const keys: (o: Record) => K[] = - Object.keys as any; - -/** Converts list to map given a function that returns a [key, value] tuple. */ -export const mapFromList = ( - list: readonly T[], - mapper: (item: T) => readonly [K, S] | null -): Record => { - const out: Partial> = {}; - return list.reduce((acc, item) => { - const res = mapper(item); - if (!res) { - return acc; - } - const [key, value] = res; - acc[key] = value; - return acc; - }, out as Record); -}; - -/** - * Work around `in` operator not narrowing type - * https://github.com/microsoft/TypeScript/issues/21732 - */ -export function has( - key: K, - obj: T -): obj is T & Record { - return obj && key in (obj as any); -} - /** * Array splice but it returns a new list instead of modifying the original one * and returning the removed items. @@ -82,11 +30,3 @@ export const splice = ( newList.splice(...args); return newList; }; - -/** - * Helper for array filter that correctly narrows type - * @example - * .filter(notNullish) - */ -export const notNullish = (item: T | null | undefined): item is T => - item != null; diff --git a/src/common/biblejs/reference.ts b/src/common/biblejs/reference.ts index 78681cd1e9..1e654cb4a6 100644 --- a/src/common/biblejs/reference.ts +++ b/src/common/biblejs/reference.ts @@ -1,4 +1,4 @@ -import { groupBy, isEqual, sum } from 'lodash'; +import { isEqual, sum } from 'lodash'; import { UnspecifiedScripturePortion } from '~/api/schema.graphql'; import { ScriptureFragment } from '../fragments'; import { Nullable } from '../types'; @@ -253,15 +253,6 @@ export const getUnspecifiedScriptureDisplay = ( return `${book} (${totalVerses} / ${validTotalVerses} verses)`; }; -/** - * Creates a dictionary from an array of scripture ranges - * Keys are bible books and the values are array of scriptureRanges that start with that book - */ -export const scriptureRangeDictionary = ( - scriptureReferenceArr: readonly ScriptureRange[] | undefined = [] -): Record => - groupBy(scriptureReferenceArr, (range) => range.start.book); - export const getFullBookRange = (book: string): ScriptureRange => { const bookObject = books.find((bookObj) => bookObj.names.includes(book)); const lastChapter = bookObject ? bookObject.chapters.length : 1; diff --git a/src/common/index.ts b/src/common/index.ts index a9ca0d6444..605fe9625f 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -11,8 +11,6 @@ export * from './log'; export * from './scalar.types'; export * from './secured'; export * from './sensitivity'; -export * from './simpleSwitch'; -export * from './sleep'; export * from './styles'; export * from './sx'; export * from './types'; diff --git a/src/common/secured.ts b/src/common/secured.ts index b989e3e881..91cf7f0530 100644 --- a/src/common/secured.ts +++ b/src/common/secured.ts @@ -1,7 +1,5 @@ -import { isPlainObject } from 'lodash'; +import { Nil } from '@seedcompany/common'; import { ConditionalKeys } from 'type-fest'; -import { has } from './array-helpers'; -import { Nullable } from './types'; interface Readable { canRead: boolean; @@ -12,25 +10,26 @@ interface Editable { } export interface SecuredProp extends Readable, Editable { - value?: Nullable; + value?: T | Nil; } export type UnsecuredProp = T extends Partial> ? P : T; export const isSecured = (value: unknown): value is SecuredProp => - Boolean(value) && - isPlainObject(value) && - 'canEdit' in (value as any) && - 'canRead' in (value as any); + !!value && + typeof value === 'object' && + 'canEdit' in value && + 'canRead' in value; -export const unwrapSecured = (value: unknown): unknown => - isPlainObject(value) && - has('__typename', value) && +export const unwrapSecured = (value: T) => + (!!value && + typeof value === 'object' && + '__typename' in value && typeof value.__typename === 'string' && value.__typename.startsWith('Secured') && - has('value', value) + 'value' in value ? value.value - : value; + : value) as T extends SecuredProp ? U : T; /** * Can the user read any of the fields of this object? @@ -38,7 +37,7 @@ export const unwrapSecured = (value: unknown): unknown => * Otherwise only the keys provided will be checked. */ export const canReadAny = >( - obj: Nullable, + obj: T | Nil, defaultValue = false, ...keys: K[] ) => { @@ -57,7 +56,7 @@ export const canReadAny = >( * Otherwise only the keys provided will be checked. */ export const canEditAny = >( - obj: Nullable, + obj: T | Nil, defaultValue = false, ...keys: K[] ) => { diff --git a/src/common/sensitivity.ts b/src/common/sensitivity.ts index 0a3a19f4f5..c96d0c124d 100644 --- a/src/common/sensitivity.ts +++ b/src/common/sensitivity.ts @@ -1,10 +1,10 @@ -import { orderBy } from 'lodash'; +import { sortBy } from '@seedcompany/common'; import { Sensitivity } from '~/api/schema.graphql'; export const highestSensitivity = ( sensitivities: Sensitivity[], defaultLevel: Sensitivity -) => orderBy(sensitivities, (sens) => ranks[sens], 'desc')[0] ?? defaultLevel; +) => sortBy(sensitivities, (sens) => ranks[sens]).at(-1) ?? defaultLevel; const ranks: Record = { Low: 0, diff --git a/src/common/simpleSwitch.ts b/src/common/simpleSwitch.ts deleted file mode 100644 index 5c8fd41a98..0000000000 --- a/src/common/simpleSwitch.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const simpleSwitch = ( - key: K, - options: Record -): T | undefined => options[key]; diff --git a/src/common/sleep.ts b/src/common/sleep.ts deleted file mode 100644 index a8c24f36c5..0000000000 --- a/src/common/sleep.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 0a98410566..953e12c75d 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -1,6 +1,5 @@ import { Typography } from '@mui/material'; import { To } from 'history'; -import { isString } from 'lodash'; import { forwardRef, ReactNode } from 'react'; import { useMatch } from 'react-router-dom'; import { Link, LinkProps } from '../Routing'; @@ -18,7 +17,7 @@ export const Breadcrumb = forwardRef< BreadcrumbProps >(function Breadcrumb({ to, children, LinkProps, ...rest }, ref) { const active = - useMatch(to == null ? '' : isString(to) ? to : to.pathname!) || + useMatch(to == null ? '' : typeof to === 'string' ? to : to.pathname!) || // RR doesn't think current page is active. maybe a bug? to === '.'; diff --git a/src/components/Changeset/ChangesetBadge.tsx b/src/components/Changeset/ChangesetBadge.tsx index 1004be7ef0..5922797948 100644 --- a/src/components/Changeset/ChangesetBadge.tsx +++ b/src/components/Changeset/ChangesetBadge.tsx @@ -1,8 +1,9 @@ import { Badge, Grid, TooltipProps, Typography } from '@mui/material'; +import { mapEntries } from '@seedcompany/common'; import { startCase } from 'lodash'; import { cloneElement, isValidElement, ReactElement, ReactNode } from 'react'; import { makeStyles } from 'tss-react/mui'; -import { mapFromList, UseStyles } from '~/common'; +import { UseStyles } from '~/common'; import { BadgeWithTooltip } from '../BadgeWithTooltip'; import { PaperTooltip } from '../PaperTooltip'; import { DiffMode } from './ChangesetDiffContext'; @@ -19,14 +20,14 @@ const useStyles = makeStyles< badge: { padding: 0, cursor: 'help', - ...mapFromList(['added', 'changed', 'removed'] as const, (mode) => { + ...mapEntries(['added', 'changed', 'removed'], (mode) => { const paletteKey = modeToPalette[mode]; const css = { color: palette[paletteKey].contrastText, background: palette[paletteKey].main, }; return [`&.${classes[mode]}`, css]; - }), + }).asRecord, }, icon: { fontSize: 12, diff --git a/src/components/Changeset/ChangesetDiffContext.tsx b/src/components/Changeset/ChangesetDiffContext.tsx index 3a3b11ffae..16906d4ef1 100644 --- a/src/components/Changeset/ChangesetDiffContext.tsx +++ b/src/components/Changeset/ChangesetDiffContext.tsx @@ -1,7 +1,8 @@ import { useApolloClient } from '@apollo/client'; +import { mapKeys, Nil } from '@seedcompany/common'; import { createContext, useCallback, useContext, useMemo } from 'react'; import { Entity } from '~/api'; -import { ChildrenProp, IdFragment, mapFromList, Nullable } from '~/common'; +import { ChildrenProp, IdFragment } from '~/common'; import { ChangesetDiffFragment as Diff, ChangesetDiffItemFragment as DiffItem, @@ -18,7 +19,7 @@ type DetermineChangesetDiffItemFn = < T extends Entity, TDiffItem extends EntityFromChangesetDiff >( - obj: Nullable + obj: T | Nil ) => | { mode: undefined; current: undefined; previous: undefined } | { mode: 'added'; current: TDiffItem; previous: undefined } @@ -53,20 +54,23 @@ export const ChangesetDiffProvider = ( } & ChildrenProp ) => { const apollo = useApolloClient(); - const diff: ProcessedDiff = useMemo(() => { - const toCacheId = (obj: any) => { - const id = apollo.cache.identify(obj); - return id ? ([id, obj] as const) : null; - }; - return { - added: mapFromList(props.value?.added ?? [], toCacheId), - removed: mapFromList(props.value?.removed ?? [], toCacheId), - changed: mapFromList(props.value?.changed ?? [], (obj) => { - const pair = toCacheId(obj.updated); - return pair ? [pair[0], obj] : null; - }), - }; - }, [apollo, props.value]); + const diff: ProcessedDiff = useMemo( + () => ({ + added: mapKeys.fromList( + props.value?.added ?? [], + (obj, { SKIP }) => apollo.cache.identify(obj) ?? SKIP + ).asRecord, + removed: mapKeys.fromList( + props.value?.removed ?? [], + (obj, { SKIP }) => apollo.cache.identify(obj) ?? SKIP + ).asRecord, + changed: mapKeys.fromList( + props.value?.changed ?? [], + (obj, { SKIP }) => apollo.cache.identify(obj.updated) ?? SKIP + ).asRecord, + }), + [apollo, props.value] + ); const determineChangesetDiffItem = useCallback( (obj: any) => { diff --git a/src/components/Changeset/ChangesetIcon.tsx b/src/components/Changeset/ChangesetIcon.tsx index 590e39e447..1b18f8346d 100644 --- a/src/components/Changeset/ChangesetIcon.tsx +++ b/src/components/Changeset/ChangesetIcon.tsx @@ -4,7 +4,7 @@ import { Remove as RemoveIcon, } from '@mui/icons-material'; import { SvgIconProps } from '@mui/material'; -import { simpleSwitch } from '~/common'; +import { simpleSwitch } from '@seedcompany/common'; import { DiffMode } from './ChangesetDiffContext'; export interface ChangesetIconProps extends SvgIconProps { diff --git a/src/components/DataButton/DataButton.tsx b/src/components/DataButton/DataButton.tsx index 5e5cc9eadc..5ef7f054bc 100644 --- a/src/components/DataButton/DataButton.tsx +++ b/src/components/DataButton/DataButton.tsx @@ -5,7 +5,6 @@ import { Skeleton, TooltipProps, } from '@mui/material'; -import { isFunction } from 'lodash'; import { ReactNode } from 'react'; import { extendSx, SecuredProp } from '~/common'; import { Redacted } from '../Redacted'; @@ -37,11 +36,12 @@ export const DataButton = ({ emptyProp ); - const data = isFunction(children) - ? showData && secured?.value - ? children(secured.value) ?? empty - : empty - : children ?? empty; + const data = + typeof children === 'function' + ? showData && secured?.value + ? children(secured.value) ?? empty + : empty + : children ?? empty; const btn = ( diff --git a/src/components/PartnershipCard/PartnershipCard.stories.tsx b/src/components/PartnershipCard/PartnershipCard.stories.tsx index 14462a5d73..c4512f4093 100644 --- a/src/components/PartnershipCard/PartnershipCard.stories.tsx +++ b/src/components/PartnershipCard/PartnershipCard.stories.tsx @@ -1,7 +1,7 @@ +import { csv } from '@seedcompany/common'; import { action } from '@storybook/addon-actions'; import { select, text } from '@storybook/addon-knobs'; import { FinancialReportingTypeList, PartnershipAgreementStatusList } from '~/api/schema.graphql'; -import { csv } from '~/common'; import { date, dateTime } from '../knobs.stories'; import { PartnershipCard } from './PartnershipCard'; import { PartnershipCardFragment } from './PartnershipCard.graphql'; diff --git a/src/components/PeriodicReports/OverviewCard/PeriodicReportCard.tsx b/src/components/PeriodicReports/OverviewCard/PeriodicReportCard.tsx index 277c5cee77..2afb622a90 100644 --- a/src/components/PeriodicReports/OverviewCard/PeriodicReportCard.tsx +++ b/src/components/PeriodicReports/OverviewCard/PeriodicReportCard.tsx @@ -8,17 +8,11 @@ import { Typography, } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { Many, simpleSwitch } from '@seedcompany/common'; import { useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { ReportType } from '~/api/schema.graphql'; -import { - extendSx, - gridTemplateAreas, - Many, - SecuredProp, - simpleSwitch, - StyleProps, -} from '~/common'; +import { extendSx, gridTemplateAreas, SecuredProp, StyleProps } from '~/common'; import { EditablePeriodicReportField, UpdatePeriodicReportDialog, diff --git a/src/components/PeriodicReports/PeriodicReportsTable.tsx b/src/components/PeriodicReports/PeriodicReportsTable.tsx index 33d9c0c1b7..7d10dab577 100644 --- a/src/components/PeriodicReports/PeriodicReportsTable.tsx +++ b/src/components/PeriodicReports/PeriodicReportsTable.tsx @@ -1,7 +1,8 @@ import { SkipNextRounded as SkipIcon } from '@mui/icons-material'; import { Box } from '@mui/material'; import { DataGrid, DataGridProps, GridColDef } from '@mui/x-data-grid'; -import { Many, without } from 'lodash'; +import { Many } from '@seedcompany/common'; +import { without } from 'lodash'; import { useSnackbar } from 'notistack'; import { useState } from 'react'; import { Except } from 'type-fest'; diff --git a/src/components/Picture/Picture.tsx b/src/components/Picture/Picture.tsx index 8161b622b1..031e3f0326 100644 --- a/src/components/Picture/Picture.tsx +++ b/src/components/Picture/Picture.tsx @@ -1,9 +1,9 @@ +import { many } from '@seedcompany/common'; import { toFinite } from 'lodash'; import { memo, useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { makeStyles } from 'tss-react/mui'; import { Merge } from 'type-fest'; -import { many } from '~/common'; import { useIsBot } from '../../hooks'; import { usePictureSizes } from '../PictureSizes'; diff --git a/src/components/RichText/EditorJsTheme.ts b/src/components/RichText/EditorJsTheme.ts index 4e9f3e3494..20d90f78c7 100644 --- a/src/components/RichText/EditorJsTheme.ts +++ b/src/components/RichText/EditorJsTheme.ts @@ -1,5 +1,5 @@ import { Theme } from '@mui/material'; -import { mapFromList } from '../../common'; +import { mapEntries } from '@seedcompany/common'; export const EditorJsTheme = (theme: Theme) => ({ zIndex: 2, // keep toolbars above other fields' input labels. @@ -67,7 +67,7 @@ export const EditorJsTheme = (theme: Theme) => ({ }, // Sync Header block to theme - ...mapFromList([1, 2, 3, 4, 5, 6] as const, (level) => { + ...mapEntries([1, 2, 3, 4, 5, 6], (level) => { const v = `h${level}` as const; const styles = { ...theme.typography[v], @@ -75,7 +75,7 @@ export const EditorJsTheme = (theme: Theme) => ({ mb: '0.35em', // matches MUI Typography gutterBottom }; return [v, styles]; - }), + }).asRecord, // Sync Delimiter block to theme '.ce-delimiter': { diff --git a/src/components/RichText/RichTextField.tsx b/src/components/RichText/RichTextField.tsx index f50d7935ee..c4318030de 100644 --- a/src/components/RichText/RichTextField.tsx +++ b/src/components/RichText/RichTextField.tsx @@ -16,6 +16,7 @@ import { TextField, TextFieldProps, } from '@mui/material'; +import { many } from '@seedcompany/common'; import { useDebounceFn, useEventListener } from 'ahooks'; import { identity, isEqual, pick, sumBy } from 'lodash'; import { @@ -30,7 +31,7 @@ import { useState, } from 'react'; import filterXSS from 'xss'; -import { extendSx, many, Nullable, StyleProps } from '~/common'; +import { extendSx, Nullable, StyleProps } from '~/common'; import { FieldConfig, useField } from '../form'; import { getHelperText, showError } from '../form/util'; import { FormattedNumber } from '../Formatters'; diff --git a/src/components/Sort/SortControl.tsx b/src/components/Sort/SortControl.tsx index 73dbbc74c5..4df2e4e3cd 100644 --- a/src/components/Sort/SortControl.tsx +++ b/src/components/Sort/SortControl.tsx @@ -1,5 +1,4 @@ import { RadioGroup } from '@mui/material'; -import { isString } from 'lodash'; import { ReactNode } from 'react'; import { Order } from '~/api/schema.graphql'; @@ -25,7 +24,7 @@ export const SortControl = ({ value={value} onChange={(e) => { // ignore DOM event - if (isString(e.target.value)) { + if (typeof e.target.value === 'string') { return; } onChange(e.target.value); diff --git a/src/components/files/FilePreview/EmailPreview.tsx b/src/components/files/FilePreview/EmailPreview.tsx index 42462675f4..5e381b36c9 100644 --- a/src/components/files/FilePreview/EmailPreview.tsx +++ b/src/components/files/FilePreview/EmailPreview.tsx @@ -1,10 +1,10 @@ import MsgReader from '@freiraum/msgreader'; import { Typography } from '@mui/material'; +import { mapEntries } from '@seedcompany/common'; import parseHtml from 'html-react-parser'; import { DateTime } from 'luxon'; import { useCallback, useEffect, useState } from 'react'; import { makeStyles } from 'tss-react/mui'; -import { mapFromList } from '~/common'; import { FormattedDateTime } from '../../Formatters'; import { PreviewerProps } from './FilePreview'; import { PreviewLoading } from './PreviewLoading'; @@ -14,10 +14,10 @@ export const parseEmail = (buffer: ArrayBuffer) => { if ('error' in data) { throw new Error(data.error); } - const matches = Array.from((data.headers ?? '').matchAll(/(.*): (.*)/g)); - const headers = mapFromList(matches, (match) => - match[1] && match[2] ? [match[1], match[2]] : null - ); + const headers = mapEntries( + (data.headers ?? '').matchAll(/(.*): (.*)/g), + ([_, k, v], { SKIP }) => (k && v ? [k, v] : SKIP) + ).asRecord; return { ...data, from: { name: data.senderName, email: data.senderEmail }, diff --git a/src/components/form/EnumField.tsx b/src/components/form/EnumField.tsx index c1ce6fe6dd..38bd89f3e2 100644 --- a/src/components/form/EnumField.tsx +++ b/src/components/form/EnumField.tsx @@ -13,7 +13,7 @@ import { ToggleButtonGroup, ToggleButtonProps, } from '@mui/material'; -import { sortBy } from 'lodash'; +import { Many, many, sortBy } from '@seedcompany/common'; import { createContext, FocusEvent, @@ -28,7 +28,7 @@ import { } from 'react'; import { makeStyles } from 'tss-react/mui'; import { Except, MergeExclusive } from 'type-fest'; -import { Many, many, StyleProps } from '~/common'; +import { StyleProps } from '~/common'; import { FieldConfig, useField, Value } from './useField'; import { getHelperText, showError } from './util'; diff --git a/src/components/form/FieldGroup.tsx b/src/components/form/FieldGroup.tsx index dcbbd1ea41..2666e348be 100644 --- a/src/components/form/FieldGroup.tsx +++ b/src/components/form/FieldGroup.tsx @@ -1,4 +1,4 @@ -import { compact } from 'lodash'; +import { isNotFalsy } from '@seedcompany/common'; import { createContext, useContext } from 'react'; import { ChildrenProp } from '~/common'; @@ -20,5 +20,5 @@ export const FieldGroup = ({ export const useFieldName = (name: string) => { const prefix = useContext(FieldGroupContext); - return compact([prefix, name]).join('.'); + return [prefix, name].filter(isNotFalsy).join('.'); }; diff --git a/src/components/form/useField.ts b/src/components/form/useField.ts index ec9277ebc8..a4b307f1a1 100644 --- a/src/components/form/useField.ts +++ b/src/components/form/useField.ts @@ -1,8 +1,9 @@ +import { Many, many } from '@seedcompany/common'; import { identity } from 'lodash'; import { useContext, useEffect } from 'react'; import { UseFieldConfig, useField as useFinalField } from 'react-final-form'; import { Except } from 'type-fest'; -import { callSome, Many, many, Nullable } from '~/common'; +import { callSome, Nullable } from '~/common'; import { useFirstMountState } from '~/hooks'; import { AutoSubmitOptionsContext } from './AutoSubmit'; import { useFieldName } from './FieldGroup'; diff --git a/src/components/form/util.ts b/src/components/form/util.ts index 7cf521d1d1..d9cba2ac4e 100644 --- a/src/components/form/util.ts +++ b/src/components/form/util.ts @@ -1,4 +1,4 @@ -import { difference, differenceWith, isEmpty, isEqual } from 'lodash'; +import { difference, differenceWith, isEqual } from 'lodash'; import { MutableRefObject, ReactNode, useCallback, useRef } from 'react'; import { FieldMetaState, useFormState } from 'react-final-form'; import { Nullable } from '~/common'; @@ -50,11 +50,11 @@ export const isListEqualBy = (compareBy: (item: T) => any) => ); export const areListsEqual = (a: any, b: any) => - isEmpty(difference(a, b)) && isEmpty(difference(b, a)); + difference(a, b).length === 0 && difference(b, a).length === 0; export const areListsDeepEqual = (a: any, b: any) => - isEmpty(differenceWith(a, b, isEqual)) && - isEmpty(differenceWith(b, a, isEqual)); + differenceWith(a, b, isEqual).length === 0 && + differenceWith(b, a, isEqual).length === 0; export const compareNullable = (fn: (a: T, b: T) => boolean) => diff --git a/src/hooks/useQueryParams.ts b/src/hooks/useQueryParams.ts index 9cc036fc29..ae3c92e1cd 100644 --- a/src/hooks/useQueryParams.ts +++ b/src/hooks/useQueryParams.ts @@ -1,12 +1,5 @@ -import { - compact, - invert, - mapKeys, - mapValues, - omit, - pick, - pickBy, -} from 'lodash'; +import { isNotFalsy, mapKeys, mapValues } from '@seedcompany/common'; +import { invert, omit, pick, pickBy } from 'lodash'; import { useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { @@ -18,7 +11,6 @@ import { StringParam, QueryParamConfig as UpstreamQueryParamConfig, } from 'serialize-query-params'; -import { entries, mapFromList } from '~/common'; import { areListsEqual, compareNullable } from '../components/form/util'; export { NumberParam, StringParam } from 'serialize-query-params'; @@ -33,8 +25,8 @@ export interface QueryParamConfig export const ListParam: QueryParamConfig = { encode: (val) => encodeDelimitedArray(val, ',') || undefined, decode: (val) => { - const list = compact(decodeDelimitedArray(val, ',')); - return list.length > 0 ? list : undefined; + const list = decodeDelimitedArray(val, ',')?.filter(isNotFalsy); + return list && list.length > 0 ? list : undefined; }, equals: compareNullable(areListsEqual), }; @@ -49,33 +41,33 @@ export const BooleanParam = (): QueryParamConfig => ({ : undefined, }); -export const EnumListParam = ( +export const EnumListParam = ( options: readonly T[], mappingOverride?: Partial> ): QueryParamConfig => { - const decodeMapping = mapFromList(options, (opt) => [ - mappingOverride?.[opt] ?? opt.toLowerCase(), - opt, - ]); + const decodeMapping = mapKeys.fromList(options, (opt) => { + return mappingOverride?.[opt] ?? opt.toLowerCase(); + }).asRecord; const encodeMapping = invert(decodeMapping); return withTransform(ListParam, { encode: (value, encoder) => encoder(value?.map((v) => encodeMapping[v] ?? v)), decode: (raw, decoder) => { - const value = compact(decoder(raw)?.map((v) => decodeMapping[v])); - return value.length > 0 ? value : undefined; + const value = decoder(raw) + ?.map((v) => decodeMapping[v]) + .filter(isNotFalsy); + return value && value.length > 0 ? value : undefined; }, }); }; -export const EnumParam = ( +export const EnumParam = ( options: readonly T[], mappingOverride?: Partial> ): QueryParamConfig => { - const decodeMapping = mapFromList(options, (opt) => [ - mappingOverride?.[opt] ?? opt.toLowerCase(), - opt, - ]); + const decodeMapping = mapKeys.fromList(options, (opt) => { + return mappingOverride?.[opt] ?? opt.toLowerCase(); + }).asRecord; const encodeMapping = invert(decodeMapping); return withTransform(StringParam, { encode: (value, encoder) => @@ -186,12 +178,15 @@ type QueryParamConfigMapShape = Record>; export const makeQueryHandler = ( paramConfigMap: QPCMap ) => { - const rawKeysToOurs = mapFromList(entries(paramConfigMap), ([key, value]) => [ - value.key ?? key, - key, - ]); + const rawKeysToOurs = mapKeys( + paramConfigMap, + (key, value) => value.key ?? key + ).asRecord; const rawKeys = Object.keys(rawKeysToOurs); - const defaultValues = mapValues(paramConfigMap, (c) => c.defaultValue); + const defaultValues = mapValues( + paramConfigMap, + (_, c) => c.defaultValue + ).asRecord; return () => { const [search, setNext] = useSearchParams(); @@ -205,11 +200,14 @@ export const makeQueryHandler = ( const filtered = pick(toObj, rawKeys); // convert raw keys to our keys - const mapped = mapKeys(filtered, (_, key) => rawKeysToOurs[key]); + const mapped = mapKeys(filtered, (key) => rawKeysToOurs[key]).asRecord; // decode values const decoded = decodeQueryParams(paramConfigMap, mapped as any); // merge in default values - const defaulted = { ...defaultValues, ...decoded }; + const defaulted: DecodedValueMap = { + ...defaultValues, + ...decoded, + }; return [defaulted, unrelated] as const; }, [search]); @@ -225,8 +223,8 @@ export const makeQueryHandler = ( // convert our keys to the configured raw keys const mapped = mapKeys( filtered, - (_, key) => paramConfigMap[key]!.key ?? key - ); + (key) => paramConfigMap[key]!.key ?? key + ).asRecord; // Merge in unrelated query params so they are preserved const merged = { ...unrelated, ...mapped }; diff --git a/src/scenes/Authentication/Logout/Logout.tsx b/src/scenes/Authentication/Logout/Logout.tsx index b02375092d..28aaecbb9f 100644 --- a/src/scenes/Authentication/Logout/Logout.tsx +++ b/src/scenes/Authentication/Logout/Logout.tsx @@ -1,9 +1,9 @@ import { useApolloClient, useMutation } from '@apollo/client'; +import { delay } from '@seedcompany/common'; import { useMount } from 'ahooks'; import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { ImpersonationContext } from '~/api/client/ImpersonationContext'; -import { sleep as delay } from '~/common'; import { AuthWaiting } from '../AuthWaiting'; import { LogoutDocument } from './logout.graphql'; diff --git a/src/scenes/Engagement/EditEngagement/EditEngagementDialog.tsx b/src/scenes/Engagement/EditEngagement/EditEngagementDialog.tsx index c9b8af9923..4835277137 100644 --- a/src/scenes/Engagement/EditEngagement/EditEngagementDialog.tsx +++ b/src/scenes/Engagement/EditEngagement/EditEngagementDialog.tsx @@ -1,6 +1,7 @@ import { useMutation } from '@apollo/client'; +import { isNotFalsy, Many, many, mapKeys } from '@seedcompany/common'; import { setIn } from 'final-form'; -import { compact, keyBy, pick, startCase } from 'lodash'; +import { pick, startCase } from 'lodash'; import { ComponentType, useMemo } from 'react'; import { Except, Merge } from 'type-fest'; import { invalidateProps } from '~/api'; @@ -15,8 +16,6 @@ import { DisplayLocationFragment, ExtractStrict, labelFrom, - Many, - many, MethodologyToApproach, } from '~/common'; import { @@ -115,7 +114,7 @@ const fieldMapping: Record< engagement.__typename === 'InternshipEngagement' ? engagement.position.options : []; - const groups = keyBy(options, (o) => o.position); + const groups = mapKeys.fromList(options, (o) => o.position).asRecord; return ( o.position)} groupBy={(p) => { const option = groups[p]; - return compact([ - labelFrom(InternshipProgramLabels)(option?.program), - labelFrom(InternshipDomainLabels)(option?.domain), - ]).join(' - '); + return [ + labelFrom(InternshipProgramLabels)(option.program), + labelFrom(InternshipDomainLabels)(option.domain), + ] + .filter(isNotFalsy) + .join(' - '); }} getOptionLabel={labelFrom(InternshipPositionLabels)} /> diff --git a/src/scenes/Engagement/InternshipEngagement/InternshipEngagementDetail.tsx b/src/scenes/Engagement/InternshipEngagement/InternshipEngagementDetail.tsx index 865dcfd8c3..aaa7c05226 100644 --- a/src/scenes/Engagement/InternshipEngagement/InternshipEngagementDetail.tsx +++ b/src/scenes/Engagement/InternshipEngagement/InternshipEngagementDetail.tsx @@ -1,12 +1,13 @@ import { DateRange } from '@mui/icons-material'; import { Breadcrumbs, Grid, Typography } from '@mui/material'; +import { Many } from '@seedcompany/common'; import { Helmet } from 'react-helmet-async'; import { makeStyles } from 'tss-react/mui'; import { EngagementStatusLabels, InternshipPositionLabels, } from '~/api/schema.graphql'; -import { labelFrom, Many } from '~/common'; +import { labelFrom } from '~/common'; import { DataButton } from '../../../components/DataButton'; import { DefinedFileCard } from '../../../components/DefinedFileCard'; import { useDialog } from '../../../components/Dialog'; diff --git a/src/scenes/Engagement/LanguageEngagement/Header/LanguageEngagementHeader.tsx b/src/scenes/Engagement/LanguageEngagement/Header/LanguageEngagementHeader.tsx index e3ec13a241..da4048b02f 100644 --- a/src/scenes/Engagement/LanguageEngagement/Header/LanguageEngagementHeader.tsx +++ b/src/scenes/Engagement/LanguageEngagement/Header/LanguageEngagementHeader.tsx @@ -1,9 +1,10 @@ import { DateRange, Edit } from '@mui/icons-material'; import { Breadcrumbs, Grid, Tooltip, Typography } from '@mui/material'; +import { Many } from '@seedcompany/common'; import { Helmet } from 'react-helmet-async'; import { makeStyles } from 'tss-react/mui'; import { EngagementStatusLabels } from '~/api/schema.graphql'; -import { canEditAny, labelFrom, Many } from '~/common'; +import { canEditAny, labelFrom } from '~/common'; import { BooleanProperty } from '../../../../components/BooleanProperty'; import { DataButton } from '../../../../components/DataButton'; import { useDialog } from '../../../../components/Dialog'; diff --git a/src/scenes/Engagement/LanguageEngagement/PlanningSpreadsheet/PlanningSpreadsheet.tsx b/src/scenes/Engagement/LanguageEngagement/PlanningSpreadsheet/PlanningSpreadsheet.tsx index 459b00417e..bb6575b92e 100644 --- a/src/scenes/Engagement/LanguageEngagement/PlanningSpreadsheet/PlanningSpreadsheet.tsx +++ b/src/scenes/Engagement/LanguageEngagement/PlanningSpreadsheet/PlanningSpreadsheet.tsx @@ -1,5 +1,6 @@ import { useMutation } from '@apollo/client'; import { Tooltip, Typography } from '@mui/material'; +import { entries } from '@seedcompany/common'; import { pick } from 'lodash'; import { makeStyles } from 'tss-react/mui'; import { @@ -9,7 +10,6 @@ import { import { ApproachMethodologies, displayMethodology, - entries, StyleProps, } from '~/common'; import { DefinedFileCard } from '../../../../components/DefinedFileCard'; @@ -99,7 +99,7 @@ export const PlanningSpreadsheet = ({ engagement, ...rest }: Props) => { {ProductApproachLabels[approach]} - {methodologies.map((option: Methodology) => ( + {methodologies.map((option) => ( { ...sort.value, filter: { ...omit(filters, 'tab'), - ...simpleSwitch(filters.tab, { - pinned: { pinned: true }, - }), + ...(filters.tab === 'pinned' ? { pinned: true } : {}), }, }, }, diff --git a/src/scenes/Partners/Detail/PartnerDetail.tsx b/src/scenes/Partners/Detail/PartnerDetail.tsx index 89b9b196e5..6a84017f8a 100644 --- a/src/scenes/Partners/Detail/PartnerDetail.tsx +++ b/src/scenes/Partners/Detail/PartnerDetail.tsx @@ -9,7 +9,7 @@ import { Tooltip, Typography, } from '@mui/material'; -import { Many } from 'lodash'; +import { Many } from '@seedcompany/common'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; diff --git a/src/scenes/Partners/Edit/EditPartner.tsx b/src/scenes/Partners/Edit/EditPartner.tsx index d70c26b4c7..1b5019919c 100644 --- a/src/scenes/Partners/Edit/EditPartner.tsx +++ b/src/scenes/Partners/Edit/EditPartner.tsx @@ -1,4 +1,5 @@ import { useMutation } from '@apollo/client'; +import { Many, many } from '@seedcompany/common'; import { Decorator } from 'final-form'; import onFieldChange from 'final-form-calculate'; import { ComponentType, useMemo } from 'react'; @@ -9,7 +10,7 @@ import { PartnerTypeList, UpdatePartner, } from '~/api/schema.graphql'; -import { ExtractStrict, labelFrom, Many, many } from '~/common'; +import { ExtractStrict, labelFrom } from '~/common'; import { DialogForm, DialogFormProps, diff --git a/src/scenes/Partners/List/PartnerList.tsx b/src/scenes/Partners/List/PartnerList.tsx index 9d657d6fe6..5293c18c72 100644 --- a/src/scenes/Partners/List/PartnerList.tsx +++ b/src/scenes/Partners/List/PartnerList.tsx @@ -10,7 +10,6 @@ import { import { useRef } from 'react'; import { Helmet } from 'react-helmet-async'; import { makeStyles } from 'tss-react/mui'; -import { simpleSwitch } from '~/common'; import { useNumberFormatter } from '../../../components/Formatters'; import { ContentContainer } from '../../../components/Layout'; import { List, useListQuery } from '../../../components/List'; @@ -48,11 +47,7 @@ export const PartnerList = () => { variables: { input: { ...sort.value, - filter: { - ...simpleSwitch(filters.tab, { - pinned: { pinned: true }, - }), - }, + filter: filters.tab === 'pinned' ? { pinned: true } : {}, }, }, }); diff --git a/src/scenes/Partnerships/InvalidateBudget/invalidateBudgetRecords.ts b/src/scenes/Partnerships/InvalidateBudget/invalidateBudgetRecords.ts index 05124dd003..d277204d25 100644 --- a/src/scenes/Partnerships/InvalidateBudget/invalidateBudgetRecords.ts +++ b/src/scenes/Partnerships/InvalidateBudget/invalidateBudgetRecords.ts @@ -1,5 +1,4 @@ import { ApolloCache, MutationUpdaterFunction } from '@apollo/client'; -import { isFunction } from 'lodash'; import { DateTime, Interval } from 'luxon'; import { invalidateProps } from '~/api'; import { Project as ProjectShape } from '~/api/schema.graphql'; @@ -23,16 +22,18 @@ export const invalidateBudgetRecords = updatedOrFn: Partnership | ((res: R) => Partnership) ): MutationUpdaterFunction> => (cache: ApolloCache, res) => { - const previous: Partnership = isFunction(previousOrFn) - ? res.data - ? previousOrFn(res.data) - : undefined - : previousOrFn; - const updated: Partnership = isFunction(updatedOrFn) - ? res.data - ? updatedOrFn(res.data) - : undefined - : updatedOrFn; + const previous: Partnership = + typeof previousOrFn === 'function' + ? res.data + ? previousOrFn(res.data) + : undefined + : previousOrFn; + const updated: Partnership = + typeof updatedOrFn === 'function' + ? res.data + ? updatedOrFn(res.data) + : undefined + : updatedOrFn; const change = determineChange(previous, updated); if (change == null) { diff --git a/src/scenes/Products/Create/CreateProduct.tsx b/src/scenes/Products/Create/CreateProduct.tsx index f81aa0b7b5..b95148ca26 100644 --- a/src/scenes/Products/Create/CreateProduct.tsx +++ b/src/scenes/Products/Create/CreateProduct.tsx @@ -1,11 +1,12 @@ import { useMutation, useQuery } from '@apollo/client'; import { Breadcrumbs, Skeleton, Typography } from '@mui/material'; +import { entries, mapEntries } from '@seedcompany/common'; import { useMemo } from 'react'; import { Helmet } from 'react-helmet-async'; import { useNavigate } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; import { addItemToList, handleFormError } from '~/api'; -import { callAll, entries, getFullBookRange, mapFromList } from '~/common'; +import { callAll, getFullBookRange } from '~/common'; import { useChangesetAwareIdFromUrl } from '../../../components/Changeset'; import { EngagementBreadcrumb } from '../../../components/EngagementBreadcrumb'; import { ProjectBreadcrumb } from '../../../components/ProjectBreadcrumb'; @@ -85,10 +86,10 @@ export const CreateProduct = () => { product: { title: '', bookSelection: 'full', - producingMediums: mapFromList( + producingMediums: mapEntries( engagement?.partnershipsProducingMediums.items ?? [], (pair) => [pair.medium, pair.partnership ?? undefined] - ), + ).asRecord, }, }; return values; diff --git a/src/scenes/Products/Detail/ProductInfo.tsx b/src/scenes/Products/Detail/ProductInfo.tsx index eec8b4e578..8cd4ee10ae 100644 --- a/src/scenes/Products/Detail/ProductInfo.tsx +++ b/src/scenes/Products/Detail/ProductInfo.tsx @@ -6,10 +6,11 @@ import { Skeleton, Typography, } from '@mui/material'; +import { mapEntries } from '@seedcompany/common'; import { ReactNode } from 'react'; import { makeStyles } from 'tss-react/mui'; import { ProductMediumLabels, ProductStepLabels } from '~/api/schema.graphql'; -import { displayMethodologyWithLabel, mapFromList } from '~/common'; +import { displayMethodologyWithLabel } from '~/common'; import { DisplaySimpleProperty, DisplaySimplePropertyProps, @@ -28,10 +29,10 @@ const useStyles = makeStyles()(() => ({ export const ProductInfo = ({ product }: { product?: Product }) => { const { classes } = useStyles(); - const ppm = mapFromList( + const ppm = mapEntries( product?.engagement.partnershipsProducingMediums.items ?? [], (pair) => [pair.medium, pair.partnership] - ); + ).asRecord; return ( <> {product?.__typename === 'OtherProduct' && ( diff --git a/src/scenes/Products/Detail/Progress/StepEditDialog.tsx b/src/scenes/Products/Detail/Progress/StepEditDialog.tsx index 1c164bc767..8f7fd23d12 100644 --- a/src/scenes/Products/Detail/Progress/StepEditDialog.tsx +++ b/src/scenes/Products/Detail/Progress/StepEditDialog.tsx @@ -1,6 +1,5 @@ import { useMutation } from '@apollo/client'; import { Alert } from '@mui/material'; -import { isBoolean } from 'lodash'; import { useMemo } from 'react'; import { Except } from 'type-fest'; import { ProductStepLabels, ProgressMeasurement } from '~/api/schema.graphql'; @@ -58,9 +57,10 @@ export const StepEditDialog = ({ {...props} initialValues={initialValues} onSubmit={async (data) => { - const completed = isBoolean(data.completed) - ? +data.completed - : data.completed; + const completed = + typeof data.completed === 'boolean' + ? +data.completed + : data.completed; await update({ variables: { input: { diff --git a/src/scenes/Products/Edit/EditProduct.tsx b/src/scenes/Products/Edit/EditProduct.tsx index 61c3a2d341..9f6b8a3f26 100644 --- a/src/scenes/Products/Edit/EditProduct.tsx +++ b/src/scenes/Products/Edit/EditProduct.tsx @@ -1,15 +1,14 @@ import { useMutation, useQuery } from '@apollo/client'; import { Breadcrumbs, Skeleton, Typography } from '@mui/material'; +import { entries, mapEntries } from '@seedcompany/common'; import { useMemo } from 'react'; import { Helmet } from 'react-helmet-async'; import { useNavigate } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; import { callAll, - entries, getFullBookRange, isFullBookRange, - mapFromList, removeScriptureTypename, } from '~/common'; import { handleFormError, removeItemFromList } from '../../../api'; @@ -158,10 +157,10 @@ export const EditProduct = () => { description: product.description.value || '', } : undefined), - producingMediums: mapFromList( + producingMediums: mapEntries( engagement?.partnershipsProducingMediums.items ?? [], (pair) => [pair.medium, pair.partnership ?? undefined] - ), + ).asRecord, }, }; return values; diff --git a/src/scenes/Products/Edit/updateProgressSteps.ts b/src/scenes/Products/Edit/updateProgressSteps.ts index 2cf0604056..987c8d0aac 100644 --- a/src/scenes/Products/Edit/updateProgressSteps.ts +++ b/src/scenes/Products/Edit/updateProgressSteps.ts @@ -1,8 +1,9 @@ import { ApolloCache, MutationUpdaterFunction } from '@apollo/client'; -import { difference, sortBy, uniqBy } from 'lodash'; +import { isNotNil, sortBy } from '@seedcompany/common'; +import { difference, uniqBy } from 'lodash'; import { readFragment } from '~/api'; import { StepProgress } from '~/api/schema.graphql'; -import { IdFragment, notNullish } from '~/common'; +import { IdFragment } from '~/common'; import { ProductFormFragment } from '../ProductForm/ProductForm.graphql'; import { modifyProgressRelatingToEngagement, @@ -58,7 +59,7 @@ export const updateProgressSteps = ? [current.progressOfCurrentReportDue] : []), ...(current?.progressReports ?? []), - ].filter(notNullish), + ].filter(isNotNil), (pp) => pp.report?.id ); diff --git a/src/scenes/Products/ProductForm/MethodologySection.tsx b/src/scenes/Products/ProductForm/MethodologySection.tsx index def19e1e9a..beea65163b 100644 --- a/src/scenes/Products/ProductForm/MethodologySection.tsx +++ b/src/scenes/Products/ProductForm/MethodologySection.tsx @@ -1,10 +1,10 @@ import { ToggleButton, Typography } from '@mui/material'; +import { entries } from '@seedcompany/common'; import { ProductApproachLabels } from '~/api/schema.graphql'; import { ApproachMethodologies, displayMethodology, displayMethodologyWithLabel, - entries, } from '~/common'; import { EnumField, EnumOption } from '../../../components/form'; import { useStyles } from './DefaultAccordion'; diff --git a/src/scenes/Products/ProductForm/ScriptureReferencesSection.tsx b/src/scenes/Products/ProductForm/ScriptureReferencesSection.tsx index add83f54c8..9341508169 100644 --- a/src/scenes/Products/ProductForm/ScriptureReferencesSection.tsx +++ b/src/scenes/Products/ProductForm/ScriptureReferencesSection.tsx @@ -1,12 +1,10 @@ import { ToggleButton } from '@mui/material'; +import { entries, groupToMapBy, simpleSwitch } from '@seedcompany/common'; import { UnspecifiedScripturePortion } from '~/api/schema.graphql'; import { - entries, getScriptureRangeDisplay, getUnspecifiedScriptureDisplay, ScriptureRange, - scriptureRangeDictionary, - simpleSwitch, } from '~/common'; import { AutocompleteField, @@ -61,13 +59,13 @@ export const ScriptureReferencesSection = ({ ); } - return entries(scriptureRangeDictionary(scriptureReferences)).map( - ([book, scriptureRangeArr]) => ( - - {getScriptureRangeDisplay(scriptureRangeArr, book)} - - ) - ); + return entries( + groupToMapBy(scriptureReferences ?? [], (range) => range.start.book) + ).map(([book, scriptureRangeArr]) => ( + + {getScriptureRangeDisplay(scriptureRangeArr, book)} + + )); }} >
diff --git a/src/scenes/ProgressReports/Detail/ProductTable.tsx b/src/scenes/ProgressReports/Detail/ProductTable.tsx index bb2aa80304..9a9e89de5b 100644 --- a/src/scenes/ProgressReports/Detail/ProductTable.tsx +++ b/src/scenes/ProgressReports/Detail/ProductTable.tsx @@ -6,7 +6,8 @@ import { GridRenderCellParams, GridRenderEditCellParams, } from '@mui/x-data-grid'; -import { sortBy, uniq } from 'lodash'; +import { mapEntries, sortBy } from '@seedcompany/common'; +import { uniq } from 'lodash'; import { useMemo } from 'react'; import { LiteralUnion } from 'type-fest'; import { @@ -14,7 +15,7 @@ import { ProductStepLabels, SecuredFloatNullable, } from '~/api/schema.graphql'; -import { isSecured, mapFromList } from '../../../common'; +import { isSecured } from '../../../common'; import { bookIndexFromName } from '../../../common/biblejs'; import { EditNumberCell } from '../../../components/Grid/EditNumberCell'; import { Link } from '../../../components/Routing'; @@ -166,10 +167,8 @@ export const ProductTable = ({ data: progress, label: product.label ?? '', plannedSteps: new Set(progress.steps.map((s) => s.step)), - ...mapFromList(progress.steps, ({ step, completed }) => [ - step, - completed, - ]), + ...mapEntries(progress.steps, ({ step, completed }) => [step, completed]) + .asRecord, }; return row; }); diff --git a/src/scenes/ProgressReports/Detail/ProductTableList.tsx b/src/scenes/ProgressReports/Detail/ProductTableList.tsx index 61ec4ab39f..e8f3405b50 100644 --- a/src/scenes/ProgressReports/Detail/ProductTableList.tsx +++ b/src/scenes/ProgressReports/Detail/ProductTableList.tsx @@ -1,5 +1,5 @@ import { Skeleton, Typography } from '@mui/material'; -import { groupBy } from 'lodash'; +import { entries, groupToMapBy } from '@seedcompany/common'; import { ProductTable } from './ProductTable'; import { ProgressOfProductForReportFragment } from './ProgressReportDetail.graphql'; @@ -8,15 +8,18 @@ interface ProductTableListProps { } export const ProductTableList = ({ products }: ProductTableListProps) => { - const grouped = groupBy(products, (product) => product.product.category); + const grouped = groupToMapBy( + products ?? [], + (product) => product.product.category + ); return ( <> {products ? 'Progress for Goals' : } - {Object.entries(grouped).map(([category, products]) => ( - + {entries(grouped).map(([category, products]) => ( + ))} ); diff --git a/src/scenes/ProgressReports/EditForm/Steps/CommunityStory/CommunityStoryStep.tsx b/src/scenes/ProgressReports/EditForm/Steps/CommunityStory/CommunityStoryStep.tsx index ccddd5953a..250060385e 100644 --- a/src/scenes/ProgressReports/EditForm/Steps/CommunityStory/CommunityStoryStep.tsx +++ b/src/scenes/ProgressReports/EditForm/Steps/CommunityStory/CommunityStoryStep.tsx @@ -1,5 +1,5 @@ import { Box, Typography } from '@mui/material'; -import { simpleSwitch } from '~/common'; +import { simpleSwitch } from '@seedcompany/common'; import { Prompt, VariantResponses } from '../PromptVariant'; import { StepComponent } from '../step.types'; import { diff --git a/src/scenes/ProgressReports/EditForm/Steps/ExplanationOfProgress/ExplanationOfProgress.tsx b/src/scenes/ProgressReports/EditForm/Steps/ExplanationOfProgress/ExplanationOfProgress.tsx index 8e42753197..ed6b37fd7a 100644 --- a/src/scenes/ProgressReports/EditForm/Steps/ExplanationOfProgress/ExplanationOfProgress.tsx +++ b/src/scenes/ProgressReports/EditForm/Steps/ExplanationOfProgress/ExplanationOfProgress.tsx @@ -1,5 +1,6 @@ import { useMutation } from '@apollo/client'; import { Card, CardContent, Tooltip, Typography } from '@mui/material'; +import { IterableItem } from '@seedcompany/common'; import { Decorator } from 'final-form'; import onFieldChange from 'final-form-calculate'; import { camelCase } from 'lodash'; @@ -21,7 +22,7 @@ import { VarianceExplanation } from '../../../Detail/VarianceExplanation/Varianc import { StepComponent } from '../step.types'; import { ExplainProgressVarianceDocument } from './ExplanationOfProgress.graphql'; -type OptionGroup = typeof groups extends Array ? T : never; +type OptionGroup = IterableItem; const groups = ['behind', 'onTime', 'ahead'] satisfies Array< keyof ReasonOptions diff --git a/src/scenes/ProgressReports/EditForm/Steps/ProgressStep/ProgressStep.tsx b/src/scenes/ProgressReports/EditForm/Steps/ProgressStep/ProgressStep.tsx index 6180887e9e..d540d76b12 100644 --- a/src/scenes/ProgressReports/EditForm/Steps/ProgressStep/ProgressStep.tsx +++ b/src/scenes/ProgressReports/EditForm/Steps/ProgressStep/ProgressStep.tsx @@ -1,6 +1,6 @@ import { useMutation } from '@apollo/client'; import { Stack } from '@mui/material'; -import { groupBy, isEmpty, sortBy } from 'lodash'; +import { entries, groupToMapBy, mapEntries, sortBy } from '@seedcompany/common'; import { useMemo, useState } from 'react'; import { Error } from '../../../../../components/Error'; import { UpdateStepProgressDocument } from '../../../../Products/Detail/Progress/ProductProgress.graphql'; @@ -15,18 +15,16 @@ import { VariantSelector } from './VariantSelector'; export const ProgressStep: StepComponent = ({ report }) => { const progressByVariant = useMemo( () => - new Map( - report.progressForAllVariants.map((progress) => { - const { variant } = progress[0]!; - const progressByCategory = groupBy( - sortBy(progress, ({ product: { category } }) => - category === 'Scripture' ? '' : category - ), - (product) => product.product.category - ); - return [variant, progressByCategory]; - }) - ), + mapEntries(report.progressForAllVariants, (progress) => { + const { variant } = progress[0]!; + const progressByCategory = groupToMapBy( + sortBy(progress, ({ product: { category } }) => + category === 'Scripture' ? '' : category + ), + (product) => product.product.category + ); + return [variant, progressByCategory]; + }).asMap, [report] ); const variants = [...progressByVariant.keys()]; @@ -39,7 +37,7 @@ export const ProgressStep: StepComponent = ({ report }) => { ? progressByVariant.get(variant)! : undefined; - if (!variant || !progressByCategory || isEmpty(progressByCategory)) { + if (!variant || !progressByCategory || progressByCategory.size === 0) { return No progress available for this report.; } @@ -55,10 +53,10 @@ export const ProgressStep: StepComponent = ({ report }) => { - {Object.entries(progressByCategory).map(([category, progress]) => ( + {entries(progressByCategory).map(([category, progress]) => ( { ...simpleSwitch(filters.tab, { mine: { mine: true }, pinned: { pinned: true }, + all: {}, }), }, }, diff --git a/src/scenes/Projects/Overview/ProjectOverview.tsx b/src/scenes/Projects/Overview/ProjectOverview.tsx index d46163b8f1..baee90bf05 100644 --- a/src/scenes/Projects/Overview/ProjectOverview.tsx +++ b/src/scenes/Projects/Overview/ProjectOverview.tsx @@ -11,12 +11,13 @@ import { Timeline as TimelineIcon, } from '@mui/icons-material'; import { Grid, Skeleton, Tooltip, Typography } from '@mui/material'; +import { Many } from '@seedcompany/common'; import { useDropzone } from 'react-dropzone'; import { Helmet } from 'react-helmet-async'; import { makeStyles } from 'tss-react/mui'; import { PartialDeep } from 'type-fest'; import { ProjectStepLabels } from '~/api/schema.graphql'; -import { labelFrom, Many } from '~/common'; +import { labelFrom } from '~/common'; import { BudgetOverviewCard } from '../../../components/BudgetOverviewCard'; import { CardGroup } from '../../../components/CardGroup'; import { ChangesetPropertyBadge } from '../../../components/Changeset'; diff --git a/src/scenes/Projects/Reports/UpdatePeriodicReportDialog.tsx b/src/scenes/Projects/Reports/UpdatePeriodicReportDialog.tsx index 974ec66b34..38d88856f2 100644 --- a/src/scenes/Projects/Reports/UpdatePeriodicReportDialog.tsx +++ b/src/scenes/Projects/Reports/UpdatePeriodicReportDialog.tsx @@ -1,8 +1,9 @@ +import { many, Many } from '@seedcompany/common'; import { pick } from 'lodash'; import { useMemo } from 'react'; import { Except } from 'type-fest'; import { UpdatePeriodicReportInput } from '~/api/schema.graphql'; -import { ExtractStrict, many, Many } from '~/common'; +import { ExtractStrict } from '~/common'; import { DialogForm, DialogFormProps, diff --git a/src/scenes/Projects/Update/UpdateProjectDialog.tsx b/src/scenes/Projects/Update/UpdateProjectDialog.tsx index 4baa16ca46..739b79bfe4 100644 --- a/src/scenes/Projects/Update/UpdateProjectDialog.tsx +++ b/src/scenes/Projects/Update/UpdateProjectDialog.tsx @@ -1,4 +1,5 @@ import { useMutation } from '@apollo/client'; +import { many, Many } from '@seedcompany/common'; import { pick } from 'lodash'; import { ComponentType, useMemo } from 'react'; import { Except, Merge } from 'type-fest'; @@ -8,8 +9,6 @@ import { DisplayFieldRegionFragment, DisplayLocationFragment, ExtractStrict, - many, - Many, } from '~/common'; import { DialogForm, diff --git a/src/scenes/Users/List/UserList.tsx b/src/scenes/Users/List/UserList.tsx index 3c748478b3..5f9dafac35 100644 --- a/src/scenes/Users/List/UserList.tsx +++ b/src/scenes/Users/List/UserList.tsx @@ -11,7 +11,6 @@ import { useRef } from 'react'; import { Helmet } from 'react-helmet-async'; import { makeStyles } from 'tss-react/mui'; import { User } from '~/api/schema.graphql'; -import { simpleSwitch } from '~/common'; import { useNumberFormatter } from '../../../components/Formatters'; import { ContentContainer } from '../../../components/Layout'; import { List, useListQuery } from '../../../components/List'; @@ -49,11 +48,7 @@ export const UserList = () => { variables: { input: { ...sort.value, - filter: { - ...simpleSwitch(filters.tab, { - pinned: { pinned: true }, - }), - }, + filter: filters.tab === 'pinned' ? { pinned: true } : {}, }, }, }); diff --git a/yarn.lock b/yarn.lock index 357457b0d9..be5227de9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3843,6 +3843,18 @@ __metadata: languageName: node linkType: hard +"@seedcompany/common@npm:>=0.13 <1": + version: 0.13.1 + resolution: "@seedcompany/common@npm:0.13.1" + peerDependencies: + luxon: ^3.3.0 + peerDependenciesMeta: + luxon: + optional: true + checksum: 50f72ec4f0fe9c826a2ca16232c42053944aea34dd78587b85841f0253abeb5b9cdae6b15dca1f0fdc8c464bdba905e8a1d23e6c81d5c1b7f7fddd3144538529 + languageName: node + linkType: hard + "@seedcompany/eslint-plugin@npm:^3.4.1": version: 3.4.1 resolution: "@seedcompany/eslint-plugin@npm:3.4.1" @@ -7455,6 +7467,7 @@ __metadata: "@react-editor-js/client": "npm:^2.1.0" "@react-editor-js/core": "npm:^2.1.0" "@react-editor-js/server": "npm:^2.1.0" + "@seedcompany/common": "npm:>=0.13 <1" "@seedcompany/eslint-plugin": "npm:^3.4.1" "@testing-library/dom": "npm:^8.20.1" "@testing-library/jest-dom": "npm:^5.17.0"