<commit_before>##############################################################################
#
#    CoCalc: Collaborative Calculation in the Cloud
#
#    Copyright (C) 2015 -- 2016, SageMath, Inc.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

{React, ReactDOM, rtypes, rclass, redux, Redux} = require('./smc-react')
{Col, Row, ButtonToolbar, ButtonGroup, MenuItem, Button, Well, FormControl, FormGroup
 ButtonToolbar, Popover, OverlayTrigger, SplitButton, MenuItem, Alert, Checkbox, Breadcrumb} =  require('react-bootstrap')
misc = require('smc-util/misc')
{ActivityDisplay, DeletedProjectWarning, DirectoryInput, Icon, Loading, ProjectState, COLORS,
 SearchInput, TimeAgo, ErrorDisplay, Space, Tip, LoginLink, Footer, CourseProjectExtraHelp} = require('./r_misc')
{SMC_Dropwrapper} = require('./smc-dropzone')
{FileTypeSelector, NewFileButton} = require('./project_new')

{BillingPageLink, BillingPageForCourseRedux, PayCourseFee}     = require('./billing')
{human_readable_size} = require('./misc_page')
{MiniTerminal}        = require('./project_miniterm')
{file_associations}   = require('./editor')
account               = require('./account')
immutable             = require('immutable')
underscore            = require('underscore')
{webapp_client}       = require('./webapp_client')
{AccountPage}         = require('./account_page')
{UsersViewing}        = require('./other-users')
{project_tasks}       = require('./project_tasks')

feature = require('./feature')

Combobox = require('react-widgets/lib/Combobox') # TODO: delete this when the combobox is in r_misc
TERM_MODE_CHAR = '/'

exports.file_action_buttons = file_action_buttons =
        compress :
            name : 'Compress'
            icon : 'compress'
        delete   :
            name : 'Delete'
            icon : 'trash-o'
        rename   :
            name : 'Rename'
            icon : 'pencil'
        duplicate:
            name : 'Duplicate'
            icon : 'clone'
        move     :
            name : 'Move'
            icon : 'arrows'
        copy     :
            name : 'Copy'
            icon : 'files-o'
        share    :
            name : 'Share'
            icon : 'share-square-o'
        download :
            name : 'Download'
            icon : 'cloud-download'

FileCheckbox = rclass
    displayName : 'ProjectFiles-FileCheckbox'

    propTypes :
        name         : rtypes.string
        checked      : rtypes.bool
        actions      : rtypes.object.isRequired
        current_path : rtypes.string
        style        : rtypes.object

    handle_click: (e) ->
        e.stopPropagation() # so we don't open the file
        full_name = misc.path_to_file(@props.current_path, @props.name)
        if e.shiftKey
            @props.actions.set_selected_file_range(full_name, not @props.checked)
        else
            @props.actions.set_file_checked(full_name, not @props.checked)

        @props.actions.set_most_recent_file_click(full_name)

    render: ->
        <span onClick={@handle_click} style={@props.style}>
            <Icon name={if @props.checked then 'check-square-o' else 'square-o'} fixedWidth style={fontSize:'14pt'}/>
        </span>

# TODO: Something should uniformly describe how sorted table headers work.
# 5/8/2017 We have 3 right now, Course students and assignments panel and this one.
ListingHeader = rclass
    propTypes:
        active_file_sort : rtypes.object    # {column_name : string, is_descending : bool}
        sort_by          : rtypes.func      # Invoked as `sort_by(string)

    render_sort_link: (column_name, display_name) ->
        <a href=''
            onClick={(e)=>e.preventDefault();@props.sort_by(column_name)}>
            {display_name}
            <Space/>
            {<Icon style={marginRight:'10px'}
                name={if @props.active_file_sort.is_descending then 'caret-up' else 'caret-down'}
            /> if @props.active_file_sort.column_name == column_name}
        </a>

    render: ->
        row_styles =
            cursor          : 'pointer'
            color           : '#666'
            backgroundColor : '#fafafa'
            border          : '1px solid #eee'
            borderRadius    : '4px'

        <Row style={row_styles}>
            <Col sm=2 xs=3>
            </Col>
            <Col sm=1 xs=3>
                {@render_sort_link("type", "Type")}
            </Col>
            <Col sm=4 smPush=5 xs=6>
                {@render_sort_link("time", "Date Modified")}
                <span className='pull-right'>
                    {@render_sort_link("size", "Size")}
                </span>
            </Col>
            <Col sm=5 smPull=4 xs=12>
                {@render_sort_link("name", "Name")}
            </Col>
        </Row>

