diff --git a/community-modules/locale/src/ar-EG.ts b/community-modules/locale/src/ar-EG.ts index b282aa30d76..ea4fa75eb46 100644 --- a/community-modules/locale/src/ar-EG.ts +++ b/community-modules/locale/src/ar-EG.ts @@ -334,6 +334,11 @@ export const AG_GRID_LOCALE_EG = { sortAbsoluteDescending: 'ترتيب تنازلي مطلق', sortUnSort: 'إلغاء الترتيب', shiftF2: 'Shift+F2', + toolbarFind: 'بحث', + toolbarFindPreviousMatch: 'التطابق السابق', + toolbarFindNextMatch: 'التطابق التالي', + toolbarQuickFilter: 'تصفية', + toolbarMenu: 'قائمة', // Enterprise Menu Aggregation and Status Bar sum: 'المجموع', diff --git a/community-modules/locale/src/bg-BG.ts b/community-modules/locale/src/bg-BG.ts index 3d9b8e9638d..d7ec457af48 100644 --- a/community-modules/locale/src/bg-BG.ts +++ b/community-modules/locale/src/bg-BG.ts @@ -337,6 +337,11 @@ export const AG_GRID_LOCALE_BG = { sortAbsoluteDescending: 'Абсолютно сортиране низходящо', sortUnSort: 'Изчистване на сортирането', shiftF2: 'Shift+F2', + toolbarFind: 'Намиране', + toolbarFindPreviousMatch: 'Предишно съвпадение', + toolbarFindNextMatch: 'Следващо съвпадение', + toolbarQuickFilter: 'Филтър', + toolbarMenu: 'Меню', // Enterprise Menu Aggregation and Status Bar sum: 'Сума', diff --git a/community-modules/locale/src/cs-CZ.ts b/community-modules/locale/src/cs-CZ.ts index 466f81ef53c..af65b0d1b81 100644 --- a/community-modules/locale/src/cs-CZ.ts +++ b/community-modules/locale/src/cs-CZ.ts @@ -335,6 +335,11 @@ export const AG_GRID_LOCALE_CZ = { sortAbsoluteDescending: 'Řadit absolutně sestupně', sortUnSort: 'Zrušit třídění', shiftF2: 'Shift+F2', + toolbarFind: 'Najít', + toolbarFindPreviousMatch: 'Předchozí shoda', + toolbarFindNextMatch: 'Další shoda', + toolbarQuickFilter: 'Filtr', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Součet', diff --git a/community-modules/locale/src/da-DK.ts b/community-modules/locale/src/da-DK.ts index 3310c360e73..500c41407aa 100644 --- a/community-modules/locale/src/da-DK.ts +++ b/community-modules/locale/src/da-DK.ts @@ -337,6 +337,11 @@ export const AG_GRID_LOCALE_DK = { sortAbsoluteDescending: 'Sorter Absolut Faldende', sortUnSort: 'Ryd Sortering', shiftF2: 'Shift+F2', + toolbarFind: 'Find', + toolbarFindPreviousMatch: 'Forrige fund', + toolbarFindNextMatch: 'Næste fund', + toolbarQuickFilter: 'Filter', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Sum', diff --git a/community-modules/locale/src/de-DE.ts b/community-modules/locale/src/de-DE.ts index bba4c48e7c8..8ba34e0d48d 100644 --- a/community-modules/locale/src/de-DE.ts +++ b/community-modules/locale/src/de-DE.ts @@ -339,6 +339,11 @@ export const AG_GRID_LOCALE_DE = { sortAbsoluteDescending: 'Absolut absteigend sortieren', sortUnSort: 'Sortierung aufheben', shiftF2: 'Shift+F2', + toolbarFind: 'Finden', + toolbarFindPreviousMatch: 'Vorherige Übereinstimmung', + toolbarFindNextMatch: 'Nächste Übereinstimmung', + toolbarQuickFilter: 'Filter', + toolbarMenu: 'Menü', // Enterprise Menu Aggregation and Status Bar sum: 'Summe', diff --git a/community-modules/locale/src/el-GR.ts b/community-modules/locale/src/el-GR.ts index 9bea45657d9..13191db5d3e 100644 --- a/community-modules/locale/src/el-GR.ts +++ b/community-modules/locale/src/el-GR.ts @@ -340,6 +340,11 @@ export const AG_GRID_LOCALE_GR = { sortAbsoluteDescending: '"Απόλυτη φθίνουσα ταξινόμηση"', sortUnSort: 'Καθαρισμός Ταξινόμησης', shiftF2: 'Shift+F2', + toolbarFind: 'Εύρεση', + toolbarFindPreviousMatch: 'Προηγούμενη Αντιστοιχία', + toolbarFindNextMatch: 'Επόμενη Αντιστοιχία', + toolbarQuickFilter: 'Φίλτρο', + toolbarMenu: 'Μενού', // Enterprise Menu Aggregation and Status Bar sum: 'Άθροισμα', diff --git a/community-modules/locale/src/es-ES.ts b/community-modules/locale/src/es-ES.ts index fe917a9e668..f4f63f2cbce 100644 --- a/community-modules/locale/src/es-ES.ts +++ b/community-modules/locale/src/es-ES.ts @@ -338,6 +338,11 @@ export const AG_GRID_LOCALE_ES = { sortAbsoluteDescending: 'Ordenar absolutamente descendente', sortUnSort: 'Limpiar Orden', shiftF2: 'Shift+F2', + toolbarFind: 'Buscar', + toolbarFindPreviousMatch: 'Coincidencia Anterior', + toolbarFindNextMatch: 'Siguiente Coincidencia', + toolbarQuickFilter: 'Filtrar', + toolbarMenu: 'Menú', // Enterprise Menu Aggregation and Status Bar sum: 'Suma', diff --git a/community-modules/locale/src/fa-IR.ts b/community-modules/locale/src/fa-IR.ts index c7f0135d1ca..1c3c99680c0 100644 --- a/community-modules/locale/src/fa-IR.ts +++ b/community-modules/locale/src/fa-IR.ts @@ -335,6 +335,11 @@ export const AG_GRID_LOCALE_IR = { sortAbsoluteDescending: 'مرتب‌سازی مطلق به صورت نزولی', sortUnSort: 'پاک کردن مرتب‌سازی', shiftF2: 'Shift+F2', + toolbarFind: 'یافتن', + toolbarFindPreviousMatch: 'مطابقت قبلی', + toolbarFindNextMatch: 'مطابقت بعدی', + toolbarQuickFilter: 'فیلتر', + toolbarMenu: 'منو', // Enterprise Menu Aggregation and Status Bar sum: 'جمع', diff --git a/community-modules/locale/src/fi-FI.ts b/community-modules/locale/src/fi-FI.ts index 4b6dafb73a9..09ee139d364 100644 --- a/community-modules/locale/src/fi-FI.ts +++ b/community-modules/locale/src/fi-FI.ts @@ -338,6 +338,11 @@ export const AG_GRID_LOCALE_FI = { sortAbsoluteDescending: 'Lajittele ehdoton laskeva', sortUnSort: 'Tyhjennä lajittelu', shiftF2: 'Shift+F2', + toolbarFind: 'Etsi', + toolbarFindPreviousMatch: 'Edellinen osuma', + toolbarFindNextMatch: 'Seuraava osuma', + toolbarQuickFilter: 'Suodatus', + toolbarMenu: 'Valikko', // Enterprise Menu Aggregation and Status Bar sum: 'Summa', diff --git a/community-modules/locale/src/fr-FR.ts b/community-modules/locale/src/fr-FR.ts index 389e46b6e6a..428fc08c73c 100644 --- a/community-modules/locale/src/fr-FR.ts +++ b/community-modules/locale/src/fr-FR.ts @@ -340,6 +340,11 @@ export const AG_GRID_LOCALE_FR = { sortAbsoluteDescending: 'Trier par ordre décroissant absolu', sortUnSort: 'Effacer le tri', shiftF2: 'Shift+F2', + toolbarFind: 'Rechercher', + toolbarFindPreviousMatch: 'Correspondance précédente', + toolbarFindNextMatch: 'Correspondance suivante', + toolbarQuickFilter: 'Filtrer', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Somme', diff --git a/community-modules/locale/src/he-IL.ts b/community-modules/locale/src/he-IL.ts index eadae51bc7f..994cfd0a9a2 100644 --- a/community-modules/locale/src/he-IL.ts +++ b/community-modules/locale/src/he-IL.ts @@ -334,6 +334,11 @@ export const AG_GRID_LOCALE_IL = { sortAbsoluteDescending: 'מיון יורד מוחלט', sortUnSort: 'נקה מיון', shiftF2: 'Shift+F2', + toolbarFind: 'חפש', + toolbarFindPreviousMatch: 'תאמה קודמת', + toolbarFindNextMatch: 'תאמה הבאה', + toolbarQuickFilter: 'סנן', + toolbarMenu: 'תפריט', // Enterprise Menu Aggregation and Status Bar sum: 'סכום', diff --git a/community-modules/locale/src/hr-HR.ts b/community-modules/locale/src/hr-HR.ts index 82e1a72c6b5..9ae2b67315c 100644 --- a/community-modules/locale/src/hr-HR.ts +++ b/community-modules/locale/src/hr-HR.ts @@ -336,6 +336,11 @@ export const AG_GRID_LOCALE_HR = { sortAbsoluteDescending: 'Sortiraj apsolutno silazno', sortUnSort: 'Očisti sortiranje', shiftF2: 'Shift+F2', + toolbarFind: 'Pronađi', + toolbarFindPreviousMatch: 'Prethodno podudaranje', + toolbarFindNextMatch: 'Sljedeće podudaranje', + toolbarQuickFilter: 'Filter', + toolbarMenu: 'Izbornik', // Enterprise Menu Aggregation and Status Bar sum: 'Zbroj', diff --git a/community-modules/locale/src/hu-HU.ts b/community-modules/locale/src/hu-HU.ts index 47a136ca734..b2e7d8f183f 100644 --- a/community-modules/locale/src/hu-HU.ts +++ b/community-modules/locale/src/hu-HU.ts @@ -339,6 +339,11 @@ export const AG_GRID_LOCALE_HU = { sortAbsoluteDescending: 'Abszolút csökkenő sorrendben rendez', sortUnSort: 'Rendezés törlése', shiftF2: 'Shift+F2', + toolbarFind: 'Keresés', + toolbarFindPreviousMatch: 'Előző találat', + toolbarFindNextMatch: 'Következő találat', + toolbarQuickFilter: 'Szűrő', + toolbarMenu: 'Menü', // Enterprise Menu Aggregation and Status Bar sum: 'Összeg', diff --git a/community-modules/locale/src/it-IT.ts b/community-modules/locale/src/it-IT.ts index d840f0d1d96..2c1ef20409f 100644 --- a/community-modules/locale/src/it-IT.ts +++ b/community-modules/locale/src/it-IT.ts @@ -338,6 +338,11 @@ export const AG_GRID_LOCALE_IT = { sortAbsoluteDescending: 'Ordina Assoluto Decrescente', sortUnSort: 'Annulla Ordinamento', shiftF2: 'Shift+F2', + toolbarFind: 'Trova', + toolbarFindPreviousMatch: 'Corrispondenza Precedente', + toolbarFindNextMatch: 'Corrispondenza Successiva', + toolbarQuickFilter: 'Filtro', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Somma', diff --git a/community-modules/locale/src/ja-JP.ts b/community-modules/locale/src/ja-JP.ts index d73b72d0727..0692707cadb 100644 --- a/community-modules/locale/src/ja-JP.ts +++ b/community-modules/locale/src/ja-JP.ts @@ -334,6 +334,11 @@ export const AG_GRID_LOCALE_JP = { sortAbsoluteDescending: '絶対降順に並べ替え', sortUnSort: 'ソート解除', shiftF2: 'Shift+F2', + toolbarFind: '検索', + toolbarFindPreviousMatch: '前の一致', + toolbarFindNextMatch: '次の一致', + toolbarQuickFilter: 'フィルター', + toolbarMenu: 'メニュー', // Enterprise Menu Aggregation and Status Bar sum: '合計', diff --git a/community-modules/locale/src/ko-KR.ts b/community-modules/locale/src/ko-KR.ts index 69df5428131..042058d9c07 100644 --- a/community-modules/locale/src/ko-KR.ts +++ b/community-modules/locale/src/ko-KR.ts @@ -335,6 +335,11 @@ export const AG_GRID_LOCALE_KR = { sortAbsoluteDescending: '절대 내림차순 정렬', sortUnSort: '정렬 해제', shiftF2: 'Shift+F2', + toolbarFind: '찾기', + toolbarFindPreviousMatch: '이전 항목 찾기', + toolbarFindNextMatch: '다음 항목 찾기', + toolbarQuickFilter: '필터', + toolbarMenu: '메뉴', // Enterprise Menu Aggregation and Status Bar sum: '합계', diff --git a/community-modules/locale/src/nb-NO.ts b/community-modules/locale/src/nb-NO.ts index 42b486a5884..81a89fef150 100644 --- a/community-modules/locale/src/nb-NO.ts +++ b/community-modules/locale/src/nb-NO.ts @@ -336,6 +336,11 @@ export const AG_GRID_LOCALE_NO = { sortAbsoluteDescending: 'Sorter absolutt synkende', sortUnSort: 'Fjern Sortering', shiftF2: 'Shift+F2', + toolbarFind: 'Finn', + toolbarFindPreviousMatch: 'Forrige treff', + toolbarFindNextMatch: 'Neste treff', + toolbarQuickFilter: 'Filter', + toolbarMenu: 'Meny', // Enterprise Menu Aggregation and Status Bar sum: 'Sum', diff --git a/community-modules/locale/src/nl-NL.ts b/community-modules/locale/src/nl-NL.ts index 459c7bb6845..d6320bf92d9 100644 --- a/community-modules/locale/src/nl-NL.ts +++ b/community-modules/locale/src/nl-NL.ts @@ -335,6 +335,11 @@ export const AG_GRID_LOCALE_NL = { sortAbsoluteDescending: 'Sorteren absoluut aflopend', sortUnSort: 'Sortering wissen', shiftF2: 'Shift+F2', + toolbarFind: 'Zoeken', + toolbarFindPreviousMatch: 'Vorige resultaat', + toolbarFindNextMatch: 'Volgende resultaat', + toolbarQuickFilter: 'Filter', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Som', diff --git a/community-modules/locale/src/pl-PL.ts b/community-modules/locale/src/pl-PL.ts index e2ec240a572..aadcc2b5132 100644 --- a/community-modules/locale/src/pl-PL.ts +++ b/community-modules/locale/src/pl-PL.ts @@ -338,6 +338,11 @@ export const AG_GRID_LOCALE_PL = { sortAbsoluteDescending: 'Sortuj bezwzględnie malejąco', sortUnSort: 'Usuń Sortowanie', shiftF2: 'Shift+F2', + toolbarFind: 'Znajdź', + toolbarFindPreviousMatch: 'Poprzednie Dopasowanie', + toolbarFindNextMatch: 'Następne Dopasowanie', + toolbarQuickFilter: 'Filtruj', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Suma', diff --git a/community-modules/locale/src/pt-BR.ts b/community-modules/locale/src/pt-BR.ts index bc60b6d41ad..db4816d4dcc 100644 --- a/community-modules/locale/src/pt-BR.ts +++ b/community-modules/locale/src/pt-BR.ts @@ -338,6 +338,11 @@ export const AG_GRID_LOCALE_BR = { sortAbsoluteDescending: 'Ordenar Absoluto Descendente', sortUnSort: 'Limpar Ordenação', shiftF2: 'Shift+F2', + toolbarFind: 'Buscar', + toolbarFindPreviousMatch: 'Correspondência Anterior', + toolbarFindNextMatch: 'Próxima Correspondência', + toolbarQuickFilter: 'Filtro', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Soma', diff --git a/community-modules/locale/src/pt-PT.ts b/community-modules/locale/src/pt-PT.ts index 4be09e7a1f3..f1dcedbbef1 100644 --- a/community-modules/locale/src/pt-PT.ts +++ b/community-modules/locale/src/pt-PT.ts @@ -338,6 +338,11 @@ export const AG_GRID_LOCALE_PT = { sortAbsoluteDescending: 'Ordenar Absolutamente Descendente', sortUnSort: 'Limpar Ordenação', shiftF2: 'Shift+F2', + toolbarFind: 'Localizar', + toolbarFindPreviousMatch: 'Correspondência Anterior', + toolbarFindNextMatch: 'Próxima Correspondência', + toolbarQuickFilter: 'Filtrar', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Soma', diff --git a/community-modules/locale/src/ro-RO.ts b/community-modules/locale/src/ro-RO.ts index 3190ea21516..b5abfeff50e 100644 --- a/community-modules/locale/src/ro-RO.ts +++ b/community-modules/locale/src/ro-RO.ts @@ -337,6 +337,11 @@ export const AG_GRID_LOCALE_RO = { sortAbsoluteDescending: 'Sortează în ordine descendentă absolută', sortUnSort: 'Șterge Sortarea', shiftF2: 'Shift+F2', + toolbarFind: 'Caută', + toolbarFindPreviousMatch: 'Potrivire Anterioară', + toolbarFindNextMatch: 'Potrivire Următoare', + toolbarQuickFilter: 'Filtru', + toolbarMenu: 'Meniu', // Enterprise Menu Aggregation and Status Bar sum: 'Sumă', diff --git a/community-modules/locale/src/sk-SK.ts b/community-modules/locale/src/sk-SK.ts index 56ba0c398fd..f14c372b441 100644 --- a/community-modules/locale/src/sk-SK.ts +++ b/community-modules/locale/src/sk-SK.ts @@ -335,6 +335,11 @@ export const AG_GRID_LOCALE_SK = { sortAbsoluteDescending: 'Zoradiť absolútne zostupne', sortUnSort: 'Zrušiť zoradenie', shiftF2: 'Shift+F2', + toolbarFind: 'Nájsť', + toolbarFindPreviousMatch: 'Predchádzajúca zhoda', + toolbarFindNextMatch: 'Nasledujúca zhoda', + toolbarQuickFilter: 'Filter', + toolbarMenu: 'Menu', // Enterprise Menu Aggregation and Status Bar sum: 'Súčet', diff --git a/community-modules/locale/src/sv-SE.ts b/community-modules/locale/src/sv-SE.ts index 0794488842d..c1e896ef0f2 100644 --- a/community-modules/locale/src/sv-SE.ts +++ b/community-modules/locale/src/sv-SE.ts @@ -337,6 +337,11 @@ export const AG_GRID_LOCALE_SE = { sortAbsoluteDescending: 'Sortera Absolut Fallande', sortUnSort: 'Rensa Sortering', shiftF2: 'Shift+F2', + toolbarFind: 'Sök', + toolbarFindPreviousMatch: 'Föregående träff', + toolbarFindNextMatch: 'Nästa träff', + toolbarQuickFilter: 'Filter', + toolbarMenu: 'Meny', // Enterprise Menu Aggregation and Status Bar sum: 'Summa', diff --git a/community-modules/locale/src/tr-TR.ts b/community-modules/locale/src/tr-TR.ts index 4b19ddc1c51..5efc8e14411 100644 --- a/community-modules/locale/src/tr-TR.ts +++ b/community-modules/locale/src/tr-TR.ts @@ -338,6 +338,11 @@ export const AG_GRID_LOCALE_TR = { sortAbsoluteDescending: 'Mutlak Azalan Sırala', sortUnSort: 'Sıralamayı Temizle', shiftF2: 'Shift+F2', + toolbarFind: 'Bul', + toolbarFindPreviousMatch: 'Önceki Eşleşme', + toolbarFindNextMatch: 'Sonraki Eşleşme', + toolbarQuickFilter: 'Filtre', + toolbarMenu: 'Menü', // Enterprise Menu Aggregation and Status Bar sum: 'Toplam', diff --git a/community-modules/locale/src/uk-UA.ts b/community-modules/locale/src/uk-UA.ts index 3898eed4350..127a16c1c3c 100644 --- a/community-modules/locale/src/uk-UA.ts +++ b/community-modules/locale/src/uk-UA.ts @@ -336,6 +336,11 @@ export const AG_GRID_LOCALE_UA = { sortAbsoluteDescending: 'Сортувати за спаданням', sortUnSort: 'Очистити Сортування', shiftF2: 'Shift+F2', + toolbarFind: 'Знайти', + toolbarFindPreviousMatch: 'Попередній збіг', + toolbarFindNextMatch: 'Наступний збіг', + toolbarQuickFilter: 'Фільтр', + toolbarMenu: 'Меню', // Enterprise Menu Aggregation and Status Bar sum: 'Сума', diff --git a/community-modules/locale/src/ur-PK.ts b/community-modules/locale/src/ur-PK.ts index 3ba9ccf3cd8..9a2770e7c3c 100644 --- a/community-modules/locale/src/ur-PK.ts +++ b/community-modules/locale/src/ur-PK.ts @@ -335,6 +335,11 @@ export const AG_GRID_LOCALE_PK = { sortAbsoluteDescending: 'مکمل اُتری ابجدی ترتیب میں ترتیب دیں', sortUnSort: 'ترتیب مٹائیں', shiftF2: 'Shift+F2', + toolbarFind: 'تلاش', + toolbarFindPreviousMatch: 'پچھلا میچ', + toolbarFindNextMatch: 'اگلا میچ', + toolbarQuickFilter: 'فلٹر', + toolbarMenu: 'مینو', // Enterprise Menu Aggregation and Status Bar sum: 'مجموعہ', diff --git a/community-modules/locale/src/vi-VN.ts b/community-modules/locale/src/vi-VN.ts index af2f2f63d12..df624c10692 100644 --- a/community-modules/locale/src/vi-VN.ts +++ b/community-modules/locale/src/vi-VN.ts @@ -335,6 +335,11 @@ export const AG_GRID_LOCALE_VN = { sortAbsoluteDescending: 'Sắp Xếp Giảm Dần Tuyệt Đối', sortUnSort: 'Xóa Sắp Xếp', shiftF2: 'Shift+F2', + toolbarFind: 'Tìm kiếm', + toolbarFindPreviousMatch: 'Kết quả trùng khớp trước', + toolbarFindNextMatch: 'Kết quả trùng khớp tiếp theo', + toolbarQuickFilter: 'Bộ lọc', + toolbarMenu: 'Trình đơn', // Enterprise Menu Aggregation and Status Bar sum: 'Tổng', diff --git a/community-modules/locale/src/zh-CN.ts b/community-modules/locale/src/zh-CN.ts index 1d478e026d6..8cf852cb505 100644 --- a/community-modules/locale/src/zh-CN.ts +++ b/community-modules/locale/src/zh-CN.ts @@ -334,6 +334,11 @@ export const AG_GRID_LOCALE_CN = { sortAbsoluteDescending: '绝对降序排序', sortUnSort: '清除排序', shiftF2: 'Shift+F2', + toolbarFind: '查找', + toolbarFindPreviousMatch: '上一个匹配', + toolbarFindNextMatch: '下一个匹配', + toolbarQuickFilter: '筛选', + toolbarMenu: '菜单', // Enterprise Menu Aggregation and Status Bar sum: '总和', diff --git a/community-modules/locale/src/zh-HK.ts b/community-modules/locale/src/zh-HK.ts index 811234d7a9d..7fc85989f84 100644 --- a/community-modules/locale/src/zh-HK.ts +++ b/community-modules/locale/src/zh-HK.ts @@ -334,6 +334,11 @@ export const AG_GRID_LOCALE_HK = { sortAbsoluteDescending: '絕對降序排序', sortUnSort: '清除排序', shiftF2: 'Shift+F2', + toolbarFind: '尋找', + toolbarFindPreviousMatch: '上一個匹配', + toolbarFindNextMatch: '下一個匹配', + toolbarQuickFilter: '篩選', + toolbarMenu: '菜單', // Enterprise Menu Aggregation and Status Bar sum: '總和', diff --git a/community-modules/locale/src/zh-TW.ts b/community-modules/locale/src/zh-TW.ts index 8698929ce2e..f18fdd40df0 100644 --- a/community-modules/locale/src/zh-TW.ts +++ b/community-modules/locale/src/zh-TW.ts @@ -334,6 +334,11 @@ export const AG_GRID_LOCALE_TW = { sortAbsoluteDescending: '絕對遞減排序', sortUnSort: '清除排列', shiftF2: 'Shift+F2', + toolbarFind: '尋找', + toolbarFindPreviousMatch: '上一次匹配', + toolbarFindNextMatch: '下一個匹配', + toolbarQuickFilter: '篩選', + toolbarMenu: '選單', // Enterprise Menu Aggregation and Status Bar sum: '總和', diff --git a/community-modules/styles/src/internal/base/_base-variables.scss b/community-modules/styles/src/internal/base/_base-variables.scss index 58776acbed1..66f14452ceb 100644 --- a/community-modules/styles/src/internal/base/_base-variables.scss +++ b/community-modules/styles/src/internal/base/_base-variables.scss @@ -25,20 +25,6 @@ --ag-toolbar-separator-color: var(--ag-border-color); --ag-toolbar-separator-width: 1px; - // Shims mapping new Theming API variable names used by toolbar SCSS to legacy equivalents - --ag-spacing: var(--ag-grid-size); - --ag-text-color: var(--ag-foreground-color); - --ag-header-row-border: var(--ag-borders) var(--ag-border-color); - --ag-toolbar-separator-border: solid var(--ag-toolbar-separator-width) var(--ag-toolbar-separator-color); - --ag-input-border: var(--ag-borders-input) var(--ag-input-border-color); - --ag-input-border-radius: var(--ag-border-radius); - --ag-input-background-color: var(--ag-background-color); - --ag-input-placeholder-text-color: var(--ag-disabled-foreground-color); - --ag-input-focus-border: var(--ag-borders-input) var(--ag-input-focus-border-color); - --ag-focus-shadow: var(--ag-input-focus-box-shadow); - --ag-icon-button-hover-background-color: transparent; - --ag-icon-button-hover-color: var(--ag-foreground-color); - --ag-tooltip-background-color: transparent; --ag-tooltip-error-background-color: color-mix( @@ -295,7 +281,7 @@ --ag-note-popup-background-color: var(--ag-menu-background-color); --ag-note-popup-text-color: color-mix(in srgb, transparent, var(--ag-foreground-color) 75%); --ag-note-popup-input-text-color: var(--ag-input-text-color); - --ag-note-popup-input-background-color: var(--ag-input-background-color); + --ag-note-popup-input-background-color: var(--ag-background-color); --ag-note-popup-border: var(--ag-dialog-border); --ag-note-popup-padding: calc(var(--ag-grid-size) / 2); } diff --git a/community-modules/styles/src/internal/base/parts/_batch-edit.scss b/community-modules/styles/src/internal/base/parts/_batch-edit.scss index b9f3c019848..adcf32aa298 100644 --- a/community-modules/styles/src/internal/base/parts/_batch-edit.scss +++ b/community-modules/styles/src/internal/base/parts/_batch-edit.scss @@ -11,7 +11,7 @@ // if the user supplies a semi-transparent background the content of the // cell below won't be visible background-color: var(--ag-background-color); - background-image: linear-gradient(0deg, var(--ag-input-background-color), var(--ag-input-background-color)); + background-image: linear-gradient(0deg, var(--ag-background-color), var(--ag-background-color)); } .ag-row-batch-edit { diff --git a/community-modules/styles/src/internal/base/parts/_column-drop.scss b/community-modules/styles/src/internal/base/parts/_column-drop.scss index 5e93dfd36c3..cddc6da54a8 100644 --- a/community-modules/styles/src/internal/base/parts/_column-drop.scss +++ b/community-modules/styles/src/internal/base/parts/_column-drop.scss @@ -7,6 +7,7 @@ height: calc(var(--ag-grid-size) * 4); padding: 0 calc(var(--ag-grid-size) * 0.5); border: 1px solid var(--ag-chip-border-color); + font-weight: normal; } @include ag.keyboard-focus((ag-column-drop-cell), 2px); @@ -37,6 +38,12 @@ color: var(--ag-secondary-foreground-color); height: var(--ag-header-height); border-bottom: var(--ag-borders) var(--ag-border-color); + + @include ag.unthemed-rtl( + ( + padding-left: var(--ag-cell-horizontal-padding), + ) + ); } .ag-column-drop-horizontal-half-width:not(:last-child) { diff --git a/community-modules/styles/src/internal/base/parts/_toolbar.scss b/community-modules/styles/src/internal/base/parts/_toolbar.scss index b75cccfaa26..c3e879213e9 100644 --- a/community-modules/styles/src/internal/base/parts/_toolbar.scss +++ b/community-modules/styles/src/internal/base/parts/_toolbar.scss @@ -5,7 +5,7 @@ display: flex; align-items: center; overflow: hidden; - border-bottom: var(--ag-header-row-border); + border-bottom: var(--ag-borders) var(--ag-border-color); min-height: var(--ag-header-height); background-color: var(--ag-toolbar-background-color); color: var(--ag-toolbar-text-color); @@ -21,20 +21,20 @@ .ag-toolbar-item { display: inline-flex; - margin: 0 calc(var(--ag-spacing, 8px) * 2); + margin: 0 calc(var(--ag-grid-size, 8px) * 2); } .ag-toolbar-button-wrapper { display: inline-flex; - padding: calc(var(--ag-spacing) * 0.25); + padding: calc(var(--ag-grid-size) * 0.25); height: 100%; } .ag-toolbar-button { display: inline-flex; align-items: center; - gap: var(--ag-spacing); - padding: calc(var(--ag-spacing)); + gap: var(--ag-grid-size); + padding: calc(var(--ag-grid-size)); border: 0; background: transparent; color: var(--ag-toolbar-text-color); @@ -45,14 +45,14 @@ } .ag-toolbar-button-wrapper:hover { - background-color: var(--ag-icon-button-hover-background-color); - color: var(--ag-icon-button-hover-color); + background-color: var(--ag-icon-button-hover-background-color, transparent); + color: var(--ag-icon-button-hover-color, var(--ag-foreground-color)); } // stylelint-disable-next-line selector-max-specificity .ag-toolbar-button-wrapper:hover .ag-toolbar-button, .ag-toolbar-button-wrapper:hover .ag-toolbar-button .ag-icon { - color: var(--ag-icon-button-hover-color); + color: var(--ag-icon-button-hover-color, var(--ag-foreground-color)); } // stylelint-disable-next-line selector-max-specificity @@ -66,7 +66,7 @@ } .ag-toolbar-button:focus-visible { - box-shadow: var(--ag-focus-shadow); + box-shadow: var(--ag-input-focus-box-shadow); } .ag-toolbar-button:focus:not(:focus-visible) { @@ -83,7 +83,6 @@ display: inline-flex; flex: 1; min-width: 235px; - padding: 0 calc(var(--ag-spacing) * 2); } .ag-toolbar-input { @@ -92,7 +91,7 @@ align-items: center; min-width: 200px; max-width: none; - margin: 0 calc(var(--ag-spacing) * 2); + margin: 0 calc(var(--ag-grid-size) * 2); } // stylelint-disable-next-line selector-max-specificity @@ -103,12 +102,12 @@ // stylelint-disable-next-line selector-max-specificity .ag-toolbar > .ag-toolbar-input:first-child, .ag-toolbar-right-start + .ag-toolbar-input { - margin-inline-start: var(--ag-spacing); + margin-inline-start: var(--ag-grid-size); } // stylelint-disable-next-line selector-max-specificity .ag-toolbar > .ag-toolbar-input:last-child { - margin-inline-end: var(--ag-spacing); + margin-inline-end: var(--ag-grid-size); } .ag-toolbar-input-icon { @@ -118,19 +117,19 @@ color: var(--ag-toolbar-text-color); opacity: 0.5; pointer-events: none; - inset-inline-start: var(--ag-spacing); + inset-inline-start: var(--ag-grid-size); } .ag-toolbar-input-field { - color: var(--ag-text-color); + color: var(--ag-foreground-color); font-size: var(--ag-font-size); line-height: 1.5; outline: none; - border: var(--ag-input-border); - border-radius: var(--ag-input-border-radius); + border: var(--ag-borders-input) var(--ag-input-border-color); + border-radius: var(--ag-border-radius); width: 100%; - padding-block: calc(var(--ag-spacing) * 0.5); - padding-inline: calc(var(--ag-icon-size) + var(--ag-spacing) * 2) var(--ag-spacing); + padding-block: calc(var(--ag-grid-size) * 0.5); + padding-inline: calc(var(--ag-icon-size) + var(--ag-grid-size) * 2) var(--ag-grid-size); } // High-specificity padding override — beats theme text-input padding-left rules @@ -140,26 +139,29 @@ input[class^='ag-'][type='text'].ag-toolbar-input-field { @include ag.unthemed-rtl( ( - padding-left: calc(var(--ag-icon-size) + var(--ag-spacing) * 2), - padding-right: var(--ag-spacing), + padding-left: calc(var(--ag-icon-size) + var(--ag-grid-size) * 2), + padding-right: var(--ag-grid-size), ) ); } } .ag-toolbar-input-field:focus { - box-shadow: var(--ag-focus-shadow); - border: var(--ag-input-focus-border); + box-shadow: var(--ag-input-focus-box-shadow); + border: var(--ag-borders-input) var(--ag-input-focus-border-color); } .ag-toolbar-input-field::placeholder { - color: var(--ag-input-placeholder-text-color); + color: var(--ag-disabled-foreground-color); } .ag-toolbar-panel .ag-column-drop-horizontal { background-color: transparent; border-bottom: none; padding: 0; + font-weight: normal; + font-size: var(--ag-font-size); + font-family: var(--ag-font-family); } // stylelint-disable-next-line selector-max-specificity @@ -175,23 +177,23 @@ .ag-toolbar-separator { align-self: stretch; width: 0; - margin: calc(var(--ag-spacing) * 1.75) 0; - border-inline-start: var(--ag-toolbar-separator-border); + margin: calc(var(--ag-grid-size) * 1.75) 0; + border-inline-start: solid var(--ag-toolbar-separator-width) var(--ag-toolbar-separator-color); } .ag-toolbar-find { - gap: calc(var(--ag-spacing) * 0.5); + gap: calc(var(--ag-grid-size) * 0.5); width: 280px; max-width: none; min-width: 220px; - border: var(--ag-input-border); - border-radius: var(--ag-input-border-radius); - background-color: var(--ag-input-background-color); + border: var(--ag-borders-input) var(--ag-input-border-color); + border-radius: var(--ag-border-radius); + background-color: var(--ag-background-color); } .ag-toolbar-find:focus-within { - box-shadow: var(--ag-focus-shadow); - border: var(--ag-input-focus-border); + box-shadow: var(--ag-input-focus-box-shadow); + border: var(--ag-borders-input) var(--ag-input-focus-border-color); } .ag-toolbar-find .ag-toolbar-input-field { @@ -226,6 +228,6 @@ .ag-toolbar-find-button { flex-shrink: 0; - padding: calc(var(--ag-spacing) * 0.5); + padding: calc(var(--ag-grid-size) * 0.5); } } diff --git a/community-modules/styles/src/internal/themes/alpine/_index.scss b/community-modules/styles/src/internal/themes/alpine/_index.scss index 43a71534870..5146a38dfa4 100644 --- a/community-modules/styles/src/internal/themes/alpine/_index.scss +++ b/community-modules/styles/src/internal/themes/alpine/_index.scss @@ -17,6 +17,10 @@ color: var(--ag-header-foreground-color); } + .ag-toolbar { + font-weight: 700; + } + .ag-row { font-size: calc(var(--ag-font-size) + 1px); } diff --git a/documentation/ag-grid-docs/public/changelog/changelog.json b/documentation/ag-grid-docs/public/changelog/changelog.json index 0d89bb0e454..8b20d07c59d 100644 --- a/documentation/ag-grid-docs/public/changelog/changelog.json +++ b/documentation/ag-grid-docs/public/changelog/changelog.json @@ -1,4 +1,18 @@ [ + { + "key": "AG-17289", + "issueType": "Task", + "manualEntry": true, + "summary": "Placeholder for 35.3.0", + "versions": ["35.3.0"], + "status": "Done", + "resolution": "Done", + "features": null, + "moreInformation": null, + "deprecationNotes": null, + "breakingChangesNotes": null, + "documentationUrl": null + }, { "key": "AG-17021", "issueType": "Bug", diff --git a/documentation/ag-grid-docs/public/changelog/releaseVersionNotes.json b/documentation/ag-grid-docs/public/changelog/releaseVersionNotes.json index e76d2dd5963..602a4090887 100644 --- a/documentation/ag-grid-docs/public/changelog/releaseVersionNotes.json +++ b/documentation/ag-grid-docs/public/changelog/releaseVersionNotes.json @@ -1,4 +1,8 @@ [ + { + "release version": "35.3.0", + "markdown": "/releases/35_3_0.md" + }, { "release version": "35.2.1", "markdown": "/releases/35_2_1.md" diff --git a/documentation/ag-grid-docs/public/changelog/releases/35_3_0.md b/documentation/ag-grid-docs/public/changelog/releases/35_3_0.md new file mode 100644 index 00000000000..df546d8359a --- /dev/null +++ b/documentation/ag-grid-docs/public/changelog/releases/35_3_0.md @@ -0,0 +1,5 @@ +#### 13th May 2026 - Grid v35.3.0 (Charts v13.3.0) + +See table below for changes included in this release. + +For more details see [v35.3 release post](https://blog.ag-grid.com/whats-new-in-ag-grid-35-3/). diff --git a/documentation/ag-grid-docs/public/theme-icons/alpine/alpine-icons.zip b/documentation/ag-grid-docs/public/theme-icons/alpine/alpine-icons.zip index 70c9a8e3ecc..d9b7747e090 100644 Binary files a/documentation/ag-grid-docs/public/theme-icons/alpine/alpine-icons.zip and b/documentation/ag-grid-docs/public/theme-icons/alpine/alpine-icons.zip differ diff --git a/documentation/ag-grid-docs/public/theme-icons/alpine/search.svg b/documentation/ag-grid-docs/public/theme-icons/alpine/search.svg index f5d1da828b1..aab50730d6a 100644 --- a/documentation/ag-grid-docs/public/theme-icons/alpine/search.svg +++ b/documentation/ag-grid-docs/public/theme-icons/alpine/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/documentation/ag-grid-docs/public/theme-icons/balham/balham-icons.zip b/documentation/ag-grid-docs/public/theme-icons/balham/balham-icons.zip index 33a8a303c6e..ac382218d1e 100644 Binary files a/documentation/ag-grid-docs/public/theme-icons/balham/balham-icons.zip and b/documentation/ag-grid-docs/public/theme-icons/balham/balham-icons.zip differ diff --git a/documentation/ag-grid-docs/public/theme-icons/balham/search.svg b/documentation/ag-grid-docs/public/theme-icons/balham/search.svg index f5d1da828b1..aab50730d6a 100644 --- a/documentation/ag-grid-docs/public/theme-icons/balham/search.svg +++ b/documentation/ag-grid-docs/public/theme-icons/balham/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/documentation/ag-grid-docs/public/theme-icons/base/base-icons.zip b/documentation/ag-grid-docs/public/theme-icons/base/base-icons.zip index b4aa3021d3a..26fcc7a0e54 100644 Binary files a/documentation/ag-grid-docs/public/theme-icons/base/base-icons.zip and b/documentation/ag-grid-docs/public/theme-icons/base/base-icons.zip differ diff --git a/documentation/ag-grid-docs/public/theme-icons/material/material-icons.zip b/documentation/ag-grid-docs/public/theme-icons/material/material-icons.zip index 3855cd9de22..185f85987eb 100644 Binary files a/documentation/ag-grid-docs/public/theme-icons/material/material-icons.zip and b/documentation/ag-grid-docs/public/theme-icons/material/material-icons.zip differ diff --git a/documentation/ag-grid-docs/public/theme-icons/material/search.svg b/documentation/ag-grid-docs/public/theme-icons/material/search.svg index f5d1da828b1..aab50730d6a 100644 --- a/documentation/ag-grid-docs/public/theme-icons/material/search.svg +++ b/documentation/ag-grid-docs/public/theme-icons/material/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/documentation/ag-grid-docs/public/theme-icons/quartz/quartz-icons.zip b/documentation/ag-grid-docs/public/theme-icons/quartz/quartz-icons.zip index 21d7ca82527..97bfb4da525 100644 Binary files a/documentation/ag-grid-docs/public/theme-icons/quartz/quartz-icons.zip and b/documentation/ag-grid-docs/public/theme-icons/quartz/quartz-icons.zip differ diff --git a/documentation/ag-grid-docs/public/theme-icons/quartz/search.svg b/documentation/ag-grid-docs/public/theme-icons/quartz/search.svg index f5d1da828b1..aab50730d6a 100644 --- a/documentation/ag-grid-docs/public/theme-icons/quartz/search.svg +++ b/documentation/ag-grid-docs/public/theme-icons/quartz/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/documentation/ag-grid-docs/src/constants.ts b/documentation/ag-grid-docs/src/constants.ts index 8e21c63d3ba..837ed0d2d72 100644 --- a/documentation/ag-grid-docs/src/constants.ts +++ b/documentation/ag-grid-docs/src/constants.ts @@ -176,6 +176,8 @@ function calculateGridUrl() { export const GRID_URL = calculateGridUrl(); +export const PRODUCTION_STUDIO_SITE_URL = 'https://www.ag-grid.com/studio'; + export const LIVE_SITEMAP_URL = import.meta.env?.LIVE_SITEMAP_URL; export const EXAMPLE_RANDOM_SEED = 'AG Grid Random Seed'; @@ -183,6 +185,7 @@ export const EXAMPLE_RANDOM_SEED = 'AG Grid Random Seed'; export const TRIAL_LICENCE_FORM_URL = import.meta.env?.PUBLIC_TRIAL_LICENCE_FORM_URL; export const EXAMPLE_STYLE_FILE_NAME = 'ag-example-styles.css'; +export const DEBUG_SCRIPT_FILE_NAME = 'ag-grid-debug.js'; export const PRODUCTION_CHANGELOG_JSON_URL = 'https://www.ag-grid.com/changelog/changelog.json'; diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/index.mdoc b/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/index.mdoc index 6307e8d09d8..265d49b4a36 100644 --- a/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/index.mdoc @@ -46,7 +46,7 @@ Refer to the [Side Bar](./side-bar/) documentation for further configuration opt ## Row Group Panel in the Toolbar -The Row Group Panel can be embedded in the [Quick Access Toolbar](./toolbar/#row-group-panel) using the `agRowGroupPanelToolbarItem` built-in item, configured independently of `rowGroupPanelShow`. +The Row Group Panel can be embedded in the [Quick Access Toolbar](./toolbar/#row-group-and-pivot-panels) using the `agRowGroupPanelToolbarItem` built-in item, configured independently of `rowGroupPanelShow`. ```{% frameworkTransform=true %} const gridOptions = { diff --git a/documentation/ag-grid-docs/src/content/docs/pivoting/index.mdoc b/documentation/ag-grid-docs/src/content/docs/pivoting/index.mdoc index e569a184426..0112ad086e0 100644 --- a/documentation/ag-grid-docs/src/content/docs/pivoting/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/pivoting/index.mdoc @@ -87,9 +87,9 @@ const gridOptions = { }; ``` -## Pivot Panel in the Toolbar +### Pivot Panel in the Toolbar -The Pivot Panel can be embedded in the [Quick Access Toolbar](./toolbar/#pivot-panel) using the `agPivotPanelToolbarItem` built-in item, configured independently of `pivotPanelShow`. +The Pivot Panel can be embedded in the [Quick Access Toolbar](./toolbar/#row-group-and-pivot-panels) using the `agPivotPanelToolbarItem` built-in item, configured independently of `pivotPanelShow`. ```{% frameworkTransform=true %} const gridOptions = { diff --git a/documentation/ag-grid-docs/src/content/docs/row-models/index.mdoc b/documentation/ag-grid-docs/src/content/docs/row-models/index.mdoc index 9af4d064e7f..640bfb5cb83 100644 --- a/documentation/ag-grid-docs/src/content/docs/row-models/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/row-models/index.mdoc @@ -15,7 +15,7 @@ The grid comes with four row models: The Client-Side Row Model deals with client-side data. The Server-Side, Infinite and Viewport Row Models deal with server-side data. The following is a summary of each: -- ## Client-Side +- ## Client-Side {% #client-side %} This is the default. The grid will load all of the data into the grid in one go. The grid can then perform filtering, sorting, grouping, pivoting and aggregation all in browser memory. diff --git a/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/example.spec.ts index 6cd99a5c8ff..8439996b6d1 100644 --- a/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/example.spec.ts +++ b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/example.spec.ts @@ -7,8 +7,8 @@ test.agExample(import.meta, () => { const toolbar = page.locator('.ag-toolbar'); await expect(toolbar).toBeVisible(); - await expect(toolbar.locator(':scope > .ag-toolbar-button-wrapper')).toHaveCount(6); - await expect(toolbar.locator(':scope > .ag-toolbar-separator')).toHaveCount(5); + await expect(toolbar.locator(':scope > .ag-toolbar-button-wrapper')).toHaveCount(7); + await expect(toolbar.locator(':scope > .ag-toolbar-separator')).toHaveCount(3); }); test.eachFramework('Action buttons have no visible label text', async ({ page }) => { diff --git a/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/main.ts b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/main.ts index 5e1be459a41..9375db14e68 100644 --- a/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/main.ts +++ b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/action-buttons/main.ts @@ -10,12 +10,13 @@ import { ValidationModule, createGrid, } from 'ag-grid-community'; -import { ContextMenuModule, ToolbarModule } from 'ag-grid-enterprise'; +import { ColumnMenuModule, ContextMenuModule, ToolbarModule } from 'ag-grid-enterprise'; ModuleRegistry.registerModules([ ClientSideRowModelModule, TextFilterModule, NumberFilterModule, + ColumnMenuModule, CsvExportModule, ColumnAutoSizeModule, ColumnApiModule, @@ -29,7 +30,7 @@ let gridApi: GridApi; const gridOptions: GridOptions = { columnDefs: [ { field: 'athlete' }, - { field: 'country' }, + { field: 'country', filter: 'agTextColumnFilter' }, { field: 'gold' }, { field: 'silver' }, { field: 'bronze' }, @@ -46,7 +47,6 @@ const gridOptions: GridOptions = { tooltip: 'Size Columns to Fit', action: (params) => params.api.sizeColumnsToFit(), }, - 'separator', { key: 'autoSizeAll', icon: 'minimize', @@ -64,19 +64,38 @@ const gridOptions: GridOptions = { defaultState: { sort: null }, }), }, + { + key: 'sortFirstColumnDesc', + icon: 'sortDescending', + tooltip: 'Sort First Column Descending', + action: (params) => + params.api.applyColumnState({ + state: [{ colId: 'athlete', sort: 'desc' }], + defaultState: { sort: null }, + }), + }, 'separator', { - key: 'resetFilters', - icon: 'clipboardCut', - tooltip: 'Reset All Filters', + key: 'addFilter', + icon: 'filter-add', + tooltip: 'Add Filter', + action: (params) => + params.api.setFilterModel({ + country: { filterType: 'text', type: 'contains', filter: 'Canada' }, + }), + }, + { + key: 'clearFilters', + icon: 'filterActive', + tooltip: 'Clear All Filters', action: (params) => params.api.setFilterModel(null), }, 'separator', { - key: 'resetColumns', + key: 'showColumnChooser', icon: 'columns', - tooltip: 'Reset Column State', - action: (params) => params.api.resetColumnState(), + tooltip: 'Open Column Chooser', + action: (params) => params.api.showColumnChooser(), }, ], }, diff --git a/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/example.spec.ts new file mode 100644 index 00000000000..815d401fb71 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/example.spec.ts @@ -0,0 +1,13 @@ +import { expect, test, waitForGridContent } from '@utils/grid/test-utils'; + +test.agExample(import.meta, () => { + test.eachFramework('Row group and pivot panels render in toolbar', async ({ page }) => { + await waitForGridContent(page); + + const toolbar = page.locator('.ag-toolbar'); + await expect(toolbar).toBeVisible(); + + await expect(toolbar.locator('.ag-column-drop-horizontal')).toHaveCount(2); + await expect(toolbar.locator('.ag-toolbar-button-wrapper')).toHaveCount(1); + }); +}); diff --git a/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/index.html b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/index.html new file mode 100644 index 00000000000..378fad58398 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/index.html @@ -0,0 +1 @@ +
diff --git a/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/main.ts b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/main.ts new file mode 100644 index 00000000000..e58fdf107f4 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/toolbar/_examples/row-group-pivot-panels/main.ts @@ -0,0 +1,66 @@ +import type { GridApi, GridOptions } from 'ag-grid-community'; +import { + ClientSideRowModelModule, + ColumnApiModule, + ModuleRegistry, + ValidationModule, + createGrid, +} from 'ag-grid-community'; +import { PivotModule, RowGroupingModule, RowGroupingPanelModule, ToolbarModule } from 'ag-grid-enterprise'; + +ModuleRegistry.registerModules([ + ClientSideRowModelModule, + ColumnApiModule, + RowGroupingModule, + RowGroupingPanelModule, + PivotModule, + ToolbarModule, + ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []), +]); + +let gridApi: GridApi; + +const gridOptions: GridOptions = { + columnDefs: [ + { field: 'country', enableRowGroup: true, rowGroup: true }, + { field: 'year', enableRowGroup: true, enablePivot: true, pivot: true }, + { field: 'sport', enableRowGroup: true, enablePivot: true }, + { field: 'gold', enableValue: true, aggFunc: 'sum' }, + { field: 'silver', enableValue: true, aggFunc: 'sum' }, + { field: 'total', enableValue: true, aggFunc: 'sum' }, + ], + defaultColDef: { + flex: 1, + minWidth: 120, + }, + autoGroupColumnDef: { + minWidth: 200, + }, + pivotMode: true, + toolbar: { + items: [ + 'agRowGroupPanelToolbarItem', + 'separator', + 'agPivotPanelToolbarItem', + 'separator', + { + icon: 'columns', + label: 'Reset', + alignment: 'right', + action: (params) => { + params.api.setGridOption('pivotMode', true); + params.api.resetColumnState(); + }, + }, + ], + }, +}; + +document.addEventListener('DOMContentLoaded', () => { + const gridDiv = document.querySelector('#myGrid')!; + gridApi = createGrid(gridDiv, gridOptions); + + fetch('https://www.ag-grid.com/example-assets/olympic-winners.json') + .then((response) => response.json()) + .then((data: IOlympicData[]) => gridApi!.setGridOption('rowData', data)); +}); diff --git a/documentation/ag-grid-docs/src/content/docs/toolbar/index.mdoc b/documentation/ag-grid-docs/src/content/docs/toolbar/index.mdoc index a907e5a2a08..a4ad48955de 100644 --- a/documentation/ag-grid-docs/src/content/docs/toolbar/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/toolbar/index.mdoc @@ -67,43 +67,17 @@ A number of built-in toolbar items are provided for common use cases that integr | `agQuickFilterToolbarItem` | Text input that filters grid rows using the [Quick Filter](./filter-quick/). | `QuickFilterModule` | | `agFindToolbarItem` | Text input that searches within grid cells using [Find](./find/). | `FindModule` | | `agRowGroupPanelToolbarItem` | Embeds the [Row Group Panel](./grouping-group-panel/). | `RowGroupingPanelModule` | -| `agPivotPanelToolbarItem` | Embeds the [Pivot Panel](./pivoting/). | `RowGroupingPanelModule` | +| `agPivotPanelToolbarItem` | Embeds the [Pivot Panel](./pivoting/#enabling-the-pivot-panel/). | `RowGroupingPanelModule` | | [`agMenuToolbarItem`](#dropdown-menus) | Button that opens a [dropdown menu](#dropdown-menus). | `ContextMenuModule` or `ColumnMenuModule` | | `separator` | Vertical divider used to group items visually. Has no behaviour of its own. | None | -{% note %} -When the same built-in item appears more than once, set a unique `key` on each occurrence. Without a `key`, the item type is used as the key, so only the first duplicate will be rendered. -{% /note %} +### Row Group and Pivot Panels -### Row Group Panel +The Row Group Panel and Pivot Panel can both be embedded in the Quick Access Toolbar using `agRowGroupPanelToolbarItem` and `agPivotPanelToolbarItem`. Both panels are configured independently of the [Row Group Panel](./grouping-group-panel/) and the [Pivot Panel](./pivoting/#enabling-the-pivot-panel/), so you can display each panel in the Toolbar, above the grid, or both at the same time. -The Row Group Panel can be embedded in the Quick Access Toolbar using the `agRowGroupPanelToolbarItem` built-in item. +The example below shows both panels in the toolbar along with a reset action button. Use the panels to rearrange columns, then click Reset to restore the initial layout. -The Toolbar Row Group Panel is configured independently of `rowGroupPanelShow`. This means you can display the Row Group Panel in the Toolbar, above the grid, or both at the same time. In most cases, if you are using the Toolbar version, you would typically not also show the standard Row Group Panel above the grid. - -```{% frameworkTransform=true %} -const gridOptions = { - toolbar: { - items: ['agRowGroupPanelToolbarItem'], - }, - // other grid options ... -} -``` - -### Pivot Panel - -The Pivot Panel can be embedded in the Quick Access Toolbar using the `agPivotPanelToolbarItem` built-in item. - -The Toolbar Pivot Panel is configured independently of `pivotPanelShow`. This means you can display the Pivot Panel in the Toolbar, above the grid, or both at the same time. In most cases, if you are using the Toolbar version, you would typically not also show the standard Pivot Panel above the grid. - -```{% frameworkTransform=true %} -const gridOptions = { - toolbar: { - items: ['agPivotPanelToolbarItem'], - }, - // other grid options ... -} -``` +{% gridExampleRunner title="Row Group and Pivot Panels" name="row-group-pivot-panels" exampleHeight=350 /%} ### Dropdown Menus @@ -162,7 +136,7 @@ The example below shows icon-only buttons with tooltips for sizing columns, sort For controls beyond a button, such as toggles, inputs, or any stateful UI, set `toolbarItem` to a custom component. Custom components can render arbitrary HTML and call any grid API, so they suit cases that the [Action Button](#action-buttons) shorthand cannot express. -The example below defines two custom items: checkbox toggles that apply column filters on the left, and a radio group that opens [Side Bar](./tool-panel/) tool panels on the right. The radio group exposes a `setSelected` method, called from the grid's `onToolPanelVisibleChanged` callback via [`getToolbarItemInstance`](#reference-getToolbarItemInstance) to keep its state in sync when a panel is opened or closed elsewhere, for example by clicking a side bar tab. +The example below defines two custom items: checkbox toggles that apply column filters on the left, and a radio group that opens [Side Bar](./tool-panel/) tool panels on the right. The radio group's `setSelected` method is called via [`getToolbarItemInstance`](#reference-getToolbarItemInstance) in `onToolPanelVisibleChanged` to stay in sync when a panel is opened or closed elsewhere, such as via a sidebar tab. {% gridExampleRunner title="Custom Toolbar Item" name="toolbar-custom" /%} @@ -210,7 +184,7 @@ The toolbar exposes the following [Theme Parameters](./theming-parameters/): ## Accessing Toolbar Items -To access a toolbar item instance use the grid api method `getToolbarItemInstance(key)`. This is demonstrated in the [Custom Components](#custom-components) example above, where it's used to keep a toolbar radio in sync with side bar tool panel changes. +To access a toolbar item instance use the grid api method `getToolbarItemInstance(key)`. The `key` must match a `key` set on the item definition; items without an explicit key are not reachable via the API. This is demonstrated in the [Custom Components](#custom-components) example above, where it's used to keep a toolbar radio in sync with side bar tool panel changes. {% apiDocumentation source="grid-api/api.json" section="accessories" names=["getToolbarItemInstance"] /%} diff --git a/documentation/ag-grid-docs/src/content/docs/upgrading-to-ag-grid-35-3/index.mdoc b/documentation/ag-grid-docs/src/content/docs/upgrading-to-ag-grid-35-3/index.mdoc new file mode 100644 index 00000000000..5b10069acbc --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/upgrading-to-ag-grid-35-3/index.mdoc @@ -0,0 +1,37 @@ +--- +title: "Upgrading to AG Grid 35.3" +description: "See what's new in AG Grid, view a full list of changes and migrate your $framework Data Grid to version v35.3." +migrationVersion: "35.3.0" +--- + +Quick Access Toolbar, Cell Notes, Grand Total Row for server-side row model, Performance Improvements, Quality Improvements + +## What's New + +AG Grid {% migrationVersion() %} adds important new features - [Quick Access Toolbar](./toolbar/), [Cell Notes](./notes/), [Grand Total Row for server-side row model](./server-side-model-grouping/#grand-total-row), Performance Improvements, Quality Improvements. +These improvements involve no breaking changes as listed below. + + + +{% documentationArchiveSection version=migrationVersionPatch() /%} + +## Breaking Changes + +There are no breaking changes in AG Grid version {% migrationVersion() %}. + +## Behaviour Changes + +There are no behaviour changes in AG Grid version {% migrationVersion() %}. + +## Removal of Deprecated APIs + +There are no deprecated API removals in AG Grid version {% migrationVersion() %}. + +## Deprecations + +There are no deprecations in AG Grid version {% migrationVersion() %}. + +{% changelogSection version=$migrationVersion /%} diff --git a/documentation/ag-grid-docs/src/content/versions/ag-grid-versions.json b/documentation/ag-grid-docs/src/content/versions/ag-grid-versions.json index 8e459bff7b6..20efb301f78 100644 --- a/documentation/ag-grid-docs/src/content/versions/ag-grid-versions.json +++ b/documentation/ag-grid-docs/src/content/versions/ag-grid-versions.json @@ -1,4 +1,31 @@ [ + { + "version": "35.3.0", + "date": "13th May 2026", + "landingPageHighlight": "Quick Access Toolbar, Cell Notes, Grand Total Row for server-side row model, Performance Improvements, Quality Improvements", + "highlights": [ + { + "text": "Quick Access Toolbar", + "path": "./toolbar/" + }, + { + "text": "Cell Notes", + "path": "./notes/" + }, + { + "text": "Grand Total Row for server-side row model", + "path": "./server-side-model-grouping/#grand-total-row" + }, + { + "text": "Performance Improvements" + }, + { + "text": "Quality Improvements" + } + ], + "notesPath": "./upgrading-to-ag-grid-35-3", + "hideBlogPostLink": false + }, { "version": "35.2.1", "date": "7th April 2026" diff --git a/external/ag-website-shared/.gitrepo b/external/ag-website-shared/.gitrepo index 952e2e0f476..d0d709179a1 100644 --- a/external/ag-website-shared/.gitrepo +++ b/external/ag-website-shared/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:ag-grid/ag-website-shared.git branch = latest - commit = 886dd2eccf0bc1359311d257544851f510b77aa7 - parent = 48d01a5528b8fbaa40f5b4e3e9f9b44c875db143 + commit = 01e1bd873328886ae6aa639e0c069ada9ca068c2 + parent = 46ce864be91ce5a79f57ddd6ffeb22c3bb2af1ed method = rebase cmdver = 0.4.9 diff --git a/external/ag-website-shared/.prettierrc b/external/ag-website-shared/.prettierrc index 81dfed058f4..57e5f02f085 100644 --- a/external/ag-website-shared/.prettierrc +++ b/external/ag-website-shared/.prettierrc @@ -7,6 +7,7 @@ "importOrderParserPlugins": ["typescript", "decorators-legacy"], "importOrderSeparation": true, "importOrderSortSpecifiers": true, + "importOrderImportAttributesKeyword": "with", "overrides": [ { "files": [ diff --git a/external/ag-website-shared/plugins/agCssAsString.ts b/external/ag-website-shared/plugins/agCssAsString.ts new file mode 100644 index 00000000000..502ec6f4b89 --- /dev/null +++ b/external/ag-website-shared/plugins/agCssAsString.ts @@ -0,0 +1,46 @@ +import type { Plugin } from 'vite'; + +type Options = { + /** + * Workspace package names whose `src/**` files should have CSS imports rewritten. + * e.g. ['ag-charts-community', 'ag-charts-enterprise', 'ag-charts-core'] + * ['ag-studio'] + */ + packages: string[]; +}; + +/* + * Vite 7 (pulled in by Astro 6) removed the default string export from plain `.css` imports. + * Library source files using `import STYLES from '.../styles.css'` as a string (fed into + * `addStyles()` / `enterpriseRegistry.styles`) relied on that deprecated behaviour. + * This plugin rewrites those imports to use `?inline` so Vite returns the CSS as a string. + * + * Remove a package from `packages` once its source files use `?inline` explicitly or stop + * importing CSS as a string (e.g. by moving style injection into their own build pipeline). + * + * Scope: only default-binding imports (`import X from '...css'`). Side-effect imports + * (`import '...css'`) are intentionally left alone — they rely on Vite's default + * stylesheet-injection behaviour (e.g. ag-grid's `main-umd-styles.ts`). + */ +export default function agCssAsString({ packages }: Options): Plugin { + const escaped = packages.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + const SOURCE_PATTERN = new RegExp(`packages/(${escaped.join('|')})/src/`); + + // Match default-binding imports of plain `.css` only. The negative lookahead + // (?!\?) explicitly excludes imports that already carry a Vite query suffix + // (`./x.css?raw`, `./x.css?url`, etc.) so those pass through unchanged. + const PLAIN_CSS_IMPORT = /(from\s+['"][^'"]+\.css)(?!\?)(['"])/g; + + return { + name: 'ag-css-as-string', + enforce: 'pre', + transform(code, id) { + if (!SOURCE_PATTERN.test(id)) return null; + if (!PLAIN_CSS_IMPORT.test(code)) return null; + + // Reset lastIndex — RegExp objects with /g remember state across .test/.exec. + PLAIN_CSS_IMPORT.lastIndex = 0; + return code.replace(PLAIN_CSS_IMPORT, '$1?inline$2'); + }, + }; +} diff --git a/external/ag-website-shared/plugins/agLinkChecker.ts b/external/ag-website-shared/plugins/agLinkChecker.ts index ae30f0b88f3..a86689823e4 100644 --- a/external/ag-website-shared/plugins/agLinkChecker.ts +++ b/external/ag-website-shared/plugins/agLinkChecker.ts @@ -91,46 +91,39 @@ const checkLinks = async (dir: string, files: string[], options: Options) => { const anchorTags: string[] = []; // uses a stream as ingesting the entire file was causing memory crashes. - const fileStream = fs.createReadStream(join(dir, filePath)); - await new Promise((resolve) => { - fileStream.on('readable', function () { - let prev; - let active = false; - let str = ''; - let chunk; - while (null !== (chunk = fileStream.read(16384))) { - const strChunk = chunk.toString(); - for (let i = 0; i < strChunk.length; i++) { - const chr = strChunk[i]; - if (!prev || prev === '<') { - if (chr === 'a') { - active = true; - } - } else if (active && chr === '>') { - active = false; - anchorTags.push(str); - str = ''; - } - - if (active) { - str += chr; - - if (str.length >= 2 && !str.startsWith('a ')) { - active = false; - str = ''; - } - } - - prev = chr; + // State is held in the closure here (not inside an event handler) so it + // survives across stream chunks — otherwise tags that straddle a chunk + // boundary are silently dropped. + const fileStream = fs.createReadStream(join(dir, filePath), { encoding: 'utf8' }); + let prev: string | undefined; + let active = false; + let str = ''; + for await (const chunk of fileStream) { + const strChunk = chunk as string; + for (let i = 0; i < strChunk.length; i++) { + const chr = strChunk[i]; + if (!prev || prev === '<') { + if (chr === 'a') { + active = true; } + } else if (active && chr === '>') { + active = false; + anchorTags.push(str); + str = ''; } - }); - fileStream.on('end', () => { - resolve(); - }); - }); - fileStream.close(); + if (active) { + str += chr; + + if (str.length >= 2 && !str.startsWith('a ')) { + active = false; + str = ''; + } + } + + prev = chr; + } + } anchorTags.forEach((tag) => { const regex = /.*href="(.*?)".*/g; @@ -205,8 +198,8 @@ const checkLinks = async (dir: string, files: string[], options: Options) => { validationResults[link] = { error }; return; } else { - // check if the hash exists in the file - if (!anchors.has(linkWithoutPrefix)) { + // Check if the hash exists in the file + if (!anchors.has(linkWithoutPrefix) && !anchors.has(linkWithoutPrefix.replace('#', '/#'))) { errors.push( `Link to ${originalLink} could not be resolved in (${filePathsString(filePaths, options)}).` ); diff --git a/external/ag-website-shared/plugins/agSvgAsString.ts b/external/ag-website-shared/plugins/agSvgAsString.ts new file mode 100644 index 00000000000..19273eddef0 --- /dev/null +++ b/external/ag-website-shared/plugins/agSvgAsString.ts @@ -0,0 +1,47 @@ +import type { Plugin } from 'vite'; + +type Options = { + /** + * Workspace package names whose `src/**` files should have SVG imports rewritten. + * e.g. ['ag-studio'] + */ + packages: string[]; +}; + +/* + * Library source files use `import SVG from '.../icon.svg'` as a string (assigned to + * `element.innerHTML`). The library's own build pipeline (esbuild with `loader: 'text'`, + * see `esbuild.config.cjs`'s svgPlugin) returns the SVG markup as a raw string. + * + * Vite's default `.svg` import returns a URL, and Astro further wraps it as an asset + * metadata object — neither matches what the library source expects, producing + * `[object Object]` rendered text instead of the icon. + * + * This plugin rewrites those imports to use `?raw` so Vite returns the file content + * as a string. + * + * Scope: only default-binding imports (`import X from '...svg'`). Side-effect imports + * (`import '...svg'`) are intentionally left alone. + */ +export default function agSvgAsString({ packages }: Options): Plugin { + const escaped = packages.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + const SOURCE_PATTERN = new RegExp(`packages/(${escaped.join('|')})/src/`); + + // Match default-binding imports of plain `.svg` only. The negative lookahead + // (?!\?) explicitly excludes imports that already carry a Vite query suffix + // (`./x.svg?url`, `./x.svg?react`, etc.) so those pass through unchanged. + const PLAIN_SVG_IMPORT = /(from\s+['"][^'"]+\.svg)(?!\?)(['"])/g; + + return { + name: 'ag-svg-as-string', + enforce: 'pre', + transform(code, id) { + if (!SOURCE_PATTERN.test(id)) return null; + if (!PLAIN_SVG_IMPORT.test(code)) return null; + + // Reset lastIndex — RegExp objects with /g remember state across .test/.exec. + PLAIN_SVG_IMPORT.lastIndex = 0; + return code.replace(PLAIN_SVG_IMPORT, '$1?raw$2'); + }, + }; +} diff --git a/external/ag-website-shared/src/components/code/Code.tsx b/external/ag-website-shared/src/components/code/Code.tsx index 06cdc7c392d..bc268e85f9a 100644 --- a/external/ag-website-shared/src/components/code/Code.tsx +++ b/external/ag-website-shared/src/components/code/Code.tsx @@ -31,6 +31,7 @@ const GrammarMap = { diff: Prism.languages.diff, scss: Prism.languages.scss, xml: Prism.languages.xml, + plain: Prism.languages.plain, }; export type Language = keyof typeof GrammarMap; @@ -92,6 +93,7 @@ function Code({ return (
 : {code};
+    return keepMarkup ? (
+        
+    ) : (
+        
+            {code}
+        
+    );
 };
 
 /**
@@ -134,7 +142,10 @@ const CodeWithPrismPlugins = ({ code, keepMarkup }: { code: string; keepMarkup:
  * small part of the Prism lifecycle.
  */
 const CodeWithoutPrismPlugins = ({ code, language }: { code: string; language: Language }) => (
-    
+    
 );
 
 export default memo(Code);
