<commit_before>import {Button} from 'widget/button'
import {ButtonGroup} from 'widget/buttonGroup'
import {CenteredProgress} from 'widget/progress'
import {CheckButton} from 'widget/checkButton'
import {Combo} from 'widget/combo'
import {CreateRecipe} from '../createRecipe'
import {CrudItem} from 'widget/crudItem'
import {Layout} from 'widget/layout'
import {ListItem} from 'widget/listItem'
import {Pageable} from 'widget/pageable/pageable'
import {ProjectsButton} from './projectsButton'
import {ScrollableContainer, Unscrollable} from 'widget/scrollable'
import {SearchBox} from 'widget/searchBox'
import {SelectProject} from './selectProject'
import {compose} from 'compose'
import {connect, select} from 'store'
import {getRecipeType} from '../recipeTypes'
import {msg} from 'translate'
import {simplifyString, splitString} from 'string'
import ButtonPopup from 'widget/buttonPopup'
import Confirm from 'widget/confirm'
import PropTypes from 'prop-types'
import React from 'react'
import RemoveButton from 'widget/removeButton'
import _ from 'lodash'
import actionBuilder from 'action-builder'
import styles from './recipeListData.module.css'

const NO_PROJECT_SYMBOL = '#'
const PROJECT_RECIPE_SEPARATOR = ' / '

const mapStateToProps = () => ({
    projects: select('process.projects'),
    projectId: select('process.projectId'),
    recipes: select('process.recipes'),
    sortingOrder: select('process.sortingOrder') ?? 'updateTime',
    sortingDirection: select('process.sortingDirection') ?? -1,
    filterValue: select('process.filterValue'),
    filterValues: select('process.filterValues') ?? [],
    selectedIds: select('process.selectedIds') ?? [],
    filteredRecipes: select('process.filteredRecipes') ?? []
})

class _RecipeListData extends React.Component {
    state = {
        edit: false,
        move: false
    }

    constructor() {
        super()
        this.setFilter = this.setFilter.bind(this)
        this.toggleAll = this.toggleAll.bind(this)
        this.moveSelected = this.moveSelected.bind(this)
        this.removeSelected = this.removeSelected.bind(this)
    }

    render() {
        return this.isLoading()
            ? this.renderProgress()
            : this.renderData()
    }

    renderProgress() {
        return <CenteredProgress title={msg('process.recipe.loading')}/>
    }

    renderData() {
        const {filterValues} = this.props
        const {move} = this.state
        const highlightMatcher = filterValues.length
            ? new RegExp(`(?:${filterValues.join('|')})`, 'i')
            : null
        return this.hasData() ? (
            <ScrollableContainer>
                <Unscrollable>
                    {this.renderHeader()}
                </Unscrollable>
                <Unscrollable className={styles.recipes}>
                    <Pageable.Data
                        itemKey={recipe => `${recipe.id}|${highlightMatcher}`}>
                        {recipe => this.renderRecipe(recipe, highlightMatcher)}
                    </Pageable.Data>
                    {move && this.renderMoveConfirmation()}
                </Unscrollable>
            </ScrollableContainer>
        ) : null
    }

    renderHeader() {
        const {recipeId} = this.props
        return (
            <Layout type='vertical' spacing='compact' className={styles.header}>
                <Layout type='horizontal' spacing='compact'>
                    {this.renderSearch()}
                    <Layout.Spacer/>
                    {this.renderEditButtons()}
                </Layout>
                <Layout type='horizontal' spacing='compact'>
                    <CreateRecipe recipeId={recipeId}/>
                    <ProjectsButton/>
                    <SelectProject/>
                    <Layout.Spacer/>
                    {this.renderSortButtons()}
                </Layout>
            </Layout>
        )
    }

    renderSearch() {
        const {filterValue} = this.props
        return (
            <SearchBox
                value={filterValue}
                placeholder={msg('process.menu.searchRecipes')}
                onSearchValue={this.setFilter}
            />
        )
    }

    renderEditButtons() {
        const {edit} = this.state
        return (
            <ButtonGroup spacing='none'>
                {edit && this.renderSelectButton()}
                {edit && this.renderMoveButton()}
                {edit && this.renderRemoveButton()}
                <Button
                    icon='pen-to-square'
                    label={msg('process.recipe.edit.label')}
                    shape='pill'
                    keybinding={edit ? 'Escape' : ''}
                    onClick={() => this.setEdit(!edit)}
                />
            </ButtonGroup>
        )
    }

    renderSelectButton() {
        const selected = this.isSelected()
        return (
            <CheckButton
                shape='pill'
                label={msg('process.recipe.select.label')}
                checked={selected}
                tooltip={msg(selected ? 'process.recipe.select.tooltip.deselect' : 'process.recipe.select.tooltip.select')}
                onToggle={this.toggleAll}/>
        )
    }

    renderMoveButton() {
        return (
            <ButtonPopup
                shape='pill'
                icon='shuffle'
                label={msg('process.recipe.move.label')}
                placement='below'
                alignment='left'
                disabled={!this.isSelected()}
                tooltip={msg('process.recipe.move.tooltip')}>
                {onBlur => (
                    <Combo
                        placement='below'
                        alignment='left'
                        placeholder={msg('process.recipe.move.destinationProject')}
                        options={this.getDestinations()}
                        standalone
                        autoFocus
                        onCancel={onBlur}
                        onChange={option => {
                            this.setMove(option)
                            onBlur()
                        }}
                    />
                )}
            </ButtonPopup>
        )
    }

    renderMoveConfirmation() {
        const {move: {value: projectId, label: projectName}} = this.state
        return (
            <Confirm
                title={msg('process.recipe.move.title')}
                message={msg('process.recipe.move.confirm', {count: this.isSelected(), project: projectName})}
                onConfirm={() => {
                    this.setMove(false)
                    this.moveSelected(projectId)
                }}
                onCancel={() => this.setMove(false)}
            />
        )
    }

    renderRemoveButton() {
        return (
            <RemoveButton
                shape='pill'
                icon='trash'
                label={msg('process.recipe.remove.label')}
                tooltip={msg('process.recipe.remove.tooltip')}
                message={msg('process.recipe.remove.confirm', {count: this.isSelected()})}
                disabled={!this.isSelected()}
                onRemove={this.removeSelected}/>
        )
    }

    getDestinations() {
        const {projects} = this.props
        return projects.map(({id, name}) => ({value: id, label: name}))
    }

    renderSortButtons() {
        return (
            <ButtonGroup layout='horizontal-nowrap' spacing='tight'>
                {this.renderSortButton('updateTime', msg('process.recipe.lastUpdate'))}
                {this.renderSortButton('name', msg('process.recipe.name'))}
            </ButtonGroup>
        )
    }

    renderSortButton(column, label) {
        const {sortingOrder, sortingDirection} = this.props
        return (
            <Button
                chromeless
                look='transparent'
                shape='pill'
                label={label}
                labelStyle={sortingOrder === column ? 'smallcaps-highlight' : 'smallcaps'}
                icon={this.getHandleIcon({column, sortingOrder, sortingDirection})}
                iconPlacement='right'
                onClick={() => this.setSorting(column)}/>
        )
    }

    renderRecipe(recipe, highlightMatcher) {
        const {projects, onClick, onDuplicate, onRemove} = this.props
        const {edit} = this.state
        const name = recipe.name
        const project = _.find(projects, ({id}) => id === recipe.projectId)
        const path = [
            project?.name ?? NO_PROJECT_SYMBOL,
            name
        ].join(PROJECT_RECIPE_SEPARATOR)
        return (
            <ListItem
                key={recipe.id}
                onClick={onClick ? () => onClick(recipe.id) : null}>
                <CrudItem
                    title={this.getRecipeTypeName(recipe.type)}
                    description={path}
                    timestamp={recipe.updateTime}
                    highlight={highlightMatcher}
                    highlightTitle={false}
                    duplicateTooltip={msg('process.menu.duplicateRecipe.tooltip')}
                    removeTooltip={msg('process.menu.removeRecipe.tooltip')}
                    selectTooltip={msg('process.menu.selectRecipe.tooltip')}
                    selected={this.isSelected(recipe.id)}
                    onDuplicate={onDuplicate ? () => onDuplicate(recipe.id) : null}
                    onRemove={onRemove ? () => onRemove(recipe.id) : null}
                    onSelect={edit ? () => this.toggleOne(recipe.id) : null}
                />
            </ListItem>
        )
    }

    setEdit(edit) {
        this.setState({edit})
    }

    setMove(move) {
        this.setState({move})
    }

    getHandleIcon({column, sortingOrder, sortingDirection}) {
        const sorted = sortingOrder === column
        return sorted
            ? sortingDirection === 1
                ? 'sort-down'
                : 'sort-up'
            : 'sort'
    }

    getRecipeTypeName(type) {
        const recipeType = getRecipeType(type)
        return recipeType && recipeType.labels.name
    }

    isLoading() {
        const {recipes, stream} = this.props
        return !recipes && stream('LOAD_RECIPES').active
    }

    hasData() {
        const {recipes} = this.props
        return recipes && recipes.length
    }

    setFilter(filterValue) {
        const filterValues = splitString(simplifyString(filterValue))
        actionBuilder('SET_FILTER_VALUES', {filterValue, filterValues})
            .set(['process.filterValue'], filterValue)
            .set(['process.filterValues'], filterValues)
            .dispatch()
    }

    setSorting(sortingOrder) {
        const {sortingOrder: prevSortingOrder, sortingDirection: prevSortingDirection} = this.props
        const sortingDirection = sortingOrder === prevSortingOrder
            ? -prevSortingDirection
            : 1
        actionBuilder('SET_SORTING_ORDER', {sortingOrder})
            .set(['process.sortingOrder'], sortingOrder)
            .set(['process.sortingDirection'], sortingDirection)
            .dispatch()
    }

    isSelected(recipeId) {
        const filteredSelectedIds = this.getFilteredSelectedIds()
        return recipeId
            ? filteredSelectedIds.includes(recipeId)
            : filteredSelectedIds.length
    }

    setSelectedIds(selectedIds) {
        actionBuilder('SET_SELECTED_IDS', {selectedIds})
            .set(['process.selectedIds'], selectedIds)
            .dispatch()
    }

    toggleOne(recipeId) {
        const {selectedIds: prevSelectedIds} = this.props
        const selectedIds = prevSelectedIds.includes(recipeId)
            ? prevSelectedIds.filter(currentRecipeId => currentRecipeId !== recipeId)
            : [...prevSelectedIds, recipeId]
        this.setSelectedIds(selectedIds)
    }

