diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..91169c45e453 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,42 @@ +# Copilot Instructions for Decap CMS Monorepo + +Use this guide to be productive quickly. Keep changes minimal and scoped to the relevant package. + +## Overview +- Monorepo under `packages/*` (core, app, backends, widgets, UI), orchestrated by Nx + Lerna; bundles via webpack in each package. +- Core domain lives in `packages/decap-cms-core` (collections, entries, formats, i18n, search, backend abstraction). +- The `Backend` class (`packages/decap-cms-core/src/backend.ts`) orchestrates entry CRUD, i18n grouping, media, search, and editorial workflow across provider backends. +- App bundle is `packages/decap-cms` (UMD browser build) used by the demo under `dev-test/`. + +## Build, Run, Test +- Dev watch all packages: `npm run develop` (Nx run-many; excludes `decap-server`). +- Build all: `npm run build` (ESM then package builds); preview: `npm run build-preview`. +- Unit tests: `npm run test:unit` (Jest, jsdom). Full CI set: `npm run test:ci`. +- E2E (Cypress): headless `npm run test:e2e`; interactive `npm run test:e2e:dev`. +- Demo site for manual testing: `npm run test:e2e:serve` then open `http://localhost:8080` (serves `dev-test/`). +- Lint/format: `npm run lint`, `npm run format`. Type-check: `npm run type-check`. + +## Core Hotspots (start here) +- Entry lifecycle: `decap-cms-core/src/backend.ts` – `processEntries`, `listEntries`, `listAllEntries`, `entryWithFormat`, `entryToRaw`, i18n helpers usage. +- Collection rules: `decap-cms-core/src/reducers/collections.*` – selectors like `selectEntryPath`, `selectFolderEntryExtension`, nested/index behavior. +- Formats: `decap-cms-core/src/formats/` – frontmatter/Markdown/YAML parsing/serialization. +- i18n: `decap-cms-core/src/lib/i18n.*` – `hasI18n`, `groupEntries`, `getI18nFilesDepth`, `I18N_STRUCTURE`. +- Registry: `decap-cms-core/src/lib/registry.*` – registers backends, widgets, media libraries. +- Build config: `scripts/webpack.js` – outputs (UMD/CJS), externals from `peerDependencies`, source maps. + +## Patterns & Conventions +- Immutable data: reducers/selectors use Immutable.js `Map/List`. Use selectors from `core/src/reducers/**` instead of direct shape access. +- Format I/O: always use `resolveFormat`, `entryWithFormat`, and `entryToRaw` for parsing/serialization. Don’t hand-roll frontmatter. +- i18n awareness: when listing/persisting, account for i18n files/folders via helpers (see `lib/i18n`). +- Backends contract: implementations in `packages/decap-cms-backend-*` must support methods used by `Backend` (e.g., `entriesByFolder|Files`, `getEntry`, `persistEntry`, `getMedia`, optional `allEntriesByFolder`, `getDeployPreview`, editorial workflow methods). Confirm call sites in `core/src/backend.ts`. +- Bundling: keep runtime deps vs `peerDependencies` correct; peers are externalized in UMD builds by `scripts/webpack.js`. + +## Integration Points +- Local git proxy for dev: `packages/decap-server` (`npx decap-server` in a site repo, then set `backend: name: proxy`). +- Cypress fixtures recording: see `cypress/Readme.md` and `mock:server:*` scripts; requires `.env` with provider tokens. + +## Practical Tips for Agents +- Scope changes to a single package unless a cross-package change is intentional. +- Preserve function signatures and Immutable types; convert to plain objects only where expected. +- If changing serialization or path/slug logic, update both read and write paths and run E2E. +- Feature-gate new backend capabilities and extend the interface used by `Backend` before implementing providers. diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index c998d645041d..9572b40c648e 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -521,9 +521,32 @@ export class Backend { }, ), ); - const formattedEntries = entries.map(this.entryWithFormat(collection)); - // If this collection has a "filter" property, filter entries accordingly + + let formattedEntries = entries.map(this.entryWithFormat(collection)); const collectionFilter = collection.get('filter'); + const i18nInfo = getI18nInfo(collection) as I18nInfo; + const isSingleFileI18n = i18nInfo && i18nInfo.structure === I18N_STRUCTURE.SINGLE_FILE; + + if (isSingleFileI18n && collectionFilter) { + // For each entry, dig into the default locale and copy the filter key to root level for filtering + const currentLocale = i18nInfo.defaultLocale; + const filterField = collectionFilter.get('field'); + + formattedEntries = formattedEntries.map(entry => { + if (!entry.data || typeof entry.data !== 'object') return entry; + const localeData = entry.data[currentLocale]; + if (!localeData || !(filterField in localeData)) return entry; + // Copy the filter field from localeData to root level + return { + ...entry, + data: { + ...entry.data, + [filterField]: localeData[filterField], + }, + }; + }); + } + const filteredEntries = collectionFilter ? this.filterEntries({ entries: formattedEntries }, collectionFilter) : formattedEntries;