<commit_before>import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { Divider } from '@blueprintjs/core';
import _ from 'lodash';
import { compose } from 'redux';
import { connect } from 'react-redux';
import queryString from 'query-string';
import { EdgeCreateDialog, TableEditor } from '@alephdata/react-ftm';

import withRouter from 'app/withRouter';
import entityEditorWrapper from 'components/Entity/entityEditorWrapper';
import { Count, ErrorSection, QueryInfiniteLoad } from 'components/common';
import { DialogToggleButton } from 'components/Toolbar';
import EntitySetSelector from 'components/EntitySet/EntitySetSelector';
import DocumentSelectDialog from 'dialogs/DocumentSelectDialog/DocumentSelectDialog';
import EntityActionBar from 'components/Entity/EntityActionBar';
import EntityDeleteButton from 'components/Toolbar/EntityDeleteButton';
import { queryEntities } from 'actions';
import { selectEntitiesResult } from 'selectors';
import { showErrorToast, showSuccessToast } from 'app/toast';
import getEntityLink from 'util/getEntityLink';

import './EntityTable.scss';

const messages = defineMessages({
  search_placeholder: {
    id: 'entity.manager.search_placeholder',
    defaultMessage: 'Search {schema}',
  },
  empty: {
    id: 'entity.manager.search_empty',
    defaultMessage: 'No matching {schema} results found',
  },
  edge_create_success: {
    id: 'entity.manager.edge_create_success',
    defaultMessage: 'Successfully linked {source} and {target}',
  },
  add_to_success: {
    id: 'entity.manager.entity_set_add_success',
    defaultMessage:
      'Successfully added {count} {count, plural, one {entity} other {entities}} to {entitySet}',
  },
  bulk_import: {
    id: 'entity.viewer.bulk_import',
    defaultMessage: 'Bulk import',
  },
  add_link: {
    id: 'entity.viewer.add_link',
    defaultMessage: 'Create link',
  },
  add_to: {
    id: 'entity.viewer.add_to',
    defaultMessage: 'Add to...',
  },
});

