feat(widget-relation): string templates support (#3659)
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { resolveBackend, Backend } from '../backend';
|
||||
import { resolveBackend, Backend, extractSearchFields } from '../backend';
|
||||
import registry from 'Lib/registry';
|
||||
import { FOLDER } from 'Constants/collectionTypes';
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
@ -466,4 +466,181 @@ describe('Backend', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractSearchFields', () => {
|
||||
it('should extract slug', () => {
|
||||
expect(extractSearchFields(['slug'])({ slug: 'entry-slug', data: {} })).toEqual(
|
||||
' entry-slug',
|
||||
);
|
||||
});
|
||||
|
||||
it('should extract path', () => {
|
||||
expect(extractSearchFields(['path'])({ path: 'entry-path', data: {} })).toEqual(
|
||||
' entry-path',
|
||||
);
|
||||
});
|
||||
|
||||
it('should extract fields', () => {
|
||||
expect(
|
||||
extractSearchFields(['title', 'order'])({ data: { title: 'Entry Title', order: 5 } }),
|
||||
).toEqual(' Entry Title 5');
|
||||
});
|
||||
|
||||
it('should extract nested fields', () => {
|
||||
expect(
|
||||
extractSearchFields(['nested.title'])({ data: { nested: { title: 'nested title' } } }),
|
||||
).toEqual(' nested title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('search/query', () => {
|
||||
const collections = [
|
||||
fromJS({
|
||||
name: 'posts',
|
||||
folder: 'posts',
|
||||
fields: [
|
||||
{ name: 'title', widget: 'string' },
|
||||
{ name: 'short_title', widget: 'string' },
|
||||
{ name: 'author', widget: 'string' },
|
||||
{ name: 'description', widget: 'string' },
|
||||
{ name: 'nested', widget: 'object', fields: { name: 'title', widget: 'string' } },
|
||||
],
|
||||
}),
|
||||
fromJS({
|
||||
name: 'pages',
|
||||
folder: 'pages',
|
||||
fields: [
|
||||
{ name: 'title', widget: 'string' },
|
||||
{ name: 'short_title', widget: 'string' },
|
||||
{ name: 'author', widget: 'string' },
|
||||
{ name: 'description', widget: 'string' },
|
||||
{ name: 'nested', widget: 'object', fields: { name: 'title', widget: 'string' } },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const posts = [
|
||||
{
|
||||
path: 'posts/find-me.md',
|
||||
slug: 'find-me',
|
||||
data: {
|
||||
title: 'find me by title',
|
||||
short_title: 'find me by short title',
|
||||
author: 'find me by author',
|
||||
description: 'find me by description',
|
||||
nested: { title: 'find me by nested title' },
|
||||
},
|
||||
},
|
||||
{ path: 'posts/not-me.md', slug: 'not-me', data: { title: 'not me' } },
|
||||
];
|
||||
|
||||
const pages = [
|
||||
{
|
||||
path: 'pages/find-me.md',
|
||||
slug: 'find-me',
|
||||
data: {
|
||||
title: 'find me by title',
|
||||
short_title: 'find me by short title',
|
||||
author: 'find me by author',
|
||||
description: 'find me by description',
|
||||
nested: { title: 'find me by nested title' },
|
||||
},
|
||||
},
|
||||
{ path: 'pages/not-me.md', slug: 'not-me', data: { title: 'not me' } },
|
||||
];
|
||||
|
||||
const implementation = {
|
||||
init: jest.fn(() => implementation),
|
||||
};
|
||||
const config = Map({});
|
||||
|
||||
let backend;
|
||||
beforeEach(() => {
|
||||
backend = new Backend(implementation, { config, backendName: 'github' });
|
||||
backend.listAllEntries = jest.fn(collection => {
|
||||
if (collection.get('name') === 'posts') {
|
||||
return Promise.resolve(posts);
|
||||
}
|
||||
if (collection.get('name') === 'pages') {
|
||||
return Promise.resolve(pages);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should search collections by title', async () => {
|
||||
const results = await backend.search(collections, 'find me by title');
|
||||
|
||||
expect(results).toEqual({
|
||||
entries: [posts[0], pages[0]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should search collections by short title', async () => {
|
||||
const results = await backend.search(collections, 'find me by short title');
|
||||
|
||||
expect(results).toEqual({
|
||||
entries: [posts[0], pages[0]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should search collections by author', async () => {
|
||||
const results = await backend.search(collections, 'find me by author');
|
||||
|
||||
expect(results).toEqual({
|
||||
entries: [posts[0], pages[0]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should search collections by summary description', async () => {
|
||||
const results = await backend.search(
|
||||
collections.map(c => c.set('summary', '{{description}}')),
|
||||
'find me by description',
|
||||
);
|
||||
|
||||
expect(results).toEqual({
|
||||
entries: [posts[0], pages[0]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should query collections by title', async () => {
|
||||
const results = await backend.query(collections[0], ['title'], 'find me by title');
|
||||
|
||||
expect(results).toEqual({
|
||||
hits: [posts[0]],
|
||||
query: 'find me by title',
|
||||
});
|
||||
});
|
||||
|
||||
it('should query collections by slug', async () => {
|
||||
const results = await backend.query(collections[0], ['slug'], 'find-me');
|
||||
|
||||
expect(results).toEqual({
|
||||
hits: [posts[0]],
|
||||
query: 'find-me',
|
||||
});
|
||||
});
|
||||
|
||||
it('should query collections by path', async () => {
|
||||
const results = await backend.query(collections[0], ['path'], 'posts/find-me.md');
|
||||
|
||||
expect(results).toEqual({
|
||||
hits: [posts[0]],
|
||||
query: 'posts/find-me.md',
|
||||
});
|
||||
});
|
||||
|
||||
it('should query collections by nested field', async () => {
|
||||
const results = await backend.query(
|
||||
collections[0],
|
||||
['nested.title'],
|
||||
'find me by nested title',
|
||||
);
|
||||
|
||||
expect(results).toEqual({
|
||||
hits: [posts[0]],
|
||||
query: 'find me by nested title',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ import {
|
||||
blobToFileObj,
|
||||
} from 'netlify-cms-lib-util';
|
||||
import { status } from './constants/publishModes';
|
||||
import { extractTemplateVars, dateParsers } from './lib/stringTemplate';
|
||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
||||
import {
|
||||
Collection,
|
||||
EntryMap,
|
||||
@ -50,6 +50,8 @@ import {
|
||||
import AssetProxy from './valueObjects/AssetProxy';
|
||||
import { FOLDER, FILES } from './constants/collectionTypes';
|
||||
|
||||
const { extractTemplateVars, dateParsers } = stringTemplate;
|
||||
|
||||
export class LocalStorageAuthStore {
|
||||
storageKey = 'netlify-cms-user';
|
||||
|
||||
@ -76,7 +78,7 @@ function getEntryBackupKey(collectionName?: string, slug?: string) {
|
||||
return `${baseKey}.${collectionName}${suffix}`;
|
||||
}
|
||||
|
||||
const extractSearchFields = (searchFields: string[]) => (entry: EntryValue) =>
|
||||
export const extractSearchFields = (searchFields: string[]) => (entry: EntryValue) =>
|
||||
searchFields.reduce((acc, field) => {
|
||||
const nestedFields = field.split('.');
|
||||
let f = entry.data;
|
||||
@ -84,7 +86,15 @@ const extractSearchFields = (searchFields: string[]) => (entry: EntryValue) =>
|
||||
f = f[nestedFields[i]];
|
||||
if (!f) break;
|
||||
}
|
||||
return f ? `${acc} ${f}` : acc;
|
||||
|
||||
if (f) {
|
||||
return `${acc} ${f}`;
|
||||
} else if (entry[nestedFields[0] as keyof EntryValue]) {
|
||||
// allows searching using entry.slug/entry.path etc.
|
||||
return `${acc} ${entry[nestedFields[0] as keyof EntryValue]}`;
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, '');
|
||||
|
||||
const sortByScore = (a: fuzzy.FilterResult<EntryValue>, b: fuzzy.FilterResult<EntryValue>) => {
|
||||
|
@ -309,20 +309,39 @@ describe('formatters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return preview url based on preview_path', () => {
|
||||
it('should return preview url based on preview_path and preview_path_date_field', () => {
|
||||
const date = new Date('2020-01-02T13:28:27.679Z');
|
||||
expect(
|
||||
previewUrlFormatter(
|
||||
'https://www.example.com',
|
||||
Map({
|
||||
preview_path: '{{year}}/{{slug}}/{{title}}/{{fields.slug}}',
|
||||
preview_path_date_field: 'date',
|
||||
preview_path_date_field: 'customDateField',
|
||||
}),
|
||||
'backendSlug',
|
||||
slugConfig,
|
||||
Map({ data: Map({ customDateField: date, slug: 'entrySlug', title: 'title' }) }),
|
||||
),
|
||||
).toBe('https://www.example.com/2020/backendslug/title/entryslug');
|
||||
});
|
||||
|
||||
it('should infer date field when preview_path_date_field is not configured', () => {
|
||||
const { selectInferedField } = require('../../reducers/collections');
|
||||
selectInferedField.mockReturnValue('date');
|
||||
|
||||
const date = new Date('2020-01-02T13:28:27.679Z');
|
||||
expect(
|
||||
previewUrlFormatter(
|
||||
'https://www.example.com',
|
||||
fromJS({
|
||||
name: 'posts',
|
||||
preview_path: '{{year}}/{{month}}/{{slug}}/{{title}}/{{fields.slug}}',
|
||||
}),
|
||||
'backendSlug',
|
||||
slugConfig,
|
||||
Map({ data: Map({ date, slug: 'entrySlug', title: 'title' }) }),
|
||||
),
|
||||
).toBe('https://www.example.com/2020/backendslug/title/entryslug');
|
||||
).toBe('https://www.example.com/2020/01/backendslug/title/entryslug');
|
||||
});
|
||||
|
||||
it('should compile filename and extension template values', () => {
|
||||
|
@ -1,16 +1,24 @@
|
||||
import { Map } from 'immutable';
|
||||
import { flow, partialRight, trimEnd, trimStart } from 'lodash';
|
||||
import { sanitizeSlug } from './urlHelper';
|
||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
||||
import {
|
||||
selectIdentifier,
|
||||
selectField,
|
||||
COMMIT_AUTHOR,
|
||||
COMMIT_DATE,
|
||||
selectInferedField,
|
||||
} from '../reducers/collections';
|
||||
import { Collection, SlugConfig, Config, EntryMap } from '../types/redux';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
const {
|
||||
compileStringTemplate,
|
||||
parseDateFromEntry,
|
||||
SLUG_MISSING_REQUIRED_DATE,
|
||||
keyToPathArray,
|
||||
} from './stringTemplate';
|
||||
import { selectIdentifier, selectField, COMMIT_AUTHOR, COMMIT_DATE } from '../reducers/collections';
|
||||
import { Collection, SlugConfig, Config, EntryMap } from '../types/redux';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { basename, fileExtension } from 'netlify-cms-lib-util';
|
||||
addFileTemplateFields,
|
||||
} = stringTemplate;
|
||||
|
||||
const commitMessageTemplates = Map({
|
||||
create: 'Create {{collection}} “{{slug}}”',
|
||||
@ -122,21 +130,6 @@ export const slugFormatter = (
|
||||
}
|
||||
};
|
||||
|
||||
const addFileTemplateFields = (entryPath: string, fields: Map<string, string>) => {
|
||||
if (!entryPath) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
const extension = fileExtension(entryPath);
|
||||
const filename = basename(entryPath, `.${extension}`);
|
||||
fields = fields.withMutations(map => {
|
||||
map.set('filename', filename);
|
||||
map.set('extension', extension);
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
||||
|
||||
export const previewUrlFormatter = (
|
||||
baseUrl: string,
|
||||
collection: Collection,
|
||||
@ -168,7 +161,9 @@ export const previewUrlFormatter = (
|
||||
const pathTemplate = collection.get('preview_path') as string;
|
||||
let fields = entry.get('data') as Map<string, string>;
|
||||
fields = addFileTemplateFields(entry.get('path'), fields);
|
||||
const date = parseDateFromEntry(entry, collection, collection.get('preview_path_date_field'));
|
||||
const dateFieldName =
|
||||
collection.get('preview_path_date_field') || selectInferedField(collection, 'date');
|
||||
const date = parseDateFromEntry((entry as unknown) as Map<string, unknown>, dateFieldName);
|
||||
|
||||
// Prepare and sanitize slug variables only, leave the rest of the
|
||||
// `preview_path` template as is.
|
||||
@ -201,7 +196,11 @@ export const summaryFormatter = (
|
||||
collection: Collection,
|
||||
) => {
|
||||
let entryData = entry.get('data');
|
||||
const date = parseDateFromEntry(entry, collection) || null;
|
||||
const date =
|
||||
parseDateFromEntry(
|
||||
(entry as unknown) as Map<string, unknown>,
|
||||
selectInferedField(collection, 'date'),
|
||||
) || null;
|
||||
const identifier = entryData.getIn(keyToPathArray(selectIdentifier(collection) as string));
|
||||
|
||||
entryData = addFileTemplateFields(entry.get('path'), entryData);
|
||||
@ -231,7 +230,11 @@ export const folderFormatter = (
|
||||
let fields = (entry.get('data') as Map<string, string>).set(folderKey, defaultFolder);
|
||||
fields = addFileTemplateFields(entry.get('path'), fields);
|
||||
|
||||
const date = parseDateFromEntry(entry, collection) || null;
|
||||
const date =
|
||||
parseDateFromEntry(
|
||||
(entry as unknown) as Map<string, unknown>,
|
||||
selectInferedField(collection, 'date'),
|
||||
) || null;
|
||||
const identifier = fields.getIn(keyToPathArray(selectIdentifier(collection) as string));
|
||||
const processSegment = getProcessSegment(slugConfig);
|
||||
|
||||
|
@ -14,10 +14,12 @@ import {
|
||||
EntryMap,
|
||||
} from '../types/redux';
|
||||
import { selectMediaFolder } from './entries';
|
||||
import { keyToPathArray } from '../lib/stringTemplate';
|
||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
||||
import { summaryFormatter } from '../lib/formatters';
|
||||
import { Backend } from '../backend';
|
||||
|
||||
const { keyToPathArray } = stringTemplate;
|
||||
|
||||
const collections = (state = null, action: CollectionsAction) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
|
11
packages/netlify-cms-lib-widgets/README.md
Normal file
11
packages/netlify-cms-lib-widgets/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Docs coming soon!
|
||||
|
||||
Netlify CMS was recently converted from a single npm package to a "monorepo" of over 20 packages.
|
||||
That's over 20 Readme's! We haven't created one for this package yet, but we will soon.
|
||||
|
||||
In the meantime, you can:
|
||||
|
||||
1. Check out the [main readme](https://github.com/netlify/netlify-cms/#readme) or the [documentation
|
||||
site](https://www.netlifycms.org) for more info.
|
||||
2. Reach out to the [community chat](https://netlifycms.org/chat/) if you need help.
|
||||
3. Help out and [write the readme yourself](https://github.com/netlify/netlify-cms/edit/master/packages/netlify-cms-lib-widgets/README.md)!
|
24
packages/netlify-cms-lib-widgets/package.json
Normal file
24
packages/netlify-cms-lib-widgets/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "netlify-cms-lib-widgets",
|
||||
"description": "Shared utilities for Netlify CMS.",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/netlify/netlify-cms/tree/master/packages/netlify-cms-lib-widgets",
|
||||
"bugs": "https://github.com/netlify/netlify-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/netlify-cms-lib-widgets.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"netlify-cms"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"develop": "yarn build:esm --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
|
||||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"immutable": "^3.7.6",
|
||||
"moment": "^2.24.0"
|
||||
}
|
||||
}
|
@ -25,29 +25,24 @@ describe('stringTemplate', () => {
|
||||
});
|
||||
|
||||
describe('parseDateFromEntry', () => {
|
||||
it('should infer date field and return entry date', () => {
|
||||
it('should return date based on dateFieldName', () => {
|
||||
const date = new Date().toISOString();
|
||||
const entry = fromJS({ data: { date } });
|
||||
const collection = fromJS({ fields: [{ name: 'date', widget: 'date' }] });
|
||||
expect(parseDateFromEntry(entry, collection).toISOString()).toBe(date);
|
||||
const dateFieldName = 'dateFieldName';
|
||||
const entry = fromJS({ data: { dateFieldName: date } });
|
||||
expect(parseDateFromEntry(entry, dateFieldName).toISOString()).toBe(date);
|
||||
});
|
||||
|
||||
it('should use supplied date field and return entry date', () => {
|
||||
const date = new Date().toISOString();
|
||||
const entry = fromJS({ data: { preview_date: date } });
|
||||
expect(parseDateFromEntry(entry, null, 'preview_date').toISOString()).toBe(date);
|
||||
});
|
||||
|
||||
it('should return undefined on non existing date', () => {
|
||||
it('should return undefined on empty dateFieldName', () => {
|
||||
const entry = fromJS({ data: {} });
|
||||
const collection = fromJS({ fields: [{}] });
|
||||
expect(parseDateFromEntry(entry, collection)).toBeUndefined();
|
||||
expect(parseDateFromEntry(entry, '')).toBeUndefined();
|
||||
expect(parseDateFromEntry(entry, null)).toBeUndefined();
|
||||
expect(parseDateFromEntry(entry, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined on invalid date', () => {
|
||||
const entry = fromJS({ data: { date: '' } });
|
||||
const collection = fromJS({ fields: [{ name: 'date', widget: 'date' }] });
|
||||
expect(parseDateFromEntry(entry, collection)).toBeUndefined();
|
||||
const dateFieldName = 'date';
|
||||
expect(parseDateFromEntry(entry, dateFieldName)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
6
packages/netlify-cms-lib-widgets/src/index.ts
Normal file
6
packages/netlify-cms-lib-widgets/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import * as stringTemplate from './stringTemplate';
|
||||
|
||||
export const NetlifyCmsLibWidgets = {
|
||||
stringTemplate,
|
||||
};
|
||||
export { stringTemplate };
|
@ -1,7 +1,10 @@
|
||||
import moment from 'moment';
|
||||
import { Map } from 'immutable';
|
||||
import { selectInferedField } from '../reducers/collections';
|
||||
import { EntryMap, Collection } from '../types/redux';
|
||||
import { basename, extname } from 'path';
|
||||
|
||||
const FIELD_PREFIX = 'fields.';
|
||||
const templateContentPattern = '[^}{]+';
|
||||
const templateVariablePattern = `{{(${templateContentPattern})}}`;
|
||||
|
||||
// prepends a Zero if the date has only 1 digit
|
||||
function formatDate(date: number) {
|
||||
@ -17,11 +20,19 @@ export const dateParsers: Record<string, (date: Date) => string> = {
|
||||
second: (date: Date) => formatDate(date.getUTCSeconds()),
|
||||
};
|
||||
|
||||
export const SLUG_MISSING_REQUIRED_DATE = 'SLUG_MISSING_REQUIRED_DATE';
|
||||
export function parseDateFromEntry(entry: Map<string, unknown>, dateFieldName?: string | null) {
|
||||
if (!dateFieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const FIELD_PREFIX = 'fields.';
|
||||
const templateContentPattern = '[^}{]+';
|
||||
const templateVariablePattern = `{{(${templateContentPattern})}}`;
|
||||
const dateValue = entry.getIn(['data', dateFieldName]);
|
||||
const dateMoment = dateValue && moment(dateValue);
|
||||
if (dateMoment && dateMoment.isValid()) {
|
||||
return dateMoment.toDate();
|
||||
}
|
||||
}
|
||||
|
||||
export const SLUG_MISSING_REQUIRED_DATE = 'SLUG_MISSING_REQUIRED_DATE';
|
||||
|
||||
export const keyToPathArray = (key?: string) => {
|
||||
if (!key) {
|
||||
@ -56,20 +67,11 @@ function getExplicitFieldReplacement(key: string, data: Map<string, unknown>) {
|
||||
return;
|
||||
}
|
||||
const fieldName = key.substring(FIELD_PREFIX.length);
|
||||
return data.getIn(keyToPathArray(fieldName), '') as string;
|
||||
}
|
||||
|
||||
export function parseDateFromEntry(entry: EntryMap, collection: Collection, fieldName?: string) {
|
||||
const dateFieldName = fieldName || selectInferedField(collection, 'date');
|
||||
if (!dateFieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateValue = entry.getIn(['data', dateFieldName]);
|
||||
const dateMoment = dateValue && moment(dateValue);
|
||||
if (dateMoment && dateMoment.isValid()) {
|
||||
return dateMoment.toDate();
|
||||
const value = data.getIn(keyToPathArray(fieldName));
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function compileStringTemplate(
|
||||
@ -130,3 +132,18 @@ export function extractTemplateVars(template: string) {
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
}
|
||||
|
||||
export const addFileTemplateFields = (entryPath: string, fields: Map<string, string>) => {
|
||||
if (!entryPath) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
const extension = extname(entryPath);
|
||||
const filename = basename(entryPath, extension);
|
||||
fields = fields.withMutations(map => {
|
||||
map.set('filename', filename);
|
||||
map.set('extension', extension === '' ? extension : extension.substr(1));
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
3
packages/netlify-cms-lib-widgets/webpack.config.js
Normal file
3
packages/netlify-cms-lib-widgets/webpack.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
@ -30,6 +30,7 @@
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.17.11",
|
||||
"netlify-cms-ui-default": "^2.6.0",
|
||||
"netlify-cms-lib-widgets": "^1.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.4",
|
||||
"uuid": "^3.3.2"
|
||||
|
@ -5,6 +5,7 @@ import { Async as AsyncSelect } from 'react-select';
|
||||
import { find, isEmpty, last, debounce } from 'lodash';
|
||||
import { List, Map, fromJS } from 'immutable';
|
||||
import { reactSelectStyles } from 'netlify-cms-ui-default';
|
||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
||||
|
||||
function optionToString(option) {
|
||||
return option && option.value ? option.value : '';
|
||||
@ -72,7 +73,7 @@ export default class RelationControl extends React.Component {
|
||||
if (value) {
|
||||
const listValue = List.isList(value) ? value : List([value]);
|
||||
listValue.forEach(val => {
|
||||
const hit = hits.find(i => this.parseNestedFields(i.data, valueField) === val);
|
||||
const hit = hits.find(hit => this.parseNestedFields(hit, valueField) === val);
|
||||
if (hit) {
|
||||
onChange(value, {
|
||||
[field.get('name')]: {
|
||||
@ -113,17 +114,15 @@ export default class RelationControl extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
parseNestedFields = (targetObject, field) => {
|
||||
const nestedField = field.split('.');
|
||||
let f = targetObject;
|
||||
for (let i = 0; i < nestedField.length; i++) {
|
||||
f = f[nestedField[i]];
|
||||
if (!f) break;
|
||||
parseNestedFields = (hit, field) => {
|
||||
const templateVars = stringTemplate.extractTemplateVars(field);
|
||||
// wrap non template fields with a template
|
||||
if (templateVars.length <= 0) {
|
||||
field = `{{fields.${field}}}`;
|
||||
}
|
||||
if (typeof f === 'object' && f !== null) {
|
||||
return JSON.stringify(f);
|
||||
}
|
||||
return f;
|
||||
const data = stringTemplate.addFileTemplateFields(hit.path, fromJS(hit.data));
|
||||
const value = stringTemplate.compileStringTemplate(field, null, hit.slug, data);
|
||||
return value;
|
||||
};
|
||||
|
||||
parseHitOptions = hits => {
|
||||
@ -136,14 +135,14 @@ export default class RelationControl extends React.Component {
|
||||
if (List.isList(displayField)) {
|
||||
labelReturn = displayField
|
||||
.toJS()
|
||||
.map(key => this.parseNestedFields(hit.data, key))
|
||||
.map(key => this.parseNestedFields(hit, key))
|
||||
.join(' ');
|
||||
} else {
|
||||
labelReturn = this.parseNestedFields(hit.data, displayField);
|
||||
labelReturn = this.parseNestedFields(hit, displayField);
|
||||
}
|
||||
return {
|
||||
data: hit.data,
|
||||
value: this.parseNestedFields(hit.data, valueField),
|
||||
value: this.parseNestedFields(hit, valueField),
|
||||
label: labelReturn,
|
||||
};
|
||||
});
|
||||
|
@ -43,7 +43,8 @@ const generateHits = length => {
|
||||
const hits = Array.from({ length }, (val, idx) => {
|
||||
const title = `Post # ${idx + 1}`;
|
||||
const slug = `post-number-${idx + 1}`;
|
||||
return { collection: 'posts', data: { title, slug } };
|
||||
const path = `posts/${slug}.md`;
|
||||
return { collection: 'posts', data: { title, slug }, slug, path };
|
||||
});
|
||||
|
||||
return [
|
||||
@ -247,6 +248,31 @@ describe('Relation widget', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle string templates', async () => {
|
||||
const stringTemplateConfig = {
|
||||
name: 'post',
|
||||
collection: 'posts',
|
||||
displayFields: ['{{slug}}', '{{filename}}', '{{extension}}'],
|
||||
searchFields: ['slug'],
|
||||
valueField: '{{slug}}',
|
||||
};
|
||||
|
||||
const field = fromJS(stringTemplateConfig);
|
||||
const { getByText, input, onChangeSpy } = setup({ field });
|
||||
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' } } },
|
||||
};
|
||||
|
||||
await wait(() => {
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||||
fireEvent.click(getByText(label));
|
||||
expect(onChangeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(value, metadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with multiple', () => {
|
||||
it('should call onChange with correct selectedItem value and metadata', async () => {
|
||||
const field = fromJS({ ...fieldConfig, multiple: true });
|
||||
|
Reference in New Issue
Block a user