diff --git a/.gitignore b/.gitignore index 73ab54020..28e0bfffa 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,9 @@ masStudio-*.json .cursor .superpowers/ +# Worktree documentation (per-ticket, not committed) +docs/ + # web-components specific !web-components/dist web-components/commerce.json diff --git a/nala/studio/translations/translations.page.js b/nala/studio/translations/translations.page.js index 5ebb7a28b..2e2edd5d4 100644 --- a/nala/studio/translations/translations.page.js +++ b/nala/studio/translations/translations.page.js @@ -47,11 +47,8 @@ export default class TranslationsPage { } async waitForListToLoad(timeout = 15000) { - await this.loadingIndicator.waitFor({ state: 'hidden', timeout }).catch(() => {}); - await Promise.race([ - this.translationTable.waitFor({ state: 'visible', timeout }), - this.emptyState.waitFor({ state: 'visible', timeout }), - ]); + const dataRow = this.translationTable.locator('sp-table-row:not(.skeleton-row)').first(); + await dataRow.or(this.emptyState).waitFor({ state: 'visible', timeout }); } async getSentOnColumnTexts() { diff --git a/nala/utils/nala.run.js b/nala/utils/nala.run.js index d2d3b0d65..3cee80da3 100644 --- a/nala/utils/nala.run.js +++ b/nala/utils/nala.run.js @@ -1,6 +1,29 @@ #!/usr/bin/env node import { spawn, spawnSync } from 'child_process'; +import { readFileSync, existsSync } from 'fs'; +import { dirname, basename, join } from 'path'; + +function detectWorktreePort() { + // Walk up from cwd looking for a parent dir named "worktrees"; if the cwd is + // <…>/worktrees//…, read <…>/worktrees/.ports for BRANCH=. + let dir = process.cwd(); + while (dir !== dirname(dir)) { + const parent = dirname(dir); + if (basename(parent) === 'worktrees') { + const branch = basename(dir); + const portsFile = join(parent, '.ports'); + if (!existsSync(portsFile)) return null; + const line = readFileSync(portsFile, 'utf-8') + .split('\n') + .find((l) => l.startsWith(`${branch}=`)); + const offset = line ? parseInt(line.split('=')[1], 10) : NaN; + return Number.isFinite(offset) ? 3000 + offset : null; + } + dir = parent; + } + return null; +} function displayHelp() { console.log(` @@ -99,7 +122,12 @@ function getLocalTestLiveUrl(env, masiourl, milolibs, repo = 'mas', owner = 'ado process.env.MILO_LIBS = `?milolibs=${milolibs}`; } - if (env === 'local') return 'http://localhost:3000'; + if (env === 'local') { + // When run from a worktree checkout, auto-target that worktree's AEM port + // so `npm run nala local` works without needing to set LOCAL_TEST_LIVE_URL. + const worktreePort = detectWorktreePort(); + return `http://localhost:${worktreePort ?? 3000}`; + } if (env === 'libs') return 'http://localhost:6456'; return `https://${env}--${repo}--${owner}.aem.live`; } diff --git a/studio/src/common/skeleton-styles.css.js b/studio/src/common/skeleton-styles.css.js new file mode 100644 index 000000000..dbd2827e7 --- /dev/null +++ b/studio/src/common/skeleton-styles.css.js @@ -0,0 +1,34 @@ +import { css } from 'lit'; + +export const skeletonStyles = css` + .skeleton-element { + background: linear-gradient( + 90deg, + var(--spectrum-gray-200) 25%, + var(--spectrum-gray-100) 50%, + var(--spectrum-gray-200) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 4px; + } + + .skeleton-table-cell { + height: 18px; + width: 80%; + border-radius: 4px; + } + + .skeleton-row sp-table-cell { + padding: 16px 20px; + } + + @keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } +`; diff --git a/studio/src/mas-content.js b/studio/src/mas-content.js index 7ed4b6acb..4dbcfb525 100644 --- a/studio/src/mas-content.js +++ b/studio/src/mas-content.js @@ -293,7 +293,7 @@ class MasContent extends LitElement { this.observedSentinel = null; } else if (loadingJustCompleted && this.hasMore.value) { this.scrollObserver?.unobserve(sentinel); - this.scrollObserver?.observe(sentinel); + requestAnimationFrame(() => this.scrollObserver?.observe(sentinel)); } } diff --git a/studio/src/mas-repository.js b/studio/src/mas-repository.js index 09c7f2228..7e2d3030b 100644 --- a/studio/src/mas-repository.js +++ b/studio/src/mas-repository.js @@ -35,6 +35,7 @@ import { } from './constants.js'; import { fragmentHasPersonalizationTag, isPznCountryTagId, PZN_TAG_ID_PREFIX } from './common/utils/personalization-utils.js'; import { Placeholder } from './aem/placeholder.js'; +import { getFragmentName } from './translation/translation-utils.js'; import generateFragmentStore from './reactivity/source-fragment-store.js'; import { getDefaultLocaleCode } from '../../io/www/src/fragment/locales.js'; import { getDictionary } from '../libs/fragment-client.js'; @@ -102,6 +103,7 @@ export class MasRepository extends LitElement { placeholders: null, promotions: null, translations: null, + collections: null, }; this.dictionaryCache = new Map(); this.inflightDictionaryRequest = null; @@ -359,6 +361,7 @@ export class MasRepository extends LitElement { localSearch.status = STATUS_PUBLISHED; } + let refilling = false; try { if (this.#abortControllers.search) this.#abortControllers.search.abort(); this.#searchCursor = null; @@ -460,6 +463,10 @@ export class MasRepository extends LitElement { this.#eagerLoadAllPznPages(cursorState, searchController); } else { this.#abortControllers.search = null; + if (cursorState) { + refilling = true; + this.#refillBelowThreshold(cursorState, searchController); + } } } @@ -477,10 +484,35 @@ export class MasRepository extends LitElement { return; } - Store.fragments.list.loading.set(false); + if (!refilling) Store.fragments.list.loading.set(false); } static MIN_PAGE_SIZE = 10; + /** + * Soft cap on the eager personalization-page loop in #eagerLoadAllPznPages. + * Once the cap is hit, hasMore is set to true and the rest is delivered on + * demand by loadNextPage() (one page per scroll-trigger). Pagination is not + * lost — it simply stops being eager-prefetched after this many pages. + */ + static MAX_EAGER_PZN_PAGES = 20; + /** + * Visible-row threshold for the post-filter refill loop in #refillBelowThreshold. + * When a cursor page, after #filterStoresByPersonalizationEnabled has been + * applied, has fewer than this many visible items AND the cursor is not + * exhausted, the loop fetches additional cursor pages until the threshold is + * met or the cursor runs out. Prevents the narrow-filter UX where a user sees + * "1 result" when the underlying catalog has many more matches spread across + * later cursor pages. + */ + static MIN_FILTERED_PAGE_RESULTS = 25; + /** + * Soft cap on the number of #fillPage rounds the refill loop will run before + * giving up. Mirrors MAX_EAGER_PZN_PAGES to keep the non-personalization + * refill path bounded when a filter matches very little in the catalog. + * When the cap is hit, hasMore stays true so loadNextPage can continue + * fetching on scroll. + */ + static MAX_REFILL_ROUNDS = 20; async #fillPage(cursor, variants, surface, fragmentStores, limit = MasRepository.MIN_PAGE_SIZE, signal) { let added = 0; @@ -501,8 +533,50 @@ export class MasRepository extends LitElement { async #eagerLoadAllPznPages(cursorSnapshot, searchController) { const { cursor, variants, surface, fragmentStores } = cursorSnapshot; + let pagesLoaded = 0; + try { + while (this.#searchCursor === cursorSnapshot) { + if (pagesLoaded >= MasRepository.MAX_EAGER_PZN_PAGES) { + Store.fragments.list.hasMore.set(true); + break; + } + const done = await this.#fillPage( + cursor, + variants, + surface, + fragmentStores, + undefined, + searchController.signal, + ); + pagesLoaded++; + if (this.#searchCursor !== cursorSnapshot) return; + Store.fragments.list.data.set([...this.#filterStoresByPersonalizationEnabled(fragmentStores)]); + if (done) { + this.#searchCursor = null; + return; + } + } + } catch (error) { + if (error.name === 'AbortError') return; + if (this.#searchCursor === cursorSnapshot) { + Store.fragments.list.hasMore.set(true); + } + } + } + + async #refillBelowThreshold(cursorSnapshot, searchController) { + const { cursor, variants, surface, fragmentStores } = cursorSnapshot; + let rounds = 0; + Store.fragments.list.loading.set(true); try { while (this.#searchCursor === cursorSnapshot) { + const filtered = this.#filterStoresByPersonalizationEnabled(fragmentStores); + if (filtered.length >= MasRepository.MIN_FILTERED_PAGE_RESULTS) return; + if (rounds >= MasRepository.MAX_REFILL_ROUNDS) { + Store.fragments.list.hasMore.set(true); + return; + } + const beforeCount = fragmentStores.length; const done = await this.#fillPage( cursor, variants, @@ -511,10 +585,16 @@ export class MasRepository extends LitElement { undefined, searchController.signal, ); + rounds++; if (this.#searchCursor !== cursorSnapshot) return; + if (fragmentStores.length === beforeCount && !done) { + Store.fragments.list.hasMore.set(true); + return; + } Store.fragments.list.data.set([...this.#filterStoresByPersonalizationEnabled(fragmentStores)]); if (done) { this.#searchCursor = null; + Store.fragments.list.hasMore.set(false); return; } } @@ -523,6 +603,10 @@ export class MasRepository extends LitElement { if (this.#searchCursor === cursorSnapshot) { Store.fragments.list.hasMore.set(true); } + } finally { + if (this.#searchCursor === cursorSnapshot || this.#searchCursor === null) { + Store.fragments.list.loading.set(false); + } } } @@ -644,6 +728,38 @@ export class MasRepository extends LitElement { } } + async loadAllCollections() { + if (!this.search.value.path) return; + try { + if (this.#abortControllers.collections) this.#abortControllers.collections.abort(); + this.#abortControllers.collections = new AbortController(); + + const damPath = getDamPath(this.search.value.path); + const locale = this.filters.value.locale; + const searchOptions = { + path: `${damPath}/${locale}`, + modelIds: [TAG_MODEL_ID_MAPPING['mas:studio/content-type/merch-card-collection']], + sort: [{ on: 'modifiedOrCreated', order: 'DESC' }], + }; + + const fragments = await this.searchFragmentList(searchOptions, 50, this.#abortControllers.collections); + const collections = []; + const collectionsByPath = new Map(); + for (const fragment of fragments) { + const collection = { ...fragment, studioPath: getFragmentName(fragment) }; + collections.push(collection); + collectionsByPath.set(fragment.path, collection); + } + + Store.translationProjects.allCollections.set(collections); + Store.translationProjects.displayCollections.set(collections); + Store.translationProjects.collectionsByPaths.set(collectionsByPath); + } catch (error) { + if (error.name === 'AbortError') return; + this.processError(error, 'Could not load collections.'); + } + } + async loadPreviewPlaceholders() { if (!this.search.value.path) return; diff --git a/studio/src/placeholders/mas-placeholders.css.js b/studio/src/placeholders/mas-placeholders.css.js index d1299a07e..252c619d3 100644 --- a/studio/src/placeholders/mas-placeholders.css.js +++ b/studio/src/placeholders/mas-placeholders.css.js @@ -1,397 +1,388 @@ import { css } from 'lit'; +import { skeletonStyles } from '../common/skeleton-styles.css.js'; + +export default [ + skeletonStyles, + css` + .placeholders-container { + height: 100%; + min-height: 200px; + border-radius: 8px; + padding: 24px; + background-color: var(--spectrum-white); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-sizing: border-box; + position: relative; + } + + .placeholders-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + } + + .header-left { + display: flex; + align-items: center; + } + + .search-filters-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + gap: 14px; + } + + .placeholders-title { + display: flex; + flex-direction: column; + gap: 4px; + } + + .placeholders-title h2 { + margin: 0; + font-size: 14px; + font-weight: 500; + } + + .filters-container { + display: flex; + gap: 14px; + align-items: center; + } + + .placeholders-content { + flex: 1; + position: relative; + } + + .error-message { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background-color: var(--spectrum-semantic-negative-color-background); + color: var(--spectrum-semantic-negative-color-text); + border-radius: 4px; + margin-bottom: 16px; + } + + .error-message sp-icon-alert { + color: var(--spectrum-semantic-negative-color-icon); + } + + .placeholders-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 8px; + border: 1px solid var(--spectrum-gray-200); + table-layout: fixed; + } + + .no-placeholders-label { + text-align: center; + } + + .placeholders-table sp-table-head { + background-color: var(--spectrum-gray-100); + border-bottom: 1px solid var(--spectrum-gray-200); + } + + .placeholders-table sp-table-head-cell { + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: flex-start; + color: var(--spectrum-gray-700); + font-size: 12px; + font-weight: 700; + } + + .placeholders-table sp-table-head-cell:last-child, + .placeholders-table sp-table-cell:last-child { + max-width: 100px; + justify-content: flex-end; + } -export const styles = css` - .placeholders-container { - height: 100%; - min-height: 200px; - border-radius: 8px; - padding: 24px; - background-color: var(--spectrum-white); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - box-sizing: border-box; - position: relative; - } - - .placeholders-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; - } - - .header-left { - display: flex; - align-items: center; - } - - .search-filters-container { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - gap: 14px; - } - - .placeholders-title { - display: flex; - flex-direction: column; - gap: 4px; - } - - .placeholders-title h2 { - margin: 0; - font-size: 14px; - font-weight: 500; - } - - .filters-container { - display: flex; - gap: 14px; - align-items: center; - } - - .placeholders-content { - flex: 1; - position: relative; - } - - .error-message { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - background-color: var(--spectrum-semantic-negative-color-background); - color: var(--spectrum-semantic-negative-color-text); - border-radius: 4px; - margin-bottom: 16px; - } - - .error-message sp-icon-alert { - color: var(--spectrum-semantic-negative-color-icon); - } - - sp-progress-circle { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - sp-progress-circle.loading-indicator { - top: -60px; - } - - .placeholders-table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - border-radius: 8px; - border: 1px solid var(--spectrum-gray-200); - table-layout: fixed; - } - - .no-placeholders-label { - text-align: center; - } - - .placeholders-table sp-table-head { - background-color: var(--spectrum-gray-100); - border-bottom: 1px solid var(--spectrum-gray-200); - } - - .placeholders-table sp-table-head-cell { - font-weight: 600; - cursor: pointer; - display: flex; - align-items: center; - justify-content: flex-start; - color: var(--spectrum-gray-700); - font-size: 12px; - font-weight: 700; - } - - .placeholders-table sp-table-head-cell:last-child, - .placeholders-table sp-table-cell:last-child { - max-width: 100px; - justify-content: flex-end; - } - - .placeholders-table sp-table-head-cell.align-right { - text-align: right; - } - - .placeholders-table sp-table-cell { - display: flex; - align-items: center; - justify-content: flex-start; - } - - .placeholders-table sp-table-cell, - .placeholders-table sp-table-checkbox-cell:not([head-cell]) { - border-block-start: var(--mod-table-border-width, var(--spectrum-table-border-width)) solid - var(--highcontrast-table-divider-color, var(--mod-table-divider-color, var(--spectrum-table-divider-color))); - border-radius: 0; - } - - .placeholders-table sp-table-cell.editing-cell { - box-sizing: border-box; - display: inline-flex; - padding: 0 30px 0 0; - } - - .placeholders-table sp-table-cell.updated-by { - overflow: hidden; - - & overlay-trigger { + .placeholders-table sp-table-head-cell.align-right { + text-align: right; + } + + .placeholders-table sp-table-cell { + display: flex; + align-items: center; + justify-content: flex-start; + } + + .placeholders-table sp-table-cell, + .placeholders-table sp-table-checkbox-cell:not([head-cell]) { + border-block-start: var(--mod-table-border-width, var(--spectrum-table-border-width)) solid + var(--highcontrast-table-divider-color, var(--mod-table-divider-color, var(--spectrum-table-divider-color))); + border-radius: 0; + } + + .placeholders-table sp-table-cell.editing-cell { + box-sizing: border-box; + display: inline-flex; + padding: 0 30px 0 0; + } + + .placeholders-table sp-table-cell.updated-by { overflow: hidden; + + & overlay-trigger { + overflow: hidden; + } + + & .cell-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .placeholders-table sp-table-body { + overflow: visible; + } + + .action-cell { + position: relative; + box-sizing: border-box; + } + + .action-buttons { + display: flex; + align-items: center; + gap: 8px; + justify-content: flex-end; + width: 100%; + } + + .action-button { + background: none; + border: none; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + flex: 0 0 auto; + + &:disabled { + filter: grayscale(1); + opacity: 0.6; + } + } + + .action-button:hover { + background-color: var(--spectrum-gray-200); + } + + .dropdown-menu-container { + position: relative; + } + + .dropdown-menu { + position: absolute; + right: 0; + top: 100%; + background: white; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 100; + width: 160px; + padding: 8px 0; + display: flex; + flex-direction: column; + } + + .dropdown-item { + flex: 1; + align-items: center; + padding: 8px 16px; + cursor: pointer; + gap: 8px; + justify-self: flex-start; + display: flex; + } + + .dropdown-item:hover { + background-color: var(--spectrum-gray-100); } - & .cell-content { + .dropdown-item.disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .dropdown-item.disabled:hover { + background-color: transparent; + } + + .dropdown-item span { + flex: 1; + display: inline-flex; + } + + .status-cell { + display: flex; + align-items: center; + justify-content: flex-start; + min-width: 120px; + padding: 8px 0; + } + + .status-cell mas-fragment-status { + width: auto; + display: inline-flex; + font-weight: 500; + } + + .edit-field-container { + width: 100%; + padding: 8px; + display: flex; + } + + .edit-field-container sp-textfield { + width: 100%; + flex: 1; + } + + .edit-field-container rte-field { + width: 100%; + min-height: 80px; + margin-bottom: 8px; + } + + .rte-container { + position: relative; + display: block; + width: 100%; + min-height: 120px; + margin: 5px 0; + } + + sp-switch { + display: inline-flex; + } + + rte-field { + display: block; + min-height: 120px; + border-radius: 4px; + width: 100%; + } + + .rich-text-cell { overflow: hidden; + padding: 4px 0; + font-size: var(--spectrum-global-font-size-100); + line-height: var(--spectrum-global-font-line-height-medium); + position: relative; text-overflow: ellipsis; - white-space: nowrap; - } - } - - .placeholders-table sp-table-body { - overflow: visible; - } - - .action-cell { - position: relative; - box-sizing: border-box; - } - - .action-buttons { - display: flex; - align-items: center; - gap: 8px; - justify-content: flex-end; - width: 100%; - } - - .action-button { - background: none; - border: none; - cursor: pointer; - padding: 4px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - flex: 0 0 auto; - - &:disabled { - filter: grayscale(1); - opacity: 0.6; - } - } - - .action-button:hover { - background-color: var(--spectrum-gray-200); - } - - .dropdown-menu-container { - position: relative; - } - - .dropdown-menu { - position: absolute; - right: 0; - top: 100%; - background: white; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - z-index: 100; - width: 160px; - padding: 8px 0; - display: flex; - flex-direction: column; - } - - .dropdown-item { - flex: 1; - align-items: center; - padding: 8px 16px; - cursor: pointer; - gap: 8px; - justify-self: flex-start; - display: flex; - } - - .dropdown-item:hover { - background-color: var(--spectrum-gray-100); - } - - .dropdown-item.disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .dropdown-item.disabled:hover { - background-color: transparent; - } - - .dropdown-item span { - flex: 1; - display: inline-flex; - } - - .status-cell { - display: flex; - align-items: center; - justify-content: flex-start; - min-width: 120px; - padding: 8px 0; - } - - .status-cell mas-fragment-status { - width: auto; - display: inline-flex; - font-weight: 500; - } - - .edit-field-container { - width: 100%; - padding: 8px; - display: flex; - } - - .edit-field-container sp-textfield { - width: 100%; - flex: 1; - } - - .edit-field-container rte-field { - width: 100%; - min-height: 80px; - margin-bottom: 8px; - } - - .rte-container { - position: relative; - display: block; - width: 100%; - min-height: 120px; - margin: 5px 0; - } - - sp-switch { - display: inline-flex; - } - - rte-field { - display: block; - min-height: 120px; - border-radius: 4px; - width: 100%; - } - - .rich-text-cell { - overflow: hidden; - padding: 4px 0; - font-size: var(--spectrum-global-font-size-100); - line-height: var(--spectrum-global-font-line-height-medium); - position: relative; - text-overflow: ellipsis; - } - - .rich-text-cell p { - margin: 0 0 8px 0; - } - - .rich-text-cell p:last-child { - margin-bottom: 0; - } - - .rich-text-cell a { - color: var(--spectrum-blue-600); - text-decoration: none; - } - - .rich-text-cell a:hover { - text-decoration: underline; - } - - .bulk-action-container { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - z-index: 1000; - opacity: 0; - visibility: hidden; - transition: - opacity 0.3s ease, - visibility 0.3s ease; - border-radius: 4px; - background-color: var(--spectrum-semantic-negative-color-background); - padding: 6px; - } - - .bulk-action-container.visible { - opacity: 1; - visibility: visible; - } - - .bulk-action-container sp-action-button { - color: var(--spectrum-white); - background-color: var(--spectrum-red-800); - box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.2); - } - - .approve-button sp-icon-checkmark { - color: var(--spectrum-semantic-positive-color-default, green); - } - - .reject-button sp-icon-close { - color: var(--spectrum-semantic-negative-color-default, red); - } - - /* Dialog styles */ - .dialog-content { - display: flex; - flex-direction: column; - gap: 0; - padding: var(calc(var(--swc-scale-factor) * 16px)); - width: 80vw; - max-width: 900px; - box-sizing: border-box; - } - - .form-field { - margin-bottom: var(calc(var(--swc-scale-factor) * 16px)); - display: flex; - flex-direction: column; - gap: 4px; - } - - .form-field:last-child { - margin-bottom: 0; - } - - .form-field sp-field-label { - display: block; - margin-bottom: var(calc(var(--swc-scale-factor) * 6px)); - } - - .form-field sp-picker, - .form-field sp-textfield { - width: 100%; - min-width: 0; - box-sizing: border-box; - } - - .form-field .rte-container { - width: 100%; - } - - sp-table .key { - flex: 2; - } - sp-table .value { - flex: 3; - } -`; - -export default styles; + } + + .rich-text-cell p { + margin: 0 0 8px 0; + } + + .rich-text-cell p:last-child { + margin-bottom: 0; + } + + .rich-text-cell a { + color: var(--spectrum-blue-600); + text-decoration: none; + } + + .rich-text-cell a:hover { + text-decoration: underline; + } + + .bulk-action-container { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: + opacity 0.3s ease, + visibility 0.3s ease; + border-radius: 4px; + background-color: var(--spectrum-semantic-negative-color-background); + padding: 6px; + } + + .bulk-action-container.visible { + opacity: 1; + visibility: visible; + } + + .bulk-action-container sp-action-button { + color: var(--spectrum-white); + background-color: var(--spectrum-red-800); + box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.2); + } + + .approve-button sp-icon-checkmark { + color: var(--spectrum-semantic-positive-color-default, green); + } + + .reject-button sp-icon-close { + color: var(--spectrum-semantic-negative-color-default, red); + } + + /* Dialog styles */ + .dialog-content { + display: flex; + flex-direction: column; + gap: 0; + padding: var(calc(var(--swc-scale-factor) * 16px)); + width: 80vw; + max-width: 900px; + box-sizing: border-box; + } + + .form-field { + margin-bottom: var(calc(var(--swc-scale-factor) * 16px)); + display: flex; + flex-direction: column; + gap: 4px; + } + + .form-field:last-child { + margin-bottom: 0; + } + + .form-field sp-field-label { + display: block; + margin-bottom: var(calc(var(--swc-scale-factor) * 6px)); + } + + .form-field sp-picker, + .form-field sp-textfield { + width: 100%; + min-width: 0; + box-sizing: border-box; + } + + .form-field .rte-container { + width: 100%; + } + + sp-table .key { + flex: 2; + } + sp-table .value { + flex: 3; + } + `, +]; diff --git a/studio/src/placeholders/mas-placeholders.js b/studio/src/placeholders/mas-placeholders.js index f93e19655..9b2c39fe6 100644 --- a/studio/src/placeholders/mas-placeholders.js +++ b/studio/src/placeholders/mas-placeholders.js @@ -13,6 +13,17 @@ import { confirmation } from '../mas-confirm-dialog.js'; import { FragmentStore } from '../reactivity/fragment-store.js'; import { clearCaches } from '../../libs/fragment-client.js'; +const placeholdersSkeletonRow = () => + html` +
+
+
+
+
+
+ +
`; + class MasPlaceholders extends LitElement { static styles = styles; @@ -324,7 +335,7 @@ class MasPlaceholders extends LitElement { -
${this.loadingIndicator()}${this.renderTable()}
+
${this.renderTable()}
${this.showCreationModal ? html``; - } - // #region Table renderTable() { @@ -399,26 +405,28 @@ class MasPlaceholders extends LitElement { )} - ${repeat( - this.internalPlaceholders, - (placeholderStore) => placeholderStore.get().key, - (placeholderStore) => { - const placeholder = placeholderStore.get(); - return html` - - `; - }, - )} - ${this.internalPlaceholders.length === 0 && !this.loading + ${this.loading + ? Array.from({ length: 5 }, placeholdersSkeletonRow) + : repeat( + this.internalPlaceholders, + (placeholderStore) => placeholderStore.get().key, + (placeholderStore) => { + const placeholder = placeholderStore.get(); + return html` + + `; + }, + )} + ${!this.loading && this.internalPlaceholders.length === 0 ? html`

