feat: expanded folder collection filter support (#820)

This commit is contained in:
Daniel Lautzenheiser
2023-05-24 14:45:22 -04:00
committed by GitHub
parent 85db6b4f8d
commit 061febfd02
6 changed files with 524 additions and 48 deletions

View File

@ -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);
}
}

View File

@ -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;

View 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]);
});
});
});

View 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);
});
}