Skip to content

feat: Index fields #7382

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

Draft
wants to merge 37 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
380bc60
feat(index-fields): allow defining own field schema for index files
demshy Jan 29, 2025
618b6ed
feat(index-file): add index file config options
demshy Jan 31, 2025
fc355a0
fix(index-file): check if indexFileConfix exists
demshy Feb 4, 2025
4c99ad1
feat(index-file): replace index file label with home icon
demshy Feb 4, 2025
db6b1b0
feat(index-fields): differentiate nested/non-nested collections
demshy Feb 4, 2025
a9e5828
Merge branch 'main' into feat/index-fields
demshy Feb 21, 2025
2552d93
feat(index-files): update decap-server to allow moving single entries…
demshy Mar 5, 2025
4df5976
feat(index-file): allow updating single entries in github backend
demshy Mar 5, 2025
b307868
feat(index-file): choose file type when adding entry, path handling f…
demshy Mar 5, 2025
991c208
feat(index-file): lint
demshy Mar 5, 2025
532d30d
feat(index-file): lint
demshy Mar 5, 2025
08c19a8
feat(index-file): remove logging
demshy Mar 5, 2025
01f6db2
fix: new path url
martinjagodic Mar 6, 2025
25d15f4
fix: function call
martinjagodic Mar 6, 2025
362ac96
fix: meta path preparing
martinjagodic Mar 6, 2025
3f1b3a3
Merge branch 'main' into feat/index-fields
demshy Mar 7, 2025
38a80b5
style: lint
demshy Mar 7, 2025
f32a871
feat(index-file): keep fileBaseName when editing leaf entries
demshy Mar 12, 2025
05b15c4
style: lint
demshy Mar 12, 2025
0a8af08
feat(index-files): editorBackLink only one level up in nested collect…
demshy Mar 12, 2025
b5dc663
feat(index-file): enable/disable editor preview for index file
demshy Mar 14, 2025
9f216b4
feat(index-file): add some types and a bit of logging
demshy Mar 14, 2025
72df729
feat(index-file): remove logging
demshy Mar 14, 2025
3b497a5
feat(index-page): path type names in dropdown
demshy Mar 14, 2025
573204a
feat(index-files): fix editorial workflow with the new nested collect…
demshy Apr 16, 2025
6d08159
style: lint code
demshy Apr 16, 2025
578dc4b
fix(index-fields): traverse index_file fields for i18n transformation…
demshy Jun 10, 2025
ec04105
feat(index-fields): paths
demshy Jun 13, 2025
a0a219d
feat(index-files): refactor customPAth
demshy Jun 13, 2025
b3b3c55
feat(indexFields): meta paths
demshy Jun 20, 2025
45de5cf
fix(indexFields): revert to original config.yml
demshy Jun 20, 2025
51ae2df
feat(index-fields): fix duplicating entries
demshy Jun 24, 2025
58a68ec
feat(indexFields): check both path types when validating metaPath
demshy Jun 24, 2025
edf033c
Merge branch 'main' into feat/index-fields
martinjagodic Jun 24, 2025
5338816
Merge branch 'main' into feat/index-fields
martinjagodic Jun 24, 2025
5f1ac72
chore: format
martinjagodic Jun 24, 2025
a6875d0
fix: remove duplicate param (merge error)
martinjagodic Jun 24, 2025
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
17 changes: 13 additions & 4 deletions packages/decap-cms-backend-github/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1391,32 +1391,41 @@ export default class API {

async updateTree(
baseSha: string,
files: { path: string; sha: string | null; newPath?: string }[],
files: { path: string; sha: string | null; newPath?: string; isFolder?: boolean }[],
branch = this.branch,
) {
const toMove: { from: string; to: string; sha: string }[] = [];
const toMove: { from: string; to: string; sha: string; isFolder?: boolean }[] = [];
const tree = files.reduce((acc, file) => {
const entry = {
path: trimStart(file.path, '/'),
mode: '100644',
type: 'blob',
sha: file.sha,
isFolder: file.isFolder,
} as TreeEntry;

if (file.newPath) {
toMove.push({ from: file.path, to: file.newPath, sha: file.sha as string });
toMove.push({
from: file.path,
to: file.newPath,
sha: file.sha as string,
isFolder: file.isFolder,
});
} else {
acc.push(entry);
}

return acc;
}, [] as TreeEntry[]);

for (const { from, to, sha } of toMove) {
for (const { from, to, sha, isFolder } of toMove) {
const sourceDir = dirname(from);
const destDir = dirname(to);
const files = await this.listFiles(sourceDir, { branch, depth: 100 });
for (const file of files) {
if (isFolder === false && file.path !== from) {
continue;
}
// delete current path
tree.push({
path: file.path,
Expand Down
6 changes: 6 additions & 0 deletions packages/decap-cms-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ declare module 'decap-cms-core' {
multiple?: boolean;
min?: number;
max?: number;
meta?: boolean;
}

export interface CmsFieldRelation {
Expand Down Expand Up @@ -334,6 +335,11 @@ declare module 'decap-cms-core' {
view_filters?: ViewFilter[];
view_groups?: ViewGroup[];
i18n?: boolean | CmsI18nConfig;
index_file?: {
pattern: string;
editor?: { preview?: boolean };
fields?: CmsField[];
};

/**
* @deprecated Use sortable_fields instead
Expand Down
51 changes: 44 additions & 7 deletions packages/decap-cms-core/src/actions/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import type {
CmsFieldBase,
CmsFieldObject,
CmsFieldList,
CmsFieldSelect,
CmsFieldMeta,
CmsI18nConfig,
CmsPublishMode,
CmsLocalBackend,
State,
CmsCollectionMeta,
} from '../types/redux';

export const CONFIG_REQUEST = 'CONFIG_REQUEST';
Expand Down Expand Up @@ -206,6 +209,39 @@ export function normalizeConfig(config: CmsConfig) {
return { ...config, collections: normalizedCollections };
}

function applyMetaFieldsToCollection(collection: CmsCollection, meta: CmsCollectionMeta) {
const metaFields: CmsFieldBase[] = [
{
name: 'path',
meta: true,
required: true,
i18n: 'duplicate',
default: '/',
...meta!.path,
} as CmsFieldMeta,
];

if (collection.index_file) {
metaFields.push({
name: 'path_type',
meta: true,
required: true,
widget: 'select',
readonly: true,
i18n: 'duplicate',
label: 'Path type',
options: ['index', 'slug'],
default: 'slug',
} as CmsFieldBase & CmsFieldSelect);
}

collection.fields = [...metaFields, ...(collection.fields || [])];

if (collection.index_file?.fields) {
collection.index_file.fields = [...metaFields, ...(collection.index_file.fields || [])];
}
}

export function applyDefaults(originalConfig: CmsConfig) {
return produce(originalConfig, config => {
config.publish_mode = config.publish_mode || SIMPLE_PUBLISH_MODE;
Expand Down Expand Up @@ -265,6 +301,13 @@ export function applyDefaults(originalConfig: CmsConfig) {
collection.fields = setI18nDefaultsForFields(collection.fields, Boolean(collectionI18n));
}

if (collection.index_file?.fields) {
collection.index_file.fields = setI18nDefaultsForFields(
collection.index_file.fields,
Boolean(collectionI18n),
);
}

const { folder, files, view_filters, view_groups, meta } = collection;

if (folder) {
Expand All @@ -286,13 +329,7 @@ export function applyDefaults(originalConfig: CmsConfig) {
collection.folder = trim(folder, '/');

if (meta && meta.path) {
const metaField = {
name: 'path',
meta: true,
required: true,
...meta.path,
};
collection.fields = [metaField, ...(collection.fields || [])];
applyMetaFieldsToCollection(collection, meta);
}
}

Expand Down
27 changes: 20 additions & 7 deletions packages/decap-cms-core/src/actions/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import { SortDirection } from '../types/redux';
import { waitForMediaLibraryToLoad, loadMedia } from './mediaLibrary';
import { waitUntil } from './waitUntil';
import { selectIsFetching, selectEntriesSortFields, selectEntryByPath } from '../reducers/entries';
import { selectCustomPath } from '../reducers/entryDraft';
import { navigateToEntry } from '../routing/history';
import { getProcessSegment } from '../lib/formatters';
import { hasI18n, duplicateDefaultI18nFields, serializeI18n, I18N, I18N_FIELD } from '../lib/i18n';
import { addNotification } from './notifications';
import { selectCustomPath } from '../reducers/entryDraft';

import type { ImplementationMediaFile } from 'decap-cms-lib-util';
import type { AnyAction } from 'redux';
Expand All @@ -34,6 +34,7 @@ import type {
ViewFilter,
ViewGroup,
Entry,
Entries,
} from '../types/redux';
import type { EntryValue } from '../valueObjects/Entry';
import type { Backend } from '../backend';
Expand Down Expand Up @@ -389,7 +390,8 @@ export function draftDuplicateEntry(entry: EntryMap) {
type: DRAFT_CREATE_DUPLICATE_FROM_ENTRY,
payload: createEntry(entry.get('collection'), '', '', {
data: entry.get('data'),
i18n: entry.get('i18n'),
meta: entry.get('meta').toJS(),
i18n: entry.get('i18n').toJS(),
mediaFiles: entry.get('mediaFiles').toJS(),
}),
};
Expand Down Expand Up @@ -1009,6 +1011,19 @@ function getPathError(
};
}

function getExistingEntry(
entries: Entries,
collection: Collection,
path: string,
path_type: string,
) {
const customPath = selectCustomPath(collection, fromJS({ entry: { meta: { path, path_type } } }));
if (!customPath) {
return undefined;
}
return selectEntryByPath(entries, collection.get('name'), customPath);
}

export function validateMetaField(
state: State,
collection: Collection,
Expand All @@ -1029,11 +1044,9 @@ export function validateMetaField(
return getPathError(value, 'invalidPath', t);
}

const customPath = selectCustomPath(collection, fromJS({ entry: { meta: { path: value } } }));
const existingEntry = customPath
? selectEntryByPath(state.entries, collection.get('name'), customPath)
: undefined;

const existingEntry =
getExistingEntry(state.entries, collection, value, 'index') ??
getExistingEntry(state.entries, collection, value, 'slug');
const existingEntryPath = existingEntry?.get('path');
const draftPath = state.entryDraft?.getIn(['entry', 'path']);

Expand Down
65 changes: 59 additions & 6 deletions packages/decap-cms-core/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
selectMediaFolders,
selectFieldsComments,
selectHasMetaPath,
isNestedSubfolders,
isNested,
} from './reducers/collections';
import { createEntry } from './valueObjects/Entry';
import { sanitizeChar } from './lib/urlHelper';
Expand Down Expand Up @@ -297,14 +299,40 @@ type Implementation = BackendImplementation & {
init: (config: CmsConfig, options: ImplementationInitOptions) => Implementation;
};

function prepareMetaPath(path: string, collection: Collection) {
function prepareMetaPath(path: string, collection: Collection, slug?: string) {
if (!selectHasMetaPath(collection)) {
return path;
}

if (
slug &&
isNested(collection) &&
!isNestedSubfolders(collection) &&
prepareMetaPathType(slug, collection) !== 'index'
) {
return slug;
}

const dir = dirname(path);
return dir.slice(collection.get('folder')!.length + 1) || '/';
}

function isIndexFile(filePath: string, pattern: string, nested: boolean) {
const fileSlug = nested ? filePath?.split('/').pop() : filePath;
return fileSlug && new RegExp(pattern).test(fileSlug);
}

function prepareMetaPathType(slug: string, collection: Collection) {
const indexFileConfig = collection.get('index_file');
if (
indexFileConfig &&
isIndexFile(slug, indexFileConfig.get('pattern'), !!collection.get('nested'))
) {
return 'index';
}
return 'slug';
}

function collectionDepth(collection: Collection) {
let depth;
depth =
Expand Down Expand Up @@ -517,7 +545,13 @@ export class Backend {
label: loadedEntry.file.label,
author: loadedEntry.file.author,
updatedOn: loadedEntry.file.updatedOn,
meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
meta: {
path: prepareMetaPath(
loadedEntry.file.path,
collection,
selectEntrySlug(collection, loadedEntry.file.path),
),
},
},
),
);
Expand Down Expand Up @@ -744,7 +778,7 @@ export class Backend {
raw,
label,
mediaFiles,
meta: { path: prepareMetaPath(path, collection) },
meta: { path: prepareMetaPath(path, collection, slug) },
}),
);
};
Expand Down Expand Up @@ -830,11 +864,16 @@ export class Backend {

const getEntryValue = async (path: string) => {
const loadedEntry = await this.implementation.getEntry(path);
let entry = createEntry(collection.get('name'), slug, loadedEntry.file.path, {
const entryPath = loadedEntry.file.path;

let entry = createEntry(collection.get('name'), slug, entryPath, {
raw: loadedEntry.data,
label,
mediaFiles: [],
meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
meta: {
path: prepareMetaPath(entryPath, collection, slug),
path_type: prepareMetaPathType(slug, collection),
},
});

entry = this.entryWithFormat(collection)(entry);
Expand Down Expand Up @@ -928,7 +967,10 @@ export class Backend {
updatedOn: entryData.updatedAt,
author: entryData.pullRequestAuthor,
status: entryData.status,
meta: { path: prepareMetaPath(path, collection) },
meta: {
path: prepareMetaPath(path, collection, slug),
path_type: prepareMetaPathType(slug, collection),
},
});

const entryWithFormat = this.entryWithFormat(collection)(entry);
Expand Down Expand Up @@ -961,6 +1003,9 @@ export class Backend {
);
entries = entries.filter(Boolean);
const grouped = await groupEntries(collection, extension, entries as EntryValue[]);
if (grouped[0]?.srcSlug) {
grouped[0].slug = grouped[0].srcSlug;
}
return grouped[0];
} else {
const entryWithFormat = await readAndFormatDataFile(dataFiles[0]);
Expand Down Expand Up @@ -1114,6 +1159,9 @@ export class Backend {
const customPath = selectCustomPath(collection, entryDraft);

let dataFile: DataFile;

let isFolder = true;

if (newEntry) {
if (!selectAllowNewEntries(collection)) {
throw new Error('Not allowed to create new entries in this collection');
Expand All @@ -1125,6 +1173,7 @@ export class Backend {
usedSlugs,
customPath,
);
isFolder = prepareMetaPathType(slug, collection) === 'index';
const path = customPath || (selectEntryPath(collection, slug) as string);
dataFile = {
path,
Expand All @@ -1135,13 +1184,16 @@ export class Backend {
updateAssetProxies(assetProxies, config, collection, entryDraft, path);
} else {
const slug = entryDraft.getIn(['entry', 'slug']);
isFolder = prepareMetaPathType(slug, collection) === 'index';
const path = entryDraft.getIn(['entry', 'path']);

dataFile = {
path,
// for workflow entries we refresh the slug on publish
slug: customPath && !useWorkflow ? slugFromCustomPath(collection, customPath) : slug,
raw: this.entryToRaw(collection, entryDraft.get('entry')),
newPath: customPath === path ? undefined : customPath,
isFolder,
};
}

Expand All @@ -1158,6 +1210,7 @@ export class Backend {
path,
slug,
newPath,
isFolder,
);
}

Expand Down
Loading
Loading