Skip to content

Commit 08d6760

Browse files
keegan-lillovladdu
authored andcommitted
feat(core): Add {{dirname}} to summary and preview_path (decaporg#4279)
1 parent 4e40222 commit 08d6760

File tree

6 files changed

+149
-10
lines changed

6 files changed

+149
-10
lines changed

packages/netlify-cms-core/src/backend.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ export class Backend {
474474
label: loadedEntry.file.label,
475475
author: loadedEntry.file.author,
476476
updatedOn: loadedEntry.file.updatedOn,
477+
meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
477478
},
478479
),
479480
);

packages/netlify-cms-core/src/lib/__tests__/formatters.spec.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,38 @@ describe('formatters', () => {
402402
).toBe('https://www.example.com/posts/title.md');
403403
});
404404

405+
it('should compile the dirname template value to empty in a regular collection', () => {
406+
expect(
407+
previewUrlFormatter(
408+
'https://www.example.com',
409+
Map({
410+
folder: '_portfolio',
411+
preview_path: 'portfolio/{{dirname}}',
412+
}),
413+
'backendSlug',
414+
slugConfig,
415+
Map({ data: Map({}), path: '_portfolio/i-am-the-slug.md' }),
416+
),
417+
).toBe('https://www.example.com/portfolio/');
418+
});
419+
420+
it('should compile dirname template value when in a nested collection', () => {
421+
expect(
422+
previewUrlFormatter(
423+
'https://www.example.com',
424+
Map({
425+
folder: '_portfolio',
426+
preview_path: 'portfolio/{{dirname}}',
427+
nested: { depth: 100 },
428+
meta: { path: { widget: 'string', label: 'Path', index_file: 'index' } },
429+
}),
430+
'backendSlug',
431+
slugConfig,
432+
Map({ data: Map({}), path: '_portfolio/drawing/i-am-the-slug/index.md' }),
433+
),
434+
).toBe('https://www.example.com/portfolio/drawing/i-am-the-slug');
435+
});
436+
405437
it('should log error and ignore preview_path when date is missing', () => {
406438
jest.spyOn(console, 'error').mockImplementation(() => {});
407439
expect(
@@ -449,6 +481,46 @@ describe('formatters', () => {
449481
summaryFormatter('{{title}}-{{year}}-{{filename}}.{{extension}}', entry, collection),
450482
).toBe('title-2020-post.md');
451483
});
484+
485+
it('should handle the dirname variable in a regular collection', () => {
486+
const { selectInferedField } = require('../../reducers/collections');
487+
selectInferedField.mockReturnValue('date');
488+
489+
const date = new Date('2020-01-02T13:28:27.679Z');
490+
const entry = fromJS({
491+
path: '_portfolio/drawing.md',
492+
data: { date, title: 'title' },
493+
});
494+
const collection = fromJS({
495+
folder: '_portfolio',
496+
fields: [{ name: 'date', widget: 'date' }],
497+
});
498+
499+
expect(summaryFormatter('{{dirname}}/{{title}}-{{year}}', entry, collection)).toBe(
500+
'/title-2020',
501+
);
502+
});
503+
504+
it('should handle the dirname variable in a nested collection', () => {
505+
const { selectInferedField } = require('../../reducers/collections');
506+
selectInferedField.mockReturnValue('date');
507+
508+
const date = new Date('2020-01-02T13:28:27.679Z');
509+
const entry = fromJS({
510+
path: '_portfolio/drawing/index.md',
511+
data: { date, title: 'title' },
512+
});
513+
const collection = fromJS({
514+
folder: '_portfolio',
515+
nested: { depth: 100 },
516+
meta: { path: { widget: 'string', label: 'Path', index_file: 'index' } },
517+
fields: [{ name: 'date', widget: 'date' }],
518+
});
519+
520+
expect(summaryFormatter('{{dirname}}/{{title}}-{{year}}', entry, collection)).toBe(
521+
'drawing/title-2020',
522+
);
523+
});
452524
});
453525