    toggleAll() {
        const {selectedIds: prevSelectedIds} = this.props
        const filteredIds = this.getFilteredIds()
        const filteredSelectedIds = this.getFilteredSelectedIds()
        const selectedIds = filteredSelectedIds.length
            ? _.difference(prevSelectedIds, filteredSelectedIds)
            : [...prevSelectedIds, ...filteredIds]
        this.setSelectedIds(selectedIds)
    }

    moveSelected(projectId) {
        const {onMove} = this.props
        const selectedIds = this.getFilteredSelectedIds()
        onMove(selectedIds, projectId)
    }

    removeSelected() {
        const {onRemove} = this.props
        const selectedIds = this.getFilteredSelectedIds()
        onRemove(selectedIds)
    }

    getFilteredIds() {
        const {filteredRecipes} = this.props
        return filteredRecipes.map(({id}) => id)
    }

    getFilteredSelectedIds() {
        const {selectedIds} = this.props
        const filteredIds = this.getFilteredIds()
        const filteredSelectedIds = _.intersection(selectedIds, filteredIds)
        return filteredSelectedIds
    }

    recipeMatchesProject(recipe) {
        const {projectId} = this.props
        return !projectId || projectId === recipe.projectId
    }

    recipeMatchesFilter(recipe, filterValues) {
        const searchMatchers = filterValues.map(filter => RegExp(filter, 'i'))
        const searchProperties = ['project', 'name']
        return filterValues
            ? _.every(searchMatchers, matcher =>
                _.find(searchProperties, property =>
                    matcher.test(simplifyString(recipe[property], {
                        removeNonAlphanumeric: true,
                        removeAccents: true
                    }))
                )
            )
            : true
    }

    getSorter(recipe, sortingOrder) {
        switch (sortingOrder) {
        case 'updateTime':
            return this.getSorterByUpdateTime(recipe)
        case 'name':
            return this.getSorterByName(recipe)
        }
    }

    getSorterByUpdateTime(recipe) {
        return recipe.updateTime
    }

    getSorterByName(recipe) {
        const project = recipe?.project
        const sorter = project
            ? `1:${recipe?.project}:${recipe.name}`
            : `0:${recipe.name}`
        return sorter.toUpperCase()
    }

    updateFilteredRecipes() {
        const {projects, recipes, filterValues} = this.props
        const {sortingOrder, sortingDirection} = this.props
        const filteredRecipes = _.chain(recipes)
            .map(recipe => {
                const project = projects.find(project => project.id === recipe.projectId)
                return {...recipe, project: project?.name}
            })
            .filter(recipe => this.recipeMatchesProject(recipe) && this.recipeMatchesFilter(recipe, filterValues))
            .orderBy(recipe => this.getSorter(recipe, sortingOrder), sortingDirection === 1 ? 'asc' : 'desc')
            .value()
        actionBuilder('SET_FILTERED_RECIPES', {filteredRecipes})
            .set(['process.filteredRecipes'], filteredRecipes)
            .dispatch()
    }

    componentDidUpdate(prevProps) {
        if (!_.isEqual(this.props, prevProps)) {
            this.updateFilteredRecipes()
        }
    }
}

export const RecipeListData = compose(
    _RecipeListData,
    connect(mapStateToProps)
)

RecipeListData.propTypes = {
    recipeId: PropTypes.string,
    onClick: PropTypes.func,
    onDuplicate: PropTypes.func,
    onRemove: PropTypes.func,
    onSelect: PropTypes.func
}
<commit_msg>Protected against missing projects.<commit_after>import {Button} from 'widget/button'
import {ButtonGroup} from 'widget/buttonGroup'
import {CenteredProgress} from 'widget/progress'
import {CheckButton} from 'widget/checkButton'
import {Combo} from 'widget/combo'
import {CreateRecipe} from '../createRecipe'
import {CrudItem} from 'widget/crudItem'
import {Layout} from 'widget/layout'
import {ListItem} from 'widget/listItem'
import {Pageable} from 'widget/pageable/pageable'
import {ProjectsButton} from './projectsButton'
import {ScrollableContainer, Unscrollable} from 'widget/scrollable'
import {SearchBox} from 'widget/searchBox'
import {SelectProject} from './selectProject'
import {compose} from 'compose'
import {connect, select} from 'store'
import {getRecipeType} from '../recipeTypes'
import {msg} from 'translate'
import {simplifyString, splitString} from 'string'
import ButtonPopup from 'widget/buttonPopup'
import Confirm from 'widget/confirm'
import PropTypes from 'prop-types'
import React from 'react'
import RemoveButton from 'widget/removeButton'
import _ from 'lodash'
import actionBuilder from 'action-builder'
import styles from './recipeListData.module.css'

const NO_PROJECT_SYMBOL = '#'
const PROJECT_RECIPE_SEPARATOR = ' / '

const mapStateToProps = () => ({
    projects: select('process.projects'),
    projectId: select('process.projectId'),
    recipes: select('process.recipes'),
    sortingOrder: select('process.sortingOrder') ?? 'updateTime',
    sortingDirection: select('process.sortingDirection') ?? -1,
    filterValue: select('process.filterValue'),
    filterValues: select('process.filterValues') ?? [],
    selectedIds: select('process.selectedIds') ?? [],
    filteredRecipes: select('process.filteredRecipes') ?? []
})

class _RecipeListData extends React.Component {
    state = {
        edit: false,
        move: false
    }

    constructor() {
        super()
        this.setFilter = this.setFilter.bind(this)
        this.toggleAll = this.toggleAll.bind(this)
        this.moveSelected = this.moveSelected.bind(this)
        this.removeSelected = this.removeSelected.bind(this)
    }

    render() {
        return this.isLoading()
            ? this.renderProgress()
            : this.renderData()
    }

    renderProgress() {
        return <CenteredProgress title={msg('process.recipe.loading')}/>
    }

    renderData() {
        const {filterValues} = this.props
        const {move} = this.state
        const highlightMatcher = filterValues.length
            ? new RegExp(`(?:${filterValues.join('|')})`, 'i')
            : null
        return this.hasData() ? (
            <ScrollableContainer>
                <Unscrollable>
                    {this.renderHeader()}
                </Unscrollable>
                <Unscrollable className={styles.recipes}>
                    <Pageable.Data
                        itemKey={recipe => `${recipe.id}|${highlightMatcher}`}>
                        {recipe => this.renderRecipe(recipe, highlightMatcher)}
                    </Pageable.Data>
                    {move && this.renderMoveConfirmation()}
                </Unscrollable>
            </ScrollableContainer>
        ) : null
    }

    renderHeader() {
        const {recipeId} = this.props
        return (
            <Layout type='vertical' spacing='compact' className={styles.header}>
                <Layout type='horizontal' spacing='compact'>
                    {this.renderSearch()}
                    <Layout.Spacer/>
                    {this.renderEditButtons()}
                </Layout>
                <Layout type='horizontal' spacing='compact'>
                    <CreateRecipe recipeId={recipeId}/>
                    <ProjectsButton/>
                    <SelectProject/>
                    <Layout.Spacer/>
                    {this.renderSortButtons()}
                </Layout>
            </Layout>
        )
    }

    renderSearch() {
        const {filterValue} = this.props
        return (
            <SearchBox
                value={filterValue}
                placeholder={msg('process.menu.searchRecipes')}
                onSearchValue={this.setFilter}
            />
        )
    }

    renderEditButtons() {
        const {edit} = this.state
        return (
            <ButtonGroup spacing='none'>
                {edit && this.renderSelectButton()}
                {edit && this.renderMoveButton()}
                {edit && this.renderRemoveButton()}
                <Button
                    icon='pen-to-square'
                    label={msg('process.recipe.edit.label')}
                    shape='pill'
                    keybinding={edit ? 'Escape' : ''}
                    onClick={() => this.setEdit(!edit)}
                />
            </ButtonGroup>
        )
    }

    renderSelectButton() {
        const selected = this.isSelected()
        return (
            <CheckButton
                shape='pill'
                label={msg('process.recipe.select.label')}
                checked={selected}
                tooltip={msg(selected ? 'process.recipe.select.tooltip.deselect' : 'process.recipe.select.tooltip.select')}
                onToggle={this.toggleAll}/>
        )
    }

    renderMoveButton() {
        return (
            <ButtonPopup
                shape='pill'
                icon='shuffle'
                label={msg('process.recipe.move.label')}
                placement='below'
                alignment='left'
                disabled={!this.isSelected()}
                tooltip={msg('process.recipe.move.tooltip')}>
                {onBlur => (
                    <Combo
                        placement='below'
                        alignment='left'
                        placeholder={msg('process.recipe.move.destinationProject')}
                        options={this.getDestinations()}
                        standalone
                        autoFocus
                        onCancel={onBlur}
                        onChange={option => {
                            this.setMove(option)
                            onBlur()
                        }}
                    />
                )}
            </ButtonPopup>
        )
    }

    renderMoveConfirmation() {
        const {move: {value: projectId, label: projectName}} = this.state
        return (
            <Confirm
                title={msg('process.recipe.move.title')}
                message={msg('process.recipe.move.confirm', {count: this.isSelected(), project: projectName})}
                onConfirm={() => {
                    this.setMove(false)
                    this.moveSelected(projectId)
                }}
                onCancel={() => this.setMove(false)}
            />
        )
    }

    renderRemoveButton() {
        return (
            <RemoveButton
                shape='pill'
                icon='trash'
                label={msg('process.recipe.remove.label')}
                tooltip={msg('process.recipe.remove.tooltip')}
                message={msg('process.recipe.remove.confirm', {count: this.isSelected()})}
                disabled={!this.isSelected()}
                onRemove={this.removeSelected}/>
        )
    }

    getDestinations() {
        const {projects} = this.props
        return projects.map(({id, name}) => ({value: id, label: name}))
    }

    renderSortButtons() {
        return (
            <ButtonGroup layout='horizontal-nowrap' spacing='tight'>
                {this.renderSortButton('updateTime', msg('process.recipe.lastUpdate'))}
                {this.renderSortButton('name', msg('process.recipe.name'))}
            </ButtonGroup>
        )
    }

    renderSortButton(column, label) {
        const {sortingOrder, sortingDirection} = this.props
        return (
            <Button
                chromeless
                look='transparent'
                shape='pill'
                label={label}
                labelStyle={sortingOrder === column ? 'smallcaps-highlight' : 'smallcaps'}
                icon={this.getHandleIcon({column, sortingOrder, sortingDirection})}
                iconPlacement='right'
                onClick={() => this.setSorting(column)}/>
        )
    }

