UNPKG

instantjob-recruiter-client

Version:

a set of tools for creating an instantjob recruiter react client

483 lines (455 loc) 16.7 kB
import React, {Component} from 'react' import {connect} from 'react-redux' import classNames from 'classnames/bind' import { MdCheckBox, MdCheckBoxOutlineBlank, MdAddCircle, MdKeyboardArrowDown, MdPerson, } from 'react-icons/lib/md' import {ItemsCounter, Count} from './styles.react.scss' import SelectedItems from 'components/filterable_list/selected_items' import NonExclusiveField from 'components/non_exclusive_field' import CommentField from 'components/comment_field' import FileField from 'components/file_field' import Filter from 'components/filter' import Button from 'components/utils/button' import List from 'components/list' import SearchBar from 'components/search_bar' import HorizontalScrollableView from 'components/utils/horizontal_scrollable_view' import NonExclusiveFieldFilter from 'components/non_exclusive_field_filter' import SecondaryActions from 'components/utils/secondary_actions' import {tolerant_equal} from 'selectors/base' import {get_fields} from 'selectors/fields' import { action_create_value, new_values_fuse, new_comments_fuse, allow_creating_value, } from 'common/fields' import store from 'common/store' import auto_bind from 'common/auto_bind' import request from 'common/request' import persistent_state from 'common/persistent_state' import event_system from 'common/event_system' import { range, set_from_array, array_contains, make_persistent, map_hash, for_all, array_from_set, array_sum, empty_set, set_count, } from 'common/utilities' import {alert_failure, alert_warning, show_popover, dismiss_popover} from 'actions/display' import {update_field, remove_field} from 'actions/fields' const cx = classNames.bind(require('styles/filterable_table.scss')) const get_values_with_selection = make_persistent( (values, selected, locked, existing_values) => { const existing_value_ids = {} existing_values.forEach((values) => values.forEach(({id}) => existing_value_ids[id] = true)) return values.filter(({id}) => existing_value_ids[id] || selected[id] || locked[id]) .map((value) => ({ ...value, selected: selected[value.id], locked: locked[value.id], })) } ) const compare = (a, b, null_values = [], ascending = true) => { let is_null = (c) => array_contains([null, undefined, ...null_values], c, true) let a_is_null = is_null(a), b_is_null = is_null(b) if (b_is_null) { if (a_is_null) { return 0 } else { return -1 } } else { if (a_is_null) { return 1 } else { if (a > b) { return ascending ? 1 : -1 } else if (a < b) { return ascending ? -1 : 1 } else { return 0 } } } } let counter = 0 class FilterableTable extends Component { constructor(props) { super(props) this.state = { comment_filtered_item_ids: {}, value_filters: {}, order_by: null, order_by_field: null, ascending: true, edit_field_mode: false, ...persistent_state.get(props.persistent_state_key), } auto_bind(this) } componentDidMount() { this.mounted = true this.props.get_table_ref(this) this.selected_items_box = `filterable_table_selected_items_${counter++}` } componentWillUnmount() { this.mounted = false persistent_state.store(this.props.persistent_state_key, this.state) } componentWillReceiveProps({items, selected}) { if (items !== this.props.items || selected !== this.props.selected) { event_system.post(this.selected_items_box, array_from_set(selected).map((id) => items[id])) } } start_edit_field(field_id) { this.setState({edit_field_mode: field_id}) } on_comment_query_change(field_id, query, results) { if (this.mounted && !tolerant_equal(results, this.state.comment_filtered_item_ids[field_id])) { this.setState({ comment_filtered_item_ids: { ...this.state.comment_filtered_item_ids, [field_id]: results, }, }) } } toggle_filtered_value(value) { if (this.mounted) { this.setState({value_filters: {...this.state.value_filters, [value.id]: !this.state.value_filters[value.id]}}) } } order_by(order_by, ascending = null) { if (this.mounted) { if (this.state.order_by == order_by) { this.setState({ascending: ascending === null ? !this.state.ascending : ascending}) } else { this.setState({order_by, ascending: ascending === null ? true : ascending}) } } } order_by_comment(field_id) { if (this.mounted) { if (this.state.order_by == 'comment' && this.state.order_by_field == field_id) { this.setState({ascending: !this.state.ascending}) } else { this.setState({order_by: 'comment', order_by_field: field_id, ascending: true}) } } } get_filtered_item_ids() { let order = map_hash(this.props.items, (item) => { if (this.state.order_by == 'comment') { let {comments} = this.props.fields[this.state.order_by_field] return comments[item.id] } else { return item[this.state.order_by] } }) return Object.keys(this.state.comment_filtered_item_ids).reduce((filtered_item_ids, field_id) => { let results = this.state.comment_filtered_item_ids[field_id] if (results) { let results_set = set_from_array(results) return filtered_item_ids.filter((id) => results_set[id]) } else { return filtered_item_ids } }, [...array_from_set(this.state.value_filters), ...array_from_set(this.props.locked_values)].reduce((filtered_item_ids, value_id) => { return filtered_item_ids.filter(this.props.filter_value(value_id)) }, this.props.filtered_item_ids)) .filter((id) => this.props.items[id]) .sort((id1, id2) => compare(order[id1], order[id2], ["", " "], this.state.ascending)) } view_selected() { const {on_card_click, render_card} = this.props store.dispatch(show_popover( SelectedItems, { on_card_click, render_card, box: this.selected_items_box, }, "" )) } render() { let {actions, selectable, selected, create_field} = this.props let fake_state = {fields: this.props} let fields = get_fields(fake_state) let filtered_item_ids = this.get_filtered_item_ids() let all_selected = for_all(filtered_item_ids, (id) => this.props.selected[id]) let secondary_actions_active = array_sum(actions.map(({badge}) => badge || 0)) return ( <div className={cx('filterable-table', {'filterable-table_selectable': this.props.selectable})}> {this.props.title ? ( <div className={cx('filterable-table__top')}> <div className={cx('filterable-table__top-title')}> {this.props.title} </div> <div className={cx('filterable-table__top-actions')}> {empty_set(selected) ? null : ( <ItemsCounter onClick={this.view_selected}> <Count> {set_count(selected)} </Count> <MdPerson /> </ItemsCounter> )} {actions.length > 0 ? ( <SecondaryActions active={secondary_actions_active}> {actions} </SecondaryActions> ) : null} {this.props.main_action ? ( <Button {...this.props.main_action} /> ) : null} </div> </div> ) : null} <HorizontalScrollableView> <div className={cx('filterable-table__main')}> <div className={cx('filterable-table__header')}> {this.props.selectable ? ( <div className={cx('filterable-table__all-items')}> <div className={cx('filterable-table__items-count')}> {filtered_item_ids.length} </div> <div className={cx('filterable-table__select-all-checkbox', {'filterable-table__select-all-checkbox_selected': all_selected})} onClick={() => this.props.on_select_all(filtered_item_ids)}> {all_selected ? ( <MdCheckBox /> ) : ( <MdCheckBoxOutlineBlank /> )} </div> </div> ) : null} <div className={cx('filterable-table__card-header')}> {this.props.children} </div> <div className={cx('filterable-table__fields-header')}> {fields.map((field) => { let props = { key: field.id, id: field.id, name: field.name, } switch (field.category) { case 'non_exclusive': return ( <NonExclusiveFieldFilter {...props} values={get_values_with_selection( field.values, this.state.value_filters, this.props.locked_values, filtered_item_ids.map((item_id) => this.props.get_item_field_values(item_id, field.id)) )} toggle_filtered_value={this.toggle_filtered_value} className={cx('filterable-table__filter_field')} stop_recycling={() => this.props.stop_recycling(field.id)} /> ) case 'comment': return ( <CommentFieldFilter {...props} comments={Object.keys(field.comments).map((id) => ({id, comment: field.comments[id]}))} default_results={Object.keys(this.props.items)} on_query_change={(query, results) => this.on_comment_query_change(field.id, query, results)} sort_items={() => this.order_by_comment(field.id)} /> ) case 'file': return ( <FileFieldFilter {...props} /> ) default: return null } })} {this.props.editable && create_field ? ( <Button icon={MdAddCircle} onClick={create_field} discreet className={cx('filterable-table__add-field-button')}> Nouveau Champ </Button> ) : null} </div> </div> <div className={cx('filterable-table__rows')}> <List item_height={60}> {filtered_item_ids.map((id) => ( <Item key={id} item={this.props.items[id]} fields={fields} get_item_field_values={this.props.get_item_field_values} add_value={(value_id) => this.props.add_value(id, value_id)} remove_value={(value_id) => this.props.remove_value(id, value_id)} update_comment={(field_id, comment) => this.props.update_comment(id, field_id, comment)} editable={this.props.editable} selectable={this.props.selectable} selected={this.props.selected[id]} render_card={this.props.render_card} on_card_click={() => this.props.on_card_click(id)} item_type={this.props.item_type} /> ))} </List> </div> </div> </HorizontalScrollableView> </div> ) } } class Item extends Component { shouldComponentUpdate(props) { return props.item !== this.props.item || props.fields !== this.props.fields || props.selected !== this.props.selected } render() { return ( <div className={cx('filterable-table__item')}> <div className={cx('filterable-table__card', {'filterable-table__card_selected': this.props.selected})} onClick={this.props.on_card_click}> {this.props.render_card(this.props.item)} </div> {this.props.fields.map((field) => { let props = { key: field.id, ...field, editable: this.props.editable, edit_mode: 'click', show_label: false, } switch (field.category) { case "non_exclusive": return ( <ItemNonExclusiveField {...props} selected_values={this.props.get_item_field_values(this.props.item.id, field.id)} add_value={this.props.add_value} remove_value={this.props.remove_value} allow_creating_values={allow_creating_value(this.props.item_type, field)} /> ) case "comment": return ( <ItemCommentField {...props} comment={field.comments[this.props.item.id]} update_comment={(comment) => this.props.update_comment(field.id, comment)} /> ) case "file": return ( <ItemFileField {...props} file={field.files[this.props.item.id]} user_id={this.props.item.id} /> ) default: return null } })} <div key="dummy1" className={cx('filterable-table__field')}> </div> <div key="dummy2" className={cx('filterable-table__field')}> </div> </div> ) } } const ItemNonExclusiveField = (props) => ( <div className={cx('filterable-table__field')}> <NonExclusiveField {...props} ellipsis={3} value_ellipsis={65} field_ellipsis={150} on_select_value={props.add_value} on_deselect_value={props.remove_value} create_value={(value_name) => new Promise((resolve, reject) => { let characters = value_name.split("") if (array_contains(characters, ',') || array_contains(characters, ';')) { store.dispatch(alert_failure(`Les caractères ',' et ';' sont interdits dans le nom d'une valeur.`)) reject() } else { action_create_value(props.id, value_name) .then(resolve) } })} /> </div> ) const ItemCommentField = (props) => ( <div className={cx('filterable-table__field')}> <CommentField {...props} comment_ellipsis={300} on_submit_comment={props.update_comment} /> </div> ) const ItemFileField = (props) => ( <div className={cx('filterable-table__field')}> <FileField {...props} /> </div> ) class CommentFieldFilter extends Component { constructor(props) { super(props) this.state = { active: false, } this.on_query_change = this.on_query_change.bind(this) } on_query_change(query, results) { if (this.state.active == (query == "")) { this.setState({active: !this.state.active}) } this.props.on_query_change(query, results) } render() { return ( <Filter className={cx('filterable-table__filter', 'filterable-table__filter_field')} active={this.state.active} label={this.props.name} onClick={this.props.sort_items} > <SearchBar ref={(search_bar) => this.search_bar = search_bar} className={cx('filterable-table__filter-search-bar')} extend={false} make_fuse={new_comments_fuse} items={this.props.comments} default_results={this.props.default_results} on_query_change={this.on_query_change} /> </Filter> ) } } const FileFieldFilter = ({name}) => ( <Filter className={cx('filterable-table__filter', 'filterable-table__filter_field')} active={false} label={name} /> ) FilterableTable.defaultProps = { items: {}, fields: {}, values: {}, on_card_click: (item_id) => {}, editable: true, selectable: false, shareable_with_users: false, selected: {}, locked_values: {}, on_select_all: (filtered_item_ids) => {}, get_item_field_values: (item_id, field_id) => null, filter_value: (value_id) => (id) => true, actions: [], get_table_ref: (table) => {}, item_type: null, persistent_state_key: "", } export default FilterableTable