454526
describe('folderFormatter', () => {
@@ -519,5 +591,50 @@ describe('formatters', () => {
519591
),
520592
).toBe('md');
521593
});
594+
595+
it('should compile dirname template value in a regular collection', () => {
596+
const entry = fromJS({
597+
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
598+
data: { category: 'Hosting And Deployment' },
599+
});
600+
const collection = fromJS({
601+
folder: 'content/en/',
602+
});
603+
604+
expect(
605+
folderFormatter(
606+
'{{dirname}}',
607+
entry,
608+
collection,
609+
'static/images',
610+
'media_folder',
611+
slugConfig,
612+
),
613+
).toBe('hosting-and-deployment');
614+
});
615+
616+
it('should compile dirname template value in a nested collection', () => {
617+
const entry = fromJS({
618+
path: '_portfolio/drawing/i-am-the-slug/index.md',
619+
data: { category: 'Hosting And Deployment' },
620+
});
621+
const collection = fromJS({
622+
folder: '_portfolio',
623+
nested: { depth: 100 },
624+
meta: { path: { widget: 'string', label: 'Path', index_file: 'index' } },
625+
fields: [{ name: 'date', widget: 'date' }],
626+
});
627+
628+
expect(
629+
folderFormatter(
630+
'{{dirname}}',
631+
entry,
632+
collection,
633+
'static/images',
634+
'media_folder',
635+
slugConfig,
636+
),
637+
).toBe('drawing/i-am-the-slug');
638+
});
522639
});
523640
});

packages/netlify-cms-core/src/lib/formatters.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,12 @@ export const prepareSlug = (slug: string) => {
103103
);
104104
};
105105

106-
export const getProcessSegment = (slugConfig: SlugConfig) =>
107-
flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)]);
106+
export const getProcessSegment = (slugConfig: SlugConfig, ignoreValues: string[] = []) => {
107+
return (value: string) =>
108+
ignoreValues.includes(value)
109+
? value
110+
: flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)])(value);
111+
};
108112