    renderRecipe(recipe, highlightMatcher) {
        const {projects, onClick, onDuplicate, onRemove} = this.props
        const {edit} = this.state
        const name = recipe.name
        const project = _.find(projects, ({id}) => id === recipe.projectId)
        const path = [
            project?.name ?? NO_PROJECT_SYMBOL,
            name
        ].join(PROJECT_RECIPE_SEPARATOR)
        return (
            <ListItem
                key={recipe.id}
                onClick={onClick ? () => onClick(recipe.id) : null}>
                <CrudItem
                    title={this.getRecipeTypeName(recipe.type)}
                    description={path}
                    timestamp={recipe.updateTime}
                    highlight={highlightMatcher}
                    highlightTitle={false}
                    duplicateTooltip={msg('process.menu.duplicateRecipe.tooltip')}
                    removeTooltip={msg('process.menu.removeRecipe.tooltip')}
                    selectTooltip={msg('process.menu.selectRecipe.tooltip')}
                    selected={this.isSelected(recipe.id)}
                    onDuplicate={onDuplicate ? () => onDuplicate(recipe.id) : null}
                    onRemove={onRemove ? () => onRemove(recipe.id) : null}
                    onSelect={edit ? () => this.toggleOne(recipe.id) : null}
                />
            </ListItem>
        )
    }

    setEdit(edit) {
        this.setState({edit})
    }

    setMove(move) {
        this.setState({move})
    }

    getHandleIcon({column, sortingOrder, sortingDirection}) {
        const sorted = sortingOrder === column
        return sorted
            ? sortingDirection === 1
                ? 'sort-down'
                : 'sort-up'
            : 'sort'
    }

    getRecipeTypeName(type) {
        const recipeType = getRecipeType(type)
        return recipeType && recipeType.labels.name
    }

    isLoading() {
        const {recipes, stream} = this.props
        return !recipes && stream('LOAD_RECIPES').active
    }

    hasData() {
        const {recipes} = this.props
        return recipes && recipes.length
    }

    setFilter(filterValue) {
        const filterValues = splitString(simplifyString(filterValue))
        actionBuilder('SET_FILTER_VALUES', {filterValue, filterValues})
            .set(['process.filterValue'], filterValue)
            .set(['process.filterValues'], filterValues)
            .dispatch()
    }

    setSorting(sortingOrder) {
        const {sortingOrder: prevSortingOrder, sortingDirection: prevSortingDirection} = this.props
        const sortingDirection = sortingOrder === prevSortingOrder
            ? -prevSortingDirection
            : 1
        actionBuilder('SET_SORTING_ORDER', {sortingOrder})
            .set(['process.sortingOrder'], sortingOrder)
            .set(['process.sortingDirection'], sortingDirection)
            .dispatch()
    }

    isSelected(recipeId) {
        const filteredSelectedIds = this.getFilteredSelectedIds()
        return recipeId
            ? filteredSelectedIds.includes(recipeId)
            : filteredSelectedIds.length
    }

    setSelectedIds(selectedIds) {
        actionBuilder('SET_SELECTED_IDS', {selectedIds})
            .set(['process.selectedIds'], selectedIds)
            .dispatch()
    }

    toggleOne(recipeId) {
        const {selectedIds: prevSelectedIds} = this.props
        const selectedIds = prevSelectedIds.includes(recipeId)
            ? prevSelectedIds.filter(currentRecipeId => currentRecipeId !== recipeId)
            : [...prevSelectedIds, recipeId]
        this.setSelectedIds(selectedIds)
    }

    toggleAll() {
        const {selectedIds: prevSelectedIds} = this.props
        const filteredIds = this.getFilteredIds()
        const filteredSelectedIds = this.getFilteredSelectedIds()
        const selectedIds = filteredSelectedIds.length
            ? _.difference(prevSelectedIds, filteredSelectedIds)
            : [...prevSelectedIds, ...filteredIds]
        this.setSelectedIds(selectedIds)
    }

    moveSelected(projectId) {
        const {onMove} = this.props
        const selectedIds = this.getFilteredSelectedIds()
        onMove(selectedIds, projectId)
    }

    removeSelected() {
        const {onRemove} = this.props
        const selectedIds = this.getFilteredSelectedIds()
        onRemove(selectedIds)
    }

    getFilteredIds() {
        const {filteredRecipes} = this.props
        return filteredRecipes.map(({id}) => id)
    }

    getFilteredSelectedIds() {
        const {selectedIds} = this.props
        const filteredIds = this.getFilteredIds()
        const filteredSelectedIds = _.intersection(selectedIds, filteredIds)
        return filteredSelectedIds
    }

    recipeMatchesProject(recipe) {
        const {projectId} = this.props
        return !projectId || projectId === recipe.projectId
    }

    recipeMatchesFilter(recipe, filterValues) {
        const searchMatchers = filterValues.map(filter => RegExp(filter, 'i'))
        const searchProperties = ['project', 'name']
        return filterValues
            ? _.every(searchMatchers, matcher =>
                _.find(searchProperties, property =>
                    matcher.test(simplifyString(recipe[property], {
                        removeNonAlphanumeric: true,
                        removeAccents: true
                    }))
                )
            )
            : true
    }

    getSorter(recipe, sortingOrder) {
        switch (sortingOrder) {
        case 'updateTime':
            return this.getSorterByUpdateTime(recipe)
        case 'name':
            return this.getSorterByName(recipe)
        }
    }

    getSorterByUpdateTime(recipe) {
        return recipe.updateTime
    }

    getSorterByName(recipe) {
        const project = recipe?.project
        const sorter = project
            ? `1:${recipe?.project}:${recipe.name}`
            : `0:${recipe.name}`
        return sorter.toUpperCase()
    }

    updateFilteredRecipes() {
        const {projects = [], recipes, filterValues} = this.props
        const {sortingOrder, sortingDirection} = this.props
        const filteredRecipes = _.chain(recipes)
            .map(recipe => {
                const project = projects.find(project => project.id === recipe.projectId)
                return {...recipe, project: project?.name}
            })
            .filter(recipe => this.recipeMatchesProject(recipe) && this.recipeMatchesFilter(recipe, filterValues))
            .orderBy(recipe => this.getSorter(recipe, sortingOrder), sortingDirection === 1 ? 'asc' : 'desc')
            .value()
        actionBuilder('SET_FILTERED_RECIPES', {filteredRecipes})
            .set(['process.filteredRecipes'], filteredRecipes)
            .dispatch()
    }

    componentDidUpdate(prevProps) {
        if (!_.isEqual(this.props, prevProps)) {
            this.updateFilteredRecipes()
        }
    }
}

export const RecipeListData = compose(
    _RecipeListData,
    connect(mapStateToProps)
)

RecipeListData.propTypes = {
    recipeId: PropTypes.string,
    onClick: PropTypes.func,
    onDuplicate: PropTypes.func,
    onRemove: PropTypes.func,
    onSelect: PropTypes.func
}
<|endoftext|>


<commit_before>fixDivHeight();
init();

function init() {
  var entryInfo = {
      entryPoint: "stupla_fs09",
      rootElementLevel: 4
  }

  var subjects = getElements(getRootElement(entryInfo));

  var cal = [];

  cal.push('BEGIN:VCALENDAR');
  cal.push('CALSCALE:GREGORIAN');
  cal.push('VERSION:2.0');
  cal.push('METHOD:PUBLISH');
  cal.push('X-WR-CALNAME:Uni');
  cal.push('X-WR-TIMEZONE:Europe/Berlin');
  cal.push('X-APPLE-CALENDAR-COLOR:#1BADF8');
  cal.push('BEGIN:VTIMEZONE');
  cal.push('TZID:Europe/Berlin');
  cal.push('BEGIN:DAYLIGHT');
  cal.push('TZOFFSETFROM:+0100');
  cal.push('RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU');
  cal.push('DTSTART:19810329T020000');
  cal.push('TZNAME:MESZ');
  cal.push('TZOFFSETTO:+0200');
  cal.push('END:DAYLIGHT');
  cal.push('BEGIN:STANDARD');
  cal.push('TZOFFSETFROM:+0200');
  cal.push('RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU');
  cal.push('DTSTART:19961027T030000');
  cal.push('TZNAME:MEZ');
  cal.push('TZOFFSETTO:+0100');
  cal.push('END:STANDARD');
  cal.push('END:VTIMEZONE');

  try {
    for (i in subjects) {
      var event = getEventData(subjects[i]);
      cal = addEvent(cal, event);
    }
  } catch (e) {}

  cal.push('END:VCALENDAR');
  var str = cal.join('\n');

  console.log(str);
  var dl = new Blob([str], {type: "text/plain;charset=utf-8"});
  saveAs(dl, "calendar.ics");
}

function getRootElement(entryInfo) {
  return $(document.getElementsByClassName(entryInfo.entryPoint)[0]).parents().eq(entryInfo.rootElementLevel)[0];
}

function getElements(root) {
  return root.getElementsByTagName('div');
}

function getNameOfLecture(object) {
    var name = object.childNodes[1].innerText;
    return name.slice(0, (name.length - 12));
}

function getSpeakerOfLecture(object) {
  return object.childNodes[3].innerText.slice(12).split(',').join('\\,');
}

function getDayOfWeek(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[3].innerText;
}

function getLocation(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[9].innerText.split(',').join('\\,');
}

function getTime(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[7].innerText;
}

function getDates(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[5].innerText;
}

function getTargetGroup(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[11].innerText;
}

function getLastUpdated(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[13].innerText.slice(13);
}

function getEventData(subject) {
  var event = {
    name: "",
    speaker: "",
    location: "",
    begin: "",
    end: ""
  }

  event.name = getNameOfLecture(subject);
  event.speaker = getSpeakerOfLecture(subject);
  event.location = getLocation(subject);

  var time = parseTime(getTime(subject));
  event.begin = time[0];
  event.end = time[1];

  return event;
}

function parseTime(raw) {
  raw = raw.split(" ");
  var hours = [];
  hours[0] = raw[0].split('.');
  hours[1] = raw[2].split('.');

  var time =[];

  time[0] = new String('20150425' + 'T' + hours[0][0] + hours[0][1] + '00');
  time[1] = new String('20150425' + 'T' + hours[1][0] + hours[1][1] + '00');

  return time;
}

function addEvent(cal, event) {
  cal.push('BEGIN:VEVENT');
  cal.push('CREATED:20150425T221630Z');
  cal.push('UID:' + uid());
  cal.push('DTEND;TZID=Europe/Berlin:' + event.end);
  cal.push("LOCATION:" + event.location);
  cal.push('TRANSP:OPAQUE');
  cal.push('SUMMARY:' + event.name);
  cal.push("DESCRIPTION:" + event.speaker);
  cal.push('DTSTART;TZID=Europe/Berlin:' + event.begin);
  cal.push('DTSTAMP:20150425T221630Z');
  cal.push('SEQUENCE:0');
  cal.push('END:VEVENT');

  return cal;
}