diff --git a/external/ag-website-shared/src/components/codeSandbox/components/OpenInCodeSandbox.tsx b/external/ag-website-shared/src/components/codeSandbox/components/OpenInCodeSandbox.tsx
index 28d781edee8..7fa2b1c9689 100644
--- a/external/ag-website-shared/src/components/codeSandbox/components/OpenInCodeSandbox.tsx
+++ b/external/ag-website-shared/src/components/codeSandbox/components/OpenInCodeSandbox.tsx
@@ -13,7 +13,6 @@ interface Props {
     internalFramework: InternalFramework;
     files: FileContents;
     htmlUrl: string;
-    boilerPlateFiles?: FileContents;
     packageJson: Record;
     isDev: boolean;
 }
@@ -23,7 +22,6 @@ export const OpenInCodeSandbox: FunctionComponent = ({
     internalFramework,
     files,
     htmlUrl,
-    boilerPlateFiles,
     packageJson,
     isDev,
 }) => {
@@ -44,7 +42,6 @@ export const OpenInCodeSandbox: FunctionComponent = ({
                 openCodeSandbox({
                     title,
                     files: sandboxFiles,
-                    boilerPlateFiles,
                     internalFramework,
                 });
             }}
diff --git a/external/ag-website-shared/src/components/codeSandbox/utils/codeSandbox.ts b/external/ag-website-shared/src/components/codeSandbox/utils/codeSandbox.ts
index d16a6e7e64f..0c33a92ac2a 100644
--- a/external/ag-website-shared/src/components/codeSandbox/utils/codeSandbox.ts
+++ b/external/ag-website-shared/src/components/codeSandbox/utils/codeSandbox.ts
@@ -1,6 +1,6 @@
 import type { InternalFramework } from '@ag-grid-types';
 import type { FileContents } from '@components/example-generator/types';
-import { EXAMPLE_STYLE_FILE_NAME } from '@constants';
+import { DEBUG_SCRIPT_FILE_NAME, EXAMPLE_STYLE_FILE_NAME } from '@constants';
 import { isReactInternalFramework } from '@utils/framework';
 import { getParameters } from 'codesandbox-import-utils/lib/api/define';
 
@@ -27,7 +27,9 @@ const getPathForFile = ({
         return `public/index.html`;
     }
 
-    if (fileName === EXAMPLE_STYLE_FILE_NAME) {
+    if (fileName === DEBUG_SCRIPT_FILE_NAME) {
+        return `public/${DEBUG_SCRIPT_FILE_NAME}`;
+    } else if (fileName === EXAMPLE_STYLE_FILE_NAME) {
         return `public/${EXAMPLE_STYLE_FILE_NAME}`;
     }
 
@@ -59,11 +61,9 @@ const getCodeSandboxRuntime = (internalFramework: InternalFramework) => {
 
 const getCodeSandboxFiles = ({
     files,
-    boilerPlateFiles,
     internalFramework,
 }: {
     files: FileContents;
-    boilerPlateFiles: FileContents;
     internalFramework: InternalFramework;
 }) => {
     const sandboxFiles: SandboxFiles = {};
@@ -107,12 +107,10 @@ const createHiddenInputFactory =
 const getCodeSandboxFilesToSubmit = ({
     title,
     files,
-    boilerPlateFiles,
     internalFramework,
 }: {
     title: string;
     files: FileContents;
-    boilerPlateFiles: FileContents;
     internalFramework: InternalFramework;
 }) => {
     const runtime = getCodeSandboxRuntime(internalFramework);
@@ -126,7 +124,6 @@ const getCodeSandboxFilesToSubmit = ({
         ...configFiles,
         ...getCodeSandboxFiles({
             files,
-            boilerPlateFiles,
             internalFramework,
         }),
     };
@@ -143,12 +140,10 @@ const getCodeSandboxFilesToSubmit = ({
 export const openCodeSandbox = ({
     title,
     files,
-    boilerPlateFiles,
     internalFramework,
 }: {
     title: string;
     files: FileContents;
-    boilerPlateFiles: FileContents;
     internalFramework: InternalFramework;
 }) => {
     const form = document.createElement('form');
@@ -162,7 +157,6 @@ export const openCodeSandbox = ({
         files: getCodeSandboxFilesToSubmit({
             title,
             files,
-            boilerPlateFiles,
             internalFramework,
         }),
         template: getCodeSandboxRuntime(internalFramework),
diff --git a/external/ag-website-shared/src/components/docs-navigation/DocsNavFromLocalStorage.tsx b/external/ag-website-shared/src/components/docs-navigation/DocsNavFromLocalStorage.tsx
index 2fbce79535e..2042fa15f7e 100644
--- a/external/ag-website-shared/src/components/docs-navigation/DocsNavFromLocalStorage.tsx
+++ b/external/ag-website-shared/src/components/docs-navigation/DocsNavFromLocalStorage.tsx
@@ -6,7 +6,15 @@ import { useEffect, useState } from 'react';
 
 import { DocsNav } from './DocsNav';
 
-export function DocsNavFromLocalStorage({ menuData, pageName }: { menuData: any; pageName?: string }) {
+export function DocsNavFromLocalStorage({
+    menuData,
+    pageName,
+    showWhatsNew = true,
+}: {
+    menuData: any;
+    pageName?: string;
+    showWhatsNew?: boolean;
+}) {
     const internalFramework = useStore($internalFramework);
     const [framework, setFramework] = useState();
 
@@ -18,5 +26,5 @@ export function DocsNavFromLocalStorage({ menuData, pageName }: { menuData: any;
         }
     }, [internalFramework]);
 
-    return ;
+    return ;
 }
diff --git a/external/ag-website-shared/src/components/example-runner/components/ExampleLogger.module.scss b/external/ag-website-shared/src/components/example-runner/components/ExampleLogger.module.scss
index de0db329aa5..f37092de3ba 100644
--- a/external/ag-website-shared/src/components/example-runner/components/ExampleLogger.module.scss
+++ b/external/ag-website-shared/src/components/example-runner/components/ExampleLogger.module.scss
@@ -11,6 +11,10 @@
     border-radius: var(--radius-md);
     border: 1px solid var(--color-border-primary);
     overflow: hidden;
+
+    @media screen and (max-width: $breakpoint-docs-search-medium) {
+        display: none;
+    }
 }
 
 .loggerHeader {
diff --git a/external/ag-website-shared/src/components/example-runner/scripts/studio-debug.js b/external/ag-website-shared/src/components/example-runner/scripts/studio-debug.js
new file mode 100644
index 00000000000..4b399a6b67b
--- /dev/null
+++ b/external/ag-website-shared/src/components/example-runner/scripts/studio-debug.js
@@ -0,0 +1,203 @@
+// @ts-check
+/** @typedef {import('ag-charts-enterprise').AgChartOptions} AgChartOptions */
+
+const html = String;
+
+const indexHtml = html`
+    
+        
+            JavaScript Example - Quick Start - Basic Example
+            
+            
+            
+            
+            
+        
+        
+            
+ + + + + `; + +/** + * @param {string} key + * @returns {string} + */ +function jsonKey(key) { + return `%%${key}%%`; +} + +/** + * @param {AgChartOptions} options + * @returns {string} + */ +function getMainJs(options) { + const replacements = new Map([ + ['data', 'getData()'], + ['container', `document.getElementById("myChart")`], + ['context', 'null'], + ]); + + for (const key of replacements.keys()) { + options = { ...options, [key]: jsonKey(key) }; + } + let optionsJs = JSON.stringify(options, null, 4); + for (const [key, value] of replacements) { + optionsJs = optionsJs.replace(JSON.stringify(jsonKey(key)), value); + } + + // Remove quotes from keys that are valid identifiers + optionsJs = optionsJs.replace(/^(\s+)"(\w+)":/gm, '$1$2:'); + + return [`const { AgCharts } = agCharts;`, `const options = ${optionsJs};`, `AgCharts.create(options);`].join( + '\n\n' + ); +} + +/** + * @param {AgChartOptions} options + * @returns {string} + */ +function getDataJs(options) { + const dataJs = JSON.stringify(options.data, null, 4).replace(/^/gm, ' ').trim(); + + return [`function getData() {`, ` return ${dataJs};`, `}`].join('\n'); +} + +/** + * @param {any} chartWidget + */ +function exportToPlunker({ widget }) { + /** @type {AgChartOptions} */ + const options = widget.getOptions(); + + const form = document.createElement('form'); + form.method = 'post'; + form.style.display = 'none'; + form.action = `https://plnkr.co/edit/?preview&open=main.js`; + form.target = '_blank'; + + /** + * @param {string} name + * @param {any} value + */ + const addHiddenInput = (name, value) => { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + form.appendChild(input); + }; + + addHiddenInput('private', true); + addHiddenInput('files[index.html]', indexHtml.replace(/^\s{4}/gm, '')); + addHiddenInput('files[main.js]', getMainJs(options)); + addHiddenInput('files[data.js]', getDataJs(options)); + + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); +} + +const exportToPlunkerToolbarButton = { + type: 'button', + text: 'Export to Plunker', + icon: 'linked', + action: exportToPlunker, +}; + +const logStateButton = { + type: 'button', + text: 'Log State', + icon: 'eye', + action: ({ api }) => console.log(api.getState()), +}; + +/** @type {any} */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const debugOverrides = { + /** + * @param {any} widgetConfigs + * @return {any} widgetConfigs + */ + widgets: (widgetConfigs) => { + for (const [widgetId, widgetConfig] of Object.entries(widgetConfigs)) { + if (widgetId.includes('chart') && !widgetConfig.toolbar.includes(exportToPlunkerToolbarButton)) { + widgetConfig.toolbar.push(exportToPlunkerToolbarButton); + } + if (!widgetConfig.toolbar?.includes(logStateButton)) { + if (widgetConfig.toolbar == null) { + widgetConfig.toolbar = []; + } + widgetConfig.toolbar.push(logStateButton); + } + } + return widgetConfigs; + }, +}; + +// Propagate URL query params into window globals consumed by ag-studio. +// Runs before the example's main.js to ensure flags are visible at startup. +// Values are *appended* to any existing window.agStudioDebug / agStudioOpts. +// ?explain= → adds 'query:explain' to window.agStudioDebug, 'options' can include 'rows' and 'plain' to add 'query:explain:rows' and set window.agStudioOpts.explainFormat = 'plain', respectively +// ?batchLog=true → adds 'query:batch' to window.agStudioDebug +// ?sf= → sets window.agStudioOpts.scaleFactor (consumed by demoDataGenerator), 1.0 = ~4.5mil rows +// ?batching=false → sets window.agStudioOpts.queryBatching = false +(function () { + const urlParams = new URLSearchParams(window.location.search); + + const agStudioOpts = { ...(window.agStudioOpts ?? {}) }; + const agStudioDebug = Array.isArray(window.agStudioDebug) ? window.agStudioDebug.slice() : []; + + const explainParam = urlParams.get('explain'); + if (explainParam && !agStudioDebug.includes('query:explain')) { + agStudioDebug.push('query:explain'); + if (explainParam.includes('rows')) { + agStudioDebug.push('query:explain:rows'); + } + if (explainParam.includes('plain')) { + agStudioOpts.explainFormat = 'plain'; + } + } + if (urlParams.get('batchLog') === 'true' && !agStudioDebug.includes('query:batch')) { + agStudioDebug.push('query:batch'); + } + if (urlParams.get('tracing') === 'true') { + agStudioDebug.push('traceMarkers'); + } + + if (agStudioDebug.length > 0) { + window.agStudioDebug = agStudioDebug; + } + + const sfParam = urlParams.get('sf'); + if (sfParam != null) { + const sf = parseFloat(sfParam); + if (Number.isFinite(sf) && sf > 0) { + agStudioOpts.scaleFactor = sf; + } + } + const batchingParam = urlParams.get('batching'); + if (batchingParam != null) { + agStudioOpts.queryBatching = batchingParam !== 'false'; + } + window.agStudioOpts = agStudioOpts; +})(); diff --git a/external/ag-website-shared/src/components/example-runner/scripts/sw.js b/external/ag-website-shared/src/components/example-runner/scripts/sw.js new file mode 100644 index 00000000000..c1942a16e59 --- /dev/null +++ b/external/ag-website-shared/src/components/example-runner/scripts/sw.js @@ -0,0 +1,37 @@ +importScripts('https://cdn.jsdelivr.net/npm/typescript@5.4.5/lib/typescript.js'); + +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); + +async function transpile(request, ext) { + const response = await fetch(request); + if (!response.ok) return response; + + const source = await response.text(); + + const result = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ESNext, + jsx: ext.endsWith('x') ? ts.JsxEmit.React : undefined, + experimentalDecorators: ext === 'ts', + emitDecoratorMetadata: ext === 'ts', + }, + }); + + return new Response(result.outputText, { + headers: { 'Content-Type': 'application/javascript' }, + }); +} + +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + const ext = url.pathname + .match(/\.([a-z0-9]+)$/i) + ?.at(1) + ?.toLowerCase(); + + if (['jsx', 'ts', 'tsx'].includes(ext)) { + event.respondWith(transpile(event.request, ext)); + } +}); diff --git a/external/ag-website-shared/src/components/icon/Icon.tsx b/external/ag-website-shared/src/components/icon/Icon.tsx index cd9806306a4..ff09af928db 100644 --- a/external/ag-website-shared/src/components/icon/Icon.tsx +++ b/external/ag-website-shared/src/components/icon/Icon.tsx @@ -154,6 +154,7 @@ export const ICON_MAP = { terminal: CarbonIcon.Terminal, pricingFeatures: CarbonIcon.CicsProgram, support: CarbonIcon.Chat, + edit: CarbonIcon.Edit, ...SOCIALS_ICON_MAP, ...CHARTS_ICON_MAP, }; diff --git a/external/ag-website-shared/src/components/landing-pages/LandingPageSection.module.scss b/external/ag-website-shared/src/components/landing-pages/LandingPageSection.module.scss index 392fbb37ef1..f1a7aec6614 100644 --- a/external/ag-website-shared/src/components/landing-pages/LandingPageSection.module.scss +++ b/external/ag-website-shared/src/components/landing-pages/LandingPageSection.module.scss @@ -54,7 +54,6 @@ font-size: var(--text-fs-lg); font-weight: var(--text-semibold); color: var(--color-util-brand-600); - text-transform: capitalize; #{$selector-darkmode} & { color: var(--color-util-brand-900); @@ -65,7 +64,6 @@ font-size: 40px; font-weight: var(--text-bold); letter-spacing: -1px; - text-transform: capitalize; @media screen and (max-width: $breakpoint-landing-page-medium) { padding: 0; @@ -246,7 +244,6 @@ } .frameworkName { - text-transform: capitalize; font-weight: 600; } diff --git a/external/ag-website-shared/src/components/landing-pages/LandingPageSection.tsx b/external/ag-website-shared/src/components/landing-pages/LandingPageSection.tsx index fba4e4902fb..d419cec6bed 100644 --- a/external/ag-website-shared/src/components/landing-pages/LandingPageSection.tsx +++ b/external/ag-website-shared/src/components/landing-pages/LandingPageSection.tsx @@ -46,6 +46,7 @@ interface Props { showBackgroundGradient?: boolean; children: ReactNode; isFramework?: boolean; + maxWidth?: string; } const CTAWithFrameworks: FunctionComponent<{ ctaId: string; ctaTitle: string; ctaUrl: string }> = ({ @@ -173,6 +174,7 @@ export const LandingPageSection: FunctionComponent = ({ sectionClass, showBackgroundGradient, children, + maxWidth, }) => { return (
= ({ [styles.withBackgroundGradient]: showBackgroundGradient, })} > -
+

{tag}

{headingHtml ? ( diff --git a/external/ag-website-shared/src/components/major-table/MajorTable.astro b/external/ag-website-shared/src/components/major-table/MajorTable.astro index 2d679505d7b..e0da71e42ee 100644 --- a/external/ag-website-shared/src/components/major-table/MajorTable.astro +++ b/external/ag-website-shared/src/components/major-table/MajorTable.astro @@ -13,13 +13,14 @@ interface Props { library: Library; major: Number; type: 'migration' | 'archive'; + suppressChangelog?: boolean; } interface Params { framework?: Framework; } -const { library, major, type } = Astro.props as Props; +const { library, major, type, suppressChangelog } = Astro.props as Props; const isMigration = type === 'migration'; @@ -72,12 +73,14 @@ const versions = isMigration ? allVersions.filter(filterMigrations) : allVersion {!isMigration && ( <> - - - Changelog - - - + {!suppressChangelog && ( + + + Changelog + + + + )} diff --git a/external/ag-website-shared/src/components/plunkr/components/OpenInPlunkr.tsx b/external/ag-website-shared/src/components/plunkr/components/OpenInPlunkr.tsx index 5e6229acf1c..1e27c6670c7 100644 --- a/external/ag-website-shared/src/components/plunkr/components/OpenInPlunkr.tsx +++ b/external/ag-website-shared/src/components/plunkr/components/OpenInPlunkr.tsx @@ -11,21 +11,12 @@ interface Props { title: string; files: FileContents; htmlUrl: string; - boilerPlateFiles?: FileContents; packageJson: Record; fileToOpen: string; isDev: boolean; } -export const OpenInPlunkr: FunctionComponent = ({ - title, - files, - htmlUrl, - boilerPlateFiles, - packageJson, - fileToOpen, - isDev, -}) => { +export const OpenInPlunkr: FunctionComponent = ({ title, files, htmlUrl, packageJson, fileToOpen, isDev }) => { return ( = ({ stripOutExampleGeneratorCode(localFiles); const plunkrExampleFiles = { ...localFiles, - ...boilerPlateFiles, 'package.json': JSON.stringify(packageJson, null, 2), 'index.html': indexHtml, }; diff --git a/external/ag-website-shared/src/components/product-dropdown/ProductDropdown.tsx b/external/ag-website-shared/src/components/product-dropdown/ProductDropdown.tsx index 84e80e35b6f..6e2a2be03ee 100644 --- a/external/ag-website-shared/src/components/product-dropdown/ProductDropdown.tsx +++ b/external/ag-website-shared/src/components/product-dropdown/ProductDropdown.tsx @@ -10,7 +10,7 @@ import styles from './ProductDropdown.module.scss'; export const ProductDropdown = ({ items, children }) => { const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); + const dropdownRef = useRef(null); const handleMenuToggle = () => { setIsOpen(!isOpen); diff --git a/external/ag-website-shared/src/components/reference-documentation/ApiReference.module.scss b/external/ag-website-shared/src/components/reference-documentation/ApiReference.module.scss index 9f5909ebaa8..90b9210528d 100644 --- a/external/ag-website-shared/src/components/reference-documentation/ApiReference.module.scss +++ b/external/ag-website-shared/src/components/reference-documentation/ApiReference.module.scss @@ -267,6 +267,7 @@ padding-left: 8px; padding-right: 8px; border-radius: var(--radius-sm); + word-break: break-word; #{$selector-darkmode} & { background-color: color-mix(in srgb, var(--color-fg-code), var(--color-bg-primary) 90%); diff --git a/external/ag-website-shared/src/components/site-header/SiteHeader.module.scss b/external/ag-website-shared/src/components/site-header/SiteHeader.module.scss index 8e5e0ac78df..a5f50bfe2f1 100644 --- a/external/ag-website-shared/src/components/site-header/SiteHeader.module.scss +++ b/external/ag-website-shared/src/components/site-header/SiteHeader.module.scss @@ -10,7 +10,8 @@ @media screen and (min-width: $breakpoint-docs-nav-medium) { position: sticky; top: 0; - z-index: 10001; // needed in order to prevent grid z-indexes overlapping on small height + width: 100%; + z-index: 10002; // needed in order to prevent grid z-indexes overlapping on small height } #{$selector-darkmode}:has([data-is-homepage='false']) &, diff --git a/external/ag-website-shared/src/components/trial-licence-form/TrialLicenceFormStudio.tsx b/external/ag-website-shared/src/components/trial-licence-form/TrialLicenceFormStudio.tsx new file mode 100644 index 00000000000..fb506d56f35 --- /dev/null +++ b/external/ag-website-shared/src/components/trial-licence-form/TrialLicenceFormStudio.tsx @@ -0,0 +1,319 @@ +import { Icon } from '@ag-website-shared/components/icon/Icon'; +import { PRIVACY_POLICY_URL } from '@ag-website-shared/constants'; +import { TRIAL_LICENCE_FORM_URL, ZI_FORM_ID } from '@constants'; +import { trackTrialLicenseFormError, trackTrialLicenseFormSuccess } from '@utils/analytics'; +import classnames from 'classnames'; +import { useCallback, useState } from 'react'; +import type { ChangeEventHandler, FormEventHandler, FunctionComponent } from 'react'; + +import { MESSAGES } from './Messages'; +import styles from './TrialLicenceForm.module.scss'; + +interface Props { + submitUrl?: string; +} + +type TrialFormState = 'success' | 'error' | 'loading' | 'idle'; + +const getFormErrorMessage = (message: string) => { + let errorMessage = MESSAGES.formErrorDefault; + + if (message === 'invalid arguments provided') { + errorMessage = MESSAGES.formErrorInvalidArguments; + } else if (message.includes('INVALID_EMAIL_ADDRESS')) { + // eg, "Error: Unable to create a lead for a trial LK for email @bc.com. Error: Insert failed. First exception on row 0; first error: INVALID_EMAIL_ADDRESS, Email: invalid email address: @bc.com: [Email]" + errorMessage = MESSAGES.formErrorInvalidEmail; + } else if (message.includes('Duplicate email')) { + // eg, "Error: Unable to create a lead for a trial LK for email something@somewhere.com. Error: Duplicate email" + errorMessage = MESSAGES.formErrorDuplicateEmail; + } + + return errorMessage; +}; + +const isEmailValid = (email: string) => { + const emailPattern = /^([a-zA-Z0-9._-]|\+)+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/; + return emailPattern.test(email); +}; + +const validateEmail = (email: string) => { + let validation = ''; + + if (!email) { + validation = MESSAGES.validationEmailRequired; + } else if (!isEmailValid(email)) { + validation = MESSAGES.validationEmailInvalid; + } + + return validation; +}; + +const validateRequired = (value: string) => { + let validation = ''; + if (value === '') { + validation = MESSAGES.validationRequiredField; + } + + return validation; +}; + +function useEmailValidation(initialValue: string = '') { + const [email, setEmail] = useState(initialValue); + const [emailError, setEmailError] = useState(validateEmail(initialValue)); + + const handleEmailChange: ChangeEventHandler = useCallback((e) => { + const value = e.target?.value; + setEmail(value); + setEmailError(validateEmail(value)); + }, []); + + return { + emailError, + email, + handleEmailChange, + }; +} + +function useRequiredValidation(initialValue: string = '') { + const [value, setValue] = useState(initialValue); + const [valueError, setValueError] = useState(validateRequired(initialValue)); + + const handleValueChange: ChangeEventHandler = useCallback((e) => { + const value = e.target?.value; + setValue(value); + setValueError(validateRequired(value)); + }, []); + + return { + valueError, + value, + handleValueChange, + }; +} + +async function submitTrialLicenceFormData({ + submitUrl = TRIAL_LICENCE_FORM_URL, + firstName, + lastName, + email, + company, +}: { + submitUrl?: string; + firstName: string; + lastName: string; + email: string; + company: string; +}) { + const response = await fetch(submitUrl, { + method: 'POST', + body: JSON.stringify({ data: { firstName, lastName, email, company } }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const json = await response.json(); + + return json; +} + +function useTrialForm({ submitUrl }: Props) { + const [formState, setFormState] = useState('idle'); + const [formError, setFormError] = useState(''); + const [wasValidated, setWasValidated] = useState(false); + const { emailError: validatedEmailError, email, handleEmailChange } = useEmailValidation(); + const emailError = wasValidated && validatedEmailError ? validatedEmailError : ''; + + const { + value: firstName, + valueError: validatedFirstNameError, + handleValueChange: handleFirstNameChange, + } = useRequiredValidation(); + const firstNameError = wasValidated && validatedFirstNameError ? validatedFirstNameError : ''; + + const { + value: lastName, + valueError: validatedLastNameError, + handleValueChange: handleLastNameChange, + } = useRequiredValidation(); + const lastNameError = wasValidated && validatedLastNameError ? validatedLastNameError : ''; + + const handleFormSubmit: FormEventHandler = useCallback( + async (e) => { + e.preventDefault(); + setWasValidated(true); + + if (validatedEmailError || validatedFirstNameError || validatedLastNameError) { + setFormState('error'); + return; + } + + setFormError(''); + setFormState('loading'); + + const currentPage = window.location.pathname; + + try { + const company = (document.getElementById('company') as HTMLInputElement)?.value || ''; + const response = await submitTrialLicenceFormData({ submitUrl, firstName, lastName, email, company }); + + if (response.error) { + setFormState('error'); + const errorMessage = getFormErrorMessage(response.error.message); + setFormError(errorMessage); + trackTrialLicenseFormError({ + error: response.error.message, + errorType: 'api_error', + page: currentPage, + }); + } else { + setFormState('success'); + trackTrialLicenseFormSuccess({ + page: currentPage, + }); + } + } catch (e) { + console.error(e); + const errorMessage = MESSAGES.formErrorDefault; + setFormError(errorMessage); + trackTrialLicenseFormError({ + error: e instanceof Error ? e.message : 'Unknown error', + errorType: 'system_error', + page: currentPage, + }); + setFormState('error'); + } + }, + [validatedEmailError, validatedFirstNameError, validatedLastNameError, firstName, lastName, email] + ); + + return { + formState, + formError, + emailError, + email, + handleEmailChange, + firstName, + firstNameError, + handleFirstNameChange, + lastName, + lastNameError, + handleLastNameChange, + handleFormSubmit, + }; +} + +export const TrialLicenceFormStudio: FunctionComponent = ({ submitUrl }: Props) => { + const { + formState, + formError, + emailError, + email, + handleEmailChange, + firstName, + firstNameError, + handleFirstNameChange, + lastName, + lastNameError, + handleLastNameChange, + handleFormSubmit, + } = useTrialForm({ submitUrl }); + const hasFormError = Boolean(emailError || firstNameError || lastNameError); + + return ( +
+
+ + +
+ + + +

+ First name required +

+
+ +
+ + + +

Last name required

+
+
+ +
+ + + + + +

+ {emailError ? emailError : 'Email required'} +

+
+ +
+ + +

+ By submitting this form you agree to our Privacy Policy. +

+ + {formState === 'success' && ( +

+ + + Thank you. Please check your inbox to validate your email and receive your AG Studio{' '} + trial licence. + +

+ )} + + {formError && ( +

+ + {formError} +

+ )} +
+
+ ); +}; diff --git a/external/ag-website-shared/src/content/license-features/chartsFeaturesMatrix.json b/external/ag-website-shared/src/content/license-features/chartsFeaturesMatrix.json index fd5de22b743..97829fb49f6 100644 --- a/external/ag-website-shared/src/content/license-features/chartsFeaturesMatrix.json +++ b/external/ag-website-shared/src/content/license-features/chartsFeaturesMatrix.json @@ -369,6 +369,22 @@ } ] }, + { + "group": { + "name": "Server-Side Rendering" + }, + "items": [ + { + "label": { + "name": "Server-Side Rendering", + "link": "https://www.ag-grid.com/charts/r/server-side-rendering/" + }, + "community": false, + "enterprise": true, + "chartsGrid": true + } + ] + }, { "group": { "name": "Data" @@ -550,6 +566,15 @@ "enterprise": true, "chartsGrid": true }, + { + "label": { + "name": "Flash On Update", + "link": "https://www.ag-grid.com/charts/r/flash-on-update/" + }, + "community": false, + "enterprise": true, + "chartsGrid": true + }, { "label": { "name": "Navigator", @@ -559,6 +584,15 @@ "enterprise": true, "chartsGrid": true }, + { + "label": { + "name": "Range Buttons", + "link": "https://www.ag-grid.com/charts/r/range-controls/" + }, + "community": false, + "enterprise": true, + "chartsGrid": true + }, { "label": { "name": "Scrollbar", diff --git a/external/ag-website-shared/src/design-system/core/_breakpoints.scss b/external/ag-website-shared/src/design-system/core/_breakpoints.scss index c9ef5dbeff7..d377a7e1fc4 100644 --- a/external/ag-website-shared/src/design-system/core/_breakpoints.scss +++ b/external/ag-website-shared/src/design-system/core/_breakpoints.scss @@ -49,6 +49,8 @@ $pricing-small: 620px; $pricing-medium: 820px; $pricing-large: 1260px; +$studio-pricing-large: 1040px; + $changelog-pipeline-large: 980px; $policy-page-extra-large: 1400px; diff --git a/external/ag-website-shared/src/images/inline-svgs/ag-charts-logomark.svg b/external/ag-website-shared/src/images/inline-svgs/ag-charts-logomark.svg new file mode 100644 index 00000000000..3f7d67956f5 --- /dev/null +++ b/external/ag-website-shared/src/images/inline-svgs/ag-charts-logomark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/external/ag-website-shared/src/utils/getArchiveUrl.ts b/external/ag-website-shared/src/utils/getArchiveUrl.ts index 8b723e7e591..8880ee41297 100644 --- a/external/ag-website-shared/src/utils/getArchiveUrl.ts +++ b/external/ag-website-shared/src/utils/getArchiveUrl.ts @@ -1,7 +1,7 @@ import type { Library } from '@ag-grid-types'; import { parseVersion } from '@ag-website-shared/utils/parseVersion'; import { versionIsGreaterOrEqual } from '@ag-website-shared/utils/versionIsGreaterOrEqual'; -import { LEGACY_CHARTS_SITE_URL, PRODUCTION_CHARTS_SITE_URL } from '@constants'; +import { LEGACY_CHARTS_SITE_URL, PRODUCTION_CHARTS_SITE_URL, PRODUCTION_STUDIO_SITE_URL } from '@constants'; import { pathJoin } from '@utils/pathJoin'; const FIRST_GRID_VERSION_WITH_HOMEPAGE = '27.3.0'; @@ -20,6 +20,8 @@ export const getArchiveUrl = ({ version, site }: { version: string; site: Librar let baseUrl = 'https://www.ag-grid.com'; if (site === 'charts') { baseUrl = (major === 10 && minor >= 1) || major > 10 ? PRODUCTION_CHARTS_SITE_URL : LEGACY_CHARTS_SITE_URL; + } else if (site === 'studio') { + baseUrl = PRODUCTION_STUDIO_SITE_URL; } return pathJoin(baseUrl, archiveBaseUrl, version); diff --git a/external/ag-website-shared/src/utils/getChangelogUrl.ts b/external/ag-website-shared/src/utils/getChangelogUrl.ts index 6bdab315ed3..a545986db7b 100644 --- a/external/ag-website-shared/src/utils/getChangelogUrl.ts +++ b/external/ag-website-shared/src/utils/getChangelogUrl.ts @@ -1,5 +1,5 @@ import type { Library } from '@ag-grid-types'; -import { PRODUCTION_CHARTS_SITE_URL, PRODUCTION_GRID_SITE_URL } from '@constants'; +import { PRODUCTION_CHARTS_SITE_URL, PRODUCTION_GRID_SITE_URL, PRODUCTION_STUDIO_SITE_URL } from '@constants'; import { pathJoin } from '@utils/pathJoin'; interface Params { @@ -10,7 +10,14 @@ interface Params { export function getChangelogUrl({ site, version }: Params) { const changelogBaseUrl = `/changelog/?fixVersion=${version}`; - const baseUrl = site === 'charts' ? PRODUCTION_CHARTS_SITE_URL : PRODUCTION_GRID_SITE_URL; + let baseUrl: string; + if (site === 'charts') { + baseUrl = PRODUCTION_CHARTS_SITE_URL; + } else if (site === 'studio') { + baseUrl = PRODUCTION_STUDIO_SITE_URL; + } else { + baseUrl = PRODUCTION_GRID_SITE_URL; + } return pathJoin(baseUrl, changelogBaseUrl); } diff --git a/packages/ag-grid-community/src/api/gridApi.ts b/packages/ag-grid-community/src/api/gridApi.ts index f92d2f705b6..12607046076 100644 --- a/packages/ag-grid-community/src/api/gridApi.ts +++ b/packages/ag-grid-community/src/api/gridApi.ts @@ -1436,8 +1436,7 @@ export interface _SideBarGridApi { /** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */ export interface _ToolbarGridApi { /** - * Gets the toolbar item instance for the given `key`. The key is either explicitly set in the item - * definition or derived from the item type when not specified. + * Gets the toolbar item instance for the given `key`. Only toolbar items configured with a `key` can be accessed. * @agModule `ToolbarModule` */ getToolbarItemInstance>(key: string): T | undefined; diff --git a/packages/ag-grid-community/src/interfaces/iToolbar.ts b/packages/ag-grid-community/src/interfaces/iToolbar.ts index 0e3dcdcd398..43aac8f64a3 100644 --- a/packages/ag-grid-community/src/interfaces/iToolbar.ts +++ b/packages/ag-grid-community/src/interfaces/iToolbar.ts @@ -39,7 +39,11 @@ export interface ToolbarItemActionParams extends Ag /** Properties common to every toolbar item definition variant. */ interface ToolbarItemDefBase { - /** Unique identifier for the item. Auto-generated if omitted. */ + /** + * Unique identifier used to look up this item via `api.getToolbarItemInstance(key)`. + * Optional — items without a key still render. Set a key only on items you want to + * access via the API. + */ key?: string; /** Alignment within the toolbar. Falls back to the toolbar-level `alignment`. */ alignment?: 'left' | 'right'; @@ -143,7 +147,11 @@ export type ToolbarItemDef extends AgGridCommon { - /** Unique identifier for the item. Auto-generated by the grid if the user did not set one. */ + /** + * Identifier for the item. Mirrors the `key` set on the item definition, or an + * auto-generated key when none was provided. Used internally; only items with an + * explicit key on the definition are reachable via `api.getToolbarItemInstance(key)`. + */ key: string; /** Explicit alignment, when set on the item definition. */ alignment?: 'left' | 'right'; diff --git a/packages/ag-grid-community/src/rendering/row/rowCtrl.ts b/packages/ag-grid-community/src/rendering/row/rowCtrl.ts index 14095a149e2..856156fb28a 100644 --- a/packages/ag-grid-community/src/rendering/row/rowCtrl.ts +++ b/packages/ag-grid-community/src/rendering/row/rowCtrl.ts @@ -194,6 +194,14 @@ export class RowCtrl extends BeanStub { this.fullWidthNotesFeature = this.isFullWidth() ? beans.notesSvc?.createFullWidthNotesFeature(this) : undefined; this.addListeners(); + + // Pre-create CellCtrls so framework wrappers can seed their first render and + // avoid the bulk-add empty-row flash. + // The animation-frame path is left to handle deferred creation. + // We are disabling this also during scroll to not cause sluggishness. + if (!useAnimationFrameForCreate && !this.isFullWidth()) { + this.createAllCellCtrls(); + } } private initRowBusinessKey(): void { @@ -657,7 +665,7 @@ export class RowCtrl extends BeanStub { } } - private getCellCtrlsForContainer(containerType: RowContainerType) { + private getCellCtrlsForContainer(containerType: RowContainerType): CellCtrl[] { switch (containerType) { case 'left': return this.leftCellCtrls.list; @@ -689,6 +697,20 @@ export class RowCtrl extends BeanStub { } } + /** + * CellCtrls for the container if eagerly created in the constructor, or `null` + * when creation is deferred (animation-frame path) or skipped (full-width rows). + * We don't want to enable this during scrolling as well, as it can cause sluggishness + * during scroll. + * Used by framework wrappers to seed first render and avoid bulk-add flicker. + */ + public getInitialCellCtrls(containerType: RowContainerType): CellCtrl[] | null { + if (this.useAnimationFrameForCreate || this.isFullWidth()) { + return null; + } + return this.getCellCtrlsForContainer(containerType); + } + private isCellEligibleToBeRemoved(cellCtrl: CellCtrl, nextContainerPinned: ColumnPinnedType): boolean { const REMOVE_CELL = true; const KEEP_CELL = false; diff --git a/packages/ag-grid-community/src/rendering/rowRenderer.ts b/packages/ag-grid-community/src/rendering/rowRenderer.ts index c6d7520315a..b3135563c03 100644 --- a/packages/ag-grid-community/src/rendering/rowRenderer.ts +++ b/packages/ag-grid-community/src/rendering/rowRenderer.ts @@ -649,7 +649,9 @@ export class RowRenderer extends BeanStub implements NamedBean { // however we can reuse the rows, so we keep them but index by rowNode.id const rowsToRecycle = recycleRows ? this.getRowsToRecycle() : null; if (!recycleRows) { - this.removeAllRowComps(); + // Forward !animate so the destroy path skips the fade-out when the + // redraw itself is non-animating; otherwise rapid replaces flicker via opacity:0. + this.removeAllRowComps(!animate); } this.workOutFirstAndLastRowsToRender(); @@ -1007,7 +1009,7 @@ export class RowRenderer extends BeanStub implements NamedBean { super.destroy(); } - private removeAllRowComps(suppressAnimation: boolean = false): void { + private removeAllRowComps(suppressAnimation: boolean): void { const rowIndexesToRemove = Object.keys(this.rowCtrlsByRowIndex); this.removeRowCtrls(rowIndexesToRemove, suppressAnimation); diff --git a/packages/ag-grid-community/src/validation/errorMessages/errorText.ts b/packages/ag-grid-community/src/validation/errorMessages/errorText.ts index d6b14abefa2..19d566245d9 100644 --- a/packages/ag-grid-community/src/validation/errorMessages/errorText.ts +++ b/packages/ag-grid-community/src/validation/errorMessages/errorText.ts @@ -797,6 +797,8 @@ export const AG_GRID_ERRORS = { rowModelType, additionalText: 'The item will not be rendered.', }), + 303: ({ key }: { key: string }) => + `Multiple toolbar items share the explicit key '${key}'. Only the first item is rendered.` as const, }; export type ErrorMap = typeof AG_GRID_ERRORS; diff --git a/packages/ag-grid-enterprise/src/pivot/pivotStage.ts b/packages/ag-grid-enterprise/src/pivot/pivotStage.ts index 81848c593ef..9898f6a1827 100644 --- a/packages/ag-grid-enterprise/src/pivot/pivotStage.ts +++ b/packages/ag-grid-enterprise/src/pivot/pivotStage.ts @@ -145,8 +145,7 @@ export class PivotStage extends BeanStub implements NamedBean, _IRowNodePivotSta const aggregationColumnIds = aggregationColumns.map((column) => column.getId()); const lastIds = this.aggregationColumnIdsLastTime; const aggregationColumnsReordered = - lastIds != null && - lastIds.length === aggregationColumnIds.length && + lastIds?.length === aggregationColumnIds.length && !_areEqual(lastIds, aggregationColumnIds) && lastIds.every((id) => aggregationColumnIds.includes(id)); this.aggregationColumnIdsLastTime = aggregationColumnIds; diff --git a/packages/ag-grid-enterprise/src/toolbar/agToolbar.css b/packages/ag-grid-enterprise/src/toolbar/agToolbar.css index 783aac88cdf..7eba60759ab 100644 --- a/packages/ag-grid-enterprise/src/toolbar/agToolbar.css +++ b/packages/ag-grid-enterprise/src/toolbar/agToolbar.css @@ -150,6 +150,9 @@ background-color: transparent; border-bottom: none; padding: 0; + font-weight: var(--ag-cell-font-weight); + font-size: var(--ag-cell-font-size); + font-family: var(--ag-cell-font-family); } /* stylelint-disable-next-line selector-max-specificity */ diff --git a/packages/ag-grid-enterprise/src/toolbar/agToolbar.ts b/packages/ag-grid-enterprise/src/toolbar/agToolbar.ts index 7e3ab48dd9b..a2ba88a2d36 100644 --- a/packages/ag-grid-enterprise/src/toolbar/agToolbar.ts +++ b/packages/ag-grid-enterprise/src/toolbar/agToolbar.ts @@ -26,6 +26,7 @@ import { _findFocusableElements, _getActiveDomElement, _removeFromParent, + _warn, } from 'ag-grid-community'; import agToolbarCSS from './agToolbar.css'; @@ -68,9 +69,16 @@ function normaliseItem(item: ToolbarItemDef | string, nextKey: () => string): No ({ label, tooltip, icon } = item as ToolbarMenuBuiltInItemDef); } - const key = item.key ?? (typeof toolbarItem === 'string' ? toolbarItem : nextKey()); - - return { toolbarItem, toolbarItemParams, alignment: item.alignment, key, label, tooltip, icon, action }; + return { + toolbarItem, + toolbarItemParams, + alignment: item.alignment, + key: item.key ?? nextKey(), + label, + tooltip, + icon, + action, + }; } const ToolbarItemComponent: ComponentType = { @@ -86,7 +94,7 @@ const AgToolbarElement: ElementParams = { class AgToolbar extends Component implements FocusableContainer, IToolbarComp { private readonly toolbarItems: Map = new Map(); - private customKeyCounter: number = 0; + private nextKey: number = 0; // Incremented on each rebuild so stale async resolves from a previous generation can be discarded private generation: number = 0; @@ -189,22 +197,10 @@ class AgToolbar extends Component implements FocusableContainer, IToolbarComp { if (!toolbar?.items) { return undefined; } - // Reset counter so keyless custom items get stable positional keys across updates - this.customKeyCounter = 0; - const nextKey = () => `custom-toolbar-item-${this.customKeyCounter++}`; - const seen = new Set(); - return toolbar.items.reduce((acc, item) => { - const normalised = normaliseItem(item, nextKey); - if (normalised.toolbarItem === 'separator') { - acc.push(normalised); - return acc; - } - if (!seen.has(normalised.key)) { - seen.add(normalised.key); - acc.push(normalised); - } - return acc; - }, []); + // Reset counter so keyless items get stable positional keys across updates + this.nextKey = 0; + const nextKey = () => `toolbar-item-${this.nextKey++}`; + return toolbar.items.map((item) => normaliseItem(item, nextKey)); } private createItemParams(itemConfig: NormalisedToolbarItem, key: string): IToolbarItemParams { @@ -226,12 +222,12 @@ class AgToolbar extends Component implements FocusableContainer, IToolbarComp { const rightItems: NormalisedToolbarItem[] = []; // Alignment is semantic, not physical — flex mirrors layout in RTL automatically, so // the default is always 'left' regardless of direction. - const defaultAlignment: 'left' | 'right' = toolbar?.alignment ?? 'left'; + const defaultAlignment = toolbar?.alignment ?? 'left'; // Separators inherit the alignment of the preceding item, unless explicitly set - let lastAlignment: 'left' | 'right' = defaultAlignment; + let lastAlignment = defaultAlignment; for (const item of items) { const isSeparator = item.toolbarItem === 'separator'; - const alignment: 'left' | 'right' = item.alignment ?? (isSeparator ? lastAlignment : defaultAlignment); + const alignment = item.alignment ?? (isSeparator ? lastAlignment : defaultAlignment); (alignment === 'right' ? rightItems : leftItems).push(item); if (!isSeparator) { lastAlignment = alignment; @@ -343,9 +339,13 @@ class AgToolbar extends Component implements FocusableContainer, IToolbarComp { // Placeholder was discarded by a rebuild or destroy — clean up the orphan component. // Don't rely on isConnected: on initial render the grid is not yet in the document. - if (!this.isAlive() || placeholder.parentNode !== this.getGui()) { + const isDuplicate = this.toolbarItems.has(key); + if (isDuplicate || !this.isAlive() || placeholder.parentNode !== this.getGui()) { _removeFromParent(placeholder); this.destroyBean(component); + if (isDuplicate) { + _warn(303, { key }); + } return; } diff --git a/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css b/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css index f2a30677509..40b30cde748 100644 --- a/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css +++ b/packages/ag-grid-enterprise/src/widgets/pillDropZonePanel.css @@ -17,6 +17,7 @@ align-items: center; background-color: var(--ag-column-drop-cell-background-color); color: var(--ag-column-drop-cell-text-color); + font-weight: normal; border-radius: 500px; padding: calc(var(--ag-spacing) * 0.25); padding-left: calc(var(--ag-spacing) * 0.75); diff --git a/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx b/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx index 206b9733100..0436afd48b2 100644 --- a/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx +++ b/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx @@ -37,8 +37,12 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: const [rowBusinessKey, setRowBusinessKey] = useState(() => rowCtrl.businessKey); const [userStyles, setUserStyles] = useState(() => rowCtrl.rowStyles); - const cellCtrlsRef = useRef(null); - const [cellCtrlsFlushSync, setCellCtrlsFlushSync] = useState(() => null); + // Seeded so bulk-add doesn't flash empty rows; getInitialCellCtrls returns + // null when creation is deferred or not applicable. + const [cellCtrlsFlushSync, setCellCtrlsFlushSync] = useState(() => + rowCtrl.getInitialCellCtrls(containerType) + ); + const cellCtrlsRef = useRef(cellCtrlsFlushSync); const [fullWidthCompDetails, setFullWidthCompDetails] = useState(); // these styles have initial values, so element is placed into the DOM with them, diff --git a/testing/behavioural/src/row-data/bulk-add-cell-flicker-react.test.tsx b/testing/behavioural/src/row-data/bulk-add-cell-flicker-react.test.tsx new file mode 100644 index 00000000000..786ffb29477 --- /dev/null +++ b/testing/behavioural/src/row-data/bulk-add-cell-flicker-react.test.tsx @@ -0,0 +1,294 @@ +import { act, cleanup, render, waitFor } from '@testing-library/react'; +import React from 'react'; + +import type { ColDef, GridApi } from 'ag-grid-community'; +import { + ClientSideRowModelApiModule, + ClientSideRowModelModule, + ColumnApiModule, + ModuleRegistry, + RowApiModule, +} from 'ag-grid-community'; +import { AgGridReact } from 'ag-grid-react'; + +import { asyncSetTimeout, ignoreConsoleLicenseKeyError } from '../test-utils'; + +const ROW_SELECTOR = '[row-id]'; + +/** + * Detection strategy: a MutationObserver records mutations where an element is + * appended INTO an already-attached row. Pre-fix this fires for every newly-added + * row (RowComp mounts empty, fills cells on a later commit); post-fix RowCtrl + * pre-creates the cells so the row's first commit already contains them. + */ +describe('Eager row content seed (bulk-add flicker regression)', () => { + beforeAll(() => { + ModuleRegistry.registerModules([ + ClientSideRowModelModule, + ClientSideRowModelApiModule, + RowApiModule, + ColumnApiModule, + ]); + ignoreConsoleLicenseKeyError(); + }); + + afterEach(() => { + cleanup(); + }); + + type FlickerRecord = { rowId: string | null; addedElements: number }; + + /** Records every mutation where a child element is appended into an already-attached row. */ + async function recordContentAppendedIntoExistingRows( + root: HTMLElement, + mutate: () => Promise | void + ): Promise { + const records: FlickerRecord[] = []; + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== 'childList' || mutation.addedNodes.length === 0) { + continue; + } + const target = mutation.target; + if (!(target instanceof HTMLElement) || !target.hasAttribute('row-id')) { + continue; + } + const addedElements = Array.from(mutation.addedNodes).filter( + (n): n is HTMLElement => n instanceof HTMLElement + ).length; + if (addedElements > 0) { + records.push({ rowId: target.getAttribute('row-id'), addedElements }); + } + } + }); + observer.observe(root, { childList: true, subtree: true }); + try { + await mutate(); + await asyncSetTimeout(0); // drain MutationObserver microtask queue + } finally { + observer.disconnect(); + } + return records; + } + + function expectNoFlicker(records: FlickerRecord[]) { + if (records.length === 0) { + return; + } + const total = records.reduce((acc, r) => acc + r.addedElements, 0); + throw new Error( + `Expected new rows to be inserted with content already attached, but ${total} element(s) were ` + + `appended into ${records.length} already-attached row(s). First: ${JSON.stringify(records.slice(0, 5))}` + ); + } + + test('center-only columns: bulk add inserts rows with cells attached', async () => { + const columnDefs: ColDef[] = [{ field: 'a' }, { field: 'b' }, { field: 'c' }]; + const initialRowData = Array.from({ length: 5 }, (_, i) => ({ a: `a${i}`, b: `b${i}`, c: `c${i}` })); + + let gridApi: GridApi | undefined; + const rendered = render( +
+ { + gridApi = p.api; + }} + /> +
+ ); + + await waitFor(() => expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan(0)); + + const records = await recordContentAppendedIntoExistingRows(rendered.container, async () => { + const additions = Array.from({ length: 20 }, (_, i) => ({ a: `na${i}`, b: `nb${i}`, c: `nc${i}` })); + act(() => { + gridApi!.applyTransaction({ add: additions }); + }); + await waitFor(() => + expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan(initialRowData.length) + ); + }); + + expectNoFlicker(records); + }); + + test('pinned columns: left and right containers also receive pre-created cells', async () => { + const columnDefs: ColDef[] = [ + { field: 'l1', pinned: 'left' }, + { field: 'l2', pinned: 'left' }, + { field: 'c1' }, + { field: 'c2' }, + { field: 'r1', pinned: 'right' }, + ]; + const makeRow = (i: number) => ({ + l1: `l1-${i}`, + l2: `l2-${i}`, + c1: `c1-${i}`, + c2: `c2-${i}`, + r1: `r1-${i}`, + }); + const initialRowData = Array.from({ length: 3 }, (_, i) => makeRow(i)); + + let gridApi: GridApi | undefined; + const rendered = render( +
+ { + gridApi = p.api; + }} + /> +
+ ); + + await waitFor(() => expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan(0)); + + // Each row appears 3 times in the DOM (left/center/right containers). + const records = await recordContentAppendedIntoExistingRows(rendered.container, async () => { + const additions = Array.from({ length: 20 }, (_, i) => makeRow(100 + i)); + act(() => { + gridApi!.applyTransaction({ add: additions }); + }); + await waitFor(() => + expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan( + initialRowData.length * 3 + ) + ); + }); + + expectNoFlicker(records); + }); + + // Mirrors the staging demo (`updating-row-data-without-row-ids`): React-state-driven + // rowData with no getRowId. Each rerender replaces the rowData prop, every viewport + // RowCtrl is destroyed and a fresh one created. + type RowDatum = { a: string; b: string; c: string }; + let driveRowData: React.Dispatch> | undefined; + const initialData = (): RowDatum[] => Array.from({ length: 8 }, (_, i) => ({ a: `a${i}`, b: `b${i}`, c: `c${i}` })); + const cycleData = (cycle: number): RowDatum[] => + Array.from({ length: 8 }, (_, i) => ({ a: `c${cycle}-${i}`, b: `c${cycle}-${i}`, c: `c${cycle}-${i}` })); + + function StreamingWrapper({ renderingMode }: { renderingMode: 'default' | 'legacy' }) { + const [data, setData] = React.useState(initialData); + driveRowData = setData; + return ( +
+ +
+ ); + } + + async function streamRowDataCycles(cycles = 3): Promise { + for (let cycle = 0; cycle < cycles; cycle++) { + act(() => driveRowData!(cycleData(cycle))); + await asyncSetTimeout(20); // flush React commits + rAF before next cycle + } + } + + test.each(['default', 'legacy'] as const)( + 'setRowData via React prop without getRowId (%s): rows mount with cells', + async (renderingMode) => { + const rendered = render(); + await waitFor(() => expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan(0)); + + const records = await recordContentAppendedIntoExistingRows(rendered.container, streamRowDataCycles); + + expectNoFlicker(records); + } + ); + + // Detects the fade-out flicker directly: assert no row gains `ag-opacity-zero` + // during a non-animating wholesale-replace. + test.each(['default', 'legacy'] as const)( + 'setRowData wholesale-replace without getRowId (%s): no fade-out class toggling', + async (renderingMode) => { + const rendered = render(); + await waitFor(() => expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan(0)); + + const opacityToggles: { rowId: string | null; value: string }[] = []; + const observer = new MutationObserver((mutations) => { + for (const m of mutations) { + if (m.type !== 'attributes' || m.attributeName !== 'class') { + continue; + } + const target = m.target; + if (!(target instanceof HTMLElement) || !target.hasAttribute('row-id')) { + continue; + } + const cls = target.getAttribute('class') ?? ''; + if (cls.includes('ag-opacity-zero')) { + opacityToggles.push({ rowId: target.getAttribute('row-id'), value: cls }); + } + } + }); + observer.observe(rendered.container, { subtree: true, attributes: true, attributeFilter: ['class'] }); + + await streamRowDataCycles(); + observer.disconnect(); + + if (opacityToggles.length > 0) { + throw new Error( + `Expected no rows to receive ag-opacity-zero during wholesale-replace without getRowId, ` + + `but ${opacityToggles.length} row class change(s) included it. ` + + `First: ${JSON.stringify(opacityToggles.slice(0, 3))}` + ); + } + } + ); + + test('column visibility toggle after bulk add still produces correct cells', async () => { + // Guards against a regression where the constructor pre-creation prevents the + // setComp-driven reconciliation from picking up subsequent column changes. + const columnDefs: ColDef[] = [{ field: 'a' }, { field: 'b' }, { field: 'c' }]; + const initialRowData = Array.from({ length: 3 }, (_, i) => ({ a: `a${i}`, b: `b${i}`, c: `c${i}` })); + + let gridApi: GridApi | undefined; + const rendered = render( +
+ { + gridApi = p.api; + }} + /> +
+ ); + + await waitFor(() => expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan(0)); + + act(() => { + gridApi!.applyTransaction({ + add: Array.from({ length: 20 }, (_, i) => ({ a: `na${i}`, b: `nb${i}`, c: `nc${i}` })), + }); + }); + await waitFor(() => + expect(rendered.container.querySelectorAll(ROW_SELECTOR).length).toBeGreaterThan(initialRowData.length) + ); + + act(() => { + gridApi!.setColumnsVisible(['b'], false); + }); + + await waitFor(() => { + const dataRows = Array.from(rendered.container.querySelectorAll(ROW_SELECTOR)).filter( + (r) => r.querySelectorAll('[role="gridcell"]').length > 0 + ); + expect(dataRows.length).toBeGreaterThan(0); + for (const row of dataRows) { + expect(row.querySelectorAll('[role="gridcell"]').length).toBe(2); + } + }); + }); +}); diff --git a/testing/behavioural/src/toolbar/toolbar-panel-items.test.ts b/testing/behavioural/src/toolbar/toolbar-panel-items.test.ts index ee8003082fc..0ba338cdc12 100644 --- a/testing/behavioural/src/toolbar/toolbar-panel-items.test.ts +++ b/testing/behavioural/src/toolbar/toolbar-panel-items.test.ts @@ -1,4 +1,4 @@ -import { ClientSideRowModelModule, QuickFilterModule } from 'ag-grid-community'; +import { ClientSideRowModelModule, CustomFilterModule, QuickFilterModule } from 'ag-grid-community'; import { ContextMenuModule, FindModule, @@ -21,6 +21,7 @@ describe('Toolbar panel items (rowGroupPanel and pivotPanel)', () => { RowGroupingModule, RowGroupingPanelModule, ToolbarModule, + CustomFilterModule, ], }); diff --git a/testing/behavioural/src/toolbar/toolbar.test.ts b/testing/behavioural/src/toolbar/toolbar.test.ts index 73f5eb5c6ca..6678ebae509 100644 --- a/testing/behavioural/src/toolbar/toolbar.test.ts +++ b/testing/behavioural/src/toolbar/toolbar.test.ts @@ -118,7 +118,7 @@ describe('Toolbar', () => { expect(instance).toBeDefined(); }); - test('returns the built-in item instance by derived key when string form is used', async () => { + test('returns built in item when string form is used (no explicit key)', async () => { const api = gridMgr.createGrid('get-instance-builtin-string-form', { columnDefs: [{ field: 'name' }], rowData: [{ name: 'Alice' }], @@ -130,7 +130,7 @@ describe('Toolbar', () => { expect(api.getToolbarItemInstance('agQuickFilterToolbarItem')).toBeDefined(); }); - test('returns the built-in item instance by derived key when no explicit key is given', async () => { + test('returns undefined when no explicit key is given on object form', async () => { const api = gridMgr.createGrid('get-instance-builtin-derived-key', { columnDefs: [{ field: 'name' }], rowData: [{ name: 'Alice' }], @@ -141,7 +141,7 @@ describe('Toolbar', () => { await waitForEvent('firstDataRendered', api); - expect(api.getToolbarItemInstance('agQuickFilterToolbarItem')).toBeDefined(); + expect(api.getToolbarItemInstance('agQuickFilterToolbarItem')).toBeUndefined(); }); test('returns undefined after toolbar items are cleared at runtime', async () => { @@ -163,6 +163,70 @@ describe('Toolbar', () => { }); }); + describe('duplicate items', () => { + test('does not render duplicate built-in items in string form', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const api = gridMgr.createGrid('duplicate-string-form', { + columnDefs: [{ field: 'name' }], + rowData: [{ name: 'Alice' }], + toolbar: { items: ['agQuickFilterToolbarItem', 'agQuickFilterToolbarItem'] }, + }); + + await waitForEvent('firstDataRendered', api); + + const gridDiv = TestGridsManager.getHTMLElement(api)!; + const toolbar = gridDiv.querySelector('.ag-toolbar')!; + expect(toolbar.querySelectorAll('.ag-toolbar-input-field').length).toBe(1); + + const warnings = warnSpy.mock.calls.flat().join(' '); + expect(warnings).toContain('303'); + + warnSpy.mockRestore(); + }); + + test('renders duplicate built-in items when passed as objects without explicit keys', async () => { + const api = gridMgr.createGrid('duplicate-keyless-object-form', { + columnDefs: [{ field: 'name' }], + rowData: [{ name: 'Alice' }], + toolbar: { + items: [{ toolbarItem: 'agQuickFilterToolbarItem' }, { toolbarItem: 'agQuickFilterToolbarItem' }], + }, + }); + + await waitForEvent('firstDataRendered', api); + + const gridDiv = TestGridsManager.getHTMLElement(api)!; + const toolbar = gridDiv.querySelector('.ag-toolbar')!; + expect(toolbar.querySelectorAll('.ag-toolbar-input-field').length).toBe(2); + }); + + test('does not render both items when explicit keys collide and warns', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const api = gridMgr.createGrid('duplicate-explicit-key-collision', { + columnDefs: [{ field: 'name' }], + rowData: [{ name: 'Alice' }], + toolbar: { + items: [ + { toolbarItem: 'agQuickFilterToolbarItem', key: 'shared' }, + { toolbarItem: 'agQuickFilterToolbarItem', key: 'shared' }, + ], + }, + }); + + await waitForEvent('firstDataRendered', api); + + const gridDiv = TestGridsManager.getHTMLElement(api)!; + const toolbar = gridDiv.querySelector('.ag-toolbar')!; + expect(toolbar.querySelectorAll('.ag-toolbar-input-field').length).toBe(1); + + const warnings = warnSpy.mock.calls.flat().join(' '); + expect(warnings).toContain('303'); + + warnSpy.mockRestore(); + }); + }); + describe('runtime updates via setGridOption', () => { test('adds items when toolbar items are populated at runtime', async () => { // Start with an empty items array so the AG-TOOLBAR element is registered up-front