109113
export const slugFormatter = (
110114
collection: Collection,
@@ -164,14 +168,14 @@ export const previewUrlFormatter = (
164168
const basePath = trimEnd(baseUrl, '/');
165169
const pathTemplate = collection.get('preview_path') as string;
166170
let fields = entry.get('data') as Map<string, string>;
167-
fields = addFileTemplateFields(entry.get('path'), fields);
171+
fields = addFileTemplateFields(entry.get('path'), fields, collection.get('folder'));
168172
const dateFieldName =
169173
collection.get('preview_path_date_field') || selectInferedField(collection, 'date');
170174
const date = parseDateFromEntry((entry as unknown) as Map<string, unknown>, dateFieldName);
171175

172176
// Prepare and sanitize slug variables only, leave the rest of the
173177
// `preview_path` template as is.
174-
const processSegment = getProcessSegment(slugConfig);
178+
const processSegment = getProcessSegment(slugConfig, [fields.get('dirname')]);
175179
let compiledPath;
176180

177181
try {
@@ -207,7 +211,7 @@ export const summaryFormatter = (
207211
) || null;
208212
const identifier = entryData.getIn(keyToPathArray(selectIdentifier(collection) as string));
209213

210-
entryData = addFileTemplateFields(entry.get('path'), entryData);
214+
entryData = addFileTemplateFields(entry.get('path'), entryData, collection.get('folder'));
211215
// allow commit information in summary template
212216
if (entry.get('author') && !selectField(collection, COMMIT_AUTHOR)) {
213217
entryData = entryData.set(COMMIT_AUTHOR, entry.get('author'));
@@ -232,22 +236,22 @@ export const folderFormatter = (
232236
}
233237

234238
let fields = (entry.get('data') as Map<string, string>).set(folderKey, defaultFolder);
235-
fields = addFileTemplateFields(entry.get('path'), fields);
239+
fields = addFileTemplateFields(entry.get('path'), fields, collection.get('folder'));
236240

237241
const date =
238242
parseDateFromEntry(
239243
(entry as unknown) as Map<string, unknown>,
240244
selectInferedField(collection, 'date'),
241245
) || null;
242246
const identifier = fields.getIn(keyToPathArray(selectIdentifier(collection) as string));
243-
const processSegment = getProcessSegment(slugConfig);
247+
const processSegment = getProcessSegment(slugConfig, [defaultFolder, fields.get('dirname')]);
244248

245249
const mediaFolder = compileStringTemplate(
246250
folderTemplate,
247251
date,
248252
identifier,
249253
fields,
250-
(value: string) => (value === defaultFolder ? defaultFolder : processSegment(value)),
254+
processSegment,
251255
);
252256

253257
return mediaFolder;

packages/netlify-cms-lib-widgets/src/stringTemplate.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import moment from 'moment';
22
import { Map } from 'immutable';
3-
import { basename, extname } from 'path';
3+
import { basename, extname, dirname } from 'path';
44
import { get, trimEnd } from 'lodash';
55

66
const FIELD_PREFIX = 'fields.';
@@ -169,14 +169,28 @@ export function extractTemplateVars(template: string) {
169169
});
170170
}
171171

172-
export const addFileTemplateFields = (entryPath: string, fields: Map<string, string>) => {
172+
/**
173+
* Appends `dirname`, `filename` and `extension` to the provided `fields` map.
174+
* @param entryPath
175+
* @param fields
176+
* @param folder - optionally include a folder that the dirname will be relative to.
177+
* eg: `addFileTemplateFields('foo/bar/baz.ext', fields, 'foo')`
178+
* will result in: `{ dirname: 'bar', filename: 'baz', extension: 'ext' }`
179+
*/
180+
export const addFileTemplateFields = (
181+
entryPath: string,
182+
fields: Map<string, string>,
183+
folder = '',
184+
) => {
173185
if (!entryPath) {
174186
return fields;
175187
}
176188

177189
const extension = extname(entryPath);
178190
const filename = basename(entryPath, extension);
191+
const dirnameExcludingFolder = dirname(entryPath).replace(new RegExp(`^(/?)${folder}/?`), '$1');
179192
fields = fields.withMutations(map => {
193+
map.set('dirname', dirnameExcludingFolder);
180194
map.set('filename', filename);
181195
map.set('extension', extension === '' ? extension : extension.substr(1));
182196
});

website/content/docs/beta-features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ And for the image field being populated with a value of `image.png`.
248248

249249
Supports all of the [`slug` templates](/docs/configuration-options#slug) and:
250250

251+
- `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`.
251252
- `{{filename}}` The file name without the extension part.
252253
- `{{extension}}` The file extension.
253254
- `{{media_folder}}` The global `media_folder`.

website/content/docs/configuration-options.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ Template tags are the same as those for [slug](#slug), with the following except
293293

294294
* `{{slug}}` is the entire slug for the current entry (not just the url-safe identifier, as is the case with [`slug` configuration](#slug))
295295
* The date based template tags, such as `{{year}}` and `{{month}}`, are pulled from a date field in your entry, and may require additional configuration - see [`preview_path_date_field`](#preview_path_date_field) for details. If a date template tag is used and no date can be found, `preview_path` will be ignored.
296+
* `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`.
296297
* `{{filename}}` The file name without the extension part.
297298
* `{{extension}}` The file extension.
298299

@@ -373,6 +374,7 @@ This setting allows the customization of the collection list view. Similar to th
373374

374375
Template tags are the same as those for [slug](#slug), with the following additions:
375376

377+
* `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`.
376378
* `{{filename}}` The file name without the extension part.
377379
* `{{extension}}` The file extension.
378380
* `{{commit_date}}` The file commit date on supported backends (git based backends).

0 commit comments

Comments
 (0)