Source: @@components/controls/file-list.js

/**
 * @file File list for displaying user's files
 * @module @@components/controls/file-list
 */
import React, { Component } from 'react';
import { message, Button, Input, Tree, Space, Popover, Modal } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { curry } from 'ramda';
import path from 'path';
import {
  getFiles, getSessionValid,
  getUser
} from '@@selectors';
import { connect, Provider } from 'react-redux';
import { store } from '@@app-state';
import { deleteFile, loadFiles, saveViewByUri, getFileUrl } from '@@actions/solid/files';
import { loadGraphFromURL } from '@@actions/load';
import { openFileOptionsInfoModal } from '@@components/controls/modal-info-edit-file';
import { translated } from '@@localization';

const newViewSuffix = '__newView'; // Used to add a key to the rendered empty node in the same folder so that it doesn't clash with the folder's key itself

class FileList extends Component {
  state = {
    creatingView: false,
    folderWithNewView: ''
  };

  fileFetchMap = {};

  saveNewView = async key => {
    saveViewByUri(await getFileUrl(key), this.props.permissions)
      .then(() => Modal.destroyAll());
  }

  onLoadView = async uri => {
    loadGraphFromURL({modelURL: await getFileUrl(uri)})
      .then(({modelURL, hasPermissions}) => {
        Modal.destroyAll();
        openFileOptionsInfoModal({modelURL, hasPermissions})
      });
  }

  onDeleteFile = uri => deleteFile(uri);

  getNewFileInput = ({key}) => {
    const ref = React.createRef();
    const onSave = () => {
      this.saveNewView(path.join(key, ref.current.input.value));
      ref.current.setValue('');
      this.setState({folderWithNewView: ''});
    }
    return <>
      <Input
        autoFocus
        ref={ref}
        style={{width: 128}}
        name="View name"
        autoComplete="off"
        type="text"
        onPressEnter={onSave}
        placeholder="New view name"
      />
      <Button onClick={onSave}>{translated('Save')}</Button>
    </>
  }

  getCreateNewFileIcon = ({key}) => {
    if (this.props.canSave) {
      return <PlusOutlined onClick={() => {this.setState({folderWithNewView: key})}} style={{color: 'blue'}}/>;
    }
  };

  getFileWithControls = ({title, key}) => {
    return (
      <Space size={4}>
        <Popover
          onConfirm={() => this.saveNewView(key)}
          okText="Save"
          trigger="click"
          cancelText="Cancel"
          content={<Space direction="horizontal">
            {this.props.canSave && <Button danger onClick={() => this.saveNewView(key)}>{translated('Overwrite')}</Button>}
            {this.props.canLoad && <Button onClick={() => this.onLoadView(key)}>{translated('Load')}</Button>}
            <Button danger onClick={() => this.onDeleteFile(key)}>{translated('Delete')}</Button>
          </Space>}
        >
          {title}
        </Popover>
      </Space>
    );
  };

  renderTreeNode = ({title, isLeaf, key, isNewFileInput, isNewFilePrompt}) => {
    key = key.replace(newViewSuffix, '');
    if (isNewFileInput) {
      return this.getNewFileInput({key});
    }

    if (isNewFilePrompt) {
      return this.getCreateNewFileIcon({key});
    }

    if (!isLeaf) {
      return <span>{title}</span>;
    }

    return this.getFileWithControls({title, key});
  }

  loadTreeNodeData = ({ key, children }) => {
    return new Promise(resolve => {
      if (children) {
        resolve();
        return;
      }

      loadFiles(key);

      this.fileFetchMap[key] = {
        resolve,
        timeout: setTimeout(() => {
          message.error(translated('There was a problem fetching files from the folder.'));
          resolve();
        }, 10000)
      }
    });
  };

  getFileSubtree = curry((curr, [title, content]) => {
    const key = path.join(curr, title);

    const {timeout, resolve} = (this.fileFetchMap[key] || {});
    const {__loaded, ...rest} = (content || {});
    if (resolve && __loaded) {
      resolve();
      clearTimeout(timeout);
      delete this.fileFetchMap[key];
    }
    let children = null;
    const isLeaf = content === null;
    const folderInitialized = !isLeaf && __loaded;
    if (folderInitialized) {
      const isNewFileInput = key === this.state.folderWithNewView && this.props.canSave;

      children = Object.entries(rest).map(this.getFileSubtree(key));
      if (this.props.canSave) {
        children.push({isNewFilePrompt: !isNewFileInput, isNewFileInput, key: `${key}${newViewSuffix}`, isLeaf: true});
      }
    }

    return {
      title,
      key,
      children,
      isLeaf
    };
  });

  getFileTreeData() {
    const {files} = this.props;
    return Object.entries(files).map(this.getFileSubtree(''));
  }

  render() {
    return <Tree.DirectoryTree
      showLine={{showLeafIcon: false}}
      selectable={false}
      titleRender={this.renderTreeNode}
      loadData={this.loadTreeNodeData}
      defaultExpandedKeys={['/']}
      treeData={this.getFileTreeData()}
    />
  }
}


const mapStateToProps = appState => ({
  user: getUser(appState),
  files: getFiles(appState),
  sessionValid: getSessionValid(appState)
});

// Connect component to enable use in modal content
const Connected = connect(mapStateToProps, null)(FileList);

export default props => <Provider store={store}><Connected {...props}/></Provider>;