Skip to content

Adds publish templates and copy from upload#3428

Merged
tzarebczan merged 4 commits intomasterfrom
publish-improve
Feb 20, 2026
Merged

Adds publish templates and copy from upload#3428
tzarebczan merged 4 commits intomasterfrom
publish-improve

Conversation

@tzarebczan
Copy link
Contributor

@tzarebczan tzarebczan commented Feb 20, 2026

Introduces upload templates for channels, allowing users to save and apply pre-defined settings to new uploads.

Adds a "Copy from Previous Upload" modal, enabling users to populate the publish form with metadata from their previously published content.

Enhances upload search and template flows

Improves publish template functionality

Enhances the publish template feature by disabling the "Save Current as Template" option when no changes are detected in the publish form.

Additionally, increases the number of recent uploads displayed in the copy from upload modal and updates the hint text to reflect this change. Also provides default values for template comparison.

Fixes

Issue Number:

What is the current behavior?

What is the new behavior?

Other information

PR Checklist

Toggle...

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting)
  • Refactoring (no functional changes)
  • Documentation changes
  • Other - Please describe:

Please check all that apply to this PR using "x":

  • I have checked that this PR is not a duplicate of an existing PR (open, closed or merged)
  • I have checked that this PR does not introduce a breaking change
  • This PR introduces breaking changes and I have provided a detailed explanation below

Summary by CodeRabbit

  • New Features

    • Upload templates: save, organize, pin, search, apply, and manage templates per channel.
    • Copy from previous upload: search past uploads and import selected metadata into the publish form.
    • New template and modal UIs: template button, manage-templates modal, and copy-from-upload modal.
  • Bug Fixes / UX

    • Publish form now prompts “Choose a file to upload” when a required file is missing.
  • Style

    • Added comprehensive styling for template UI and copy-from-upload modal.
  • Other

    • Utilities and selectors added to support templates and homepage settings.

Introduces upload templates for channels, allowing users to save and apply pre-defined settings to new uploads.

Adds a "Copy from Previous Upload" modal, enabling users to populate the publish form with metadata from their previously published content.

Enhances upload search and template flows

Improves publish template functionality

Enhances the publish template feature by disabling the "Save Current as Template" option when no changes are detected in the publish form.

Additionally, increases the number of recent uploads displayed in the copy from upload modal and updates the hint text to reflect this change.
Also provides default values for template comparison.
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Adds upload-template support: new Flow types, normalization utilities, selectors, actions (search/populate/update), UI components (PublishTemplateButton, Copy From Upload modal, Manage Templates modal), styles, modal routing/types, and publish-form integration to fetch/apply per-channel templates.

Changes

