Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown stability patch #5

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9e18ffb
1896 fetching other collections from preview (#6523)
cdennington Mar 20, 2024
2c847d5
chore(): change branch from master to main accross the codebase (#7154)
demshy Mar 21, 2024
875f4ca
chore(release): publish
demshy Mar 21, 2024
3f96112
chore(deps-dev): bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#7159)
dependabot[bot] Mar 27, 2024
85c92f0
feat: filtering for relation widget (#2405) (#7161)
JimmyOei Mar 28, 2024
4262b63
Fix typings for EditorComponentField (#7160)
mathiasandresen Mar 28, 2024
e67e067
Potential Fix for #7152 (Slate crashing) (#7153)
DavidWells Mar 28, 2024
13c0b4a
chore(release): publish
demshy Mar 28, 2024
8207143
chore(deps): bump express from 4.18.2 to 4.19.2 (#7163)
dependabot[bot] Mar 28, 2024
a07a85b
refactor: datetime to support custom date and time formats (#7091)
martinjagodic Mar 29, 2024
dd31fcf
Add <span> tag to (optional) suffix for customization (#7103)
leomp12 Mar 29, 2024
7c85595
chore(release): publish
demshy Mar 29, 2024
248f36f
fix: handle single values in multiselect relation widget (#7164)
demshy Apr 2, 2024
c15463f
chore(release): publish
demshy Apr 2, 2024
c91a70f
Fix: Set correct branch when it's not specified in the config (#5844)
bytrangle Apr 3, 2024
46294d6
fix(datetime-widget): revert default date format to include timezone …
demshy Apr 3, 2024
d3678b0
chore(release): publish
demshy Apr 3, 2024
0b4b1cb
fix: fix formatInputValue ignoring format (#7170)
demshy Apr 11, 2024
28b1d63
chore(release): publish
demshy Apr 11, 2024
0e6f93b
feat: nested fields and integer values for filter relation widget (#7…
JimmyOei Apr 16, 2024
9718772
fix: datetime parsing display value when format not provided (#7181)
demshy Apr 16, 2024
663fd39
chore(deps-dev): bump axios from 0.27.2 to 1.6.8 (#7174)
dependabot[bot] Apr 16, 2024
29423c7
chore(release): publish
demshy Apr 16, 2024
365e766
chore(deps): bump ejs from 3.1.9 to 3.1.10 (#7196)
dependabot[bot] May 3, 2024
cdd0899
chore(deps-dev): bump postcss from 8.4.31 to 8.4.32 (#7182)
dependabot[bot] May 3, 2024
7ca7fc3
chore: update readme (#7187)
woldtwerk Jun 14, 2024
6715701
chore(deps-dev): bump ws from 7.5.9 to 7.5.10 (#7236)
dependabot[bot] Jun 26, 2024
7feca52
fix: display custom logo when using a proxy server (#7235)
MichalRsa Jun 26, 2024
7483f19
fix: change AppHeaderButton style fixing #7208 (#7209)
camdenvaughan Jun 26, 2024
041e34e
fix(map-widget): ensure map renders correctly when expanding initiall…
afieif Jun 26, 2024
dc71d72
fix: change EditorToolbar position style (#7198)
camdenvaughan Jun 26, 2024
232b012
chore(release): publish
demshy Jun 27, 2024
3a0a701
fix: enforce empty paragraph when converting from markdown to slate
demshy Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: filtering for relation widget (#2405) (#7161)
* feat: filtering for relation widget (#2405)

* feat: filter relation widget (#2405)

---------

Co-authored-by: Anze Demsar <anze.demsar@p-m.si>
  • Loading branch information
JimmyOei and demshy authored Mar 28, 2024
commit 85c92f0b1a40a7d0e34efb5e9bc4955e53725aeb
2 changes: 1 addition & 1 deletion dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ collections: # A list of collections the CMS should be able to edit
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
- { name: 'gallery', widget: 'image', choose_url: true, media_library: {config: {multiple: true, max_files: 999}}}
- { name: 'post', widget: relation, collection: posts, multiple: true, search_fields: [ "title" ], display_fields: [ "title" ], value_field: "{{slug}}"}
- { name: 'post', widget: relation, collection: posts, multiple: true, search_fields: [ "title" ], display_fields: [ "title" ], value_field: "{{slug}}", filters: [ {field: "draft", values: [false]} ] }
- name: authors
label: Authors
label_singular: 'Author'
Expand Down
51 changes: 33 additions & 18 deletions packages/decap-cms-widget-relation/src/RelationControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,12 @@ function uniqOptions(initial, current) {
return uniqBy(initial.concat(current), o => o.value);
}

function getSearchFieldArray(searchFields) {
return List.isList(searchFields) ? searchFields.toJS() : [searchFields];
function getFieldArray(field) {
if (!field) {
return [];
}

return List.isList(field) ? field.toJS() : [field];
}

function getSelectedValue({ value, options, isMultiple }) {
Expand Down Expand Up @@ -238,7 +242,7 @@ export default class RelationControl extends React.Component {
const initialSearchValues = value && (this.isMultiple() ? getSelectedOptions(value) : [value]);
if (initialSearchValues && initialSearchValues.length > 0) {
const metadata = {};
const searchFieldsArray = getSearchFieldArray(field.get('search_fields'));
const searchFieldsArray = getFieldArray(field.get('search_fields'));
const { payload } = await query(forID, collection, searchFieldsArray, '', file);
const hits = payload.hits || [];
const options = this.parseHitOptions(hits);
Expand All @@ -249,12 +253,13 @@ export default class RelationControl extends React.Component {
return selectedOption;
})
.filter(Boolean);
const filteredValue = initialOptions.map(option => option.value);

this.mounted && this.setState({ initialOptions });

//set metadata
this.mounted &&
onChange(value, {
onChange(filteredValue.length === 1 ? filteredValue[0] : fromJS(filteredValue), {
[field.get('name')]: {
[field.get('collection')]: metadata,
},
Expand Down Expand Up @@ -337,18 +342,28 @@ export default class RelationControl extends React.Component {
const { field } = this.props;
const valueField = field.get('value_field');
const displayField = field.get('display_fields') || List([field.get('value_field')]);
const filters = getFieldArray(field.get('filters'));

const options = hits.reduce((acc, hit) => {
const valuesPaths = stringTemplate.expandPath({ data: hit.data, path: valueField });
for (let i = 0; i < valuesPaths.length; i++) {
const label = displayField
.toJS()
.map(key => {
const displayPaths = stringTemplate.expandPath({ data: hit.data, path: key });
return this.parseNestedFields(hit, displayPaths[i] || displayPaths[0]);
})
.join(' ');
const value = this.parseNestedFields(hit, valuesPaths[i]);
acc.push({ data: hit.data, value, label });
if (
filters.every(
filter =>
Object.prototype.hasOwnProperty.call(hit.data, filter.field) &&
filter.values.includes(hit.data[filter.field]),
)
) {
const valuesPaths = stringTemplate.expandPath({ data: hit.data, path: valueField });
for (let i = 0; i < valuesPaths.length; i++) {
const label = displayField
.toJS()
.map(key => {
const displayPaths = stringTemplate.expandPath({ data: hit.data, path: key });
return this.parseNestedFields(hit, displayPaths[i] || displayPaths[0]);
})
.join(' ');
const value = this.parseNestedFields(hit, valuesPaths[i]);
acc.push({ data: hit.data, value, label });
}
}

return acc;
Expand All @@ -361,13 +376,13 @@ export default class RelationControl extends React.Component {
const { field, query, forID } = this.props;
const collection = field.get('collection');
const optionsLength = field.get('options_length') || 20;
const searchFieldsArray = getSearchFieldArray(field.get('search_fields'));
const searchFieldsArray = getFieldArray(field.get('search_fields'));
const file = field.get('file');

query(forID, collection, searchFieldsArray, term, file, optionsLength).then(({ payload }) => {
query(forID, collection, searchFieldsArray, term, file).then(({ payload }) => {
const hits = payload.hits || [];
const options = this.parseHitOptions(hits);
const uniq = uniqOptions(this.state.initialOptions, options);
const uniq = uniqOptions(this.state.initialOptions, options).slice(0, optionsLength);
callback(uniq);
});
}, 500);
Expand Down
158 changes: 152 additions & 6 deletions packages/decap-cms-widget-relation/src/__tests__/relation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,73 @@ const nestedFieldConfig = {
value_field: 'title',
};

const filterBooleanFieldConfig = {
name: 'post',
collection: 'posts',
display_fields: ['title', 'slug'],
search_fields: ['title', 'body'],
value_field: 'title',
filters: [
{
field: 'draft',
values: [false],
},
],
};

const filterStringFieldConfig = {
name: 'post',
collection: 'posts',
display_fields: ['title', 'slug'],
search_fields: ['title', 'body'],
value_field: 'title',
filters: [
{
field: 'title',
values: ['Post # 1', 'Post # 2', 'Post # 7', 'Post # 9', 'Post # 15'],
},
],
};

const multipleFiltersFieldConfig = {
name: 'post',
collection: 'posts',
display_fields: ['title', 'slug'],
search_fields: ['title', 'body'],
value_field: 'title',
filters: [
{
field: 'title',
values: ['Post # 1', 'Post # 2', 'Post # 7', 'Post # 9', 'Post # 15'],
},
{
field: 'draft',
values: [true],
},
],
};

const emptyFilterFieldConfig = {
name: 'post',
collection: 'posts',
display_fields: ['title', 'slug'],
search_fields: ['title', 'body'],
value_field: 'title',
filters: [
{
field: 'draft',
values: [],
},
],
};

function generateHits(length) {
const hits = Array.from({ length }, (val, idx) => {
const title = `Post # ${idx + 1}`;
const slug = `post-number-${idx + 1}`;
const draft = idx % 2 === 0;
const path = `posts/${slug}.md`;
return { collection: 'posts', data: { title, slug }, slug, path };
return { collection: 'posts', data: { title, slug, draft }, slug, path };
});

return [
Expand Down Expand Up @@ -277,7 +338,7 @@ describe('Relation widget', () => {
const value = 'Post # 1';
const label = 'Post # 1 post-number-1';
const metadata = {
post: { posts: { 'Post # 1': { title: 'Post # 1', slug: 'post-number-1' } } },
post: { posts: { 'Post # 1': { title: 'Post # 1', draft: true, slug: 'post-number-1' } } },
};

fireEvent.keyDown(input, { key: 'ArrowDown' });
Expand All @@ -295,7 +356,7 @@ describe('Relation widget', () => {
const { getByText, onChangeSpy, setQueryHitsSpy } = setup({ field, value });
const label = 'Post # 1 post-number-1';
const metadata = {
post: { posts: { 'Post # 1': { title: 'Post # 1', slug: 'post-number-1' } } },
post: { posts: { 'Post # 1': { title: 'Post # 1', draft: true, slug: 'post-number-1' } } },
};

setQueryHitsSpy(generateHits(1));
Expand Down Expand Up @@ -343,7 +404,9 @@ describe('Relation widget', () => {
const value = 'post-number-1';
const label = 'post-number-1 post-number-1 md';
const metadata = {
post: { posts: { 'post-number-1': { title: 'Post # 1', slug: 'post-number-1' } } },
post: {
posts: { 'post-number-1': { title: 'Post # 1', draft: true, slug: 'post-number-1' } },
},
};

fireEvent.keyDown(input, { key: 'ArrowDown' });
Expand Down Expand Up @@ -399,10 +462,10 @@ describe('Relation widget', () => {
const field = fromJS({ ...fieldConfig, multiple: true });
const { getByText, input, onChangeSpy } = setup({ field });
const metadata1 = {
post: { posts: { 'Post # 1': { title: 'Post # 1', slug: 'post-number-1' } } },
post: { posts: { 'Post # 1': { title: 'Post # 1', draft: true, slug: 'post-number-1' } } },
};
const metadata2 = {
post: { posts: { 'Post # 2': { title: 'Post # 2', slug: 'post-number-2' } } },
post: { posts: { 'Post # 2': { title: 'Post # 2', draft: false, slug: 'post-number-2' } } },
};

fireEvent.keyDown(input, { key: 'ArrowDown' });
Expand Down Expand Up @@ -481,4 +544,87 @@ describe('Relation widget', () => {
});
});
});

describe('with filter', () => {
it('should list the 10 option hits on initial load using a filter on boolean value', async () => {
const field = fromJS(filterBooleanFieldConfig);
const { getAllByText, input } = setup({ field });
const expectedOptions = [];
for (let i = 2; i <= 25; i += 2) {
expectedOptions.push(`Post # ${i} post-number-${i}`);
}
fireEvent.keyDown(input, { key: 'ArrowDown' });

await waitFor(() => {
const displayedOptions = getAllByText(/^Post # (\d{1,2}) post-number-\1$/);
expect(displayedOptions).toHaveLength(expectedOptions.length);
for (let i = 0; i < expectedOptions.length; i++) {
const expectedOption = expectedOptions[i];
const optionFound = displayedOptions.some(
option => option.textContent === expectedOption,
);
expect(optionFound).toBe(true);
}
});
});

it('should list the 5 option hits on initial load using a filter on string value', async () => {
const field = fromJS(filterStringFieldConfig);
const { getAllByText, input } = setup({ field });
const expectedOptions = [
'Post # 1 post-number-1',
'Post # 2 post-number-2',
'Post # 7 post-number-7',
'Post # 9 post-number-9',
'Post # 15 post-number-15',
];
fireEvent.keyDown(input, { key: 'ArrowDown' });

await waitFor(() => {
const displayedOptions = getAllByText(/^Post # (\d{1,2}) post-number-\1$/);
expect(displayedOptions).toHaveLength(expectedOptions.length);
for (let i = 0; i < expectedOptions.length; i++) {
const expectedOption = expectedOptions[i];
const optionFound = displayedOptions.some(
option => option.textContent === expectedOption,
);
expect(optionFound).toBe(true);
}
});
});

it('should list 4 option hits on initial load using multiple filters', async () => {
const field = fromJS(multipleFiltersFieldConfig);
const { getAllByText, input } = setup({ field });
const expectedOptions = [
'Post # 1 post-number-1',
'Post # 7 post-number-7',
'Post # 9 post-number-9',
'Post # 15 post-number-15',
];
fireEvent.keyDown(input, { key: 'ArrowDown' });

await waitFor(() => {
const displayedOptions = getAllByText(/^Post # (\d{1,2}) post-number-\1$/);
expect(displayedOptions).toHaveLength(expectedOptions.length);
for (let i = 0; i < expectedOptions.length; i++) {
const expectedOption = expectedOptions[i];
const optionFound = displayedOptions.some(
option => option.textContent === expectedOption,
);
expect(optionFound).toBe(true);
}
});
});

it('should list 0 option hits on initial load on empty filter values array', async () => {
const field = fromJS(emptyFilterFieldConfig);
const { getAllByText, input } = setup({ field });
fireEvent.keyDown(input, { key: 'ArrowDown' });

await waitFor(() => {
expect(() => getAllByText(/^Post # (\d{1,2}) post-number-\1$/)).toThrow(Error);
});
});
});
});
11 changes: 11 additions & 0 deletions packages/decap-cms-widget-relation/src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ export default {
max: { type: 'integer' },
display_fields: { type: 'array', minItems: 1, items: { type: 'string' } },
options_length: { type: 'integer' },
filters: {
type: 'array',
items: {
type: 'object',
properties: {
field: { type: 'string' },
values: { type: 'array', minItems: 1, items: { type: ['string', 'boolean'] } },
},
required: ['field', 'values'],
},
},
},
oneOf: [
{
Expand Down