function uid() {
  function S4() {
    return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
  }
  console.log(S4);

  return(S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase();
}

function fixDivHeight() {
    document.getElementById("rechtespalte2").style.height = "auto";
}
<commit_msg>finish lecture export<commit_after>fixDivHeight();
init();

function init() {
  var entryInfo = {
      entryPoint: "stupla_fs09",
      rootElementLevel: 4
  }

  var subjects = getElements(getRootElement(entryInfo));

  var cal = [];

  cal.push('BEGIN:VCALENDAR');
  cal.push('CALSCALE:GREGORIAN');
  cal.push('VERSION:2.0');
  cal.push('METHOD:PUBLISH');
  cal.push('X-WR-CALNAME:Uni');
  cal.push('X-WR-TIMEZONE:Europe/Berlin');
  cal.push('X-APPLE-CALENDAR-COLOR:#1BADF8');
  cal.push('BEGIN:VTIMEZONE');
  cal.push('TZID:Europe/Berlin');
  cal.push('BEGIN:DAYLIGHT');
  cal.push('TZOFFSETFROM:+0100');
  cal.push('RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU');
  cal.push('DTSTART:19810329T020000');
  cal.push('TZNAME:MESZ');
  cal.push('TZOFFSETTO:+0200');
  cal.push('END:DAYLIGHT');
  cal.push('BEGIN:STANDARD');
  cal.push('TZOFFSETFROM:+0200');
  cal.push('RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU');
  cal.push('DTSTART:19961027T030000');
  cal.push('TZNAME:MEZ');
  cal.push('TZOFFSETTO:+0100');
  cal.push('END:STANDARD');
  cal.push('END:VTIMEZONE');

  try {
    for (i in subjects) {
      console.log(subjects[i]);
      var event = getEventData(subjects[i]);
      cal = addEvent(cal, event);
      console.log(event);
    }
  } catch (e) {}

  cal.push('END:VCALENDAR');
  var str = cal.join('\n');

  console.log([str]);

  var dl = new Blob([str], {type: "text/plain;charset=utf-8"});
  saveAs(dl, "calendar.ics");
}

function getRootElement(entryInfo) {
  return $(document.getElementsByClassName(entryInfo.entryPoint)[0]).parents().eq(entryInfo.rootElementLevel)[0];
}

function getElements(root) {
  return root.getElementsByTagName('div');
}

function getNameOfLecture(object) {
    var name = object.childNodes[1].innerText;
    return name.slice(0, (name.length - 12));
}

function getSpeakerOfLecture(object) {
  return object.childNodes[3].innerText.slice(12).split(',').join('\\,');
}

function getDayOfWeek(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[3].innerText;
}

function getLocation(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[9].innerText.split(',').join('\\,');
}

function getTime(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[7].innerText;
}

function getDates(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[5].innerText;
}

function getTargetGroup(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[11].innerText;
}

function getLastUpdated(object) {
  return object.childNodes[5].childNodes[3].childNodes[0].childNodes[13].innerText.slice(13);
}

function getEventData(subject) {
  var event = {
    name: "",
    speaker: "",
    location: "",
    begin: "",
    end: ""
  }

  event.name = getNameOfLecture(subject);
  event.speaker = getSpeakerOfLecture(subject);
  event.location = getLocation(subject);

  var time = parseTime(getTime(subject), getDayOfWeek(subject));
  event.begin = time[0];
  event.end = time[1];

  return event;
}

function parseTime(raw, day) {
  raw = raw.split(" ");
  var hours = [];
  hours[0] = raw[0].split('.');
  hours[1] = raw[2].split('.');

  var time =[];
  try {
    var dt = getDateOfWeek(18, 2015, day);
  } catch(e) { console.log(e); }

  var year = dt.getFullYear().toString();

  var month = dt.getMonth() + 1;
  if (month < 10) {
    month = '0' + month.toString();
  }

  var date = dt.getDate().toString();

  time[0] = new String(year + month + date + 'T' + hours[0][0] + hours[0][1] + '00');
  time[1] = new String(year + month + date + 'T' + hours[1][0] + hours[1][1] + '00');

  return time;
}

function addEvent(cal, event) {
  cal.push('BEGIN:VEVENT');
  cal.push('CREATED:20150425T221630Z');
  cal.push('UID:' + uid());
  cal.push('DTEND;TZID=Europe/Berlin:' + event.end);
  cal.push("LOCATION:" + event.location);
  cal.push('TRANSP:OPAQUE');
  cal.push('SUMMARY:' + event.name);
  cal.push("DESCRIPTION:" + event.speaker);
  cal.push('DTSTART;TZID=Europe/Berlin:' + event.begin);
  cal.push('DTSTAMP:20150425T221630Z');
  cal.push('SEQUENCE:0');
  cal.push('END:VEVENT');

  return cal;
}

function getDateOfWeek(w, y, day) {
    var simple = new Date(y, 0, 1 + (w - 1) * 7);
    var date = simple;
    var dow = simple.getDay();
    var weekStart = simple;
    if (dow <= 4)
        weekStart.setDate(simple.getDate() - simple.getDay() + 1);
    else
        weekStart.setDate(simple.getDate() + 8 - simple.getDay());

    switch(day) {
      case 'Montag':
        break;

      case 'Dienstag':
        date.setDate(weekStart.getDate() + 1);
        break;

      case 'Mittwoch':
        date.setDate(weekStart.getDate() + 2);
        break;

      case 'Donnerstag':
        date.setDate(weekStart.getDate() + 3);
        break;

      case 'Freitag':
        date.setDate(weekStart.getDate() + 4);
        break;

      case 'Samstag':
        date.setDate(weekStart.getDate() + 5);
        break;

      case 'Sonntag':
        date.setDate(weekStart.getDate() + 6);
        break;
        }

    return date;
}

function uid() {
  function S4() {
    return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
  }

  return(S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase();
}

function fixDivHeight() {
    document.getElementById("rechtespalte2").style.height = "auto";
}
<|endoftext|>


<commit_before>(function() {
  'use strict';

  angular
    .module('vmsFrontend')
    .controller('CreateProjectController', CreateProjectController);

  /** @ngInject */
  function CreateProjectController(
    $log,
    $state,
    $translate,
    project,
    projectHyperlink,
    PERMISSION_OPTIONS,
    FROALA_OPTIONS
  ) {
    var vm = this;
    vm.permissionOptions = PERMISSION_OPTIONS;
    vm.froalaOptions = FROALA_OPTIONS;
    vm.hyperlinks = [
      {
        name: '',
        link: ''
      }
    ];

    vm.create = function() {
      vm.alert = [];

      project
        .create(vm.project)
        .then(function(value) {
          $log.debug(value);

          var projectId = value.data.id;

          storeHyperlinks(projectId);
        })
        .catch(function(alert) {
          vm.alert.push(alert);
        });
    };

    function storeHyperlinks(projectId) {
      projectHyperlink
        .create(projectId, vm.hyperlinks)
        .then(function() {
          $log.debug("storeHyperlinks() success");
          $log.debug("projectId = " + projectId);

          $state.go('projectDetail', {
            id: projectId
          });
        })
        .catch(function() {
          $log.debug("storeHyperlinks() failure");
        })
    }
  }
})();
<commit_msg>Fix the error that change page after creating project<commit_after>(function() {
  'use strict';

  angular
    .module('vmsFrontend')
    .controller('CreateProjectController', CreateProjectController);

  /** @ngInject */
  function CreateProjectController(
    $log,
    $state,
    $translate,
    project,
    projectHyperlink,
    PERMISSION_OPTIONS,
    FROALA_OPTIONS
  ) {
    var vm = this;
    vm.permissionOptions = PERMISSION_OPTIONS;
    vm.froalaOptions = FROALA_OPTIONS;
    vm.hyperlinks = [
      {
        name: '',
        link: ''
      }
    ];

    vm.create = function() {
      vm.alert = [];

      project
        .create(vm.project)
        .then(function(value) {
          $log.debug(value);

          var projectId = value.id;

          storeHyperlinks(projectId);
        })
        .catch(function(alert) {
          vm.alert.push(alert);
        });
    };

    function storeHyperlinks(projectId) {
      projectHyperlink
        .create(projectId, vm.hyperlinks)
        .then(function() {
          $log.debug("storeHyperlinks() success");
          $log.debug("projectId = " + projectId);

          $state.go('projectDetail', {
            id: projectId
          });
        })
        .catch(function() {
          $log.debug("storeHyperlinks() failure");
        })
    }
  }
})();
<|endoftext|>


<commit_before>//@ts-check
((cl) => {
	typeof module !== 'undefined' ?
		module.exports = cl :
		//@ts-ignore
		window.changelogLog = cl;
})(function (config) {
	'use strict';
	const changelog = {
		'2.0.0': [
			'Revamped the UI',
			'Rewrote the entire extension',
			'Added the CRM-API for more features',
			'Added a stylesheet element',
			'Added the ability to only show certain nodes on specified websites and/or content types',
			'Added the ability to reposition nodes',
			'Made it possible to delete or export selected nodes',
			'Added compatability with Grease/Tamper-Monkey UserScripts',
			'Added compatability with UserStylesheets',
			'Added the ability to connect a script/stylesheet to a local editor',
			'Now uses the better CodeMirror editor'
		],
		'2.0.4': [
			'Scripts that use the Chrome API are now transferred over to using the crmAPI.chrome API',
			'More clearly reflects a script created by you and what that means (all permissions)',
			'Keeps track of info of local nodes as well now',
			'Allows the editing of the version number'
		],
		'2.0.5': [
			'Fixed a bug that caused scripts to not run when the catch errors option was on'
		],
		'2.0.6': [
			'Added a docs button on script-edit page',
			'Shows a warning when using the deprecated direct chrome API (use crmAPI.chrome instead)',
			'Fixed a bug that occurred on saving',
		],
		'2.0.7': [
			'Fixed json parser bug that sometimes occurred on loading page',
			'Code cleanup and dead code removal',
			'Node instances are now all shown',
			'Fixed some bugs related to scripts not running in their own context'
		],
		'2.0.8': [
			'Fixed some nodes missing IDs after creation',
			'Fixed error that occurred on backgroundpage when running any script'
		],
		'2.0.9': [
			'Fixed bug with buttons no longer working'
		],
		'2.0.10': [
			'Fixed a bug in the crmAPI.GM.GM_notification function'
		],
		'2.0.11': [
			'Fixed the "Â" character appearing in the editors',
			'Added an indicator for when the exporting field is slow',
			'Added/removed permissions are updated instantly',
			'Fixed userscript installation page',
			'Fixed unsafewindow permission',
			'No longer enables userscript installation if TamperMonkey is already installed'
		],
		'2.0.12': [
			'Added background.runScript, background.runSelf and background.addKeyboardListener functions to crmAPI',
			'Rewrote crmAPI file and fixed some of its bugs',
			'Cleaned up and structured logs on background file console'
		],
		'2.0.13': [
			'Fixed GM_xmlhttpRequest error (thanks to [bumaociyuan](https://github.com/bumaociyuan) for the report)'
		],
		'2.0.14': [
			'Added a remove button for libraries (thanks to robinchew for the suggestion)',
			'Fixed some bugs related to background-pages'
		],
		'2.0.15': [
			'It\'s now possible to change the root menu name (thanks to anonymous user)'
		],
		'2.0.16': [
			'Fixed possible bug that can break the CRM on the options page from appearing since the last update'
		],
		'2.0.17': [
			'Nodes are selectable again',
			'Link nodes can be removed properly',
			'%s in link nodes gets replaced with the currently selected text'
		],
		'2.0.18': [
			'Fixed dragging',
			'Types can be changed again',
			'Widened nodes'
		],
		'2.0.19': [
			'Fixed cancelling selection',
			'Fixed type switching animation'
		],
		'2.1.0': [
			'Uses polymer 2 instead of polymer 1 (~25% faster loading)',
			'Works on multiple browsers now (chrome, firefox, edge and opera)',
			'Uses the monaco editor instead of codemirror, featuring better code completion and more features overall',
			'Supports writing scripts in [typescript](https://www.typescriptlang.org/) and compiling them in-extension',
			'Supports typescript libraries and includes code completion for them as well',
			'Added a "libraries" tab in the script dialog for quick toggling of libraries',
			'Libraries can be edited and/or removed',
			'You can now change the name of the root node',
			'Uses [TypeDoc](http://typedoc.org/) for documentation instead',
			'Added the ' +
				'[crmAPI.storageSync](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#storagesync)' +
				' API for syncing your script\'s storage across browser instances',
			'Added the ' +
				'[crmAPI.browser](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#browser)' +
				' API which combines the [webExtensions](https://developer.mozilla.org/nl/Add-ons/WebExtensions)' +
				' and [crmAPI.chrome](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#chrome)' +
				' functionalities',
			'All callback-based crmAPI functions now also return a promise as well',
			'Added the ' +
				'[crmAPI.contextMenuItem](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#contextmenuitem)' +
				' API for changing the contextmenu item\'s look,' +
				' such as adding/removing a checkmark, changing the title, disabling or hiding it',
			'Added the lastError, instanceId, chromeAPISupported and browserAPISupported properties' +
				' to the crmAPI object',
			'Added better code completion for the CRM API',
			'The script and stylesheet options tab has better code completion',
			'It\'s now possible to create contextmenu items as a script',
			'Removed the external editor\'s functionality as chrome apps are being [deprecated](https://blog.chromium.org/2016/08/from-chrome-apps-to-web.html)',
			'Added some more options to the "get page property" ribbon tool',
			'Added support for a more programmatic way of editing the CRM through window.consoleInfo()',
			'Reversed changelog order so the newest changes are on top',
			'Plenty of changes/improvements to the UI',
			'A lot of bug fixes and improvements to code legibility and documentation'
		],
		'2.1.1': [
			'Changed the logo. Huge thanks to [Ulises](https://github.com/tjulises) for designing the logo',
			'Added link to search engine dialog on main options page',
			'Replaces %s with selected text in links, enabling easy search engine creation',
			'Added checkmark after clicking some buttons, giving a bit more visual feedback',
			'Hides code options button if there are no options'
		]
	};

	return changelog;
}());<commit_msg>update changelog<commit_after>//@ts-check
((cl) => {
	typeof module !== 'undefined' ?
		module.exports = cl :
		//@ts-ignore
		window.changelogLog = cl;
})(function (config) {
	'use strict';
	const changelog = {
		'2.0.0': [
			'Revamped the UI',
			'Rewrote the entire extension',
			'Added the CRM-API for more features',
			'Added a stylesheet element',
			'Added the ability to only show certain nodes on specified websites and/or content types',
			'Added the ability to reposition nodes',
			'Made it possible to delete or export selected nodes',
			'Added compatability with Grease/Tamper-Monkey UserScripts',
			'Added compatability with UserStylesheets',
			'Added the ability to connect a script/stylesheet to a local editor',
			'Now uses the better CodeMirror editor'
		],
		'2.0.4': [
			'Scripts that use the Chrome API are now transferred over to using the crmAPI.chrome API',
			'More clearly reflects a script created by you and what that means (all permissions)',
			'Keeps track of info of local nodes as well now',
			'Allows the editing of the version number'
		],
		'2.0.5': [
			'Fixed a bug that caused scripts to not run when the catch errors option was on'
		],
		'2.0.6': [
			'Added a docs button on script-edit page',
			'Shows a warning when using the deprecated direct chrome API (use crmAPI.chrome instead)',
			'Fixed a bug that occurred on saving',
		],
		'2.0.7': [
			'Fixed json parser bug that sometimes occurred on loading page',
			'Code cleanup and dead code removal',
			'Node instances are now all shown',
			'Fixed some bugs related to scripts not running in their own context'
		],
		'2.0.8': [
			'Fixed some nodes missing IDs after creation',
			'Fixed error that occurred on backgroundpage when running any script'
		],
		'2.0.9': [
			'Fixed bug with buttons no longer working'
		],
		'2.0.10': [
			'Fixed a bug in the crmAPI.GM.GM_notification function'
		],
		'2.0.11': [
			'Fixed the "Â" character appearing in the editors',
			'Added an indicator for when the exporting field is slow',
			'Added/removed permissions are updated instantly',
			'Fixed userscript installation page',
			'Fixed unsafewindow permission',
			'No longer enables userscript installation if TamperMonkey is already installed'
		],
		'2.0.12': [
			'Added background.runScript, background.runSelf and background.addKeyboardListener functions to crmAPI',
			'Rewrote crmAPI file and fixed some of its bugs',
			'Cleaned up and structured logs on background file console'
		],
		'2.0.13': [
			'Fixed GM_xmlhttpRequest error (thanks to [bumaociyuan](https://github.com/bumaociyuan) for the report)'
		],
		'2.0.14': [
			'Added a remove button for libraries (thanks to robinchew for the suggestion)',
			'Fixed some bugs related to background-pages'
		],
		'2.0.15': [
			'It\'s now possible to change the root menu name (thanks to anonymous user)'
		],
		'2.0.16': [
			'Fixed possible bug that can break the CRM on the options page from appearing since the last update'
		],
		'2.0.17': [
			'Nodes are selectable again',
			'Link nodes can be removed properly',
			'%s in link nodes gets replaced with the currently selected text'
		],
		'2.0.18': [
			'Fixed dragging',
			'Types can be changed again',
			'Widened nodes'
		],
		'2.0.19': [
			'Fixed cancelling selection',
			'Fixed type switching animation'
		],
		'2.1.0': [
			'Uses polymer 2 instead of polymer 1 (~25% faster loading)',
			'Works on multiple browsers now (chrome, firefox, edge and opera)',
			'Uses the monaco editor instead of codemirror, featuring better code completion and more features overall',
			'Supports writing scripts in [typescript](https://www.typescriptlang.org/) and compiling them in-extension',
			'Supports typescript libraries and includes code completion for them as well',
			'Added a "libraries" tab in the script dialog for quick toggling of libraries',
			'Libraries can be edited and/or removed',
			'You can now change the name of the root node',
			'Uses [TypeDoc](http://typedoc.org/) for documentation instead',
			'Added the ' +
				'[crmAPI.storageSync](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#storagesync)' +
				' API for syncing your script\'s storage across browser instances',
			'Added the ' +
				'[crmAPI.browser](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#browser)' +
				' API which combines the [webExtensions](https://developer.mozilla.org/nl/Add-ons/WebExtensions)' +
				' and [crmAPI.chrome](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#chrome)' +
				' functionalities',
			'All callback-based crmAPI functions now also return a promise as well',
			'Added the ' +
				'[crmAPI.contextMenuItem](https://sanderronde.github.io/CustomRightClickMenu/documentation/classes/crm.crmapi.instance.html#contextmenuitem)' +
				' API for changing the contextmenu item\'s look,' +
				' such as adding/removing a checkmark, changing the title, disabling or hiding it',
			'Added the lastError, instanceId, chromeAPISupported and browserAPISupported properties' +
				' to the crmAPI object',
			'Added better code completion for the CRM API',
			'The script and stylesheet options tab has better code completion',
			'It\'s now possible to create contextmenu items as a script',
			'Removed the external editor\'s functionality as chrome apps are being [deprecated](https://blog.chromium.org/2016/08/from-chrome-apps-to-web.html)',
			'Added some more options to the "get page property" ribbon tool',
			'Added support for a more programmatic way of editing the CRM through window.consoleInfo()',
			'Reversed changelog order so the newest changes are on top',
			'Plenty of changes/improvements to the UI',
			'A lot of bug fixes and improvements to code legibility and documentation'
		],
		'2.1.1': [
			'Changed the logo. Huge thanks to [Ulises](https://github.com/tjulises) for designing the logo',
			'Added link to search engine dialog on main options page',
			'Replaces %s with selected text in links, enabling easy search engine creation',
			'Added checkmark after clicking some buttons, giving a bit more visual feedback',
			'Hides code options button if there are no options',
			'Content types can be toggled instead of only being able to select a single one',
			'Nodes that are not shown on the current content type are greyed out instead of hidden'
		]
	};

	return changelog;
}());<|endoftext|>


<commit_before>Router.configure({
    loadingTemplate: 'loading',
    notFoundTemplate: 'notFound',
    layoutTemplate: 'layout'
})

Router.map(function () {
    this.route('home', { path: '/' });
    this.route('categories', { path: '/categories' });
    this.route('notes', { path: '/notes' });
});<commit_msg>Update file format<commit_after>Router.configure({
    loadingTemplate: 'loading',
    notFoundTemplate: 'notFound',
    layoutTemplate: 'layout'
})

Router.map(function () {
    this.route('home', {
        path: '/'
    });
    this.route('categories', {
        path: '/categories'
    });
    this.route('notes', {
        path: '/notes'
    });
});<|endoftext|>


<commit_before>// Licensed under the Apache License, Version 2.0 (the “License”); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

import {
  browsers,
  flow,
  jsx,
  node,
  platforms,
  react,
  typeScript,
} from '../flags'
import {CONFIGS_DIR, PACKAGE_DIR} from '../paths'
import {join} from 'path'
import {exitOnError, runCommandSync} from '../system'
import {exitOnPackageNotFound} from '../util'

const CONFIG_PATH = join(CONFIGS_DIR, 'eslint', 'index.js')
const BASIC_ARGS = [
  `--config "${CONFIG_PATH}"`,
  `"${join(PACKAGE_DIR, 'src')}"`,
]

function builder(yargs) {
  browsers(yargs)
  flow(yargs)
  jsx(yargs)
  node(yargs)
  platforms(yargs)
  react(yargs)
  typeScript(yargs)

  yargs.option('fix', {
    description: 'Fix lint errors.',
  })
}

function lintSources(args) {
  let eslintArgs = [
    '--ignore-pattern "src/**/__tests__"',
    '--ignore-pattern "src/**/__tests__/**"',
    ...BASIC_ARGS,
  ]

  let {fix} = args
  if (fix)
    eslintArgs.push('--fix')

  return runCommandSync('eslint', {
    args: eslintArgs,
    env: args,
  })
}

function lintTests(args) {
  let eslintArgs = [
    '--ignore-pattern "src/**"',
    '--ignore-pattern "!**/__tests__"',
    '--ignore-pattern "!**/__tests__/**"',
    ...BASIC_ARGS,
  ]

  let {fix} = args
  if (fix)
    eslintArgs.push('--fix')

  return runCommandSync('eslint', {
    args: eslintArgs,
    env: {...args, jest: true},
  })
}

function handler(args) {
  exitOnPackageNotFound()

  console.log()
  console.log('[0/2] Linting sources...')
  let resultSources = lintSources(args)
  console.log('[1/2] Linting tests...')
  let resultTests = lintTests(args)
  console.log('[2/2] Done linting.')

  exitOnError(resultSources)
  exitOnError(resultTests)
}

export default {
  command: 'lint',
  description: 'Run ESLint',
  builder,
  handler,
}
<commit_msg>Use stdout<commit_after>// Licensed under the Apache License, Version 2.0 (the “License”); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

import {
  browsers,
  flow,
  jsx,
  node,
  platforms,
  react,
  typeScript,
} from '../flags'
import {CONFIGS_DIR, PACKAGE_DIR} from '../paths'
import {join} from 'path'
import {exitOnError, runCommandSync} from '../system'
import {exitOnPackageNotFound} from '../util'

const CONFIG_PATH = join(CONFIGS_DIR, 'eslint', 'index.js')
const BASIC_ARGS = [
  `--config "${CONFIG_PATH}"`,
  `"${join(PACKAGE_DIR, 'src')}"`,
]

function builder(yargs) {
  browsers(yargs)
  flow(yargs)
  jsx(yargs)
  node(yargs)
  platforms(yargs)
  react(yargs)
  typeScript(yargs)

  yargs.option('fix', {
    description: 'Fix lint errors.',
  })
}

function lintSources(args) {
  let eslintArgs = [
    '--ignore-pattern "src/**/__tests__"',
    '--ignore-pattern "src/**/__tests__/**"',
    ...BASIC_ARGS,
  ]

  let {fix} = args
  if (fix)
    eslintArgs.push('--fix')

  return runCommandSync('eslint', {
    args: eslintArgs,
    env: args,
  })
}

function lintTests(args) {
  let eslintArgs = [
    '--ignore-pattern "src/**"',
    '--ignore-pattern "!**/__tests__"',
    '--ignore-pattern "!**/__tests__/**"',
    ...BASIC_ARGS,
  ]

  let {fix} = args
  if (fix)
    eslintArgs.push('--fix')

  return runCommandSync('eslint', {
    args: eslintArgs,
    env: {...args, jest: true},
  })
}

function handler(args) {
  exitOnPackageNotFound()

  const STDOUT = process.stdout
  STDOUT.write('[0/2] Linting sources...\n')
  let resultSources = lintSources(args)
  STDOUT.write('[1/2] Linting tests...\n')
  let resultTests = lintTests(args)
  STDOUT.write('[2/2] Done.')

  exitOnError(resultSources)
  exitOnError(resultTests)
}

export default {
  command: 'lint',
  description: 'Run ESLint',
  builder,
  handler,
}
<|endoftext|>


<commit_before>var WebSocketServer = require('../../').Server
  , express = require('express')
  , fs = require('fs')
  , path = require('path')
  , app = express.createServer();

app.use(express.static(__dirname + '/public'));

function makePathForFile(filePath, prefix, cb) {
  if (typeof cb !== 'function') throw new Error('callback is required');
  filePath = path.dirname(path.normalize(filePath)).replace(/^(\/|\\)+/, '');
  var pieces = filePath.split('/');
  var incrementalPath = prefix;
  function step(error) {
    if (error) return cb(error);
    if (pieces.length == 0) return cb(null, incrementalPath);
    incrementalPath += '/' + pieces.shift();
    path.exists(incrementalPath, function(exists) {
      if (!exists) fs.mkdir(incrementalPath, step);
      else process.nextTick(step);
    });
  }
  step();
}

var wss = new WebSocketServer({server: app});
wss.on('connection', function(ws) {
  var currentFile = null;
  ws.on('message', function(data, flags) {
    if (!flags.binary) {
      currentFile = JSON.parse(data);
      // note: a real-world app would want to sanity check the data
    }
    else {
      if (currentFile == null) return;
      makePathForFile(currentFile.path, __dirname + '/uploaded', function(error, path) {
        if (error) {
          console.log(error);
          ws.send(JSON.stringify({event: 'error', path: currentFile.path, message: error.message}));
          return;
        }
        fs.writeFile(path + '/' + currentFile.name, data, function(error) {
          currentFile = null;
          console.log('received %d bytes long file, %s', data.length, currentFile.path);
          ws.send(JSON.stringify({event: 'complete', path: currentFile.path}));
        });
      });
    }
  });
  ws.on('close', function() {
    console.log('closed', arguments);
  });
  ws.on('error', function(e) {
    console.log('error', e);
  });
});

fs.mkdir(__dirname + '/uploaded', function(error) {
  // ignore errors, most likely means directory exists
  console.log('Uploaded files will be saved to %s/uploaded.', __dirname);
  console.log('Remember to wipe this directory if you upload lots and lots.');
  app.listen(8080);
  console.log('Listening on http://localhost:8080');
});
<commit_msg>corrected refactoring issue in example<commit_after>var WebSocketServer = require('../../').Server
  , express = require('express')
  , fs = require('fs')
  , path = require('path')
  , app = express.createServer();

app.use(express.static(__dirname + '/public'));

function makePathForFile(filePath, prefix, cb) {
  if (typeof cb !== 'function') throw new Error('callback is required');
  filePath = path.dirname(path.normalize(filePath)).replace(/^(\/|\\)+/, '');
  var pieces = filePath.split('/');
  var incrementalPath = prefix;
  function step(error) {
    if (error) return cb(error);
    if (pieces.length == 0) return cb(null, incrementalPath);
    incrementalPath += '/' + pieces.shift();
    path.exists(incrementalPath, function(exists) {
      if (!exists) fs.mkdir(incrementalPath, step);
      else process.nextTick(step);
    });
  }
  step();
}

var wss = new WebSocketServer({server: app});
wss.on('connection', function(ws) {
  var currentFile = null;
  ws.on('message', function(data, flags) {
    if (!flags.binary) {
      currentFile = JSON.parse(data);
      // note: a real-world app would want to sanity check the data
    }
    else {
      if (currentFile == null) return;
      makePathForFile(currentFile.path, __dirname + '/uploaded', function(error, path) {
        if (error) {
          console.log(error);
          ws.send(JSON.stringify({event: 'error', path: currentFile.path, message: error.message}));
          return;
        }
        fs.writeFile(path + '/' + currentFile.name, data, function(error) {
          console.log('received %d bytes long file, %s', data.length, currentFile.path);
          ws.send(JSON.stringify({event: 'complete', path: currentFile.path}));
          currentFile = null;
        });
      });
    }
  });
  ws.on('close', function() {
    console.log('closed', arguments);
  });
  ws.on('error', function(e) {
    console.log('error', e);
  });
});

fs.mkdir(__dirname + '/uploaded', function(error) {
  // ignore errors, most likely means directory exists
  console.log('Uploaded files will be saved to %s/uploaded.', __dirname);
  console.log('Remember to wipe this directory if you upload lots and lots.');
  app.listen(8080);
  console.log('Listening on http://localhost:8080');
});
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014
 *
 * This file is licensed under the Affero General Public License version 3
 * or later.
 *
 * See the COPYING-README file.
 *
 */

/* global Files */

(function() {

	var TEMPLATE_MENU =
		'
' +
		'
' +
		'{{uploadLabel}}' +
		'
' +
		'{{#each items}}' +
		'
' +
		'{{displayName}}' +
		'
' +
		'{{/each}}' +
		'
';

	var TEMPLATE_FILENAME_FORM =
		'<form class="filenameform">' +
		'<input id="{{cid}}-input-{{fileType}}" type="text" value="{{fileName}}" autocomplete="off" autocapitalize="off">' +
		'<input type="submit" value=" " class="icon-confirm" />'
		'</form>';

	/**
	 * Construct a new NewFileMenu instance
	 * @constructs NewFileMenu
	 *
	 * @memberof OCA.Files
	 */
	var NewFileMenu = OC.Backbone.View.extend({
		tagName: 'div',
		// Menu is opened by default because it's rendered on "add-button" click
		className: 'newFileMenu popovermenu bubble menu open menu-left',

		events: {
			'click .menuitem': '_onClickAction'
		},

		/**
		 * @type OCA.Files.FileList
		 */
		fileList: null,

		initialize: function(options) {
			var self = this;
			var $uploadEl = $('#file_upload_start');
			if ($uploadEl.length) {
				$uploadEl.on('fileuploadstart', function() {
					self.trigger('actionPerformed', 'upload');
				});
			} else {
				console.warn('Missing upload element "file_upload_start"');
			}

			this.fileList = options && options.fileList;

			this._menuItems = [{
				id: 'folder',
				displayName: t('files', 'New folder'),
				templateName: t('files', 'New folder'),
				iconClass: 'icon-folder',
				fileType: 'folder',
				actionHandler: function(name) {
					self.fileList.createDirectory(name);
				}
		        }];

			OC.Plugins.attach('OCA.Files.NewFileMenu', this);
		},

		template: function(data) {
			if (!OCA.Files.NewFileMenu._TEMPLATE) {
				OCA.Files.NewFileMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU);
			}
			return OCA.Files.NewFileMenu._TEMPLATE(data);
		},

		/**
		 * Event handler whenever an action has been clicked within the menu
		 *
		 * @param {Object} event event object
		 */
		_onClickAction: function(event) {
			var $target = $(event.target);
			if (!$target.hasClass('menuitem')) {
				$target = $target.closest('.menuitem');
			}
			var action = $target.attr('data-action');
			// note: clicking the upload label will automatically
			// set the focus on the "file_upload_start" hidden field
			// which itself triggers the upload dialog.
			// Currently the upload logic is still in file-upload.js and filelist.js
			if (action === 'upload') {
				OC.hideMenus();
			} else {
				event.preventDefault();
				this.$el.find('.menuitem.active').removeClass('active');
				$target.addClass('active');
				this._promptFileName($target);
			}
		},

		_promptFileName: function($target) {
			var self = this;
			if (!OCA.Files.NewFileMenu._TEMPLATE_FORM) {
				OCA.Files.NewFileMenu._TEMPLATE_FORM = Handlebars.compile(TEMPLATE_FILENAME_FORM);
			}

			if ($target.find('form').length) {
				$target.find('input[type=\'text\']').focus();
				return;
			}

			// discard other forms
			this.$el.find('form').remove();
			this.$el.find('.displayname').removeClass('hidden');

			$target.find('.displayname').addClass('hidden');

			var newName = $target.attr('data-templatename');
			var fileType = $target.attr('data-filetype');
			var $form = $(OCA.Files.NewFileMenu._TEMPLATE_FORM({
				fileName: newName,
				cid: this.cid,
				fileType: fileType
			}));

			//this.trigger('actionPerformed', action);
			$target.append($form);

			// here comes the OLD code
			var $input = $form.find('input[type=\'text\']');
			var $submit = $form.find('input[type=\'submit\']');

			var lastPos;
			var checkInput = function () {
				var filename = $input.val();
				try {
					if (!Files.isFileNameValid(filename)) {
						// Files.isFileNameValid(filename) throws an exception itself
					} else if (self.fileList.inList(filename)) {
						throw t('files', '{newName} already exists', {newName: filename}, undefined, {
							escape: false
						});
					} else {
						return true;
					}
				} catch (error) {
					$input.attr('title', error);
					$input.tooltip({placement: 'right', trigger: 'manual', 'container': '.newFileMenu'});
					$input.tooltip('fixTitle');
					$input.tooltip('show');
					$input.addClass('error');
				}
				return false;
			};

			// verify filename on typing
			$input.keyup(function() {
				if (checkInput()) {
					$input.tooltip('hide');
					$input.removeClass('error');
				}
			});

			$submit.click(function(){
				$form.submit();
			});

			$input.focus();
			// pre select name up to the extension
			lastPos = newName.lastIndexOf('.');
			if (lastPos === -1) {
				lastPos = newName.length;
			}
			$input.selectRange(0, lastPos);

			$form.submit(function(event) {
				event.stopPropagation();
				event.preventDefault();

				if (checkInput()) {
					var newname = $input.val();

					/* Find the right actionHandler that should be called.
					 * Actions is retrieved by using `actionSpec.id` */
					action = _.filter(self._menuItems, function(item) {
						return item.id == $target.attr('data-action');
					}).pop();
					action.actionHandler(newname);

					$form.remove();
					$target.find('.displayname').removeClass('hidden');
					OC.hideMenus();
				}
			});
		},

		/**
		* Add a new item menu entry in the “New” file menu (in
		* last position). By clicking on the item, the
		* `actionHandler` function is called.
		*
		* @param {Object} actionSpec item’s properties
		*/
		addMenuEntry: function(actionSpec) {
			this._menuItems.push({
				id: actionSpec.id,
				displayName: actionSpec.displayName,
				templateName: actionSpec.templateName,
				iconClass: actionSpec.iconClass,
				fileType: actionSpec.fileType,
				actionHandler: actionSpec.actionHandler,
		        });
		},

		/**
		 * Renders the menu with the currently set items
		 */
		render: function() {
			this.$el.html(this.template({
				uploadMaxHumanFileSize: 'TODO',
				uploadLabel: t('files', 'Upload file'),
				items: this._menuItems
			}));
			OC.Util.scaleFixForIE8(this.$('.svg'));
		},

		/**
		 * Displays the menu under the given element
		 *
		 * @param {Object} $target target element
		 */
		showAt: function($target) {
			this.render();
			OC.showMenu(null, this.$el);
		}
	});

	OCA.Files.NewFileMenu = NewFileMenu;

})();
<commit_msg>Fixed event propagation on safari<commit_after>/*
 * Copyright (c) 2014
 *
 * This file is licensed under the Affero General Public License version 3
 * or later.
 *
 * See the COPYING-README file.
 *
 */

/* global Files */

(function() {

	var TEMPLATE_MENU =
		'
' +
		'
' +
		'{{uploadLabel}}' +
		'
' +
		'{{#each items}}' +
		'
' +
		'{{displayName}}' +
		'
' +
		'{{/each}}' +
		'
';

	var TEMPLATE_FILENAME_FORM =
		'<form class="filenameform">' +
		'<input id="{{cid}}-input-{{fileType}}" type="text" value="{{fileName}}" autocomplete="off" autocapitalize="off">' +
		'<input type="submit" value=" " class="icon-confirm" />'
		'</form>';

	/**
	 * Construct a new NewFileMenu instance
	 * @constructs NewFileMenu
	 *
	 * @memberof OCA.Files
	 */
	var NewFileMenu = OC.Backbone.View.extend({
		tagName: 'div',
		// Menu is opened by default because it's rendered on "add-button" click
		className: 'newFileMenu popovermenu bubble menu open menu-left',

		events: {
			'click .menuitem': '_onClickAction'
		},

		/**
		 * @type OCA.Files.FileList
		 */
		fileList: null,

		initialize: function(options) {
			var self = this;
			var $uploadEl = $('#file_upload_start');
			if ($uploadEl.length) {
				$uploadEl.on('fileuploadstart', function() {
					self.trigger('actionPerformed', 'upload');
				});
			} else {
				console.warn('Missing upload element "file_upload_start"');
			}

			this.fileList = options && options.fileList;

			this._menuItems = [{
				id: 'folder',
				displayName: t('files', 'New folder'),
				templateName: t('files', 'New folder'),
				iconClass: 'icon-folder',
				fileType: 'folder',
				actionHandler: function(name) {
					self.fileList.createDirectory(name);
				}
		        }];

			OC.Plugins.attach('OCA.Files.NewFileMenu', this);
		},

		template: function(data) {
			if (!OCA.Files.NewFileMenu._TEMPLATE) {
				OCA.Files.NewFileMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU);
			}
			return OCA.Files.NewFileMenu._TEMPLATE(data);
		},

		/**
		 * Event handler whenever an action has been clicked within the menu
		 *
		 * @param {Object} event event object
		 */
		_onClickAction: function(event) {
			var $target = $(event.target);
			if (!$target.hasClass('menuitem')) {
				$target = $target.closest('.menuitem');
			}
			var action = $target.attr('data-action');
			// note: clicking the upload label will automatically
			// set the focus on the "file_upload_start" hidden field
			// which itself triggers the upload dialog.
			// Currently the upload logic is still in file-upload.js and filelist.js
			if (action === 'upload') {
				OC.hideMenus();
			} else {
				event.preventDefault();
				this.$el.find('.menuitem.active').removeClass('active');
				$target.addClass('active');
				this._promptFileName($target);
			}
		},

		_promptFileName: function($target) {
			var self = this;
			if (!OCA.Files.NewFileMenu._TEMPLATE_FORM) {
				OCA.Files.NewFileMenu._TEMPLATE_FORM = Handlebars.compile(TEMPLATE_FILENAME_FORM);
			}

			if ($target.find('form').length) {
				$target.find('input[type=\'text\']').focus();
				return;
			}

			// discard other forms
			this.$el.find('form').remove();
			this.$el.find('.displayname').removeClass('hidden');

			$target.find('.displayname').addClass('hidden');

			var newName = $target.attr('data-templatename');
			var fileType = $target.attr('data-filetype');
			var $form = $(OCA.Files.NewFileMenu._TEMPLATE_FORM({
				fileName: newName,
				cid: this.cid,
				fileType: fileType
			}));

			//this.trigger('actionPerformed', action);
			$target.append($form);

			// here comes the OLD code
			var $input = $form.find('input[type=\'text\']');
			var $submit = $form.find('input[type=\'submit\']');

			var lastPos;
			var checkInput = function () {
				var filename = $input.val();
				try {
					if (!Files.isFileNameValid(filename)) {
						// Files.isFileNameValid(filename) throws an exception itself
					} else if (self.fileList.inList(filename)) {
						throw t('files', '{newName} already exists', {newName: filename}, undefined, {
							escape: false
						});
					} else {
						return true;
					}
				} catch (error) {
					$input.attr('title', error);
					$input.tooltip({placement: 'right', trigger: 'manual', 'container': '.newFileMenu'});
					$input.tooltip('fixTitle');
					$input.tooltip('show');
					$input.addClass('error');
				}
				return false;
			};

			// verify filename on typing
			$input.keyup(function() {
				if (checkInput()) {
					$input.tooltip('hide');
					$input.removeClass('error');
				}
			});

			$submit.click(function(event) {
				event.stopPropagation();
				event.preventDefault();
				$form.submit();
			});

			$input.focus();
			// pre select name up to the extension
			lastPos = newName.lastIndexOf('.');
			if (lastPos === -1) {
				lastPos = newName.length;
			}
			$input.selectRange(0, lastPos);

			$form.submit(function(event) {
				event.stopPropagation();
				event.preventDefault();

				if (checkInput()) {
					var newname = $input.val();

					/* Find the right actionHandler that should be called.
					 * Actions is retrieved by using `actionSpec.id` */
					action = _.filter(self._menuItems, function(item) {
						return item.id == $target.attr('data-action');
					}).pop();
					action.actionHandler(newname);

					$form.remove();
					$target.find('.displayname').removeClass('hidden');
					OC.hideMenus();
				}
			});
		},

		/**
		* Add a new item menu entry in the “New” file menu (in
		* last position). By clicking on the item, the
		* `actionHandler` function is called.
		*
		* @param {Object} actionSpec item’s properties
		*/
		addMenuEntry: function(actionSpec) {
			this._menuItems.push({
				id: actionSpec.id,
				displayName: actionSpec.displayName,
				templateName: actionSpec.templateName,
				iconClass: actionSpec.iconClass,
				fileType: actionSpec.fileType,
				actionHandler: actionSpec.actionHandler,
		        });
		},

		/**
		 * Renders the menu with the currently set items
		 */
		render: function() {
			this.$el.html(this.template({
				uploadMaxHumanFileSize: 'TODO',
				uploadLabel: t('files', 'Upload file'),
				items: this._menuItems
			}));
			OC.Util.scaleFixForIE8(this.$('.svg'));
		},

		/**
		 * Displays the menu under the given element
		 *
		 * @param {Object} $target target element
		 */
		showAt: function($target) {
			this.render();
			OC.showMenu(null, this.$el);
		}
	});

	OCA.Files.NewFileMenu = NewFileMenu;

})();
<|endoftext|>


