@allurereport/web-awesome 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/multi/173.app-79c65c7bff941abcbc51.js +1 -0
  2. package/dist/multi/174.app-79c65c7bff941abcbc51.js +1 -0
  3. package/dist/multi/252.app-79c65c7bff941abcbc51.js +1 -0
  4. package/dist/multi/282.app-79c65c7bff941abcbc51.js +1 -0
  5. package/dist/multi/29.app-79c65c7bff941abcbc51.js +1 -0
  6. package/dist/multi/416.app-79c65c7bff941abcbc51.js +1 -0
  7. package/dist/multi/527.app-79c65c7bff941abcbc51.js +1 -0
  8. package/dist/multi/600.app-79c65c7bff941abcbc51.js +1 -0
  9. package/dist/multi/605.app-79c65c7bff941abcbc51.js +1 -0
  10. package/dist/multi/638.app-79c65c7bff941abcbc51.js +1 -0
  11. package/dist/multi/672.app-79c65c7bff941abcbc51.js +1 -0
  12. package/dist/multi/686.app-79c65c7bff941abcbc51.js +1 -0
  13. package/dist/multi/725.app-79c65c7bff941abcbc51.js +1 -0
  14. package/dist/multi/741.app-79c65c7bff941abcbc51.js +1 -0
  15. package/dist/multi/755.app-79c65c7bff941abcbc51.js +1 -0
  16. package/dist/multi/894.app-79c65c7bff941abcbc51.js +1 -0
  17. package/dist/multi/91.app-79c65c7bff941abcbc51.js +1 -0
  18. package/dist/multi/943.app-79c65c7bff941abcbc51.js +1 -0
  19. package/dist/multi/980.app-79c65c7bff941abcbc51.js +1 -0
  20. package/dist/multi/app-79c65c7bff941abcbc51.js +2 -0
  21. package/dist/multi/{app-9931797d1602fc52db5b.js.LICENSE.txt → app-79c65c7bff941abcbc51.js.LICENSE.txt} +7 -0
  22. package/dist/multi/manifest.json +21 -21
  23. package/dist/multi/styles-9e390bad7ce54a807a8e.css +49 -0
  24. package/dist/single/app-3ca67f29d0f1166c08ca.js +2 -0
  25. package/dist/single/{app-6199dc1c2fd3bddc2526.js.LICENSE.txt → app-3ca67f29d0f1166c08ca.js.LICENSE.txt} +7 -0
  26. package/dist/single/manifest.json +1 -1
  27. package/package.json +8 -8
  28. package/src/assets/scss/palette.scss +102 -102
  29. package/src/assets/scss/vars.scss +3 -0
  30. package/src/components/BaseLayout/index.tsx +25 -21
  31. package/src/components/BaseLayout/styles.scss +1 -0
  32. package/src/components/Charts/index.tsx +5 -2
  33. package/src/components/Footer/FooterVersion.tsx +14 -8
  34. package/src/components/Header/index.tsx +9 -7
  35. package/src/components/HeaderControls/index.tsx +5 -2
  36. package/src/components/MainReport/styles.scss +1 -0
  37. package/src/components/Metadata/index.tsx +24 -7
  38. package/src/components/ReportBody/HeaderActions.tsx +4 -13
  39. package/src/components/ReportBody/SortBy.tsx +28 -17
  40. package/src/components/ReportBody/index.tsx +12 -17
  41. package/src/components/ReportBody/styles.scss +4 -1
  42. package/src/components/ReportFilters/BaseFilters.tsx +345 -0
  43. package/src/components/ReportFilters/RetryFlaky.tsx +29 -0
  44. package/src/components/ReportFilters/TagsFilter.tsx +41 -0
  45. package/src/components/ReportFilters/TransitionFilter.tsx +49 -0
  46. package/src/components/ReportFilters/index.tsx +44 -0
  47. package/src/components/ReportFilters/styles.scss +55 -0
  48. package/src/components/ReportSearch/index.tsx +29 -0
  49. package/src/components/ReportTabs/index.tsx +37 -0
  50. package/src/components/SectionPicker/index.tsx +1 -1
  51. package/src/components/SplitLayout/index.tsx +7 -5
  52. package/src/components/TestResult/TestStepsEmpty/index.tsx +1 -7
  53. package/src/components/TestResult/TrEmpty/index.tsx +1 -7
  54. package/src/components/TestResult/TrEnvironmentItem/index.tsx +2 -2
  55. package/src/components/TestResult/TrError/index.tsx +9 -2
  56. package/src/components/TestResult/TrHeader/TrBreadcrumbs.tsx +2 -2
  57. package/src/components/TestResult/TrHistory/TrHistoryItem.tsx +38 -7
  58. package/src/components/TestResult/TrHistory/index.tsx +18 -8
  59. package/src/components/TestResult/TrHistory/styles.scss +4 -7
  60. package/src/components/TestResult/TrInfo/styles.scss +1 -0
  61. package/src/components/TestResult/TrNavigation/index.tsx +109 -68
  62. package/src/components/TestResult/TrNavigation/styles.scss +15 -25
  63. package/src/components/TestResult/TrPwTraces/PwTraceButton.tsx +1 -8
  64. package/src/components/TestResult/TrRetriesView/TrRetriesItem.tsx +2 -3
  65. package/src/components/TestResult/TrRetriesView/index.tsx +4 -3
  66. package/src/components/TestResult/TrSteps/TrAttachment.tsx +5 -3
  67. package/src/components/TestResult/TrSteps/TrAttachmentInfo.tsx +10 -3
  68. package/src/components/TestResult/TrSteps/TrStep.tsx +3 -3
  69. package/src/components/TestResult/TrTabs/index.tsx +7 -23
  70. package/src/components/TestResult/index.tsx +9 -4
  71. package/src/components/TestResult/styles.scss +1 -0
  72. package/src/components/Tree/index.tsx +22 -25
  73. package/src/index.html +19 -18
  74. package/src/index.tsx +20 -28
  75. package/src/locales/az.json +42 -12
  76. package/src/locales/de.json +42 -12
  77. package/src/locales/en.json +42 -12
  78. package/src/locales/es.json +42 -12
  79. package/src/locales/fr.json +42 -12
  80. package/src/locales/he.json +42 -12
  81. package/src/locales/hy.json +42 -12
  82. package/src/locales/it.json +42 -12
  83. package/src/locales/ja.json +42 -12
  84. package/src/locales/ka.json +42 -12
  85. package/src/locales/kr.json +42 -12
  86. package/src/locales/nl.json +42 -12
  87. package/src/locales/pl.json +42 -12
  88. package/src/locales/pt.json +42 -12
  89. package/src/locales/ru.json +42 -12
  90. package/src/locales/sv.json +42 -12
  91. package/src/locales/tr.json +42 -12
  92. package/src/locales/ua.json +42 -12
  93. package/src/locales/zh.json +42 -12
  94. package/src/stores/chart.ts +2 -2
  95. package/src/stores/env.ts +6 -6
  96. package/src/stores/envInfo.ts +2 -2
  97. package/src/stores/globals.ts +1 -1
  98. package/src/stores/index.ts +0 -1
  99. package/src/stores/layout.ts +20 -11
  100. package/src/stores/locale.ts +2 -1
  101. package/src/stores/qualityGate.ts +2 -2
  102. package/src/stores/router.ts +25 -91
  103. package/src/stores/sections.ts +32 -45
  104. package/src/stores/stats.ts +4 -4
  105. package/src/stores/testResult.ts +5 -0
  106. package/src/stores/testResults.ts +7 -5
  107. package/src/stores/tree.ts +49 -126
  108. package/src/stores/treeFilters/actions.ts +63 -0
  109. package/src/stores/treeFilters/constants.ts +13 -0
  110. package/src/stores/treeFilters/model.ts +51 -0
  111. package/src/stores/treeFilters/store.ts +273 -0
  112. package/src/stores/treeFilters/utils.ts +132 -0
  113. package/src/stores/treeSort.ts +71 -0
  114. package/src/stores/variables.ts +3 -3
  115. package/src/utils/persist.ts +23 -0
  116. package/src/utils/tree.ts +12 -5
  117. package/src/utils/treeFilters.ts +48 -54
  118. package/test/components/Header.test.tsx +49 -58
  119. package/test/utils/treeFilters.test.ts +18 -176
  120. package/types.d.ts +4 -1
  121. package/dist/multi/173.app-9931797d1602fc52db5b.js +0 -1
  122. package/dist/multi/174.app-9931797d1602fc52db5b.js +0 -1
  123. package/dist/multi/252.app-9931797d1602fc52db5b.js +0 -1
  124. package/dist/multi/282.app-9931797d1602fc52db5b.js +0 -1
  125. package/dist/multi/29.app-9931797d1602fc52db5b.js +0 -1
  126. package/dist/multi/416.app-9931797d1602fc52db5b.js +0 -1
  127. package/dist/multi/527.app-9931797d1602fc52db5b.js +0 -1
  128. package/dist/multi/600.app-9931797d1602fc52db5b.js +0 -1
  129. package/dist/multi/605.app-9931797d1602fc52db5b.js +0 -1
  130. package/dist/multi/638.app-9931797d1602fc52db5b.js +0 -1
  131. package/dist/multi/672.app-9931797d1602fc52db5b.js +0 -1
  132. package/dist/multi/686.app-9931797d1602fc52db5b.js +0 -1
  133. package/dist/multi/725.app-9931797d1602fc52db5b.js +0 -1
  134. package/dist/multi/741.app-9931797d1602fc52db5b.js +0 -1
  135. package/dist/multi/755.app-9931797d1602fc52db5b.js +0 -1
  136. package/dist/multi/894.app-9931797d1602fc52db5b.js +0 -1
  137. package/dist/multi/91.app-9931797d1602fc52db5b.js +0 -1
  138. package/dist/multi/943.app-9931797d1602fc52db5b.js +0 -1
  139. package/dist/multi/980.app-9931797d1602fc52db5b.js +0 -1
  140. package/dist/multi/app-9931797d1602fc52db5b.js +0 -2
  141. package/dist/multi/styles-8fe37354d1c2270c691e.css +0 -48
  142. package/dist/single/app-6199dc1c2fd3bddc2526.js +0 -2
  143. package/src/components/ReportBody/Filters.tsx +0 -71
  144. package/src/components/Tabs/index.tsx +0 -62
  145. package/src/stores/theme.ts +0 -30
  146. /package/src/components/{Tabs → ReportTabs}/styles.scss +0 -0
