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

Commit d39ed3c

Browse files
authored
feat: add rich summary for relation fields when used as summary field (#992)
1 parent edc1665 commit d39ed3c

File tree

8 files changed

+353
-121
lines changed

8 files changed

+353
-121
lines changed

packages/core/src/backend.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ import type { AsyncLock } from './lib/util';
9191
import type { RootState } from './store';
9292
import type AssetProxy from './valueObjects/AssetProxy';
9393

94+
const LIST_ALL_ENTRIES_CACHE_TIME = 5000;
95+
9496
function updatePath(entryPath: string, assetPath: string): string | null {
9597
const pathDir = dirname(entryPath);
9698

@@ -592,15 +594,13 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
592594
};
593595
}
594596

595-
// The same as listEntries, except that if a cursor with the "next"
596-
// action available is returned, it calls "next" on the cursor and
597-
// repeats the process. Once there is no available "next" action, it
598-
// returns all the collected entries. Used to retrieve all entries
599-
// for local searches and queries.
600-
async listAllEntries<EF extends BaseField>(
597+
backendPromise: Record<string, { expires: number; data?: Entry[]; promise?: Promise<Entry[]> }> =
598+
{};
599+
600+
async listAllEntriesExecutor<EF extends BaseField>(
601601
collection: CollectionWithDefaults<EF>,
602602
config: ConfigWithDefaults<EF>,
603-
) {
603+
): Promise<Entry[]> {
604604
if ('folder' in collection && collection.folder && this.implementation.allEntriesByFolder) {
605605
const depth = collectionDepth(collection);
606606
const extension = selectFolderEntryExtension(collection);
@@ -632,6 +632,50 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
632632
return entries;
633633
}
634634

635+
// The same as listEntries, except that if a cursor with the "next"
636+
// action available is returned, it calls "next" on the cursor and
637+
// repeats the process. Once there is no available "next" action, it
638+
// returns all the collected entries. Used to retrieve all entries
639+
// for local searches and queries.
640+
async listAllEntries<EF extends BaseField>(
641+
collection: CollectionWithDefaults<EF>,
642+
config: ConfigWithDefaults<EF>,
643+
): Promise<Entry[]> {
644+
const now = new Date().getTime();
645+
if (collection.name in this.backendPromise) {
646+
const cachedRequest = this.backendPromise[collection.name];
647+
if (cachedRequest && cachedRequest.expires >= now) {
648+
if (cachedRequest.data) {
649+
return Promise.resolve(cachedRequest.data);
650+
}
651+
652+
if (cachedRequest.promise) {
653+
return cachedRequest.promise;
654+
}
655+
}
656+
657+
delete this.backendPromise[collection.name];
658+
}
659+
660+
const p = new Promise<Entry[]>(resolve => {
661+
this.listAllEntriesExecutor(collection, config).then(entries => {
662+
const responseNow = new Date().getTime();
663+
this.backendPromise[collection.name] = {
664+
expires: responseNow + LIST_ALL_ENTRIES_CACHE_TIME,
665+
data: entries,
666+
};
667+
resolve(entries);
668+
});
669+
});
670+
671+
this.backendPromise[collection.name] = {
672+
expires: now + LIST_ALL_ENTRIES_CACHE_TIME,
673+
promise: p,
674+
};
675+
676+
return p;
677+
}
678+
635679
printError(error: Error) {
636680
return `\n\n${error.stack}`;
637681
}

packages/core/src/components/collections/entries/Entries.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
55
import entriesClasses from './Entries.classes';
66
import EntryListing from './EntryListing';
77

8-
import type { ViewStyle } from '@staticcms/core/constants/views';
98
import type { CollectionWithDefaults, CollectionsWithDefaults, Entry } from '@staticcms/core';
9+
import type { ViewStyle } from '@staticcms/core/constants/views';
1010
import type Cursor from '@staticcms/core/lib/util/Cursor';
1111
import type { FC } from 'react';
1212

packages/core/src/components/collections/entries/EntryRow.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import TableCell from '../../common/table/TableCell';
1717
import TableRow from '../../common/table/TableRow';
1818
import WorkflowStatusPill from '../../workflow/WorkflowStatusPill';
1919
import entriesClasses from './Entries.classes';
20+
import RelationSummary from '@staticcms/relation/RelationSummary';
21+
import { getI18nInfo } from '@staticcms/core/lib/i18n';
2022

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

45+
const { default_locale } = useMemo(() => getI18nInfo(collection), [collection]) ?? {};
46+
4347
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
4448

4549
const fields = useMemo(() => getFields(collection, entry.slug), [collection, entry.slug]);
@@ -107,6 +111,8 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
107111
<FieldPreviewComponent collection={collection} field={field} value={value} />
108112
) : isNullish(value) ? (
109113
''
114+
) : field?.widget === 'relation' ? (
115+
<RelationSummary field={field} value={value} locale={default_locale} entry={entry} />
110116
) : (
111117
String(value)
112118
)}

packages/core/src/widgets/relation/RelationControl.tsx

Lines changed: 9 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as fuzzy from 'fuzzy';
2-
import get from 'lodash/get';
32
import uniqBy from 'lodash/uniqBy';
43
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
54

@@ -15,29 +14,22 @@ import Pill from '@staticcms/core/components/common/pill/Pill';
1514
import CircularProgress from '@staticcms/core/components/common/progress/CircularProgress';
1615
import classNames from '@staticcms/core/lib/util/classNames.util';
1716
import { getFields } from '@staticcms/core/lib/util/collection.util';
18-
import { isNullish } from '@staticcms/core/lib/util/null.util';
1917
import { fileSearch, sortByScore } from '@staticcms/core/lib/util/search.util';
20-
import { isEmpty } from '@staticcms/core/lib/util/string.util';
2118
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
22-
import {
23-
addFileTemplateFields,
24-
compileStringTemplate,
25-
expandPath,
26-
extractTemplateVars,
27-
} from '@staticcms/core/lib/widgets/stringTemplate';
2819
import { selectCollection } from '@staticcms/core/reducers/selectors/collections';
2920
import { useAppSelector } from '@staticcms/core/store/hooks';
21+
import { getSelectedValue, parseHitOptions } from './util';
3022

