From 23bcdb814e475a2ffeb26d0a6ca9f9f01f2e7e29 Mon Sep 17 00:00:00 2001 From: Iukou Siarhei <45054016+BlazarQSO@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:12:31 +0300 Subject: [PATCH] EPMRPP-91556 || Filter organization (#4116) * EPMRPP-91556 || Filter organization * EPMRPP-91556 || Code Review fix - 1 * EPMRPP-9155 || fix url name * EPMRPP-91556 || change conditions * EPMRPP-91556 || add help text in organization projects filter * EPMRPP-91556 || fix last run date * EPMRPP-91556 || Code Review fix - 2 * EPMRPP-91556 || Code Review fix - 3 * EPMRPP-91556 || Code Review fix - 4 * remove unnecessary imports --- app/localization/translated/be.json | 29 ++++ app/localization/translated/es.json | 29 ++++ app/localization/translated/ru.json | 29 ++++ app/localization/translated/uk.json | 29 ++++ app/localization/translated/zh.json | 29 ++++ app/package-lock.json | 8 +- app/package.json | 2 +- app/src/common/constants/localization.js | 4 + .../img/newIcons/filters-outline-inline.svg | 3 - app/src/common/urls.js | 3 + .../containers/filterEntitiesURLContainer.jsx | 65 ++++++++- .../filterEntities/containers/index.js | 2 +- app/src/components/filterEntities/utils.js | 59 ++++++++ .../components/main/filterButton/constants.js | 51 +++++++ .../main/filterButton/filterButton.jsx | 115 ++++++++++++++++ .../main/filterButton/filterButton.scss | 128 ++++++++++++++++++ .../filterContent/filterContent.jsx | 122 +++++++++++++++++ .../filterContent/filterContent.scss | 36 +++++ .../filterContent/filterInput/filterInput.jsx | 79 +++++++++++ .../filterInput/filterInput.scss | 60 ++++++++ .../filterContent/filterInput/index.js | 17 +++ .../main/filterButton/filterContent/index.js | 17 +++ .../filterButton/filterContent/messages.js | 24 ++++ .../components/main/filterButton/index.jsx | 27 ++++ .../components/main/filterButton/messages.js | 68 ++++++++++ app/src/controllers/instance/events/utils.js | 4 +- .../instance/organizations/actionCreators.js | 6 +- .../instance/organizations/constants.js | 2 + .../instance/organizations/index.js | 4 +- .../instance/organizations/sagas.js | 21 ++- .../organization/projects/actionCreators.js | 11 +- .../organization/projects/constants.js | 2 + .../organization/projects/index.js | 3 +- .../organization/projects/sagas.js | 35 ++++- .../organization/projects/selectors.js | 2 +- app/src/controllers/pages/selectors.js | 4 +- app/src/controllers/sorting/constants.js | 1 + .../addEditNotificationModal.jsx | 6 +- .../organizationsPage/organizationsPage.jsx | 5 +- .../organizationsFilter/index.js | 17 +++ .../organizationsFilter/messages.js | 52 +++++++ .../organizationsFilter.jsx | 112 +++++++++++++++ .../organizationsPageHeader.jsx | 19 ++- .../organizationProjectsPage.jsx | 5 +- .../projectsFilter/index.js | 17 +++ .../projectsFilter/messages.js | 52 +++++++ .../projectsFilter/projectsFilter.jsx | 112 +++++++++++++++ .../projectsPageHeader/projectsPageHeader.jsx | 19 ++- .../projectTeamPageHeader.jsx | 8 +- 49 files changed, 1507 insertions(+), 47 deletions(-) delete mode 100644 app/src/common/img/newIcons/filters-outline-inline.svg create mode 100644 app/src/components/main/filterButton/constants.js create mode 100644 app/src/components/main/filterButton/filterButton.jsx create mode 100644 app/src/components/main/filterButton/filterButton.scss create mode 100644 app/src/components/main/filterButton/filterContent/filterContent.jsx create mode 100644 app/src/components/main/filterButton/filterContent/filterContent.scss create mode 100644 app/src/components/main/filterButton/filterContent/filterInput/filterInput.jsx create mode 100644 app/src/components/main/filterButton/filterContent/filterInput/filterInput.scss create mode 100644 app/src/components/main/filterButton/filterContent/filterInput/index.js create mode 100644 app/src/components/main/filterButton/filterContent/index.js create mode 100644 app/src/components/main/filterButton/filterContent/messages.js create mode 100644 app/src/components/main/filterButton/index.jsx create mode 100644 app/src/components/main/filterButton/messages.js create mode 100644 app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/index.js create mode 100644 app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/messages.js create mode 100644 app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/organizationsFilter.jsx create mode 100644 app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/index.js create mode 100644 app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/messages.js create mode 100644 app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/projectsFilter.jsx diff --git a/app/localization/translated/be.json b/app/localization/translated/be.json index 1f94f69efe..c63db82272 100644 --- a/app/localization/translated/be.json +++ b/app/localization/translated/be.json @@ -775,6 +775,18 @@ "Filter.name": "Назва", "Filter.namePlaceholder": "Увядзіце назву фільтра", "FilterAdd.addTitle": "Дадаць новы фільтр", + "FilterButton.any": "Нейкі", + "FilterButton.contains": "Змяшчае", + "FilterButton.equals": "Раўняецца", + "FilterButton.greaterOrEqual": "Больш або роўна", + "FilterButton.helpText": "Дазволеныя толькі лічбы", + "FilterButton.lessOrEqual": "Менш або роўна", + "FilterButton.last2days": "Апошнія 2 дні", + "FilterButton.last7days": "Апошнія 7 дзён", + "FilterButton.last30days": "Апошнія 30 дзён", + "FilterButton.notContains": "Не змяшчае", + "FilterButton.notEqual": "Не роўна", + "FilterButton.today": "Сёння", "FilterEdit.editTitle": "Рэдагаваць фільтр", "FilterNameById.statistics$defects$automation_bug": "Automation Bug", "FilterNameById.statistics$defects$no_defect": "No Defect", @@ -1572,6 +1584,14 @@ "OrganizationsControl.all": "Усе", "OrganizationsControl.allOrganizations": "Усе арганізацыі", "OrganizationsControl.organization": "Арганізацыя", + "OrganizationsFilter.lastRunDate": "Дата апошняга запуску", + "OrganizationsFilter.lastRunDatePlaceholder": "Любая", + "OrganizationsFilter.launches": "Запускі", + "OrganizationsFilter.launchesPlaceholder": "Увядзіце колькасць запускаў", + "OrganizationsFilter.name": "Назва Арганізацыі", + "OrganizationsFilter.namePlaceholder": "Увядзіце частку імя", + "OrganizationsFilter.users": "Карыстальнік", + "OrganizationsFilter.usersPlaceholder": "Увядзіце колькасць удзельнікаў", "OrganizationsItem.open": "адкрыць", "OrganizationsPage.title": "Усе арганізацыі", "OrganizationsPage.description": "Спіс даступных вам арганізацый у дадзены момант пусты. Калі ласка, звяжыцеся са сваім адміністратарам, каб атрымаць прызначэнне ва ўжо існуючую арганізацыю.", @@ -1801,6 +1821,15 @@ "ProjectsGrid.nameCol": "Назва", "ProjectsGrid.organizationCol": "Арганізацыя", "ProjectsGrid.projectTypeCol": "Тып праекта", + "ProjectsFilter.lastRunDate": "Дата апошняга запуску", + "ProjectsFilter.lastRunDatePlaceholder": "Любая", + "ProjectsFilter.launches": "Запускі", + "ProjectsFilter.launchesPlaceholder": "Увядзіце колькасць запускаў", + "ProjectsFilter.name": "Назва Праекта", + "ProjectsFilter.namePlaceholder": "Увядзіце частку імя", + "ProjectsFilter.users": "Таварышы па камандзе", + "ProjectsFilter.usersPlaceholder": "Увядзіце колькасць удзельнікаў", + "ProjectsFilterPopover.clearAllFilters": "Ачысціць усе фільтры", "ProjectsPage.addProject": "Дадаць Праект", "ProjectsPage.addProjectSuccess": "Праект ''{name}'' быў паспяхова створаны", "ProjectsPage.addProjectTitle": "Дадаць Праект", diff --git a/app/localization/translated/es.json b/app/localization/translated/es.json index 00ab123867..d34581bed6 100644 --- a/app/localization/translated/es.json +++ b/app/localization/translated/es.json @@ -774,6 +774,18 @@ "Filter.name": "Nombre", "Filter.namePlaceholder": "Introduce el nombre del filtro", "FilterAdd.addTitle": "Agregar nuevo filtro", + "FilterButton.any": "Any", + "FilterButton.contains": "Contains", + "FilterButton.equals": "Equals", + "FilterButton.greaterOrEqual": "Greater or equal", + "FilterButton.helpText": "Only digits are allowed", + "FilterButton.lessOrEqual": "Less or equal", + "FilterButton.last2days": "Last 2 days", + "FilterButton.last7days": "Last 7 days", + "FilterButton.last30days": "Last 30 days", + "FilterButton.notContains": "Not contains", + "FilterButton.notEqual": "Not equal", + "FilterButton.today": "Today", "FilterEdit.editTitle": "Editar filtro", "FilterNameById.statistics$defects$automation_bug": "Error de automatización", "FilterNameById.statistics$defects$no_defect": "Sin defecto", @@ -1571,6 +1583,14 @@ "OrganizationsControl.all": "All", "OrganizationsControl.allOrganizations": "All organizations", "OrganizationsControl.organization": "Organization", + "OrganizationsFilter.lastRunDate": "Last Run Date", + "OrganizationsFilter.lastRunDatePlaceholder": "Any", + "OrganizationsFilter.launches": "Launches", + "OrganizationsFilter.launchesPlaceholder": "Enter the number of launches", + "OrganizationsFilter.name": "Organization Name", + "OrganizationsFilter.namePlaceholder": "Enter part of the name", + "OrganizationsFilter.users": "Users", + "OrganizationsFilter.usersPlaceholder": "Enter the number of members", "OrganizationsItem.open": "open", "OrganizationsPage.title": "All Organizations", "OrganizationsPage.description": "The list of organizations available to you is currently empty. Please contact your Administrator to be assigned to an existing one.", @@ -1800,6 +1820,15 @@ "ProjectsGrid.nameCol": "Nombre", "ProjectsGrid.organizationCol": "Organización", "ProjectsGrid.projectTypeCol": "Tipo de proyecto", + "ProjectsFilter.lastRunDate": "Last Run Date", + "ProjectsFilter.lastRunDatePlaceholder": "Any", + "ProjectsFilter.launches": "Launches", + "ProjectsFilter.launchesPlaceholder": "Enter the number of launches", + "ProjectsFilter.name": "Project Name", + "ProjectsFilter.namePlaceholder": "Enter part of the name", + "ProjectsFilter.users": "Teammates", + "ProjectsFilter.usersPlaceholder": "Enter the number of members", + "ProjectsFilterPopover.clearAllFilters": "Clear all filters", "ProjectsPage.addProject": "Crear Proyecto", "ProjectsPage.addProjectSuccess": "El proyecto ''{name}'' ha sido creado exitosamente", "ProjectsPage.addProjectTitle": "Agregar Proyecto", diff --git a/app/localization/translated/ru.json b/app/localization/translated/ru.json index aac3f200fe..a09d9f9939 100644 --- a/app/localization/translated/ru.json +++ b/app/localization/translated/ru.json @@ -775,6 +775,18 @@ "Filter.name": "Имя", "Filter.namePlaceholder": "Ввести имя фильтра", "FilterAdd.addTitle": "Добавить новый фильтр", + "FilterButton.any": "Любой", + "FilterButton.contains": "Содержит", + "FilterButton.equals": "Равняется", + "FilterButton.greaterOrEqual": "Больше или равно", + "FilterButton.helpText": "Разрешены только цифры", + "FilterButton.lessOrEqual": "Меньше или равно", + "FilterButton.last2days": "Последние 2 дня", + "FilterButton.last7days": "Последние 7 дней", + "FilterButton.last30days": "Последние 30 дней", + "FilterButton.notContains": "Не содержит", + "FilterButton.notEqual": "Не равны", + "FilterButton.today": "Сегодня", "FilterEdit.editTitle": "Редактировать фильтр", "FilterNameById.statistics$defects$automation_bug": "Automation Bug", "FilterNameById.statistics$defects$no_defect": "No Defect", @@ -1568,6 +1580,14 @@ "OrganizationsControl.all": "Все", "OrganizationsControl.allOrganizations": "Все организации", "OrganizationsControl.organization": "Организация", + "OrganizationsFilter.lastRunDate": "Дата последнего запуска", + "OrganizationsFilter.lastRunDatePlaceholder": "Любой", + "OrganizationsFilter.launches": "Запуски", + "OrganizationsFilter.launchesPlaceholder": "Введите количество запусков", + "OrganizationsFilter.name": "Название Организации", + "OrganizationsFilter.namePlaceholder": "Введите часть имени", + "OrganizationsFilter.users": "Пользователи", + "OrganizationsFilter.usersPlaceholder": "Введите количество участников", "OrganizationsItem.open": "открыть", "OrganizationsPage.title": "Все организации", "OrganizationsPage.description": "Список доступных вам организаций в данный момент пуст. Пожалуйста, свяжитесь со своим администратором, чтобы получить назначение в уже существующую организацию.", @@ -1797,6 +1817,15 @@ "ProjectsGrid.nameCol": "Название", "ProjectsGrid.organizationCol": "Организация", "ProjectsGrid.projectTypeCol": "Тип проекта", + "ProjectsFilter.lastRunDate": "Дата последнего запуска", + "ProjectsFilter.lastRunDatePlaceholder": "Любой", + "ProjectsFilter.launches": "Запуски", + "ProjectsFilter.launchesPlaceholder": "Введите количество запусков", + "ProjectsFilter.name": "Название проекта", + "ProjectsFilter.namePlaceholder": "Введите часть имени", + "ProjectsFilter.users": "Товарищи по команде", + "ProjectsFilter.usersPlaceholder": "Введите количество участников", + "ProjectsFilterPopover.clearAllFilters": "Очистить все фильтры", "ProjectsPage.addProject": "Создать Проект", "ProjectsPage.addProjectSuccess": "Проект ''{name}'' успешно создан", "ProjectsPage.addProjectTitle": "Добавить Проект", diff --git a/app/localization/translated/uk.json b/app/localization/translated/uk.json index 1c304afdec..aecfa94f2f 100644 --- a/app/localization/translated/uk.json +++ b/app/localization/translated/uk.json @@ -775,6 +775,18 @@ "Filter.name": "Ім’я", "Filter.namePlaceholder": "Ввести ім’я фільтра", "FilterAdd.addTitle": "Додати новий фільтр", + "FilterButton.any": "Будь-який", + "FilterButton.contains": "Містити", + "FilterButton.equals": "Дорівнювати", + "FilterButton.greaterOrEqual": "Більше або дорівнює", + "FilterButton.helpText": "Дозволені лише цифри", + "FilterButton.lessOrEqual": "Менше або дорівнює", + "FilterButton.last2days": "Останні 2 дні", + "FilterButton.last7days": "Останні 7 днів", + "FilterButton.last30days": "Останні 30 днів", + "FilterButton.notContains": "Не містить", + "FilterButton.notEqual": "Не рівні", + "FilterButton.today": "Сьогодні", "FilterEdit.editTitle": "Редагувати фільтр", "FilterNameById.statistics$defects$automation_bug": "Помилка Автоматизації", "FilterNameById.statistics$defects$no_defect": "Ніякої Дефект", @@ -1570,6 +1582,14 @@ "OrganizationsControl.all": "Всі", "OrganizationsControl.allOrganizations": "Всі організації", "OrganizationsControl.organization": "Організація", + "OrganizationsFilter.lastRunDate": "Last Run Date", + "OrganizationsFilter.lastRunDatePlaceholder": "Будь-який", + "OrganizationsFilter.launches": "Запуски", + "OrganizationsFilter.launchesPlaceholder": "Введіть кількість запусків", + "OrganizationsFilter.name": "Назва організації", + "OrganizationsFilter.namePlaceholder": "Введіть частину імені", + "OrganizationsFilter.users": "Користувачі", + "OrganizationsFilter.usersPlaceholder": "Введіть кількість учасників", "OrganizationsItem.open": "відкрити", "OrganizationsPage.title": "Всі організації", "OrganizationsPage.description": "Список доступних вам організацій в даний момент порожній. Будь ласка, зв'яжіться зі своїм адміністратором, щоб отримати призначення в уже існуючу організацію.", @@ -1799,6 +1819,15 @@ "ProjectsGrid.nameCol": "Назва", "ProjectsGrid.organizationCol": "Організація", "ProjectsGrid.projectTypeCol": "Тип проекту", + "ProjectsFilter.lastRunDate": "Дата останнього запуску", + "ProjectsFilter.lastRunDatePlaceholder": "Будь-який", + "ProjectsFilter.launches": "Запуски", + "ProjectsFilter.launchesPlaceholder": "Введіть кількість запусків", + "ProjectsFilter.name": "Назва проекту", + "ProjectsFilter.namePlaceholder": "Введіть частину імені", + "ProjectsFilter.users": "Товариші по команді", + "ProjectsFilter.usersPlaceholder": "Введіть кількість учасників", + "ProjectsFilterPopover.clearAllFilters": "Очистити всі фільтри", "ProjectsPage.addProject": "Створити Проект", "ProjectsPage.addProjectSuccess": "Проект ''{name}'' успешно создан", "ProjectsPage.addProjectTitle": "Проект Додати", diff --git a/app/localization/translated/zh.json b/app/localization/translated/zh.json index 3dad4c944c..c6ae298e59 100644 --- a/app/localization/translated/zh.json +++ b/app/localization/translated/zh.json @@ -775,6 +775,18 @@ "Filter.name": "名称", "Filter.namePlaceholder": "请输入过滤器名称", "FilterAdd.addTitle": "添加新过滤器", + "FilterButton.any": "Any", + "FilterButton.contains": "Contains", + "FilterButton.equals": "Equals", + "FilterButton.greaterOrEqual": "Greater or equal", + "FilterButton.helpText": "Only digits are allowed", + "FilterButton.lessOrEqual": "Less or equal", + "FilterButton.last2days": "Last 2 days", + "FilterButton.last7days": "Last 7 days", + "FilterButton.last30days": "Last 30 days", + "FilterButton.notContains": "Not contains", + "FilterButton.notEqual": "Not equal", + "FilterButton.today": "Today", "FilterEdit.editTitle": "编辑过滤器", "FilterNameById.statistics$defects$automation_bug": "自动化错误", "FilterNameById.statistics$defects$no_defect": "无缺陷", @@ -1570,6 +1582,14 @@ "OrganizationsControl.all": "All", "OrganizationsControl.allOrganizations": "All organizations", "OrganizationsControl.organization": "组织", + "OrganizationsFilter.lastRunDate": "Last Run Date", + "OrganizationsFilter.lastRunDatePlaceholder": "Any", + "OrganizationsFilter.launches": "Launches", + "OrganizationsFilter.launchesPlaceholder": "Enter the number of launches", + "OrganizationsFilter.name": "Organization Name", + "OrganizationsFilter.namePlaceholder": "Enter part of the name", + "OrganizationsFilter.users": "Users", + "OrganizationsFilter.usersPlaceholder": "Enter the number of members", "OrganizationsItem.open": "open", "OrganizationsPage.title": "All Organizations", "OrganizationsPage.description": "The list of organizations available to you is currently empty. Please contact your Administrator to be assigned to an existing one.", @@ -1799,6 +1819,15 @@ "ProjectsGrid.nameCol": "名称", "ProjectsGrid.organizationCol": "组织", "ProjectsGrid.projectTypeCol": "项目类型", + "ProjectsFilter.lastRunDate": "Last Run Date", + "ProjectsFilter.lastRunDatePlaceholder": "Any", + "ProjectsFilter.launches": "Launches", + "ProjectsFilter.launchesPlaceholder": "Enter the number of launches", + "ProjectsFilter.name": "Project Name", + "ProjectsFilter.namePlaceholder": "Enter part of the name", + "ProjectsFilter.users": "Teammates", + "ProjectsFilter.usersPlaceholder": "Enter the number of members", + "ProjectsFilterPopover.clearAllFilters": "Clear all filters", "ProjectsPage.addProject": "Create Project", "ProjectsPage.addProjectSuccess": "项目“{name}”创建成功", "ProjectsPage.addProjectTitle": "创建项目", diff --git a/app/package-lock.json b/app/package-lock.json index c4d471cb76..e490c72df6 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -12,7 +12,7 @@ "@formatjs/intl-pluralrules": "1.3.9", "@formatjs/intl-relativetimeformat": "4.5.1", "@formatjs/intl-utils": "1.6.0", - "@reportportal/ui-kit": "^0.0.1-alpha.34", + "@reportportal/ui-kit": "^0.0.1-alpha.35", "axios": "1.6.4", "c3": "0.7.20", "chart.js": "2.9.4", @@ -3677,9 +3677,9 @@ "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, "node_modules/@reportportal/ui-kit": { - "version": "0.0.1-alpha.34", - "resolved": "https://registry.npmjs.org/@reportportal/ui-kit/-/ui-kit-0.0.1-alpha.34.tgz", - "integrity": "sha512-VQfwBAeZ9otmrYTpERBOoGhYMkncnA4Upchw1LH7AflFP26YGlN8Z/nSnTcB5kbqnRDGAFn8/UF2Z7G3/Q09WQ==", + "version": "0.0.1-alpha.35", + "resolved": "https://registry.npmjs.org/@reportportal/ui-kit/-/ui-kit-0.0.1-alpha.35.tgz", + "integrity": "sha512-Ru2VkZfy3KAP62JC4sU1uYYA3EORL6JcaUPFVnNks6Fn+ezPEIdKfI9AAQn0/d1PKVxZT8qx7jvlL+IkNjA5Pg==", "dependencies": { "@floating-ui/react": "^0.26.16", "@floating-ui/react-dom": "^2.0.1", diff --git a/app/package.json b/app/package.json index 37b89eed27..7aebfdbde6 100644 --- a/app/package.json +++ b/app/package.json @@ -25,7 +25,7 @@ "@formatjs/intl-pluralrules": "1.3.9", "@formatjs/intl-relativetimeformat": "4.5.1", "@formatjs/intl-utils": "1.6.0", - "@reportportal/ui-kit": "^0.0.1-alpha.34", + "@reportportal/ui-kit": "^0.0.1-alpha.35", "axios": "1.6.4", "c3": "0.7.20", "chart.js": "2.9.4", diff --git a/app/src/common/constants/localization.js b/app/src/common/constants/localization.js index 1f2c379024..f40847058f 100644 --- a/app/src/common/constants/localization.js +++ b/app/src/common/constants/localization.js @@ -31,6 +31,10 @@ export const COMMON_LOCALE_KEYS = defineMessages({ id: 'Common.cancel', defaultMessage: 'Cancel', }, + APPLY: { + id: 'Common.apply', + defaultMessage: 'Apply', + }, RENAME: { id: 'Common.rename', defaultMessage: 'Rename', diff --git a/app/src/common/img/newIcons/filters-outline-inline.svg b/app/src/common/img/newIcons/filters-outline-inline.svg deleted file mode 100644 index d0cb45fbdc..0000000000 --- a/app/src/common/img/newIcons/filters-outline-inline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/src/common/urls.js b/app/src/common/urls.js index 3d91a6ed92..990721b5b8 100644 --- a/app/src/common/urls.js +++ b/app/src/common/urls.js @@ -127,8 +127,11 @@ export const URLS = { organizationList: (preferencesObj = {}) => `${urlCommonBase}organizations${getQueryParams(preferencesObj)}`, + organizationSearches: () => `${urlCommonBase}organizations/searches`, organizationProjects: (organizationId, preferencesObj = {}) => `${urlCommonBase}organizations/${organizationId}/projects${getQueryParams(preferencesObj)}`, + organizationProjectsSearches: (organizationId) => + `${urlCommonBase}organizations/${organizationId}/projects/searches`, organizationUsers: (organizationId, preferencesObj = {}) => `${urlCommonBase}organizations/${organizationId}/users${getQueryParams(preferencesObj)}`, projectDelete: ({ organizationId, projectId }) => diff --git a/app/src/components/filterEntities/containers/filterEntitiesURLContainer.jsx b/app/src/components/filterEntities/containers/filterEntitiesURLContainer.jsx index bf8838a32f..c45db0a6e1 100644 --- a/app/src/components/filterEntities/containers/filterEntitiesURLContainer.jsx +++ b/app/src/components/filterEntities/containers/filterEntitiesURLContainer.jsx @@ -24,7 +24,6 @@ import { collectFilterEntities, createFilterQuery } from './utils'; const FilterEntitiesURL = ({ entities = {}, updateFilters = () => {}, - render, debounced = true, debounceTime = 1000, defaultPagination, @@ -48,22 +47,78 @@ const FilterEntitiesURL = ({ debounceTime, ]); - return render({ + return { entities, onChange: debounced ? debouncedHandleChange : handleChange, - }); + }; }; FilterEntitiesURL.propTypes = { entities: PropTypes.object, updateFilters: PropTypes.func, - render: PropTypes.func.isRequired, debounced: PropTypes.bool, debounceTime: PropTypes.number, defaultPagination: PropTypes.any.isRequired, prefixQueryKey: PropTypes.string, }; +const filterEntitiesURLForContainer = ({ + entities = {}, + updateFilters = () => {}, + debounced = true, + debounceTime = 1000, + defaultPagination, + prefixQueryKey, + render, +}) => { + const filterEntitiesURLParams = FilterEntitiesURL({ + entities, + updateFilters, + debounced, + debounceTime, + defaultPagination, + prefixQueryKey, + }); + return render(filterEntitiesURLParams); +}; + +export const withFilterEntitiesURL = (namespace, prefixQueryKey) => (WrappedComponent) => { + const filterEntitiesURL = (props) => { + const { + entities, + defaultPagination, + updateFilters, + debounced, + debounceTime, + ...restProps + } = props; + + const { entities: filteredEntities, onChange } = FilterEntitiesURL({ + entities, + updateFilters, + debounced, + debounceTime, + defaultPagination, + prefixQueryKey, + }); + + return ( + + ); + }; + + return connectRouter( + (query) => ({ + entities: collectFilterEntities(query, prefixQueryKey), + defaultPagination: defaultPaginationSelector(), + }), + { + updateFilters: (query, page) => ({ ...query, [PAGE_KEY]: page }), + }, + { namespace }, + )(filterEntitiesURL); +}; + const createFilterEntitiesURLContainer = (prefixQueryKey) => connectRouter( (query) => ({ @@ -73,6 +128,6 @@ const createFilterEntitiesURLContainer = (prefixQueryKey) => { updateFilters: (query, page) => ({ ...query, [PAGE_KEY]: page }), }, - )(FilterEntitiesURL); + )(filterEntitiesURLForContainer); export const FilterEntitiesURLContainer = createFilterEntitiesURLContainer(); diff --git a/app/src/components/filterEntities/containers/index.js b/app/src/components/filterEntities/containers/index.js index c887364d38..3894dad8c7 100644 --- a/app/src/components/filterEntities/containers/index.js +++ b/app/src/components/filterEntities/containers/index.js @@ -15,4 +15,4 @@ */ export { FilterEntitiesContainer } from './filterEntitiesContainer'; -export { FilterEntitiesURLContainer } from './filterEntitiesURLContainer'; +export { FilterEntitiesURLContainer, withFilterEntitiesURL } from './filterEntitiesURLContainer'; diff --git a/app/src/components/filterEntities/utils.js b/app/src/components/filterEntities/utils.js index 7524271626..2f91d5a774 100644 --- a/app/src/components/filterEntities/utils.js +++ b/app/src/components/filterEntities/utils.js @@ -14,6 +14,11 @@ * limitations under the License. */ +import moment from 'moment/moment'; +import { getMinutesFromTimestamp } from 'common/utils'; +import { LAST_RUN_DATE_FILTER_NAME } from 'components/main/filterButton'; +import { getAppliedFilters } from 'controllers/instance/events/utils'; + export function bindDefaultValue(key, options = {}) { const { filterValues } = this.props; if (key in filterValues) { @@ -25,3 +30,57 @@ export function bindDefaultValue(key, options = {}) { ...options, }; } + +const getFormattedDate = (value) => { + const utcString = moment().format('ZZ'); + const calculateStartDate = (days) => + moment() + .startOf('day') + .subtract(days - 1, 'days') + .valueOf(); + const endOfToday = moment() + .add(1, 'days') + .startOf('day') + .valueOf(); + let start = null; + switch (value) { + case 'today': + start = calculateStartDate(1); + break; + case 'last2days': + start = calculateStartDate(2); + break; + case 'last7days': + start = calculateStartDate(7); + break; + case 'last30days': + start = calculateStartDate(30); + break; + default: + break; + } + return `${getMinutesFromTimestamp(start)};${getMinutesFromTimestamp(endOfToday)};${utcString}`; +}; + +export const prepareQueryFilters = (filtersParams) => { + const { limit, sort, offset, order, ...rest } = filtersParams; + + const searchCriteria = getAppliedFilters(rest)?.search_criterias; + + const lastRunDateFilterIndex = Object.values(searchCriteria).findIndex( + (el) => el.filter_key === LAST_RUN_DATE_FILTER_NAME, + ); + if (lastRunDateFilterIndex !== -1) { + searchCriteria[lastRunDateFilterIndex].value = getFormattedDate( + searchCriteria[lastRunDateFilterIndex].value, + ); + } + + return { + limit, + sort, + offset, + order, + search_criteria: searchCriteria, + }; +}; diff --git a/app/src/components/main/filterButton/constants.js b/app/src/components/main/filterButton/constants.js new file mode 100644 index 0000000000..c824da52be --- /dev/null +++ b/app/src/components/main/filterButton/constants.js @@ -0,0 +1,51 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CONDITION_CNT, + CONDITION_EQ, + CONDITION_GREATER_EQ, + CONDITION_LESS_EQ, + CONDITION_NOT_CNT_EVENTS, + CONDITION_NOT_EQ, +} from 'components/filterEntities/constants'; +import { messages } from './messages'; + +export const LAST_RUN_DATE_FILTER_NAME = 'last_launch_occurred'; +export const LAUNCHES_FILTER_NAME = 'launches'; +export const TEAMMATES_FILTER_NAME = 'users'; +export const FILTER_NAME = 'name'; + +export const getTimeRange = (formatMessage) => [ + { label: formatMessage(messages.any), value: '' }, + { label: formatMessage(messages.today), value: 'today' }, + { label: formatMessage(messages.last2days), value: 'last2days' }, + { label: formatMessage(messages.last7days), value: 'last7days' }, + { label: formatMessage(messages.last30days), value: 'last30days' }, +]; + +export const getRangeComparisons = (formatMessage) => [ + { label: formatMessage(messages.equals), value: CONDITION_EQ.toUpperCase() }, + { label: formatMessage(messages.greaterOrEqual), value: CONDITION_GREATER_EQ.toUpperCase() }, + { label: formatMessage(messages.lessOrEqual), value: CONDITION_LESS_EQ.toUpperCase() }, +]; + +export const getContainmentComparisons = (formatMessage) => [ + { label: formatMessage(messages.equals), value: CONDITION_EQ.toUpperCase() }, + { label: formatMessage(messages.notEqual), value: CONDITION_NOT_EQ.toUpperCase() }, + { label: formatMessage(messages.contains), value: CONDITION_CNT.toUpperCase() }, + { label: formatMessage(messages.notContains), value: CONDITION_NOT_CNT_EVENTS.toUpperCase() }, +]; diff --git a/app/src/components/main/filterButton/filterButton.jsx b/app/src/components/main/filterButton/filterButton.jsx new file mode 100644 index 0000000000..92c8b34cc1 --- /dev/null +++ b/app/src/components/main/filterButton/filterButton.jsx @@ -0,0 +1,115 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames/bind'; +import { Popover, FilterOutlineIcon, FilterFilledIcon } from '@reportportal/ui-kit'; +import { FilterContent } from './filterContent'; +import styles from './filterButton.scss'; + +const cx = classNames.bind(styles); + +export const FilterButton = ({ + definedFilters = {}, + onFilterChange, + appliedFiltersCount, + setAppliedFiltersCount, + defaultFilters, + filteredAction, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [filters, setFilters] = useState(defaultFilters); + const [initialFilters, setInitialFilters] = useState(defaultFilters); + + useEffect(() => { + setAppliedFiltersCount(Object.keys(definedFilters).length); + filteredAction(); + }, []); + + useEffect(() => { + if (Object.keys(definedFilters).length) { + let definedAppliedFiltersCount = 0; + const updatedFilters = { ...filters }; + Object.keys(definedFilters).forEach((filterKey) => { + updatedFilters[filterKey] = { + ...updatedFilters[filterKey], + condition: definedFilters[filterKey].condition, + value: definedFilters[filterKey].value, + }; + definedAppliedFiltersCount += definedFilters[filterKey].value ? 1 : 0; + }); + setFilters(updatedFilters); + setInitialFilters(updatedFilters); + setAppliedFiltersCount(definedAppliedFiltersCount); + } else { + setFilters(defaultFilters); + setInitialFilters(defaultFilters); + setAppliedFiltersCount(0); + } + }, [definedFilters]); + + return ( + + } + placement="bottom-end" + className={cx('filter-popover')} + isOpened={isOpen} + setIsOpened={setIsOpen} + > +
+ + {appliedFiltersCount ? : } + + {appliedFiltersCount ? ( + {appliedFiltersCount} + ) : null} +
+
+ ); +}; + +FilterButton.propTypes = { + definedFilters: PropTypes.objectOf( + PropTypes.shape({ + filter_key: PropTypes.string, + value: PropTypes.string, + condition: PropTypes.string, + }), + ), + onFilterChange: PropTypes.func, + appliedFiltersCount: PropTypes.number, + setAppliedFiltersCount: PropTypes.func, + defaultFilters: PropTypes.object, + filteredAction: PropTypes.func.isRequired, +}; diff --git a/app/src/components/main/filterButton/filterButton.scss b/app/src/components/main/filterButton/filterButton.scss new file mode 100644 index 0000000000..84f9baa600 --- /dev/null +++ b/app/src/components/main/filterButton/filterButton.scss @@ -0,0 +1,128 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.filters-icon-container { + height: 36px; + width: 40px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + + i.filter-icon { + display: block; + + svg { + width: 16px; + height: 16px; + + path { + fill: $COLOR--e-300; + } + } + } + + &:focus-visible { + outline: 2px solid $COLOR--topaz; + } + + &:hover { + i.filter-icon { + svg { + path { + fill: $COLOR--e-400; + } + } + } + } + + &.opened { + i.filter-icon { + svg { + path { + fill: $COLOR--topaz-pressed; + } + } + } + + &:hover { + i.filter-icon { + svg { + path { + fill: $COLOR--topaz-hover-2; + } + } + } + } + } + + &.with-applied { + gap: 8px; + padding: 0 12px; + background-color: $COLOR--bg-200; + + i.filter-icon { + svg { + path { + fill: $COLOR--topaz-2; + stroke: $COLOR--topaz-2; + } + } + } + + .filters-count { + font-size: 13px; + line-height: 20px; + color: $COLOR--topaz-2; + font-family: $FONT-ROBOTO-MEDIUM; + } + + &:hover { + i.filter-icon { + svg { + path { + fill: $COLOR--topaz-hover-2; + stroke: $COLOR--topaz-hover-2; + } + } + } + + .filters-count { + color: $COLOR--topaz-hover-2; + } + } + + &.opened { + i.filter-icon { + svg { + path { + fill: $COLOR--topaz-pressed; + stroke: $COLOR--topaz-pressed; + } + } + } + + .filters-count { + color: $COLOR--topaz-pressed; + } + } + } +} + +.filter-popover { + max-width: 480px; + width: 480px; +} diff --git a/app/src/components/main/filterButton/filterContent/filterContent.jsx b/app/src/components/main/filterButton/filterContent/filterContent.jsx new file mode 100644 index 0000000000..744b45924c --- /dev/null +++ b/app/src/components/main/filterButton/filterContent/filterContent.jsx @@ -0,0 +1,122 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from 'classnames/bind'; +import { Button } from '@reportportal/ui-kit'; +import { useIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import isEqual from 'fast-deep-equal'; +import { COMMON_LOCALE_KEYS } from 'common/constants/localization'; +import { useMemo } from 'react'; +import { FilterInput } from './filterInput'; +import { messages } from './messages'; +import styles from './filterContent.scss'; + +const cx = classNames.bind(styles); + +export const FilterContent = ({ + setIsOpen, + setAppliedFiltersCount, + onFilterChange, + defaultFilters, + filters, + setFilters, + initialFilters, + filteredAction, +}) => { + const { formatMessage } = useIntl(); + + const closePopover = () => { + setIsOpen(false); + setFilters(initialFilters); + }; + + const clearAllFilters = () => { + setFilters(defaultFilters); + }; + + const handleApply = () => { + let appliedFiltersCount = 0; + + const fields = Object.values(filters).reduce((acc, { filterName, value, condition }) => { + acc[filterName] = { + value, + condition, + }; + appliedFiltersCount += value ? 1 : 0; + + return acc; + }, {}); + onFilterChange(fields); + setAppliedFiltersCount(appliedFiltersCount); + filteredAction(); + setIsOpen(false); + }; + + const isDefaultFilters = useMemo(() => isEqual(defaultFilters, filters), [ + defaultFilters, + filters, + ]); + const isDefinedFilters = useMemo(() => isEqual(initialFilters, filters), [ + initialFilters, + filters, + ]); + + const handleChangeFilters = (newFilters) => + setFilters((prevFilters) => ({ + ...prevFilters, + ...newFilters, + })); + + return ( +
+
+ {Object.values(filters).map((filter) => ( + + ))} +
+
+ +
+ + +
+
+
+ ); +}; + +FilterContent.propTypes = { + setIsOpen: PropTypes.func.isRequired, + setAppliedFiltersCount: PropTypes.func.isRequired, + onFilterChange: PropTypes.func.isRequired, + defaultFilters: PropTypes.object.isRequired, + initialFilters: PropTypes.array.isRequired, + filters: PropTypes.array.isRequired, + setFilters: PropTypes.func.isRequired, + filteredAction: PropTypes.func.isRequired, +}; diff --git a/app/src/components/main/filterButton/filterContent/filterContent.scss b/app/src/components/main/filterButton/filterContent/filterContent.scss new file mode 100644 index 0000000000..b760c08945 --- /dev/null +++ b/app/src/components/main/filterButton/filterContent/filterContent.scss @@ -0,0 +1,36 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.filter-popover-content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.filter-items { + display: flex; + flex-direction: column; +} + +.actions { + display: flex; + justify-content: space-between; + + .controls { + display: flex; + gap: 8px; + } +} diff --git a/app/src/components/main/filterButton/filterContent/filterInput/filterInput.jsx b/app/src/components/main/filterButton/filterContent/filterInput/filterInput.jsx new file mode 100644 index 0000000000..8170bafad8 --- /dev/null +++ b/app/src/components/main/filterButton/filterContent/filterInput/filterInput.jsx @@ -0,0 +1,79 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from 'classnames/bind'; +import { Dropdown, FieldText } from '@reportportal/ui-kit'; +import PropTypes from 'prop-types'; +import styles from './filterInput.scss'; + +const cx = classNames.bind(styles); + +export const FilterInput = ({ filter, onFilter }) => { + const { filterName, options, value, condition, placeholder, title, withField, helpText } = filter; + + const onChangeOption = (newValue) => { + onFilter({ + [filterName]: { + ...filter, + ...(withField ? { condition: newValue } : { value: newValue }), + }, + }); + }; + + const onTextFieldChange = ({ target }) => { + if (helpText && !Number(target.value)) { + return; + } + + onFilter({ [filterName]: { ...filter, value: target.value } }); + }; + + const onClear = () => onFilter({ [filterName]: { ...filter, value: '' } }); + + return ( +
+ {title} +
+ + {withField && ( +
+ +
+ )} +
+
+ ); +}; + +FilterInput.propTypes = { + filter: PropTypes.object, + onFilter: PropTypes.func, +}; diff --git a/app/src/components/main/filterButton/filterContent/filterInput/filterInput.scss b/app/src/components/main/filterButton/filterContent/filterInput/filterInput.scss new file mode 100644 index 0000000000..4c37692b73 --- /dev/null +++ b/app/src/components/main/filterButton/filterContent/filterInput/filterInput.scss @@ -0,0 +1,60 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.filter-item { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 16px; + + .label { + font-size: 13px; + line-height: 20px; + color: $COLOR--almost-black; + font-family: $FONT-ROBOTO-MEDIUM; + } + + .container { + display: flex; + width: 100%; + gap: 8px; + + .dropdown { + min-width: 156px; + width: 156px; + } + + .input-field { + flex: 1; + } + } +} + +.with-help-text { + margin-bottom: 0; +} + +.input-field-container { + width: 100%; + + &:last-child span { + color: $COLOR--e-400; + } + + div.input-field { + width: 100%; + } +} diff --git a/app/src/components/main/filterButton/filterContent/filterInput/index.js b/app/src/components/main/filterButton/filterContent/filterInput/index.js new file mode 100644 index 0000000000..92bef2de3a --- /dev/null +++ b/app/src/components/main/filterButton/filterContent/filterInput/index.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { FilterInput } from './filterInput'; diff --git a/app/src/components/main/filterButton/filterContent/index.js b/app/src/components/main/filterButton/filterContent/index.js new file mode 100644 index 0000000000..16b3e405f8 --- /dev/null +++ b/app/src/components/main/filterButton/filterContent/index.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { FilterContent } from './filterContent'; diff --git a/app/src/components/main/filterButton/filterContent/messages.js b/app/src/components/main/filterButton/filterContent/messages.js new file mode 100644 index 0000000000..d798aa5f93 --- /dev/null +++ b/app/src/components/main/filterButton/filterContent/messages.js @@ -0,0 +1,24 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + clearAllFilters: { + id: 'ProjectsFilterPopover.clearAllFilters', + defaultMessage: 'Clear all filters', + }, +}); diff --git a/app/src/components/main/filterButton/index.jsx b/app/src/components/main/filterButton/index.jsx new file mode 100644 index 0000000000..446d829301 --- /dev/null +++ b/app/src/components/main/filterButton/index.jsx @@ -0,0 +1,27 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { FilterButton } from './filterButton'; +export { + LAST_RUN_DATE_FILTER_NAME, + LAUNCHES_FILTER_NAME, + TEAMMATES_FILTER_NAME, + FILTER_NAME, + getContainmentComparisons, + getRangeComparisons, + getTimeRange, +} from './constants'; +export { messages } from './messages'; diff --git a/app/src/components/main/filterButton/messages.js b/app/src/components/main/filterButton/messages.js new file mode 100644 index 0000000000..e7333803e5 --- /dev/null +++ b/app/src/components/main/filterButton/messages.js @@ -0,0 +1,68 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + any: { + id: 'FilterButton.any', + defaultMessage: 'Any', + }, + today: { + id: 'FilterButton.today', + defaultMessage: 'Today', + }, + last2days: { + id: 'FilterButton.last2days', + defaultMessage: 'Last 2 days', + }, + last7days: { + id: 'FilterButton.last7days', + defaultMessage: 'Last 7 days', + }, + last30days: { + id: 'FilterButton.last30days', + defaultMessage: 'Last 30 days', + }, + equals: { + id: 'FilterButton.equals', + defaultMessage: 'Equals', + }, + greaterOrEqual: { + id: 'FilterButton.greaterOrEqual', + defaultMessage: 'Greater or equal', + }, + lessOrEqual: { + id: 'FilterButton.lessOrEqual', + defaultMessage: 'Less or equal', + }, + notEqual: { + id: 'FilterButton.notEqual', + defaultMessage: 'Not equal', + }, + contains: { + id: 'FilterButton.contains', + defaultMessage: 'Contains', + }, + notContains: { + id: 'FilterButton.notContains', + defaultMessage: 'Not contains', + }, + helpText: { + id: 'FilterButton.helpText', + defaultMessage: 'Only digits are allowed', + }, +}); diff --git a/app/src/controllers/instance/events/utils.js b/app/src/controllers/instance/events/utils.js index 5e3825cb94..2d40807b47 100644 --- a/app/src/controllers/instance/events/utils.js +++ b/app/src/controllers/instance/events/utils.js @@ -57,5 +57,7 @@ export const getAppliedFilters = (filters, projectKey) => { }; }); - return { search_criterias: [...appliedFilters, projectIdFilterParam] }; + return { + search_criterias: [...appliedFilters, ...(projectKey ? [projectIdFilterParam] : [])], + }; }; diff --git a/app/src/controllers/instance/organizations/actionCreators.js b/app/src/controllers/instance/organizations/actionCreators.js index 46a7ea66f6..44eb3ea8f9 100644 --- a/app/src/controllers/instance/organizations/actionCreators.js +++ b/app/src/controllers/instance/organizations/actionCreators.js @@ -14,8 +14,12 @@ * limitations under the License. */ -import { FETCH_ORGANIZATIONS } from './constants'; +import { FETCH_ORGANIZATIONS, FETCH_FILTERED_ORGANIZATIONS } from './constants'; export const fetchOrganizationsAction = () => ({ type: FETCH_ORGANIZATIONS, }); + +export const fetchFilteredOrganizationsAction = () => ({ + type: FETCH_FILTERED_ORGANIZATIONS, +}); diff --git a/app/src/controllers/instance/organizations/constants.js b/app/src/controllers/instance/organizations/constants.js index 1e93837fdd..32431cb22f 100644 --- a/app/src/controllers/instance/organizations/constants.js +++ b/app/src/controllers/instance/organizations/constants.js @@ -18,7 +18,9 @@ import { PAGE_KEY, SIZE_KEY } from 'controllers/pagination'; export const NAMESPACE = 'organizations'; +// TODO: After joining the filter and the search, leave one constant export const FETCH_ORGANIZATIONS = 'fetchOrganizations'; +export const FETCH_FILTERED_ORGANIZATIONS = 'fetchFilteredOrganizations'; export const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; export const DEFAULT_LIMITATION = 20; export const initialPaginationState = { diff --git a/app/src/controllers/instance/organizations/index.js b/app/src/controllers/instance/organizations/index.js index b89052a05d..1763ffeded 100644 --- a/app/src/controllers/instance/organizations/index.js +++ b/app/src/controllers/instance/organizations/index.js @@ -14,8 +14,8 @@ * limitations under the License. */ -export { FETCH_ORGANIZATIONS } from './constants'; -export { fetchOrganizationsAction } from './actionCreators'; +export { FETCH_ORGANIZATIONS, FETCH_FILTERED_ORGANIZATIONS } from './constants'; +export { fetchOrganizationsAction, fetchFilteredOrganizationsAction } from './actionCreators'; export { organizationsReducer } from './reducer'; export { organizationsSelector, diff --git a/app/src/controllers/instance/organizations/sagas.js b/app/src/controllers/instance/organizations/sagas.js index 398cd30330..eecdfacf16 100644 --- a/app/src/controllers/instance/organizations/sagas.js +++ b/app/src/controllers/instance/organizations/sagas.js @@ -18,8 +18,9 @@ import { takeEvery, all, put, select } from 'redux-saga/effects'; import { URLS } from 'common/urls'; import { showDefaultErrorNotification } from 'controllers/notification'; import { fetchDataAction } from 'controllers/fetch'; +import { prepareQueryFilters } from 'components/filterEntities/utils'; import { querySelector } from './selectors'; -import { FETCH_ORGANIZATIONS, NAMESPACE } from './constants'; +import { FETCH_ORGANIZATIONS, FETCH_FILTERED_ORGANIZATIONS, NAMESPACE } from './constants'; function* fetchOrganizations() { try { @@ -35,6 +36,22 @@ function* watchFetchOrganizations() { yield takeEvery(FETCH_ORGANIZATIONS, fetchOrganizations); } +function* fetchFilteredOrganizations() { + const filtersParams = yield select(querySelector); + const data = prepareQueryFilters(filtersParams); + + yield put( + fetchDataAction(NAMESPACE)(URLS.organizationSearches(), { + method: 'post', + data, + }), + ); +} + +function* watchFetchFilteredProjects() { + yield takeEvery(FETCH_FILTERED_ORGANIZATIONS, fetchFilteredOrganizations); +} + export function* organizationsSagas() { - yield all([watchFetchOrganizations()]); + yield all([watchFetchOrganizations(), watchFetchFilteredProjects()]); } diff --git a/app/src/controllers/organization/projects/actionCreators.js b/app/src/controllers/organization/projects/actionCreators.js index d6117f8c2a..5c2fd07c9b 100644 --- a/app/src/controllers/organization/projects/actionCreators.js +++ b/app/src/controllers/organization/projects/actionCreators.js @@ -14,7 +14,12 @@ * limitations under the License. */ -import { CREATE_PROJECT, DELETE_PROJECT, FETCH_ORGANIZATION_PROJECTS } from './constants'; +import { + CREATE_PROJECT, + DELETE_PROJECT, + FETCH_ORGANIZATION_PROJECTS, + FETCH_FILTERED_PROJECTS, +} from './constants'; export const fetchOrganizationProjectsAction = (params) => { return { @@ -32,3 +37,7 @@ export const deleteProjectAction = (project) => ({ type: DELETE_PROJECT, payload: project, }); + +export const fetchFilteredProjectAction = () => ({ + type: FETCH_FILTERED_PROJECTS, +}); diff --git a/app/src/controllers/organization/projects/constants.js b/app/src/controllers/organization/projects/constants.js index c9f1abcb85..88d783b4d9 100644 --- a/app/src/controllers/organization/projects/constants.js +++ b/app/src/controllers/organization/projects/constants.js @@ -17,7 +17,9 @@ import { PAGE_KEY, SIZE_KEY } from 'controllers/pagination'; import { formatSortingString, SORTING_ASC } from 'controllers/sorting'; +// TODO: After joining the filter and the search, leave one constant export const FETCH_ORGANIZATION_PROJECTS = 'fetchOrganizationProjects'; +export const FETCH_FILTERED_PROJECTS = 'fetchFilteredProjects'; export const NAMESPACE = 'organizationProjects'; export const CREATE_PROJECT = 'createProject'; export const DELETE_PROJECT = 'deleteProject'; diff --git a/app/src/controllers/organization/projects/index.js b/app/src/controllers/organization/projects/index.js index 1c5865f409..b448d58f98 100644 --- a/app/src/controllers/organization/projects/index.js +++ b/app/src/controllers/organization/projects/index.js @@ -14,7 +14,7 @@ * limitations under the License. */ -export { fetchOrganizationProjectsAction } from './actionCreators'; +export { fetchOrganizationProjectsAction, fetchFilteredProjectAction } from './actionCreators'; export { projectsReducer } from './reducer'; export { projectsPaginationSelector, projectsSelector, loadingSelector } from './selectors'; export { projectsSagas } from './sagas'; @@ -26,4 +26,5 @@ export { DEFAULT_QUERY_PARAMS, FETCH_ORGANIZATION_PROJECTS, SORTING_KEY, + FETCH_FILTERED_PROJECTS, } from './constants'; diff --git a/app/src/controllers/organization/projects/sagas.js b/app/src/controllers/organization/projects/sagas.js index e06d9044fe..290d9801af 100644 --- a/app/src/controllers/organization/projects/sagas.js +++ b/app/src/controllers/organization/projects/sagas.js @@ -20,17 +20,19 @@ import { URLS } from 'common/urls'; import { fetch } from 'common/utils'; import { hideModalAction } from 'controllers/modal'; import { NOTIFICATION_TYPES, showNotification } from 'controllers/notification'; -import { fetchOrganizationBySlugAction } from '..'; -import { querySelector } from './selectors'; -import { activeOrganizationSelector } from '../selectors'; -import { fetchOrganizationProjectsAction } from './actionCreators'; +import { prepareQueryFilters } from 'components/filterEntities/utils'; import { CREATE_PROJECT, FETCH_ORGANIZATION_PROJECTS, ERROR_CODES, NAMESPACE, DELETE_PROJECT, + FETCH_FILTERED_PROJECTS, } from './constants'; +import { fetchOrganizationBySlugAction } from '..'; +import { querySelector } from './selectors'; +import { activeOrganizationIdSelector, activeOrganizationSelector } from '../selectors'; +import { fetchOrganizationProjectsAction } from './actionCreators'; function* fetchOrganizationProjects({ payload: organizationId }) { const query = yield select(querySelector); @@ -116,6 +118,29 @@ function* deleteProject({ payload: { projectId, projectName } }) { function* watchDeleteProject() { yield takeEvery(DELETE_PROJECT, deleteProject); } + +function* fetchFilteredProjects() { + const activeOrganizationId = yield select(activeOrganizationIdSelector); + const filtersParams = yield select(querySelector); + const data = prepareQueryFilters(filtersParams); + + yield put( + fetchDataAction(NAMESPACE)(URLS.organizationProjectsSearches(activeOrganizationId), { + method: 'post', + data, + }), + ); +} + +function* watchFetchFilteredProjects() { + yield takeEvery(FETCH_FILTERED_PROJECTS, fetchFilteredProjects); +} + export function* projectsSagas() { - yield all([watchFetchProjects(), watchCreateProject(), watchDeleteProject()]); + yield all([ + watchFetchProjects(), + watchCreateProject(), + watchDeleteProject(), + watchFetchFilteredProjects(), + ]); } diff --git a/app/src/controllers/organization/projects/selectors.js b/app/src/controllers/organization/projects/selectors.js index f88be72964..14e0ad37f1 100644 --- a/app/src/controllers/organization/projects/selectors.js +++ b/app/src/controllers/organization/projects/selectors.js @@ -16,8 +16,8 @@ import { createAlternativeQueryParametersSelector } from 'controllers/pages/selectors'; import { SORTING_ASC } from 'controllers/sorting'; -import { DEFAULT_PAGINATION, NAMESPACE, SORTING_KEY } from './constants'; import { organizationSelector } from '../selectors'; +import { DEFAULT_PAGINATION, NAMESPACE, SORTING_KEY } from './constants'; const domainSelector = (state) => organizationSelector(state).projects || {}; diff --git a/app/src/controllers/pages/selectors.js b/app/src/controllers/pages/selectors.js index efd7de301e..6ec1c1623d 100644 --- a/app/src/controllers/pages/selectors.js +++ b/app/src/controllers/pages/selectors.js @@ -17,7 +17,7 @@ import { createSelector } from 'reselect'; import { extractNamespacedQuery } from 'common/utils/routingUtils'; import { DEFAULT_PAGINATION, SIZE_KEY, PAGE_KEY } from 'controllers/pagination/constants'; -import { SORTING_KEY } from 'controllers/sorting/constants'; +import { SORTING_KEY, SORTING_ORDER_KEY } from 'controllers/sorting/constants'; import { getStorageItem } from 'common/utils/storageUtils'; import { activeProjectSelector, @@ -148,7 +148,7 @@ export const createAlternativeQueryParametersSelector = ({ sortingKey, namespace, }), - ({ [SIZE_KEY]: limit, [SORTING_KEY]: sort, [PAGE_KEY]: pageNumber, ...rest }) => { + ({ [SIZE_KEY]: limit, [SORTING_ORDER_KEY]: sort, [PAGE_KEY]: pageNumber, ...rest }) => { return { ...getAlternativePaginationAndSortParams(sort, limit, pageNumber), ...rest }; }, ); diff --git a/app/src/controllers/sorting/constants.js b/app/src/controllers/sorting/constants.js index 985b404dc6..ebd451eb17 100644 --- a/app/src/controllers/sorting/constants.js +++ b/app/src/controllers/sorting/constants.js @@ -17,3 +17,4 @@ export const SORTING_ASC = 'ASC'; export const SORTING_DESC = 'DESC'; export const SORTING_KEY = 'page.sort'; +export const SORTING_ORDER_KEY = 'order'; diff --git a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx index 7b5473d9f4..505c99a860 100644 --- a/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx +++ b/app/src/pages/inside/projectSettingsPageContainer/content/notifications/modals/addEditNotificationModal/addEditNotificationModal.jsx @@ -37,7 +37,7 @@ import { EMAIL } from 'common/constants/pluginNames'; import { FieldTextFlex } from 'componentLibrary/fieldTextFlex'; import { ruleField } from 'pages/inside/projectSettingsPageContainer/content/notifications/propTypes'; import { fetchProjectAction } from 'controllers/project/actionCreators'; -import { projectIdSelector } from 'controllers/pages'; +import { projectKeySelector } from 'controllers/project'; import { capitalizeWord } from '../util'; import { RecipientsContainer } from './recipientsContainer'; import { LaunchNamesContainer } from './launchNamesContainer'; @@ -215,14 +215,14 @@ const AddEditNotificationModal = ({ }) => { const { formatMessage } = useIntl(); const dispatch = useDispatch(); - const projectId = useSelector(projectIdSelector); + const projectKey = useSelector(projectKeySelector); const [isEditorShown, setShowEditor] = React.useState(data.notification.attributes.length > 0); const attributesValue = useSelector((state) => attributesValueSelector(state, ATTRIBUTES_FIELD_KEY)) ?? []; useEffect(() => { initialize(data.notification); - dispatch(fetchProjectAction(projectId, false)); + dispatch(fetchProjectAction(projectKey, false)); }, []); const caseOptions = [ diff --git a/app/src/pages/instance/organizationsPage/organizationsPage.jsx b/app/src/pages/instance/organizationsPage/organizationsPage.jsx index 57f0568501..394ce1c0db 100644 --- a/app/src/pages/instance/organizationsPage/organizationsPage.jsx +++ b/app/src/pages/instance/organizationsPage/organizationsPage.jsx @@ -49,6 +49,7 @@ export const OrganizationsPage = () => { const isOrganizationsLoading = useSelector(organizationsListLoadingSelector); const userId = useSelector(userIdSelector); const [searchValue, setSearchValue] = useState(null); + const [appliedFiltersCount, setAppliedFiltersCount] = useState(0); const isEmptyOrganizations = !isOrganizationsLoading && organizationsList.length === 0; const [isOpenTableView, setIsOpenTableView] = useState( getStorageItem(`${userId}_settings`)?.organizationsPanel === TABLE_VIEW, @@ -73,7 +74,7 @@ export const OrganizationsPage = () => { ); } - return searchValue === null ? ( + return searchValue === null && appliedFiltersCount === 0 ? ( { openPanelView={openPanelView} openTableView={openTableView} isOpenTableView={isOpenTableView} + appliedFiltersCount={appliedFiltersCount} + setAppliedFiltersCount={setAppliedFiltersCount} /> {isEmptyOrganizations ? ( getEmptyPageState() diff --git a/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/index.js b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/index.js new file mode 100644 index 0000000000..36998207a6 --- /dev/null +++ b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/index.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OrganizationsFilter } from './organizationsFilter'; diff --git a/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/messages.js b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/messages.js new file mode 100644 index 0000000000..43a51fc83e --- /dev/null +++ b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/messages.js @@ -0,0 +1,52 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + lastRunDate: { + id: 'OrganizationsFilter.lastRunDate', + defaultMessage: 'Last Run Date', + }, + lastRunDatePlaceholder: { + id: 'OrganizationsFilter.lastRunDatePlaceholder', + defaultMessage: 'Any', + }, + launches: { + id: 'OrganizationsFilter.launches', + defaultMessage: 'Launches', + }, + launchesPlaceholder: { + id: 'OrganizationsFilter.launchesPlaceholder', + defaultMessage: 'Enter the number of launches', + }, + users: { + id: 'OrganizationsFilter.users', + defaultMessage: 'Users', + }, + usersPlaceholder: { + id: 'OrganizationsFilter.usersPlaceholder', + defaultMessage: 'Enter the number of members', + }, + name: { + id: 'OrganizationsFilter.name', + defaultMessage: 'Organization name', + }, + namePlaceholder: { + id: 'OrganizationsFilter.namePlaceholder', + defaultMessage: 'Enter part of the name', + }, +}); diff --git a/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/organizationsFilter.jsx b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/organizationsFilter.jsx new file mode 100644 index 0000000000..2eb3126ca6 --- /dev/null +++ b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsFilter/organizationsFilter.jsx @@ -0,0 +1,112 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import { useDispatch } from 'react-redux'; +import { + LAUNCHES_FILTER_NAME, + FILTER_NAME, + TEAMMATES_FILTER_NAME, + LAST_RUN_DATE_FILTER_NAME, + getContainmentComparisons, + getRangeComparisons, + getTimeRange, + messages as helpMessage, +} from 'components/main/filterButton'; +import { FilterButton } from 'components/main/filterButton/filterButton'; +import { fetchFilteredOrganizationsAction } from 'controllers/instance/organizations'; +import { CONDITION_BETWEEN } from 'components/filterEntities/constants'; +import { messages } from './messages'; + +export const OrganizationsFilter = ({ + entities, + onFilterChange, + appliedFiltersCount, + setAppliedFiltersCount, +}) => { + const { formatMessage } = useIntl(); + const dispatch = useDispatch(); + + const timeRange = getTimeRange(formatMessage); + const rangeComparisons = getRangeComparisons(formatMessage); + const containmentComparisons = getContainmentComparisons(formatMessage); + + const filters = { + [LAST_RUN_DATE_FILTER_NAME]: { + filterName: LAST_RUN_DATE_FILTER_NAME, + value: timeRange[0].value, + title: formatMessage(messages.lastRunDate), + options: timeRange, + condition: CONDITION_BETWEEN.toUpperCase(), + placeholder: formatMessage(messages.lastRunDatePlaceholder), + }, + [LAUNCHES_FILTER_NAME]: { + filterName: LAUNCHES_FILTER_NAME, + value: '', + title: formatMessage(messages.launches), + placeholder: formatMessage(messages.launchesPlaceholder), + options: rangeComparisons, + condition: rangeComparisons[0].value, + helpText: formatMessage(helpMessage.helpText), + withField: true, + }, + [TEAMMATES_FILTER_NAME]: { + filterName: TEAMMATES_FILTER_NAME, + value: '', + title: formatMessage(messages.users), + placeholder: formatMessage(messages.usersPlaceholder), + options: rangeComparisons, + condition: rangeComparisons[0].value, + helpText: formatMessage(helpMessage.helpText), + withField: true, + }, + [FILTER_NAME]: { + filterName: FILTER_NAME, + value: '', + title: formatMessage(messages.name), + placeholder: formatMessage(messages.namePlaceholder), + options: containmentComparisons, + condition: containmentComparisons[0].value, + withField: true, + }, + }; + + return ( + dispatch(fetchFilteredOrganizationsAction())} + /> + ); +}; + +OrganizationsFilter.propTypes = { + entities: PropTypes.objectOf( + PropTypes.shape({ + filter_key: PropTypes.string, + value: PropTypes.string, + condition: PropTypes.string, + }), + ), + onFilterChange: PropTypes.func, + appliedFiltersCount: PropTypes.number, + setAppliedFiltersCount: PropTypes.func, + defaultFilters: PropTypes.object, +}; diff --git a/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsPageHeader.jsx b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsPageHeader.jsx index 9ffd5e96aa..ce3692bae0 100644 --- a/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsPageHeader.jsx +++ b/app/src/pages/instance/organizationsPage/organizationsPageHeader/organizationsPageHeader.jsx @@ -15,21 +15,22 @@ */ import React from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import Parser from 'html-react-parser'; import classNames from 'classnames/bind'; -import filterIcon from 'common/img/newIcons/filters-outline-inline.svg'; +import { BaseIconButton } from '@reportportal/ui-kit'; import { useIntl } from 'react-intl'; import { SearchField } from 'components/fields/searchField'; import { SEARCH_KEY } from 'controllers/organization/projects/constants'; import { withFilter } from 'controllers/filter'; import { NAMESPACE } from 'controllers/instance/organizations/constants'; -import { useSelector } from 'react-redux'; import { organizationsListLoadingSelector } from 'controllers/instance/organizations'; import { ORGANIZATION_PAGE_EVENTS } from 'components/main/analytics/events/ga4Events/organizationsPageEvents'; -import { BaseIconButton } from '@reportportal/ui-kit'; +import { withFilterEntitiesURL } from 'components/filterEntities/containers'; import PanelViewIcon from '../img/panel-view-inline.svg'; import TableViewIcon from '../img/table-view-inline.svg'; +import { OrganizationsFilter } from './organizationsFilter'; import { messages } from '../messages'; import styles from './organizationsPageHeader.scss'; @@ -39,6 +40,8 @@ const SearchFieldWithFilter = withFilter({ filterKey: SEARCH_KEY, namespace: NAM SearchField, ); +const FiltersFields = withFilterEntitiesURL(NAMESPACE)(OrganizationsFilter); + export const OrganizationsPageHeader = ({ isEmpty, searchValue, @@ -46,6 +49,8 @@ export const OrganizationsPageHeader = ({ openPanelView, openTableView, isOpenTableView, + appliedFiltersCount, + setAppliedFiltersCount, }) => { const { formatMessage } = useIntl(); const projectsLoading = useSelector(organizationsListLoadingSelector); @@ -64,7 +69,11 @@ export const OrganizationsPageHeader = ({ placeholder={formatMessage(messages.searchPlaceholder)} event={ORGANIZATION_PAGE_EVENTS.SEARCH_ORGANIZATION_FIELD} /> - {Parser(filterIcon)} + { const isProjectsEmpty = !projectsLoading && projects.length === 0; const [searchValue, setSearchValue] = useState(null); + const [appliedFiltersCount, setAppliedFiltersCount] = useState(0); const showCreateProjectModal = () => { dispatch( @@ -91,7 +92,7 @@ export const OrganizationProjectsPage = () => { }; const getEmptyPageState = () => { - return searchValue === null ? ( + return searchValue === null && appliedFiltersCount === 0 ? ( { onCreateProject={showCreateProjectModal} searchValue={searchValue} setSearchValue={setSearchValue} + appliedFiltersCount={appliedFiltersCount} + setAppliedFiltersCount={setAppliedFiltersCount} /> {renderContent()} diff --git a/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/index.js b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/index.js new file mode 100644 index 0000000000..e788469ec0 --- /dev/null +++ b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/index.js @@ -0,0 +1,17 @@ +/*! + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ProjectsFilter } from './projectsFilter'; diff --git a/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/messages.js b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/messages.js new file mode 100644 index 0000000000..9911e14050 --- /dev/null +++ b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/messages.js @@ -0,0 +1,52 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + lastRunDate: { + id: 'ProjectsFilter.lastRunDate', + defaultMessage: 'Last Run Date', + }, + lastRunDatePlaceholder: { + id: 'ProjectsFilter.lastRunDatePlaceholder', + defaultMessage: 'Any', + }, + launches: { + id: 'ProjectsFilter.launches', + defaultMessage: 'Launches', + }, + launchesPlaceholder: { + id: 'ProjectsFilter.launchesPlaceholder', + defaultMessage: 'Enter the number of launches', + }, + users: { + id: 'ProjectsFilter.users', + defaultMessage: 'Teammates', + }, + usersPlaceholder: { + id: 'ProjectsFilter.usersPlaceholder', + defaultMessage: 'Enter the number of members', + }, + name: { + id: 'ProjectsFilter.name', + defaultMessage: 'Project Name', + }, + namePlaceholder: { + id: 'ProjectsFilter.namePlaceholder', + defaultMessage: 'Enter part of the name', + }, +}); diff --git a/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/projectsFilter.jsx b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/projectsFilter.jsx new file mode 100644 index 0000000000..5c0d918cf1 --- /dev/null +++ b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsFilter/projectsFilter.jsx @@ -0,0 +1,112 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useIntl } from 'react-intl'; +import { useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + LAUNCHES_FILTER_NAME, + FILTER_NAME, + TEAMMATES_FILTER_NAME, + LAST_RUN_DATE_FILTER_NAME, + getContainmentComparisons, + getRangeComparisons, + getTimeRange, + messages as helpMessage, +} from 'components/main/filterButton'; +import { FilterButton } from 'components/main/filterButton/filterButton'; +import { fetchFilteredProjectAction } from 'controllers/organization/projects'; +import { CONDITION_BETWEEN } from 'components/filterEntities/constants'; +import { messages } from './messages'; + +export const ProjectsFilter = ({ + entities, + onFilterChange, + appliedFiltersCount, + setAppliedFiltersCount, +}) => { + const { formatMessage } = useIntl(); + const dispatch = useDispatch(); + + const timeRange = getTimeRange(formatMessage); + const rangeComparisons = getRangeComparisons(formatMessage); + const containmentComparisons = getContainmentComparisons(formatMessage); + + const filters = { + [LAST_RUN_DATE_FILTER_NAME]: { + filterName: LAST_RUN_DATE_FILTER_NAME, + value: timeRange[0].value, + title: formatMessage(messages.lastRunDate), + options: timeRange, + condition: CONDITION_BETWEEN.toUpperCase(), + placeholder: formatMessage(messages.lastRunDatePlaceholder), + }, + [LAUNCHES_FILTER_NAME]: { + filterName: LAUNCHES_FILTER_NAME, + value: '', + title: formatMessage(messages.launches), + placeholder: formatMessage(messages.launchesPlaceholder), + options: rangeComparisons, + condition: rangeComparisons[0].value, + helpText: formatMessage(helpMessage.helpText), + withField: true, + }, + [TEAMMATES_FILTER_NAME]: { + filterName: TEAMMATES_FILTER_NAME, + value: '', + title: formatMessage(messages.users), + placeholder: formatMessage(messages.usersPlaceholder), + options: rangeComparisons, + condition: rangeComparisons[0].value, + helpText: formatMessage(helpMessage.helpText), + withField: true, + }, + [FILTER_NAME]: { + filterName: FILTER_NAME, + value: '', + title: formatMessage(messages.name), + placeholder: formatMessage(messages.namePlaceholder), + options: containmentComparisons, + condition: containmentComparisons[0].value, + withField: true, + }, + }; + + return ( + dispatch(fetchFilteredProjectAction())} + /> + ); +}; + +ProjectsFilter.propTypes = { + entities: PropTypes.objectOf( + PropTypes.shape({ + filter_key: PropTypes.string, + value: PropTypes.string, + condition: PropTypes.string, + }), + ), + onFilterChange: PropTypes.func, + appliedFiltersCount: PropTypes.number, + setAppliedFiltersCount: PropTypes.func, + defaultFilters: PropTypes.object, +}; diff --git a/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.jsx b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.jsx index ab181c6aff..1840c40299 100644 --- a/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.jsx +++ b/app/src/pages/organization/organizationProjectsPage/projectsPageHeader/projectsPageHeader.jsx @@ -21,17 +21,18 @@ import Parser from 'html-react-parser'; import { Button, PlusIcon } from '@reportportal/ui-kit'; import classNames from 'classnames/bind'; import { ORGANIZATIONS_PAGE } from 'controllers/pages'; -import filterIcon from 'common/img/newIcons/filters-outline-inline.svg'; import { Breadcrumbs } from 'componentLibrary/breadcrumbs'; import { activeOrganizationSelector } from 'controllers/organization'; import { loadingSelector } from 'controllers/organization/projects'; import { SearchField } from 'components/fields/searchField'; import { SEARCH_KEY, NAMESPACE } from 'controllers/organization/projects/constants'; import { withFilter } from 'controllers/filter'; +import { withFilterEntitiesURL } from 'components/filterEntities/containers'; import projectsIcon from './img/projects-inline.svg'; -import styles from './projectsPageHeader.scss'; -import { messages } from '../messages'; import userIcon from './img/user-inline.svg'; +import { ProjectsFilter } from './projectsFilter'; +import { messages } from '../messages'; +import styles from './projectsPageHeader.scss'; const cx = classNames.bind(styles); @@ -39,11 +40,15 @@ const SearchFieldWithFilter = withFilter({ filterKey: SEARCH_KEY, namespace: NAM SearchField, ); +const FiltersFields = withFilterEntitiesURL(NAMESPACE)(ProjectsFilter); + export const ProjectsPageHeader = ({ hasPermission, onCreateProject, searchValue, setSearchValue, + appliedFiltersCount, + setAppliedFiltersCount, }) => { const { formatMessage } = useIntl(); const organization = useSelector(activeOrganizationSelector); @@ -95,7 +100,11 @@ export const ProjectsPageHeader = ({ setSearchValue={setSearchValue} placeholder={formatMessage(messages.searchPlaceholder)} /> - {Parser(filterIcon)} + )} {isNotEmpty && hasPermission && ( @@ -114,6 +123,8 @@ ProjectsPageHeader.propTypes = { onCreateProject: PropTypes.func.isRequired, searchValue: PropTypes.string || null, setSearchValue: PropTypes.func.isRequired, + appliedFiltersCount: PropTypes.number, + setAppliedFiltersCount: PropTypes.func, }; ProjectsPageHeader.defaultProps = { diff --git a/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx b/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx index bfcff49831..63c765e609 100644 --- a/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx +++ b/app/src/pages/organization/projectTeamPage/projectTeamPageHeader/projectTeamPageHeader.jsx @@ -17,15 +17,13 @@ import React from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; -import Parser from 'html-react-parser'; import classNames from 'classnames/bind'; -import { Button } from '@reportportal/ui-kit'; +import { Button, FilterOutlineIcon } from '@reportportal/ui-kit'; import { useIntl } from 'react-intl'; import { projectMembersSelector, projectNameSelector } from 'controllers/project'; import { SearchField } from 'components/fields/searchField'; import { NAMESPACE, SEARCH_KEY } from 'controllers/members/constants'; import { withFilter } from 'controllers/filter'; -import filterIcon from 'common/img/newIcons/filters-outline-inline.svg'; import { PROJECT_PAGE_EVENTS } from 'components/main/analytics/events/ga4Events/projectPageEvents'; import { activeOrganizationNameSelector } from 'controllers/organization'; import { messages } from '../../common/membersPage/membersPageHeader/messages'; @@ -68,7 +66,9 @@ export const ProjectTeamPageHeader = ({ placeholder={formatMessage(messages.searchPlaceholder)} event={PROJECT_PAGE_EVENTS.SEARCH_PROJECT_TEAM_FIELD} /> - {Parser(filterIcon)} + + + {hasPermission && (