Adds publish templates and copy from upload#3428
Conversation
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.
📝 WalkthroughWalkthroughAdds 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
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorAdd rollback/re-sync of settings on
setting_updatefailure.The optimistic dispatch at line 2015 updates the store immediately, but the
catchblock (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 thecatchblock 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:getLighthouseClaimIdis overly defensive with multiple property-name guesses.Checking
claimId,claim_id,claimID,claimid, andidsuggests 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
channelIdsCsvthroughencodeURIComponent, which is correct for the value. However, thetermon line 934 is also encoded. The overall query string is built via concatenation rather thanURLSearchParams. 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/memberRestrictionTierIdson lines 1075-1076 is later conditionally overwritten (lines 1119-1138) only if the source/target channels match. If thevisibilityfield is selected butMEMBERS_ONLY_CONTENT_TAGis 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:homepageSectionsin the dependency array is redundant alongsidesettingsByChannelId.
homepageSectionsis derived fromsettingsByChannelId(viarawHomepageSettings), 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 onlyhomepageSections(which already encodes the "settings loaded" information via the null check on line 57-58) and droppingsettingsByChannelIdfrom 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#fffmay not work well with all themes.Lines 89, 95, and 100 use hardcoded
#ffffor 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
selectUploadTemplatesForChannelIdandselectUploadTemplatesForActiveChannelare plain functions. The underlyinggetUploadTemplatesFromSettingsallocates a new array on every call (via.map().filter(Boolean)), so everyuseSelector/mapStateToPropsconsumer will see a new reference and re-render on every unrelated state change.Use
createCachedSelectorkeyed onchannelIdto stabilize the reference, matching the pattern already established byselectFeaturedChannelsForChannelId(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, andgetTemplateKeyare duplicated verbatim inui/modal/modalUploadTemplates/view.jsx(lines 30-44, 61-63, 83-85). Consider extracting them into a shared utility module (e.g., alongsideui/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.
cloneValueis functionally identical tocloneTemplateValuein bothpublishTemplateButton/view.jsxandmodalUploadTemplates/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, andgetTemplateKeyare exact duplicates of the same functions inui/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.
createdNameis written inside thesetTemplatesByChannelIdupdater 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.
| 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 }; | ||
| } |
There was a problem hiding this comment.
🧩 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 -100Repository: 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 -100Repository: 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 -C5Repository: 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 -60Repository: 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
ui/component/publish/shared/publishTemplateButton/view.jsx (1)
107-121:cloneTemplateValueduplicatescloneValuefrommodalCopyFromUpload/view.jsxBoth 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.
| 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 || {})); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
ui/scss/component/_snack-bar.scss (2)
115-115: Hardcodedrgba(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:!importantoncolor: inheritis unnecessary and can be removed.
.snack-bar__action .button:hover .button__labeland.button:focus-visible .button__labelhave specificity 0,4,0, which already outranks the.snack-bar .button__labeldeclaration at line 19 (0,2,0). The snack-bar action button has no button variant class (.button--primary,.button--secondary, etc.), so global!importantrules from the button component do not apply here. The!importantis 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:handleSavecloses the modal optimistically — failed API writes are silent.
doUpdateCreatorSettingsis fired for each changed channel without awaiting its result. The success toast anddoHideModal()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
doUpdateCreatorSettingscall 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:cloneDeepsilently corruptsDate,Map,Set, andRegExpvalues.
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.
| case 'languages': { | ||
| const sourceLanguages = normalizeLanguageValuesForCompare(metadata.languageList, null); | ||
| const targetLanguages = normalizeLanguageValuesForCompare(formValues.languages, formValues.language); | ||
| return !areArraysEqualForCompare(sourceLanguages, targetLanguages); |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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 }) }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| .button__content { | ||
| padding: 0 var(--spacing-xs); | ||
| min-height: 1.75rem; | ||
| } | ||
|
|
||
| .button:hover, | ||
| .button:focus-visible { | ||
| background: rgba(0, 0, 0, 0.16); | ||
| } |
There was a problem hiding this comment.
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.
| .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.
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?
Please check all that apply to this PR using "x":
Summary by CodeRabbit
New Features
Bug Fixes / UX
Style
Other