@@ -1,21 +1,12 @@
1
- import { SearchBox } from "@allurereport/web-components";
2
- import { useI18n } from "@/stores/locale";
3
- import { setTreeQuery, treeFiltersStore } from "@/stores/tree";
4
- import { Filters } from "./Filters";
1
+ import { ReportFilters } from "../ReportFilters";
2
+ import { ReportSearch } from "../ReportSearch";
5
3
  import * as styles from "./styles.scss";
6
4
 
7
- const Search = () => {
8
- const { query } = treeFiltersStore.value;
9
- const { t } = useI18n("search");
10
-
11
- return <SearchBox placeholder={t("search-placeholder")} value={query} onChange={setTreeQuery} />;
12
- };
13
-
14
5
  export const HeaderActions = () => {
15
6
  return (
16
7
  <div className={styles.headerActions}>
17
- <Search />
18
- <Filters />
8
+ <ReportSearch />
9
+ <ReportFilters />
19
10
  </div>
20
11
  );
21
12
  };
@@ -1,26 +1,34 @@
1
- import { DropdownButton, allureIcons } from "@allurereport/web-components";
2
- import { Link } from "@allurereport/web-components";
3
- import { Menu } from "@allurereport/web-components";
4
- import { SvgIcon } from "@allurereport/web-components";
5
- import { Text } from "@allurereport/web-components";
1
+ import { DropdownButton, Link, Menu, SvgIcon, Text, allureIcons } from "@allurereport/web-components";
6
2
  import clsx from "clsx";
