fix: slug template variable (#795)
This commit is contained in:
parent
d28c43e95a
commit
8ee9a464ab
@ -71,6 +71,7 @@ collections:
|
||||
label: FAQ
|
||||
folder: _faqs
|
||||
create: true
|
||||
media_folder: '/_faqs_images/{{slug}}'
|
||||
fields:
|
||||
- label: Question
|
||||
name: title
|
||||
@ -78,6 +79,9 @@ collections:
|
||||
- label: Answer
|
||||
name: body
|
||||
widget: markdown
|
||||
- label: Image
|
||||
name: image
|
||||
widget: image
|
||||
- name: posts
|
||||
label: Posts
|
||||
label_singular: Post
|
||||
@ -475,3 +479,26 @@ collections:
|
||||
- label: File
|
||||
name: file
|
||||
widget: file
|
||||
- name: pages
|
||||
label: Nested Pages
|
||||
label_singular: 'Page'
|
||||
folder: _nested_pages
|
||||
create: true
|
||||
media_folder: '/_nested_page_pictures/{{slug}}'
|
||||
# adding a nested object will show the collection folder structure
|
||||
nested:
|
||||
depth: 100 # max depth to show in the collection tree
|
||||
summary: '{{title}}' # optional summary for a tree node, defaults to the inferred title field
|
||||
# adding a path object allows editing the path of entries
|
||||
# moving an existing entry will move the entire sub tree of the entry to the new location
|
||||
path: { label: 'Path', index_file: 'index' }
|
||||
fields:
|
||||
- label: Title
|
||||
name: title
|
||||
widget: string
|
||||
- label: Image
|
||||
name: image
|
||||
widget: image
|
||||
- label: Body
|
||||
name: body
|
||||
widget: markdown
|
||||
|
@ -833,7 +833,7 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
|
||||
|
||||
const newEntry = entryDraft.entry.newRecord ?? false;
|
||||
|
||||
const customPath = selectCustomPath(draft.entry, collection, rootSlug, config);
|
||||
const customPath = selectCustomPath(draft.entry, collection, rootSlug, config.slug);
|
||||
|
||||
let dataFile: DataFile;
|
||||
if (newEntry) {
|
||||
|
@ -6,7 +6,7 @@ import type { Options } from '../API';
|
||||
|
||||
describe('gitea API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
});
|
||||
@ -29,7 +29,7 @@ describe('gitea API', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch url with authorization header', async () => {
|
||||
|
@ -6,7 +6,7 @@ import type { Options } from '../API';
|
||||
|
||||
describe('github API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||
});
|
||||
@ -65,7 +65,7 @@ describe('github API', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch url with authorization header', async () => {
|
||||
|
@ -5,7 +5,7 @@ global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch insi
|
||||
|
||||
describe('GitLab API', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
@ -63,7 +63,7 @@ const EditorControlPane = ({
|
||||
const config = useAppSelector(selectConfig);
|
||||
|
||||
const defaultNestedPath = useMemo(
|
||||
() => getNestedSlug(collection, entry, slug, config),
|
||||
() => getNestedSlug(collection, entry, slug, config?.slug),
|
||||
[collection, config, entry, slug],
|
||||
);
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { sanitizeSlug } from './urlHelper';
|
||||
import { selectIdentifier, selectInferredField } from './util/collection.util';
|
||||
import { selectField } from './util/field.util';
|
||||
import { set } from './util/object.util';
|
||||
import { isEmpty } from './util/string.util';
|
||||
import {
|
||||
addFileTemplateFields,
|
||||
compileStringTemplate,
|
||||
@ -94,16 +95,21 @@ export function slugFormatter<EF extends BaseField = UnknownField>(
|
||||
collection: Collection<EF>,
|
||||
entryData: EntryData,
|
||||
slugConfig?: Slug,
|
||||
) {
|
||||
): string {
|
||||
const slugTemplate = collection.slug || '{{slug}}';
|
||||
|
||||
const identifier = get(entryData, keyToPathArray(selectIdentifier(collection)));
|
||||
if (!identifier) {
|
||||
const identifierField = selectIdentifier(collection);
|
||||
if (!identifierField) {
|
||||
throw new Error(
|
||||
'Collection must have a field name that is a valid entry identifier, or must have `identifier_field` set',
|
||||
);
|
||||
}
|
||||
|
||||
const identifier = get(entryData, keyToPathArray(identifierField));
|
||||
if (isEmpty(identifier)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const processSegment = getProcessSegment(slugConfig);
|
||||
const date = new Date();
|
||||
const slug = compileStringTemplate(slugTemplate, date, identifier, entryData, processSegment);
|
||||
@ -122,10 +128,12 @@ export function summaryFormatter<EF extends BaseField>(
|
||||
summaryTemplate: string,
|
||||
entry: Entry,
|
||||
collection: Collection<EF>,
|
||||
slugConfig?: Slug,
|
||||
) {
|
||||
const slug = slugFormatter(collection, entry.data, slugConfig);
|
||||
|
||||
let entryData = entry.data;
|
||||
const date = parseDateFromEntry(entry, selectInferredField(collection, 'date')) || null;
|
||||
const identifier = get(entryData, keyToPathArray(selectIdentifier(collection)));
|
||||
|
||||
entryData =
|
||||
addFileTemplateFields(entry.path, entryData, 'folder' in collection ? collection.folder : '') ??
|
||||
@ -137,7 +145,7 @@ export function summaryFormatter<EF extends BaseField>(
|
||||
if (entry.updatedOn && !selectField(collection, COMMIT_DATE)) {
|
||||
entryData = set(entryData, COMMIT_DATE, entry.updatedOn);
|
||||
}
|
||||
const summary = compileStringTemplate(summaryTemplate, date, identifier, entryData);
|
||||
const summary = compileStringTemplate(summaryTemplate, date, slug, entryData);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@ -160,8 +168,10 @@ export function folderFormatter<EF extends BaseField>(
|
||||
'folder' in collection ? collection.folder : '',
|
||||
);
|
||||
|
||||
const date = parseDateFromEntry(entry, selectInferredField(collection, 'date')) || null;
|
||||
const slug = slugFormatter(collection, entry.data, slugConfig);
|
||||
|
||||
const date = parseDateFromEntry(entry, selectInferredField(collection, 'date')) || null;
|
||||
|
||||
const processSegment = getProcessSegment(slugConfig, [defaultFolder, fields?.dirname as string]);
|
||||
|
||||
const mediaFolder = compileStringTemplate(folderTemplate, date, slug, fields, processSegment);
|
||||
|
@ -1,11 +1,12 @@
|
||||
import url from 'url';
|
||||
import urlJoin from 'url-join';
|
||||
import diacritics from 'diacritics';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
import isString from 'lodash/isString';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import flow from 'lodash/flow';
|
||||
import isString from 'lodash/isString';
|
||||
import partialRight from 'lodash/partialRight';
|
||||
import trim from 'lodash/trim';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
import url from 'url';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import type { Slug } from '../interface';
|
||||
|
||||
@ -124,5 +125,5 @@ export function sanitizeSlug(str: string, options?: Slug) {
|
||||
}
|
||||
|
||||
export function joinUrlPath(base: string, ...path: string[]) {
|
||||
return urlJoin(base, ...path);
|
||||
return urlJoin(trim(base, '/'), ...path.map(p => trim(p, '/')));
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { createMockCollection } from '@staticcms/test/data/collections.mock';
|
||||
import { createMockConfig } from '@staticcms/test/data/config.mock';
|
||||
import { createMockEntry } from '@staticcms/test/data/entry.mock';
|
||||
import { mockImageField as mockBaseImageField } from '@staticcms/test/data/fields.mock';
|
||||
import { selectMediaFolder } from '../media.util';
|
||||
import { selectMediaFilePublicPath, selectMediaFolder } from '../media.util';
|
||||
|
||||
import type { FileOrImageField, FolderCollection, UnknownField } from '@staticcms/core/interface';
|
||||
|
||||
@ -145,6 +145,80 @@ describe('media.util', () => {
|
||||
'path/to/entry/path/to/some/other/media/i-am-a-title-fish',
|
||||
);
|
||||
});
|
||||
|
||||
it('should substitute slug from top level media_folder (always considered an absolute path)', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder/{{slug}}',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(selectMediaFolder(mockConfig, mockCollection, mockEntry, mockImageField)).toBe(
|
||||
'path/to/media/folder/i-am-a-title-fish',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw error when evaluating slug for new entry', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: 'path/to/some/other/media/{{slug}}',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: {},
|
||||
newRecord: true,
|
||||
});
|
||||
|
||||
expect(selectMediaFolder(mockConfig, mockCollection, mockEntry, mockImageField)).toBe(
|
||||
'path/to/entry/path/to/some/other/media',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -238,6 +312,730 @@ describe('media.util', () => {
|
||||
'path/to/some/other/media/i-am-a-title-fish',
|
||||
);
|
||||
});
|
||||
|
||||
it('should substitute slug from top level media_folder', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: '/path/to/media/folder/{{slug}}',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(selectMediaFolder(mockConfig, mockCollection, mockEntry, mockImageField)).toBe(
|
||||
'path/to/media/folder/i-am-a-title-fish',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw error when evaluating slug for new entry', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: '/path/to/some/other/media/{{slug}}',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: {},
|
||||
newRecord: true,
|
||||
});
|
||||
|
||||
expect(selectMediaFolder(mockConfig, mockCollection, mockEntry, mockImageField)).toBe(
|
||||
'path/to/some/other/media',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectMediaFilePublicPath', () => {
|
||||
const mockBaseCollection = createMockCollection<UnknownField>({
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const mockBaseEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: {
|
||||
title: 'I am a title',
|
||||
},
|
||||
});
|
||||
|
||||
describe('top level', () => {
|
||||
it('should default to top level config public_folder', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [mockBaseCollection],
|
||||
media_folder: 'path/to/media/folder',
|
||||
public_folder: 'path/to/public/folder',
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
undefined,
|
||||
'image.png',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toBe('path/to/public/folder/image.png');
|
||||
});
|
||||
|
||||
it('should use media_folder as an absolute path if public_folder is not provided', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [mockBaseCollection],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
undefined,
|
||||
'image.png',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toBe('/path/to/media/folder/image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('entry', () => {
|
||||
it('should default to top level config public_folder', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [mockBaseCollection],
|
||||
media_folder: 'path/to/media/folder',
|
||||
public_folder: 'path/to/public/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('path/to/public/folder/image.png');
|
||||
});
|
||||
|
||||
it('should default to top level config media_folder as an absolute path', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [mockBaseCollection],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/media/folder/image.png');
|
||||
});
|
||||
|
||||
describe('relative path', () => {
|
||||
it('should use collection public_folder if available', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: 'path/to/collection/media/folder',
|
||||
public_folder: 'path/to/collection/public/folder',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('path/to/collection/public/folder/image.png');
|
||||
});
|
||||
|
||||
it('should use collection media_folder if no public_folder is available', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: 'path/to/collection/media/folder',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('path/to/collection/media/folder/image.png');
|
||||
});
|
||||
|
||||
describe('template variable', () => {
|
||||
it('should substitute field value', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: 'path/to/some/other/media/{{fields.title}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('path/to/some/other/media/i-am-a-title/image.png');
|
||||
});
|
||||
|
||||
it('should substitute slug', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: 'path/to/some/other/media/{{slug}}',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('path/to/some/other/media/i-am-a-title-fish/image.png');
|
||||
});
|
||||
|
||||
it('should substitute slug from top level media_folder if no public_folders', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder/{{slug}}',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/media/folder/i-am-a-title-fish/image.png');
|
||||
});
|
||||
|
||||
it('should substitute slug from top level public_folder', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder/{{slug}}',
|
||||
public_folder: 'path/to/public/folder/{{slug}}',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('path/to/public/folder/i-am-a-title-fish/image.png');
|
||||
});
|
||||
|
||||
it('should not throw error when evaluating slug for new entry', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: 'path/to/collection/media/folder/{{slug}}',
|
||||
public_folder: 'path/to/collection/public/folder/{{slug}}',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: {},
|
||||
newRecord: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('path/to/collection/public/folder/image.png');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('absolute path', () => {
|
||||
it('should use collection public_folder if available', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: 'path/to/collection/media/folder',
|
||||
public_folder: '/path/to/collection/public/folder',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/collection/public/folder/image.png');
|
||||
});
|
||||
|
||||
it('should use collection media_folder if no public_folder is available', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: '/path/to/collection/media/folder',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/collection/media/folder/image.png');
|
||||
});
|
||||
|
||||
describe('template variable', () => {
|
||||
it('should substitute field value', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: '/path/to/some/other/media/{{fields.title}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockBaseEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/some/other/media/i-am-a-title/image.png');
|
||||
});
|
||||
|
||||
it('should substitute slug', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: '/path/to/some/other/media/{{slug}}',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/some/other/media/i-am-a-title-fish/image.png');
|
||||
});
|
||||
|
||||
it('should substitute slug from top level media_folder if no public_folders', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: '/path/to/media/folder/{{slug}}',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/media/folder/i-am-a-title-fish/image.png');
|
||||
});
|
||||
|
||||
it('should substitute slug from top level public_folder', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: '/path/to/media/folder/{{slug}}',
|
||||
public_folder: '/path/to/public/folder/{{slug}}',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: { title: 'i am a title', name: 'fish' },
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/public/folder/i-am-a-title-fish/image.png');
|
||||
});
|
||||
|
||||
it('should not throw error when evaluating slug for new entry', () => {
|
||||
const mockConfig = createMockConfig({
|
||||
collections: [
|
||||
createMockCollection<UnknownField>({
|
||||
folder: 'base/folder',
|
||||
media_folder: '/path/to/collection/media/folder/{{slug}}',
|
||||
public_folder: '/path/to/collection/public/folder/{{slug}}',
|
||||
slug: '{{fields.title}}-{{fields.name}}',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
widget: 'string',
|
||||
},
|
||||
mockBaseImageField,
|
||||
],
|
||||
}),
|
||||
],
|
||||
media_folder: 'path/to/media/folder',
|
||||
});
|
||||
|
||||
const mockCollection = mockConfig.collections[0];
|
||||
const mockImageField = (mockConfig.collections[0] as FolderCollection)
|
||||
.fields[3] as FileOrImageField;
|
||||
|
||||
const mockEntry = createMockEntry({
|
||||
path: 'path/to/entry/index.md',
|
||||
data: {},
|
||||
newRecord: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
mockConfig,
|
||||
mockCollection,
|
||||
'image.png',
|
||||
mockEntry,
|
||||
mockImageField,
|
||||
),
|
||||
).toBe('/path/to/collection/public/folder/image.png');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -236,7 +236,14 @@ export function selectMediaFolder<EF extends BaseField>(
|
||||
field: MediaField | undefined,
|
||||
currentFolder?: string,
|
||||
) {
|
||||
let mediaFolder = config['media_folder'] ?? '';
|
||||
let mediaFolder = folderFormatter(
|
||||
config.media_folder ?? '',
|
||||
entryMap,
|
||||
collection as Collection,
|
||||
config.media_folder ?? '',
|
||||
'media_folder',
|
||||
config.slug,
|
||||
);
|
||||
|
||||
if (currentFolder) {
|
||||
mediaFolder = currentFolder;
|
||||
@ -270,7 +277,15 @@ export function selectMediaFilePublicPath<EF extends BaseField>(
|
||||
return mediaPath;
|
||||
}
|
||||
|
||||
let publicFolder = config['public_folder']!;
|
||||
let publicFolder = folderFormatter(
|
||||
config.public_folder ?? '',
|
||||
entryMap,
|
||||
collection as Collection,
|
||||
config.public_folder ?? '',
|
||||
'public_folder',
|
||||
config.slug,
|
||||
);
|
||||
|
||||
let selectedPublicFolder = publicFolder;
|
||||
|
||||
const customPublicFolder = hasCustomFolder('public_folder', collection, entryMap?.slug, field);
|
||||
@ -288,11 +303,12 @@ export function selectMediaFilePublicPath<EF extends BaseField>(
|
||||
selectedPublicFolder = trim(currentFolder, '/').replace(trim(mediaFolder!, '/'), publicFolder);
|
||||
}
|
||||
|
||||
if (isAbsolutePath(selectedPublicFolder)) {
|
||||
return joinUrlPath(selectedPublicFolder, basename(mediaPath));
|
||||
const finalPublicPath = joinUrlPath(selectedPublicFolder, basename(mediaPath));
|
||||
if (selectedPublicFolder.startsWith('/')) {
|
||||
return `/${finalPublicPath}`;
|
||||
}
|
||||
|
||||
return joinUrlPath(selectedPublicFolder, basename(mediaPath));
|
||||
return finalPublicPath;
|
||||
}
|
||||
|
||||
export function selectMediaFilePath(
|
||||
|
@ -2,11 +2,11 @@ import trim from 'lodash/trim';
|
||||
import { basename, dirname, extname, join } from 'path';
|
||||
|
||||
import { sanitizeSlug } from '../urlHelper';
|
||||
import { stringTemplate } from '../widgets';
|
||||
import { selectEntryCollectionTitle, selectFolderEntryExtension } from './collection.util';
|
||||
import { isEmpty, isNotEmpty } from './string.util';
|
||||
import { stringTemplate } from '../widgets';
|
||||
|
||||
import type { BaseField, Collection, Config, Entry } from '@staticcms/core/interface';
|
||||
import type { BaseField, Collection, Entry, Slug } from '@staticcms/core/interface';
|
||||
|
||||
const { addFileTemplateFields } = stringTemplate;
|
||||
|
||||
@ -28,7 +28,7 @@ export function selectCustomPath(
|
||||
entry: Entry,
|
||||
collection: Collection,
|
||||
rootSlug: string | undefined,
|
||||
config: Config | undefined,
|
||||
slugConfig: Slug | undefined,
|
||||
): string | undefined {
|
||||
if (!('nested' in collection) || !collection.nested?.path) {
|
||||
return undefined;
|
||||
@ -37,7 +37,7 @@ export function selectCustomPath(
|
||||
const indexFile = collection.nested.path.index_file;
|
||||
const extension = selectFolderEntryExtension(collection);
|
||||
|
||||
const slug = entry.meta?.path ?? getNestedSlug(collection, entry, rootSlug, config);
|
||||
const slug = entry.meta?.path ?? getNestedSlug(collection, entry, rootSlug, slugConfig);
|
||||
|
||||
const customPath = join(collection.folder, slug, `${indexFile}.${extension}`);
|
||||
return customPath;
|
||||
@ -72,7 +72,7 @@ export function getNestedSlug(
|
||||
collection: Collection,
|
||||
entry: Entry,
|
||||
slug: string | undefined,
|
||||
config: Config | undefined,
|
||||
slugConfig: Slug | undefined,
|
||||
) {
|
||||
if ('nested' in collection && collection.nested?.path) {
|
||||
if (isNotEmpty(entry.slug)) {
|
||||
@ -85,7 +85,7 @@ export function getNestedSlug(
|
||||
|
||||
return `${customPathFromSlug(collection, slug)}/${sanitizeSlug(
|
||||
summarySlug.toLowerCase(),
|
||||
config?.slug,
|
||||
slugConfig,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ describe(RelationControl.name, () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
interface RenderRelationControlOptions {
|
||||
@ -653,7 +653,7 @@ describe(RelationControl.name, () => {
|
||||
});
|
||||
|
||||
describe('parse options', () => {
|
||||
fit('should default to valueField if displayFields is not set', async () => {
|
||||
it('should default to valueField if displayFields is not set', async () => {
|
||||
const field: RelationField = {
|
||||
label: 'Relation',
|
||||
name: 'relation',
|
||||
|
Loading…
x
Reference in New Issue
Block a user