FileRow = rclass
    displayName : 'ProjectFiles-FileRow'

    propTypes :
        name         : rtypes.string.isRequired
        display_name : rtypes.string  # if given, will display this, and will show true filename in popover
        size         : rtypes.number.isRequired
        time         : rtypes.number
        checked      : rtypes.bool
        bordered     : rtypes.bool
        color        : rtypes.string
        mask         : rtypes.bool
        public_data  : rtypes.object
        is_public    : rtypes.bool
        current_path : rtypes.string
        actions      : rtypes.object.isRequired
        no_select    : rtypes.bool

    shouldComponentUpdate: (next) ->
        return @props.name != next.name          or
        @props.display_name != next.display_name or
        @props.size != next.size                 or
        @props.time != next.time                 or
        @props.checked != next.checked           or
        @props.mask != next.mask                 or
        @props.public_data != next.public_data   or
        @props.current_path != next.current_path or
        @props.bordered != next.border           or
        @props.no_select != next.no_select

    render_icon: ->
        # get the file_associations[ext] just like it is defined in the editor
        {file_options} = require('./editor')
        name           = file_options(@props.name)?.icon ? 'file'
        style          =
            color         : if @props.mask then '#bbbbbb'
            verticalAlign : 'sub'
        <a style={style}>
            <Icon name={name} style={fontSize:'14pt'} />
        </a>

    render_name_link: (styles, name, ext) ->
        <a style={styles}>
            <span style={fontWeight: if @props.mask then 'normal' else 'bold'}>{misc.trunc_middle(name,50)}</span>
            <span style={color: if not @props.mask then '#999'}>{if ext is '' then '' else ".#{ext}"}</span>
        </a>

    render_name: ->
        name = @props.display_name ? @props.name
        name_and_ext = misc.separate_file_extension(name)
        name = name_and_ext.name
        ext = name_and_ext.ext

        show_tip = (@props.display_name? and @props.name isnt @props.display_name) or name.length > 50

        styles =
            whiteSpace    : 'pre-wrap'
            wordWrap      : 'break-word'
            overflowWrap  : 'break-word'
            verticalAlign : 'middle'
            color         : if @props.mask then '#bbbbbb'

        if show_tip
            <Tip title={if @props.display_name then 'Displayed filename is an alias. The actual name is:' else '<NAME>'} tip={@props.name}>
                {@render_name_link(styles, name, ext)}
            </Tip>
        else
            @render_name_link(styles, name, ext)

    render_public_file_info_popover: ->
        <Popover title='This file is being shared publicly' id='public_share' >
            <span style={wordWrap:'break-word'}>
                Description: {@props.public_data.description}
            </span>
        </Popover>

    render_public_file_info: ->
        if @props.public_data? and @props.is_public
            <span><Space/>
                <OverlayTrigger
                    trigger   = 'click'
                    rootClose
                    overlay   = {@render_public_file_info_popover()} >
                    <Button
                        bsStyle = 'info'
                        bsSize  = 'xsmall'
                        onClick = {(e)->e.stopPropagation()}
                    >
                        <Icon name='bullhorn' /> <span className='hidden-xs'>Public</span>
                    </Button>
                </OverlayTrigger>
            </span>

    fullpath: ->
        misc.path_to_file(@props.current_path, @props.name)

    handle_mouse_down: (e) ->
        @setState
            selection_at_last_mouse_down : window.getSelection().toString()

    handle_click: (e) ->
        if window.getSelection().toString() == @state.selection_at_last_mouse_down
            foreground = misc.should_open_in_foreground(e)
            @props.actions.open_file
                path       : @fullpath()
                foreground : foreground
            if foreground
                @props.actions.set_file_search('')

    handle_download_click: (e) ->
        e.preventDefault()
        e.stopPropagation()
        @props.actions.download_file
            path : @fullpath()
            log : true

    render: ->
        row_styles =
            cursor          : 'pointer'
            borderRadius    : '4px'
            backgroundColor : @props.color
            borderStyle     : 'solid'
            borderColor     : if @props.bordered then COLORS.BLUE_BG else @props.color

        # See https://github.com/sagemathinc/cocalc/issues/1020
        # support right-click → copy url for the download button
        url_href = project_tasks(@props.actions.project_id).url_href(@fullpath())

        <Row
            style       = {row_styles}
            onMouseDown = {@handle_mouse_down}
            onClick     = {@handle_click}
            className   = {'noselect' if @props.no_select}
        >
            <Col sm=2 xs=3>
                <FileCheckbox
                    name         = {@props.name}
                    checked      = {@props.checked}
                    current_path = {@props.current_path}
                    actions      = {@props.actions}
                    style        = {verticalAlign:'sub'} />
                {@render_public_file_info()}
            </Col>
            <Col sm=1 xs=3>
                {@render_icon()}
            </Col>
            <Col sm=4 smPush=5 xs=6>
                <TimeAgo date={(new Date(@props.time * 1000)).toISOString()} style={color:'#666'}/>
                <span className='pull-right' style={color:'#666'}>
                    {human_readable_size(@props.size)}
                    <Button style   = {marginLeft: '1em', background:'transparent'}
                            bsStyle = 'default'
                            bsSize  = 'xsmall'
                            href    = "#{url_href}"
                            onClick = {@handle_download_click}>
                        <Icon name='cloud-download' style={color: '#666'} />
                    </Button>
                </span>
            </Col>
            <Col sm=5 smPull=4 xs=12>
                {@render_name()}
            </Col>
        </Row>