7
3
  import type { ComponentChildren } from "preact";
8
4
  import { useI18n } from "@/stores/locale";
9
- import { setTreeDirection, setTreeSortBy, treeFiltersStore } from "@/stores/tree";
5
+ import { type SortByDirection, type SortByField, type SortBy as TSortBy, setSortBy, sortBy } from "@/stores/treeSort";
10
6
  import * as styles from "./styles.scss";
11
7
 
12
8
  const BtnWrapper = ({ children }: { children: ComponentChildren }) => {
13
9
  return <div className={styles.sortByBtnWrap}>{children}</div>;
14
10
  };
15
11
 
12
+ const setSortByField = (value: SortByField) => {
13
+ const direction = sortBy.peek().split(",")[1];
14
+ setSortBy(`${value},${direction}` as TSortBy);
15
+ };
16
+
17
+ const setDirection = (value: SortByDirection) => {
18
+ setSortBy(`${sortBy.peek().split(",")[0]},${value}` as TSortBy);
19
+ };
20
+
16
21
  export const SortBy = () => {
17
22
  const { t: sortByLocale } = useI18n("sort-by");
18
23
  const { t: sortByValuesLocale } = useI18n("sort-by.values");
19
24
  const { t: sortByDirectionsLocale } = useI18n("sort-by.directions");
20
- const { sortBy, direction } = treeFiltersStore.value;
25
+ const sortByValue = sortBy.value.split(",")[0] as SortByField;
26
+ const direction = sortBy.value.split(",")[1] as SortByDirection;
21
27
 
22
- const displayedSortByValue = sortByValuesLocale(sortBy);
23
- const displayedDirection = sortByDirectionsLocale(`${sortBy}-${direction}-short`);
28
+ const displayedSortByValue = sortByValuesLocale(sortByValue === "name" ? "alphabet" : sortByValue);
29
+ const displayedDirection = sortByDirectionsLocale(
30
+ `${sortByValue === "name" ? "alphabet" : sortByValue}-${direction}-short`,
31
+ );
24
32
 
25
33
  return (
26
34
  <div>
@@ -68,16 +76,19 @@ export const SortBy = () => {
68
76
  )}
69
77
  >
70
78
  <Menu.Section>
71
- <Menu.ItemWithCheckmark onClick={() => setTreeSortBy("order")} isChecked={sortBy === "order"}>
79
+ <Menu.ItemWithCheckmark onClick={() => setSortByField("order")} isChecked={sortByValue === "order"}>
72
80
  {sortByValuesLocale("order")}
73
81
  </Menu.ItemWithCheckmark>
74
- <Menu.ItemWithCheckmark onClick={() => setTreeSortBy("duration")} isChecked={sortBy === "duration"}>
82
+ <Menu.ItemWithCheckmark
83
+ onClick={() => setSortByField("duration")}
84
+ isChecked={sortByValue === "duration"}
85
+ >
75
86
  {sortByValuesLocale("duration")}
76
87
  </Menu.ItemWithCheckmark>
77
- <Menu.ItemWithCheckmark onClick={() => setTreeSortBy("status")} isChecked={sortBy === "status"}>
88
+ <Menu.ItemWithCheckmark onClick={() => setSortByField("status")} isChecked={sortByValue === "status"}>
78
89
  {sortByValuesLocale("status")}
79
90
  </Menu.ItemWithCheckmark>
80
- <Menu.ItemWithCheckmark onClick={() => setTreeSortBy("alphabet")} isChecked={sortBy === "alphabet"}>
91
+ <Menu.ItemWithCheckmark onClick={() => setSortByField("name")} isChecked={sortByValue === "name"}>
81
92
  {sortByValuesLocale("alphabet")}
82
93
  </Menu.ItemWithCheckmark>
83
94
  </Menu.Section>
@@ -109,18 +120,18 @@ export const SortBy = () => {
109
120
  >
110
121
  <Menu.Section>
111
122
  <Menu.ItemWithCheckmark
112
- onClick={() => setTreeDirection("asc")}
123
+ onClick={() => setDirection("asc")}
113
124
  leadingIcon={allureIcons.lineArrowsSortLineAsc}
114
125
  isChecked={direction === "asc"}
115
126
  >
116
- {sortByDirectionsLocale(`${sortBy}-asc`)}
127
+ {sortByDirectionsLocale(`${sortByValue}-asc`)}
117
128
  </Menu.ItemWithCheckmark>
118
129
  <Menu.ItemWithCheckmark
119
- onClick={() => setTreeDirection("desc")}
130
+ onClick={() => setDirection("desc")}
120
131
  leadingIcon={allureIcons.lineArrowsSortLineDesc}
121
132
  isChecked={direction === "desc"}
122
133
  >
123
- {sortByDirectionsLocale(`${sortBy}-desc`)}
134
+ {sortByDirectionsLocale(`${sortByValue}-desc`)}
124
135
  </Menu.ItemWithCheckmark>
125
136
  </Menu.Section>
126
137
  </Menu>
@@ -3,8 +3,8 @@ import { Counter, Loadable } from "@allurereport/web-components";
3
3
  import { reportStatsStore, statsByEnvStore } from "@/stores";
4
4
  import { currentEnvironment } from "@/stores/env";
5
5
  import { useI18n } from "@/stores/locale";
6
- import { setTreeStatus, treeFiltersStore } from "@/stores/tree";
7
- import { Tab, Tabs, TabsList, useTabsContext } from "../Tabs";
6
+ import { setTreeStatus, treeStatus } from "@/stores/treeFilters/store";
7
+ import { ReportTab, ReportTabsList } from "../ReportTabs";
8
8
  import { TreeList } from "../Tree";
9
9
  import { HeaderActions } from "./HeaderActions";
10
10
  import { SortBy } from "./SortBy";
@@ -15,13 +15,12 @@ const ALL_TAB = "total";
15
15
 
16
16
  const Header = () => {
17
17
  const { t } = useI18n("statuses");
18
- const { currentTab, setCurrentTab } = useTabsContext();
19
18
 
20
19
  return (
21
20
  <header className={styles.header}>
22
21
  <HeaderActions />
23
22
  <div className={styles.headerRow}>
24
- <TabsList>
23
+ <ReportTabsList>
25
24
  <Loadable
26
25
  source={statsByEnvStore}
27
26
  renderData={(stats) => {
@@ -31,29 +30,28 @@ const Header = () => {
31
30
  return { status, value: currentEnv[status] };
32
31
  })
33
32
  .filter(({ value }) => value);
34
- const isStatListHaveCurrentTab = statList.filter(({ status }) => status === currentTab);
35
- if (!isStatListHaveCurrentTab.length && currentTab !== "total") {
36
- setCurrentTab("total");
33
+ const isStatListHaveCurrentTab = statList.filter(({ status }) => status === treeStatus.value);
34
+ if (!isStatListHaveCurrentTab.length && treeStatus.value !== "total") {
37
35
  setTreeStatus("total");
38
36
  }
39
37
 
40
38
  const allStatuses = statList.map(({ status, value }) => (
41
- <Tab data-testid={`tab-${status}`} key={status} id={status}>
39
+ <ReportTab data-testid={`tab-${status}`} key={status} id={status}>
42
40
  {capitalize(t(status) ?? status)} <Counter count={value} size="s" status={status} />
43
- </Tab>
41
+ </ReportTab>
44
42
  ));
45
43
 
46
44
  return (
47
45
  <>
48
- <Tab data-testid="tab-all" id={ALL_TAB}>
46
+ <ReportTab data-testid="tab-all" id={ALL_TAB}>
49
47
  {capitalize(t("total"))} <Counter count={currentEnv?.total ?? 0} size="s" />
50
- </Tab>
48
+ </ReportTab>
51
49
  {allStatuses}
52
50
  </>
53
51
  );
54
52
  }}
55
53
  />
56
- </TabsList>
54
+ </ReportTabsList>
57
55
  <SortBy />
58
56
  </div>
59
57
  </header>
@@ -69,14 +67,11 @@ const Body = () => {
69
67
  };
70
68
 
71
69
  export const ReportBody = () => {
72
- const initialTab = treeFiltersStore.value.status;
73
70
  return (
74
71
  <ReportContentProvider>
75
72
  <section>
76
- <Tabs initialTab={initialTab}>
77
- <Header />
78
- <Body />
79
- </Tabs>
73
+ <Header />
74
+ <Body />
80
75
  </section>
81
76
  </ReportContentProvider>
82
77
  );
@@ -28,7 +28,10 @@
28
28
 
29
29
  .headerActions {
30
30
  display: flex;
31
- gap: 8px;
31
+ flex-wrap: wrap;
32
+ flex-direction: row;
33
+ column-gap: 8px;
34
+ row-gap: 12px;
32
35
  }
33
36
 
34
37
  /* This is a hack because button do not have this state */
@@ -0,0 +1,345 @@
1
+ import {
2
+ type ArrayField,
3
+ type BooleanField,
4
+ type FieldFilter,
5
+ type FieldFilterGroup,
6
+ type LogicalOperator,
7
+ MAX_ARRAY_FIELD_VALUES,
8
+ } from "@allurereport/web-commons";
9
+ import {
10
+ Button,
11
+ Counter,
12
+ DropdownButton,
13
+ IconButton,
14
+ Menu,
15
+ Text,
16
+ Tooltip,
17
+ allureIcons,
18
+ useTooltip,
19
+ } from "@allurereport/web-components";
20
+ import { createPortal } from "preact/compat";
21
+ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
22
+ import { useI18n } from "@/stores/locale";
23
+ import * as styles from "./styles.scss";
24
+
25
+ const FilterBtn = (props: {
26
+ icon: string;
27
+ text: string;
28
+ isActive: boolean;
29
+ counter?: number;
30
+ onClick: () => void;
31
+ onClear?: () => void;
32
+ isDropdown?: boolean;
33
+ isExpanded?: boolean;
34
+ error?: string;
35
+ testId?: string;
36
+ description?: string;
37
+ disabled?: boolean;
38
+ }) => {
39
+ const {
40
+ icon,
41
+ text,
42
+ isActive,
43
+ counter,
44
+ onClick,
45
+ onClear,
46
+ isDropdown,
47
+ isExpanded,
48
+ error,
49
+ testId,
50
+ description,
51
+ disabled,
52
+ } = props;
53
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false);
54
+ const tooltipRef = useRef<HTMLDivElement>(null);
55
+ const triggerRef = useRef<HTMLDivElement>(null);
56
+
57
+ useTooltip({ isVisible: isTooltipVisible, triggerRef, tooltipRef, placement: "top" });
58
+
59
+ const hasError = error?.length > 0;
60
+ const hasDescription = description?.length > 0;
61
+
62
+ useEffect(() => {
63
+ if (!hasError && !hasDescription) {
64
+ setIsTooltipVisible(false);
65
+ }
66
+ }, [hasError, hasDescription]);
67
+
68
+ const handleMouseOver = useCallback(() => {
69
+ if (hasError || hasDescription) {
70
+ setIsTooltipVisible(true);
71
+ }
72
+ }, [hasError, hasDescription]);
73
+
74
+ const handleMouseLeave = useCallback(() => {
75
+ setIsTooltipVisible(false);
76
+ }, []);
77
+
78
+ const commonProps = {
79
+ icon,
80
+ text,
81
+ size: "m",
82
+ style: isActive ? "raised" : "ghost",
83
+ action: hasError ? "danger" : "default",
84
+ onClick,
85
+ trailingSlot: counter !== undefined ? <Counter count={counter} size="s" truncateCount /> : null,
86
+ isDisabled: disabled,
87
+ } as const;
88
+
89
+ const clearButton =
90
+ isActive && onClear ? (
91
+ <div className={styles.clear}>
92
+ <IconButton size="xs" style="ghost" icon={allureIcons.solidXCircle} onClick={onClear} rounded />
93
+ </div>
94
+ ) : null;
95
+
96
+ let content = <Button {...commonProps} />;
97
+
98
+ if (isDropdown) {
99
+ content = <DropdownButton {...commonProps} isExpanded={isExpanded} />;
100
+ }
101
+
102
+ const hasTooltipContent = !isExpanded && (hasError || hasDescription);
103
+
104
+ return (
105
+ <div className={styles.btnWrapper} data-testid={testId}>
106
+ <div ref={triggerRef} onPointerOver={handleMouseOver} onPointerLeave={handleMouseLeave}>
107
+ {content}
108
+ </div>
109
+ {clearButton}
110
+ {createPortal(
111
+ <div
112
+ ref={tooltipRef}
113
+ className={styles.tooltip}
114
+ data-testid="filter-tooltip"
115
+ data-visible={(isTooltipVisible && hasTooltipContent) || undefined}
116
+ >
117
+ {error && <Tooltip ref={tooltipRef}>{error}</Tooltip>}
118
+ {!error && description && <Tooltip ref={tooltipRef}>{description}</Tooltip>}
119
+ </div>,
120
+ document.body,
121
+ )}
122
+ </div>
123
+ );
124
+ };
125
+
126
+ export const BooleanFieldFilter = <T extends BooleanField = BooleanField>(props: {
127
+ field: T;
128
+ onChange: (filter: T) => void;
129
+ icon?: string;
130
+ label?: string;
131
+ testId?: string;
132
+ description?: string;
133
+ }) => {
134
+ const { field, onChange, icon, label, testId, description } = props;
135
+ const { value, key } = field;
136
+
137
+ const handleChange = useCallback(() => {
138
+ onChange({ ...field, value: !value });
139
+ }, [field, onChange, value]);
140
+
141
+ return (
142
+ <FilterBtn
143
+ icon={icon}
144
+ text={label ?? key}
145
+ isActive={value}
146
+ onClick={handleChange}
147
+ testId={testId}
148
+ description={description}
149
+ />
150
+ );
151
+ };
152
+
153
+ export const MultipleChoiceFieldFilter = (props: {
154
+ group: FieldFilterGroup;
155
+ counter?: boolean;
156
+ onChange: (group: FieldFilterGroup) => void;
157
+ onClear?: () => void;
158
+ options: { key: string; label?: string; description?: string; icon?: string }[];
159
+ fieldKey: string;
160
+ logicalOperator?: LogicalOperator;
161
+ strict?: boolean;
162
+ icon?: string;
163
+ label?: string;
164
+ testId?: string;
165
+ disabled?: boolean;
166
+ }) => {
167
+ const {
168
+ group,
169
+ onChange,
170
+ icon,
171
+ label,
172
+ fieldKey,
173
+ options,
174
+ logicalOperator = "AND",
175
+ strict = true,
176
+ counter = true,
177
+ onClear,
178
+ testId,
179
+ disabled,
180
+ } = props;
181
+ const { value } = group;
182
+
183
+ const handleOptionClick = useCallback(
184
+ (optionKey: string, optionValue: boolean) => {
185
+ const newValues = value.filter(
186
+ (v) =>
187
+ v.type === "field" && v.value.type === "string" && v.value.key === fieldKey && v.value.value !== optionKey,
188
+ );
189
+
190
+ if (optionValue) {
191
+ newValues.push({
192
+ type: "field",
193
+ value: {
194
+ type: "string",
195
+ key: fieldKey,
196
+ value: optionKey,
197
+ strict,
198
+ },
199
+ logicalOperator,
200
+ });
201
+ }
202
+
203
+ onChange({ ...group, value: newValues });
204
+ },
205
+ [group, onChange, value, fieldKey, strict, logicalOperator],
206
+ );
207
+
208
+ const isOptionChecked = (optionKey: string) => {
209
+ return value.some(
210
+ (v) => v.type === "field" && v.value.type === "string" && v.value.key === fieldKey && v.value.value === optionKey,
211
+ );
212
+ };
213
+
214
+ const checkedValuesCount = options.map(({ key }) => key).filter(isOptionChecked).length;
215
+ const hasCheckedValues = checkedValuesCount > 0;
216
+
217
+ if (disabled) {
218
+ return (
219
+ <FilterBtn
220
+ icon={icon}
221
+ text={label ?? fieldKey}
222
+ isActive={hasCheckedValues}
223
+ onClick={() => {}}
224
+ disabled
225
+ testId={testId}
226
+ />
227
+ );
228
+ }
229
+
230
+ return (
231
+ <Menu
232
+ size="l"
233
+ placement="bottom-start"
234
+ menuTrigger={({ onClick, isOpened }) => (
235
+ <FilterBtn
236
+ icon={icon}
237
+ text={label ?? fieldKey}
238
+ isActive={hasCheckedValues}
239
+ isExpanded={isOpened}
240
+ counter={counter && hasCheckedValues ? checkedValuesCount : undefined}
241
+ onClick={onClick}
242
+ onClear={onClear}
243
+ isDropdown
244
+ testId={testId}
245
+ />
246
+ )}
247
+ >
248
+ <Menu.Section>
249
+ {options.map((option) => (
250
+ <Menu.ItemWithCheckmark
251
+ key={option.key}
252
+ onClick={() => handleOptionClick(option.key, !isOptionChecked(option.key))}
253
+ isChecked={isOptionChecked(option.key)}
254
+ leadingIcon={option.icon}
255
+ closeMenuOnClick={false}
256
+ dataTestId={`${option.key}-filter`}
257
+ >
258
+ <div className={styles.itemContent}>
259
+ <Text tag="div">{option.label ?? option.key}</Text>
260
+ {option.description && (
261
+ <Text tag="div" size="s" type="paragraph" className={styles.description}>
262
+ {option.description}
263
+ </Text>
264
+ )}
265
+ </div>
266
+ </Menu.ItemWithCheckmark>
267
+ ))}
268
+ </Menu.Section>
269
+ </Menu>
270
+ );
271
+ };
272
+
273
+ export const ArrayFieldFilter = <T extends ArrayField = ArrayField>(props: {
274
+ filter: FieldFilter & { value: T };
275
+ counter?: boolean;
276
+ onChange: (filter: FieldFilter & { value: T }) => void;
277
+ onClear?: () => void;
278
+ options: { key: string; label?: string; icon?: string }[];
279
+ icon?: string;
280
+ label?: string;
281
+ description?: string;
282
+ disabled?: boolean;
283
+ }) => {
284
+ const { filter, onChange, icon, label, options, counter = true, onClear, description, disabled } = props;
285
+ const { value, key } = filter.value;
286
+ const { t } = useI18n("filters");
287
+
288
+ const handleOptionClick = useCallback(
289
+ (optionKey: string, optionValue: boolean) => {
290
+ const newValues = value.filter((v) => v !== optionKey);
291
+
292
+ if (optionValue) {
293
+ newValues.push(optionKey);
294
+ }
295
+
296
+ onChange({ ...filter, value: { ...filter.value, value: newValues } });
297
+ },
298
+ [filter, onChange, value],
299
+ );
300
+
301
+ const isOptionChecked = (optionKey: string) => {
302
+ return value.some((v) => v === optionKey);
303
+ };
304
+
305
+ const checkedValuesCount = options.map(({ key: optionKey }) => optionKey).filter(isOptionChecked).length;
306
+ const hasCheckedValues = checkedValuesCount > 0;
307
+
308
+ const errorText = t("errors.max_values", { count: MAX_ARRAY_FIELD_VALUES });
309
+ const hasError = checkedValuesCount > MAX_ARRAY_FIELD_VALUES;
310
+
311
+ return (
312
+ <Menu
313
+ placement="bottom-start"
314
+ menuTrigger={({ onClick, isOpened }) => (
315
+ <FilterBtn
316
+ icon={icon}
317
+ text={label ?? key}
318
+ isActive={hasCheckedValues}
319
+ isExpanded={isOpened}
320
+ counter={counter && hasCheckedValues ? checkedValuesCount : undefined}
321
+ onClick={onClick}
322
+ onClear={onClear}
323
+ isDropdown
324
+ error={hasError ? errorText : undefined}
325
+ description={description}
326
+ disabled={disabled}
327
+ />
328
+ )}
329
+ >
330
+ <Menu.Section>
331
+ {options.map((option) => (
332
+ <Menu.ItemWithCheckmark
333
+ closeMenuOnClick={false}
334
+ key={option.key}
335
+ onClick={() => handleOptionClick(option.key, !isOptionChecked(option.key))}
336
+ isChecked={isOptionChecked(option.key)}
337
+ leadingIcon={option.icon}
338
+ >
339
+ {option.label ?? option.key}
340
+ </Menu.ItemWithCheckmark>
341
+ ))}
342
+ </Menu.Section>
343
+ </Menu>
344
+ );
345
+ };
@@ -0,0 +1,29 @@
1
+ import { allureIcons } from "@allurereport/web-components";
2
+ import { useI18n } from "@/stores";
3
+ import type { AwesomeBooleanField, AwesomeFieldFilter } from "@/stores/treeFilters/model";
4
+ import { BooleanFieldFilter } from "./BaseFilters";
5
+
6
+ type RetryOrFlakyFilter = AwesomeFieldFilter & {
7
+ value: AwesomeBooleanField;
8
+ };
9
+
10
+ export const RetryFlakyFilter = (props: {
11
+ filter: RetryOrFlakyFilter;
12
+ onChange: (filter: RetryOrFlakyFilter) => void;
13
+ }) => {
14
+ const { filter, onChange } = props;
15
+ const { value: field } = filter;
16
+ const { key } = field;
17
+ const { t, t: tDescription } = useI18n("filters");
18
+
19
+ return (
20
+ <BooleanFieldFilter
21
+ field={field}
22
+ onChange={(value) => onChange({ ...filter, value })}
23
+ icon={key === "retry" ? allureIcons.lineArrowsRefreshCcw1 : allureIcons.lineIconBomb2}
24
+ label={t(key)}
25
+ testId={`${key}-filter`}
26
+ description={tDescription(`description.${key}`)}
27
+ />
28
+ );
29
+ };
@@ -0,0 +1,41 @@
1
+ import { computed } from "@preact/signals";
2
+ import { useI18n } from "@/stores";
3
+ import { treeTags } from "@/stores/treeFilters/store";
4
+ import type { AwesomeArrayFieldFilter } from "../../stores/treeFilters/model";
5
+ import { ArrayFieldFilter } from "./BaseFilters";
6
+
7
+ const options = computed(() => {
8
+ return treeTags.value.map((tag) => ({ key: tag, label: tag }));
9
+ });
10
+
11
+ export const TagsFilter = (props: {
12
+ filter: AwesomeArrayFieldFilter;
13
+ onChange: (filter: AwesomeArrayFieldFilter) => void;
14
+ }) => {
15
+ const { filter, onChange } = props;
16
+ const { t } = useI18n("filters");
17
+
18
+ if (options.value.length === 0) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <ArrayFieldFilter
24
+ filter={filter}
25
+ onChange={onChange}
26
+ options={options.value}
27
+ label={t("tags")}
28
+ description={t("description.tags")}
29
+ counter
30
+ onClear={() =>
31
+ onChange({
32
+ ...filter,
33
+ value: {
34
+ ...filter.value,
35
+ value: [],
36
+ },
37
+ })
38
+ }
39
+ />
40
+ );
41
+ };
@@ -0,0 +1,49 @@
1
+ import { allureIcons } from "@allurereport/web-components";
2
+ import { useMemo } from "preact/hooks";
3
+ import { useI18n } from "@/stores";
4
+ import type { AwesomeFilterGroupSimple } from "../../stores/treeFilters/model";
5
+ import { MultipleChoiceFieldFilter } from "./BaseFilters";
6
+
7
+ const transitionOptions = [
8
+ { key: "new", icon: allureIcons.lineAlertsNew },
9
+ { key: "fixed", icon: allureIcons.lineAlertsFixed },
10
+ { key: "regressed", icon: allureIcons.lineAlertsRegressed },
11
+ { key: "malfunctioned", icon: allureIcons.lineAlertsMalfunctioned },
12
+ ];
13
+
14
+ export const TransitionFilter = (props: {
15
+ group: AwesomeFilterGroupSimple;
16
+ onChange: (group: AwesomeFilterGroupSimple) => void;
17
+ }) => {
18
+ const { group, onChange } = props;
19
+ const { t } = useI18n("filters");
20
+ const options = useMemo(
21
+ () =>
22
+ transitionOptions.map((option) => ({
23
+ ...option,
24
+ label: t(option.key),
25
+ description: t(`description.${option.key}`),
26
+ })),
27
+ [t],
28
+ );
29
+
30
+ return (
31
+ <MultipleChoiceFieldFilter
32
+ group={group}
33
+ onChange={onChange}
34
+ options={options}
35
+ label={t("transition")}
36
+ fieldKey="transition"
37
+ logicalOperator="OR"
38
+ strict
39
+ counter
40
+ testId="transition-filter"
41
+ onClear={() =>
42
+ onChange({
43
+ ...group,
44
+ value: [],
45
+ })
46
+ }
47
+ />
48
+ );
49
+ };