Skip to content
This repository has been archived by the owner on Sep 9, 2024. It is now read-only.

Commit

Permalink
feat: add rich summary for relation fields when used as summary field (
Browse files Browse the repository at this point in the history
  • Loading branch information
KaneFreeman authored Nov 30, 2023
1 parent edc1665 commit d39ed3c
Show file tree
Hide file tree
Showing 8 changed files with 353 additions and 121 deletions.
58 changes: 51 additions & 7 deletions packages/core/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ import type { AsyncLock } from './lib/util';
import type { RootState } from './store';
import type AssetProxy from './valueObjects/AssetProxy';

const LIST_ALL_ENTRIES_CACHE_TIME = 5000;

function updatePath(entryPath: string, assetPath: string): string | null {
const pathDir = dirname(entryPath);

Expand Down Expand Up @@ -592,15 +594,13 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
};
}

// The same as listEntries, except that if a cursor with the "next"
// action available is returned, it calls "next" on the cursor and
// repeats the process. Once there is no available "next" action, it
// returns all the collected entries. Used to retrieve all entries
// for local searches and queries.
async listAllEntries<EF extends BaseField>(
backendPromise: Record<string, { expires: number; data?: Entry[]; promise?: Promise<Entry[]> }> =
{};

async listAllEntriesExecutor<EF extends BaseField>(
collection: CollectionWithDefaults<EF>,
config: ConfigWithDefaults<EF>,
) {
): Promise<Entry[]> {
if ('folder' in collection && collection.folder && this.implementation.allEntriesByFolder) {
const depth = collectionDepth(collection);
const extension = selectFolderEntryExtension(collection);
Expand Down Expand Up @@ -632,6 +632,50 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
return entries;
}

// The same as listEntries, except that if a cursor with the "next"
// action available is returned, it calls "next" on the cursor and
// repeats the process. Once there is no available "next" action, it
// returns all the collected entries. Used to retrieve all entries
// for local searches and queries.
async listAllEntries<EF extends BaseField>(
collection: CollectionWithDefaults<EF>,
config: ConfigWithDefaults<EF>,
): Promise<Entry[]> {
const now = new Date().getTime();
if (collection.name in this.backendPromise) {
const cachedRequest = this.backendPromise[collection.name];
if (cachedRequest && cachedRequest.expires >= now) {
if (cachedRequest.data) {
return Promise.resolve(cachedRequest.data);
}

if (cachedRequest.promise) {
return cachedRequest.promise;
}
}

delete this.backendPromise[collection.name];
}

const p = new Promise<Entry[]>(resolve => {
this.listAllEntriesExecutor(collection, config).then(entries => {
const responseNow = new Date().getTime();
this.backendPromise[collection.name] = {
expires: responseNow + LIST_ALL_ENTRIES_CACHE_TIME,
data: entries,
};
resolve(entries);
});
});

this.backendPromise[collection.name] = {
expires: now + LIST_ALL_ENTRIES_CACHE_TIME,
promise: p,
};

return p;
}

printError(error: Error) {
return `\n\n${error.stack}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
import entriesClasses from './Entries.classes';
import EntryListing from './EntryListing';

import type { ViewStyle } from '@staticcms/core/constants/views';
import type { CollectionWithDefaults, CollectionsWithDefaults, Entry } from '@staticcms/core';
import type { ViewStyle } from '@staticcms/core/constants/views';
import type Cursor from '@staticcms/core/lib/util/Cursor';
import type { FC } from 'react';

Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/components/collections/entries/EntryRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import TableCell from '../../common/table/TableCell';
import TableRow from '../../common/table/TableRow';
import WorkflowStatusPill from '../../workflow/WorkflowStatusPill';
import entriesClasses from './Entries.classes';
import RelationSummary from '@staticcms/relation/RelationSummary';
import { getI18nInfo } from '@staticcms/core/lib/i18n';

import type { BackupEntry, CollectionWithDefaults, Entry, TranslatedProps } from '@staticcms/core';
import type { FC } from 'react';
Expand All @@ -40,6 +42,8 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
[collection.name, entry.slug],
);

const { default_locale } = useMemo(() => getI18nInfo(collection), [collection]) ?? {};

const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);

const fields = useMemo(() => getFields(collection, entry.slug), [collection, entry.slug]);
Expand Down Expand Up @@ -107,6 +111,8 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
<FieldPreviewComponent collection={collection} field={field} value={value} />
) : isNullish(value) ? (
''
) : field?.widget === 'relation' ? (
<RelationSummary field={field} value={value} locale={default_locale} entry={entry} />
) : (
String(value)
)}
Expand Down
122 changes: 9 additions & 113 deletions packages/core/src/widgets/relation/RelationControl.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as fuzzy from 'fuzzy';
import get from 'lodash/get';
import uniqBy from 'lodash/uniqBy';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

Expand All @@ -15,29 +14,22 @@ import Pill from '@staticcms/core/components/common/pill/Pill';
import CircularProgress from '@staticcms/core/components/common/progress/CircularProgress';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { getFields } from '@staticcms/core/lib/util/collection.util';
import { isNullish } from '@staticcms/core/lib/util/null.util';
import { fileSearch, sortByScore } from '@staticcms/core/lib/util/search.util';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import {
addFileTemplateFields,
compileStringTemplate,
expandPath,
extractTemplateVars,
} from '@staticcms/core/lib/widgets/stringTemplate';
import { selectCollection } from '@staticcms/core/reducers/selectors/collections';
import { useAppSelector } from '@staticcms/core/store/hooks';
import { getSelectedValue, parseHitOptions } from './util';

import type {
ConfigWithDefaults,
Entry,
EntryData,
ObjectValue,
RelationField,
WidgetControlProps,
} from '@staticcms/core';
import type { FC, ReactNode } from 'react';
import type { ListChildComponentProps } from 'react-window';
import type { HitOption } from './types';

import './RelationControl.css';

Expand All @@ -55,64 +47,15 @@ function Option({ index, style, data }: ListChildComponentProps<{ options: React
return <div style={style}>{data.options[index]}</div>;
}

export interface HitOption {
data: EntryData;
value: string;
label: string;
}

export interface Option {
value: string;
label: string;
}

function getSelectedOptions(value: HitOption[] | undefined | null): HitOption[] | null;
function getSelectedOptions(value: string[] | undefined | null): string[] | null;
function getSelectedOptions(value: string[] | HitOption[] | undefined | null) {
if (!value || !Array.isArray(value)) {
return null;
}

return value;
}

function uniqOptions(initial: HitOption[], current: HitOption[]): HitOption[] {
return uniqBy(initial.concat(current), o => o.value);
}

function getSelectedValue(value: string, options: HitOption[], isMultiple: boolean): string | null;
function getSelectedValue(
value: string[],
options: HitOption[],
isMultiple: boolean,
): string[] | null;
function getSelectedValue(
value: string | string[] | null | undefined,
options: HitOption[],
isMultiple: boolean,
): string | string[] | null;
function getSelectedValue(
value: string | string[] | null | undefined,
options: HitOption[],
isMultiple: boolean,
): string | string[] | null {
if (isMultiple && Array.isArray(value)) {
const selectedOptions = getSelectedOptions(value);
if (selectedOptions === null) {
return null;
}

const selected = selectedOptions
.map(i => options.find(o => o.value === i))
.filter(Boolean)
.map(option => (typeof option === 'string' ? option : option?.value)) as string[];

return selected;
} else {
return options.find(option => option.value === value)?.value ?? null;
}
}

const DEFAULT_OPTIONS_LIMIT = 20;

const RelationControl: FC<WidgetControlProps<string | string[], RelationField>> = ({
Expand Down Expand Up @@ -146,56 +89,6 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
return field.multiple ?? false;
}, [field.multiple]);

const parseNestedFields = useCallback(
(hit: Entry, field: string): string => {
const hitData =
locale != null && hit.i18n != null && hit.i18n[locale] != null
? hit.i18n[locale].data
: hit.data;

const templateVars = extractTemplateVars(field);
// return non template fields as is
if (templateVars.length <= 0) {
return get(hitData, field) as string;
}
const data = addFileTemplateFields(hit.path, hitData);
return compileStringTemplate(field, null, hit.slug, data, searchCollectionFields);
},
[locale, searchCollectionFields],
);

const parseHitOptions = useCallback(
(hits: Entry[]) => {
const valueField = field.value_field;
const displayField = field.display_fields || [field.value_field];

const options = hits.reduce((acc, hit) => {
const valuesPaths = expandPath({ data: hit.data, path: valueField });
for (let i = 0; i < valuesPaths.length; i++) {
const value = parseNestedFields(hit, valuesPaths[i]) as string;

const label = displayField
.map(key => {
const displayPaths = expandPath({ data: hit.data, path: key });
const path = displayPaths[i] ?? displayPaths[0];
if (isNullish(path) || isEmpty(path)) {
return value;
}
return parseNestedFields(hit, displayPaths[i] ?? displayPaths[0]);
})
.join(' ');

acc.push({ data: hit.data, value, label });
}

return acc;
}, [] as HitOption[]);

return options;
},
[field.display_fields, field.value_field, parseNestedFields],
);

const [options, setOptions] = useState<HitOption[]>([]);
const [entries, setEntries] = useState<Entry[] | null>(null);
const loading = useMemo(() => !entries, [entries]);
Expand Down Expand Up @@ -232,15 +125,18 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
);
}

let options = uniqBy(parseHitOptions(hits), o => o.value);
let options = uniqBy(
parseHitOptions(hits, field, locale, searchCollectionFields),
o => o.value,
);

if (limit !== undefined && limit > 0) {
options = options.slice(0, limit);
}

setOptions(options);
},
[entries, field.file, field.options_length, field.search_fields, parseHitOptions],
[entries, field, locale, searchCollectionFields],
);

useEffect(() => {
Expand All @@ -261,7 +157,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
if (alive) {
setEntries(options);

const hitOptions = parseHitOptions(options);
const hitOptions = parseHitOptions(options, field, locale, searchCollectionFields);

if (value) {
const byValue = hitOptions.reduce(
Expand Down Expand Up @@ -294,7 +190,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
alive = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchCollection, config, loading, parseHitOptions]);
}, [searchCollection, config, loading, field, locale, searchCollectionFields]);

const uniqueOptions = useMemo(() => {
let uOptions = uniqOptions(initialOptions, options);
Expand Down
Loading

0 comments on commit d39ed3c

Please sign in to comment.