DirectoryRow = rclass
    displayName : 'ProjectFiles-DirectoryRow'

    propTypes :
        name         : rtypes.string.isRequired
        display_name : rtypes.string  # if given, will display this, and will show true filename in popover
        checked      : rtypes.bool
        color        : rtypes.string
        bordered     : rtypes.bool
        time         : rtypes.number
        mask         : rtypes.bool
        public_data  : rtypes.object
        is_public    : rtypes.bool
        current_path : rtypes.string
        actions      : rtypes.object.isRequired
        no_select    : rtypes.bool

    handle_mouse_down: (e) ->
        @setState
            selection_at_last_mouse_down : window.getSelection().toString()

    handle_click: (e) ->
        if window.getSelection().toString() == @state.selection_at_last_mouse_down
            path = misc.path_to_file(@props.current_path, @props.name)
            @props.actions.open_directory(path)
            @props.actions.set_file_search('')

    render_public_directory_info_popover: ->
        <Popover id={@props.name} title='This folder is being shared publicly' style={wordWrap:'break-word'}>
            Description: {@props.public_data.description}
        </Popover>

    render_public_directory_info: ->
        if @props.public_data? and @props.is_public
            <span><Space/>
                <OverlayTrigger
                    trigger   = 'click'
                    rootClose
                    overlay   = {@render_public_directory_info_popover()} >
                    <Button
                        bsStyle = 'info'
                        bsSize  = 'xsmall'
                        onClick = {(e)->e.stopPropagation()}
                    >
                        <Icon name='bullhorn' /> <span className='hidden-xs'>Public</span>
                    </Button>
                </OverlayTrigger>
            </span>

    render_time: ->
        if @props.time?
            <TimeAgo date={(new Date(@props.time * 1000)).toISOString()} style={color:'#666'} />

    render_name_link: ->
        if (@props.display_name and @props.display_name isnt @props.name) or @props.name.length > 50
            <Tip title={if @props.display_name then 'Displayed directory name is an alias. The actual name is:' else 'Full name'} tip={@props.name}>
                <a style={color : if @props.mask then '#bbbbbb'}>
                    {misc.trunc_middle(@props.display_name ? @props.name, 50)}
                </a>
            </Tip>
        else
            <a style={color : if @props.mask then '#bbbbbb'}>
                {misc.trunc_middle(@props.display_name ? @props.name, 50)}
            </a>

    render: ->
        row_styles =
            cursor          : 'pointer'
            borderRadius    : '4px'
            backgroundColor : @props.color
            borderStyle     : 'solid'
            borderColor     : if @props.bordered then COLORS.BLUE_BG else @props.color

        directory_styles =
            fontWeight     : 'bold'
            whiteSpace     : 'pre-wrap'
            wordWrap       : 'break-word'
            overflowWrap   : 'break-word'
            verticalAlign  : 'sub'

        <Row style={row_styles} onMouseDown={@handle_mouse_down} onClick={@handle_click} className={'noselect' if @props.no_select}>
            <Col sm=2 xs=3>
                <FileCheckbox
                    name         = {@props.name}
                    checked      = {@props.checked}
                    current_path = {@props.current_path}
                    actions      = {@props.actions}
                    style        = {verticalAlign:'sub'} />
                {@render_public_directory_info()}
            </Col>
            <Col sm=1 xs=3>
                <a style={color : if @props.mask then '#bbbbbb'}>
                    <Icon name='folder-open-o' style={fontSize:'14pt',verticalAlign:'sub'} />
                    <Icon name='caret-right' style={marginLeft:'3px',fontSize:'14pt',verticalAlign:'sub'} />
                </a>
            </Col>
            <Col sm=4 smPush=5 xs=6>
                {@render_time()}
                {#size (not applicable for directories)}
            </Col>
            <Col sm=5 smPull=4 xs=12 style={directory_styles}>
                {@render_name_link()}
            </Col>
        </Row>

TerminalModeDisplay = rclass
    render: ->
        <Row style={textAlign:'left', color:'#888', marginTop:'5px', wordWrap:'break-word'} >
            <Col sm=2>
            </Col>
            <Col sm=8>
                <Alert style={marginTop: '5px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: You are in terminal mode.<br/>
                This was caused by the leading / in your file search. If you want to just see your folders, enter a space in front of the /.<br/>
                Terminal mode inside the search bar is experimental and comes with no guarantees about its usability or future existence.
            </Alert>
            </Col>
            <Col sm=2>
            </Col>
        </Row>

NoFiles = rclass
    propTypes :
        actions       : rtypes.object.isRequired
        create_folder : rtypes.func.isRequired
        create_file   : rtypes.func.isRequired
        public_view   : rtypes.bool
        file_search   : rtypes.string
        current_path  : rtypes.string

    displayName : 'ProjectFiles-NoFiles'

    getDefaultProps: ->
        file_search : ''

    # Go to the new file tab if there is no file search
    handle_click: ->
        if @props.file_search.length == 0
            @props.actions.set_active_tab('new')
        else if @props.file_search[@props.file_search.length - 1] == '/'
            @props.create_folder()
        else
            @props.create_file()

    # Returns the full file_search text in addition to the default extension if applicable
    full_path_text: ->
        if @props.file_search.lastIndexOf('.') <= @props.file_search.lastIndexOf('/')
          ext = "sagews"
        if ext and @props.file_search.slice(-1) isnt '/'
            "#{@props.file_search}.#{ext}"
        else
            "#{@props.file_search}"

    # Text for the large create button
    button_text: ->
        if @props.file_search.length == 0
            "Create or upload files..."
        else
            "Create #{@full_path_text()}"

    render_create_button: ->
        <Button
            style   = {fontSize:'40px', color:'#888', maxWidth:'100%'}
            onClick = {=>@handle_click()} >
            <Icon name='plus-circle' /> {@button_text()}
        </Button>

    render_help_alert: ->
        last_folder_index = @props.file_search.lastIndexOf('/')
        if @props.file_search.indexOf('\\') != -1
            <Alert style={marginTop: '10px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: \ is an illegal character
            </Alert>
        else if @props.file_search.indexOf('/') == 0
            <Alert style={marginTop: '10px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: Names cannot begin with /
            </Alert>
        else if ['.', '..'].indexOf(@props.file_search) > -1
            <Alert style={marginTop: '10px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: Cannot create a file named . or ..
            </Alert>
        # Non-empty search and there is a file divisor ('/')
        else if @props.file_search.length > 0 and last_folder_index > 0
            <Alert style={marginTop: '10px'} bsStyle='info'>
                {@render_help_text(last_folder_index)}
            </Alert>

    render_help_text: (last_folder_index) ->
        # Ends with a '/' ie. only folders
        if last_folder_index == @props.file_search.length - 1
            if last_folder_index isnt @props.file_search.indexOf('/')
                # More than one sub folder
                <div>
                    <span style={fontWeight:'bold'}>
                            {@props.file_search}
                        </span> will be created as a <span style={fontWeight:'bold'}>folder path</span> if non-existant
                </div>
            else
                # Only one folder
                <div>
                    Creates a <span style={fontWeight:'bold'}>folder</span> named <span style={fontWeight:'bold'}>
                        {@props.file_search}
                    </span>
                </div>
        else
            <div>
                <span style={fontWeight:'bold'}>
                    {@full_path_text().slice(last_folder_index + 1)}
                </span> will be created under the folder path <span style={fontWeight:'bold'}>
                    {@props.file_search.slice(0, last_folder_index + 1)}
                </span>
            </div>

    render_file_type_selection: ->
        <div>
            <h4 style={color:"#666"}>Or select a file type</h4>
            <FileTypeSelector create_file={@props.create_file} create_folder={@props.create_folder} >
                <Row>
                    <Col sm=12>
                        <Tip title='Create a Chatroom'  placement='right'  icon='comment'
                            tip='Create a chatroom for chatting with other collaborators on this project.'>
                            <NewFileButton icon='comment' name='Chatroom' on_click={@props.create_file} ext='sage-chat' />
                        </Tip>
                    </Col>
                </Row>
            </FileTypeSelector>
        </div>

    render: ->
        <Row style={textAlign:'left', color:'#888', marginTop:'20px', wordWrap:'break-word'} >
            <Col sm=2>
            </Col>
            <Col sm=8>
                <span style={fontSize:'20px'}>
                    No Files Found
                </span>
                <hr/>
                {@render_create_button() if not @props.public_view}
                {@render_help_alert()}
                {@render_file_type_selection() if @props.file_search.length > 0}
            </Col>
            <Col sm=2>
            </Col>
        </Row>

pager_range = (page_size, page_number) ->
    start_index = page_size*page_number
    return {start_index: start_index, end_index: start_index + page_size}

FileListing = rclass
    displayName: 'ProjectFiles-FileListing'

    propTypes:
        active_file_sort    : rtypes.object
        listing             : rtypes.array.isRequired
        file_map            : rtypes.object.isRequired
        file_search         : rtypes.string
        checked_files       : rtypes.object
        current_path        : rtypes.string
        page_number         : rtypes.number
        page_size           : rtypes.number
        public_view         : rtypes.bool
        actions             : rtypes.object.isRequired
        create_folder       : rtypes.func.isRequired
        create_file         : rtypes.func.isRequired
        selected_file_index : rtypes.number
        project_id          : rtypes.string
        show_upload         : rtypes.bool
        shift_is_down       : rtypes.bool
        sort_by             : rtypes.func

    getDefaultProps: ->
        file_search : ''
        show_upload : false

    render_row: (name, size, time, mask, isdir, display_name, public_data, index) ->
        checked = @props.checked_files.has(misc.path_to_file(@props.current_path, name))
        is_public = @props.file_map[name].is_public
        if checked
            if index % 2 == 0
                color = 'rgb(250, 250, 209)'
            else
                color = 'rgb(255, 255, 220)'
        else if index % 2 == 0<commit_msg>change file highlight color scheme slightly....<commit_after>##############################################################################
#
#    CoCalc: Collaborative Calculation in the Cloud
#
#    Copyright (C) 2015 -- 2016, SageMath, Inc.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

{React, ReactDOM, rtypes, rclass, redux, Redux} = require('./smc-react')
{Col, Row, ButtonToolbar, ButtonGroup, MenuItem, Button, Well, FormControl, FormGroup
 ButtonToolbar, Popover, OverlayTrigger, SplitButton, MenuItem, Alert, Checkbox, Breadcrumb} =  require('react-bootstrap')
misc = require('smc-util/misc')
{ActivityDisplay, DeletedProjectWarning, DirectoryInput, Icon, Loading, ProjectState, COLORS,
 SearchInput, TimeAgo, ErrorDisplay, Space, Tip, LoginLink, Footer, CourseProjectExtraHelp} = require('./r_misc')
{SMC_Dropwrapper} = require('./smc-dropzone')
{FileTypeSelector, NewFileButton} = require('./project_new')

{BillingPageLink, BillingPageForCourseRedux, PayCourseFee}     = require('./billing')
{human_readable_size} = require('./misc_page')
{MiniTerminal}        = require('./project_miniterm')
{file_associations}   = require('./editor')
account               = require('./account')
immutable             = require('immutable')
underscore            = require('underscore')
{webapp_client}       = require('./webapp_client')
{AccountPage}         = require('./account_page')
{UsersViewing}        = require('./other-users')
{project_tasks}       = require('./project_tasks')

feature = require('./feature')

Combobox = require('react-widgets/lib/Combobox') # TODO: delete this when the combobox is in r_misc
TERM_MODE_CHAR = '/'

exports.file_action_buttons = file_action_buttons =
        compress :
            name : 'Compress'
            icon : 'compress'
        delete   :
            name : 'Delete'
            icon : 'trash-o'
        rename   :
            name : 'Rename'
            icon : 'pencil'
        duplicate:
            name : 'Duplicate'
            icon : 'clone'
        move     :
            name : 'Move'
            icon : 'arrows'
        copy     :
            name : 'Copy'
            icon : 'files-o'
        share    :
            name : 'Share'
            icon : 'share-square-o'
        download :
            name : 'Download'
            icon : 'cloud-download'

FileCheckbox = rclass
    displayName : 'ProjectFiles-FileCheckbox'

    propTypes :
        name         : rtypes.string
        checked      : rtypes.bool
        actions      : rtypes.object.isRequired
        current_path : rtypes.string
        style        : rtypes.object

    handle_click: (e) ->
        e.stopPropagation() # so we don't open the file
        full_name = misc.path_to_file(@props.current_path, @props.name)
        if e.shiftKey
            @props.actions.set_selected_file_range(full_name, not @props.checked)
        else
            @props.actions.set_file_checked(full_name, not @props.checked)

        @props.actions.set_most_recent_file_click(full_name)

    render: ->
        <span onClick={@handle_click} style={@props.style}>
            <Icon name={if @props.checked then 'check-square-o' else 'square-o'} fixedWidth style={fontSize:'14pt'}/>
        </span>

# TODO: Something should uniformly describe how sorted table headers work.
# 5/8/2017 We have 3 right now, Course students and assignments panel and this one.
ListingHeader = rclass
    propTypes:
        active_file_sort : rtypes.object    # {column_name : string, is_descending : bool}
        sort_by          : rtypes.func      # Invoked as `sort_by(string)

    render_sort_link: (column_name, display_name) ->
        <a href=''
            onClick={(e)=>e.preventDefault();@props.sort_by(column_name)}>
            {display_name}
            <Space/>
            {<Icon style={marginRight:'10px'}
                name={if @props.active_file_sort.is_descending then 'caret-up' else 'caret-down'}
            /> if @props.active_file_sort.column_name == column_name}
        </a>

    render: ->
        row_styles =
            cursor          : 'pointer'
            color           : '#666'
            backgroundColor : '#fafafa'
            border          : '1px solid #eee'
            borderRadius    : '4px'

        <Row style={row_styles}>
            <Col sm=2 xs=3>
            </Col>
            <Col sm=1 xs=3>
                {@render_sort_link("type", "Type")}
            </Col>
            <Col sm=4 smPush=5 xs=6>
                {@render_sort_link("time", "Date Modified")}
                <span className='pull-right'>
                    {@render_sort_link("size", "Size")}
                </span>
            </Col>
            <Col sm=5 smPull=4 xs=12>
                {@render_sort_link("name", "Name")}
            </Col>
        </Row>

FileRow = rclass
    displayName : 'ProjectFiles-FileRow'

    propTypes :
        name         : rtypes.string.isRequired
        display_name : rtypes.string  # if given, will display this, and will show true filename in popover
        size         : rtypes.number.isRequired
        time         : rtypes.number
        checked      : rtypes.bool
        bordered     : rtypes.bool
        color        : rtypes.string
        mask         : rtypes.bool
        public_data  : rtypes.object
        is_public    : rtypes.bool
        current_path : rtypes.string
        actions      : rtypes.object.isRequired
        no_select    : rtypes.bool

    shouldComponentUpdate: (next) ->
        return @props.name != next.name          or
        @props.display_name != next.display_name or
        @props.size != next.size                 or
        @props.time != next.time                 or
        @props.checked != next.checked           or
        @props.mask != next.mask                 or
        @props.public_data != next.public_data   or
        @props.current_path != next.current_path or
        @props.bordered != next.border           or
        @props.no_select != next.no_select

    render_icon: ->
        # get the file_associations[ext] just like it is defined in the editor
        {file_options} = require('./editor')
        name           = file_options(@props.name)?.icon ? 'file'
        style          =
            color         : if @props.mask then '#bbbbbb'
            verticalAlign : 'sub'
        <a style={style}>
            <Icon name={name} style={fontSize:'14pt'} />
        </a>

    render_name_link: (styles, name, ext) ->
        <a style={styles}>
            <span style={fontWeight: if @props.mask then 'normal' else 'bold'}>{misc.trunc_middle(name,50)}</span>
            <span style={color: if not @props.mask then '#999'}>{if ext is '' then '' else ".#{ext}"}</span>
        </a>

    render_name: ->
        name = @props.display_name ? @props.name
        name_and_ext = misc.separate_file_extension(name)
        name = name_and_ext.name
        ext = name_and_ext.ext

        show_tip = (@props.display_name? and @props.name isnt @props.display_name) or name.length > 50

        styles =
            whiteSpace    : 'pre-wrap'
            wordWrap      : 'break-word'
            overflowWrap  : 'break-word'
            verticalAlign : 'middle'
            color         : if @props.mask then '#bbbbbb'

        if show_tip
            <Tip title={if @props.display_name then 'Displayed filename is an alias. The actual name is:' else 'Full name'} tip={@props.name}>
                {@render_name_link(styles, name, ext)}
            </Tip>
        else
            @render_name_link(styles, name, ext)

    render_public_file_info_popover: ->
        <Popover title='This file is being shared publicly' id='public_share' >
            <span style={wordWrap:'break-word'}>
                Description: {@props.public_data.description}
            </span>
        </Popover>

    render_public_file_info: ->
        if @props.public_data? and @props.is_public
            <span><Space/>
                <OverlayTrigger
                    trigger   = 'click'
                    rootClose
                    overlay   = {@render_public_file_info_popover()} >
                    <Button
                        bsStyle = 'info'
                        bsSize  = 'xsmall'
                        onClick = {(e)->e.stopPropagation()}
                    >
                        <Icon name='bullhorn' /> <span className='hidden-xs'>Public</span>
                    </Button>
                </OverlayTrigger>
            </span>

    fullpath: ->
        misc.path_to_file(@props.current_path, @props.name)

    handle_mouse_down: (e) ->
        @setState
            selection_at_last_mouse_down : window.getSelection().toString()

    handle_click: (e) ->
        if window.getSelection().toString() == @state.selection_at_last_mouse_down
            foreground = misc.should_open_in_foreground(e)
            @props.actions.open_file
                path       : @fullpath()
                foreground : foreground
            if foreground
                @props.actions.set_file_search('')

    handle_download_click: (e) ->
        e.preventDefault()
        e.stopPropagation()
        @props.actions.download_file
            path : @fullpath()
            log : true

    render: ->
        row_styles =
            cursor          : 'pointer'
            borderRadius    : '4px'
            backgroundColor : @props.color
            borderStyle     : 'solid'
            borderColor     : if @props.bordered then COLORS.BLUE_BG else @props.color

        # See https://github.com/sagemathinc/cocalc/issues/1020
        # support right-click → copy url for the download button
        url_href = project_tasks(@props.actions.project_id).url_href(@fullpath())

        <Row
            style       = {row_styles}
            onMouseDown = {@handle_mouse_down}
            onClick     = {@handle_click}
            className   = {'noselect' if @props.no_select}
        >
            <Col sm=2 xs=3>
                <FileCheckbox
                    name         = {@props.name}
                    checked      = {@props.checked}
                    current_path = {@props.current_path}
                    actions      = {@props.actions}
                    style        = {verticalAlign:'sub'} />
                {@render_public_file_info()}
            </Col>
            <Col sm=1 xs=3>
                {@render_icon()}
            </Col>
            <Col sm=4 smPush=5 xs=6>
                <TimeAgo date={(new Date(@props.time * 1000)).toISOString()} style={color:'#666'}/>
                <span className='pull-right' style={color:'#666'}>
                    {human_readable_size(@props.size)}
                    <Button style   = {marginLeft: '1em', background:'transparent'}
                            bsStyle = 'default'
                            bsSize  = 'xsmall'
                            href    = "#{url_href}"
                            onClick = {@handle_download_click}>
                        <Icon name='cloud-download' style={color: '#666'} />
                    </Button>
                </span>
            </Col>
            <Col sm=5 smPull=4 xs=12>
                {@render_name()}
            </Col>
        </Row>

DirectoryRow = rclass
    displayName : 'ProjectFiles-DirectoryRow'

    propTypes :
        name         : rtypes.string.isRequired
        display_name : rtypes.string  # if given, will display this, and will show true filename in popover
        checked      : rtypes.bool
        color        : rtypes.string
        bordered     : rtypes.bool
        time         : rtypes.number
        mask         : rtypes.bool
        public_data  : rtypes.object
        is_public    : rtypes.bool
        current_path : rtypes.string
        actions      : rtypes.object.isRequired
        no_select    : rtypes.bool

    handle_mouse_down: (e) ->
        @setState
            selection_at_last_mouse_down : window.getSelection().toString()

    handle_click: (e) ->
        if window.getSelection().toString() == @state.selection_at_last_mouse_down
            path = misc.path_to_file(@props.current_path, @props.name)
            @props.actions.open_directory(path)
            @props.actions.set_file_search('')

    render_public_directory_info_popover: ->
        <Popover id={@props.name} title='This folder is being shared publicly' style={wordWrap:'break-word'}>
            Description: {@props.public_data.description}
        </Popover>

    render_public_directory_info: ->
        if @props.public_data? and @props.is_public
            <span><Space/>
                <OverlayTrigger
                    trigger   = 'click'
                    rootClose
                    overlay   = {@render_public_directory_info_popover()} >
                    <Button
                        bsStyle = 'info'
                        bsSize  = 'xsmall'
                        onClick = {(e)->e.stopPropagation()}
                    >
                        <Icon name='bullhorn' /> <span className='hidden-xs'>Public</span>
                    </Button>
                </OverlayTrigger>
            </span>

    render_time: ->
        if @props.time?
            <TimeAgo date={(new Date(@props.time * 1000)).toISOString()} style={color:'#666'} />

    render_name_link: ->
        if (@props.display_name and @props.display_name isnt @props.name) or @props.name.length > 50
            <Tip title={if @props.display_name then 'Displayed directory name is an alias. The actual name is:' else 'Full name'} tip={@props.name}>
                <a style={color : if @props.mask then '#bbbbbb'}>
                    {misc.trunc_middle(@props.display_name ? @props.name, 50)}
                </a>
            </Tip>
        else
            <a style={color : if @props.mask then '#bbbbbb'}>
                {misc.trunc_middle(@props.display_name ? @props.name, 50)}
            </a>

    render: ->
        row_styles =
            cursor          : 'pointer'
            borderRadius    : '4px'
            backgroundColor : @props.color
            borderStyle     : 'solid'
            borderColor     : if @props.bordered then COLORS.BLUE_BG else @props.color

        directory_styles =
            fontWeight     : 'bold'
            whiteSpace     : 'pre-wrap'
            wordWrap       : 'break-word'
            overflowWrap   : 'break-word'
            verticalAlign  : 'sub'

        <Row style={row_styles} onMouseDown={@handle_mouse_down} onClick={@handle_click} className={'noselect' if @props.no_select}>
            <Col sm=2 xs=3>
                <FileCheckbox
                    name         = {@props.name}
                    checked      = {@props.checked}
                    current_path = {@props.current_path}
                    actions      = {@props.actions}
                    style        = {verticalAlign:'sub'} />
                {@render_public_directory_info()}
            </Col>
            <Col sm=1 xs=3>
                <a style={color : if @props.mask then '#bbbbbb'}>
                    <Icon name='folder-open-o' style={fontSize:'14pt',verticalAlign:'sub'} />
                    <Icon name='caret-right' style={marginLeft:'3px',fontSize:'14pt',verticalAlign:'sub'} />
                </a>
            </Col>
            <Col sm=4 smPush=5 xs=6>
                {@render_time()}
                {#size (not applicable for directories)}
            </Col>
            <Col sm=5 smPull=4 xs=12 style={directory_styles}>
                {@render_name_link()}
            </Col>
        </Row>

TerminalModeDisplay = rclass
    render: ->
        <Row style={textAlign:'left', color:'#888', marginTop:'5px', wordWrap:'break-word'} >
            <Col sm=2>
            </Col>
            <Col sm=8>
                <Alert style={marginTop: '5px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: You are in terminal mode.<br/>
                This was caused by the leading / in your file search. If you want to just see your folders, enter a space in front of the /.<br/>
                Terminal mode inside the search bar is experimental and comes with no guarantees about its usability or future existence.
            </Alert>
            </Col>
            <Col sm=2>
            </Col>
        </Row>

NoFiles = rclass
    propTypes :
        actions       : rtypes.object.isRequired
        create_folder : rtypes.func.isRequired
        create_file   : rtypes.func.isRequired
        public_view   : rtypes.bool
        file_search   : rtypes.string
        current_path  : rtypes.string

    displayName : 'ProjectFiles-NoFiles'

    getDefaultProps: ->
        file_search : ''

    # Go to the new file tab if there is no file search
    handle_click: ->
        if @props.file_search.length == 0
            @props.actions.set_active_tab('new')
        else if @props.file_search[@props.file_search.length - 1] == '/'
            @props.create_folder()
        else
            @props.create_file()

    # Returns the full file_search text in addition to the default extension if applicable
    full_path_text: ->
        if @props.file_search.lastIndexOf('.') <= @props.file_search.lastIndexOf('/')
          ext = "sagews"
        if ext and @props.file_search.slice(-1) isnt '/'
            "#{@props.file_search}.#{ext}"
        else
            "#{@props.file_search}"

    # Text for the large create button
    button_text: ->
        if @props.file_search.length == 0
            "Create or upload files..."
        else
            "Create #{@full_path_text()}"

    render_create_button: ->
        <Button
            style   = {fontSize:'40px', color:'#888', maxWidth:'100%'}
            onClick = {=>@handle_click()} >
            <Icon name='plus-circle' /> {@button_text()}
        </Button>

    render_help_alert: ->
        last_folder_index = @props.file_search.lastIndexOf('/')
        if @props.file_search.indexOf('\\') != -1
            <Alert style={marginTop: '10px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: \ is an illegal character
            </Alert>
        else if @props.file_search.indexOf('/') == 0
            <Alert style={marginTop: '10px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: Names cannot begin with /
            </Alert>
        else if ['.', '..'].indexOf(@props.file_search) > -1
            <Alert style={marginTop: '10px', fontWeight : 'bold'} bsStyle='danger'>
                Warning: Cannot create a file named . or ..
            </Alert>
        # Non-empty search and there is a file divisor ('/')
        else if @props.file_search.length > 0 and last_folder_index > 0
            <Alert style={marginTop: '10px'} bsStyle='info'>
                {@render_help_text(last_folder_index)}
            </Alert>

    render_help_text: (last_folder_index) ->
        # Ends with a '/' ie. only folders
        if last_folder_index == @props.file_search.length - 1
            if last_folder_index isnt @props.file_search.indexOf('/')
                # More than one sub folder
                <div>
                    <span style={fontWeight:'bold'}>
                            {@props.file_search}
                        </span> will be created as a <span style={fontWeight:'bold'}>folder path</span> if non-existant
                </div>
            else
                # Only one folder
                <div>
                    Creates a <span style={fontWeight:'bold'}>folder</span> named <span style={fontWeight:'bold'}>
                        {@props.file_search}
                    </span>
                </div>
        else
            <div>
                <span style={fontWeight:'bold'}>
                    {@full_path_text().slice(last_folder_index + 1)}
                </span> will be created under the folder path <span style={fontWeight:'bold'}>
                    {@props.file_search.slice(0, last_folder_index + 1)}
                </span>
            </div>

    render_file_type_selection: ->
        <div>
            <h4 style={color:"#666"}>Or select a file type</h4>
            <FileTypeSelector create_file={@props.create_file} create_folder={@props.create_folder} >
                <Row>
                    <Col sm=12>
                        <Tip title='Create a Chatroom'  placement='right'  icon='comment'
                            tip='Create a chatroom for chatting with other collaborators on this project.'>
                            <NewFileButton icon='comment' name='Chatroom' on_click={@props.create_file} ext='sage-chat' />
                        </Tip>
                    </Col>
                </Row>
            </FileTypeSelector>
        </div>

    render: ->
        <Row style={textAlign:'left', color:'#888', marginTop:'20px', wordWrap:'break-word'} >
            <Col sm=2>
            </Col>
            <Col sm=8>
                <span style={fontSize:'20px'}>
                    No Files Found
                </span>
                <hr/>
                {@render_create_button() if not @props.public_view}
                {@render_help_alert()}
                {@render_file_type_selection() if @props.file_search.length > 0}
            </Col>
            <Col sm=2>
            </Col>
        </Row>

pager_range = (page_size, page_number) ->
    start_index = page_size*page_number
    return {start_index: start_index, end_index: start_index + page_size}

FileListing = rclass
    displayName: 'ProjectFiles-FileListing'

    propTypes:
        active_file_sort    : rtypes.object
        listing             : rtypes.array.isRequired
        file_map            : rtypes.object.isRequired
        file_search         : rtypes.string
        checked_files       : rtypes.object
        current_path        : rtypes.string
        page_number         : rtypes.number
        page_size           : rtypes.number
        public_view         : rtypes.bool
        actions             : rtypes.object.isRequired
        create_folder       : rtypes.func.isRequired
        create_file         : rtypes.func.isRequired
        selected_file_index : rtypes.number
        project_id          : rtypes.string
        show_upload         : rtypes.bool
        shift_is_down       : rtypes.bool
        sort_by             : rtypes.func

    getDefaultProps: ->
        file_search : ''
        show_upload : false

    render_row: (name, size, time, mask, isdir, display_name, public_data, index) ->
        checked = @props.checked_files.has(misc.path_to_file(@props.current_path, name))
        is_public = @props.file_map[name].is_public
        if checked
            if index % 2 == 0
                color = '#a3d4ff'
            else
                color = '#a3d4f0'
        else if index % 2 == 0
{'fraction_non_alphanumeric': 0.118528, 'fraction_numerical': 0.007488, 'mean_word_length': 1.7141450987203983, 'pattern_counts': {'":': 0, '<': 328, '<?xml version=': 0, '>': 416, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3608744', 'n_tokens_mistral': 13376, 'n_tokens_neox': 12803, 'n_words': 2975}