Cohort / File(s) Summary
Type Definitions
flow-typed/Comment.js
Add UploadTemplateData and UploadTemplate types; expose optional upload_templates on PerChannelSettings, SettingsResponse, and UpdateSettingsParams.
Utilities
ui/util/homepage-settings.js, ui/util/clone.js
New homepage settings normalization and upload-template extraction (normalizeHomepageSettings, getUploadTemplatesFromSettings) and a generic cloneDeep utility.
Redux — Actions
ui/redux/actions/publish.js, ui/redux/actions/comments.js
Add doSearchMyUploads and doPopulatePublishFormFromClaim; modify doFetchCreatorSettings (cache-first signing) and make doUpdateCreatorSettings optimistic.
Redux — Selectors
ui/redux/selectors/comments.js
Add selectUploadTemplatesForChannelId and selectUploadTemplatesForActiveChannel.
Publish Form Integration
ui/component/publish/upload/uploadForm/index.js, ui/component/publish/upload/uploadForm/view.jsx
Expose channelId & fetchCreatorSettings, call fetch on channel change, introduce missingRequiredFile gating, render PublishTemplateButton when not editing.
PublishTemplateButton UI
ui/component/publish/shared/publishTemplateButton/...
index.js, view.jsx, style.scss
New connected component: list/search/apply/save/update/pin templates per channel, optimistic updates, undo support, and full styling.
Modals — Copy From Upload
ui/modal/modalCopyFromUpload/...
index.js, view.jsx, style.scss
New two-step modal: search previous uploads (filters, debounced), select claim, choose fields to copy, handle overwrites, and populate publish form; includes styles and Redux wiring.
Modals — Manage Upload Templates
ui/modal/modalUploadTemplates/...
index.js, view.jsx, style.scss
New modal to list/edit/rename/duplicate/delete/pin templates across channels, preview prefill fields, and persist changes to channel settings; includes styles and Redux wiring.
Modal Routing & Types
ui/modal/modalRouter/view.jsx, ui/constants/modal_types.js
Register lazy-loaded modal routes and add COPY_FROM_UPLOAD and MANAGE_UPLOAD_TEMPLATES modal type constants.
Publish Errors UI
ui/component/publish/shared/publishFormErrors/view.jsx
Add optional missingRequiredFile?: boolean prop and render "Choose a file to upload" when true.
Homepage Settings UI
ui/page/.../homeTab/view.jsx
Preserve non-section homepage_settings when saving by merging sections with existing object; add rawHomepageSettings / homepageSections handling.
Build & Styling
web/webpack.config.js, ui/scss/component/_snack-bar.scss
Tighten SENTRY_AUTH_TOKEN check and make Sentry plugin errors non-fatal; adjust snack-bar action button styles.
Other
ui/component/publish/shared/publishFormErrors/view.jsx, ui/component/publish/upload/uploadForm/*
Minor prop additions and wiring to support template features and missing-file validation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant PublishForm as Publish Form
    participant Redux as Redux Store
    participant Server
    participant TemplateBtn as PublishTemplateButton
    participant Modal as Modal (Manage/Copy)

    User->>PublishForm: Select channel
    PublishForm->>Redux: fetchCreatorSettings(channelId)
    Redux->>Server: GET channel settings
    Server-->>Redux: settings (may include upload_templates)
    Redux-->>TemplateBtn: provide templates

    User->>TemplateBtn: Save current form as template
    TemplateBtn->>Redux: doUpdateCreatorSettings (optimistic)
    Redux->>Server: Persist settings update
    Server-->>Redux: Confirm update
    Redux-->>User: Toast: saved
Loading
sequenceDiagram
    actor User
    participant CopyModal as Copy From Upload Modal
    participant Redux as Redux Store
    participant Server
    participant Lighthouse as Lighthouse Search
    participant PublishForm as Publish Form

    User->>CopyModal: Enter search term
    CopyModal->>Redux: doSearchMyUploads(term, filter)
    alt short term (<3)
        Redux->>Server: claim_list (local)
    else channels exist
        Redux->>Lighthouse: cross-channel search
        Lighthouse-->>Redux: results
        Redux->>Server: hydrate claims
    end
    Server-->>Redux: claims
    Redux-->>CopyModal: results
    User->>CopyModal: Select claim & fields
    CopyModal->>Redux: doPopulatePublishFormFromClaim
    Redux-->>PublishForm: update form values (with undo data)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • keikari

Poem

🐰 I hopped through code and found templates bright,

Saved a form by moon and morning light.
Pin and copy, manage with a cheer,
Uploads remembered, workflows clear.
— your rabbit reviewer, nibbling bugs away 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding publish templates and a copy-from-upload feature, which aligns with the substantial feature additions across the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch publish-improve

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/redux/actions/comments.js (1)

2012-2043: ⚠️ Potential issue | 🟠 Major

Add rollback/re-sync of settings on setting_update failure.

The optimistic dispatch at line 2015 updates the store immediately, but the catch block (line 2035) only shows an error toast without rolling back the optimistic state or re-syncing from the server. This leaves the store with stale/incorrect settings if the update fails.

Since the success path calls doFetchCreatorSettings() to re-sync, do the same in the catch block to maintain consistency:

Suggested fix
.catch((err) => {
  dispatch(
    doToast({
      message: __('Failed to update settings.'),
      subMessage: err?.message,
      isError: true,
    })
  );
  dispatch(doFetchCreatorSettings(channelClaim.claim_id));
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/redux/actions/comments.js` around lines 2012 - 2043, The optimistic update
dispatched via ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED is not rolled back on
failure of Comments.setting_update; update the catch handler to dispatch the
same re-sync used on success by calling
doFetchCreatorSettings(channelClaim.claim_id) after showing the doToast error so
the store is refreshed from the server and stale settings are removed (keep the
existing optimistic dispatch and error toast, just add the
dispatch(doFetchCreatorSettings(channelClaim.claim_id)) call in the .catch for
Comments.setting_update).
🧹 Nitpick comments (10)
ui/redux/actions/publish.js (3)

816-825: getLighthouseClaimId is overly defensive with multiple property-name guesses.

Checking claimId, claim_id, claimID, claimid, and id suggests uncertainty about Lighthouse's response schema. If the API contract is known, consider narrowing this to the actual field(s) and adding a comment referencing the Lighthouse docs. If the schema genuinely varies across versions, this is fine as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/redux/actions/publish.js` around lines 816 - 825, The getLighthouseClaimId
function is overly defensive by checking multiple possible property names;
update it to only read the canonical Lighthouse response field(s) (e.g., use
just claimId or claim_id per the API contract) and remove the extra fallbacks
(rawClaimId/claimID/claimid/id) so the code matches the documented schema; add a
concise comment above getLighthouseClaimId that cites the Lighthouse API doc or
version that defines the chosen field(s) and, if schema truly varies, add a
short note explaining why multiple keys are necessary.

932-959: Lighthouse search constructs query string manually — consider encoding safety.

Line 938 passes channelIdsCsv through encodeURIComponent, which is correct for the value. However, the term on line 934 is also encoded. The overall query string is built via concatenation rather than URLSearchParams. This works but is fragile if values contain & or =.

Using URLSearchParams for safer query construction
-    const channelIdsCsv = myChannelIds.join(',');
-    const queryBase = `from=0&s=${encodeURIComponent(term)}&sort_by=release_time&nsfw=${
-      showMature ? 'true' : 'false'
-    }&size=${SEARCH_PAGE_SIZE_PER_CHANNEL}`;
-
-    const response = await lighthouse.search(`${queryBase}&channel_id=${encodeURIComponent(channelIdsCsv)}`);
+    const params = new URLSearchParams({
+      from: '0',
+      s: term,
+      sort_by: 'release_time',
+      nsfw: showMature ? 'true' : 'false',
+      size: String(SEARCH_PAGE_SIZE_PER_CHANNEL),
+      channel_id: myChannelIds.join(','),
+    });
+    const response = await lighthouse.search(params.toString());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/redux/actions/publish.js` around lines 932 - 959, The query string is
built by concatenating encoded values (see variables queryBase, term,
channelIdsCsv and the call to lighthouse.search), which is fragile when values
contain & or =; replace the manual concatenation with a URLSearchParams
instance: create a params object, set from, s (term), sort_by, nsfw (using
showMature), size (SEARCH_PAGE_SIZE_PER_CHANNEL) and channel_id (channelIdsCsv),
then call lighthouse.search(params.toString()) so encoding is handled correctly
while leaving the subsequent logic (getLighthouseClaimId, uniqueClaimIds,
doResolveClaimIds and processing resolveInfo into resolvedClaims) untouched.

1063-1077: Visibility copy intentionally excludes scheduled — document rationale inline.

Line 1072 has a comment about not copying scheduled visibility, which is good. However, the unconditional reset of memberRestrictionOn/memberRestrictionTierIds on lines 1075-1076 is later conditionally overwritten (lines 1119-1138) only if the source/target channels match. If the visibility field is selected but MEMBERS_ONLY_CONTENT_TAG is in tags and the channels don't match, the restriction is silently dropped. This seems intentional but worth a brief inline comment for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/redux/actions/publish.js` around lines 1063 - 1077, The unconditional
reset of publishData.memberRestrictionOn and
publishData.memberRestrictionTierIds inside the visibility handling silently
drops members-only restrictions when tags include MEMBERS_ONLY_CONTENT_TAG but
source/target channels don't match; add a brief inline comment next to the reset
of publishData.memberRestrictionOn/memberRestrictionTierIds explaining this is
intentional: we clear prior member restrictions by default and they will only be
restored later when the source/target channel match (the conditional overwrite
that checks channel equality and restores
memberRestrictionOn/memberRestrictionTierIds runs further down), so maintainers
understand why restrictions can be lost here rather than moving logic.
ui/page/claim/internal/claimPageComponent/internal/channelPage/tabs/homeTab/view.jsx (1)

56-65: homepageSections in the dependency array is redundant alongside settingsByChannelId.

homepageSections is derived from settingsByChannelId (via rawHomepageSettings), so both will change together. Having both means the effect body runs on the same render cycle for both changes, which is harmless but unnecessary. Consider keeping only homepageSections (which already encodes the "settings loaded" information via the null check on line 57-58) and dropping settingsByChannelId from the dependency array, or vice versa.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ui/page/claim/internal/claimPageComponent/internal/channelPage/tabs/homeTab/view.jsx`
around lines 56 - 65, The effect in the component uses both homepageSections and
settingsByChannelId in its dependency array even though homepageSections is
derived from settingsByChannelId; remove settingsByChannelId from the dependency
array and keep only homepageSections so the effect triggers based on the derived
value (update the useEffect dependency array referenced in the component around
the React.useEffect and remove the redundant eslint-disable comment or adjust it
to reflect the new dependencies for clarity).
ui/component/publish/shared/publishTemplateButton/style.scss (1)

86-101: Hardcoded #fff may not work well with all themes.

Lines 89, 95, and 100 use hardcoded #fff for the selected menu item text/icon color. If the active/selected background color ever changes or a high-contrast theme is introduced, these could lose contrast. Consider using a theme-aware CSS variable if one exists (e.g., var(--color-text-inverse) or similar).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/component/publish/shared/publishTemplateButton/style.scss` around lines 86
- 101, The selected menu item styles use hardcoded `#fff` (in selectors
[data-reach-menu-item][data-selected], [data-reach-menu-item][data-selected]
.menu__link, [data-reach-menu-item][data-selected] * and
[data-reach-menu-item][data-selected] .icon, svg) which can break contrast in
alternate themes; replace those hardcoded colors with a theme-aware CSS variable
(e.g., var(--color-text-inverse) or a more specific var like
var(--color-button-toggle-text-active)) and keep an appropriate fallback,
preserving any necessary !important flags so the selection styles still override
defaults.
ui/redux/selectors/comments.js (1)

215-224: Consider memoizing these selectors to avoid unnecessary re-renders.

Both selectUploadTemplatesForChannelId and selectUploadTemplatesForActiveChannel are plain functions. The underlying getUploadTemplatesFromSettings allocates a new array on every call (via .map().filter(Boolean)), so every useSelector / mapStateToProps consumer will see a new reference and re-render on every unrelated state change.

Use createCachedSelector keyed on channelId to stabilize the reference, matching the pattern already established by selectFeaturedChannelsForChannelId (line 230).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/redux/selectors/comments.js` around lines 215 - 224,
selectUploadTemplatesForChannelId and selectUploadTemplatesForActiveChannel
should be converted to memoized selectors because getUploadTemplatesFromSettings
allocates a new array each call; replace the plain functions with a
createCachedSelector that takes selectSettingsForChannelId as its input and
returns getUploadTemplatesFromSettings(channelSettings), keyed by channelId
(matching the selectFeaturedChannelsForChannelId pattern), and then create
selectUploadTemplatesForActiveChannel by reading selectActiveChannelId and
delegating to the cached selector (returning [] when no active id). Ensure you
export the same selector names (selectUploadTemplatesForChannelId,
selectUploadTemplatesForActiveChannel) so consumers remain unchanged.
ui/component/publish/shared/publishTemplateButton/view.jsx (1)

85-121: Duplicated utility functions across files.

cloneTemplateValue, getTemplateSortTimestamp, and getTemplateKey are duplicated verbatim in ui/modal/modalUploadTemplates/view.jsx (lines 30-44, 61-63, 83-85). Consider extracting them into a shared utility module (e.g., alongside ui/util/homepage-settings.js).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/component/publish/shared/publishTemplateButton/view.jsx` around lines 85 -
121, The functions cloneTemplateValue, normalizeTemplateValue (used by
areTemplateDataEqual), getTemplateSortTimestamp, and getTemplateKey are
duplicated; extract these utilities into a new shared module (e.g., create and
export normalizeTemplateValue, cloneTemplateValue, areTemplateDataEqual,
getTemplateSortTimestamp, and getTemplateKey) and replace the duplicate
implementations in both view.jsx files with imports from that shared util,
updating call sites to use the exported names and removing the local copies to
avoid drift.
ui/modal/modalCopyFromUpload/view.jsx (1)

90-104: Third copy of deep-clone utility.

cloneValue is functionally identical to cloneTemplateValue in both publishTemplateButton/view.jsx and modalUploadTemplates/view.jsx. This is the third copy. All three should live in a shared utility module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/modal/modalCopyFromUpload/view.jsx` around lines 90 - 104, The deep-clone
logic is duplicated (cloneValue here and cloneTemplateValue in
publishTemplateButton/view.jsx and modalUploadTemplates/view.jsx); refactor by
extracting a single shared utility function (e.g., deepClone or
cloneTemplateValue) into a common utilities module and replace the local
cloneValue implementations with imports of that function; update usages in
cloneValue, cloneTemplateValue and the other occurrence to call the centralized
function and remove the duplicate implementations to avoid drift and repetition.
ui/modal/modalUploadTemplates/view.jsx (2)

30-44: Duplicated utility functions — consolidate with publishTemplateButton.

cloneTemplateValue, getTemplateSortTimestamp, and getTemplateKey are exact duplicates of the same functions in ui/component/publish/shared/publishTemplateButton/view.jsx. Extract these into a shared module to keep a single source of truth.

Also applies to: 61-63, 83-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/modal/modalUploadTemplates/view.jsx` around lines 30 - 44, The functions
cloneTemplateValue, getTemplateSortTimestamp, and getTemplateKey are duplicated
and should be moved into one shared utility module; create a new module
exporting these functions (e.g., export functions named cloneTemplateValue,
getTemplateSortTimestamp, getTemplateKey), replace the local definitions in
modalUploadTemplates/view.jsx and in publishTemplateButton/view.jsx with imports
from that new module, and update any call sites to use the imported names so
both components use the single shared implementation.

404-425: Side-effecting read from inside state updater is fragile.

createdName is written inside the setTemplatesByChannelId updater callback (line 408) and read outside it (line 422). While this works synchronously in React 16, it's an anti-pattern that can break under React 18's automatic batching or concurrent features.

Suggested refactor
  function handleDuplicate(template: TemplateEntry) {
-   let createdName = '';
-   updateTemplatesForChannel(template.channelId, (channelTemplates) => {
-     const duplicateName = makeDuplicateTemplateName(template.name, channelTemplates);
-     createdName = duplicateName;
-     const duplicateTemplate: UploadTemplate = {
+   const channelTemplates = templatesByChannelId[template.channelId] || [];
+   const duplicateName = makeDuplicateTemplateName(template.name, channelTemplates);
+   const duplicateTemplate: UploadTemplate = {
        ...template,
        id: uuid(),
-       name: duplicateName,
+       name: duplicateName,
        createdAt: Date.now(),
        lastUsedAt: undefined,
        isPinned: false,
        data: cloneTemplateValue(template.data || {}),
-     };
-
-     return [duplicateTemplate, ...channelTemplates];
-   });
+   };
+
+   updateTemplatesForChannel(template.channelId, (existing) => [duplicateTemplate, ...existing]);

-   if (createdName) {
-     doToast({ message: __('Template "%name%" duplicated', { name: createdName }) });
-   }
+   doToast({ message: __('Template "%name%" duplicated', { name: duplicateName }) });
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/modal/modalUploadTemplates/view.jsx` around lines 404 - 425,
handleDuplicate currently writes to createdName inside the
updateTemplatesForChannel updater and then reads it outside, which is fragile
under React 18 batching; instead change updateTemplatesForChannel so it returns
the created name (or null) from the updater callback (or accept an onComplete
callback) and use that return value for the toast. Specifically, update the call
in handleDuplicate (function handleDuplicate) to capture the result: const
createdName = updateTemplatesForChannel(template.channelId, (channelTemplates)
=> { ... return [duplicateTemplate, ...channelTemplates]; }); and then call
doToast when createdName is truthy; update the implementation of
updateTemplatesForChannel to forward a string result from the updater callback
so no external mutated variable (createdName) is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/component/publish/shared/publishTemplateButton/style.scss`:
- Line 67: The SCSS rule uses the incorrect casing for the CSS keyword: replace
the declaration "stroke: currentColor !important;" with the lowercase keyword
"currentcolor" (i.e., "stroke: currentcolor !important;") to satisfy Stylelint's
value-keyword-case rule; update the same pattern wherever "currentColor" is used
in SCSS variables or declarations in this component.

In `@ui/component/publish/shared/publishTemplateButton/view.jsx`:
- Around line 223-235: The optimisticTemplatesByChannelId cache is never cleared
so getTemplatesForChannelId continues to return stale
optimisticTemplatesByChannelId[channelId] even after settingsByChannelId is
updated by the server; add an effect that watches settingsByChannelId and
optimisticTemplatesByChannelId and, when settingsByChannelId[channelId] becomes
available (or changes) while an optimisticTemplatesByChannelId[channelId]
exists, remove that optimistic entry (e.g. call the state updater that owns
optimisticTemplatesByChannelId to delete the channel key). Tie this cleanup to
the same component where getTemplatesForChannelId is defined so
doUpdateCreatorSettings flow will no longer be shadowed by stale
optimisticTemplatesByChannelId.

In `@ui/modal/modalCopyFromUpload/view.jsx`:
- Around line 637-642: The hint in modalCopyFromUpload (`<p
className="copy-from-upload__result-hint">`) is misleading because `runSearch`
is called for any non-empty `trimmedTerm` and additional client-side filtering
is applied, so change the text to clarify that short terms do perform a partial
search but full results require 3+ characters (e.g., "Type at least 3 characters
for full search results"); update the conditional rendering that uses
`activeFilter` and `trimmedTerm` to display this revised message and ensure it
aligns with the behavior of `runSearch` and the client-side filtering logic.

In `@ui/modal/modalUploadTemplates/style.scss`:
- Line 94: The CSS uses the incorrect capitalization "currentColor" which
violates the project's Stylelint rule; update all occurrences of the stroke
property values in modalUploadTemplates/style.scss (the lines setting "stroke:
currentColor;"—three occurrences referenced in the diff) to use the lowercase
keyword "currentcolor" so they conform to value-keyword-case and pass linting.

In `@ui/redux/actions/publish.js`:
- Around line 871-886: The claim_search call for the 'unlisted' (and similarly
scheduled) branch is invoked even when myChannelIds is empty, causing global
results; update the logic in the block that builds csParams (and the parallel
scheduled branch) to only call Lbry.claim_search when myChannelIds.length > 0
and csParams.channel_ids is set, otherwise call Lbry.claim_list (wallet-scoped)
and then reuse extractStreamClaims/hydrateClaimsInStore and apply the existing
client-side tag filter (VISIBILITY_TAGS.UNLISTED or scheduled tag) and
clientTitleFilter; ensure you reference and update csParams, myChannelIds,
Lbry.claim_search, Lbry.claim_list, extractStreamClaims, hydrateClaimsInStore,
and clientTitleFilter accordingly.

---

Outside diff comments:
In `@ui/redux/actions/comments.js`:
- Around line 2012-2043: The optimistic update dispatched via
ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED is not rolled back on failure of
Comments.setting_update; update the catch handler to dispatch the same re-sync
used on success by calling doFetchCreatorSettings(channelClaim.claim_id) after
showing the doToast error so the store is refreshed from the server and stale
settings are removed (keep the existing optimistic dispatch and error toast,
just add the dispatch(doFetchCreatorSettings(channelClaim.claim_id)) call in the
.catch for Comments.setting_update).

---

Nitpick comments:
In `@ui/component/publish/shared/publishTemplateButton/style.scss`:
- Around line 86-101: The selected menu item styles use hardcoded `#fff` (in
selectors [data-reach-menu-item][data-selected],
[data-reach-menu-item][data-selected] .menu__link,
[data-reach-menu-item][data-selected] * and
[data-reach-menu-item][data-selected] .icon, svg) which can break contrast in
alternate themes; replace those hardcoded colors with a theme-aware CSS variable
(e.g., var(--color-text-inverse) or a more specific var like
var(--color-button-toggle-text-active)) and keep an appropriate fallback,
preserving any necessary !important flags so the selection styles still override
defaults.

In `@ui/component/publish/shared/publishTemplateButton/view.jsx`:
- Around line 85-121: The functions cloneTemplateValue, normalizeTemplateValue
(used by areTemplateDataEqual), getTemplateSortTimestamp, and getTemplateKey are
duplicated; extract these utilities into a new shared module (e.g., create and
export normalizeTemplateValue, cloneTemplateValue, areTemplateDataEqual,
getTemplateSortTimestamp, and getTemplateKey) and replace the duplicate
implementations in both view.jsx files with imports from that shared util,
updating call sites to use the exported names and removing the local copies to
avoid drift.

In `@ui/modal/modalCopyFromUpload/view.jsx`:
- Around line 90-104: The deep-clone logic is duplicated (cloneValue here and
cloneTemplateValue in publishTemplateButton/view.jsx and
modalUploadTemplates/view.jsx); refactor by extracting a single shared utility
function (e.g., deepClone or cloneTemplateValue) into a common utilities module
and replace the local cloneValue implementations with imports of that function;
update usages in cloneValue, cloneTemplateValue and the other occurrence to call
the centralized function and remove the duplicate implementations to avoid drift
and repetition.

In `@ui/modal/modalUploadTemplates/view.jsx`:
- Around line 30-44: The functions cloneTemplateValue, getTemplateSortTimestamp,
and getTemplateKey are duplicated and should be moved into one shared utility
module; create a new module exporting these functions (e.g., export functions
named cloneTemplateValue, getTemplateSortTimestamp, getTemplateKey), replace the
local definitions in modalUploadTemplates/view.jsx and in
publishTemplateButton/view.jsx with imports from that new module, and update any
call sites to use the imported names so both components use the single shared
implementation.
- Around line 404-425: handleDuplicate currently writes to createdName inside
the updateTemplatesForChannel updater and then reads it outside, which is
fragile under React 18 batching; instead change updateTemplatesForChannel so it
returns the created name (or null) from the updater callback (or accept an
onComplete callback) and use that return value for the toast. Specifically,
update the call in handleDuplicate (function handleDuplicate) to capture the
result: const createdName = updateTemplatesForChannel(template.channelId,
(channelTemplates) => { ... return [duplicateTemplate, ...channelTemplates]; });
and then call doToast when createdName is truthy; update the implementation of
updateTemplatesForChannel to forward a string result from the updater callback
so no external mutated variable (createdName) is used.

In
`@ui/page/claim/internal/claimPageComponent/internal/channelPage/tabs/homeTab/view.jsx`:
- Around line 56-65: The effect in the component uses both homepageSections and
settingsByChannelId in its dependency array even though homepageSections is
derived from settingsByChannelId; remove settingsByChannelId from the dependency
array and keep only homepageSections so the effect triggers based on the derived
value (update the useEffect dependency array referenced in the component around
the React.useEffect and remove the redundant eslint-disable comment or adjust it
to reflect the new dependencies for clarity).

In `@ui/redux/actions/publish.js`:
- Around line 816-825: The getLighthouseClaimId function is overly defensive by
checking multiple possible property names; update it to only read the canonical
Lighthouse response field(s) (e.g., use just claimId or claim_id per the API
contract) and remove the extra fallbacks (rawClaimId/claimID/claimid/id) so the
code matches the documented schema; add a concise comment above
getLighthouseClaimId that cites the Lighthouse API doc or version that defines
the chosen field(s) and, if schema truly varies, add a short note explaining why
multiple keys are necessary.
- Around line 932-959: The query string is built by concatenating encoded values
(see variables queryBase, term, channelIdsCsv and the call to
lighthouse.search), which is fragile when values contain & or =; replace the
manual concatenation with a URLSearchParams instance: create a params object,
set from, s (term), sort_by, nsfw (using showMature), size
(SEARCH_PAGE_SIZE_PER_CHANNEL) and channel_id (channelIdsCsv), then call
lighthouse.search(params.toString()) so encoding is handled correctly while
leaving the subsequent logic (getLighthouseClaimId, uniqueClaimIds,
doResolveClaimIds and processing resolveInfo into resolvedClaims) untouched.
- Around line 1063-1077: The unconditional reset of
publishData.memberRestrictionOn and publishData.memberRestrictionTierIds inside
the visibility handling silently drops members-only restrictions when tags
include MEMBERS_ONLY_CONTENT_TAG but source/target channels don't match; add a
brief inline comment next to the reset of
publishData.memberRestrictionOn/memberRestrictionTierIds explaining this is
intentional: we clear prior member restrictions by default and they will only be
restored later when the source/target channel match (the conditional overwrite
that checks channel equality and restores
memberRestrictionOn/memberRestrictionTierIds runs further down), so maintainers
understand why restrictions can be lost here rather than moving logic.

In `@ui/redux/selectors/comments.js`:
- Around line 215-224: selectUploadTemplatesForChannelId and
selectUploadTemplatesForActiveChannel should be converted to memoized selectors
because getUploadTemplatesFromSettings allocates a new array each call; replace
the plain functions with a createCachedSelector that takes
selectSettingsForChannelId as its input and returns
getUploadTemplatesFromSettings(channelSettings), keyed by channelId (matching
the selectFeaturedChannelsForChannelId pattern), and then create
selectUploadTemplatesForActiveChannel by reading selectActiveChannelId and
delegating to the cached selector (returning [] when no active id). Ensure you
export the same selector names (selectUploadTemplatesForChannelId,
selectUploadTemplatesForActiveChannel) so consumers remain unchanged.

Comment on lines +871 to +886
if (filter === 'unlisted') {
const csParams: any = {
page: 1,
page_size: RECENT_PAGE_SIZE,
any_tags: [VISIBILITY_TAGS.UNLISTED],
claim_type: ['stream'],
order_by: ['release_time'],
remove_duplicates: true,
};
if (myChannelIds.length > 0) {
csParams.channel_ids = myChannelIds;
}
const result = await Lbry.claim_search(csParams);
const claims = await hydrateClaimsInStore(dispatch, sortClaimsByNewest(extractStreamClaims(result)));
return { claims: term.length > 0 ? clientTitleFilter(claims, term) : claims };
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for claim_search usage patterns
echo "=== claim_search usage patterns ==="
rg -n 'claim_search' --type=js -C3 | head -100

Repository: OdyseeTeam/odysee-frontend

Length of output: 6411


🏁 Script executed:

#!/bin/bash
# Search for claim_list usage patterns
echo "=== claim_list usage patterns ==="
rg -n 'claim_list' --type=js -C3 | head -100

Repository: OdyseeTeam/odysee-frontend

Length of output: 5775


🏁 Script executed:

#!/bin/bash
# Look for Lbry.claim_search API calls and their parameters
echo "=== Lbry.claim_search API calls ==="
rg 'Lbry\.claim_search' --type=js -C5

Repository: OdyseeTeam/odysee-frontend

Length of output: 8432


🏁 Script executed:

#!/bin/bash
# Check for VISIBILITY_TAGS definition and usage
echo "=== VISIBILITY_TAGS definition ==="
rg 'VISIBILITY_TAGS' --type=js -C2 | head -60

Repository: OdyseeTeam/odysee-frontend

Length of output: 3159


claim_search without channel_ids returns global results, exposing other users' unlisted content when the current user has no channels.

When myChannelIds is empty (anonymous uploader with no channel), the claim_search calls on lines 883 and 902 lack a channel filter and return all unlisted/scheduled streams globally, not just the user's own. The "All uploads" filter correctly handles this case by using the wallet-scoped Lbry.claim_list instead.

Require at least one channel ID before using claim_search for unlisted/scheduled filters, or fall back to claim_list + client-side tag filtering for anonymous users.

Proposed fix: fall back to wallet-scoped claim_list when no channels
     // ── Unlisted filter: use claim_search with any_tags ──
     if (filter === 'unlisted') {
+      if (myChannelIds.length === 0) {
+        // No channels → fall back to wallet-scoped claim_list + client-side tag filter
+        const result = await Lbry.claim_list({
+          page: 1,
+          page_size: RECENT_PAGE_SIZE,
+          resolve: true,
+          claim_type: ['stream'],
+        });
+        const allClaims = sortClaimsByNewest(extractStreamClaims(result));
+        const unlisted = allClaims.filter((c) => (c?.value?.tags || []).includes(VISIBILITY_TAGS.UNLISTED));
+        const claims = await hydrateClaimsInStore(dispatch, unlisted);
+        return { claims: term.length > 0 ? clientTitleFilter(claims, term) : claims };
+      }
+
       const csParams: any = {
         page: 1,
         page_size: RECENT_PAGE_SIZE,
         any_tags: [VISIBILITY_TAGS.UNLISTED],
         claim_type: ['stream'],
         order_by: ['release_time'],
         remove_duplicates: true,
-        channel_ids: myChannelIds,
+        channel_ids: myChannelIds,
       };
-      if (myChannelIds.length > 0) {
-        csParams.channel_ids = myChannelIds;
-      }
       const result = await Lbry.claim_search(csParams);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/redux/actions/publish.js` around lines 871 - 886, The claim_search call
for the 'unlisted' (and similarly scheduled) branch is invoked even when
myChannelIds is empty, causing global results; update the logic in the block
that builds csParams (and the parallel scheduled branch) to only call
Lbry.claim_search when myChannelIds.length > 0 and csParams.channel_ids is set,
otherwise call Lbry.claim_list (wallet-scoped) and then reuse
extractStreamClaims/hydrateClaimsInStore and apply the existing client-side tag
filter (VISIBILITY_TAGS.UNLISTED or scheduled tag) and clientTitleFilter; ensure
you reference and update csParams, myChannelIds, Lbry.claim_search,
Lbry.claim_list, extractStreamClaims, hydrateClaimsInStore, and
clientTitleFilter accordingly.

Fixes an issue where optimistic templates were not cleared when channel settings became available or changed.
This ensures that the UI reflects the correct template status after settings are loaded or modified.

Also corrects a typo in the scss, and improves the hint in the copy from upload modal, clarifying the behavior of short search terms.
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: 2

🧹 Nitpick comments (1)
ui/component/publish/shared/publishTemplateButton/view.jsx (1)

107-121: cloneTemplateValue duplicates cloneValue from modalCopyFromUpload/view.jsx

Both functions are structurally identical recursive deep-clone utilities. Consider extracting to a shared helper (e.g., util/template-helpers.js).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/component/publish/shared/publishTemplateButton/view.jsx` around lines 107
- 121, cloneTemplateValue is a duplicate of cloneValue (both are recursive
deep-clone utilities); extract the shared logic into a new helper (e.g., export
a function named cloneDeep or cloneTemplateValue from a new util module) and
replace the local implementations in this file and the other file that defines
cloneValue to import and use the shared helper; ensure the helper preserves
array/object recursion and default value return semantics and update both call
sites to import the helper and remove the duplicated local functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/component/publish/shared/publishTemplateButton/view.jsx`:
- Around line 85-105: normalizeTemplateValue currently preserves array order so
areTemplateDataEqual can produce false negatives when arrays (e.g., tags)
contain the same items in different orders; update normalizeTemplateValue to
canonicalize arrays by first normalizing each element then, for arrays of
primitives sort them (e.g., strings/numbers/booleans) and for arrays of objects
sort by JSON.stringify of the normalized element so that arrays with same
contents in different orders compare equal; keep function names
normalizeTemplateValue and areTemplateDataEqual unchanged.

In `@ui/modal/modalCopyFromUpload/view.jsx`:
- Around line 171-177: The tag comparison is order-sensitive causing false
positives; update the 'tags' branch in fieldWouldChangeValue to sort the
normalized tag arrays before calling areArraysEqualForCompare (or normalize+sort
within areArraysEqualForCompare when comparing tags) so that
['gaming','tutorial'] and ['tutorial','gaming'] are treated equal; locate the
tags handling in fieldWouldChangeValue and ensure both arrays are trimmed,
lowercased, sorted, then compared using the existing areArraysEqualForCompare
(or change areArraysEqualForCompare to accept a sort flag and use it for tags).

---

Nitpick comments:
In `@ui/component/publish/shared/publishTemplateButton/view.jsx`:
- Around line 107-121: cloneTemplateValue is a duplicate of cloneValue (both are
recursive deep-clone utilities); extract the shared logic into a new helper
(e.g., export a function named cloneDeep or cloneTemplateValue from a new util
module) and replace the local implementations in this file and the other file
that defines cloneValue to import and use the shared helper; ensure the helper
preserves array/object recursion and default value return semantics and update
both call sites to import the helper and remove the duplicated local functions.

Comment on lines +85 to +105
function normalizeTemplateValue(value: any): any {
if (Array.isArray(value)) {
return value.map((item) => normalizeTemplateValue(item));
}

if (value && typeof value === 'object') {
const normalized = {};
Object.keys(value)
.sort()
.forEach((key) => {
normalized[key] = normalizeTemplateValue(value[key]);
});
return normalized;
}

return value;
}

function areTemplateDataEqual(a: UploadTemplateData, b: UploadTemplateData): boolean {
return JSON.stringify(normalizeTemplateValue(a || {})) === JSON.stringify(normalizeTemplateValue(b || {}));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

normalizeTemplateValue sorts object keys but not array elements — order-sensitive tag comparison

normalizeTemplateValue (line 87) recursively maps array items but never sorts them. areTemplateDataEqual (line 104) serialises via JSON.stringify, so two UploadTemplateData objects whose tags arrays contain the same entries in different order will compare as unequal.

Practical impact:

  • canUpdateSelectedTemplate: after applying a template and having the form reorder tags internally, the "Update Selected Template" menu item appears even though nothing meaningful changed.
  • hasPublishFormTemplateChanges: same false-positive can keep "Save Current as Template" enabled when the form is semantically at its default state.
🛠️ Proposed fix – sort string arrays in normalizeTemplateValue
 function normalizeTemplateValue(value: any): any {
   if (Array.isArray(value)) {
-    return value.map((item) => normalizeTemplateValue(item));
+    const items = value.map((item) => normalizeTemplateValue(item));
+    // Sort flat string arrays (e.g. tags) for order-insensitive comparison
+    if (items.every((item) => typeof item === 'string')) {
+      return [...items].sort();
+    }
+    return items;
   }
   ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/component/publish/shared/publishTemplateButton/view.jsx` around lines 85 -
105, normalizeTemplateValue currently preserves array order so
areTemplateDataEqual can produce false negatives when arrays (e.g., tags)
contain the same items in different orders; update normalizeTemplateValue to
canonicalize arrays by first normalizing each element then, for arrays of
primitives sort them (e.g., strings/numbers/booleans) and for arrays of objects
sort by JSON.stringify of the normalized element so that arrays with same
contents in different orders compare equal; keep function names
normalizeTemplateValue and areTemplateDataEqual unchanged.

Enhances the template preview to accurately display price details based on the paywall configuration.
It introduces a check to ensure price information is shown only when relevant to the selected paywall type.

Additionally, updates the snackbar styling to improve button appearance and provide a more consistent user experience.

Refactors: Uses shared deep clone utility

Removes duplicate deep clone functions and uses the newly created shared utility.
This ensures consistency and reduces code duplication.
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 (4)
ui/scss/component/_snack-bar.scss (2)

115-115: Hardcoded rgba(0, 0, 0, 0.16) may not theme correctly.

All other interactive colors in this file use CSS custom properties (e.g., var(--color-background), var(--color-primary)). A hardcoded dark-tinted overlay works for dark-background snackbars but may be near-invisible or visually inconsistent in a light theme where the snackbar background is already light. Consider a CSS variable or a semi-transparent variable already present in the design system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/scss/component/_snack-bar.scss` at line 115, Replace the hardcoded
background rgba(0, 0, 0, 0.16) in the snack-bar rule with a theme-aware CSS
custom property (for example use background: var(--snack-bar-overlay,
rgba(0,0,0,0.16))) so themes can override it; update the SCSS variable name to
match your design system (e.g., --color-overlay or --snack-bar-overlay) and
ensure the snackbar background logic falls back to a sensible default while
allowing light/dark themes to provide an appropriate semi-transparent value.

118-121: !important on color: inherit is unnecessary and can be removed.

.snack-bar__action .button:hover .button__label and .button:focus-visible .button__label have specificity 0,4,0, which already outranks the .snack-bar .button__label declaration at line 19 (0,2,0). The snack-bar action button has no button variant class (.button--primary, .button--secondary, etc.), so global !important rules from the button component do not apply here. The !important is redundant and can lead to unnecessary specificity escalation over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/scss/component/_snack-bar.scss` around lines 118 - 121, Remove the
unnecessary "!important" from the rule targeting .button:hover .button__label
and .button:focus-visible .button__label in the snack-bar styles; update the
selector block that currently reads ".button:hover .button__label,
.button:focus-visible .button__label { color: inherit !important; }" to use
"color: inherit;" so the .snack-bar__action variant (.snack-bar__action
.button__label) still inherits color via normal cascade without forcing higher
specificity or encouraging future overrides.
ui/modal/modalUploadTemplates/view.jsx (1)

500-529: handleSave closes the modal optimistically — failed API writes are silent.

doUpdateCreatorSettings is fired for each changed channel without awaiting its result. The success toast and doHideModal() execute regardless of whether the server accepted the changes. If any channel save fails, the user has no feedback and may believe their changes were persisted.

Consider either (a) awaiting each doUpdateCreatorSettings call and handling errors before closing, or (b) keeping the optimistic pattern but subscribing to a Redux error state and showing a follow-up error toast.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/modal/modalUploadTemplates/view.jsx` around lines 500 - 529, handleSave
currently fires doUpdateCreatorSettings for each channel and closes the modal
and shows success toasts without waiting for results, so failed API writes are
silent; update handleSave to collect and await the promises returned by
doUpdateCreatorSettings for each changedChannelIds (or use Promise.allSettled),
count successes vs failures, show appropriate success/failure toasts via
doToast, and only call doHideModal() after resolving results (or if keeping
optimistic UI, subscribe to the Redux error state for creator settings and
display a follow-up error toast referencing doUpdateCreatorSettings, doToast,
doHideModal, changedChannelIds, and templatesByChannelId).
ui/util/clone.js (1)

3-17: cloneDeep silently corrupts Date, Map, Set, and RegExp values.

Object.keys(new Date()) returns [], so any Date in the cloned tree becomes {}. The same applies to Map, Set, RegExp, etc. For the current callers (JSON-serializable template/form data) this is safe, but as a shared utility it's a footgun. Either:

  • Add a JSDoc comment scoping it to plain JSON-like structures, or
  • Replace with the platform-native structuredClone (Chrome 98+, Safari 15.4+, Node 17+) which handles all standard types correctly and also guards against circular references.
♻️ Option A – minimal: document the constraint
+/**
+ * Deep-clones a plain JSON-serializable value (primitives, plain objects,
+ * arrays). Does NOT correctly handle Date, Map, Set, RegExp, or circular
+ * references; callers must ensure the value fits this constraint.
+ */
 export function cloneDeep(value: any): any {
♻️ Option B – replace with structuredClone
-export function cloneDeep(value: any): any {
-  if (Array.isArray(value)) {
-    return value.map((item) => cloneDeep(item));
-  }
-
-  if (value && typeof value === 'object') {
-    const clone = {};
-    Object.keys(value).forEach((key) => {
-      clone[key] = cloneDeep(value[key]);
-    });
-    return clone;
-  }
-
-  return value;
-}
+export function cloneDeep<T>(value: T): T {
+  return structuredClone(value);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/util/clone.js` around lines 3 - 17, The cloneDeep utility currently
corrupts Dates, Maps, Sets, RegExps etc.; update the cloneDeep function to use
the platform native structuredClone when available (call structuredClone(value))
so it correctly handles all standard built-in types and circular refs, and add a
small fallback for older runtimes (e.g., JSON.parse(JSON.stringify(value)) or
throw a clear error) for environments without structuredClone; also add a brief
JSDoc on cloneDeep explaining it prefers structuredClone and what the fallback
behavior is so callers know the semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/modal/modalCopyFromUpload/view.jsx`:
- Around line 234-237: The languages comparison is order-sensitive because
normalizeLanguageValuesForCompare does not sort values; update the 'languages'
case so it sorts the normalized source and target arrays before calling
areArraysEqualForCompare (same approach used for tags). Locate the 'languages'
case in view.jsx and, using
normalizeLanguageValuesForCompare(metadata.languageList, null) and
normalizeLanguageValuesForCompare(formValues.languages, formValues.language),
sort both returned arrays (e.g., by value) prior to passing them to
areArraysEqualForCompare so ['en','fr'] and ['fr','en'] compare equal and
'languages' is not wrongly added to fieldsToApply.

In `@ui/modal/modalUploadTemplates/view.jsx`:
- Around line 435-456: The toast never fires because handleDuplicate mutates
createdName inside the updateTemplatesForChannel updater (a side-effect run
later during render); fix by computing duplicateName synchronously from the
current rendered templates for that channel before calling
updateTemplatesForChannel (e.g., read the channel's templates from the component
state/selector, call makeDuplicateTemplateName(template.name, channelTemplates)
to get createdName), then call updateTemplatesForChannel to insert the new
UploadTemplate (using the precomputed name) and finally call doToast({ message:
__('Template "%name%" duplicated', { name: createdName }) }); ensure no external
mutation occurs inside the updater function.
- Around line 30-44: Remove the duplicate cloneTemplateValue function and
instead import the shared cloneDeep utility; replace all calls to
cloneTemplateValue(...) with cloneDeep(...), preserving TS/JS types and ensuring
the module import (import cloneDeep from 'util/clone' or existing project import
path) is added at the top of the file; verify there are no remaining references
to cloneTemplateValue and run tests/lint to confirm no unresolved identifiers.

In `@ui/scss/component/_snack-bar.scss`:
- Around line 108-116: The .snack-bar .button__content:hover rule is
unintentionally overriding the .button hover on action buttons; inside the
.snack-bar__action .button__content selector, add a reset for hover/active
styles to neutralize inheritance — e.g., restore background to transparent (or
var(--color-background) as appropriate), remove or disable the icon rotation
animation (animation: none or transform: none), and ensure no extra
min-height/padding changes on hover; update the .snack-bar__action
.button__content rules (and its :hover state) to explicitly override the
inherited .button__content:hover behavior so the outer .button:hover
(rgba(0,0,0,0.16)) remains visible.

---

Nitpick comments:
In `@ui/modal/modalUploadTemplates/view.jsx`:
- Around line 500-529: handleSave currently fires doUpdateCreatorSettings for
each channel and closes the modal and shows success toasts without waiting for
results, so failed API writes are silent; update handleSave to collect and await
the promises returned by doUpdateCreatorSettings for each changedChannelIds (or
use Promise.allSettled), count successes vs failures, show appropriate
success/failure toasts via doToast, and only call doHideModal() after resolving
results (or if keeping optimistic UI, subscribe to the Redux error state for
creator settings and display a follow-up error toast referencing
doUpdateCreatorSettings, doToast, doHideModal, changedChannelIds, and
templatesByChannelId).

In `@ui/scss/component/_snack-bar.scss`:
- Line 115: Replace the hardcoded background rgba(0, 0, 0, 0.16) in the
snack-bar rule with a theme-aware CSS custom property (for example use
background: var(--snack-bar-overlay, rgba(0,0,0,0.16))) so themes can override
it; update the SCSS variable name to match your design system (e.g.,
--color-overlay or --snack-bar-overlay) and ensure the snackbar background logic
falls back to a sensible default while allowing light/dark themes to provide an
appropriate semi-transparent value.
- Around line 118-121: Remove the unnecessary "!important" from the rule
targeting .button:hover .button__label and .button:focus-visible .button__label
in the snack-bar styles; update the selector block that currently reads
".button:hover .button__label, .button:focus-visible .button__label { color:
inherit !important; }" to use "color: inherit;" so the .snack-bar__action
variant (.snack-bar__action .button__label) still inherits color via normal
cascade without forcing higher specificity or encouraging future overrides.

In `@ui/util/clone.js`:
- Around line 3-17: The cloneDeep utility currently corrupts Dates, Maps, Sets,
RegExps etc.; update the cloneDeep function to use the platform native
structuredClone when available (call structuredClone(value)) so it correctly
handles all standard built-in types and circular refs, and add a small fallback
for older runtimes (e.g., JSON.parse(JSON.stringify(value)) or throw a clear
error) for environments without structuredClone; also add a brief JSDoc on
cloneDeep explaining it prefers structuredClone and what the fallback behavior
is so callers know the semantics.

Comment on lines +234 to +237
case 'languages': {
const sourceLanguages = normalizeLanguageValuesForCompare(metadata.languageList, null);
const targetLanguages = normalizeLanguageValuesForCompare(formValues.languages, formValues.language);
return !areArraysEqualForCompare(sourceLanguages, targetLanguages);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Language array comparison is order-sensitive — same class of bug as the recently-fixed tags case.

normalizeLanguageValuesForCompare normalizes values but does not sort them. A claim with ['en', 'fr'] and a form with ['fr', 'en'] (reordered) produces a false overwrite warning and incorrectly includes 'languages' in fieldsToApply. The tags comparison was fixed in the same function — apply the same fix here.

🐛 Proposed fix
 case 'languages': {
-  const sourceLanguages = normalizeLanguageValuesForCompare(metadata.languageList, null);
-  const targetLanguages = normalizeLanguageValuesForCompare(formValues.languages, formValues.language);
+  const sourceLanguages = normalizeLanguageValuesForCompare(metadata.languageList, null).sort();
+  const targetLanguages = normalizeLanguageValuesForCompare(formValues.languages, formValues.language).sort();
   return !areArraysEqualForCompare(sourceLanguages, targetLanguages);
 }
📝 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
case 'languages': {
const sourceLanguages = normalizeLanguageValuesForCompare(metadata.languageList, null);
const targetLanguages = normalizeLanguageValuesForCompare(formValues.languages, formValues.language);
return !areArraysEqualForCompare(sourceLanguages, targetLanguages);
case 'languages': {
const sourceLanguages = normalizeLanguageValuesForCompare(metadata.languageList, null).sort();
const targetLanguages = normalizeLanguageValuesForCompare(formValues.languages, formValues.language).sort();
return !areArraysEqualForCompare(sourceLanguages, targetLanguages);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/modal/modalCopyFromUpload/view.jsx` around lines 234 - 237, The languages
comparison is order-sensitive because normalizeLanguageValuesForCompare does not
sort values; update the 'languages' case so it sorts the normalized source and
target arrays before calling areArraysEqualForCompare (same approach used for
tags). Locate the 'languages' case in view.jsx and, using
normalizeLanguageValuesForCompare(metadata.languageList, null) and
normalizeLanguageValuesForCompare(formValues.languages, formValues.language),
sort both returned arrays (e.g., by value) prior to passing them to
areArraysEqualForCompare so ['en','fr'] and ['fr','en'] compare equal and
'languages' is not wrongly added to fieldsToApply.

Comment on lines +30 to +44
function cloneTemplateValue(value: any): any {
if (Array.isArray(value)) {
return value.map((item) => cloneTemplateValue(item));
}

if (value && typeof value === 'object') {
const clone = {};
Object.keys(value).forEach((key) => {
clone[key] = cloneTemplateValue(value[key]);
});
return clone;
}

return value;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

cloneTemplateValue is a verbatim duplicate of the new shared cloneDeep — consolidation was incomplete.

modalCopyFromUpload/view.jsx already imports cloneDeep from util/clone, but this file still carries its own copy, directly contradicting the PR's stated intent.

♻️ Proposed fix
+import { cloneDeep } from 'util/clone';
 import './style.scss';
 
-function cloneTemplateValue(value: any): any {
-  if (Array.isArray(value)) {
-    return value.map((item) => cloneTemplateValue(item));
-  }
-
-  if (value && typeof value === 'object') {
-    const clone = {};
-    Object.keys(value).forEach((key) => {
-      clone[key] = cloneTemplateValue(value[key]);
-    });
-    return clone;
-  }
-
-  return value;
-}

Then replace the single call site (line 447):

-        data: cloneTemplateValue(template.data || {}),
+        data: cloneDeep(template.data || {}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/modal/modalUploadTemplates/view.jsx` around lines 30 - 44, Remove the
duplicate cloneTemplateValue function and instead import the shared cloneDeep
utility; replace all calls to cloneTemplateValue(...) with cloneDeep(...),
preserving TS/JS types and ensuring the module import (import cloneDeep from
'util/clone' or existing project import path) is added at the top of the file;
verify there are no remaining references to cloneTemplateValue and run
tests/lint to confirm no unresolved identifiers.

Comment on lines +435 to +456
function handleDuplicate(template: TemplateEntry) {
let createdName = '';
updateTemplatesForChannel(template.channelId, (channelTemplates) => {
const duplicateName = makeDuplicateTemplateName(template.name, channelTemplates);
createdName = duplicateName;
const duplicateTemplate: UploadTemplate = {
...template,
id: uuid(),
name: duplicateName,
createdAt: Date.now(),
lastUsedAt: undefined,
isPinned: false,
data: cloneTemplateValue(template.data || {}),
};

return [duplicateTemplate, ...channelTemplates];
});

if (createdName) {
doToast({ message: __('Template "%name%" duplicated', { name: createdName }) });
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

createdName is always '' at the toast check — success notification is silently dropped.

After the event handler completes React triggers a re-render and processes the queued updates; updater functions run during rendering, so they must be pure and must not produce side effects. createdName = duplicateName is a side effect inside the setTemplatesByChannelId updater. By the time if (createdName) is reached at line 453, the updater has not yet executed, so createdName remains '' and the toast never fires. React Strict Mode (which calls updaters twice) further makes the extraction non-deterministic.

Fix: compute the name synchronously from the current rendered state before calling the updater.

🐛 Proposed fix
 function handleDuplicate(template: TemplateEntry) {
-  let createdName = '';
+  const existingTemplates = templatesByChannelId[template.channelId] || [];
+  const duplicateName = makeDuplicateTemplateName(template.name, existingTemplates);
+
   updateTemplatesForChannel(template.channelId, (channelTemplates) => {
-    const duplicateName = makeDuplicateTemplateName(template.name, channelTemplates);
-    createdName = duplicateName;
     const duplicateTemplate: UploadTemplate = {
       ...template,
       id: uuid(),
       name: duplicateName,
       createdAt: Date.now(),
       lastUsedAt: undefined,
       isPinned: false,
-      data: cloneTemplateValue(template.data || {}),
+      data: cloneDeep(template.data || {}),
     };
 
     return [duplicateTemplate, ...channelTemplates];
   });
 
-  if (createdName) {
-    doToast({ message: __('Template "%name%" duplicated', { name: createdName }) });
-  }
+  doToast({ message: __('Template "%name%" duplicated', { name: duplicateName }) });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/modal/modalUploadTemplates/view.jsx` around lines 435 - 456, The toast
never fires because handleDuplicate mutates createdName inside the
updateTemplatesForChannel updater (a side-effect run later during render); fix
by computing duplicateName synchronously from the current rendered templates for
that channel before calling updateTemplatesForChannel (e.g., read the channel's
templates from the component state/selector, call
makeDuplicateTemplateName(template.name, channelTemplates) to get createdName),
then call updateTemplatesForChannel to insert the new UploadTemplate (using the
precomputed name) and finally call doToast({ message: __('Template "%name%"
duplicated', { name: createdName }) }); ensure no external mutation occurs
inside the updater function.

Comment on lines +108 to +116
.button__content {
padding: 0 var(--spacing-xs);
min-height: 1.75rem;
}

.button:hover,
.button:focus-visible {
background: rgba(0, 0, 0, 0.16);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inherited .snack-bar .button__content:hover background will cover the intended action-button hover effect.

Since .snack-bar__action is a DOM child of .snack-bar, the rule at lines 29–36 — compiled as .snack-bar .button__content:hover { background: var(--color-background); } (specificity 0,3,0) — still fires for .button__content elements inside the action button on hover. This opaque background renders directly on top of (and hides) the rgba(0,0,0,0.16) applied to the outer .button via the new rule at line 115. Additionally, the icon-rotation animation at lines 32–35 would also trigger, which is likely unintended for a text-only action button.

Add a reset inside .snack-bar__action .button__content to neutralize the inherited hover styles:

🐛 Proposed fix
  .button__content {
    padding: 0 var(--spacing-xs);
    min-height: 1.75rem;
+
+   &:hover {
+     background: none;
+     .icon {
+       transform: none;
+     }
+   }
  }
📝 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
.button__content {
padding: 0 var(--spacing-xs);
min-height: 1.75rem;
}
.button:hover,
.button:focus-visible {
background: rgba(0, 0, 0, 0.16);
}
.button__content {
padding: 0 var(--spacing-xs);
min-height: 1.75rem;
&:hover {
background: none;
.icon {
transform: none;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/scss/component/_snack-bar.scss` around lines 108 - 116, The .snack-bar
.button__content:hover rule is unintentionally overriding the .button hover on
action buttons; inside the .snack-bar__action .button__content selector, add a
reset for hover/active styles to neutralize inheritance — e.g., restore
background to transparent (or var(--color-background) as appropriate), remove or
disable the icon rotation animation (animation: none or transform: none), and
ensure no extra min-height/padding changes on hover; update the
.snack-bar__action .button__content rules (and its :hover state) to explicitly
override the inherited .button__content:hover behavior so the outer
.button:hover (rgba(0,0,0,0.16)) remains visible.

- Sorts languages for comparison in the copy from upload modal to prevent false positives.
- Replaces custom deep clone function with `cloneDeep` from `util/clone` for template duplication in the upload template modal, fixing potential issues with the previous implementation.
- Improves snackbar button styles.
@tzarebczan tzarebczan merged commit e476caf into master Feb 20, 2026
2 checks passed
@tzarebczan tzarebczan deleted the publish-improve branch February 20, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant