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

feat: Index fields #7382

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,501 changes: 1,223 additions & 278 deletions dev-test/config.yml

Large diffs are not rendered by default.

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 @@ -1386,32 +1386,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
5 changes: 5 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 @@ -332,6 +333,10 @@ declare module 'decap-cms-core' {
view_filters?: ViewFilter[];
view_groups?: ViewGroup[];
i18n?: boolean | CmsI18nConfig;
index_file?: {
pattern: string;
fields?: CmsField[];
};

/**
* @deprecated Use sortable_fields instead
Expand Down
40 changes: 33 additions & 7 deletions packages/decap-cms-core/src/actions/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,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 @@ -204,6 +207,35 @@ export function normalizeConfig(config: CmsConfig) {
return { ...config, collections: normalizedCollections };
}

function applyMetaFieldsToCollection(collection: CmsCollection, meta: CmsCollectionMeta) {
const metaFields = [
{
name: 'path',
meta: true,
required: true,
i18n: 'duplicate',
...meta!.path,
} as CmsFieldMeta,
{
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 @@ -284,13 +316,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
25 changes: 14 additions & 11 deletions packages/decap-cms-core/src/actions/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { addAssets, getAsset } from './media';
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 { selectIsFetching, selectEntriesSortFields } from '../reducers/entries';
import { navigateToEntry } from '../routing/history';
import { getProcessSegment } from '../lib/formatters';
import { hasI18n, duplicateDefaultI18nFields, serializeI18n, I18N, I18N_FIELD } from '../lib/i18n';
Expand Down Expand Up @@ -1028,17 +1027,21 @@ 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;
console.log(collection);

const existingEntryPath = existingEntry?.get('path');
const draftPath = state.entryDraft?.getIn(['entry', 'path']);
// update path validation

if (existingEntryPath && existingEntryPath !== draftPath) {
return getPathError(value, 'pathExists', t);
}
// const customPath = selectCustomPath(collection, fromJS({ entry: { meta: { path: value } } }), state.config);
// const existingEntry = customPath
// ? selectEntryByPath(state.entries, collection.get('name'), customPath)
// : undefined;

// const existingEntryPath = existingEntry?.get('path');
// const draftPath = state.entryDraft?.getIn(['entry', 'path']);

// if (existingEntryPath && existingEntryPath !== draftPath) {
// return getPathError(value, 'pathExists', t);
// }
}
return { error: false };
}
42 changes: 39 additions & 3 deletions packages/decap-cms-core/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,22 @@ function prepareMetaPath(path: string, collection: Collection) {
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 @@ -823,11 +839,24 @@ 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;
const path_type = prepareMetaPathType(slug, collection);

let metaPath = entryPath;
if (path_type === 'index') {
const pathArr = dirname(entryPath).split('/').slice(0, -1);
pathArr.push(basename(entryPath));
metaPath = pathArr.join('/');
}

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

entry = this.entryWithFormat(collection)(entry);
Expand Down Expand Up @@ -1104,9 +1133,12 @@ export class Backend {

const useWorkflow = selectUseWorkflow(config);

const customPath = selectCustomPath(collection, entryDraft);
const customPath = selectCustomPath(collection, entryDraft, config);

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 @@ -1118,6 +1150,7 @@ export class Backend {
usedSlugs,
customPath,
);
isFolder = prepareMetaPathType(slug, collection) === 'index';
const path = customPath || (selectEntryPath(collection, slug) as string);
dataFile = {
path,
Expand All @@ -1128,13 +1161,15 @@ export class Backend {
updateAssetProxies(assetProxies, config, collection, entryDraft, path);
} else {
const slug = entryDraft.getIn(['entry', 'slug']);
const 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 @@ -1151,6 +1186,7 @@ export class Backend {
path,
slug,
newPath,
isFolder,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import React from 'react';
import styled from '@emotion/styled';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom';
import { components, buttons, shadows } from 'decap-cms-ui-default';
import {
Dropdown,
DropdownItem,
StyledDropdownButton,
components,
buttons,
shadows,
} from 'decap-cms-ui-default';
import { createHashHistory } from 'history';

const CollectionTopContainer = styled.div`
${components.cardTop};
Expand All @@ -30,6 +38,15 @@ const CollectionTopNewButton = styled(Link)`
padding: 0 30px;
`;

const CollectionTopDropdownButton = styled(StyledDropdownButton)`
${buttons.button};
${shadows.dropDeep};
${buttons.default};
${buttons.gray};

padding: 0 30px 0 15px;
`;

const CollectionTopDescription = styled.p`
${components.cardTopDescription};
margin-bottom: 0;
Expand All @@ -47,17 +64,43 @@ function getCollectionProps(collection) {
};
}

const history = createHashHistory();

function CollectionTop({ collection, newEntryUrl, t }) {
const { collectionLabel, collectionLabelSingular, collectionDescription } = getCollectionProps(
collection,
t,
);

const indexFileConfig = collection.get('index_file');
// const indexFile = get(collection.toJS(), ['meta', 'path', 'index_file'])

function handleNew(pathType) {
const delimiter = newEntryUrl.includes('?') ? '&' : '?';
history.push(`${newEntryUrl}${delimiter}path_type=${pathType}`)
}

return (
<CollectionTopContainer>
<CollectionTopRow>
<CollectionTopHeading>{collectionLabel}</CollectionTopHeading>
{newEntryUrl ? (
{indexFileConfig && collection.get('nested') ? (
<Dropdown
renderButton={() => (
<CollectionTopDropdownButton>
{t('collection.collectionTop.newButton', {
collectionLabel: collectionLabelSingular || collectionLabel,
})}
</CollectionTopDropdownButton>
)}
dropdownTopOverlap="30px"
dropdownWidth="160px"
dropdownPosition="left"
>
<DropdownItem key={'_index'} label={`Index page`} onClick={() => handleNew('index')} />
<DropdownItem key={'{{slug}}'} label={'Leaf page'} onClick={() => handleNew('slug')} />
</Dropdown>
) : newEntryUrl ? (
<CollectionTopNewButton to={newEntryUrl}>
{t('collection.collectionTop.newButton', {
collectionLabel: collectionLabelSingular || collectionLabel,
Expand Down
Loading
Loading