<commit_before>b6b19606-973a-11e6-b514-8086f2004150<commit_msg>Does anyone even read these anymore???<commit_after>b6b2286e-973a-11e6-b514-8086f2004150<|endoftext|>


<commit_before>'use strict';

function getSpotifyMetadata(uri, serviceType) {
  return `<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
        xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
        <item id="00030020${uri}" restricted="true"><upnp:class>object.item.audioItem.musicTrack</upnp:class>
        <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON${serviceType}_X_#Svc${serviceType}-0-Token</desc></item></DIDL-Lite>`;
}

function spotify(player, values) {
  const action = values[0];
  const spotifyUri = values[1];
  const encodedSpotifyUri = encodeURIComponent(spotifyUri);
  const sid = player.system.getServiceId('Spotify');

  let uri;

  //check if current uri is either a track or a playlist/album
  if (spotifyUri.startsWith('spotify:track:')) {
    uri = `x-sonos-spotify:${encodedSpotifyUri}?sid=${sid}&flags=32&sn=1`;
  } else {
    uri = `x-rincon-cpcontainer:0006206c${encodedSpotifyUri}`;
  }

  var metadata = getSpotifyMetadata(encodedSpotifyUri, player.system.getServiceType('Spotify'));

  if (action == 'queue') {
    return player.coordinator.addURIToQueue(uri, metadata);
  } else if (action == 'now') {
    var nextTrackNo = player.coordinator.state.trackNo + 1;
    let promise = Promise.resolve();
    if (player.coordinator.avTransportUri.startsWith('x-rincon-queue') === false) {
      promise = promise.then(() => player.coordinator.setAVTransport(`x-rincon-queue:${player.coordinator.uuid}#0`));
    }

    return promise.then(() => {
      return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo)
        .then((addToQueueStatus) => player.coordinator.trackSeek(addToQueueStatus.firsttracknumberenqueued))
        .then(() => player.coordinator.play());
    });

  } else if (action == 'next') {
    var nextTrackNo = player.coordinator.state.trackNo + 1;
    return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo);
  }
}

