Unverified Commit db64a9a6 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Adds text alignment and row height options to the reusable table (#642)

* Adding row height and styling polishing
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Adds header and column alignment options
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Fixing betterer issue
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>
parent a58a7917
......@@ -99,8 +99,8 @@ exports[`strict null compilation`] = {
[255, 6, 11, "No overload matches this call.\\n The last overload gave the following error.\\n Type \'(() => SubmitSearchRequest) | null\' is not assignable to type \'ActionCreator<any>\'.\\n Type \'null\' is not assignable to type \'ActionCreator<any>\'.", "2296208050"],
[270, 4, 18, "No overload matches this call.\\n The last overload gave the following error.\\n Argument of type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is not assignable to parameter of type \'DispatchFromProps\'.\\n Type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is missing the following properties from type \'DispatchFromProps\': submitSearch, onInputChange, onSelectInlineResult", "2926224796"]
],
"js/components/common/Table/index.tsx:4167447816": [
[47, 20, 7, "Type \'unknown\' is not assignable to type \'ReactNode\'.\\n Type \'unknown\' is not assignable to type \'ReactPortal\'.", "3446393192"]
"js/components/common/Table/index.tsx:1963290118": [
[121, 18, 7, "Type \'unknown\' is not assignable to type \'ReactNode\'.\\n Type \'unknown\' is not assignable to type \'ReactPortal\'.", "3446393192"]
],
"js/components/common/Tags/TagInput/index.tsx:3754832290": [
[63, 22, 6, "Type \'undefined\' is not assignable to type \'GetAllTagsRequest\'.", "1979467425"],
......
......@@ -30,169 +30,262 @@ describe('Table', () => {
}).not.toThrow();
});
describe('when empty data is passed', () => {
const { columns, data } = dataBuilder.withEmptyData().build();
describe('data', () => {
describe('when empty data is passed', () => {
const { columns, data } = dataBuilder.withEmptyData().build();
it('renders a table', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table').length;
expect(actual).toEqual(expected);
});
describe('table header', () => {
it('renders a table header', () => {
it('renders a table', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-header').length;
const actual = wrapper.find('.ams-table').length;
expect(actual).toEqual(expected);
});
it('renders one cell inside the header', () => {
const { wrapper } = setup({
data,
columns,
describe('table header', () => {
it('renders a table header', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-header').length;
expect(actual).toEqual(expected);
});
const expected = 1;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-cell'
).length;
expect(actual).toEqual(expected);
it('renders one cell inside the header', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-cell'
).length;
expect(actual).toEqual(expected);
});
});
});
describe('table body', () => {
it('renders a table body', () => {
const { wrapper } = setup({
data,
columns,
describe('table body', () => {
it('renders a table body', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-body').length;
expect(actual).toEqual(expected);
});
const expected = 1;
const actual = wrapper.find('.ams-table-body').length;
expect(actual).toEqual(expected);
});
it('renders one row', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-row').length;
it('renders one row', () => {
const { wrapper } = setup({
data,
columns,
expect(actual).toEqual(expected);
});
const expected = 1;
const actual = wrapper.find('.ams-table-row').length;
expect(actual).toEqual(expected);
});
it('renders an empty message', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find(
'.ams-table-row .ams-empty-message-cell'
).length;
it('renders an empty message', () => {
const { wrapper } = setup({
data,
columns,
expect(actual).toEqual(expected);
});
});
});
describe('when simple data is passed', () => {
it('renders a table', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.ams-table-row .ams-empty-message-cell')
.length;
const actual = wrapper.find('.ams-table').length;
expect(actual).toEqual(expected);
});
});
});
describe('when simple data is passed', () => {
it('renders a table', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.ams-table').length;
describe('table header', () => {
it('renders a table header', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.ams-table-header').length;
expect(actual).toEqual(expected);
});
expect(actual).toEqual(expected);
});
describe('table header', () => {
it('renders a table header', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.ams-table-header').length;
it('renders a three cells inside the header', () => {
const { wrapper } = setup();
const expected = 3;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-cell'
).length;
expect(actual).toEqual(expected);
expect(actual).toEqual(expected);
});
});
it('renders a three cells inside the header', () => {
const { wrapper } = setup();
const expected = 3;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-cell'
).length;
describe('table body', () => {
it('renders a table body', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.ams-table-body').length;
expect(actual).toEqual(expected);
expect(actual).toEqual(expected);
});
it('renders three rows', () => {
const { wrapper } = setup();
const expected = 3;
const actual = wrapper.find('.ams-table-row').length;
expect(actual).toEqual(expected);
});
it('renders nine cells', () => {
const { wrapper } = setup();
const expected = 9;
const actual = wrapper.find('.ams-table-row .ams-table-cell')
.length;
expect(actual).toEqual(expected);
});
});
});
describe('table body', () => {
it('renders a table body', () => {
const { wrapper } = setup();
const expected = 1;
const actual = wrapper.find('.ams-table-body').length;
describe('when more data than columns', () => {
const { columns, data } = dataBuilder.withMoreDataThanColumns().build();
expect(actual).toEqual(expected);
describe('table header', () => {
it('renders a three cells inside the header', () => {
const { wrapper } = setup({ columns, data });
const expected = 3;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-cell'
).length;
expect(actual).toEqual(expected);
});
});
it('renders three rows', () => {
const { wrapper } = setup();
const expected = 3;
const actual = wrapper.find('.ams-table-row').length;
describe('table body', () => {
it('renders four rows', () => {
const { wrapper } = setup({ columns, data });
const expected = 4;
const actual = wrapper.find('.ams-table-row').length;
expect(actual).toEqual(expected);
});
expect(actual).toEqual(expected);
});
it('renders nine cells', () => {
const { wrapper } = setup();
const expected = 9;
const actual = wrapper.find('.ams-table-row .ams-table-cell').length;
it('renders twelve cells', () => {
const { wrapper } = setup({ columns, data });
const expected = 12;
const actual = wrapper.find('.ams-table-row .ams-table-cell')
.length;
expect(actual).toEqual(expected);
expect(actual).toEqual(expected);
});
});
});
});
describe('when more data than columns', () => {
const { columns, data } = dataBuilder.withMoreDataThanColumns().build();
describe('colums', () => {
describe('when horizontal alignment is passed', () => {
const { columns, data } = dataBuilder.withAlignedColumns().build();
describe('table header', () => {
it('renders a three cells inside the header', () => {
const { wrapper } = setup({ columns, data });
const expected = 3;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-cell'
).length;
describe('table header', () => {
it('renders the first column as left aligned', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = { textAlign: 'left' };
const actual = wrapper
.find('.ams-table-header .ams-table-heading-cell')
.get(0).props.style;
expect(actual).toEqual(expected);
});
});
expect(actual).toEqual(expected);
});
describe('table body', () => {
it('renders four rows', () => {
const { wrapper } = setup({ columns, data });
const expected = 4;
const actual = wrapper.find('.ams-table-row').length;
it('renders the second column as center aligned', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = { textAlign: 'center' };
const actual = wrapper
.find('.ams-table-header .ams-table-heading-cell')
.get(1).props.style;
expect(actual).toEqual(expected);
expect(actual).toEqual(expected);
});
it('renders the first column as right aligned', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = { textAlign: 'right' };
const actual = wrapper
.find('.ams-table-header .ams-table-heading-cell')
.get(2).props.style;
expect(actual).toEqual(expected);
});
});
it('renders twelve cells', () => {
const { wrapper } = setup({ columns, data });
const expected = 12;
const actual = wrapper.find('.ams-table-row .ams-table-cell').length;
describe('table body', () => {
it('renders the first column as left aligned', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = { textAlign: 'left' };
const actual = wrapper
.find('.ams-table-body .ams-table-cell')
.get(0).props.style;
expect(actual).toEqual(expected);
expect(actual).toEqual(expected);
});
it('renders the second column as center aligned', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = { textAlign: 'center' };
const actual = wrapper
.find('.ams-table-body .ams-table-cell')
.get(1).props.style;
expect(actual).toEqual(expected);
});
it('renders the first column as right aligned', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = { textAlign: 'right' };
const actual = wrapper
.find('.ams-table-body .ams-table-cell')
.get(2).props.style;
expect(actual).toEqual(expected);
});
});
});
});
......@@ -210,6 +303,18 @@ describe('Table', () => {
});
});
describe('when a rowHeight is passed', () => {
it('adds the styling to the rows', () => {
const { wrapper } = setup({
options: { rowHeight: 20 },
});
const expected = { height: '20px' };
const actual = wrapper.find('.ams-table-row').get(0).props.style;
expect(actual).toEqual(expected);
});
});
describe('when isLoading is active', () => {
it('renders a table', () => {
const { wrapper } = setup({
......
......@@ -7,11 +7,13 @@ import ShimmeringResourceLoader from '../ShimmeringResourceLoader';
import './styles.scss';
type TextAlignmentValues = 'left' | 'right' | 'center';
export interface TableColumn {
title: string;
field: string;
horAlign?: TextAlignmentValues;
// className?: string;
// horAlign?: 'left' | 'right' | 'center';
// width?: number;
// sortable?: bool (false)
// data?: () => React.ReactNode ((row,index) => <div>{index}</div>)
......@@ -22,6 +24,7 @@ export interface TableOptions {
tableClassName?: string;
isLoading?: boolean;
numLoadingBlocks?: number;
rowHeight?: number;
}
export interface TableProps {
......@@ -32,13 +35,23 @@ export interface TableProps {
const DEFAULT_EMPTY_MESSAGE = 'No Results';
const DEFAULT_LOADING_ITEMS = 3;
const DEFAULT_ROW_HEIGHT = 30;
const DEFAULT_TEXT_ALIGNMENT = 'left';
type RowStyles = {
height: string;
};
type EmptyRowProps = {
colspan: number;
rowStyles: RowStyles;
};
const EmptyRow: React.FC<EmptyRowProps> = ({ colspan }: EmptyRowProps) => (
<tr className="ams-table-row">
const EmptyRow: React.FC<EmptyRowProps> = ({
colspan,
rowStyles,
}: EmptyRowProps) => (
<tr className="ams-table-row" style={rowStyles}>
<td className="ams-empty-message-cell" colSpan={colspan}>
{DEFAULT_EMPTY_MESSAGE}
</td>
......@@ -76,22 +89,40 @@ const Table: React.FC<TableProps> = ({
tableClassName = '',
isLoading = false,
numLoadingBlocks = DEFAULT_LOADING_ITEMS,
rowHeight = DEFAULT_ROW_HEIGHT,
} = options;
const fields = columns.map(({ field }) => field);
const rowStyles = { height: `${rowHeight}px` };
let body: React.ReactNode = <EmptyRow colspan={fields.length} />;
let body: React.ReactNode = (
<EmptyRow colspan={fields.length} rowStyles={rowStyles} />
);
if (data.length) {
body = data.map((item, index) => {
return (
<tr className="ams-table-row" key={`index:${index}`}>
<tr className="ams-table-row" key={`index:${index}`} style={rowStyles}>
{Object.entries(item)
.filter(([key]) => fields.includes(key))
.map(([, value], index) => (
<td className="ams-table-cell" key={`index:${index}`}>
{value}
</td>
))}
.map(([key, value], index) => {
const columnInfo = columns.find(({ field }) => field === key);
const horAlign = columnInfo
? columnInfo.horAlign || DEFAULT_TEXT_ALIGNMENT
: DEFAULT_TEXT_ALIGNMENT;
const cellStyle = {
textAlign: `${horAlign}` as TextAlignmentValues,
};
return (
<td
className="ams-table-cell"
key={`index:${index}`}
style={cellStyle}
>
{value}
</td>
);
})}
</tr>
);
});
......@@ -99,9 +130,17 @@ const Table: React.FC<TableProps> = ({
let header: React.ReactNode = (
<tr>
{columns.map(({ title }, index) => {
{columns.map(({ title, horAlign = DEFAULT_TEXT_ALIGNMENT }, index) => {
const cellStyle = {
textAlign: `${horAlign}` as TextAlignmentValues,
};
return (
<th className="ams-table-heading-cell" key={`index:${index}`}>
<th
className="ams-table-heading-cell"
key={`index:${index}`}
style={cellStyle}
>
{title}
</th>
);
......
......@@ -30,15 +30,27 @@ $shimmer-block-width: 40%;
.ams-table-heading-cell {
height: $table-header-height;
text-transform: uppercase;
&:first-child {
padding-left: $spacer-3;
}
&:last-child {
padding-right: $spacer-3;
}
}
.ams-empty-message-cell {
@extend %text-body-w3;
.ams-table-cell {
&:first-child {
padding-left: $spacer-3;
}
color: $text-primary;
text-align: center;
&:last-child {
padding-right: $spacer-3;
}
}
// Loading State
.ams-table-heading-loading-cell {
padding: $spacer-1;
}
......@@ -57,3 +69,11 @@ $shimmer-block-width: 40%;
height: $shimmer-block-height;
width: $shimmer-block-width;
}
// Empty State
.ams-empty-message-cell {
@extend %text-body-w3;
color: $text-primary;
text-align: center;
}
......@@ -8,7 +8,12 @@ import StorySection from '../StorySection';
import Table from '.';
import TestDataBuilder from './testDataBuilder';
const { columns, data } = new TestDataBuilder().build();
const dataBuilder = new TestDataBuilder();
const { columns, data } = dataBuilder.build();
const {
columns: alignedColumns,
data: alignedData,
} = dataBuilder.withAlignedColumns().build();
const stories = storiesOf('Components/Table', module);
......@@ -23,5 +28,11 @@ stories.add('Table', () => (
<StorySection title="Loading Table">
<Table columns={[]} data={[]} options={{ isLoading: true }} />
</StorySection>
<StorySection title="Table with different column alignment">
<Table columns={alignedColumns} data={alignedData} />
</StorySection>
<StorySection title="Table with 50px row height">
<Table columns={columns} data={data} options={{ rowHeight: 50 }} />
</StorySection>
</>
));
......@@ -50,6 +50,31 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr);
};
this.withAlignedColumns = () => {
const attr = {
data: [...this.config.data],
columns: [
{
title: 'Name',
field: 'name',
horAlign: 'left',
},
{
title: 'Type',
field: 'type',
horAlign: 'center',
},
{
title: 'Value',
field: 'value',
horAlign: 'right',
},
],
};
return new this.Klass(attr);
};
this.withEmptyData = () => {
const attr = {
data: [],
......
......@@ -5314,6 +5314,14 @@
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
},
"dependencies": {
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==",
"dev": true
}
}
},
"@emotion/unitless": {
......@@ -5370,6 +5378,14 @@
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
},
"dependencies": {
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==",
"dev": true
}
}
},
"@emotion/unitless": {
......@@ -5479,6 +5495,14 @@
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
},
"dependencies": {
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==",
"dev": true
}
}
},
"@emotion/unitless": {
......@@ -5566,6 +5590,14 @@
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
},
"dependencies": {
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==",
"dev": true
}
}
},
"@emotion/unitless": {
......@@ -7913,6 +7945,14 @@
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
},
"dependencies": {
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==",
"dev": true
}
}
},
"@emotion/unitless": {
......@@ -9329,6 +9369,14 @@
"requires": {
"@types/prop-types": "*",
"csstype": "^2.2.0"
},
"dependencies": {
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==",
"dev": true
}
}
},
"@types/react-redux": {
......@@ -13295,6 +13343,13 @@
"csstype": "^2.5.2",
"stylis": "^3.5.0",
"stylis-rule-sheet": "^0.0.10"
},
"dependencies": {
"csstype": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz",
"integrity": "sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A=="
}
}
},
"create-hash": {
......@@ -13565,7 +13620,7 @@
},
"css-select": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"dev": true,
"requires": {
......@@ -13684,11 +13739,6 @@
}
}
},
"csstype": {
"version": "2.6.10",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz",
"integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w=="
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
......@@ -14094,7 +14144,7 @@
"dependencies": {
"domelementtype": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
"integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
"dev": true
}
......@@ -20570,6 +20620,16 @@
"walker": "^1.0.7"
},
"dependencies": {
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"dev": true,
"optional": true,
"requires": {
"minipass": "^2.6.0"
}
},
"fsevents": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
......@@ -21121,6 +21181,16 @@
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"dev": true,
"optional": true,
"requires": {
"minipass": "^2.9.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
......@@ -21146,7 +21216,9 @@
"optional": true,
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
......@@ -33551,7 +33623,7 @@
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment