feat: expanded folder collection filter support (#820)
This commit is contained in:
committed by
GitHub
parent
85db6b4f8d
commit
061febfd02
@ -39,6 +39,7 @@ import {
|
||||
selectInferredField,
|
||||
selectMediaFolders,
|
||||
} from './lib/util/collection.util';
|
||||
import filterEntries from './lib/util/filter.util';
|
||||
import {
|
||||
DRAFT_MEDIA_FILES,
|
||||
selectMediaFilePath,
|
||||
@ -1020,15 +1021,8 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
|
||||
return file.fields.map(f => f.name);
|
||||
}
|
||||
|
||||
filterEntries(collection: { entries: Entry[] }, filterRule: FilterRule) {
|
||||
return collection.entries.filter(entry => {
|
||||
const fieldValue = entry.data?.[filterRule.field];
|
||||
// TODO Investigate when the value could be a string array
|
||||
// if (Array.isArray(fieldValue)) {
|
||||
// return fieldValue.includes(filterRule.value);
|
||||
// }
|
||||
return fieldValue === filterRule.value;
|
||||
});
|
||||
filterEntries(collection: { entries: Entry[] }, filterRule: FilterRule | FilterRule[]) {
|
||||
return filterEntries(collection.entries, filterRule);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,11 +170,27 @@ export interface EntryDraft {
|
||||
fieldsErrors: FieldsErrors;
|
||||
}
|
||||
|
||||
export interface FilterRule {
|
||||
value: string;
|
||||
export interface BaseFieldFilterRule {
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface FieldPatternFilterRule extends BaseFieldFilterRule {
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
export interface FieldValueFilterRule extends BaseFieldFilterRule {
|
||||
value: string | string[];
|
||||
matchAll?: boolean;
|
||||
}
|
||||
|
||||
export type FieldFilterRule = FieldPatternFilterRule | FieldValueFilterRule;
|
||||
|
||||
export interface FileNameFilterRule {
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
export type FilterRule = FieldFilterRule | FileNameFilterRule;
|
||||
|
||||
export interface EditorConfig {
|
||||
preview?: boolean;
|
||||
frame?: boolean;
|
||||
@ -224,7 +240,7 @@ export interface BaseCollection {
|
||||
isFetching?: boolean;
|
||||
summary?: string;
|
||||
summary_fields?: string[];
|
||||
filter?: FilterRule;
|
||||
filter?: FilterRule | FilterRule[];
|
||||
label_singular?: string;
|
||||
label: string;
|
||||
sortable_fields?: SortableFields;
|
||||
|
200
packages/core/src/lib/util/__tests__/filter.util.spec.ts
Normal file
200
packages/core/src/lib/util/__tests__/filter.util.spec.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { createMockEntry } from '@staticcms/test/data/entry.mock';
|
||||
import filterEntries from '../filter.util';
|
||||
|
||||
import type { Entry } from '@staticcms/core/interface';
|
||||
|
||||
describe('filterEntries', () => {
|
||||
const mockEnglishEntry = createMockEntry({
|
||||
path: 'path/to/file-1.md',
|
||||
data: {
|
||||
language: 'en',
|
||||
tags: ['tag-1', 'tag-2', 'fish-catfish'],
|
||||
},
|
||||
});
|
||||
|
||||
const mockFrenchEntry = createMockEntry({
|
||||
path: 'path/to/file-2.md',
|
||||
data: {
|
||||
language: 'fr',
|
||||
tags: ['tag-1', 'tag-4'],
|
||||
},
|
||||
});
|
||||
|
||||
const mockIndexEntry = createMockEntry({
|
||||
path: 'path/to/index.md',
|
||||
data: {
|
||||
language: 'gr',
|
||||
tags: ['tag-1', 'tag-4'],
|
||||
},
|
||||
});
|
||||
|
||||
const mockUnderscoreIndexEntry = createMockEntry({
|
||||
path: 'path/to/_index.md',
|
||||
data: {
|
||||
language: 'gr',
|
||||
tags: ['tag-1', 'tag-3'],
|
||||
},
|
||||
});
|
||||
|
||||
const mockRandomFileNameEntry = createMockEntry({
|
||||
path: 'path/to/someOtherFile.md',
|
||||
data: {
|
||||
language: 'gr',
|
||||
tags: ['tag-3'],
|
||||
},
|
||||
});
|
||||
|
||||
const mockTags1and4Entry = createMockEntry({
|
||||
path: 'path/to/thatOtherPost.md',
|
||||
data: {
|
||||
language: 'gr',
|
||||
tags: ['tag-1', 'tag-4', 'fish-pike'],
|
||||
},
|
||||
});
|
||||
|
||||
const entries: Entry[] = [
|
||||
mockEnglishEntry,
|
||||
mockFrenchEntry,
|
||||
mockIndexEntry,
|
||||
mockUnderscoreIndexEntry,
|
||||
mockRandomFileNameEntry,
|
||||
mockTags1and4Entry,
|
||||
];
|
||||
|
||||
describe('field rules', () => {
|
||||
it('should filter fields', () => {
|
||||
expect(filterEntries(entries, { field: 'language', value: 'en' })).toEqual([
|
||||
mockEnglishEntry,
|
||||
]);
|
||||
|
||||
expect(filterEntries(entries, { field: 'language', value: 'fr' })).toEqual([mockFrenchEntry]);
|
||||
|
||||
expect(filterEntries(entries, { field: 'language', value: 'gr' })).toEqual([
|
||||
mockIndexEntry,
|
||||
mockUnderscoreIndexEntry,
|
||||
mockRandomFileNameEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter fields if multiple filter values are provided (must match only one)', () => {
|
||||
expect(filterEntries(entries, { field: 'language', value: ['en', 'fr'] })).toEqual([
|
||||
mockEnglishEntry,
|
||||
mockFrenchEntry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter fields based on pattern', () => {
|
||||
// Languages with an r in their name
|
||||
expect(filterEntries(entries, { field: 'language', pattern: 'r' })).toEqual([
|
||||
mockFrenchEntry,
|
||||
mockIndexEntry,
|
||||
mockUnderscoreIndexEntry,
|
||||
mockRandomFileNameEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter fields if field value is an array (must include value)', () => {
|
||||
expect(filterEntries(entries, { field: 'tags', value: 'tag-4' })).toEqual([
|
||||
mockFrenchEntry,
|
||||
mockIndexEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter fields if field value is an array and multiple filter values are provided (must include only one)', () => {
|
||||
expect(filterEntries(entries, { field: 'tags', value: ['tag-3', 'tag-4'] })).toEqual([
|
||||
mockFrenchEntry,
|
||||
mockIndexEntry,
|
||||
mockUnderscoreIndexEntry,
|
||||
mockRandomFileNameEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter fields if field value is an array and multiple filter values are provided (must include only one)', () => {
|
||||
expect(filterEntries(entries, { field: 'tags', value: ['tag-3', 'tag-4'] })).toEqual([
|
||||
mockFrenchEntry,
|
||||
mockIndexEntry,
|
||||
mockUnderscoreIndexEntry,
|
||||
mockRandomFileNameEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should match all values if matchAll is one (value is an array, multiple filter values are provided)', () => {
|
||||
expect(
|
||||
filterEntries(entries, { field: 'tags', value: ['tag-1', 'tag-4'], matchAll: true }),
|
||||
).toEqual([mockFrenchEntry, mockIndexEntry, mockTags1and4Entry]);
|
||||
});
|
||||
|
||||
it('should filter fields based on pattern when value is an array', () => {
|
||||
// Tags containing the word "fish"
|
||||
expect(filterEntries(entries, { field: 'tags', pattern: 'fish' })).toEqual([
|
||||
mockEnglishEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter based on multiple rules (must match all rules)', () => {
|
||||
expect(
|
||||
filterEntries(entries, [
|
||||
{ field: 'tags', value: ['tag-3', 'tag-4'] },
|
||||
{ field: 'language', value: 'gr' },
|
||||
]),
|
||||
).toEqual([
|
||||
mockIndexEntry,
|
||||
mockUnderscoreIndexEntry,
|
||||
mockRandomFileNameEntry,
|
||||
mockTags1and4Entry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter based on multiple rules (must match all rules) (matchAll on)', () => {
|
||||
expect(
|
||||
filterEntries(entries, [
|
||||
{ field: 'tags', value: ['tag-1', 'tag-4'], matchAll: true },
|
||||
{ field: 'language', value: 'gr' },
|
||||
]),
|
||||
).toEqual([mockIndexEntry, mockTags1and4Entry]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file rule', () => {
|
||||
it('should filter based on file name', () => {
|
||||
expect(filterEntries(entries, { pattern: '^index.md$' })).toEqual([mockIndexEntry]);
|
||||
|
||||
expect(filterEntries(entries, { pattern: '^_index.md$' })).toEqual([
|
||||
mockUnderscoreIndexEntry,
|
||||
]);
|
||||
|
||||
expect(filterEntries(entries, { pattern: 'index.md$' })).toEqual([
|
||||
mockIndexEntry,
|
||||
mockUnderscoreIndexEntry,
|
||||
]);
|
||||
|
||||
// File names containing the word file (case insensitive)
|
||||
expect(filterEntries(entries, { pattern: '[fF][iI][lL][eE]' })).toEqual([
|
||||
mockEnglishEntry,
|
||||
mockFrenchEntry,
|
||||
mockRandomFileNameEntry,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter based on multiple rules (must match all rules)', () => {
|
||||
// File names containing the word file (case insensitive)
|
||||
expect(
|
||||
filterEntries(entries, [{ pattern: '[fF][iI][lL][eE]' }, { pattern: 'some' }]),
|
||||
).toEqual([mockRandomFileNameEntry]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combined field and file rule', () => {
|
||||
it('should filter based on multiple rules (must match all rules)', () => {
|
||||
expect(
|
||||
filterEntries(entries, [{ pattern: 'index.md$' }, { field: 'tags', value: 'tag-3' }]),
|
||||
).toEqual([mockUnderscoreIndexEntry]);
|
||||
});
|
||||
});
|
||||
});
|
54
packages/core/src/lib/util/filter.util.ts
Normal file
54
packages/core/src/lib/util/filter.util.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { parse } from 'path';
|
||||
|
||||
import type { Entry, FieldFilterRule, FilterRule } from '@staticcms/core/interface';
|
||||
|
||||
function entryMatchesFieldRule(entry: Entry, filterRule: FieldFilterRule): boolean {
|
||||
const fieldValue = entry.data?.[filterRule.field];
|
||||
if ('pattern' in filterRule) {
|
||||
if (Array.isArray(fieldValue)) {
|
||||
return Boolean(fieldValue.find(v => new RegExp(filterRule.pattern).test(String(v))));
|
||||
}
|
||||
|
||||
return new RegExp(filterRule.pattern).test(String(fieldValue));
|
||||
}
|
||||
|
||||
if (Array.isArray(fieldValue)) {
|
||||
if (Array.isArray(filterRule.value)) {
|
||||
if (filterRule.matchAll) {
|
||||
return Boolean(filterRule.value.every(ruleValue => fieldValue.includes(ruleValue)));
|
||||
}
|
||||
|
||||
return Boolean(fieldValue.find(v => filterRule.value.includes(String(v))));
|
||||
}
|
||||
|
||||
return fieldValue.includes(filterRule.value);
|
||||
}
|
||||
|
||||
if (Array.isArray(filterRule.value)) {
|
||||
if (filterRule.matchAll) {
|
||||
return Boolean(filterRule.value.every(ruleValue => fieldValue === ruleValue));
|
||||
}
|
||||
|
||||
return filterRule.value.includes(String(fieldValue));
|
||||
}
|
||||
|
||||
return fieldValue === filterRule.value;
|
||||
}
|
||||
|
||||
function entryMatchesRule(entry: Entry, filterRule: FilterRule) {
|
||||
if ('field' in filterRule) {
|
||||
return entryMatchesFieldRule(entry, filterRule);
|
||||
}
|
||||
|
||||
return new RegExp(filterRule.pattern).test(parse(entry.path).base);
|
||||
}
|
||||
|
||||
export default function filterEntries(entries: Entry[], filterRule: FilterRule | FilterRule[]) {
|
||||
return entries.filter(entry => {
|
||||
if (Array.isArray(filterRule)) {
|
||||
return filterRule.every(r => entryMatchesRule(entry, r));
|
||||
}
|
||||
|
||||
return entryMatchesRule(entry, filterRule);
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user