module.exports = function (api) {
  api.registerAction('spotify', spotify);
}
<commit_msg>Update spotify/now/album|playlist to always set AVtransport to queue<commit_after>'use strict';

function getSpotifyMetadata(uri, serviceType) {
  return `<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
        xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
        <item id="00030020${uri}" restricted="true"><upnp:class>object.item.audioItem.musicTrack</upnp:class>
        <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON${serviceType}_X_#Svc${serviceType}-0-Token</desc></item></DIDL-Lite>`;
}

function spotify(player, values) {
  const action = values[0];
  const spotifyUri = values[1];
  const encodedSpotifyUri = encodeURIComponent(spotifyUri);
  const sid = player.system.getServiceId('Spotify');

  let uri;

  //check if current uri is either a track or a playlist/album
  if (spotifyUri.startsWith('spotify:track:')) {
    uri = `x-sonos-spotify:${encodedSpotifyUri}?sid=${sid}&flags=32&sn=1`;
  } else {
    uri = `x-rincon-cpcontainer:0006206c${encodedSpotifyUri}`;
  }

  var metadata = getSpotifyMetadata(encodedSpotifyUri, player.system.getServiceType('Spotify'));

  if (action == 'queue') {
    return player.coordinator.addURIToQueue(uri, metadata);
  } else if (action == 'now') {
    var nextTrackNo = player.coordinator.state.trackNo + 1;
    let promise = Promise.resolve();
    return promise.then(() => player.coordinator.setAVTransport(`x-rincon-queue:${player.coordinator.uuid}#0`))
    .then(() => player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo))
    .then((addToQueueStatus) => player.coordinator.trackSeek(addToQueueStatus.firsttracknumberenqueued))
    .then(() => player.coordinator.play());

  } else if (action == 'next') {
    var nextTrackNo = player.coordinator.state.trackNo + 1;
    return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo);
  }
}

