diff --git a/nx/blocks/media-library/core/export.js b/nx/blocks/media-library/core/export.js index bfa8df78f..512ffd975 100644 --- a/nx/blocks/media-library/core/export.js +++ b/nx/blocks/media-library/core/export.js @@ -1,7 +1,7 @@ import { etcFetch } from './urls.js'; import { getMediaType, getSubtype } from './media.js'; import { decodeDisplayName, getFileName } from './files.js'; -import { t } from './messages.js'; +import { getMessage } from './messages.js'; import { isMediaLibraryPluginMode, withTimeout } from './utils.js'; function escapeCsvCell(value) { @@ -187,8 +187,8 @@ export async function copyMediaToClipboard(media) { try { await copyNonImageLinkRichClipboard(media); return { - heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'), - message: t('NOTIFY_PLUGIN_FALLBACK_LINK_MSG'), + heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'), + message: getMessage('NOTIFY_PLUGIN_FALLBACK_LINK_MSG'), }; } catch { /* fall through to plain writeText */ @@ -202,23 +202,23 @@ export async function copyMediaToClipboard(media) { await copyImageToClipboard(mediaUrl); if (plugin) { return { - heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'), - message: t('NOTIFY_PLUGIN_FALLBACK_IMAGE_MSG'), + heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'), + message: getMessage('NOTIFY_PLUGIN_FALLBACK_IMAGE_MSG'), }; } - return { heading: t('NOTIFY_COPIED'), message: t('NOTIFY_COPIED_IMAGE') }; + return { heading: getMessage('NOTIFY_COPIED'), message: getMessage('NOTIFY_COPIED_IMAGE') }; } await navigator.clipboard.writeText(mediaUrl); if (plugin) { return { - heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'), - message: t('NOTIFY_PLUGIN_FALLBACK_PLAIN_MSG'), + heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'), + message: getMessage('NOTIFY_PLUGIN_FALLBACK_PLAIN_MSG'), }; } - return { heading: t('NOTIFY_COPIED'), message: t('NOTIFY_COPIED_URL') }; + return { heading: getMessage('NOTIFY_COPIED'), message: getMessage('NOTIFY_COPIED_URL') }; } catch (error) { // eslint-disable-next-line no-console console.error('Failed to copy to clipboard:', error); - return { heading: t('NOTIFY_ERROR'), message: t('NOTIFY_COPY_ERROR') }; + return { heading: getMessage('NOTIFY_ERROR'), message: getMessage('NOTIFY_COPY_ERROR') }; } } diff --git a/nx/blocks/media-library/core/ims-adapter.js b/nx/blocks/media-library/core/ims-adapter.js new file mode 100644 index 000000000..e0cf7d325 --- /dev/null +++ b/nx/blocks/media-library/core/ims-adapter.js @@ -0,0 +1,104 @@ +/** + * IMS adapter for media-library + * Supports plugin mode token injection and falls back to nx2/utils/ims.js for app mode + */ + +let cachedImsDetails; +let imsLoadPromise; +let isRedirectingToSignIn = false; + +/** + * Inject IMS token for plugin mode + * @param {string} token - Bearer token from plugin host + */ +export function setImsDetails(token) { + cachedImsDetails = { accessToken: { token } }; + imsLoadPromise = Promise.resolve(cachedImsDetails); +} + +/** + * Initialize IMS authentication + * @returns {Promise} IMS details with accessToken + */ +export async function initIms() { + if (cachedImsDetails && !cachedImsDetails.anonymous) return cachedImsDetails; + if (imsLoadPromise) return imsLoadPromise; + + imsLoadPromise = (async () => { + const { loadIms } = await import('../../../../nx2/utils/ims.js'); + try { + const imsDetails = await loadIms(); + + // Only cache authenticated sessions + if (imsDetails && !imsDetails.anonymous) { + cachedImsDetails = imsDetails; + } + return imsDetails; + } catch (_) { + // Fallback: if IMS timeout but token exists in window, use it + if (window.adobeIMS?.getAccessToken) { + const activeToken = window.adobeIMS.getAccessToken(); + if (activeToken) { + const accessToken = typeof activeToken === 'string' ? { token: activeToken } : activeToken; + const fallbackDetails = { accessToken, anonymous: false }; + cachedImsDetails = fallbackDetails; + return fallbackDetails; + } + } + + imsLoadPromise = null; + return null; + } + })(); + + return imsLoadPromise; +} + +/** + * Authenticated fetch with IMS token + * @param {string} url - URL to fetch + * @param {Object} opts - Fetch options + * @returns {Promise} Fetch response + */ +export const daFetch = async (url, opts = {}) => { + opts.headers ||= {}; + + // Add auth header if user is authenticated + const hasAuthSession = !!localStorage.getItem('nx-ims'); + if (hasAuthSession || cachedImsDetails) { + const imsDetails = await initIms(); + if (imsDetails && !imsDetails.anonymous && imsDetails.accessToken) { + opts.headers.Authorization = `Bearer ${imsDetails.accessToken.token}`; + } + } + + let response; + try { + response = await fetch(url, opts); + } catch (err) { + response = new Response(null, { status: 500, statusText: err.message }); + } + + // Smart 401 handling: auto-redirect for anonymous users, show error for authenticated users + if (response.status === 401) { + const returningFromSignIn = new URLSearchParams(window.location.search).get('from_ims') === 'true' + || window.location.hash.includes('from_ims=true'); + + if (!returningFromSignIn) { + const userHasAuthSession = !!localStorage.getItem('nx-ims'); + const hasActiveToken = !!window.adobeIMS?.getAccessToken?.(); + + if (!userHasAuthSession && !hasActiveToken && !isRedirectingToSignIn) { + // Anonymous user - auto-redirect to sign-in + isRedirectingToSignIn = true; + const { handleSignIn } = await import('../../../../nx2/utils/ims.js'); + handleSignIn(); + } + } + } + + // Add DA permissions metadata + response.permissions = response.headers.get('x-da-actions')?.split('=').pop().split(','); + + return response; +}; diff --git a/nx/blocks/media-library/core/indexing-adapter.js b/nx/blocks/media-library/core/indexing-adapter.js index 601853a01..4eb16f623 100644 --- a/nx/blocks/media-library/core/indexing-adapter.js +++ b/nx/blocks/media-library/core/indexing-adapter.js @@ -6,7 +6,7 @@ */ import { updateAppState, getAppState, showNotification } from './state.js'; -import { t } from './messages.js'; +import { getMessage } from './messages.js'; import { clearProcessDataCache } from '../ui/filters.js'; import { getDedupeKey } from './urls.js'; import { getCanonicalMediaTimestamp } from './utils.js'; @@ -82,22 +82,22 @@ function getErrorMessage(errorCode, technicalMessage) { switch (errorCode) { case IndexingErrorCode.AUTH_REQUIRED: case IndexingErrorCode.AUTH_FAILED: - return t('NOTIFY_SIGN_IN'); + return getMessage('NOTIFY_SIGN_IN'); case IndexingErrorCode.DA_READ_DENIED: - return t('DA_READ_DENIED'); + return getMessage('DA_READ_DENIED'); case IndexingErrorCode.DA_WRITE_DENIED: case IndexingErrorCode.DA_SAVE_FAILED: - return t('DA_SAVE_FAILED'); + return getMessage('DA_SAVE_FAILED'); case IndexingErrorCode.INDEX_PARSE_ERROR: - return t('INDEX_PARSE_ERROR'); + return getMessage('INDEX_PARSE_ERROR'); case IndexingErrorCode.LOCK_CREATE_FAILED: - return t('LOCK_CREATE_FAILED_GENERIC'); + return getMessage('LOCK_CREATE_FAILED_GENERIC'); case IndexingErrorCode.LOCK_REMOVE_FAILED: - return t('LOCK_REMOVE_FAILED'); + return getMessage('LOCK_REMOVE_FAILED'); case IndexingErrorCode.NETWORK_TIMEOUT: case IndexingErrorCode.BUILD_FAILED: default: - return technicalMessage || t('NOTIFY_DISCOVERY_FAILED'); + return technicalMessage || getMessage('NOTIFY_DISCOVERY_FAILED'); } } @@ -147,7 +147,7 @@ export function handleIndexingEvent(event, onMediaDataUpdated) { clearProcessDataCache(); if (event.lockRemoveFailed) { - showNotification(t('NOTIFY_WARNING'), t('LOCK_REMOVE_FAILED'), 'danger'); + showNotification(getMessage('NOTIFY_WARNING'), getMessage('LOCK_REMOVE_FAILED'), 'danger'); } if (event.hasChanges && event.data && event.data.length > 0) { @@ -214,9 +214,9 @@ export function handleIndexingEvent(event, onMediaDataUpdated) { // Show notification for non-persistent errors if (!event.isPersistent && event.code !== IndexingErrorCode.AUTH_REQUIRED) { const userMessage = getErrorMessage(event.code, event.message); - showNotification(t('NOTIFY_ERROR'), userMessage, 'danger'); + showNotification(getMessage('NOTIFY_ERROR'), userMessage, 'danger'); } else if (event.code === IndexingErrorCode.AUTH_REQUIRED) { - showNotification(t('NOTIFY_ERROR'), t('NOTIFY_SIGN_IN'), 'danger'); + showNotification(getMessage('NOTIFY_ERROR'), getMessage('NOTIFY_SIGN_IN'), 'danger'); } resetProgressiveState(); diff --git a/nx/blocks/media-library/core/messages.js b/nx/blocks/media-library/core/messages.js index 9b1b75cbc..52fc89f66 100644 --- a/nx/blocks/media-library/core/messages.js +++ b/nx/blocks/media-library/core/messages.js @@ -22,11 +22,16 @@ const MESSAGES = { VALIDATION_PATH_EMPTY: 'Parent path not found or empty: {path}', VALIDATION_SITE_PATH_FILE: 'Site path cannot point to a file', VALIDATION_ENTER_SITE_URL: 'Enter a site URL to start. Example: https://main--site--org.aem.page', + VALIDATION_SERVER_ERROR: 'Server error occurred. Please try again or sign in if you\'re not authenticated.', + VALIDATION_NETWORK_ERROR: 'Network error. Check your connection and try again.', DA_WRITE_DENIED: "Couldn't save your media results. You can still browse current results, but your changes weren't saved. Try again, refresh the page, or contact your admin for write access.", DA_SAVE_FAILED: "Couldn't save your media results. You can still browse current results, but your changes weren't saved. Try again, refresh the page, or contact your admin.", PARTIAL_SAVE: "Some save steps didn't complete. Media results may be incomplete or out of date. Try refreshing the page.", - EDS_LOG_DENIED: 'You need Author or higher permissions on EDS to see the latest media data. You can still browse existing media.', + EDS_LOG_DENIED: 'You need one of these roles in the EDS project: author, publish, or admin. Contact your project admin to add you to one of these roles. You can still browse existing media.', + EDS_LOG_DENIED_HINT: 'The author role (or higher: publish, admin) is required for log:read permission. Note: basic_author and basic_publish do NOT have log:read access.', EDS_AUTH_EXPIRED: 'Session expired. Sign in again.', + PROTECTED_SITE_IMAGES_WARNING: 'Protected site - some images may not load, refresh or login into site in a new tab using sidekick', + DATA_LOAD_FAILED: 'Failed to load media data. Please ensure you are signed in.', NOTIFY_SIGN_IN: 'Sign in to run discovery.', NOTIFY_VERIFY_AUTH: 'Failed to verify authentication.', @@ -89,7 +94,7 @@ const MESSAGES = { UI_OPEN_IN_NEW_TAB: 'Open in new tab', }; -export function t(key, params = {}) { +export function getMessage(key, params = {}) { const str = MESSAGES[key]; if (str == null) return key; diff --git a/nx/blocks/media-library/core/paths.js b/nx/blocks/media-library/core/paths.js index 27271aae2..2e9821386 100644 --- a/nx/blocks/media-library/core/paths.js +++ b/nx/blocks/media-library/core/paths.js @@ -1,8 +1,8 @@ -import { daFetch } from '../../../utils/daFetch.js'; +import { daFetch } from './ims-adapter.js'; import { DA_ADMIN } from '../../../utils/utils.js'; import { Paths, Domains, DA_LIVE_EDIT_BASE } from './constants.js'; import { ErrorCodes, logMediaLibraryError } from './errors.js'; -import { t } from './messages.js'; +import { getMessage } from './messages.js'; import { normalizeSitePath as _normalizeSitePath, getContentPathFromSitePath as _getContentPathFromSitePath, @@ -172,7 +172,7 @@ export function resolveAbsolutePath(path, isFolder = false) { export async function validateSitePath(sitePath) { if (!sitePath) { logMediaLibraryError(ErrorCodes.VALIDATION_SITE_PATH_MISSING, {}); - return { valid: false, error: t('VALIDATION_ENTER_SITE_URL') }; + return { valid: false, error: getMessage('VALIDATION_ENTER_SITE_URL') }; } const parts = sitePath.split('/').filter(Boolean); @@ -181,7 +181,7 @@ export async function validateSitePath(sitePath) { logMediaLibraryError(ErrorCodes.VALIDATION_SITE_PATH_MISSING, {}); return { valid: false, - error: t('VALIDATION_ENTER_SITE_URL'), + error: getMessage('VALIDATION_ENTER_SITE_URL'), }; } @@ -200,7 +200,7 @@ export async function validateSitePath(sitePath) { logMediaLibraryError(ErrorCodes.VALIDATION_SITE_NOT_FOUND, { path: `/${org}/${repo}`, status: 404 }); return { valid: false, - error: t('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }), + error: getMessage('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }), }; } @@ -209,18 +209,22 @@ export async function validateSitePath(sitePath) { if (resp.status === 404) { logMediaLibraryError(ErrorCodes.VALIDATION_SITE_NOT_FOUND, { path: `/${org}/${repo}`, status: 404 }); - return { valid: false, error: t('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }) }; + return { valid: false, error: getMessage('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }) }; } if (resp.status === 401 || resp.status === 403) { logMediaLibraryError(ErrorCodes.DA_READ_DENIED, { path: `/${org}/${repo}`, status: resp.status }); return { valid: false, - error: t('VALIDATION_SITE_403'), - suggestion: t('VALIDATION_SITE_403_SUGGESTION'), + error: getMessage('VALIDATION_SITE_403'), + suggestion: getMessage('VALIDATION_SITE_403_SUGGESTION'), }; } + if (resp.status >= 500) { + return { valid: false, error: getMessage('VALIDATION_SERVER_ERROR') }; + } + return { valid: false, error: `Validation failed: ${resp.status}` }; } catch (error) { return { valid: false, error: error.message }; @@ -246,7 +250,7 @@ export async function validateSitePath(sitePath) { }); return { valid: false, - error: t('VALIDATION_PATH_NOT_FOUND', { path: parentPath }), + error: getMessage('VALIDATION_PATH_NOT_FOUND', { path: parentPath }), }; } @@ -257,11 +261,15 @@ export async function validateSitePath(sitePath) { ); return { valid: false, - error: t('VALIDATION_PATH_403'), - suggestion: t('VALIDATION_PATH_403_SUGGESTION'), + error: getMessage('VALIDATION_PATH_403'), + suggestion: getMessage('VALIDATION_PATH_403_SUGGESTION'), }; } + if (resp.status >= 500) { + return { valid: false, error: getMessage('VALIDATION_SERVER_ERROR') }; + } + return { valid: false, error: `Validation failed: ${resp.status}` }; } @@ -271,7 +279,7 @@ export async function validateSitePath(sitePath) { if (!items || items.length === 0) { return { valid: false, - error: t('VALIDATION_PATH_EMPTY', { path: parentPath }), + error: getMessage('VALIDATION_PATH_EMPTY', { path: parentPath }), }; } @@ -284,15 +292,15 @@ export async function validateSitePath(sitePath) { logMediaLibraryError(ErrorCodes.VALIDATION_PATH_NOT_FOUND, { path: `${parentPath}/${lastSegment}`, status: 404 }); return { valid: false, - error: t('VALIDATION_PATH_NOT_FOUND_CHILD', { path: `/${[org, repo, ...restPath].join('/')}` }), - suggestion: t('VALIDATION_PATH_NOT_FOUND_SUGGESTION', { segment: lastSegment, parentPath }), + error: getMessage('VALIDATION_PATH_NOT_FOUND_CHILD', { path: `/${[org, repo, ...restPath].join('/')}` }), + suggestion: getMessage('VALIDATION_PATH_NOT_FOUND_SUGGESTION', { segment: lastSegment, parentPath }), }; } if (targetEntry.ext) { return { valid: false, - error: t('VALIDATION_SITE_PATH_FILE'), + error: getMessage('VALIDATION_SITE_PATH_FILE'), suggestion: parentPath, isFile: true, fileType: targetEntry.ext, diff --git a/nx/blocks/media-library/core/utils.js b/nx/blocks/media-library/core/utils.js index 9366e3c77..ec3831753 100644 --- a/nx/blocks/media-library/core/utils.js +++ b/nx/blocks/media-library/core/utils.js @@ -129,7 +129,8 @@ export async function checkSiteAuthRequired(org, repo) { try { const response = await etcFetch(indexUrl, 'cors', { method: 'HEAD' }); - const requiresAuth = response.status === 401 || response.status === 403; + const requiresAuth = response.status === 401 + || response.status === 403; const result = { requiresAuth, status: response.status }; debugLog('Site auth check result', result); @@ -145,8 +146,8 @@ export async function checkSiteAuthRequired(org, repo) { export async function livePreviewLogin(owner, repo) { try { - const { loadIms } = await import('../../../../nx2/utils/ims.js'); - const { accessToken } = await loadIms() || {}; + const { initIms } = await import('./ims-adapter.js'); + const { accessToken } = (await initIms()) || {}; const url = `${getLivePreviewUrl(owner, repo)}/gimme_cookie`; debugLog('Setting preview.da.live cookie', { owner, repo, url }); diff --git a/nx/blocks/media-library/indexing/admin-api.js b/nx/blocks/media-library/indexing/admin-api.js index 04f91c9ad..f5139f908 100644 --- a/nx/blocks/media-library/indexing/admin-api.js +++ b/nx/blocks/media-library/indexing/admin-api.js @@ -1,4 +1,4 @@ -import { daFetch } from '../../../utils/daFetch.js'; +import { daFetch } from '../core/ims-adapter.js'; import { DA_ADMIN, HLX_ADMIN } from '../../../utils/utils.js'; import { etcFetch } from '../core/urls.js'; import { @@ -8,7 +8,7 @@ import { } from '../core/constants.js'; import { MediaLibraryError, ErrorCodes, logMediaLibraryError } from '../core/errors.js'; import { isPerfEnabled } from '../core/params.js'; -import { t } from '../core/messages.js'; +import { getMessage } from '../core/messages.js'; const AEM_PAGE_MARKDOWN_RATE = 180; /* keep some headroom under the 200 RPS host limit */ const AEM_SITE_AUTH_DENIED = new Set([401, 403]); @@ -368,22 +368,34 @@ export async function streamLog( if (!resp.ok) { if (resp.status === 403) { - logMediaLibraryError(ErrorCodes.EDS_LOG_DENIED, { status: 403, endpoint: nextUrl }); - throw new MediaLibraryError( + logMediaLibraryError( ErrorCodes.EDS_LOG_DENIED, - t('EDS_LOG_DENIED'), { status: 403, endpoint: nextUrl }, ); + const err = new MediaLibraryError( + ErrorCodes.EDS_LOG_DENIED, + getMessage('EDS_LOG_DENIED'), + { status: 403, endpoint: nextUrl, hint: getMessage('EDS_LOG_DENIED_HINT') }, + ); + err.status = 403; + throw err; } if (resp.status === 401) { - logMediaLibraryError(ErrorCodes.EDS_AUTH_EXPIRED, { status: 401, endpoint: nextUrl }); - throw new MediaLibraryError( + logMediaLibraryError( ErrorCodes.EDS_AUTH_EXPIRED, - t('EDS_AUTH_EXPIRED'), { status: 401, endpoint: nextUrl }, ); + const err = new MediaLibraryError( + ErrorCodes.EDS_AUTH_EXPIRED, + getMessage('EDS_AUTH_EXPIRED'), + { status: 401, endpoint: nextUrl }, + ); + err.status = 401; + throw err; } - throw new Error(`${endpoint} API error: ${resp.status} ${resp.statusText}`); + const err = new Error(`${endpoint} API error: ${resp.status} ${resp.statusText}`); + err.status = resp.status; + throw err; } const data = await resp.json(); diff --git a/nx/blocks/media-library/indexing/build.js b/nx/blocks/media-library/indexing/build.js index ba56b026b..6559b4cfb 100644 --- a/nx/blocks/media-library/indexing/build.js +++ b/nx/blocks/media-library/indexing/build.js @@ -79,7 +79,9 @@ async function runWorkerBuild( // When running with ?nx=local, files load from localhost but page is on da.live // Workers must be same-origin, so we create a blob URL const workerUrl = new URL('./worker/worker.js', import.meta.url).href; - const response = await fetch(workerUrl); + // Add cache-busting to ensure fresh worker code is loaded + const cacheBustedUrl = `${workerUrl}?t=${Date.now()}`; + const response = await fetch(cacheBustedUrl, { cache: 'no-store' }); if (!response.ok) { throw new Error(`Failed to fetch worker code: ${response.status}`); } @@ -87,13 +89,14 @@ async function runWorkerBuild( let workerCode = await response.text(); // Replace ALL relative imports with absolute URLs so worker can fetch them - // This converts: import './foo.js' → import 'http://localhost:6456/.../foo.js' + // This converts: import './foo.js' → import 'http://localhost:6456/.../foo.js?t=...' const baseUrl = new URL('./worker/', import.meta.url).href; + const cacheBuster = Date.now(); workerCode = workerCode.replace( /from\s+['"](\.\.[^'"]*|\.\/[^'"]*)['"]/g, (match, path) => { const absoluteUrl = new URL(path, baseUrl).href; - return `from '${absoluteUrl}'`; + return `from '${absoluteUrl}?t=${cacheBuster}'`; }, ); @@ -154,7 +157,10 @@ async function runWorkerBuild( resolve(data); } else if (type === 'error') { clearTimeout(watchdogTimer); - reject(new Error(error.message || 'Worker error')); + const err = new Error(error.message || 'Worker error'); + err.code = error.code; + err.status = error.status; + reject(err); } }; diff --git a/nx/blocks/media-library/indexing/linked-content.js b/nx/blocks/media-library/indexing/linked-content.js index 972d4727b..669853281 100644 --- a/nx/blocks/media-library/indexing/linked-content.js +++ b/nx/blocks/media-library/indexing/linked-content.js @@ -1,4 +1,5 @@ import { fetchFileGetInfo, fetchFileHeadInfo } from './admin-api.js'; +import { Operation } from '../core/constants.js'; import { isPdf, isSvg, @@ -10,7 +11,6 @@ import { toExternalMediaEntry, } from './parse.js'; import { normalizePath } from './parse-utils.js'; -import { Operation } from '../core/constants.js'; import { isIndexedExternalMediaOperation, isIndexedExternalMediaEntry, diff --git a/nx/blocks/media-library/indexing/locks.js b/nx/blocks/media-library/indexing/locks.js index 598c26a72..1ba9456e9 100644 --- a/nx/blocks/media-library/indexing/locks.js +++ b/nx/blocks/media-library/indexing/locks.js @@ -5,11 +5,11 @@ * It handles lock creation, refresh (heartbeat), removal, ownership, and checking. */ -import { daFetch } from '../../../utils/daFetch.js'; +import { daFetch } from '../core/ims-adapter.js'; import { DA_ADMIN } from '../../../utils/utils.js'; import { createSheet } from './admin-api.js'; import { MediaLibraryError, ErrorCodes, logMediaLibraryError } from '../core/errors.js'; -import { t } from '../core/messages.js'; +import { getMessage } from '../core/messages.js'; import { IndexFiles, IndexConfig } from '../core/constants.js'; const LOCK_OWNER_STORAGE_KEY = 'media-library-lock-owner-id'; @@ -97,8 +97,9 @@ export async function createIndexLock(sitePath) { }); if (!resp.ok) { logMediaLibraryError(ErrorCodes.LOCK_CREATE_FAILED, { status: resp.status, path }); - const isDenied = resp.status === 401 || resp.status === 403; - const msg = isDenied ? t('LOCK_CREATE_FAILED_PERMISSION') : t('LOCK_CREATE_FAILED_GENERIC'); + const isDenied = resp.status === 401 + || resp.status === 403; + const msg = isDenied ? getMessage('LOCK_CREATE_FAILED_PERMISSION') : getMessage('LOCK_CREATE_FAILED_GENERIC'); throw new MediaLibraryError(ErrorCodes.LOCK_CREATE_FAILED, msg, { status: resp.status, path }); } return resp; @@ -121,8 +122,9 @@ export async function refreshIndexLock(sitePath, lockData = {}) { }); if (!resp.ok) { logMediaLibraryError(ErrorCodes.LOCK_CREATE_FAILED, { status: resp.status, path }); - const isDenied = resp.status === 401 || resp.status === 403; - const msg = isDenied ? t('LOCK_CREATE_FAILED_PERMISSION') : t('LOCK_CREATE_FAILED_GENERIC'); + const isDenied = resp.status === 401 + || resp.status === 403; + const msg = isDenied ? getMessage('LOCK_CREATE_FAILED_PERMISSION') : getMessage('LOCK_CREATE_FAILED_GENERIC'); throw new MediaLibraryError(ErrorCodes.LOCK_CREATE_FAILED, msg, { status: resp.status, path }); } return resp; @@ -136,7 +138,7 @@ export async function removeIndexLock(sitePath) { if (!resp.ok) { if (resp.status === 404) return resp; logMediaLibraryError(ErrorCodes.LOCK_REMOVE_FAILED, { status: resp.status, path }); - throw new MediaLibraryError(ErrorCodes.LOCK_REMOVE_FAILED, t('LOCK_REMOVE_FAILED'), { status: resp.status, path }); + throw new MediaLibraryError(ErrorCodes.LOCK_REMOVE_FAILED, getMessage('LOCK_REMOVE_FAILED'), { status: resp.status, path }); } return resp; } diff --git a/nx/blocks/media-library/indexing/worker/fetch.js b/nx/blocks/media-library/indexing/worker/fetch.js index a2c2da432..9f7ae679e 100644 --- a/nx/blocks/media-library/indexing/worker/fetch.js +++ b/nx/blocks/media-library/indexing/worker/fetch.js @@ -448,7 +448,9 @@ export async function fetchPageMarkdown( // Retry on 401/403 with fresh token (protected sites) // Main thread clears cache and refetches, so even if token value is same, retry is valid - if ((resp.status === 401 || resp.status === 403) && siteToken) { + const isAuthError = resp.status === 401 + || resp.status === 403; + if (isAuthError && siteToken) { const freshToken = await requestTokenRefresh(); if (freshToken) { // Retry with fresh token (even if same value - cache was cleared on main thread) @@ -674,18 +676,34 @@ export async function streamLog( const baseUrl = `https://admin.hlx.page/${endpoint}/${org}/${repo}/${ref}`; const separator = endpoint === 'medialog' ? '/' : ''; let nextUrl = `${baseUrl}${separator}?${fetchParams.toString()}`; + let currentToken = imsToken; while (nextUrl) { - const resp = await workerFetchWithAuth(nextUrl, imsToken); + let resp = await workerFetchWithAuth(nextUrl, currentToken); + + // Retry once with fresh IMS token on 401 + if (!resp.ok && resp.status === 401) { + const freshImsToken = await requestTokenRefresh(); + if (freshImsToken) { + currentToken = freshImsToken; + resp = await workerFetchWithAuth(nextUrl, currentToken); + } + } if (!resp.ok) { if (resp.status === 403) { - throw new Error(`403 Forbidden: ${endpoint} access denied for ${nextUrl}`); + const err = new Error(`403 Forbidden: ${endpoint} access denied for ${nextUrl}`); + err.status = 403; + throw err; } if (resp.status === 401) { - throw new Error(`401 Unauthorized: IMS token expired for ${nextUrl}`); + const err = new Error(`401 Unauthorized: IMS token expired for ${nextUrl}`); + err.status = 401; + throw err; } - throw new Error(`${endpoint} API error: ${resp.status} ${resp.statusText}`); + const err = new Error(`${endpoint} API error: ${resp.status} ${resp.statusText}`); + err.status = resp.status; + throw err; } const data = await resp.json(); diff --git a/nx/blocks/media-library/indexing/worker/full.js b/nx/blocks/media-library/indexing/worker/full.js index 63372c8c1..540cdcdbf 100644 --- a/nx/blocks/media-library/indexing/worker/full.js +++ b/nx/blocks/media-library/indexing/worker/full.js @@ -157,8 +157,6 @@ export async function buildFullIndex( const deletedPaths = new Set(); const deletedPages = new Set(); const medialogStart = Date.now(); - const medialogChunks = []; - const auditlogChunks = []; let medialogResourcePathCount = 0; const earlyLinkedEntries = []; @@ -181,12 +179,32 @@ export async function buildFullIndex( const contentPath = getContentPathFromSitePath(sitePath); const progressiveMediaMap = new Map(); + + // Accumulate medialog entries directly (not chunks) to save memory + const medialogEntries = []; + let totalMedialogEntries = 0; + const medialogPromise = streamLog('medialog', org, repo, ref, null, IndexConfig.API_PAGE_SIZE, (chunk) => { perf.medialog.chunks += 1; chunk.forEach((m) => { if (m.resourcePath) medialogResourcePathCount += 1; + + // Field projection: Only keep the 10 fields actually used in processing + // This reduces memory by ~40-50% (257K entries with 10 fields vs 20-30 fields) + medialogEntries.push({ + path: m.path, + resourcePath: m.resourcePath, + mediaHash: m.mediaHash, + timestamp: m.timestamp, + user: m.user, + operation: m.operation, + originalFilename: m.originalFilename, + contentType: m.contentType, + modifiedTimestamp: m.modifiedTimestamp, + originalPath: m.originalPath, + }); }); - medialogChunks.push(chunk); + totalMedialogEntries += chunk.length; const pathScope = contentPath || ''; mergeMedialogChunkIntoMap(chunk, progressiveMediaMap, org, repo, pathScope); @@ -197,14 +215,13 @@ export async function buildFullIndex( onProgress({ stage: 'fetching', - message: `Status job polling, Medialog: ${medialogChunks.reduce((s, c) => s + c.length, 0)} entries...`, + message: `Status job polling, Medialog: ${totalMedialogEntries} entries...`, }); }, imsToken, { fullHistory: true }); - // Fetch auditlog for fragment/PDF/SVG file discovery (like helix-tools does) - const auditlogPromise = streamLog('log', org, repo, ref, null, IndexConfig.API_PAGE_SIZE, (chunk) => { - auditlogChunks.push(chunk); - }, imsToken, {}); + // Note: Auditlog NOT fetched for full builds - Status API provides all files + // (fragments, PDFs, SVGs) with accurate timestamps. Auditlog only needed for + // incremental builds to track changes over time. try { const progressCallback = (p) => { @@ -223,13 +240,15 @@ export async function buildFullIndex( isPerfEnabled, }); - // Run status API, auditlog, and medialog in parallel + // Run status API and medialog in parallel const bulkStatusResult = (await Promise.all([ statusPromise, - auditlogPromise, medialogPromise, ]))[0]; + // Release progressive media map after streaming completes (~5-10MB) + progressiveMediaMap.clear(); + const { resources, perf: bulkPerf } = bulkStatusResult; perf.statusAPI.jobCreationMs = bulkPerf.jobCreationMs; @@ -246,9 +265,11 @@ export async function buildFullIndex( perf.statusAPI.partitionDetailsMs = bulkPerf.partitionDetailsMs; } - const payloadJson = JSON.stringify(resources); - const payloadSizeKB = Math.round((payloadJson.length / 1024) * 10) / 10; - const payloadSizeMB = Math.round((payloadJson.length / (1024 * 1024)) * 100) / 100; + // Estimate payload size instead of JSON.stringify to avoid 10MB+ string allocation + const estimatedBytesPerResource = 200; + const estimatedBytes = resources.length * estimatedBytesPerResource; + const payloadSizeKB = Math.round((estimatedBytes / 1024) * 10) / 10; + const payloadSizeMB = Math.round((estimatedBytes / (1024 * 1024)) * 100) / 100; perf.statusAPI.payloadSizeKB = payloadSizeKB; if (payloadSizeMB >= 1) { perf.statusAPI.payloadSizeMB = payloadSizeMB; @@ -295,39 +316,22 @@ export async function buildFullIndex( perf.statusAPI.fragmentsDiscovered = fragmentCount; perf.statusAPI.filesDiscovered = fileCount; - // Process auditlog entries for fragment/PDF/SVG files (like helix-tools) - const auditlogEntries = auditlogChunks.flat(); - const auditlogFiles = auditlogEntries.filter( - (e) => e.route === 'preview' && !isPage(e.path) && (isPdfOrSvg(e.path) || isFragmentDoc(e.path)), - ); - - auditlogFiles.forEach((e) => { - const fp = toAbsoluteFilePath(e.path); - const syntheticEvent = { - path: e.path, - timestamp: e.timestamp || currentTimestamp, - user: e.user || '', - method: e.method || 'UPDATE', - route: 'preview', - }; - const existing = filesByPath.get(fp); - if (!existing || syntheticEvent.timestamp < existing.timestamp) { - filesByPath.set(fp, syntheticEvent); - } - }); + // Release resources array after processing into maps (~5-10MB) + resources.length = 0; emitEarlyLinked(); } finally { await medialogPromise?.catch(() => {}); - await auditlogPromise?.catch(() => {}); } - perf.medialog.streamed = medialogChunks.reduce((s, c) => s + c.length, 0); + perf.medialog.streamed = medialogEntries.length; perf.medialog.resourcePathCount = medialogResourcePathCount; perf.medialog.durationMs = Date.now() - medialogStart; const pages = []; pagesByPath.forEach((events) => pages.push(...events)); + // Release pagesByPath after converting to array (~2-5MB) + pagesByPath.clear(); perf.statusAPI.pagesForParsing = pages.length; @@ -342,13 +346,16 @@ export async function buildFullIndex( message: `Building index from ${perf.medialog.streamed} medialog (page-based)...`, }); - let medialogEntries = medialogChunks.flat(); + // medialogEntries already accumulated during streaming - filter by contentPath if needed if (contentPath) { const pathPrefix = contentPath.endsWith('/') ? contentPath : `${contentPath}/`; const isUnderPath = (path) => path === contentPath || (path && path.startsWith(pathPrefix)); - medialogEntries = medialogEntries.filter( + const filtered = medialogEntries.filter( (m) => m.resourcePath && isUnderPath(normalizePath(m.resourcePath)), ); + // Replace with filtered version + medialogEntries.length = 0; + medialogEntries.push(...filtered); } const canonicalTimestamps = buildCanonicalTimestampMap(medialogEntries); @@ -403,17 +410,7 @@ export async function buildFullIndex( index.push(entry); }); - // Build oldUsageMap from medialog-based index (before markdown validation) - // This represents what medialog says is on each page - const oldUsageMap = new Map(); - index.forEach((entry) => { - if (entry.doc && entry.hash) { - if (!oldUsageMap.has(entry.doc)) { - oldUsageMap.set(entry.doc, new Set()); - } - oldUsageMap.get(entry.doc).add(entry.hash); - } - }); + // oldUsageMap removed - was built but never used in full builds (~5-10MB saved) // Normalize hash format in index entries built from medialog // Hash should always be bare (e.g. "abc123"), never with prefix (e.g. "media_abc123.jpg") @@ -423,14 +420,7 @@ export async function buildFullIndex( } }); - // Build existingIndexMap for markdown parsing (metadata lookup) - const existingIndexMap = new Map(); - index.forEach((entry) => { - const dedupeKey = getDedupeKey(entry.url); - if (!existingIndexMap.has(dedupeKey)) { - existingIndexMap.set(dedupeKey, entry); - } - }); + // existingIndexMap removed - was built but never used in full builds (~5-10MB saved) deletedPages.forEach((doc) => { const toRemove = index.filter((e) => e.doc === doc); @@ -439,6 +429,9 @@ export async function buildFullIndex( }); }); + // Release medialogEntries after all processing (~10-20MB) + medialogEntries.length = 0; + if (onProgressiveData && (index.length > 0 || earlyLinkedEntries.length > 0)) { const combined = [...earlyLinkedEntries, ...index]; onProgressiveData(combined); @@ -563,7 +556,13 @@ export async function buildFullIndex( imagePathsInMarkdown: imageToPages.size, }; + // Release image-to-pages map after truthing (~5-10MB) + imageToPages.clear(); + const files = Array.from(filesByPath.values()); + // Release filesByPath after converting to array (~2-5MB) + filesByPath.clear(); + const linkedStart = Date.now(); const linkedResults = await processLinkedContent( index, @@ -577,6 +576,15 @@ export async function buildFullIndex( usageMap, usageMapContext, ); + // Release usageMap after linked content processing (~5-10MB) + usageMap.images?.clear(); + usageMap.videos?.clear(); + usageMap.external?.clear(); + + // Release pages and files arrays (~5-10MB) + pages.length = 0; + files.length = 0; + const linkedDurationMs = Date.now() - linkedStart; if (onProgressiveData && (index.length > 0 || earlyLinkedEntries.length > 0)) { diff --git a/nx/blocks/media-library/indexing/worker/worker.js b/nx/blocks/media-library/indexing/worker/worker.js index 193109530..1ea090feb 100644 --- a/nx/blocks/media-library/indexing/worker/worker.js +++ b/nx/blocks/media-library/indexing/worker/worker.js @@ -134,12 +134,12 @@ self.onmessage = async (event) => { } catch (error) { console.error('[IndexWorker] Build failed:', error); - // Send error response self.postMessage({ type: 'error', error: { message: error.message || 'Unknown error', code: error.code || 'WORKER_ERROR', + status: error.status, stack: error.stack, }, }); diff --git a/nx/blocks/media-library/media-library.css b/nx/blocks/media-library/media-library.css index 3c2137095..ea2339109 100644 --- a/nx/blocks/media-library/media-library.css +++ b/nx/blocks/media-library/media-library.css @@ -28,7 +28,7 @@ height: 100%; width: 100%; max-width: 100%; - background: var(--s2-gray-50); + background: light-dark(rgb(248 248 248), rgb(27 27 27)); overflow: hidden; position: relative; } @@ -74,7 +74,7 @@ nx-media-scan { padding: var(--s2-spacing-200, var(--spacing-200, 12px)) var(--s2-spacing-300, var(--spacing-300, 16px)); border: 1px solid var(--s2-gray-200); border-radius: var(--s2-radius-75); - background: var(--s2-gray-50); + background: light-dark(rgb(248 248 248), rgb(27 27 27)); color: var(--s2-gray-700); font-size: var(--s2-font-size-100); font-weight: 500; @@ -85,7 +85,7 @@ nx-media-scan { } .view-btn:hover { - background: var(--s2-gray-50); + background: light-dark(rgb(248 248 248), rgb(27 27 27)); border-color: var(--s2-gray-700); } @@ -245,6 +245,7 @@ nx-media-sidebar { display: flex; flex-direction: column; position: relative; + background: inherit; } .content:has(nx-media-grid[pluginmode]) { @@ -287,7 +288,7 @@ nx-media-sidebar { position: fixed; top: var(--s2-spacing-400, var(--spacing-400, 24px)); right: var(--s2-spacing-400, var(--spacing-400, 24px)); - background: var(--s2-gray-50); + background: light-dark(rgb(248 248 248), rgb(27 27 27)); border: 1px solid var(--s2-gray-200); border-radius: var(--s2-radius-100); padding: var(--s2-spacing-200, var(--spacing-200, 12px)) var(--s2-spacing-300, var(--spacing-300, 16px)); @@ -310,7 +311,7 @@ nx-media-sidebar { position: fixed; top: 16px; right: 16px; - background: var(--s2-gray-50); + background: light-dark(rgb(248 248 248), rgb(27 27 27)); border: 1px solid var(--s2-gray-200); border-radius: 4px; padding: 16px; @@ -380,7 +381,8 @@ nx-media-sidebar { min-height: 0; padding: var(--s2-spacing-600, var(--spacing-600, 40px)); text-align: center; - background: light-dark(var(--s2-gray-50), #1a1a1a); + background: transparent; + border-radius: 0; } .indexing-spinner { @@ -472,7 +474,7 @@ nx-media-sidebar { justify-content: center; width: 100%; height: 100vh; - background: light-dark(var(--s2-gray-50), #1a1a1a); + background: light-dark(rgb(248 248 248), rgb(27 27 27)); } .validation-content, @@ -488,6 +490,13 @@ nx-media-sidebar { border-radius: var(--s2-radius-100); } +/* Override card styling when showing loading/indexing state */ +.validation-content.indexing-state { + background: transparent; + border-radius: 0; + max-width: none; +} + .validation-content p { font-size: 16px; color: light-dark(var(--s2-gray-700), var(--s2-gray-200)); diff --git a/nx/blocks/media-library/media-library.js b/nx/blocks/media-library/media-library.js index 36d88bcc7..0318bffac 100644 --- a/nx/blocks/media-library/media-library.js +++ b/nx/blocks/media-library/media-library.js @@ -37,11 +37,10 @@ import { loadPinnedFolders, savePinnedFolders } from './ui/pin.js'; import { getAppState, updateAppState, onStateChange, showNotification, dismissNotification, } from './core/state.js'; -import { t } from './core/messages.js'; +import { getMessage } from './core/messages.js'; import { initService, disposeService, triggerBuild } from './indexing/coordinator.js'; import { handleIndexingEvent } from './core/indexing-adapter.js'; import { fetchSidekickConfig } from './indexing/admin-api.js'; -import '../../public/sl/components.js'; import './ui/views/topbar/topbar.js'; import './ui/views/sidebar/sidebar.js'; import './ui/views/grid/grid.js'; @@ -50,8 +49,9 @@ import './ui/views/onboard/onboard.js'; const EL_NAME = 'nx-media-library'; const nx = `${new URL(import.meta.url).origin}/nx`; -const sl = await loadStyle(`${nx}/public/sl/styles.css`); -const slComponents = await loadStyle(`${nx}/public/sl/components.css`); +const nx2 = `${new URL(import.meta.url).origin}/nx2`; +const sl = await loadStyle(`${nx2}/styles/styles.css`); +const slComponents = await loadStyle(`${nx2}/public/sl/components.css`); const topbarStyles = await loadStyle(`${nx}/blocks/media-library/ui/views/topbar/topbar.css`); const style = await loadStyle(import.meta.url); const shellStyles = await loadStyle(new URL('./media-library-shell.css', import.meta.url).href); @@ -612,7 +612,7 @@ class NxMediaLibrary extends LitElement { validationError: null, validationSuggestion: null, persistentError: this.siteAuthInfo?.authFailed - ? 'Protected site - some images may not load, refresh or login into site in a new tab using sidekick' + ? getMessage('PROTECTED_SITE_IMAGES_WARNING') : null, isValidating: false, }); @@ -713,13 +713,13 @@ class NxMediaLibrary extends LitElement { }); await this.setMediaData([]); if (!isPersistent) { - showNotification(t('NOTIFY_ERROR'), error.message, 'danger'); + showNotification(getMessage('NOTIFY_ERROR'), error.message, 'danger'); } } else { // eslint-disable-next-line no-console console.error('[MEDIA-LIB:loadMediaData]', error); updateAppState({ - validationError: 'Failed to load media data. Please ensure you are signed in.', + validationError: getMessage('DATA_LOAD_FAILED'), sitePathValid: false, }); } @@ -783,7 +783,7 @@ class NxMediaLibrary extends LitElement {
-

${isMediaLibraryPluginMode() ? t('UI_LOADING') : t('UI_DISCOVERING')}

+

${isMediaLibraryPluginMode() ? getMessage('UI_LOADING') : getMessage('UI_DISCOVERING')}

`; @@ -827,11 +827,11 @@ class NxMediaLibrary extends LitElement { ${this._appState.persistentError ? html`
- ${t('NOTIFY_ERROR')} + ${getMessage('NOTIFY_ERROR')} - ${t('UI_MEDIA_POSITION', { current: pos, total })} + ${getMessage('UI_MEDIA_POSITION', { current: pos, total })}
`; } @@ -431,14 +431,14 @@ class NxMediaInfo extends LitElement { if (!response.ok) { this._pdfState = 'error'; - this._pdfError = t('UI_PDF_HTTP_ERROR', { status: response.status }); + this._pdfError = getMessage('UI_PDF_HTTP_ERROR', { status: response.status }); return; } const contentType = response.headers.get('content-type'); if (contentType && !contentType.includes('application/pdf') && !contentType.includes('application/octet-stream')) { this._pdfState = 'error'; - this._pdfError = t('UI_PDF_INVALID_TYPE'); + this._pdfError = getMessage('UI_PDF_INVALID_TYPE'); return; } @@ -461,7 +461,7 @@ class NxMediaInfo extends LitElement { } if (this._pdfCurrentUrl === pdfUrl) { this._pdfState = 'error'; - this._pdfError = t('UI_PDF_INACCESSIBLE'); + this._pdfError = getMessage('UI_PDF_INACCESSIBLE'); } } } @@ -715,14 +715,14 @@ class NxMediaInfo extends LitElement { const blob = await getResponse.blob(); this._fileSize = formatFileSize(blob.size); } else { - this._fileSize = isExternal ? t('UI_EXTERNAL_RESOURCE') : t('UI_UNABLE_TO_FETCH'); + this._fileSize = isExternal ? getMessage('UI_EXTERNAL_RESOURCE') : getMessage('UI_UNABLE_TO_FETCH'); } } } else { - this._fileSize = isExternal ? t('UI_EXTERNAL_RESOURCE') : t('UI_UNABLE_TO_FETCH'); + this._fileSize = isExternal ? getMessage('UI_EXTERNAL_RESOURCE') : getMessage('UI_UNABLE_TO_FETCH'); } } catch { - this._fileSize = isExternal ? t('UI_EXTERNAL_RESOURCE') : t('UI_UNABLE_TO_FETCH'); + this._fileSize = isExternal ? getMessage('UI_EXTERNAL_RESOURCE') : getMessage('UI_UNABLE_TO_FETCH'); } this._pendingRequests.delete(controller); @@ -792,20 +792,20 @@ class NxMediaInfo extends LitElement { type="button" class="action-button copy-button" @click=${this.handleCopyUrl} - title="${isPluginMode ? t('UI_INSERT_MEDIA') : t('UI_COPY_URL')}" - aria-label="${isPluginMode ? t('UI_INSERT_MEDIA') : t('UI_COPY_URL')}" + title="${isPluginMode ? getMessage('UI_INSERT_MEDIA') : getMessage('UI_COPY_URL')}" + aria-label="${isPluginMode ? getMessage('UI_INSERT_MEDIA') : getMessage('UI_COPY_URL')}" > - ${isPluginMode ? t('UI_INSERT_BUTTON') : t('UI_COPY_BUTTON')} + ${isPluginMode ? getMessage('UI_INSERT_BUTTON') : getMessage('UI_COPY_BUTTON')}
`; @@ -982,7 +982,7 @@ class NxMediaInfo extends LitElement { return html`
- +
`; } @@ -1349,7 +1349,7 @@ class NxMediaInfo extends LitElement { const isError = result.heading === 'Error'; this.showModalNotification(result.heading, result.message, isError ? 'danger' : 'success'); } catch (_) { - this.showModalNotification(t('NOTIFY_ERROR'), t('NOTIFY_COPY_ERROR'), 'danger'); + this.showModalNotification(getMessage('NOTIFY_ERROR'), getMessage('NOTIFY_COPY_ERROR'), 'danger'); } } diff --git a/nx/blocks/media-library/ui/views/onboard/onboard.js b/nx/blocks/media-library/ui/views/onboard/onboard.js index 709e4dab4..d46092c47 100644 --- a/nx/blocks/media-library/ui/views/onboard/onboard.js +++ b/nx/blocks/media-library/ui/views/onboard/onboard.js @@ -5,7 +5,7 @@ import { normalizeSitePath } from '../../../core/paths.js'; import { loadHrefSvg } from '../../../../../../nx2/utils/svg.js'; import { Storage } from '../../../core/constants.js'; import { showNotification } from '../../../core/state.js'; -import { t } from '../../../core/messages.js'; +import { getMessage } from '../../../core/messages.js'; import { ErrorCodes, logMediaLibraryError } from '../../../core/errors.js'; const EL_NAME = 'nx-media-onboard'; @@ -167,7 +167,7 @@ class NxMediaOnboard extends LitElement { const shareUrl = `${baseUrl}${window.location.search}#${sitePath}`; navigator.clipboard.writeText(shareUrl).then(() => { - showNotification(t('NOTIFY_LINK_COPIED'), t('NOTIFY_LINK_COPIED_MSG'), 'success'); + showNotification(getMessage('NOTIFY_LINK_COPIED'), getMessage('NOTIFY_LINK_COPIED_MSG'), 'success'); }); } diff --git a/nx/blocks/media-library/ui/views/sidebar/sidebar.js b/nx/blocks/media-library/ui/views/sidebar/sidebar.js index bcaa766d4..57d927155 100644 --- a/nx/blocks/media-library/ui/views/sidebar/sidebar.js +++ b/nx/blocks/media-library/ui/views/sidebar/sidebar.js @@ -2,12 +2,13 @@ import { html, LitElement } from 'da-lit'; import { loadStyle } from '../../../../../../nx2/utils/utils.js'; import { loadHrefSvg } from '../../../../../../nx2/utils/svg.js'; import { getAppState, onStateChange } from '../../../core/state.js'; -import { t } from '../../../core/messages.js'; +import { getMessage } from '../../../core/messages.js'; const style = await loadStyle(import.meta.url); const nx = `${new URL(import.meta.url).origin}/nx`; -const sl = await loadStyle(`${nx}/public/sl/styles.css`); -const slComponents = await loadStyle(`${nx}/public/sl/components.css`); +const nx2 = `${new URL(import.meta.url).origin}/nx2`; +const sl = await loadStyle(`${nx2}/styles/styles.css`); +const slComponents = await loadStyle(`${nx2}/public/sl/components.css`); const ICONS = [ `${nx}/img/icons/S2_Icon_Properties_20_N.svg`, `${nx}/img/icons/S2_GraphBarVertical_18_N.svg`, @@ -147,7 +148,7 @@ class NxMediaSidebar extends LitElement { return html`