No placeholders found

` : nothing}
diff --git a/studio/src/translation/mas-items-selector.css.js b/studio/src/translation/mas-items-selector.css.js index 1b7f11512..ed86c5795 100644 --- a/studio/src/translation/mas-items-selector.css.js +++ b/studio/src/translation/mas-items-selector.css.js @@ -4,19 +4,66 @@ import { ghostButtonStyles } from './translation-common-styles.css.js'; export const styles = [ ghostButtonStyles, css` + .dialog-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 16px; + } + + .dialog-header h2 { + margin: 0; + white-space: nowrap; + font-size: 18px; + } + + .dialog-header sp-search { + flex: 1; + max-width: 400px; + } + + :host { + display: flex; + flex-direction: column; + min-width: 80vw; + max-height: 70vh; + min-height: 0; + } + + sp-tabs { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + } + sp-tab-panel[selected] { display: flex; flex-direction: column; + flex: 1; + min-height: 0; gap: 12px; + padding-top: 16px; } .container { display: flex; - width: 80vw; + flex: 1; + min-height: 0; + width: 100%; + padding-bottom: 48px; + } + + mas-select-items-table { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; } .container.view-only { width: 100%; + padding-bottom: 0; } sp-tab-panel.view-only { @@ -33,8 +80,8 @@ export const styles = [ .selected-items-count { position: fixed; - bottom: 98px; - right: 22px; + bottom: 95px; + right: 42px; display: flex; justify-content: flex-end; align-items: center; diff --git a/studio/src/translation/mas-items-selector.js b/studio/src/translation/mas-items-selector.js index 176a6b7d6..38ecd89a3 100644 --- a/studio/src/translation/mas-items-selector.js +++ b/studio/src/translation/mas-items-selector.js @@ -8,6 +8,7 @@ import './mas-select-items-table.js'; import './mas-selected-items.js'; import './mas-search-and-filters.js'; import { styles } from './mas-items-selector.css.js'; +import { debounce } from '../utils.js'; export const TABS = [ { value: TABLE_TYPE.CARDS, label: 'Fragments' }, @@ -20,11 +21,15 @@ class MasItemsSelector extends LitElement { static properties = { viewOnly: { type: Boolean, state: true }, + searchQuery: { type: String, state: true }, + selectedTab: { type: String, state: true }, }; constructor() { super(); this.viewOnly = false; + this.searchQuery = ''; + this.selectedTab = TABLE_TYPE.CARDS; } connectedCallback() { @@ -43,17 +48,34 @@ class MasItemsSelector extends LitElement { } get selectedCount() { - return [ - ...Store.translationProjects.selectedCards.value, - ...Store.translationProjects.selectedPlaceholders.value, - ...Store.translationProjects.selectedCollections.value, - ].length; + return ( + Store.translationProjects.selectedCards.value.length + + Store.translationProjects.selectedPlaceholders.value.length + + Store.translationProjects.selectedCollections.value.length + ); } #toggleShowSelected() { Store.translationProjects.showSelected.set(!this.showSelected); } + #setSearchQuery = debounce((value) => { + this.searchQuery = value; + }, 300); + + #handleSearchInput(e) { + this.#setSearchQuery(e.currentTarget?.value ?? ''); + } + + #handleSearchSubmit(e) { + e.preventDefault(); + this.searchQuery = e.currentTarget?.value ?? ''; + } + + #handleTabChange({ target: { selected } }) { + this.selectedTab = selected; + } + #getTabLabel(tab) { if (this.viewOnly) { const valueUppercase = tab.value.charAt(0).toUpperCase() + tab.value.slice(1); @@ -72,8 +94,24 @@ class MasItemsSelector extends LitElement { } render() { + const count = this.selectedCount; + const showingSelection = this.showSelected && count; + const toggleLabel = showingSelection ? 'Hide selection' : 'Selected items'; return html` - + ${this.viewOnly + ? nothing + : html` +
+

Select items

+ +
+ `} + ${repeat( TABS, (tab) => tab.value, @@ -89,6 +127,7 @@ class MasItemsSelector extends LitElement { : html` `} @@ -113,18 +152,13 @@ class MasItemsSelector extends LitElement { - + ${toggleSidebarIcon} - ${this.showSelected && this.selectedCount ? 'Hide selection' : 'Selected items'} - (${this.selectedCount}) + ${toggleLabel} (${count}) `} diff --git a/studio/src/translation/mas-search-and-filters.css.js b/studio/src/translation/mas-search-and-filters.css.js index f2a277f76..4a950c807 100644 --- a/studio/src/translation/mas-search-and-filters.css.js +++ b/studio/src/translation/mas-search-and-filters.css.js @@ -5,18 +5,6 @@ export const styles = css` display: block; } - .search { - display: flex; - align-items: center; - gap: 8px; - margin: 32px 0 20px 0; - } - - .search sp-search { - flex: 1; - max-width: 400px; - } - .result-count { color: var(--spectrum-gray-700); font-size: 14px; @@ -25,6 +13,7 @@ export const styles = css` .filters { display: flex; + align-items: center; gap: 12px; margin-bottom: 8px; flex-wrap: wrap; diff --git a/studio/src/translation/mas-search-and-filters.js b/studio/src/translation/mas-search-and-filters.js index fe7a7535a..b330dc524 100644 --- a/studio/src/translation/mas-search-and-filters.js +++ b/studio/src/translation/mas-search-and-filters.js @@ -129,17 +129,20 @@ class MasSearchAndFilters extends LitElement { this.productOptions = Array.from(products.values()).sort((a, b) => a.title.localeCompare(b.title)); } - #handleSearchInput({ target: { value: query } }) { - this.searchQuery = query; - this.#applyFilters(); - } - - #handleSearchSubmit(e) { - e.preventDefault(); - this.#applyFilters(); + willUpdate(changed) { + if ( + changed.has('searchQuery') || + changed.has('templateFilter') || + changed.has('marketSegmentFilter') || + changed.has('customerSegmentFilter') || + changed.has('productFilter') + ) { + this.#applyFilters(); + } } #handleCheckboxChange(filterType, optionId, e) { + e.stopPropagation(); let currentValues; switch (filterType) { case FILTER_TYPE.TEMPLATE: @@ -180,7 +183,6 @@ class MasSearchAndFilters extends LitElement { this.productFilter = currentValues; break; } - this.#applyFilters(); } #handleTagDelete({ @@ -202,7 +204,6 @@ class MasSearchAndFilters extends LitElement { this.productFilter = this.productFilter.filter((filterId) => filterId !== id); break; } - this.#applyFilters(); } #clearAllFilters() { @@ -210,7 +211,6 @@ class MasSearchAndFilters extends LitElement { this.marketSegmentFilter = []; this.customerSegmentFilter = []; this.productFilter = []; - this.#applyFilters(); } #renderAppliedFilters() { @@ -332,46 +332,28 @@ class MasSearchAndFilters extends LitElement { } render() { + if (this.searchOnly) { + return html`${this.renderCount()}`; + } return html` -