export class EntityTable extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selection: [],
    };
    this.updateQuery = this.updateQuery.bind(this);
    this.updateSelection = this.updateSelection.bind(this);
    this.onSortColumn = this.onSortColumn.bind(this);
    this.onSearchSubmit = this.onSearchSubmit.bind(this);
    this.onEdgeCreate = this.onEdgeCreate.bind(this);
    this.onDocSelected = this.onDocSelected.bind(this);
    this.getEntity = this.getEntity.bind(this);
    this.clearSelection = this.clearSelection.bind(this);
  }

  componentDidMount() {
    this.fetchIfNeeded();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.schema !== this.props.schema) {
      this.clearSelection();
    }
    this.fetchIfNeeded();
  }

  fetchIfNeeded() {
    const { query, result } = this.props;
    if (result.shouldLoad) {
      this.props.queryEntities({ query });
    }
  }

  updateQuery(newQuery) {
    const { navigate, location } = this.props;
    navigate({
      pathname: location.pathname,
      search: newQuery.toLocation(),
      hash: location.hash,
    });
  }

  updateSelection(entityIds, newVal) {
    this.setState(({ selection }) => {
      let newSelection;
      if (newVal) {
        newSelection = [...new Set([...selection, ...entityIds])];
      } else {
        newSelection = selection.filter((id) => entityIds.indexOf(id) < 0);
      }
      return { selection: newSelection };
    });
  }

  onEntityClick = (entity) => {
    if (entity) {
      const { navigate } = this.props;
      navigate(getEntityLink(entity));
    }
  };

  onSortColumn(newField) {
    const { query, sort } = this.props;
    const { field: currentField, direction } = sort;

    if (currentField !== newField) {
      return this.updateQuery(query.sortBy(`properties.${newField}`, 'asc'));
    }

    // Toggle through sorting states: ascending, descending, or unsorted.
    this.updateQuery(
      query.sortBy(
        `properties.${currentField}`,
        direction === 'asc' ? 'desc' : undefined
      )
    );
  }

  onSearchSubmit(queryText) {
    const { query } = this.props;
    const newQuery = query.set('q', queryText);
    this.updateQuery(newQuery);
  }

  async onEdgeCreate(source, target, type) {
    const { entityManager, intl } = this.props;
    if (!source || !target || !type) {
      return;
    }

    try {
      if (type.property) {
        source.setProperty(type.property, target.id);
        entityManager.updateEntity(source);
      } else if (type.schema?.edge) {
        await entityManager.createEntity({
          schema: type.schema,
          properties: {
            [type.schema.edge.source]: source.id,
            [type.schema.edge.target]: target.id,
          },
        });
      }
      showSuccessToast(
        intl.formatMessage(messages.edge_create_success, {
          source: source.getCaption(),
          target: target.getCaption(),
        })
      );
      this.setState({ selection: [] });
    } catch (e) {
      showErrorToast(e);
    }
  }

  onDocSelected(table) {
    if (!table?.id) return;
    const { navigate, schema } = this.props;
    const pathname = getEntityLink(table);
    navigate({
      pathname,
      hash: queryString.stringify({ mode: 'mapping', schema: schema.name }),
    });
  }

  clearSelection() {
    this.setState({ selection: [] });
  }

  getEntity(entityId) {
    return this.props.result.results.find(({ id }) => entityId === id);
  }

  render() {
    const {
      collection,
      entityManager,
      query,
      intl,
      result,
      schema,
      isEntitySet,
      sort,
      updateStatus,
      writeable,
    } = this.props;
    const { selection } = this.state;
    const visitEntity = schema.isThing() ? this.onEntityClick : undefined;<commit_msg>Always link to entites referenced in entity tables<commit_after>import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { Divider } from '@blueprintjs/core';
import _ from 'lodash';
import { compose } from 'redux';
import { connect } from 'react-redux';
import queryString from 'query-string';
import { EdgeCreateDialog, TableEditor } from '@alephdata/react-ftm';

import withRouter from 'app/withRouter';
import entityEditorWrapper from 'components/Entity/entityEditorWrapper';
import { Count, ErrorSection, QueryInfiniteLoad } from 'components/common';
import { DialogToggleButton } from 'components/Toolbar';
import EntitySetSelector from 'components/EntitySet/EntitySetSelector';
import DocumentSelectDialog from 'dialogs/DocumentSelectDialog/DocumentSelectDialog';
import EntityActionBar from 'components/Entity/EntityActionBar';
import EntityDeleteButton from 'components/Toolbar/EntityDeleteButton';
import { queryEntities } from 'actions';
import { selectEntitiesResult } from 'selectors';
import { showErrorToast, showSuccessToast } from 'app/toast';
import getEntityLink from 'util/getEntityLink';

import './EntityTable.scss';

const messages = defineMessages({
  search_placeholder: {
    id: 'entity.manager.search_placeholder',
    defaultMessage: 'Search {schema}',
  },
  empty: {
    id: 'entity.manager.search_empty',
    defaultMessage: 'No matching {schema} results found',
  },
  edge_create_success: {
    id: 'entity.manager.edge_create_success',
    defaultMessage: 'Successfully linked {source} and {target}',
  },
  add_to_success: {
    id: 'entity.manager.entity_set_add_success',
    defaultMessage:
      'Successfully added {count} {count, plural, one {entity} other {entities}} to {entitySet}',
  },
  bulk_import: {
    id: 'entity.viewer.bulk_import',
    defaultMessage: 'Bulk import',
  },
  add_link: {
    id: 'entity.viewer.add_link',
    defaultMessage: 'Create link',
  },
  add_to: {
    id: 'entity.viewer.add_to',
    defaultMessage: 'Add to...',
  },
});

export class EntityTable extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selection: [],
    };
    this.updateQuery = this.updateQuery.bind(this);
    this.updateSelection = this.updateSelection.bind(this);
    this.onSortColumn = this.onSortColumn.bind(this);
    this.onSearchSubmit = this.onSearchSubmit.bind(this);
    this.onEdgeCreate = this.onEdgeCreate.bind(this);
    this.onDocSelected = this.onDocSelected.bind(this);
    this.getEntity = this.getEntity.bind(this);
    this.clearSelection = this.clearSelection.bind(this);
  }

  componentDidMount() {
    this.fetchIfNeeded();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.schema !== this.props.schema) {
      this.clearSelection();
    }
    this.fetchIfNeeded();
  }

  fetchIfNeeded() {
    const { query, result } = this.props;
    if (result.shouldLoad) {
      this.props.queryEntities({ query });
    }
  }

  updateQuery(newQuery) {
    const { navigate, location } = this.props;
    navigate({
      pathname: location.pathname,
      search: newQuery.toLocation(),
      hash: location.hash,
    });
  }

  updateSelection(entityIds, newVal) {
    this.setState(({ selection }) => {
      let newSelection;
      if (newVal) {
        newSelection = [...new Set([...selection, ...entityIds])];
      } else {
        newSelection = selection.filter((id) => entityIds.indexOf(id) < 0);
      }
      return { selection: newSelection };
    });
  }

  onEntityClick = (entity) => {
    if (entity) {
      const { navigate } = this.props;
      navigate(getEntityLink(entity));
    }
  };

  onSortColumn(newField) {
    const { query, sort } = this.props;
    const { field: currentField, direction } = sort;

    if (currentField !== newField) {
      return this.updateQuery(query.sortBy(`properties.${newField}`, 'asc'));
    }

    // Toggle through sorting states: ascending, descending, or unsorted.
    this.updateQuery(
      query.sortBy(
        `properties.${currentField}`,
        direction === 'asc' ? 'desc' : undefined
      )
    );
  }

  onSearchSubmit(queryText) {
    const { query } = this.props;
    const newQuery = query.set('q', queryText);
    this.updateQuery(newQuery);
  }

  async onEdgeCreate(source, target, type) {
    const { entityManager, intl } = this.props;
    if (!source || !target || !type) {
      return;
    }

    try {
      if (type.property) {
        source.setProperty(type.property, target.id);
        entityManager.updateEntity(source);
      } else if (type.schema?.edge) {
        await entityManager.createEntity({
          schema: type.schema,
          properties: {
            [type.schema.edge.source]: source.id,
            [type.schema.edge.target]: target.id,
          },
        });
      }
      showSuccessToast(
        intl.formatMessage(messages.edge_create_success, {
          source: source.getCaption(),
          target: target.getCaption(),
        })
      );
      this.setState({ selection: [] });
    } catch (e) {
      showErrorToast(e);
    }
  }

  onDocSelected(table) {
    if (!table?.id) return;
    const { navigate, schema } = this.props;
    const pathname = getEntityLink(table);
    navigate({
      pathname,
      hash: queryString.stringify({ mode: 'mapping', schema: schema.name }),
    });
  }

  clearSelection() {
    this.setState({ selection: [] });
  }

  getEntity(entityId) {
    return this.props.result.results.find(({ id }) => entityId === id);
  }

  render() {
    const {
      collection,
      entityManager,
      query,
      intl,
      result,
      schema,
      isEntitySet,
      sort,
      updateStatus,
      writeable,
    } = this.props;
    const { selection } = this.state;
    const visitEntity = this.onEntityClick;
{'fraction_non_alphanumeric': 0.1344732771953944, 'fraction_numerical': 0.00017185083347654237, 'mean_word_length': 3.099682986967242, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6951617', 'n_tokens_mistral': 3512, 'n_tokens_neox': 3504, 'n_words': 724}