Skip to content

Conversation

@sapayth
Copy link
Member

@sapayth sapayth commented Sep 22, 2025

fixes #132

added a Duplicate option for single docs only. The duplicate functionality will clone a doc with it's child (if exists). the duplicated docs status will be draft.

CleanShot 2025-09-22 at 12 43 01

Summary by CodeRabbit

  • New Features

    • Admins can duplicate an article (and its entire subtree) from the action menu.
    • Duplicates preserve content, hierarchy, metadata, taxonomy, and featured images.
    • After duplication, the article list refreshes automatically and remains visible.
    • Success and error notifications provide immediate feedback.
  • UI/UX

    • Added a “Duplicate” option to article actions for administrators.

@coderabbitai
Copy link

coderabbitai bot commented Sep 22, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds a document duplication REST endpoint and recursive copy logic; integrates a new duplicate action in the UI and data layer. Updates built assets: PHP asset manifests (version hashes, opening tags), CSS (comments/source maps), JS bundles (webpack runtime). Overhauls built store.js into modular docs/settings stores.

Changes

Cohort / File(s) Summary of changes
Asset manifests (PHP tags + version bumps)
assets/build/block.asset.php, assets/build/frontend.asset.php, assets/build/index.asset.php, assets/build/print.asset.php, assets/build/store.asset.php
Added PHP opening tags where missing; updated version hashes; dependencies arrays unchanged.
Built CSS (comments and source maps)
assets/build/frontend.css, assets/build/index.css, assets/build/print.css, assets/build/style-block.css
Added loader-generated header comments and sourceMappingURL references; reformatted/expanded CSS output; no selector/value changes.
Built JS runtime additions
assets/build/frontend.js, assets/build/print.js
Added webpack bootstrap/runtime including webpack_require.r to mark ES modules; noted CSS extraction and source maps.
Data store (built bundle refactor)
assets/build/store.js
Replaced monolithic store with modular docs and settings stores, adding actions, reducers, selectors, resolvers, and controls; registers public stores.
API: document duplication
includes/API/API.php
Added REST route /docs/{id}/duplicate; implemented duplicate_doc and private duplicate_document_recursive to deep-copy posts, meta, terms, thumbnail, and children.
UI and source data actions
src/components/DocActions.js, src/data/docs/actions.js
Added Duplicate menu action for articles (admin); implemented *duplicateDoc(docId) generator to call API and refresh docs tree; added notifications and refresh handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

QA Approved, Ready To Merge

Suggested reviewers

  • iftakharul-islam

Poem

