Skip to content

Commit

Permalink
Merge pull request #1766 from jdi-testing/issue_950-refactoring-filter
Browse files Browse the repository at this point in the history
Issue 950: refactoring filter
  • Loading branch information
Iogsotot authored Aug 14, 2024
2 parents d2089e4 + 7b77c28 commit 632408b
Show file tree
Hide file tree
Showing 26 changed files with 435 additions and 203 deletions.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "JDN — Page Object Generator",
"description": "JDN – helps Test Automation Engineer to create Page Objects in the test automation framework and speed up test development",
"devtools_page": "index.html",
"version": "3.16.3",
"version": "3.16.4",
"icons": {
"128": "icon128.png"
},
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jdn-ai-chrome-extension",
"version": "3.16.3",
"version": "3.16.4",
"description": "jdn-ai chrome extension",
"scripts": {
"start": "webpack --watch --env devenv",
Expand Down
7 changes: 5 additions & 2 deletions src/__tests__/generationClassesMap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ const optionsMUI = [
'Breadcrumbs',
'Button',
'ButtonGroup',
'Card',
'Checkbox',
'Chip',
'Dialog',
'Drawer',
'Link',
'List',
'Menu',
'Modal',
'ProgressBar',
'RadioButtons',
'Select',
Expand All @@ -37,6 +39,7 @@ const optionsHTML5 = [
'DateTimeSelector',
'Dropdown',
'FileInput',
'Form',
'Label',
'Link',
'MultiSelector',
Expand Down Expand Up @@ -74,11 +77,11 @@ describe('Get JDI class by predicted label', () => {
expect(getJdiClassName(undefined, ElementLibrary.MUI)).toBe('UIElement (undefined)');
});

test('get types', () => {
test('get types MUI', () => {
expect(getTypesMenuOptions(ElementLibrary.MUI)).toStrictEqual(optionsMUI);
});

test('get types', () => {
test('get types HTML5', () => {
expect(getTypesMenuOptions(ElementLibrary.HTML5)).toStrictEqual(optionsHTML5);
});
});
3 changes: 3 additions & 0 deletions src/common/utils/isEmptyObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isEmptyObject = (obj: Record<string, any>): boolean => {
return Object.keys(obj).length === 0 && obj.constructor === Object;
};
1 change: 0 additions & 1 deletion src/common/utils/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// keys
export enum LocalStorageKey {
IsOnboardingPassed = 'JDN_IS_ONBOARDING_PASSED',
AnnotationType = 'JDN_ANNOTATION_TYPE',
Expand Down
101 changes: 66 additions & 35 deletions src/features/filter/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import React, { ChangeEvent, useMemo, useState } from 'react';
import { Badge, Button, Checkbox, Divider, Dropdown, Input, Switch, Typography } from 'antd';
import { SwitchChangeEventHandler } from 'antd/lib/switch';
import { useDispatch, useSelector } from 'react-redux';
import { selectCurrentPageObject } from '../pageObjects/selectors/pageObjects.selectors';
import { ElementClass } from '../locators/types/generationClasses.types';
import { FilterHeader } from './components/FilterHeader';
import { selectDetectedClassesFilter, selectIfSelectedAll, selectIsFiltered } from './filter.selectors';
import { selectDetectedClassesFilter, selectIsDefaultSetOn, selectIsFiltered } from './filter.selectors';
import { toggleClassFilter } from './reducers/toggleClassFilter.thunk';
import { toggleClassFilterAll } from './reducers/toggleClassFilterAll.thunk';
import { convertFilterToArr } from './utils/filterSet';
import { convertFilterToArr, mapJDIclassesToFilter } from './utils/filterSet';
import { FilterIcon } from './components/shared/FilterIcon';
import { AppDispatch } from '../../app/store/store';
import { AppDispatch, RootState } from '../../app/store/store';
import { areAllValuesFalse } from '../locators/utils/helpers';
import { clearFilters, setDefaultFilterSetOff, setDefaultFilterSetOn, setFilter } from './filter.slice';
import { getLocalStorage, LocalStorageKey } from '../../common/utils/localStorage';
import { defaultFilters } from './utils/defaultFilters';
import { isEmptyObject } from '../../common/utils/isEmptyObject';

export const Filter = () => {
const pageObject = useSelector(selectCurrentPageObject);
if (!pageObject) return null;

const [searchTerm, setSearchTerm] = useState<string>('');
const [open, setOpen] = useState(false);
const pageObject = useSelector(selectCurrentPageObject);

const dispatch = useDispatch<AppDispatch>();

const classFilter = useSelector(selectDetectedClassesFilter);
const areSelectedAll = useSelector(selectIfSelectedAll);

const classFilterArr = useMemo(() => convertFilterToArr(classFilter, searchTerm), [classFilter, searchTerm]);

const isFiltered = useSelector(selectIsFiltered);

const handleFilterChange = (key: string, oldValue: boolean) => () => {
if (!pageObject) return;
dispatch(setDefaultFilterSetOff({ pageObjectId: pageObject.id }));
dispatch(
toggleClassFilter({
pageObjectId: pageObject.id,
Expand All @@ -35,6 +41,7 @@ export const Filter = () => {
}),
);
};

const menuItems = {
items: classFilterArr.map(([key, value]) => {
return {
Expand All @@ -52,51 +59,70 @@ export const Filter = () => {
setSearchTerm(event.target.value);
};

const handleSelectAllChange: SwitchChangeEventHandler = (checked) => {
if (!pageObject) return;
dispatch(
toggleClassFilterAll({
pageObjectId: pageObject.id,
library: pageObject.library,
value: checked,
}),
);
const clearAllFilers: React.MouseEventHandler<HTMLElement> = () => {
dispatch(clearFilters({ pageObjectId: pageObject.id, library: pageObject.library, isDefaultSetOn: false }));
dispatch(setDefaultFilterSetOff({ pageObjectId: pageObject.id }));
};

const handleToggleFilterOpen = () => {
setOpen((prev) => !prev);
};

const renderFilterButton = useMemo(() => {
// uncomment for issue 950
// const usedFiltersCount = classFilterArr.filter((subArray) => subArray.includes(false)).length;
const isFilterClear = areAllValuesFalse(classFilterArr);
const usedFiltersCount = classFilterArr.filter((subArray) => subArray.includes(true)).length;

return (
<Button
className="jdn__filter_filter-button"
type="link"
onClick={handleToggleFilterOpen}
icon={
isFiltered ? (
// uncomment for issue 950
// <Badge count={usedFiltersCount} color="blue" size="small" offset={[2, 2]}>
// <FilterIcon />
// </Badge>
<Badge dot={true} color="blue" offset={[1, 4]}>
<div className="jdn__filter_filter-button" onClick={handleToggleFilterOpen}>
<span className="jdn__filter_filter-button_icon">
{isFiltered ? (
<Badge count={isFilterClear ? 0 : usedFiltersCount} color="blue" size="small" offset={[2, 2]}>
<FilterIcon />
</Badge>
) : (
<FilterIcon />
)
}
/>
)}
</span>
Filter
</div>
);
}, [isFiltered, classFilterArr]);

const handleCloseFilter = () => {
setOpen(false);
};

const savedFilters = getLocalStorage(LocalStorageKey.Filter);

const isDefaultSetOn = useSelector((state: RootState) => selectIsDefaultSetOn(state, pageObject.id));

const defaultSetToggle = () => {
const prevFilterState = savedFilters;
const prevDefaultSetToggleState = isDefaultSetOn;
const library = pageObject.library;
const isSavedFiltersForCurrentLibrary = prevFilterState[library] && !isEmptyObject(prevFilterState[library]);

if (prevDefaultSetToggleState) {
if (isSavedFiltersForCurrentLibrary) {
dispatch(
setFilter({ pageObjectId: pageObject.id, JDIclassFilter: prevFilterState[library], isDefaultSetOn: false }),
);
} else if (!isSavedFiltersForCurrentLibrary) {
dispatch(clearFilters({ pageObjectId: pageObject.id, library, isDefaultSetOn: false }));
}
dispatch(setDefaultFilterSetOff({ pageObjectId: pageObject.id })); // переключаем тоггл в false
} else if (!prevDefaultSetToggleState) {
dispatch(
setFilter({
pageObjectId: pageObject.id,
JDIclassFilter: mapJDIclassesToFilter(library, defaultFilters[library]),
isDefaultSetOn: true,
}),
);
dispatch(setDefaultFilterSetOn({ pageObjectId: pageObject.id })); // переключаем тоггл в true
}
};

return (
<Dropdown
menu={menuItems}
Expand All @@ -109,10 +135,15 @@ export const Filter = () => {
</div>
<div className="jdn__filter_dropdown_scroll">
<div className="jdn__filter_dropdown_control">
<Switch size="small" checked={areSelectedAll} onChange={handleSelectAllChange} />
<Typography.Text> Select all</Typography.Text>
<Switch size="small" checked={isDefaultSetOn} onChange={defaultSetToggle} />
<Typography.Text> Default set</Typography.Text>
</div>
{menu}
<div className="jdn__filter_dropdown_control">
<Button type="link" onClick={clearAllFilers}>
Clear
</Button>
</div>
</div>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/features/filter/components/shared/FilterIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import { Funnel } from '@phosphor-icons/react';

export const FilterIcon = (): React.ReactElement => <Funnel size={14} color="#8C8C8C" />;
export const FilterIcon = (): React.ReactElement => <Funnel size={14} />;
30 changes: 21 additions & 9 deletions src/features/filter/filter.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { selectCurrentPageObject, selectPageObjById } from '../pageObjects/selec
import { PageObjectId } from '../pageObjects/types/pageObjectSlice.types';
import { defaultLibrary, ElementClass } from '../locators/types/generationClasses.types';
import { Filter, FilterKey } from './types/filter.types';

import { jdiClassFilterInit } from './utils/filterSet';
import { selectLocatorById } from '../locators/selectors/locators.selectors';
import { isNil } from 'lodash';
import { hasFalseValue } from './utils/hasFalseValue';
import { jdiClassFilterInit } from './utils/filterSet';

export const filterAdapter = createEntityAdapter<Filter>({
selectId: (filter) => filter.pageObjectId,
sortComparer: false,
});

export const { selectAll: simpleSelectFilters, selectById: simpleSelectFilterById } = filterAdapter.getSelectors();
Expand Down Expand Up @@ -39,17 +39,29 @@ export const selectDetectedClassesFilter = createSelector(
(state: RootState) => state,
(pageObj, state) => {
const classFilterPO = selectClassFilterByPO(state, pageObj?.id);

if (pageObj?.locators) {
const locatorType = new Set(pageObj?.locators.map((locatorId) => selectLocatorById(state, locatorId)?.type));
const locatorType = new Set(
pageObj.locators.map((locatorId) => {
const locator = selectLocatorById(state, locatorId);
return locator?.type;
}),
);

return Object.entries(classFilterPO).reduce(
(result: Record<ElementClass, boolean>, entry) => {
const [key, value] = entry;
locatorType.has(key as ElementClass) ? (result[key as ElementClass] = value) : null;

if (locatorType.has(key as ElementClass)) {
result[key as ElementClass] = value;
}

return result;
},
{} as Record<ElementClass, boolean>,
);
}

return classFilterPO;
},
);
Expand All @@ -63,11 +75,6 @@ export const selectAvailableClasses = createSelector(
},
);

export const selectIfSelectedAll = createSelector(selectDetectedClassesFilter, (classFilter) => {
const arr = Object.entries(classFilter);
return !arr.some(([, value]) => !value);
});

export const selectIfUnselectedAll = createSelector(selectDetectedClassesFilter, (classFilter) => {
const arr = Object.entries(classFilter);
return arr.every(([, value]) => !value);
Expand All @@ -76,3 +83,8 @@ export const selectIfUnselectedAll = createSelector(selectDetectedClassesFilter,
export const selectIsFiltered = createSelector(selectDetectedClassesFilter, (classFilter) =>
hasFalseValue(classFilter),
);

export const selectIsDefaultSetOn = createSelector(
(state: RootState, pageObjectId: PageObjectId) => selectFilterById(state, pageObjectId),
(filter) => (filter ? filter.isDefaultSetOn ?? false : false),
);
46 changes: 43 additions & 3 deletions src/features/filter/filter.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { Filter, FilterKey } from './types/filter.types';
import { filterAdapter } from './filter.selectors';
import { toggleClassFilterReducer } from './reducers/toggleClassFilter.thunk';
import { toggleClassFilterAllReducer } from './reducers/toggleClassFilterAll.thunk';
import { getLocalStorage, LocalStorageKey, setLocalStorage } from '../../common/utils/localStorage';
import { ElementLibrary } from '../locators/types/generationClasses.types';
import { mapJDIclassesToFilter } from './utils/filterSet';

const filterSlice = createSlice({
name: 'filter',
Expand All @@ -16,18 +19,55 @@ const filterSlice = createSlice({
const { pageObjectIds } = payload;
filterAdapter.removeMany(state, pageObjectIds);
},
clearFilters(
state,
{ payload }: PayloadAction<{ pageObjectId: PageObjectId; library: ElementLibrary; isDefaultSetOn: boolean }>,
) {
const { pageObjectId, library, isDefaultSetOn } = payload;
if (!library) return;
const savedFilters = getLocalStorage(LocalStorageKey.Filter);

filterAdapter.upsertOne(state, {
pageObjectId,
[FilterKey.JDIclassFilter]: mapJDIclassesToFilter(library),
isDefaultSetOn,
});

if (savedFilters && savedFilters[library]) {
delete savedFilters[library];
}

setLocalStorage(LocalStorageKey.Filter, savedFilters);
},
setFilter(state, { payload }: PayloadAction<Filter>) {
const { pageObjectId, JDIclassFilter } = payload;
const { pageObjectId, JDIclassFilter, isDefaultSetOn } = payload;
filterAdapter.upsertOne(state, {
pageObjectId,
[FilterKey.JDIclassFilter]: { ...JDIclassFilter },
isDefaultSetOn,
});
},
setDefaultFilterSetOn(state, { payload }: PayloadAction<{ pageObjectId: PageObjectId }>) {
const { pageObjectId } = payload;
const existingFilter = state.entities[pageObjectId];
if (existingFilter) {
existingFilter.isDefaultSetOn = true;
}
},
setDefaultFilterSetOff(state, { payload }: PayloadAction<{ pageObjectId: PageObjectId }>) {
const { pageObjectId } = payload;
const existingFilter = state.entities[pageObjectId];
if (existingFilter) {
existingFilter.isDefaultSetOn = false;
}
},
},
extraReducers: (builder) => {
toggleClassFilterReducer(builder), toggleClassFilterAllReducer(builder);
toggleClassFilterReducer(builder);
toggleClassFilterAllReducer(builder);
},
});

export default filterSlice.reducer;
export const { removeAll, removeFilters, setFilter } = filterSlice.actions;
export const { removeAll, removeFilters, setFilter, setDefaultFilterSetOn, setDefaultFilterSetOff, clearFilters } =
filterSlice.actions;
4 changes: 2 additions & 2 deletions src/features/filter/reducers/toggleClassFilter.thunk.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { filterAdapter, simpleSelectFilterById } from '../filter.selectors';
import { RootState } from '../../../app/store/store';
import { FilterKey, Filter, ClassFilterValue } from '../types/filter.types';
import { ClassFilterValue, Filter, FilterKey } from '../types/filter.types';
import { jdiClassFilterInit } from '../utils/filterSet';
import { PageObjectId } from '../../pageObjects/types/pageObjectSlice.types';
import { ElementClass, ElementLibrary } from '../../locators/types/generationClasses.types';
import { LocalStorageKey, setLocalStorage, getLocalStorage } from '../../../common/utils/localStorage';
import { getLocalStorage, LocalStorageKey, setLocalStorage } from '../../../common/utils/localStorage';

interface toggleClassFilterPayload {
pageObjectId: PageObjectId;
Expand Down
Loading

0 comments on commit 632408b

Please sign in to comment.