@transferwise/components 0.0.0-experimental-d1715ff → 0.0.0-experimental-3064bdb
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/build/i18n/en.json +2 -0
- package/build/i18n/en.json.js +2 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +2 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/main.css +214 -0
- package/build/styles/main.css +214 -0
- package/build/styles/table/Table.css +214 -0
- package/build/types/table/Table.d.ts +23 -0
- package/build/types/table/Table.d.ts.map +1 -0
- package/build/types/table/Table.messages.d.ts +12 -0
- package/build/types/table/Table.messages.d.ts.map +1 -0
- package/build/types/table/TableCell.d.ts +37 -0
- package/build/types/table/TableCell.d.ts.map +1 -0
- package/build/types/table/TableHeader.d.ts +12 -0
- package/build/types/table/TableHeader.d.ts.map +1 -0
- package/build/types/table/TableRow.d.ts +17 -0
- package/build/types/table/TableRow.d.ts.map +1 -0
- package/build/types/table/TableStatusText.d.ts +9 -0
- package/build/types/table/TableStatusText.d.ts.map +1 -0
- package/build/types/table/index.d.ts +6 -0
- package/build/types/table/index.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/i18n/en.json +2 -0
- package/src/main.css +214 -0
- package/src/main.less +1 -0
- package/src/table/Table.css +214 -0
- package/src/table/Table.less +253 -0
- package/src/table/Table.messages.ts +12 -0
- package/src/table/Table.spec.tsx +87 -0
- package/src/table/Table.story.tsx +352 -0
- package/src/table/Table.tsx +121 -0
- package/src/table/TableCell.spec.tsx +298 -0
- package/src/table/TableCell.tsx +153 -0
- package/src/table/TableHeader.spec.tsx +58 -0
- package/src/table/TableHeader.tsx +50 -0
- package/src/table/TableRow.spec.tsx +104 -0
- package/src/table/TableRow.tsx +62 -0
- package/src/table/TableStatusText.spec.tsx +53 -0
- package/src/table/TableStatusText.tsx +35 -0
- package/src/table/index.ts +11 -0
- package/src/test-utils/assets/avatar-rectangle-fox.webp +0 -0
- package/src/test-utils/assets/avatar-square-dude.webp +0 -0
- package/src/test-utils/assets/tapestry-01.png +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useIntl } from 'react-intl';
|
|
2
|
+
import TableCell from './TableCell';
|
|
3
|
+
import TableHeader, { TableHeaderType } from './TableHeader';
|
|
4
|
+
import TableRow, { TableRowClickableType, TableRowType } from './TableRow';
|
|
5
|
+
import Alert from '../alert';
|
|
6
|
+
|
|
7
|
+
import messages from './Table.messages';
|
|
8
|
+
import Loader from '../loader';
|
|
9
|
+
import { Sentiment, Size } from '../common';
|
|
10
|
+
import StatusIcon from '../statusIcon';
|
|
11
|
+
import clsx from 'clsx';
|
|
12
|
+
|
|
13
|
+
export interface TableProps {
|
|
14
|
+
data: {
|
|
15
|
+
headers?: TableHeaderType[];
|
|
16
|
+
content?: TableRowType[] | TableRowClickableType[];
|
|
17
|
+
onRowClick?: (rowData: TableRowType | TableRowClickableType) => void;
|
|
18
|
+
};
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
className?: string | undefined;
|
|
21
|
+
fullWidth?: boolean;
|
|
22
|
+
error?: {
|
|
23
|
+
message?: string;
|
|
24
|
+
action?: {
|
|
25
|
+
href?: string;
|
|
26
|
+
text?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
onRetry?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const Table: React.FC<TableProps> = ({ data, loading, className, fullWidth = true, error }) => {
|
|
33
|
+
const { formatMessage } = useIntl();
|
|
34
|
+
|
|
35
|
+
const getTableContent = () => {
|
|
36
|
+
if (loading) {
|
|
37
|
+
return (
|
|
38
|
+
<TableRow>
|
|
39
|
+
<TableCell>
|
|
40
|
+
<Loader data-testid="np-table-loader" />
|
|
41
|
+
</TableCell>
|
|
42
|
+
</TableRow>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Shows the `emptyData` message when there is no data to display
|
|
47
|
+
if (!data?.content?.length) {
|
|
48
|
+
return (
|
|
49
|
+
<TableRow>
|
|
50
|
+
<TableCell colSpan={data?.headers?.length ? data?.headers?.length : undefined}>
|
|
51
|
+
<div className="np-table-empty-data" data-testid="np-table-empty-data">
|
|
52
|
+
<StatusIcon sentiment={Sentiment.WARNING} size={Size.MEDIUM} />
|
|
53
|
+
<div className="np-text-body-large-bold">{formatMessage(messages.emptyData)}</div>
|
|
54
|
+
</div>
|
|
55
|
+
</TableCell>
|
|
56
|
+
</TableRow>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return data?.content?.map((rowData, rowIndex) => {
|
|
61
|
+
return (
|
|
62
|
+
<TableRow
|
|
63
|
+
key={'table-row'.concat(rowIndex.toString())}
|
|
64
|
+
rowData={rowData}
|
|
65
|
+
hasSeparator={data?.content?.length ? data.content.length - 1 !== rowIndex : false}
|
|
66
|
+
onRowClick={data?.onRowClick}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (error) {
|
|
73
|
+
return (
|
|
74
|
+
<Alert
|
|
75
|
+
message={error.message}
|
|
76
|
+
type={Sentiment.NEGATIVE}
|
|
77
|
+
action={{
|
|
78
|
+
href: error?.action?.href ?? '/',
|
|
79
|
+
text: error?.action?.text ?? formatMessage(messages.refreshPage),
|
|
80
|
+
}}
|
|
81
|
+
data-testid="np-table-error"
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
className={clsx('np-table-container', className, {
|
|
89
|
+
'np-table-container--loading': loading,
|
|
90
|
+
'np-table-container--center': !fullWidth,
|
|
91
|
+
'np-table-container--full-width': fullWidth,
|
|
92
|
+
})}
|
|
93
|
+
data-testid="np-table-container"
|
|
94
|
+
>
|
|
95
|
+
<div className="np-table-inner-container">
|
|
96
|
+
<table className="np-table">
|
|
97
|
+
<thead>
|
|
98
|
+
<tr>
|
|
99
|
+
{(loading ?? (data?.headers && !data?.headers.length)) ? (
|
|
100
|
+
<TableHeader />
|
|
101
|
+
) : (
|
|
102
|
+
data?.headers?.map((headerItem: TableHeaderType, index) => (
|
|
103
|
+
<TableHeader
|
|
104
|
+
key={headerItem.header?.concat(index.toString())}
|
|
105
|
+
hasActionColumn={
|
|
106
|
+
data?.onRowClick && index === Number(data?.headers?.length) - 1
|
|
107
|
+
}
|
|
108
|
+
{...headerItem}
|
|
109
|
+
/>
|
|
110
|
+
))
|
|
111
|
+
)}
|
|
112
|
+
</tr>
|
|
113
|
+
</thead>
|
|
114
|
+
<tbody>{getTableContent()}</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default Table;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import TableCell, {
|
|
4
|
+
CurrencyContentType,
|
|
5
|
+
LeadingContentType,
|
|
6
|
+
StatusContentType,
|
|
7
|
+
TextContentType,
|
|
8
|
+
} from './TableCell';
|
|
9
|
+
import { IntlProvider } from 'react-intl';
|
|
10
|
+
import { mockMatchMedia } from '../test-utils';
|
|
11
|
+
|
|
12
|
+
mockMatchMedia();
|
|
13
|
+
|
|
14
|
+
describe('TableCell Component', () => {
|
|
15
|
+
const cellContentMocks = {
|
|
16
|
+
leading: {
|
|
17
|
+
primaryText: 'Leading text',
|
|
18
|
+
secondaryText: 'Description',
|
|
19
|
+
initials: 'AB',
|
|
20
|
+
} satisfies LeadingContentType,
|
|
21
|
+
text: {
|
|
22
|
+
text: 'Cell text',
|
|
23
|
+
} satisfies TextContentType,
|
|
24
|
+
currency: {
|
|
25
|
+
primaryCurrency: {
|
|
26
|
+
value: '12345.67',
|
|
27
|
+
currency: 'eur',
|
|
28
|
+
},
|
|
29
|
+
secondaryCurrency: {
|
|
30
|
+
value: '11000.00',
|
|
31
|
+
currency: 'gbp',
|
|
32
|
+
},
|
|
33
|
+
} satisfies CurrencyContentType,
|
|
34
|
+
status: {
|
|
35
|
+
primaryText: 'Primary text',
|
|
36
|
+
secondaryText: 'Description',
|
|
37
|
+
sentiment: 'negative',
|
|
38
|
+
} satisfies StatusContentType,
|
|
39
|
+
custom: <div>Custom content</div>,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const renderWithIntl = (component: React.ReactElement) => {
|
|
43
|
+
return render(<IntlProvider locale="en">{component}</IntlProvider>);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
it('renders without content correctly', () => {
|
|
47
|
+
renderWithIntl(<TableCell />);
|
|
48
|
+
expect(screen.getByRole('cell')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders with colSpan correctly', () => {
|
|
52
|
+
renderWithIntl(<TableCell colSpan={2} />);
|
|
53
|
+
expect(screen.getByRole('cell')).toHaveAttribute('colSpan', '2');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders with custom className correctly', () => {
|
|
57
|
+
renderWithIntl(<TableCell className="custom-class" />);
|
|
58
|
+
expect(screen.getByRole('cell')).toHaveClass('custom-class');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders separator correctly', () => {
|
|
62
|
+
renderWithIntl(<TableCell hasSeparator />);
|
|
63
|
+
expect(screen.getByTestId('np-table-cell-separator')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders cell with children's content correctly", () => {
|
|
67
|
+
renderWithIntl(<TableCell>{cellContentMocks.custom}</TableCell>);
|
|
68
|
+
expect(screen.getByText('Custom content')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('applies alignment class correctly', () => {
|
|
72
|
+
renderWithIntl(<TableCell alignment="right" />);
|
|
73
|
+
expect(screen.getByRole('cell')).toHaveClass('np-table-cell--right');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders text content correctly', () => {
|
|
77
|
+
renderWithIntl(<TableCell content={{ ...cellContentMocks.text }} />);
|
|
78
|
+
expect(screen.getByText('Cell text')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders text with error content correctly', () => {
|
|
82
|
+
renderWithIntl(<TableCell content={{ ...cellContentMocks.text, status: 'error' }} />);
|
|
83
|
+
expect(screen.getByText('Cell text')).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText('Cell text')).toHaveClass('np-table-content--error');
|
|
85
|
+
expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('renders text with success content correctly', () => {
|
|
89
|
+
renderWithIntl(<TableCell content={{ ...cellContentMocks.text, status: 'success' }} />);
|
|
90
|
+
expect(screen.getByText('Cell text')).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByText('Cell text')).toHaveClass('np-table-content--success');
|
|
92
|
+
expect(screen.getByTestId('check-icon')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('renders leading content with initials correctly', () => {
|
|
96
|
+
renderWithIntl(<TableCell type="leading" content={{ ...cellContentMocks.leading }} />);
|
|
97
|
+
expect(screen.getByText('AB')).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByText('Leading text')).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('renders leading cell without media content when initials are undefined', () => {
|
|
103
|
+
renderWithIntl(
|
|
104
|
+
<TableCell
|
|
105
|
+
type="leading"
|
|
106
|
+
content={{
|
|
107
|
+
...cellContentMocks.leading,
|
|
108
|
+
initials: undefined,
|
|
109
|
+
}}
|
|
110
|
+
/>,
|
|
111
|
+
);
|
|
112
|
+
expect(screen.queryByText('AB')).not.toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('Leading text')).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
115
|
+
expect(screen.queryByTestId('np-table-content-media')).not.toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders leading cell without primary text if it is undefined', () => {
|
|
119
|
+
renderWithIntl(
|
|
120
|
+
<TableCell
|
|
121
|
+
type="leading"
|
|
122
|
+
content={{
|
|
123
|
+
...cellContentMocks.leading,
|
|
124
|
+
primaryText: undefined,
|
|
125
|
+
}}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByText('AB')).toBeInTheDocument();
|
|
129
|
+
expect(screen.queryByText('Leading text')).not.toBeInTheDocument();
|
|
130
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('renders leading cell without secondary text if it is undefined', () => {
|
|
134
|
+
renderWithIntl(
|
|
135
|
+
<TableCell
|
|
136
|
+
type="leading"
|
|
137
|
+
content={{
|
|
138
|
+
...cellContentMocks.leading,
|
|
139
|
+
secondaryText: undefined,
|
|
140
|
+
}}
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
expect(screen.getByText('AB')).toBeInTheDocument();
|
|
144
|
+
expect(screen.getByText('Leading text')).toBeInTheDocument();
|
|
145
|
+
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('renders leading content with error content correctly', () => {
|
|
149
|
+
renderWithIntl(
|
|
150
|
+
<TableCell
|
|
151
|
+
type="leading"
|
|
152
|
+
content={{
|
|
153
|
+
...cellContentMocks.leading,
|
|
154
|
+
status: 'error',
|
|
155
|
+
}}
|
|
156
|
+
/>,
|
|
157
|
+
);
|
|
158
|
+
expect(screen.getByText('AB')).toBeInTheDocument();
|
|
159
|
+
expect(screen.getByText('Leading text')).toHaveClass('np-table-content--error');
|
|
160
|
+
expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('renders leading content with success content correctly', () => {
|
|
164
|
+
renderWithIntl(
|
|
165
|
+
<TableCell
|
|
166
|
+
type="leading"
|
|
167
|
+
content={{
|
|
168
|
+
...cellContentMocks.leading,
|
|
169
|
+
status: 'success',
|
|
170
|
+
}}
|
|
171
|
+
/>,
|
|
172
|
+
);
|
|
173
|
+
expect(screen.getByText('AB')).toBeInTheDocument();
|
|
174
|
+
expect(screen.getByText('Leading text')).toHaveClass('np-table-content--success');
|
|
175
|
+
expect(screen.getByTestId('check-icon')).toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('renders status content correctly', () => {
|
|
179
|
+
renderWithIntl(<TableCell type="status" content={{ ...cellContentMocks.status }} />);
|
|
180
|
+
expect(screen.getByTestId('status-icon')).toBeInTheDocument();
|
|
181
|
+
expect(screen.getByTestId('status-icon')).toHaveClass('negative');
|
|
182
|
+
expect(screen.getByText('Primary text')).toBeInTheDocument();
|
|
183
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('renders status type with default sentiment', () => {
|
|
187
|
+
renderWithIntl(
|
|
188
|
+
<TableCell
|
|
189
|
+
type="status"
|
|
190
|
+
content={{ primaryText: 'Primary text', secondaryText: 'Description' }}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
expect(screen.getByTestId('info-icon')).toBeInTheDocument();
|
|
194
|
+
expect(screen.getByText('Primary text')).toBeInTheDocument();
|
|
195
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('renders currency content correctly', () => {
|
|
199
|
+
renderWithIntl(<TableCell type="currency" content={{ ...cellContentMocks.currency }} />);
|
|
200
|
+
expect(screen.getByRole('presentation')).toBeInTheDocument();
|
|
201
|
+
expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
|
|
202
|
+
expect(screen.getByText('11,000.00 GBP')).toBeInTheDocument();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('renders currency cell without media content when currency value of primaryCurrency is empty', () => {
|
|
206
|
+
renderWithIntl(
|
|
207
|
+
<TableCell
|
|
208
|
+
type="currency"
|
|
209
|
+
content={{
|
|
210
|
+
...cellContentMocks.currency,
|
|
211
|
+
primaryCurrency: {
|
|
212
|
+
value: cellContentMocks.currency.primaryCurrency.value,
|
|
213
|
+
currency: '',
|
|
214
|
+
},
|
|
215
|
+
}}
|
|
216
|
+
/>,
|
|
217
|
+
);
|
|
218
|
+
expect(screen.queryByRole('presentation')).not.toBeInTheDocument();
|
|
219
|
+
expect(screen.getByText('12,345.67')).toBeInTheDocument();
|
|
220
|
+
expect(screen.getByText('11,000.00 GBP')).toBeInTheDocument();
|
|
221
|
+
expect(screen.queryByTestId('np-table-content-media')).not.toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('renders currency cell without primary currency when it is undefined', () => {
|
|
225
|
+
renderWithIntl(
|
|
226
|
+
<TableCell
|
|
227
|
+
type="currency"
|
|
228
|
+
content={{
|
|
229
|
+
...cellContentMocks.currency,
|
|
230
|
+
primaryCurrency: undefined,
|
|
231
|
+
}}
|
|
232
|
+
/>,
|
|
233
|
+
);
|
|
234
|
+
expect(screen.queryByText('12,345.67 EUR')).not.toBeInTheDocument();
|
|
235
|
+
expect(screen.getByText('11,000.00 GBP')).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('renders currency cell without secondary currency when it is undefined', () => {
|
|
239
|
+
renderWithIntl(
|
|
240
|
+
<TableCell
|
|
241
|
+
type="currency"
|
|
242
|
+
content={{
|
|
243
|
+
...cellContentMocks.currency,
|
|
244
|
+
secondaryCurrency: undefined,
|
|
245
|
+
}}
|
|
246
|
+
/>,
|
|
247
|
+
);
|
|
248
|
+
expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
|
|
249
|
+
expect(screen.queryByText('11,000.00 GBP')).not.toBeInTheDocument();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('renders currency cell without secondary currency when its currency is empty', () => {
|
|
253
|
+
renderWithIntl(
|
|
254
|
+
<TableCell
|
|
255
|
+
type="currency"
|
|
256
|
+
content={{
|
|
257
|
+
...cellContentMocks.currency,
|
|
258
|
+
secondaryCurrency: {
|
|
259
|
+
value: cellContentMocks.currency.secondaryCurrency.value,
|
|
260
|
+
currency: '',
|
|
261
|
+
},
|
|
262
|
+
}}
|
|
263
|
+
/>,
|
|
264
|
+
);
|
|
265
|
+
expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
|
|
266
|
+
expect(screen.getByText('11,000.00')).toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('renders currency content correctly with error content correctly', () => {
|
|
270
|
+
renderWithIntl(
|
|
271
|
+
<TableCell
|
|
272
|
+
type="currency"
|
|
273
|
+
content={{
|
|
274
|
+
...cellContentMocks.currency,
|
|
275
|
+
status: 'error',
|
|
276
|
+
}}
|
|
277
|
+
/>,
|
|
278
|
+
);
|
|
279
|
+
expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
|
|
280
|
+
expect(screen.getByText('12,345.67 EUR')).toHaveClass('np-table-content--error');
|
|
281
|
+
expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('renders currency content correctly with success content correctly', () => {
|
|
285
|
+
renderWithIntl(
|
|
286
|
+
<TableCell
|
|
287
|
+
type="currency"
|
|
288
|
+
content={{
|
|
289
|
+
...cellContentMocks.currency,
|
|
290
|
+
status: 'success',
|
|
291
|
+
}}
|
|
292
|
+
/>,
|
|
293
|
+
);
|
|
294
|
+
expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
|
|
295
|
+
expect(screen.getByText('12,345.67 EUR')).toHaveClass('np-table-content--success');
|
|
296
|
+
expect(screen.getByTestId('check-icon')).toBeInTheDocument();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import TableStatusText from './TableStatusText';
|
|
2
|
+
import Avatar, { AvatarType } from '../avatar';
|
|
3
|
+
import StatusIcon from '../statusIcon';
|
|
4
|
+
import { Flag } from '@wise/art';
|
|
5
|
+
import Body from '../body';
|
|
6
|
+
import { formatMoney } from '@transferwise/formatting';
|
|
7
|
+
import { useIntl } from 'react-intl';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { clsx } from 'clsx';
|
|
10
|
+
|
|
11
|
+
// `Leading` and `Status` cells' types have 2 text fields: primary and secondary
|
|
12
|
+
interface TextPropsType {
|
|
13
|
+
primaryText?: string;
|
|
14
|
+
secondaryText?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// `Leading`, `Text` and `Status` cells' types can have a status indicator with `error` or `success` values
|
|
18
|
+
interface StatusPropsType {
|
|
19
|
+
status?: 'error' | 'success';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// TODO: Instead of initials it should be an avatar, change implementation
|
|
23
|
+
export interface LeadingContentType extends TextPropsType, StatusPropsType {
|
|
24
|
+
initials?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TextContentType extends StatusPropsType {
|
|
28
|
+
text?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CurrencyType {
|
|
32
|
+
value: string | number;
|
|
33
|
+
currency: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CurrencyContentType extends StatusPropsType {
|
|
37
|
+
primaryCurrency?: CurrencyType;
|
|
38
|
+
secondaryCurrency?: CurrencyType;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StatusContentType extends TextPropsType {
|
|
42
|
+
sentiment?: 'negative' | 'neutral' | 'positive' | 'warning' | 'pending';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TableCellProps {
|
|
46
|
+
type?: 'leading' | 'text' | 'currency' | 'status';
|
|
47
|
+
content?: LeadingContentType & TextContentType & CurrencyContentType & StatusContentType;
|
|
48
|
+
alignment?: 'right' | 'left';
|
|
49
|
+
className?: string;
|
|
50
|
+
colSpan?: number;
|
|
51
|
+
hasSeparator?: boolean;
|
|
52
|
+
children?: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const TableCell = ({
|
|
56
|
+
type = 'text',
|
|
57
|
+
content,
|
|
58
|
+
alignment,
|
|
59
|
+
className,
|
|
60
|
+
colSpan,
|
|
61
|
+
hasSeparator,
|
|
62
|
+
children,
|
|
63
|
+
}: TableCellProps) => {
|
|
64
|
+
const { locale } = useIntl();
|
|
65
|
+
|
|
66
|
+
const getContentMedia = () => {
|
|
67
|
+
let mediaContent = null;
|
|
68
|
+
|
|
69
|
+
if (type === 'leading' && content?.initials) {
|
|
70
|
+
mediaContent = (
|
|
71
|
+
<Avatar size={40} type={AvatarType.INITIALS}>
|
|
72
|
+
{content?.initials}
|
|
73
|
+
</Avatar>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (type === 'currency' && content?.primaryCurrency?.currency) {
|
|
78
|
+
mediaContent = (
|
|
79
|
+
<Flag code={content?.primaryCurrency?.currency?.toLowerCase()} intrinsicSize={24} />
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (type === 'status') {
|
|
84
|
+
mediaContent = <StatusIcon size="md" sentiment={content?.sentiment ?? 'neutral'} />;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (mediaContent) {
|
|
88
|
+
return (
|
|
89
|
+
<div className="np-table-content-media" data-testid="np-table-content-media">
|
|
90
|
+
{mediaContent}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const formatCurrencyValue = (currency?: CurrencyType) => {
|
|
97
|
+
if (currency) {
|
|
98
|
+
return formatMoney(Number(currency.value), currency.currency, locale, {
|
|
99
|
+
alwaysShowDecimals: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return '';
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<td
|
|
108
|
+
className={clsx('np-table-cell', `np-table-cell--${type}`, className, {
|
|
109
|
+
'np-table-cell--right': alignment === 'right',
|
|
110
|
+
})}
|
|
111
|
+
colSpan={colSpan}
|
|
112
|
+
>
|
|
113
|
+
{type === 'text' && content?.text && (
|
|
114
|
+
<TableStatusText text={content?.text} status={content?.status} />
|
|
115
|
+
)}
|
|
116
|
+
{['leading', 'currency', 'status'].includes(type) && (
|
|
117
|
+
<div
|
|
118
|
+
className={clsx('np-table-content', {
|
|
119
|
+
'np-table-content--reversed': type === 'currency',
|
|
120
|
+
})}
|
|
121
|
+
>
|
|
122
|
+
{getContentMedia()}
|
|
123
|
+
<div className="np-table-content-body">
|
|
124
|
+
{(content?.primaryCurrency ?? content?.primaryText) && (
|
|
125
|
+
<TableStatusText
|
|
126
|
+
text={
|
|
127
|
+
type === 'currency'
|
|
128
|
+
? formatCurrencyValue(content?.primaryCurrency)
|
|
129
|
+
: (content?.primaryText ?? '')
|
|
130
|
+
}
|
|
131
|
+
status={type !== 'status' ? content?.status : undefined}
|
|
132
|
+
typography="large-bold"
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
{(content?.secondaryCurrency ?? content?.secondaryText) && (
|
|
136
|
+
<Body>
|
|
137
|
+
{type === 'currency'
|
|
138
|
+
? formatCurrencyValue(content?.secondaryCurrency)
|
|
139
|
+
: content?.secondaryText}
|
|
140
|
+
</Body>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
{hasSeparator && (
|
|
146
|
+
<div className="np-table-cell-separator" data-testid="np-table-cell-separator" />
|
|
147
|
+
)}
|
|
148
|
+
{children}
|
|
149
|
+
</td>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default TableCell;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import TableHeader, { TableHeaderProps } from './TableHeader';
|
|
3
|
+
|
|
4
|
+
describe('TableHeader Component', () => {
|
|
5
|
+
const renderComponent = (props: Partial<TableHeaderProps> = {}) => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
header: '',
|
|
8
|
+
className: '',
|
|
9
|
+
alignment: 'left',
|
|
10
|
+
hasError: false,
|
|
11
|
+
} satisfies TableHeaderProps;
|
|
12
|
+
return render(<TableHeader {...defaultProps} {...props} />);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('should render without crashing', () => {
|
|
16
|
+
const { container } = renderComponent();
|
|
17
|
+
expect(container).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should render header text when provided', () => {
|
|
21
|
+
const headerText = 'Test Header';
|
|
22
|
+
renderComponent({ header: headerText });
|
|
23
|
+
expect(screen.getByText(headerText)).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should apply custom className', () => {
|
|
27
|
+
const className = 'custom-class';
|
|
28
|
+
renderComponent({ className });
|
|
29
|
+
expect(screen.getByRole('columnheader')).toHaveClass(className);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should align text to the right when alignment is set to right', () => {
|
|
33
|
+
renderComponent({ alignment: 'right' });
|
|
34
|
+
expect(screen.getByRole('columnheader')).toHaveClass('np-table-header--right');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should show error class when hasError is passed', () => {
|
|
38
|
+
renderComponent({ hasError: true });
|
|
39
|
+
expect(screen.getByRole('columnheader')).toHaveClass('np-table-header--has-error');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should render empty header content when header is not provided', () => {
|
|
43
|
+
renderComponent();
|
|
44
|
+
expect(screen.getByTestId('np-table-empty-header').innerHTML).toBe(' ');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should render with colSpan of 2 when hasActionColumn is passed', () => {
|
|
48
|
+
renderComponent({ hasActionColumn: true });
|
|
49
|
+
expect(screen.getByRole('columnheader')).toHaveAttribute('colSpan', '2');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render TableStatusText with error status when hasError is passed', () => {
|
|
53
|
+
const headerText = 'Test Header';
|
|
54
|
+
renderComponent({ header: headerText, hasError: true });
|
|
55
|
+
expect(screen.getByText(headerText)).toHaveClass('np-table-content--error');
|
|
56
|
+
expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import TableStatusText from './TableStatusText';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
|
|
4
|
+
// TODO: Add `width` prop in next iterations
|
|
5
|
+
export interface TableHeaderType {
|
|
6
|
+
header?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
alignment?: 'right' | 'left';
|
|
9
|
+
hasError?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TableHeaderProps extends TableHeaderType {
|
|
13
|
+
hasActionColumn?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TableHeader: React.FC<TableHeaderProps> = ({
|
|
17
|
+
header,
|
|
18
|
+
className,
|
|
19
|
+
alignment = 'left',
|
|
20
|
+
hasError = false,
|
|
21
|
+
hasActionColumn = false,
|
|
22
|
+
}: TableHeaderProps) => {
|
|
23
|
+
return (
|
|
24
|
+
<th
|
|
25
|
+
className={clsx('np-table-header', className, {
|
|
26
|
+
'np-table-header--right': alignment === 'right',
|
|
27
|
+
'np-table-header--has-error': hasError,
|
|
28
|
+
})}
|
|
29
|
+
colSpan={hasActionColumn ? 2 : undefined}
|
|
30
|
+
>
|
|
31
|
+
{header ? (
|
|
32
|
+
<TableStatusText
|
|
33
|
+
text={header}
|
|
34
|
+
className="np-table-header-content"
|
|
35
|
+
status={hasError ? 'error' : undefined}
|
|
36
|
+
typography="large-bold"
|
|
37
|
+
/>
|
|
38
|
+
) : (
|
|
39
|
+
<div
|
|
40
|
+
className="np-table-header-content np-text-body-large-bold"
|
|
41
|
+
data-testid="np-table-empty-header"
|
|
42
|
+
>
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</th>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default TableHeader;
|