@orchestrator-ui/orchestrator-ui-components 2.2.0 → 2.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Library of UI Components used to display the workflow orchestrator frontend",
6
6
  "author": {
@@ -10,6 +10,9 @@ export type WfoSortButtonsProps = {
10
10
  onChangeSortOrder: (updatedSortOrder: SortOrder) => void;
11
11
  };
12
12
 
13
+ /**
14
+ * @deprecated This component is not used anymore and will be removed in the next major version.
15
+ */
13
16
  export const WfoSortButtons: FC<WfoSortButtonsProps> = ({
14
17
  sortOrder,
15
18
  onChangeSortOrder,
@@ -1,11 +1,13 @@
1
1
  import React, { FC } from 'react';
2
2
 
3
3
  import { useOrchestratorTheme } from '@/hooks';
4
- import { WfoArrowNarrowDown, WfoArrowNarrowUp } from '@/icons';
4
+ import { WfoArrowNarrowDown, WfoArrowNarrowUp, WfoArrowsUpDown } from '@/icons';
5
5
  import { SortOrder } from '@/types';
6
6
 
7
+ import { SORTABLE_ICON_CLASS } from './styles';
8
+
7
9
  export type WfoSortDirectionIconProps = {
8
- sortDirection: SortOrder;
10
+ sortDirection?: SortOrder;
9
11
  };
10
12
 
11
13
  export const WfoSortDirectionIcon: FC<WfoSortDirectionIconProps> = ({
@@ -13,6 +15,16 @@ export const WfoSortDirectionIcon: FC<WfoSortDirectionIconProps> = ({
13
15
  }) => {
14
16
  const { theme } = useOrchestratorTheme();
15
17
 
18
+ if (!sortDirection) {
19
+ return (
20
+ <WfoArrowsUpDown
21
+ className={SORTABLE_ICON_CLASS}
22
+ css={{ visibility: 'hidden' }}
23
+ color={theme.colors.subduedText}
24
+ />
25
+ );
26
+ }
27
+
16
28
  return sortDirection === SortOrder.ASC ? (
17
29
  <WfoArrowNarrowUp
18
30
  color={theme.colors.subduedText}
@@ -10,12 +10,16 @@ import {
10
10
  useGeneratedHtmlId,
11
11
  } from '@elastic/eui';
12
12
 
13
- import { WfoSortButtons } from '@/components';
13
+ import { getUpdatedSortOrder } from '@/components/WfoTable/WfoTable/utils';
14
14
  import { useWithOrchestratorTheme } from '@/hooks';
15
15
  import { SortOrder } from '@/types';
16
16
 
17
17
  import { WfoSortDirectionIcon } from './WfoSortDirectionIcon';
18
- import { getWfoBasicTableStyles } from './styles';
18
+ import {
19
+ HEADER_CELL_SORT_BUTTON_CLASS,
20
+ HEADER_CELL_TITLE_BUTTON_CLASS,
21
+ getWfoBasicTableStyles,
22
+ } from './styles';
19
23
 
20
24
  export type WfoTableHeaderCellProps = {
21
25
  fieldName: string;
@@ -33,17 +37,18 @@ export const WfoTableHeaderCell: FC<WfoTableHeaderCellProps> = ({
33
37
  onSearch,
34
38
  }) => {
35
39
  const {
36
- headerCellContentStyle,
40
+ headerCellStyle,
41
+ getHeaderCellContentStyle,
37
42
  headerCellPopoverHeaderStyle,
38
43
  headerCellPopoverHeaderTitleStyle,
39
44
  headerCellPopoverContentStyle,
40
- getHeaderCellButtonStyle,
45
+ getTitleButtonStyle,
46
+ sortButtonStyle,
41
47
  } = useWithOrchestratorTheme(getWfoBasicTableStyles);
42
48
  const t = useTranslations('common');
43
49
 
44
50
  const isSortable = !!onSetSortOrder;
45
51
  const isFilterable = !!onSearch;
46
- const shouldShowPopover = isSortable || isFilterable;
47
52
 
48
53
  const smallContextMenuPopoverId = useGeneratedHtmlId({
49
54
  prefix: 'smallContextMenuPopover',
@@ -53,24 +58,14 @@ export const WfoTableHeaderCell: FC<WfoTableHeaderCellProps> = ({
53
58
  const handleButtonClick = () => setPopover(!isPopoverOpen);
54
59
  const closePopover = () => setPopover(false);
55
60
 
56
- const handleChangeSortOrder = (updatedSortOrder: SortOrder) => {
57
- onSetSortOrder?.(updatedSortOrder);
58
- closePopover();
59
- };
60
-
61
61
  const handleSearch = (searchText: string) => {
62
62
  onSearch?.(searchText);
63
63
  closePopover();
64
64
  };
65
65
 
66
66
  const WfoHeaderCellContentButton = () => (
67
- <button onClick={handleButtonClick} disabled={!shouldShowPopover}>
68
- <div css={getHeaderCellButtonStyle(shouldShowPopover)}>
69
- <div css={headerCellContentStyle}>{children}</div>
70
- {sortOrder && (
71
- <WfoSortDirectionIcon sortDirection={sortOrder} />
72
- )}
73
- </div>
67
+ <button onClick={handleButtonClick} disabled={!isFilterable}>
68
+ <div css={getHeaderCellContentStyle(isFilterable)}>{children}</div>
74
69
  </button>
75
70
  );
76
71
 
@@ -79,12 +74,6 @@ export const WfoTableHeaderCell: FC<WfoTableHeaderCellProps> = ({
79
74
  <EuiText size="xs" css={headerCellPopoverHeaderTitleStyle}>
80
75
  {children}
81
76
  </EuiText>
82
- {isSortable && (
83
- <WfoSortButtons
84
- sortOrder={sortOrder}
85
- onChangeSortOrder={handleChangeSortOrder}
86
- />
87
- )}
88
77
  </div>
89
78
  );
90
79
 
@@ -100,22 +89,34 @@ export const WfoTableHeaderCell: FC<WfoTableHeaderCellProps> = ({
100
89
  );
101
90
 
102
91
  return (
103
- <EuiPopover
104
- initialFocus={`.euiPanel .euiFieldSearch.${fieldName}`}
105
- id={smallContextMenuPopoverId}
106
- button={<WfoHeaderCellContentButton />}
107
- isOpen={isPopoverOpen}
108
- closePopover={closePopover}
109
- panelPaddingSize="none"
110
- anchorPosition="downLeft"
111
- >
112
- <WfoPopoverHeader />
113
- {isFilterable && (
114
- <>
115
- <EuiHorizontalRule margin="none" />
116
- <WfoPopoverContent />
117
- </>
92
+ <div css={headerCellStyle}>
93
+ <EuiPopover
94
+ className={HEADER_CELL_TITLE_BUTTON_CLASS}
95
+ css={getTitleButtonStyle(sortOrder)}
96
+ initialFocus={`.euiPanel .euiFieldSearch.${fieldName}`}
97
+ id={smallContextMenuPopoverId}
98
+ button={<WfoHeaderCellContentButton />}
99
+ isOpen={isPopoverOpen}
100
+ closePopover={closePopover}
101
+ panelPaddingSize="none"
102
+ anchorPosition="downLeft"
103
+ >
104
+ <WfoPopoverHeader />
105
+ <EuiHorizontalRule margin="none" />
106
+ <WfoPopoverContent />
107
+ </EuiPopover>
108
+
109
+ {isSortable && (
110
+ <button
111
+ className={HEADER_CELL_SORT_BUTTON_CLASS}
112
+ css={sortButtonStyle}
113
+ onClick={() =>
114
+ onSetSortOrder(getUpdatedSortOrder(sortOrder))
115
+ }
116
+ >
117
+ <WfoSortDirectionIcon sortDirection={sortOrder} />
118
+ </button>
118
119
  )}
119
- </EuiPopover>
120
+ </div>
120
121
  );
121
122
  };
@@ -1,6 +1,11 @@
1
1
  import { css } from '@emotion/react';
2
2
 
3
3
  import { WfoTheme } from '@/hooks';
4
+ import { SortOrder } from '@/types';
5
+
6
+ export const HEADER_CELL_TITLE_BUTTON_CLASS = 'headerCellTitleButton';
7
+ export const HEADER_CELL_SORT_BUTTON_CLASS = 'headerCellSortButton';
8
+ export const SORTABLE_ICON_CLASS = 'sortableIcon';
4
9
 
5
10
  export const getWfoBasicTableStyles = ({ theme }: WfoTheme) => {
6
11
  const radius = theme.border.radius.medium;
@@ -72,10 +77,23 @@ export const getWfoBasicTableStyles = ({ theme }: WfoTheme) => {
72
77
  },
73
78
  ]);
74
79
 
75
- const headerCellContentStyle = css({
76
- fontWeight: theme.font.weight.semiBold,
80
+ const headerCellStyle = css({
81
+ display: 'flex',
82
+ justifyContent: 'flex-start',
83
+ alignItems: 'center',
84
+
85
+ [`.${HEADER_CELL_TITLE_BUTTON_CLASS}:has(+ .${HEADER_CELL_SORT_BUTTON_CLASS}:focus-visible)`]:
86
+ {
87
+ overflow: 'hidden',
88
+ },
77
89
  });
78
90
 
91
+ const getHeaderCellContentStyle = (isSortable: boolean) =>
92
+ css({
93
+ cursor: isSortable ? 'pointer' : 'not-allowed',
94
+ fontWeight: theme.font.weight.semiBold,
95
+ });
96
+
79
97
  const headerCellPopoverHeaderStyle = css({
80
98
  margin: theme.size.m,
81
99
  display: 'flex',
@@ -91,20 +109,30 @@ export const getWfoBasicTableStyles = ({ theme }: WfoTheme) => {
91
109
  margin: theme.size.m,
92
110
  });
93
111
 
94
- const getHeaderCellButtonStyle = (isSortable: boolean) =>
112
+ const getTitleButtonStyle = (sortOrder?: SortOrder) =>
95
113
  css({
96
- display: 'flex',
97
- alignItems: 'center',
98
- cursor: isSortable ? 'pointer' : 'not-allowed',
114
+ flex: '0 1 auto',
115
+ overflow: sortOrder === undefined ? 'visible' : 'hidden',
99
116
  });
100
117
 
118
+ const sortButtonStyle = css({
119
+ display: 'flex',
120
+ flex: '0 0 auto',
121
+ alignItems: 'center',
122
+ [`&:focus-visible .${SORTABLE_ICON_CLASS}`]: {
123
+ visibility: 'visible',
124
+ },
125
+ });
126
+
101
127
  return {
102
128
  basicTableStyle,
103
- headerCellContentStyle,
129
+ headerCellStyle,
130
+ getHeaderCellContentStyle,
104
131
  headerCellPopoverHeaderStyle,
105
132
  headerCellPopoverHeaderTitleStyle,
106
133
  headerCellPopoverContentStyle,
107
- getHeaderCellButtonStyle,
134
+ getTitleButtonStyle,
135
+ sortButtonStyle,
108
136
  getStatusColumnStyle,
109
137
  dropDownTableStyle,
110
138
  expandableTableStyle,
@@ -28,7 +28,7 @@ export const WfoTableHeaderRow = <T extends object>({
28
28
  onUpdateDataSearch,
29
29
  className,
30
30
  }: WfoTableHeaderRowProps<T>) => {
31
- const { cellStyle, rowStyle, setWidth } =
31
+ const { cellStyle, headerCellStyle, rowStyle, setWidth } =
32
32
  useWithOrchestratorTheme(getWfoTableStyles);
33
33
 
34
34
  const sortedVisibleColumns = getSortedVisibleColumns(
@@ -55,6 +55,10 @@ export const WfoTableHeaderRow = <T extends object>({
55
55
  cellStyle,
56
56
  !columnConfig.disableDefaultCellStyle,
57
57
  ),
58
+ ...toOptionalArrayEntry(
59
+ headerCellStyle,
60
+ !!columnConfig.isSortable,
61
+ ),
58
62
  setWidth(columnConfig.width),
59
63
  ]}
60
64
  >
@@ -2,9 +2,14 @@ import { CSSProperties } from 'react';
2
2
 
3
3
  import { css, keyframes } from '@emotion/react';
4
4
 
5
- import { TABLE_ROW_HEIGHT } from '@/components/WfoTable';
6
5
  import { WfoTheme } from '@/hooks';
7
6
 
7
+ import {
8
+ HEADER_CELL_TITLE_BUTTON_CLASS,
9
+ SORTABLE_ICON_CLASS,
10
+ } from './WfoTableHeaderCell/styles';
11
+ import { TABLE_ROW_HEIGHT } from './constants';
12
+
8
13
  export const getWfoTableStyles = ({ theme }: WfoTheme) => {
9
14
  const radius = theme.border.radius.medium;
10
15
 
@@ -77,6 +82,17 @@ export const getWfoTableStyles = ({ theme }: WfoTheme) => {
77
82
  backgroundColor: theme.colors.lightestShade,
78
83
  });
79
84
 
85
+ const headerCellStyle = css({
86
+ [`&:hover`]: {
87
+ [`.${SORTABLE_ICON_CLASS}`]: {
88
+ visibility: 'visible',
89
+ },
90
+ [`.${HEADER_CELL_TITLE_BUTTON_CLASS}`]: {
91
+ overflow: 'hidden',
92
+ },
93
+ },
94
+ });
95
+
80
96
  const cellStyle = css({
81
97
  paddingLeft: theme.size.m,
82
98
  paddingRight: theme.size.m,
@@ -109,6 +125,7 @@ export const getWfoTableStyles = ({ theme }: WfoTheme) => {
109
125
  rowStyle,
110
126
  dataRowStyle,
111
127
  expandedRowStyle,
128
+ headerCellStyle,
112
129
  cellStyle,
113
130
  emptyTableMessageStyle,
114
131
  clickableStyle,
@@ -2,8 +2,12 @@ import {
2
2
  ColumnType,
3
3
  WfoTableColumnConfig,
4
4
  } from '@/components/WfoTable/WfoTable/WfoTable';
5
+ import { SortOrder } from '@/types';
5
6
 
6
- import { mapSortableAndFilterableValuesToTableColumnConfig } from './utils';
7
+ import {
8
+ getUpdatedSortOrder,
9
+ mapSortableAndFilterableValuesToTableColumnConfig,
10
+ } from './utils';
7
11
 
8
12
  type TestObject = {
9
13
  name: string;
@@ -21,59 +25,83 @@ const tableColumnConfig: WfoTableColumnConfig<TestObject> = {
21
25
  },
22
26
  };
23
27
 
24
- describe('mapSortableAndFilterableValuesToTableColumnConfig', () => {
25
- it('sets the sortable and filterable properties for the columnConfig object to true when the colum name is specified in the list', () => {
26
- // Given
27
- const sortableFieldNames = ['name', 'age', 'nonExistingFieldName'];
28
- const filterableFieldNames = ['name', 'age', 'nonExistingFieldName'];
28
+ describe('utils', () => {
29
+ describe('getUpdatedSortOrder()', () => {
30
+ it('returns SortOrder.DESC if the currentSortOrder is SortOrder.ASC', () => {
31
+ const currentSortOrder = SortOrder.ASC;
32
+ const result = getUpdatedSortOrder(currentSortOrder);
33
+ expect(result).toBe(SortOrder.DESC);
34
+ });
35
+ it('returns SortOrder.ASC if the currentSortOrder is SortOrder.DESC', () => {
36
+ const currentSortOrder = SortOrder.DESC;
37
+ const result = getUpdatedSortOrder(currentSortOrder);
38
+ expect(result).toBe(SortOrder.ASC);
39
+ });
40
+ it('returns SortOrder.ASC if the currentSortOrder is undefined', () => {
41
+ const currentSortOrder = undefined;
42
+ const result = getUpdatedSortOrder(currentSortOrder);
43
+ expect(result).toBe(SortOrder.ASC);
44
+ });
45
+ });
46
+
47
+ describe('mapSortableAndFilterableValuesToTableColumnConfig()', () => {
48
+ it('sets the sortable and filterable properties for the columnConfig object to true when the colum name is specified in the list', () => {
49
+ // Given
50
+ const sortableFieldNames = ['name', 'age', 'nonExistingFieldName'];
51
+ const filterableFieldNames = [
52
+ 'name',
53
+ 'age',
54
+ 'nonExistingFieldName',
55
+ ];
29
56
 
30
- // When
31
- const result =
32
- mapSortableAndFilterableValuesToTableColumnConfig<TestObject>(
33
- tableColumnConfig,
34
- sortableFieldNames,
35
- filterableFieldNames,
36
- );
57
+ // When
58
+ const result =
59
+ mapSortableAndFilterableValuesToTableColumnConfig<TestObject>(
60
+ tableColumnConfig,
61
+ sortableFieldNames,
62
+ filterableFieldNames,
63
+ );
37
64
 
38
- // Then
39
- if (
40
- result.name?.columnType === ColumnType.DATA &&
41
- result.age?.columnType === ColumnType.DATA
42
- ) {
43
- expect(result.name.isSortable).toEqual(true);
44
- expect(result.name.isFilterable).toEqual(true);
45
- expect(result.age.isSortable).toEqual(true);
46
- expect(result.age.isFilterable).toEqual(true);
47
- } else {
48
- // Preventing silently skipping above expects
49
- throw Error('Some of the fields are not data fields');
50
- }
51
- });
52
- it('sets the sortable and filterable properties for the columnConfig object to false when the colum name is not specified in the list', () => {
53
- // Given
54
- const sortableFieldNames = ['nonExistingFieldName'];
55
- const filterableFieldNames: string[] = [];
65
+ // Then
66
+ if (
67
+ result.name?.columnType === ColumnType.DATA &&
68
+ result.age?.columnType === ColumnType.DATA
69
+ ) {
70
+ expect(result.name.isSortable).toEqual(true);
71
+ expect(result.name.isFilterable).toEqual(true);
72
+ expect(result.age.isSortable).toEqual(true);
73
+ expect(result.age.isFilterable).toEqual(true);
74
+ } else {
75
+ // Preventing silently skipping above expects
76
+ throw Error('Some of the fields are not data fields');
77
+ }
78
+ });
79
+ it('sets the sortable and filterable properties for the columnConfig object to false when the colum name is not specified in the list', () => {
80
+ // Given
81
+ const sortableFieldNames = ['nonExistingFieldName'];
82
+ const filterableFieldNames: string[] = [];
56
83
 
57
- // When
58
- const result =
59
- mapSortableAndFilterableValuesToTableColumnConfig<TestObject>(
60
- tableColumnConfig,
61
- sortableFieldNames,
62
- filterableFieldNames,
63
- );
84
+ // When
85
+ const result =
86
+ mapSortableAndFilterableValuesToTableColumnConfig<TestObject>(
87
+ tableColumnConfig,
88
+ sortableFieldNames,
89
+ filterableFieldNames,
90
+ );
64
91
 
65
- // Then
66
- if (
67
- result.name?.columnType === ColumnType.DATA &&
68
- result.age?.columnType === ColumnType.DATA
69
- ) {
70
- expect(result.name.isSortable).toEqual(false);
71
- expect(result.name.isFilterable).toEqual(false);
72
- expect(result.age.isSortable).toEqual(false);
73
- expect(result.age.isFilterable).toEqual(false);
74
- } else {
75
- // Preventing silently skipping above expects
76
- throw Error('Some of the fields are not data fields');
77
- }
92
+ // Then
93
+ if (
94
+ result.name?.columnType === ColumnType.DATA &&
95
+ result.age?.columnType === ColumnType.DATA
96
+ ) {
97
+ expect(result.name.isSortable).toEqual(false);
98
+ expect(result.name.isFilterable).toEqual(false);
99
+ expect(result.age.isSortable).toEqual(false);
100
+ expect(result.age.isFilterable).toEqual(false);
101
+ } else {
102
+ // Preventing silently skipping above expects
103
+ throw Error('Some of the fields are not data fields');
104
+ }
105
+ });
78
106
  });
79
107
  });
@@ -1,4 +1,5 @@
1
1
  import { TableColumnKeys } from '@/components';
2
+ import { SortOrder } from '@/types';
2
3
  import { toObjectWithSortedKeys } from '@/utils';
3
4
 
4
5
  import {
@@ -33,6 +34,9 @@ export const getSortedVisibleColumns = <T extends object>(
33
34
  );
34
35
  };
35
36
 
37
+ export const getUpdatedSortOrder = (currentSortOrder?: SortOrder) =>
38
+ currentSortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
39
+
36
40
  /**
37
41
  * Maps from WfoTableColumnConfig to WfoTableColumnConfig.
38
42
  * A generic type must be provided to prevent type errors
@@ -1 +1 @@
1
- export const ORCHESTRATOR_UI_LIBRARY_VERSION = '2.2.0';
1
+ export const ORCHESTRATOR_UI_LIBRARY_VERSION = '2.3.0';
@@ -0,0 +1,27 @@
1
+ import React, { FC } from 'react';
2
+
3
+ import { WfoIconProps } from '@/icons/WfoIconProps';
4
+
5
+ import { withWfoHeroIconsWrapper } from './WfoHeroIconsWrapper';
6
+
7
+ const WfoArrowsUpDownSvg: FC<WfoIconProps> = ({
8
+ width = 20,
9
+ height = 20,
10
+ color = '#000000',
11
+ }) => (
12
+ <svg
13
+ width={width}
14
+ height={height}
15
+ viewBox="0 0 24 24"
16
+ fill={color}
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ >
19
+ <path
20
+ fillRule="evenodd"
21
+ d="M6.97 2.47a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.25 4.81V16.5a.75.75 0 0 1-1.5 0V4.81L3.53 8.03a.75.75 0 0 1-1.06-1.06l4.5-4.5Zm9.53 4.28a.75.75 0 0 1 .75.75v11.69l3.22-3.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 1 1 1.06-1.06l3.22 3.22V7.5a.75.75 0 0 1 .75-.75Z"
22
+ clipRule="evenodd"
23
+ />
24
+ </svg>
25
+ );
26
+
27
+ export const WfoArrowsUpDown = withWfoHeroIconsWrapper(WfoArrowsUpDownSvg);
@@ -0,0 +1,43 @@
1
+ import React, { FC, ReactElement } from 'react';
2
+
3
+ import { useOrchestratorTheme } from '@/hooks';
4
+ import { WfoIconProps } from '@/icons';
5
+
6
+ export type WfoHeroIconsWrapperProps = {
7
+ children: ReactElement;
8
+ className?: string;
9
+ };
10
+
11
+ export const WfoHeroIconsWrapper: FC<WfoHeroIconsWrapperProps> = ({
12
+ children,
13
+ className,
14
+ }) => {
15
+ const { theme } = useOrchestratorTheme();
16
+
17
+ return (
18
+ <div
19
+ className={className}
20
+ css={{
21
+ marginLeft: theme.size.xs,
22
+ marginRight: theme.size.xs,
23
+ display: 'flex',
24
+ alignItems: 'center',
25
+ }}
26
+ >
27
+ {children}
28
+ </div>
29
+ );
30
+ };
31
+
32
+ export const withWfoHeroIconsWrapper = (Icon: FC<WfoIconProps>) => {
33
+ const wfoHeroIconsWrapperWithIcon = ({
34
+ className,
35
+ ...iconProps
36
+ }: WfoIconProps & Omit<WfoHeroIconsWrapperProps, 'children'>) => (
37
+ <WfoHeroIconsWrapper className={className}>
38
+ <Icon {...iconProps} />
39
+ </WfoHeroIconsWrapper>
40
+ );
41
+ wfoHeroIconsWrapperWithIcon.displayName = 'WfoHeroIconsWrapper';
42
+ return wfoHeroIconsWrapperWithIcon;
43
+ };
@@ -0,0 +1,2 @@
1
+ export * from './WfoArrowsUpDown';
2
+ export * from './WfoHeroIconsWrapper';
@@ -1,3 +1,5 @@
1
+ export * from './heroicons';
2
+
1
3
  export * from './WfoXCircleFill';
2
4
  export * from './WfoArrowsExpand';
3
5
  export * from './WfoStatusDotIcon';