module.exports = function (api) {
  api.registerAction('spotify', spotify);
}
<|endoftext|>


<commit_before>var StatusElement = function(statusPosition){
  this.statusPosition = statusPosition;
};

StatusElement.prototype.printStatusElement = function(event){
  var url = '/tasks/' + window.location.href.split("/").pop() + '/'
  var currentPositionInTime = this.statusPosition;
  $.ajax({
    url: url,
    type: 'get',
    dataType: 'JSON',
    data: {"position_in_time": currentPositionInTime}
  }).done(function(response) {
    $("div.task_log_container").append('

' + "Step " + currentPositionInTime + '

' + "Assigned to: " + response.assignee + '

' + "Status: " + response.current_status  + '
');
  });
};<commit_msg>Comments added to statusElement_object.js<commit_after>// StatusElement object
var StatusElement = function(statusPosition){
  this.statusPosition = statusPosition;
};

// Ajax call to get status details from database
StatusElement.prototype.printStatusElement = function(event){
  var url = '/tasks/' + window.location.href.split("/").pop() + '/' // Scrapping task id from href
  var currentPositionInTime = this.statusPosition;
  $.ajax({
    url: url,
    type: 'get',
    dataType: 'JSON',
    data: {"position_in_time": currentPositionInTime}
  }).done(function(response) {
    $("div.task_log_container").append('

' + "Step " + currentPositionInTime + '

' + "Assigned to: " + response.assignee + '

' + "Status: " + response.current_status  + '
');
  });
};<|endoftext|>


<commit_before>import GroupCtrl from '../controllers/GroupCtr';
import Auth from '../middleware/Auth';

const GroupRoutes = (router) => {
  router.route('/group')
      .post(GroupCtrl.createGroup);

  router.route('/group/:id/user')
      .post(GroupCtrl.addUserToGroup)
      .get(GroupCtrl.retrieveGroupUsers);
};

export default GroupRoutes;
<commit_msg>add middleware to secure routes<commit_after>import GroupCtrl from '../controllers/GroupCtr';
import Auth from '../middleware/Auth';

const GroupRoutes = (router) => {
  router.route('/group')
      .post(Auth.verifyToken, GroupCtrl.createGroup);

  router.route('/group/:id/user')
      .post(Auth.verifyToken, GroupCtrl.addUserToGroup)
      .get(Auth.verifyToken, GroupCtrl.retrieveGroupUsers);
};

export default GroupRoutes;
<|endoftext|>


<commit_before>/**
 * Class for Count Sorting an array
 */
class CountSort {
  constructor(data = [], compareFunc = (a, b) => a < b) {
    /** @private */
    this._unsortedList = data;
    /** @private */
    this._compareFunc = compareFunc;
    /** @private */
    this._sortedList = this._sort(data.slice());
    /** @private */
    this._length = data.length;
  }

  /**
   * Get size of array
   * @return {Number} Size of array
   * @public
   */
  get size() {
    return this._length;
  }

  /**
   * Get unsorted array
   * @return {Array} Unsorted/Initial array
   * @public
   */
  get unsortedList() {
    return this._unsortedList;
  }

  /**
   * Get sorted array
   * @return {Array} Sorted array
   * @public
   */
  get sortedList() {
    return this._sortedList;
  }

  /**
   * Count Sorts the array
   * @param  {Array} list Array to be sorted
   * @return {Array}      Sorted array
   * @private
   */
  _sort(list) {
    const len = list.length;

    const range = Math.max(...list);
    const count = new Array(range + 1);
    const output = new Array(len);
    count.fill(0);

    for (let i = 0; i < len; i += 1) {
      count[list[i]] += 1;
    }

    for (let i = 1; i <= range; i += 1) {
      count[i] += count[i - 1];
    }

    for (let i = 0; i < len; i += 1) {
      output[count[list[i]]-1] = list[i];
      count[list[i]] -= 1;
    }

    for (let i = 0; i < len; i += 1) {
      list[i] = output[i];
    }

    return list;
  }

  /**
   * Get string form of array
   * @return {String} Comma separated string array
   * @public
   */
  toString() {
    return this._sortedList.join(', ');
  }
}

module.exports = CountSort;
<commit_msg>Count Sort: Handle empty arrays<commit_after>/**
 * Class for Count Sorting an array
 */
class CountSort {
  constructor(data = [], reverse=false) {
    /** @private */
    this._unsortedList = data;
    /** @private */
    this._reverse = reverse;
    /** @private */
    this._sortedList = this._sort(data.slice());
    /** @private */
    this._length = data.length;
  }

  /**
   * Get size of array
   * @return {Number} Size of array
   * @public
   */
  get size() {
    return this._length;
  }

  /**
   * Get unsorted array
   * @return {Array} Unsorted/Initial array
   * @public
   */
  get unsortedList() {
    return this._unsortedList;
  }

  /**
   * Get sorted array
   * @return {Array} Sorted array
   * @public
   */
  get sortedList() {
    return this._sortedList;
  }

  /**
   * Count Sorts the array
   * @param  {Array} list Array to be sorted
   * @return {Array}      Sorted array
   * @private
   */
  _sort(list) {
    const len = list.length;

    const range = Math.max(...list);

    if (range === -Infinity) {
      return list;
    }

    const count = new Array(range + 1);
    const output = new Array(len);
    count.fill(0);

    for (let i = 0; i < len; i += 1) {
      count[list[i]] += 1;
    }

    for (let i = 1; i <= range; i += 1) {
      count[i] += count[i - 1];
    }

    for (let i = 0; i < len; i += 1) {
      output[count[list[i]]-1] = list[i];
      count[list[i]] -= 1;
    }

    for (let i = 0; i < len; i += 1) {
      list[i] = output[i];
    }

    return this._reverse ? list.reverse() : list;
  }

  /**
   * Get string form of array
   * @return {String} Comma separated string array
   * @public
   */
  toString() {
    return this._sortedList.join(', ');
  }
}

module.exports = CountSort;
<|endoftext|>