A hop, a skip, I copy trees of docs,
With whiskers twitching through recursive blocks.
A click—Duplicate!—and voilà, twins bloom,
The stash refreshed, no hint of gloom.
Hashes turn, maps align, bundles sing—
A rabbit’s merge, a springy thing. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning The PR is linked to issue [#132], which requests that the search widget include article tags, but the changes in this PR implement a docs duplicator (new duplicate REST route and recursive copy in includes/API/API.php, UI Duplicate action in src/components/DocActions.js, and new store actions/resolvers in src/data/docs) and contain no modifications to the search widget or search/tag query logic; therefore the PR does not satisfy the coding requirements of [#132]. Resolution: Remove the reference to issue #132 or extend the PR to implement the tag-in-search changes required by [#132] (update the search query/widget to include tags and add tests or examples demonstrating tag-based search results).
Out of Scope Changes Check ⚠️ Warning Several changes in this PR are unrelated to the linked issue's objective (include tags in search): the addition of a duplication REST API and recursive duplicator, UI/menu changes to trigger duplication, a large refactor/modularization of the client store, and numerous built asset/version updates are all outside the single search-related objective, so the PR contains out-of-scope changes relative to the linked issue. Resolution: Split the duplication feature, store refactor, and build-asset updates into a dedicated, focused PR and keep the search/tag-change work in a separate PR that directly implements and tests the tag-in-search behavior, or update the linked issue to match the actual scope of this PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat: docs duplicator” clearly and concisely identifies the primary change of adding a document duplication feature, follows conventional commit style, and avoids unnecessary detail or noise, making it easy for teammates to grasp the main enhancement at a glance.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (13)
assets/build/print.css (1)

1-3: Sourcemap linkage looks consistent.

No style changes; only header and map ref added. Same advice as frontend.css regarding shipping maps vs stripping in prod.

Also applies to: 185-186

assets/build/style-block.css (3)

53-55: Duplicate width is intentional but triggers linter.

width: -moz-fit-content; width: fit-content; is a vendor fallback. If Biome/linters flag it, suppress in the SCSS source or drop the -moz- variant if you don’t support legacy Firefox.


141-141: @charset must be first; move or remove.

@charset "UTF-8"; mid-file is ignored per spec. Prefer removing it from generated CSS or ensuring the toolchain places it at the very top.

-@charset "UTF-8";
+/* charset handled by bundler; removed from mid-file */

319-323: Selector nit: tighten ::marker selector.

Use li::marker (no space) for clarity.

-.wedocs-docs-grid__articles li ::marker {
+.wedocs-docs-grid__articles li::marker {
src/data/docs/actions.js (1)

173-190: DRY the refresh path by reusing updateParentDocs().

You’re repeating the fetch/sort/update flow already implemented in updateParentDocs().

   const response = yield { type: 'UPDATE_TO_API', path, data: {} };
-  // Refresh the docs list after duplication
-  const getDocsPath = wp.hooks.applyFilters(
-    'wedocs_documentation_fetching_path',
-    `/wp/v2/docs?per_page=-1&status=publish${ typeof weDocsAdminVars !== 'undefined' ? ',draft' : ''}`
-  );
-  const updatedDocs = yield actions.fetchFromAPI( getDocsPath );
-  const parentDocs = updatedDocs.filter( ( doc ) => ! doc.parent );
-  const sortableDocs = parentDocs?.sort( ( a, b ) => a.menu_order - b.menu_order );
-
-  yield actions.setParentDocs( sortableDocs );
-  yield actions.setDocs( updatedDocs );
+  // Refresh the docs list after duplication
+  yield actions.updateParentDocs();
 
   return response;
src/components/DocActions.js (1)

46-75: Polish: contextualize toast copy and keep UX consistent.

Make success/error text reflect the current item type (doc vs article).

- title             : __( 'Documentation duplicated successfully!', 'wedocs' ),
+ title             : __( `${ type === 'doc' ? 'Documentation' : 'Article' } duplicated successfully!`, 'wedocs' ),
 ...
- text              : __( 'The document and all its child articles have been duplicated as drafts.', 'wedocs' ),
+ text              : __( `${ type === 'doc' ? 'The documentation' : 'The article' } and its child items have been duplicated as drafts.`, 'wedocs' ),

Also, this PR’s description says “Adds a Duplicate option for single docs only,” but the menu shows it only when type === 'article'. Confirm intended scope and adjust the condition if needed.

includes/API/API.php (3)

957-989: Prefer existing get_doc() and return WP core shape consistently.

Use get_doc() for validation (consistent error codes), then permission check can also rely on edit_post for that ID. Optional.


1001-1060: Safer duplication: i18n title suffix, skip ephemeral meta, consider menu_order.

  • Title suffix should be translatable.
  • Avoid copying ephemeral meta like _edit_lock/_edit_last.
  • Consider placing the duplicate at the end of siblings to avoid menu_order collisions.
- 'post_title'     => $original_doc->post_title . ' (Copy)',
+ 'post_title'     => sprintf( __( '%s (Copy)', 'wedocs' ), $original_doc->post_title ),

Example meta filter:

$skip_meta = [ '_edit_lock', '_edit_last' ];
foreach ( $meta_data as $key => $values ) {
    if ( in_array( $key, $skip_meta, true ) ) { continue; }
    foreach ( $values as $value ) {
        add_post_meta( $new_doc_id, $key, maybe_unserialize( $value ) );
    }
}

Optional ordering:

// Place at end if same parent to avoid tie with menu_order
if ( $new_post_data['post_parent'] ) {
    $siblings = get_children([ 'post_parent' => $new_post_data['post_parent'], 'post_type' => 'docs', 'fields' => 'ids' ]);
    $new_post_data['menu_order'] = is_array( $siblings ) ? count( $siblings ) : 0;
}

960-962: Docblocks: replace placeholder since tag.

Replace “@SInCE WEDOCS_SINCE” with the actual next version.

Also applies to: 992-999

assets/build/store.js (4)

349-358: Deduplicate userDocIds to avoid unbounded growth and repeats.

Merging arrays without a set can introduce duplicates.

     case 'SET_USER_DOC_IDS':
       return {
         ...state,
-        userDocIds: [...state.userDocIds, ...action.userDocIds]
+        userDocIds: Array.from(new Set([...state.userDocIds, ...action.userDocIds]))
       };
     case 'SET_USER_DOC_ID':
       return {
         ...state,
-        userDocIds: [...state.userDocIds, action.userDocId]
+        userDocIds: state.userDocIds.includes(action.userDocId)
+          ? state.userDocIds
+          : [...state.userDocIds, action.userDocId]
       };

506-511: Selector may miss matches due to id type mismatch.

Route/query params are often strings; coerce before comparison.

-  getDoc: (state, id) => {
+  getDoc: (state, id) => {
     const {
       docs
     } = state;
-    return docs.find(doc => doc.id === id);
+    const targetId = typeof id === 'string' ? Number(id) : id;
+    return docs.find(doc => doc.id === targetId);
   },

461-467: Avoid in-place sort side effects by sorting a copy.

Defensive copy improves readability and prevents accidental shared mutations.

-    const helpfulDocs = docs.sort((a, b) => helpfulDocIds.indexOf(a.id) - helpfulDocIds.indexOf(b.id)).filter(doc => helpfulDocIds?.includes(doc?.id));
+    const helpfulDocs = [...docs]
+      .sort((a, b) => helpfulDocIds.indexOf(a.id) - helpfulDocIds.indexOf(b.id))
+      .filter(doc => helpfulDocIds?.includes(doc?.id));

573-580: Naming: getArticleChildrensgetArticleChildren (keep a compat alias).

Public API polish; the current name is grammatically incorrect. Add a new selector and keep the old as an alias to avoid breaks.

   getArticleChildrens: (state, id) => {
     const {
       docs
     } = state;
     const childrens = docs?.filter(doc => doc.parent === id)?.reverse();
     const sortableChildrens = childrens?.sort((a, b) => a.menu_order - b.menu_order);
     return sortableChildrens;
   },
+  // Back-compat alias with corrected name
+  getArticleChildren: (state, id) => {
+    const { docs } = state;
+    const children = docs?.filter(doc => doc.parent === id)?.reverse();
+    return children?.sort((a, b) => a.menu_order - b.menu_order);
+  },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c80fc84 and 6632d1b.

⛔ Files ignored due to path filters (9)
  • assets/build/block.js.map is excluded by !**/*.map
  • assets/build/frontend.css.map is excluded by !**/*.map
  • assets/build/frontend.js.map is excluded by !**/*.map
  • assets/build/index.css.map is excluded by !**/*.map
  • assets/build/index.js.map is excluded by !**/*.map
  • assets/build/print.css.map is excluded by !**/*.map
  • assets/build/print.js.map is excluded by !**/*.map
  • assets/build/store.js.map is excluded by !**/*.map
  • assets/build/style-block.css.map is excluded by !**/*.map
📒 Files selected for processing (15)
  • assets/build/block.asset.php (1 hunks)
  • assets/build/frontend.asset.php (1 hunks)
  • assets/build/frontend.css (2 hunks)
  • assets/build/frontend.js (1 hunks)
  • assets/build/index.asset.php (1 hunks)
  • assets/build/index.css (2 hunks)
  • assets/build/print.asset.php (1 hunks)
  • assets/build/print.css (2 hunks)
  • assets/build/print.js (1 hunks)
  • assets/build/store.asset.php (1 hunks)
  • assets/build/store.js (1 hunks)
  • assets/build/style-block.css (1 hunks)
  • includes/API/API.php (4 hunks)
  • src/components/DocActions.js (2 hunks)
  • src/data/docs/actions.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/DocActions.js (3)
assets/build/index.js (1)
  • Swal (32002-32002)
src/components/Documentations/ParentDocs.js (1)
  • isAdmin (19-19)
src/components/DocListing/DocSections.js (1)
  • isAdmin (22-22)
src/data/docs/actions.js (3)
src/data/docs/resolvers.js (9)
  • response (11-11)
  • response (24-24)
  • response (30-30)
  • response (35-35)
  • response (40-40)
  • response (63-63)
  • getDocsPath (3-6)
  • parentDocs (13-13)
  • sortableDocs (14-16)
src/blocks/DocsGrid/edit.js (1)
  • wp (16-16)
src/components/Documentations/index.js (1)
  • parentDocs (24-27)
assets/build/store.js (5)
src/components/DocListing/index.js (3)
  • docs (27-29)
  • loading (37-40)
  • sections (103-103)
src/components/Documentations/index.js (3)
  • docs (19-22)
  • loading (29-32)
  • parentDocs (24-27)
src/components/SelectBox.js (1)
  • pages (16-16)
src/components/DocListing/ListingHeader.js (1)
  • loading (13-16)
src/components/Documentations/ParentDocs.js (2)
  • sections (36-40)
  • articles (42-46)
🪛 Biome (2.1.2)
assets/build/index.css

[error] 5851-5851: Unexpected value or character.

Expected one of:

(parse)


[error] 5851-5851: Unexpected value or character.

Expected one of:

(parse)


[error] 5851-5851: Unexpected value or character.

Expected one of:

(parse)


[error] 5851-5851: Unexpected value or character.

Expected one of:

(parse)


[error] 5851-5851: Unexpected value or character.

Expected one of:

(parse)


[error] 5851-5851: Unexpected shorthand property flex-flow after flex-direction

(lint/suspicious/noShorthandPropertyOverrides)


[error] 5851-5851: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

display is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)


[error] 5851-5851: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

display is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)


[error] 5851-5851: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

transition is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

assets/build/style-block.css

[error] 54-54: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

width is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

assets/build/store.js

[error] 338-341: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 342-342: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 392-392: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)


[error] 393-393: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)

🔇 Additional comments (15)
assets/build/block.asset.php (1)

1-1: Version bump and PHP open tag OK.

Cache-busting string updated; deps unchanged. Looks good.

assets/build/style-block.css (1)

124-134: CSS var theming: nice.

Good use of CSS custom properties for button/icon states; improves block theming.

assets/build/frontend.js (1)

1-28: ESM flag runtime added: OK.

Adding __webpack_require__.r and marking exports as ESM is standard with mini-css-extract output; no behavior change for consumers.

assets/build/index.asset.php (1)

1-1: Asset version updated.

Deps unchanged; version bump aligns with new build.

assets/build/print.asset.php (1)

1-1: Version bump only.

All good.

assets/build/store.asset.php (1)

1-1: Store asset manifest updated.

Deps remain wp-api-fetch, wp-data; version string updated—OK.

assets/build/frontend.css (1)

1-3: Sourcemaps present — include in releases or strip sourceMappingURL in prod. assets/build contains frontend.css.map (45K), style-block.css.map (16K), print.css.map (5.5K); sizes reasonable.

assets/build/frontend.asset.php (1)

1-1: LGTM: manifest header and version bump align with build output.

assets/build/print.js (1)

1-28: LGTM: standard webpack runtime wrapper; no functional risks.

src/components/DocActions.js (1)

131-140: Verify gating matches the product decision (“single docs only”).

Currently shown only for articles:

  • If duplication should be available for top‑level docs, gate on type === 'doc' (or both).
assets/build/index.css (1)

5848-5854: Exclude vendor/minified CSS from Biome/style linting (assets/build/)**

Minified react-responsive-carousel CSS in assets/build/index.css triggers Biome parse/duplicate‑prop warnings — exclude assets/build/** from Biome/style linting and retain third‑party license headers. Verified: sourcemap found at /home/jailuser/git/assets/build/index.css.map (stat unavailable in sandbox).

assets/build/store.js (4)

1-4: Linked issue mismatch with PR scope.

This PR implements document duplication, but the linked issue (#132) is “Include tags in search.” Update the PR linkage/description or add a separate issue to avoid confusion in release notes and triage.


1239-1241: Store registration LGTM.

Registering modular stores via createReduxStore + register is correct and aligns with WP data conventions.


115-116: Confirm WP REST per_page=-1 support and add a paginated-fetch fallback

per_page=-1 is commonly capped by WP core (commonly 100) and may be ignored, causing truncated results. Implement a paginated fetch that requests pages 1..N and aggregates results using the X-WP-TotalPages response header, or ensure the server explicitly supports per_page=-1.

Locations: assets/build/store.js — lines 115–116 (also review 184–189 and 427–437).

Verification note: attempted ripgrep search returned no files (filter excluded assets/**). Re-run search including assets or manually confirm usage and implement pagination/fallback.


200-213: Confirm REST endpoint for promotion notice dismissal exists and is permissioned.

Just a heads-up to verify server routes and caps (/wp/v2/docs/hide-promotion-notice) for non-admin users don’t leak capability.

Comment on lines +214 to +230
*duplicateDoc(docId) {
const path = `/wp/v2/docs/${docId}/duplicate`;
const response = yield {
type: 'UPDATE_TO_API',
path,
data: {}
};

// Refresh the docs list after duplication
const getDocsPath = wp.hooks.applyFilters('wedocs_documentation_fetching_path', `/wp/v2/docs?per_page=-1&status=publish${typeof weDocsAdminVars !== 'undefined' ? ',draft' : ''}`);
const updatedDocs = yield actions.fetchFromAPI(getDocsPath);
const parentDocs = updatedDocs.filter(doc => !doc.parent);
const sortableDocs = parentDocs?.sort((a, b) => a.menu_order - b.menu_order);
yield actions.setParentDocs(sortableDocs);
yield actions.setDocs(updatedDocs);
return response;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate action: add loading + DRY the refresh via existing action; handle errors.

Use the existing updateParentDocs() action to refresh, toggle loading, and ensure cleanup even on failure.

   *duplicateDoc(docId) {
     const path = `/wp/v2/docs/${docId}/duplicate`;
-    const response = yield {
-      type: 'UPDATE_TO_API',
-      path,
-      data: {}
-    };
-
-    // Refresh the docs list after duplication
-    const getDocsPath = wp.hooks.applyFilters('wedocs_documentation_fetching_path', `/wp/v2/docs?per_page=-1&status=publish${typeof weDocsAdminVars !== 'undefined' ? ',draft' : ''}`);
-    const updatedDocs = yield actions.fetchFromAPI(getDocsPath);
-    const parentDocs = updatedDocs.filter(doc => !doc.parent);
-    const sortableDocs = parentDocs?.sort((a, b) => a.menu_order - b.menu_order);
-    yield actions.setParentDocs(sortableDocs);
-    yield actions.setDocs(updatedDocs);
-    return response;
+    yield actions.setLoading(true);
+    try {
+      const response = yield {
+        type: 'UPDATE_TO_API',
+        path,
+        data: {}
+      };
+      yield actions.updateParentDocs();
+      return response;
+    } finally {
+      yield actions.setLoading(false);
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
*duplicateDoc(docId) {
const path = `/wp/v2/docs/${docId}/duplicate`;
const response = yield {
type: 'UPDATE_TO_API',
path,
data: {}
};
// Refresh the docs list after duplication
const getDocsPath = wp.hooks.applyFilters('wedocs_documentation_fetching_path', `/wp/v2/docs?per_page=-1&status=publish${typeof weDocsAdminVars !== 'undefined' ? ',draft' : ''}`);
const updatedDocs = yield actions.fetchFromAPI(getDocsPath);
const parentDocs = updatedDocs.filter(doc => !doc.parent);
const sortableDocs = parentDocs?.sort((a, b) => a.menu_order - b.menu_order);
yield actions.setParentDocs(sortableDocs);
yield actions.setDocs(updatedDocs);
return response;
}
*duplicateDoc(docId) {
const path = `/wp/v2/docs/${docId}/duplicate`;
yield actions.setLoading(true);
try {
const response = yield {
type: 'UPDATE_TO_API',
path,
data: {}
};
yield actions.updateParentDocs();
return response;
} finally {
yield actions.setLoading(false);
}
}
🤖 Prompt for AI Agents
In assets/build/store.js around lines 214 to 230, the duplicateDoc action
duplicates a doc then manually refreshes and sorts docs without using the
existing updateParentDocs() helper, lacks loading state toggling and does not
handle errors/cleanup; replace the manual refresh with a call to
updateParentDocs() (which updates parent and child docs), wrap the operation in
a try/catch/finally to set a loading flag before the API call and clear it in
finally, log or rethrow any caught error, and return the response—this DRYs the
refresh logic, ensures loading is shown during the op, and guarantees cleanup on
failure.

Comment on lines +338 to +348
const setDocState = {
...state,
docs: [...state.docs, action.doc]
};
const isNotInParent = !state.parents.some(parent => parent?.id === action?.doc?.id);
if (!action.doc.parent && isNotInParent) {
setDocState.parents = [{
...action.doc
}, ...state.parents];
}
return setDocState;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Switch-case declarations must be wrapped; also upsert instead of append to avoid duplicate docs.

Avoid cross‑case leakage (Biome: noSwitchDeclarations) and prevent duplicate entries when the same doc is set twice.

-    case 'SET_DOC':
-      const setDocState = {
-        ...state,
-        docs: [...state.docs, action.doc]
-      };
-      const isNotInParent = !state.parents.some(parent => parent?.id === action?.doc?.id);
-      if (!action.doc.parent && isNotInParent) {
-        setDocState.parents = [{
-          ...action.doc
-        }, ...state.parents];
-      }
-      return setDocState;
+    case 'SET_DOC': {
+      const idx = state.docs.findIndex(d => d?.id === action?.doc?.id);
+      const docs =
+        idx >= 0
+          ? [...state.docs.slice(0, idx), action.doc, ...state.docs.slice(idx + 1)]
+          : [...state.docs, action.doc];
+      const isNotInParent = !state.parents.some(parent => parent?.id === action?.doc?.id);
+      const base = { ...state, docs };
+      return !action.doc?.parent && isNotInParent
+        ? { ...base, parents: [{ ...action.doc }, ...state.parents] }
+        : base;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const setDocState = {
...state,
docs: [...state.docs, action.doc]
};
const isNotInParent = !state.parents.some(parent => parent?.id === action?.doc?.id);
if (!action.doc.parent && isNotInParent) {
setDocState.parents = [{
...action.doc
}, ...state.parents];
}
return setDocState;
case 'SET_DOC': {
const idx = state.docs.findIndex(d => d?.id === action?.doc?.id);
const docs =
idx >= 0
? [...state.docs.slice(0, idx), action.doc, ...state.docs.slice(idx + 1)]
: [...state.docs, action.doc];
const isNotInParent = !state.parents.some(parent => parent?.id === action?.doc?.id);
const base = { ...state, docs };
return !action.doc?.parent && isNotInParent
? { ...base, parents: [{ ...action.doc }, ...state.parents] }
: base;
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 338-341: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 342-342: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In assets/build/store.js around lines 338 to 348, the case-level declarations
leak because they are not wrapped in a block and the code appends docs/parents
which can create duplicates; wrap the case body in braces to scope const
setDocState, then change the append logic to perform an upsert by id: replace
docs with a new array that maps existing docs replacing the one with matching id
or adds it if missing, and do the same for parents (only when doc.parent is
falsy) ensuring you check ids for equality before inserting so duplicates are
prevented.

Comment on lines +392 to +394
docs: [...state.docs?.filter(doc => doc.id !== action.docId)],
parents: [...state.parents?.filter(parent => parent.id !== action.docId)]
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unsafe optional chaining in array spreads (Biome: noUnsafeOptionalChaining).

state.docs?.filter(...) can short‑circuit to undefined, which breaks the spread. Default to [].

-        docs: [...state.docs?.filter(doc => doc.id !== action.docId)],
-        parents: [...state.parents?.filter(parent => parent.id !== action.docId)]
+        docs: [...(Array.isArray(state.docs) ? state.docs : []).filter(doc => doc.id !== action.docId)],
+        parents: [...(Array.isArray(state.parents) ? state.parents : []).filter(parent => parent.id !== action.docId)]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
docs: [...state.docs?.filter(doc => doc.id !== action.docId)],
parents: [...state.parents?.filter(parent => parent.id !== action.docId)]
};
docs: [...(Array.isArray(state.docs) ? state.docs : []).filter(doc => doc.id !== action.docId)],
parents: [...(Array.isArray(state.parents) ? state.parents : []).filter(parent => parent.id !== action.docId)]
};
🧰 Tools
🪛 Biome (2.1.2)

[error] 392-392: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)


[error] 393-393: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)

🤖 Prompt for AI Agents
In assets/build/store.js around lines 392 to 394, the spread uses optional
chaining like state.docs?.filter(...) and state.parents?.filter(...) which can
yield undefined and break the array spread; change both to use a safe default
empty array before filtering (e.g., replace optional chaining with a
nullish-coalescing or logical-or fallback to [] so the spread always receives an
array), ensuring the resulting docs and parents are arrays even when state.docs
or state.parents is undefined.

Comment on lines +113 to +120
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/duplicate', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'duplicate_doc' ],
'permission_callback' => [ $this, 'create_item_permissions_check' ],
],
] );

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: permissive permission_callback allows any logged‑in user to duplicate.

duplicate uses create_item_permissions_check(), which returns true for any logged‑in user (and expects name/email unrelated to this route). Limit to users with edit_docs (or edit_post on the target).

- 'permission_callback' => [ $this, 'create_item_permissions_check' ],
+ 'permission_callback' => [ $this, 'duplicate_item_permissions_check' ],

Add this method (outside this hunk):

public function duplicate_item_permissions_check( $request ) {
    $doc_id = absint( $request->get_param( 'id' ) );
    if ( ! is_user_logged_in() ) {
        return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.', 'wedocs' ) );
    }
    if ( ! current_user_can( 'edit_post', $doc_id ) && ! current_user_can( 'edit_docs' ) ) {
        return new WP_Error( 'wedocs_permission_failure', __( 'You are not allowed to duplicate this document.', 'wedocs' ), [ 'status' => 403 ] );
    }
    return true;
}
🤖 Prompt for AI Agents
In includes/API/API.php around lines 113 to 120, the route for duplicating
documents uses create_item_permissions_check which permits any logged-in user;
replace that with a stricter permission check and add the new method: implement
duplicate_item_permissions_check( $request ) that extracts the id param,
verifies the user is logged in, then checks current_user_can('edit_post',
$doc_id) OR current_user_can('edit_docs') and returns WP_Error on failure (403
for permission failure) or true on success; update the register_rest_route call
to use this new duplicate_item_permissions_check as the permission_callback.

@sapayth sapayth changed the base branch from develop to doc_dupulcate_hirarchy_update September 25, 2025 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants