193 lines
6.8 KiB
TypeScript
193 lines
6.8 KiB
TypeScript
import * as _ from 'lodash';
|
||
import CircularProgress from 'material-ui/CircularProgress';
|
||
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table';
|
||
import * as React from 'react';
|
||
|
||
import { Retry } from 'ts/components/ui/retry';
|
||
import { Text } from 'ts/components/ui/text';
|
||
import { HeaderItem } from 'ts/pages/jobs/list/header_item';
|
||
import { ListItem } from 'ts/pages/jobs/list/list_item';
|
||
import { colors } from 'ts/style/colors';
|
||
import { styled } from 'ts/style/theme';
|
||
import { ScreenWidths, WebsiteBackendJobInfo } from 'ts/types';
|
||
import { backendClient } from 'ts/utils/backend_client';
|
||
|
||
const labelStyle = { fontFamily: 'Roboto Mono', fontSize: 18 };
|
||
const HEADER_TEXT = 'Open Positions';
|
||
const TABLE_ROW_MIN_HEIGHT = 100;
|
||
|
||
export interface OpenPositionsProps {
|
||
hash: string;
|
||
screenWidth: ScreenWidths;
|
||
}
|
||
export interface OpenPositionsState {
|
||
jobInfos?: WebsiteBackendJobInfo[];
|
||
error?: Error;
|
||
}
|
||
|
||
export class OpenPositions extends React.Component<OpenPositionsProps, OpenPositionsState> {
|
||
private _isUnmounted: boolean;
|
||
constructor(props: OpenPositionsProps) {
|
||
super(props);
|
||
this._isUnmounted = false;
|
||
this.state = {
|
||
jobInfos: undefined,
|
||
error: undefined,
|
||
};
|
||
}
|
||
public componentWillMount(): void {
|
||
// tslint:disable-next-line:no-floating-promises
|
||
this._fetchJobInfosAsync();
|
||
}
|
||
public componentWillUnmount(): void {
|
||
this._isUnmounted = true;
|
||
}
|
||
public render(): React.ReactNode {
|
||
const isReadyToRender = _.isUndefined(this.state.error) && !_.isUndefined(this.state.jobInfos);
|
||
return (
|
||
<div id={this.props.hash} className="mx-auto max-width-4">
|
||
{isReadyToRender ? this._renderBody() : this._renderLoading()}
|
||
</div>
|
||
);
|
||
}
|
||
private _renderBody(): React.ReactNode {
|
||
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
|
||
return isSmallScreen ? this._renderList() : this._renderTable();
|
||
}
|
||
private _renderLoading(): React.ReactNode {
|
||
return (
|
||
// TODO: consolidate this loading component with the one in portal and RelayerIndex
|
||
// TODO: possibly refactor into a generic loading container with spinner and retry UI
|
||
<div className="center">
|
||
{_.isUndefined(this.state.error) ? (
|
||
<CircularProgress size={40} thickness={5} />
|
||
) : (
|
||
<Retry onRetry={this._fetchJobInfosAsync.bind(this)} />
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
private _renderList(): React.ReactNode {
|
||
return (
|
||
<div style={{ backgroundColor: colors.jobsPageBackground }}>
|
||
<HeaderItem headerText={HEADER_TEXT} />
|
||
{_.map(this.state.jobInfos, jobInfo => (
|
||
<JobInfoListItem
|
||
key={jobInfo.id}
|
||
title={jobInfo.title}
|
||
description={jobInfo.department}
|
||
onClick={this._openJobInfoUrl.bind(this, jobInfo)}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
private _renderTable(): React.ReactNode {
|
||
return (
|
||
<div>
|
||
<HeaderItem headerText={HEADER_TEXT} />
|
||
<Table selectable={false} onCellClick={this._onCellClick.bind(this)}>
|
||
<TableHeader displaySelectAll={false} adjustForCheckbox={false}>
|
||
<TableRow>
|
||
<TableHeaderColumn colSpan={5} style={labelStyle}>
|
||
Position
|
||
</TableHeaderColumn>
|
||
<TableHeaderColumn colSpan={3} style={labelStyle}>
|
||
Department
|
||
</TableHeaderColumn>
|
||
<TableHeaderColumn colSpan={4} style={labelStyle}>
|
||
Office
|
||
</TableHeaderColumn>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody displayRowCheckbox={false} showRowHover={true}>
|
||
{_.map(this.state.jobInfos, jobInfo => {
|
||
return this._renderJobInfoTableRow(jobInfo);
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
);
|
||
}
|
||
private _renderJobInfoTableRow(jobInfo: WebsiteBackendJobInfo): React.ReactNode {
|
||
return (
|
||
<TableRow
|
||
key={jobInfo.id}
|
||
hoverable={true}
|
||
displayBorder={false}
|
||
style={{ height: TABLE_ROW_MIN_HEIGHT, border: 2 }}
|
||
>
|
||
<TableRowColumn colSpan={5} style={labelStyle}>
|
||
{jobInfo.title}
|
||
</TableRowColumn>
|
||
<TableRowColumn colSpan={3} style={labelStyle}>
|
||
{jobInfo.department}
|
||
</TableRowColumn>
|
||
<TableRowColumn colSpan={4} style={labelStyle}>
|
||
{jobInfo.office}
|
||
</TableRowColumn>
|
||
</TableRow>
|
||
);
|
||
}
|
||
private async _fetchJobInfosAsync(): Promise<void> {
|
||
try {
|
||
if (!this._isUnmounted) {
|
||
this.setState({
|
||
jobInfos: undefined,
|
||
error: undefined,
|
||
});
|
||
}
|
||
const jobInfos = await backendClient.getJobInfosAsync();
|
||
if (!this._isUnmounted) {
|
||
this.setState({
|
||
jobInfos,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
if (!this._isUnmounted) {
|
||
this.setState({
|
||
error,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
private _onCellClick(rowNumber: number): void {
|
||
if (_.isUndefined(this.state.jobInfos)) {
|
||
return;
|
||
}
|
||
const jobInfo = this.state.jobInfos[rowNumber];
|
||
this._openJobInfoUrl(jobInfo);
|
||
}
|
||
|
||
private _openJobInfoUrl(jobInfo: WebsiteBackendJobInfo): void {
|
||
const url = jobInfo.url;
|
||
window.open(url, '_blank');
|
||
}
|
||
}
|
||
|
||
export interface JobInfoListItemProps {
|
||
title?: string;
|
||
description?: string;
|
||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||
}
|
||
|
||
const PlainJobInfoListItem: React.StatelessComponent<JobInfoListItemProps> = ({ title, description, onClick }) => (
|
||
<div className="mb3" onClick={onClick}>
|
||
<ListItem>
|
||
<Text fontWeight="bold" fontSize="16px" fontColor={colors.mediumBlue}>
|
||
{title + ' ›'}
|
||
</Text>
|
||
<Text className="pt1" fontSize="16px" fontColor={colors.darkGrey}>
|
||
{description}
|
||
</Text>
|
||
</ListItem>
|
||
</div>
|
||
);
|
||
|
||
export const JobInfoListItem = styled(PlainJobInfoListItem)`
|
||
cursor: pointer;
|
||
&:hover {
|
||
opacity: 0.5;
|
||
}
|
||
`;
|