3123
import type {
3224
ConfigWithDefaults,
3325
Entry,
34-
EntryData,
3526
ObjectValue,
3627
RelationField,
3728
WidgetControlProps,
3829
} from '@staticcms/core';
3930
import type { FC, ReactNode } from 'react';
4031
import type { ListChildComponentProps } from 'react-window';
32+
import type { HitOption } from './types';
4133

4234
import './RelationControl.css';
4335

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

58-
export interface HitOption {
59-
data: EntryData;
60-
value: string;
61-
label: string;
62-
}
63-
6450
export interface Option {
6551
value: string;
6652
label: string;
6753
}
6854

69-
function getSelectedOptions(value: HitOption[] | undefined | null): HitOption[] | null;
70-
function getSelectedOptions(value: string[] | undefined | null): string[] | null;
71-
function getSelectedOptions(value: string[] | HitOption[] | undefined | null) {
72-
if (!value || !Array.isArray(value)) {
73-
return null;
74-
}
75-
76-
return value;
77-
}
78-
7955
function uniqOptions(initial: HitOption[], current: HitOption[]): HitOption[] {
8056
return uniqBy(initial.concat(current), o => o.value);
8157
}
8258

83-
function getSelectedValue(value: string, options: HitOption[], isMultiple: boolean): string | null;
84-
function getSelectedValue(
85-
value: string[],
86-
options: HitOption[],
87-
isMultiple: boolean,
88-
): string[] | null;
89-
function getSelectedValue(
90-
value: string | string[] | null | undefined,
91-
options: HitOption[],
92-
isMultiple: boolean,
93-
): string | string[] | null;
94-
function getSelectedValue(
95-
value: string | string[] | null | undefined,
96-
options: HitOption[],
97-
isMultiple: boolean,
98-
): string | string[] | null {
99-
if (isMultiple && Array.isArray(value)) {
100-
const selectedOptions = getSelectedOptions(value);
101-
if (selectedOptions === null) {
102-
return null;
103-
}
104-
105-
const selected = selectedOptions
106-
.map(i => options.find(o => o.value === i))
107-
.filter(Boolean)
108-
.map(option => (typeof option === 'string' ? option : option?.value)) as string[];
109-
110-
return selected;
111-
} else {
112-
return options.find(option => option.value === value)?.value ?? null;
113-
}
114-
}
115-
11659
const DEFAULT_OPTIONS_LIMIT = 20;
11760

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

149-
const parseNestedFields = useCallback(
150-
(hit: Entry, field: string): string => {
151-
const hitData =
152-
locale != null && hit.i18n != null && hit.i18n[locale] != null
153-
? hit.i18n[locale].data
154-
: hit.data;
155-
156-
const templateVars = extractTemplateVars(field);
157-
// return non template fields as is
158-
if (templateVars.length <= 0) {
159-
return get(hitData, field) as string;
160-
}
161-
const data = addFileTemplateFields(hit.path, hitData);
162-
return compileStringTemplate(field, null, hit.slug, data, searchCollectionFields);
163-
},
164-
[locale, searchCollectionFields],
165-
);
166-
167-
const parseHitOptions = useCallback(
168-
(hits: Entry[]) => {
169-
const valueField = field.value_field;
170-
const displayField = field.display_fields || [field.value_field];
171-
172-
const options = hits.reduce((acc, hit) => {
173-
const valuesPaths = expandPath({ data: hit.data, path: valueField });
174-
for (let i = 0; i < valuesPaths.length; i++) {
175-
const value = parseNestedFields(hit, valuesPaths[i]) as string;
176-
177-
const label = displayField
178-
.map(key => {
179-
const displayPaths = expandPath({ data: hit.data, path: key });
180-
const path = displayPaths[i] ?? displayPaths[0];
181-
if (isNullish(path) || isEmpty(path)) {
182-
return value;
183-
}
184-
return parseNestedFields(hit, displayPaths[i] ?? displayPaths[0]);
185-
})
186-
.join(' ');
187-
188-
acc.push({ data: hit.data, value, label });
189-
}
190-
191-
return acc;
192-
}, [] as HitOption[]);
193-
194-
return options;
195-
},
196-
[field.display_fields, field.value_field, parseNestedFields],
197-
);
198-
19992
const [options, setOptions] = useState<HitOption[]>([]);
20093
const [entries, setEntries] = useState<Entry[] | null>(null);
20194
const loading = useMemo(() => !entries, [entries]);
@@ -232,15 +125,18 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
232125
);
233126
}
234127

235-
let options = uniqBy(parseHitOptions(hits), o => o.value);
128+
let options = uniqBy(
129+
parseHitOptions(hits, field, locale, searchCollectionFields),
130+
o => o.value,
131+
);
236132

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

241137
setOptions(options);
242138
},
243-
[entries, field.file, field.options_length, field.search_fields, parseHitOptions],
139+
[entries, field, locale, searchCollectionFields],
244140
);
245141

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

264-
const hitOptions = parseHitOptions(options);
160+
const hitOptions = parseHitOptions(options, field, locale, searchCollectionFields);
265161

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

299195
const uniqueOptions = useMemo(() => {
300196
let uOptions = uniqOptions(initialOptions, options);

0 commit comments

Comments
 (0)