<commit_before>import {BottomBar, Content, SectionLayout, TopBar} from 'widget/sectionLayout'
import {Button} from 'widget/button'
import {Buttons} from 'widget/buttons'
import {Layout} from 'widget/layout'
import {PageControls, PageData, PageInfo, Pageable} from 'widget/pageable'
import {Scrollable, ScrollableContainer, Unscrollable} from 'widget/scrollable'
import {compose} from 'compose'
import {connect} from 'store'
import {forkJoin} from 'rxjs'
import {map, share, tap, zip} from 'rxjs/operators'
import {msg} from 'translate'
import Highlight from 'react-highlighter'
import Icon from 'widget/icon'
import Label from 'widget/label'
import Notifications from 'widget/notifications'
import React from 'react'
import SearchBox from 'widget/searchBox'
import UserDetails from './user'
import _ from 'lodash'
import api from 'api'
import format from 'format'
import lookStyles from 'style/look.module.css'
import moment from 'moment'
import styles from './users.module.css'

const getUserList$ = () => forkJoin(
    api.user.getUserList$(),
    api.user.getBudgetReport$()
).pipe(
    map(([users, budget]) =>
        _.map(users, user => ({
            ...user,
            report: budget[user.username || {}]
        }))
    )
)

// const getUserList$ = () => api.user.getUserList$().pipe(
//     share()
// )

// const getBudgetReport$ = () => api.user.getBudgetReport$().pipe(
//     zip(getUserList$()),
//     map(([budgetReport]) => budgetReport)
// )

class Users extends React.Component {
    state = {
        users: [],
        sortingOrder: 'updateTime',
        sortingDirection: -1,
        textFilterValues: [],
        statusFilter: null,
        userDetails: null
    }

    search = React.createRef()

    componentDidMount() {
        const setUserList = userList =>
            this.setState({
                users: this.getSortedUsers(userList)
            })

        // const mergeBudgetReport = budgetReport =>
        //     this.setState(({users}) => ({
        //         users: _.map(users, user => ({
        //             ...user,
        //             report: budgetReport[user.username || {}]
        //         }))
        //     }))

        this.props.stream('LOAD_USER_LIST',
            getUserList$(),
            userList => setUserList(userList)
        )
        // this.props.stream('LOAD_BUDGET_REPORT',
        //     getBudgetReport$(),
        //     budgetReport => mergeBudgetReport(budgetReport)
        // )
    }

    setSorting(sortingOrder) {
        this.setState(prevState => {
            const sortingDirection = sortingOrder === prevState.sortingOrder ? -prevState.sortingDirection : 1
            return {
                ...prevState,
                sortingOrder,
                sortingDirection,
                users: this.getSortedUsers(prevState.users, sortingOrder, sortingDirection)
            }
        })
    }

    getSortedUsers(users, sortingOrder = this.state.sortingOrder, sortingDirection = this.state.sortingDirection) {
        return _.orderBy(users, user => {
            const item = _.get(user, sortingOrder)
            return _.isString(item) ? item.toUpperCase() : item
        }, sortingDirection === 1 ? 'asc' : 'desc')
    }

    setTextFilter(textFilterValues) {
        this.setState({
            textFilterValues
        })
    }

    setActiveFilter(activeFilter) {
        this.setState({activeFilter})
    }

    userMatchesFilters(user) {
        return this.userMatchesTextFilter(user) && this.userMatchesStatusFilter(user)
    }

    userMatchesTextFilter(user) {
        const {textFilterValues} = this.state
        const searchMatchers = textFilterValues.map(filter => RegExp(filter, 'i'))
        const searchProperties = ['name', 'username', 'email', 'organization']
        return textFilterValues
            ? _.every(searchMatchers, matcher =>
                _.find(searchProperties, property =>
                    matcher.test(user[property])
                )
            )
            : true
    }

    userMatchesStatusFilter(user) {
        const {statusFilter} = this.state
        return statusFilter
            ? statusFilter === 'OVERBUDGET'
                ? this.isUserOverBudget(user)
                : user.status === statusFilter
            : true
    }

    isUserOverBudget({report: {budget, current}}) {
        return current.instanceSpending > budget.instanceSpending
            || current.storageSpending > budget.storageSpending
            || current.storageQuota > budget.storageQuota
    }

    onKeyDown({key}) {
        const keyMap = {
            Escape: () => {
                this.setTextFilter([])
            }
        }
        const keyAction = keyMap[key]
        keyAction && keyAction()
        this.search.current.focus()
    }

    editUser(user) {
        this.setState({
            userDetails: {
                username: user.username,
                name: user.name,
                email: user.email,
                organization: user.organization,
                monthlyBudgetInstanceSpending: user.report.budget.instanceSpending,
                monthlyBudgetStorageSpending: user.report.budget.storageSpending,
                monthlyBudgetStorageQuota: user.report.budget.storageQuota
            }
        })
    }

    inviteUser() {
        this.setState({
            userDetails: {
                newUser: true,
                monthlyBudgetInstanceSpending: 1,
                monthlyBudgetStorageSpending: 1,
                monthlyBudgetStorageQuota: 20
            }
        })
    }

    cancelUser() {
        this.setState({
            userDetails: null
        })
    }

    updateUser(userDetails) {
        const update$ = userDetails =>
            updateUserDetails$(userDetails).pipe(
                zip(updateUserBudget$(userDetails)),
                map(([userDetails, userBudget]) => ({
                    ...userDetails,
                    report: {
                        budget: userBudget
                    }
                }))
            )

        const updateUserDetails$ = ({newUser, username, name, email, organization}) =>
            newUser
                ? api.user.inviteUser$({username, name, email, organization})
                : api.user.updateUser$({username, name, email, organization})

        const updateUserBudget$ = ({
            username,
            monthlyBudgetInstanceSpending: instanceSpending,
            monthlyBudgetStorageSpending: storageSpending,
            monthlyBudgetStorageQuota: storageQuota
        }) => api.user.updateUserBudget$({
            username,
            instanceSpending,
            storageSpending,
            storageQuota
        })

        const updateLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    const index = users.findIndex(user => user.username === userDetails.username)
                    index === -1
                        ? users.push(userDetails)
                        : users[index] = userDetails
                }
                return {
                    users: this.getSortedUsers(users)
                }
            })

        const removeFromLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    _.remove(users, user => user.username === userDetails.username)
                }
                return {users}
            })

        this.cancelUser()

        updateLocalState({
            username: userDetails.username,
            name: userDetails.name,
            email: userDetails.email,
            organization: userDetails.organization,
            report: {
                budget: {
                    instanceSpending: userDetails.monthlyBudgetInstanceSpending,
                    storageSpending: userDetails.monthlyBudgetStorageSpending,
                    storageQuota: userDetails.monthlyBudgetStorageQuota
                }
            }
        })

        this.props.stream('UPDATE_USER',
            update$(userDetails),
            userDetails => {
                updateLocalState(userDetails)
                Notifications.success({message: msg('user.userDetails.update.success')})
            },
            error => {
                removeFromLocalState(userDetails)
                Notifications.error({message: msg('user.userDetails.update.error'), error})
            }
        )
    }

    renderSortingHandle(column) {
        return this.state.sortingOrder === column
            ? this.state.sortingDirection === 1
                ? <Icon name={'sort-down'}/>
                : <Icon name={'sort-up'}/>
            : <Icon name={'sort'}/>
    }

    renderColumnHeader(column, label, classNames = []) {
        const {sortingOrder} = this.state
        return (

                <Button
                    chromeless
                    shape='none'
                    additionalClassName='itemType'
                    onClick={() => this.setSorting(column)}>

                        {label}

                        {this.renderSortingHandle(column)}

                </Button>

        )
    }

    renderHeader() {
        return (

                    {this.renderInfo()}

                {this.renderColumnHeader('name', msg('user.userDetails.form.name.label'), [styles.name])}
                {this.renderColumnHeader('status', msg('user.userDetails.form.status.label'), [styles.status])}
                {this.renderColumnHeader('updateTime', msg('user.userDetails.form.updateTime.label'), [styles.updateTime])}
                {this.renderColumnHeader('report.budget.instanceSpending', msg('user.report.resources.quota'), [styles.instanceBudgetQuota, styles.number])}
                {this.renderColumnHeader('report.current.instanceSpending', msg('user.report.resources.used'), [styles.instanceBudgetUsed, styles.number])}
                {this.renderColumnHeader('report.budget.storageSpending', msg('user.report.resources.quota'), [styles.storageBudgetQuota, styles.number])}
                {this.renderColumnHeader('report.current.storageSpending', msg('user.report.resources.used'), [styles.storageBudgetUsed, styles.number])}
                {this.renderColumnHeader('report.budget.storageQuota', msg('user.report.resources.quota'), [styles.storageQuota, styles.number])}
                {this.renderColumnHeader('report.current.storageQuota', msg('user.report.resources.used'), [styles.storageUsed, styles.number])}

        )
    }

    renderTextFilter() {
        return (
            <SearchBox
                placeholder={msg('users.filter.search.placeholder')}
                onSearchValues={searchValues => this.setTextFilter(searchValues)}/>
        )
    }

    renderStatusFilter() {
        const {statusFilter} = this.state
        const options = [{
            label: msg('users.filter.status.ignore.label'),
            value: null
        }, {
            label: msg('users.filter.status.pending.label'),
            value: 'PENDING'
        }, {
            label: msg('users.filter.status.active.label'),
            value: 'ACTIVE'
        }, {
            label: msg('users.filter.budget.over.label'),
            value: 'OVERBUDGET'
        }]
        return (
            <Buttons
                chromeless
                type='horizontal-tight'
                options={options}
                selected={statusFilter}
                onChange={statusFilter => this.setState({statusFilter})}
            />
        )
    }

    renderInviteUser() {
        return (
            <Button
                additionalClassName={styles.inviteUser}
                look='add'
                size='xx-large'
                shape='circle'
                icon='plus'
                tooltip={msg('users.invite.label')}
                tooltipPlacement='left'
                onClick={() => this.inviteUser()}/>
        )
    }

    renderInfo() {
        const results = (count, start, stop) => msg('users.count', {count, start, stop})
        return (
            <PageInfo>
                {({count, start, stop}) =>

                        {results(count, start, stop)}

                }
            </PageInfo>
        )
    }

    renderUsers() {
        const {textFilterValues} = this.state
        const highlightMatcher = textFilterValues.length
            ? new RegExp(`(?:${textFilterValues.join('|')})`, 'i')
            : ''
        return (
            // [HACK] adding filter to key to force re-rendering
            <PageData itemKey={user => `${user.username || user.id}|${highlightMatcher}`}>
                {user =>
                    <User
                        user={user}
                        highlight={highlightMatcher}
                        onClick={() => this.editUser(user)}/>
                }
            </PageData>
        )
    }

    renderUserDetails() {
        const {userDetails} = this.state
        return userDetails ? (
            <UserDetails
                userDetails={userDetails}
                onCancel={() => this.cancelUser()}
                onSave={userDetails => this.updateUser(userDetails)}/>
        ) : null
    }

    render() {
        const {users} = this.state
        return (

                <Pageable
                    items={users}
                    matcher={user => this.userMatchesFilters(user)}>
                    <SectionLayout>
                        <TopBar label={msg('home.sections.users')}/>
                        <Content horizontalPadding verticalPadding menuPadding>
                            <ScrollableContainer>
                                <Unscrollable>
                                    <Layout type='horizontal' spacing='compact'>
                                        {this.renderTextFilter()}
                                        {this.renderStatusFilter()}
                                    </Layout>
                                </Unscrollable>
                                <Scrollable direction='x'>
                                    <ScrollableContainer className={styles.content}>
                                        <Unscrollable>
                                            {this.renderHeader()}
                                        </Unscrollable>
                                        <Scrollable direction='y' className={styles.users}>
                                            {this.renderUsers()}
                                        </Scrollable>
                                    </ScrollableContainer>
                                </Scrollable>
                            </ScrollableContainer>
                            {this.renderInviteUser()}
                        </Content>
                        <BottomBar className={styles.bottomBar}>
                            <PageControls/>
                        </BottomBar>
                    </SectionLayout>
                </Pageable>
                {this.renderUserDetails()}

        )
    }
}

export default compose(
    Users,
    connect()
)

class User extends React.Component {
    render() {
        const {
            user: {
                name,
                status,
                updateTime,
                report: {
                    budget = {},
                    current = {}
                } = {}
            },
            highlight,
            onClick
        } = this.props
        return (

 status ? onClick() : null}>

<Highlight search={highlight} ignoreDiacritics={true} matchClass={styles.highlight}>{name}</Highlight>

{status ? msg(`user.userDetails.form.status.${status}`) : <Icon name='spinner'/>}

{moment(updateTime).fromNow()}

{format.dollars(budget.instanceSpending)}

 budget.instanceSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.instanceSpending)}

{format.dollars(budget.storageSpending)}

 budget.storageSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.storageSpending)}

{format.fileSize(budget.storageQuota, {scale: 'G'})}

 budget.storageQuota ? styles.overBudget : null].join(' ')}>
                    {format.fileSize(current.storageQuota, {scale: 'G'})}

        )
    }
}
<commit_msg>Implemented users default sorting by column.<commit_after>import {BottomBar, Content, SectionLayout, TopBar} from 'widget/sectionLayout'
import {Button} from 'widget/button'
import {Buttons} from 'widget/buttons'
import {Layout} from 'widget/layout'
import {PageControls, PageData, PageInfo, Pageable} from 'widget/pageable'
import {Scrollable, ScrollableContainer, Unscrollable} from 'widget/scrollable'
import {compose} from 'compose'
import {connect} from 'store'
import {forkJoin} from 'rxjs'
import {map, share, tap, zip} from 'rxjs/operators'
import {msg} from 'translate'
import Highlight from 'react-highlighter'
import Icon from 'widget/icon'
import Label from 'widget/label'
import Notifications from 'widget/notifications'
import React from 'react'
import SearchBox from 'widget/searchBox'
import UserDetails from './user'
import _ from 'lodash'
import api from 'api'
import format from 'format'
import lookStyles from 'style/look.module.css'
import moment from 'moment'
import styles from './users.module.css'

const getUserList$ = () => forkJoin(
    api.user.getUserList$(),
    api.user.getBudgetReport$()
).pipe(
    map(([users, budget]) =>
        _.map(users, user => ({
            ...user,
            report: budget[user.username || {}]
        }))
    )
)

// const getUserList$ = () => api.user.getUserList$().pipe(
//     share()
// )

// const getBudgetReport$ = () => api.user.getBudgetReport$().pipe(
//     zip(getUserList$()),
//     map(([budgetReport]) => budgetReport)
// )

class Users extends React.Component {
    state = {
        users: [],
        sortingOrder: 'updateTime',
        sortingDirection: -1,
        textFilterValues: [],
        statusFilter: null,
        userDetails: null
    }

    search = React.createRef()

    componentDidMount() {
        const setUserList = userList =>
            this.setState({
                users: this.getSortedUsers(userList)
            })

        // const mergeBudgetReport = budgetReport =>
        //     this.setState(({users}) => ({
        //         users: _.map(users, user => ({
        //             ...user,
        //             report: budgetReport[user.username || {}]
        //         }))
        //     }))

        this.props.stream('LOAD_USER_LIST',
            getUserList$(),
            userList => setUserList(userList)
        )
        // this.props.stream('LOAD_BUDGET_REPORT',
        //     getBudgetReport$(),
        //     budgetReport => mergeBudgetReport(budgetReport)
        // )
    }

    setSorting(sortingOrder, defaultSorting) {
        this.setState(prevState => {
            const sortingDirection = sortingOrder === prevState.sortingOrder
                ? -prevState.sortingDirection
                : defaultSorting
            return {
                ...prevState,
                sortingOrder,
                sortingDirection,
                users: this.getSortedUsers(prevState.users, sortingOrder, sortingDirection)
            }
        })
    }

    getSortedUsers(users, sortingOrder = this.state.sortingOrder, sortingDirection = this.state.sortingDirection) {
        return _.orderBy(users, user => {
            const item = _.get(user, sortingOrder)
            return _.isString(item) ? item.toUpperCase() : item
        }, sortingDirection === 1 ? 'asc' : 'desc')
    }

    setTextFilter(textFilterValues) {
        this.setState({
            textFilterValues
        })
    }

    setActiveFilter(activeFilter) {
        this.setState({activeFilter})
    }

    userMatchesFilters(user) {
        return this.userMatchesTextFilter(user) && this.userMatchesStatusFilter(user)
    }

    userMatchesTextFilter(user) {
        const {textFilterValues} = this.state
        const searchMatchers = textFilterValues.map(filter => RegExp(filter, 'i'))
        const searchProperties = ['name', 'username', 'email', 'organization']
        return textFilterValues
            ? _.every(searchMatchers, matcher =>
                _.find(searchProperties, property =>
                    matcher.test(user[property])
                )
            )
            : true
    }

    userMatchesStatusFilter(user) {
        const {statusFilter} = this.state
        return statusFilter
            ? statusFilter === 'OVERBUDGET'
                ? this.isUserOverBudget(user)
                : user.status === statusFilter
            : true
    }

    isUserOverBudget({report: {budget, current}}) {
        return current.instanceSpending > budget.instanceSpending
            || current.storageSpending > budget.storageSpending
            || current.storageQuota > budget.storageQuota
    }

    onKeyDown({key}) {
        const keyMap = {
            Escape: () => {
                this.setTextFilter([])
            }
        }
        const keyAction = keyMap[key]
        keyAction && keyAction()
        this.search.current.focus()
    }

    editUser(user) {
        this.setState({
            userDetails: {
                username: user.username,
                name: user.name,
                email: user.email,
                organization: user.organization,
                monthlyBudgetInstanceSpending: user.report.budget.instanceSpending,
                monthlyBudgetStorageSpending: user.report.budget.storageSpending,
                monthlyBudgetStorageQuota: user.report.budget.storageQuota
            }
        })
    }

    inviteUser() {
        this.setState({
            userDetails: {
                newUser: true,
                monthlyBudgetInstanceSpending: 1,
                monthlyBudgetStorageSpending: 1,
                monthlyBudgetStorageQuota: 20
            }
        })
    }

    cancelUser() {
        this.setState({
            userDetails: null
        })
    }

    updateUser(userDetails) {
        const update$ = userDetails =>
            updateUserDetails$(userDetails).pipe(
                zip(updateUserBudget$(userDetails)),
                map(([userDetails, userBudget]) => ({
                    ...userDetails,
                    report: {
                        budget: userBudget
                    }
                }))
            )

        const updateUserDetails$ = ({newUser, username, name, email, organization}) =>
            newUser
                ? api.user.inviteUser$({username, name, email, organization})
                : api.user.updateUser$({username, name, email, organization})

        const updateUserBudget$ = ({
            username,
            monthlyBudgetInstanceSpending: instanceSpending,
            monthlyBudgetStorageSpending: storageSpending,
            monthlyBudgetStorageQuota: storageQuota
        }) => api.user.updateUserBudget$({
            username,
            instanceSpending,
            storageSpending,
            storageQuota
        })

        const updateLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    const index = users.findIndex(user => user.username === userDetails.username)
                    index === -1
                        ? users.push(userDetails)
                        : users[index] = userDetails
                }
                return {
                    users: this.getSortedUsers(users)
                }
            })

        const removeFromLocalState = userDetails =>
            this.setState(({users}) => {
                if (userDetails) {
                    _.remove(users, user => user.username === userDetails.username)
                }
                return {users}
            })

        this.cancelUser()

        updateLocalState({
            username: userDetails.username,
            name: userDetails.name,
            email: userDetails.email,
            organization: userDetails.organization,
            report: {
                budget: {
                    instanceSpending: userDetails.monthlyBudgetInstanceSpending,
                    storageSpending: userDetails.monthlyBudgetStorageSpending,
                    storageQuota: userDetails.monthlyBudgetStorageQuota
                }
            }
        })

        this.props.stream('UPDATE_USER',
            update$(userDetails),
            userDetails => {
                updateLocalState(userDetails)
                Notifications.success({message: msg('user.userDetails.update.success')})
            },
            error => {
                removeFromLocalState(userDetails)
                Notifications.error({message: msg('user.userDetails.update.error'), error})
            }
        )
    }

    renderSortingHandle(column, defaultSorting) {
        const {sortingOrder, sortingDirection} = this.state
        return sortingOrder === column
            ? sortingDirection === defaultSorting
                ? <Icon name={'sort-down'}/>
                : <Icon name={'sort-up'}/>
            : <Icon name={'sort'}/>
    }

    renderColumnHeader({column, label, defaultSorting, classNames = []}) {
        const {sortingOrder} = this.state
        return (

                <Button
                    chromeless
                    shape='none'
                    additionalClassName='itemType'
                    onClick={() => this.setSorting(column, defaultSorting)}>

                        {label}

                        {this.renderSortingHandle(column, defaultSorting)}

                </Button>

        )
    }

    renderHeader() {
        return (

                    {this.renderInfo()}

                {this.renderColumnHeader({
                    column: 'name',
                    label: msg('user.userDetails.form.name.label'),
                    defaultSorting: 1,
                    classNames: [styles.name]
                })}
                {this.renderColumnHeader({
                    column: 'status',
                    label: msg('user.userDetails.form.status.label'),
                    defaultSorting: 1,
                    classNames: [styles.status]
                })}
                {this.renderColumnHeader({
                    column: 'updateTime',
                    label: msg('user.userDetails.form.updateTime.label'),
                    defaultSorting: -1,
                    classNames: [styles.updateTime]
                })}
                {this.renderColumnHeader({
                    column: 'report.budget.instanceSpending',
                    label: msg('user.report.resources.quota'),
                    defaultSorting: -1,
                    classNames: [styles.instanceBudgetQuota, styles.number]
                })}
                {this.renderColumnHeader({
                    column: 'report.current.instanceSpending',
                    label: msg('user.report.resources.used'),
                    defaultSorting: -1,
                    classNames: [styles.instanceBudgetUsed, styles.number]
                })}
                {this.renderColumnHeader({
                    column: 'report.budget.storageSpending',
                    label: msg('user.report.resources.quota'),
                    defaultSorting: -1,
                    classNames: [styles.storageBudgetQuota, styles.number]
                })}
                {this.renderColumnHeader({
                    column: 'report.current.storageSpending',
                    label: msg('user.report.resources.used'),
                    defaultSorting: -1,
                    classNames: [styles.storageBudgetUsed, styles.number]
                })}
                {this.renderColumnHeader({
                    column: 'report.budget.storageQuota',
                    label: msg('user.report.resources.quota'),
                    defaultSorting: -1,
                    classNames: [styles.storageQuota, styles.number]
                })}
                {this.renderColumnHeader({
                    column: 'report.current.storageQuota',
                    label: msg('user.report.resources.used'),
                    defaultSorting: -1,
                    classNames: [styles.storageUsed, styles.number]
                })}

        )
    }

    renderTextFilter() {
        return (
            <SearchBox
                placeholder={msg('users.filter.search.placeholder')}
                onSearchValues={searchValues => this.setTextFilter(searchValues)}/>
        )
    }

    renderStatusFilter() {
        const {statusFilter} = this.state
        const options = [{
            label: msg('users.filter.status.ignore.label'),
            value: null
        }, {
            label: msg('users.filter.status.pending.label'),
            value: 'PENDING'
        }, {
            label: msg('users.filter.status.active.label'),
            value: 'ACTIVE'
        }, {
            label: msg('users.filter.budget.over.label'),
            value: 'OVERBUDGET'
        }]
        return (
            <Buttons
                chromeless
                type='horizontal-tight'
                options={options}
                selected={statusFilter}
                onChange={statusFilter => this.setState({statusFilter})}
            />
        )
    }

    renderInviteUser() {
        return (
            <Button
                additionalClassName={styles.inviteUser}
                look='add'
                size='xx-large'
                shape='circle'
                icon='plus'
                tooltip={msg('users.invite.label')}
                tooltipPlacement='left'
                onClick={() => this.inviteUser()}/>
        )
    }

    renderInfo() {
        const results = (count, start, stop) => msg('users.count', {count, start, stop})
        return (
            <PageInfo>
                {({count, start, stop}) =>

                        {results(count, start, stop)}

                }
            </PageInfo>
        )
    }

    renderUsers() {
        const {textFilterValues} = this.state
        const highlightMatcher = textFilterValues.length
            ? new RegExp(`(?:${textFilterValues.join('|')})`, 'i')
            : ''
        return (
            // [HACK] adding filter to key to force re-rendering
            <PageData itemKey={user => `${user.username || user.id}|${highlightMatcher}`}>
                {user =>
                    <User
                        user={user}
                        highlight={highlightMatcher}
                        onClick={() => this.editUser(user)}/>
                }
            </PageData>
        )
    }

    renderUserDetails() {
        const {userDetails} = this.state
        return userDetails ? (
            <UserDetails
                userDetails={userDetails}
                onCancel={() => this.cancelUser()}
                onSave={userDetails => this.updateUser(userDetails)}/>
        ) : null
    }

    render() {
        const {users} = this.state
        return (

                <Pageable
                    items={users}
                    matcher={user => this.userMatchesFilters(user)}>
                    <SectionLayout>
                        <TopBar label={msg('home.sections.users')}/>
                        <Content horizontalPadding verticalPadding menuPadding>
                            <ScrollableContainer>
                                <Unscrollable>
                                    <Layout type='horizontal' spacing='compact'>
                                        {this.renderTextFilter()}
                                        {this.renderStatusFilter()}
                                    </Layout>
                                </Unscrollable>
                                <Scrollable direction='x'>
                                    <ScrollableContainer className={styles.content}>
                                        <Unscrollable>
                                            {this.renderHeader()}
                                        </Unscrollable>
                                        <Scrollable direction='y' className={styles.users}>
                                            {this.renderUsers()}
                                        </Scrollable>
                                    </ScrollableContainer>
                                </Scrollable>
                            </ScrollableContainer>
                            {this.renderInviteUser()}
                        </Content>
                        <BottomBar className={styles.bottomBar}>
                            <PageControls/>
                        </BottomBar>
                    </SectionLayout>
                </Pageable>
                {this.renderUserDetails()}

        )
    }
}

export default compose(
    Users,
    connect()
)

class User extends React.Component {
    render() {
        const {
            user: {
                name,
                status,
                updateTime,
                report: {
                    budget = {},
                    current = {}
                } = {}
            },
            highlight,
            onClick
        } = this.props
        return (

 status ? onClick() : null}>

<Highlight search={highlight} ignoreDiacritics={true} matchClass={styles.highlight}>{name}</Highlight>

{status ? msg(`user.userDetails.form.status.${status}`) : <Icon name='spinner'/>}

{moment(updateTime).fromNow()}

{format.dollars(budget.instanceSpending)}

 budget.instanceSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.instanceSpending)}

{format.dollars(budget.storageSpending)}

 budget.storageSpending ? styles.overBudget : null].join(' ')}>
                    {format.dollars(current.storageSpending)}

{format.fileSize(budget.storageQuota, {scale: 'G'})}

 budget.storageQuota ? styles.overBudget : null].join(' ')}>
                    {format.fileSize(current.storageQuota, {scale: 'G'})}

        )
    }
}
<|endoftext|>


<commit_before>var WOLF_AMOUNT = 1;
var WOLF_SIZE;
var WOLF_SPEED = 75;

var WOLF_STATE_ATTACKING = 1;
var WOLF_STATE_HUNTING = 2;
var WOLF_STATE_RETREAT = 3;

var WOLF_EATING_TIME = 3000;
var KILL_DISTANCE;

var WOLF_SPAWN_DISTANCE;
var WOLF_SPAWN_TIME = 150;
var WOLF_WIMP_COOLDOWN_RESET = 2;
var WOLF_STRESS_MIN = 0;
var WOLF_STRESS_MAX = 15;
var WOLF_STRESS_THRESHOLD = 10.0;
var WOLF_STRESS_RUN_SPEED = 150;
var WOLF_STRESS_DOG_PROXIMITY_RANGE;
var WOLF_STRESS_RANGE_CONTRIB_PER_SECOND = 2.0;
var WOLF_STRESS_COOLDOWN_PER_SECOND = WOLF_STRESS_RANGE_CONTRIB_PER_SECOND * 0.15

var wolfs = [];
var wolfNextRespawn = WOLF_SPAWN_TIME;

function wolf_init()
{
    WOLF_SIZE = TILE_SIZE * 0.25;
    KILL_DISTANCE = SHEEP_SIZE * 3.0;
    WOLF_SPAWN_DISTANCE = TILE_SIZE * MAP_SIZE * 0.5;
    WOLF_STRESS_DOG_PROXIMITY_RANGE = DOG_SIZE * 4.0;

    for (var i = 0; i < WOLF_AMOUNT; i++)
    {
        wolf_spawn();
    }
}

function wolf_create(wolfPos)
{
    var wolf = {
        dir: 'e',
        position: new Vector2(wolfPos),
        state: WOLF_STATE_HUNTING,
        size: WOLF_SIZE,
        stress: WOLF_STRESS_MIN,
        target: sheeps[0],
        retreatPosition: new Vector2(0,0),
        wimperCoolDown: 0,
        renderFn: wolf_render,
        fearFactor: 10
    };

    wolf.spriteAnim = playSpriteAnim("wolf.spriteanim", "run_e");

    return wolf;
}

function wolf_render(wolf)
{
//    SpriteBatch.drawSprite(null, wolf.position, Color.BLACK, 0, 20);
    SpriteBatch.drawSpriteAnim(wolf.spriteAnim, wolf.position);
}

function wolf_spawn()
{
    var wolfTryPos = Random.randCircleEdge(MAP_CENTER, WOLF_SPAWN_DISTANCE);
    var wolf = wolf_create(wolfTryPos)
    wolfs.push(wolf);
    pushers.push(wolf);
    renderables.push(wolf);
}

function wolfs_update(dt)
{
    if (sheeps.length > 0)
    {
        wolfNextRespawn -= dt;
        if (wolfNextRespawn < 0)
        {
            wolf_spawn();
            wolfNextRespawn = WOLF_SPAWN_TIME;
        }
    }
    for (var i = 0; i < wolfs.length; ++i)
    {
        var wolf = wolfs[i];
        wolf_update(wolf, dt);
    }
}

function wolf_targetAcquisition(wolf, sheep)
{
    if (sheep.dead) return; // Shoudln't happen, but just in case.

    if (wolf.target == null || wolf.target.dead) // if the target was killed by another sheep.
    {
        wolf.target = sheep;
        return;
    }

    var distance = Vector2.distance(sheep.position, wolf.position);
    var currentTargetDistance = Vector2.distance(wolf.target.position, wolf.position);
    if (distance < currentTargetDistance)
    {
        wolf.target = sheep;
    }
}

function wolf_moveToward(wolf, targetPosition, speed, dt)
{
    var distance = Vector2.distance(targetPosition, wolf.position);
    if (distance > 0)
    {
        distance -= speed * dt;
        if (distance < 0)
        {
            distance = 0;
            wolf.position = targetPosition;
            return true;
        }
        var dir = targetPosition.sub(wolf.position).normalize();
        wolf.position = wolf.position.add(dir.mul(speed * dt));
        if (dir.x > .1) wolf.spriteAnim.play("run_e");
        else if (dir.x < -.1) wolf.spriteAnim.play("run_w");
    }
    return true;
}

function wolf_calculateStress(wolf, dt)
{
    for (var i = 0; i < dogs.length; ++i)
    {
        var dog = dogs[i];

        var distance = Vector2.distance(wolf.position, dog.position);

        // barking dogs only stress the wolves if the dog is close enough.
        if (distance <= WOLF_STRESS_DOG_PROXIMITY_RANGE)
        {
            wolf.stress = Math.min(wolf.stress + (dog.fearFactor * WOLF_STRESS_RANGE_CONTRIB_PER_SECOND * dt), WOLF_STRESS_MAX);

            // running away 1000 miles away from the dog. Wolf will most likely never reach this destination because the wolf
            // will turn around as soon as the wolf's stress level comes back to normal.
            var factor = new Vector2(TILE_SIZE);
            wolf.retreatPosition = wolf.position.add(factor.mul(wolf.position.sub(dog.position)));
        }
    }
}

function wolf_update(wolf, dt)
{
    wolf_calculateStress(wolf, dt);

    if (wolf.stress > WOLF_STRESS_THRESHOLD)
    {
        wolf.state = WOLF_STATE_RETREAT;
    }

    switch (wolf.state)
    {
        case WOLF_STATE_HUNTING:
            for (var i = 0; i < sheeps.length; i++)
            {
                wolf_targetAcquisition(wolf, sheeps[i]);
            }
            wolf_moveToward(wolf, wolf.target.position, WOLF_SPEED, dt);
            var targetDistance = +Vector2.distance(wolf.target.position, wolf.position);
            if (targetDistance <= KILL_DISTANCE)
            {
                wolf.state = WOLF_STATE_ATTACKING;
                wolf.position = wolf.target.position;
                wolf.spriteAnim.play("eat_" + wolf.dir);
                wolf.attackTime = 2;
                for (var i = 0; i < 3; ++i)
                {
                    setTimeout(function ()
                    {
                        var emitter = emitParticles(getParticleSystem("blood.pfx"), new Vector3(wolf.position.x, wolf.position.y, 0));
                        emitter.setRenderEnabled(false);
                        particles.push(emitter);
                        playSound("SFX_Dog_Growl_" + Random.randInt(1, 11) + ".wav");
                        splat_spawn(
                            Random.randVector2(
                                new Vector2(wolf.position.x - TILE_SIZE / 2, wolf.position.y - TILE_SIZE / 2),
                                new Vector2(wolf.position.x + TILE_SIZE / 2, wolf.position.y + TILE_SIZE / 2)));
                    }, 500 * (i + 1));
                }
            }
            break;
        case WOLF_STATE_ATTACKING:
            wolf.attackTime -= dt;
            if (wolf.attackTime <= 0)
            {
                sheep_kill_instantly(wolf.target);
                wolf.state = WOLF_STATE_HUNTING;
                wolf.spriteAnim.play("run_" + wolf.dir);
            }
            break;
        case WOLF_STATE_RETREAT:
            if (wolf.wimperCoolDown <= 0)
            {
                playSound("SFX_dog_wimper_" + Random.randInt(1, 2) + ".wav", 0.5);
                wolf.wimperCoolDown = WOLF_WIMP_COOLDOWN_RESET;
            }
            wolf.wimperCoolDown -= dt;
            wolf_moveToward(wolf, wolf.retreatPosition, WOLF_STRESS_RUN_SPEED, dt);
            if (wolf.stress <= WOLF_STRESS_THRESHOLD && sheeps.length > 0)
            {
                wolf.state = WOLF_STATE_HUNTING;
            }
            break;
    }

    // wolf's stress level cools down over time.
    wolf.stress = Math.max(wolf.stress-(WOLF_STRESS_COOLDOWN_PER_SECOND*dt), WOLF_STRESS_MIN);

}<commit_msg>Wolf array reset<commit_after>var WOLF_AMOUNT = 1;
var WOLF_SIZE;
var WOLF_SPEED = 75;

var WOLF_STATE_ATTACKING = 1;
var WOLF_STATE_HUNTING = 2;
var WOLF_STATE_RETREAT = 3;

var WOLF_EATING_TIME = 3000;
var KILL_DISTANCE;

var WOLF_SPAWN_DISTANCE;
var WOLF_SPAWN_TIME = 15;
var WOLF_WIMP_COOLDOWN_RESET = 2;
var WOLF_STRESS_MIN = 0;
var WOLF_STRESS_MAX = 15;
var WOLF_STRESS_THRESHOLD = 10.0;
var WOLF_STRESS_RUN_SPEED = 150;
var WOLF_STRESS_DOG_PROXIMITY_RANGE;
var WOLF_STRESS_RANGE_CONTRIB_PER_SECOND = 2.0;
var WOLF_STRESS_COOLDOWN_PER_SECOND = WOLF_STRESS_RANGE_CONTRIB_PER_SECOND * 0.15

var wolfs;
var wolfNextRespawn = WOLF_SPAWN_TIME;

function wolf_init()
{
    wolfs = [];
    WOLF_SIZE = TILE_SIZE * 0.25;
    KILL_DISTANCE = SHEEP_SIZE * 3.0;
    WOLF_SPAWN_DISTANCE = TILE_SIZE * MAP_SIZE * 0.5;
    WOLF_STRESS_DOG_PROXIMITY_RANGE = DOG_SIZE * 4.0;

    for (var i = 0; i < WOLF_AMOUNT; i++)
    {
        wolf_spawn();
    }
}

function wolf_create(wolfPos)
{
    var wolf = {
        dir: 'e',
        position: new Vector2(wolfPos),
        state: WOLF_STATE_HUNTING,
        size: WOLF_SIZE,
        stress: WOLF_STRESS_MIN,
        target: sheeps[0],
        retreatPosition: new Vector2(0,0),
        wimperCoolDown: 0,
        renderFn: wolf_render,
        fearFactor: 10
    };

    wolf.spriteAnim = playSpriteAnim("wolf.spriteanim", "run_e");

    return wolf;
}

function wolf_render(wolf)
{
//    SpriteBatch.drawSprite(null, wolf.position, Color.BLACK, 0, 20);
    SpriteBatch.drawSpriteAnim(wolf.spriteAnim, wolf.position);
}

function wolf_spawn()
{
    var wolfTryPos = Random.randCircleEdge(MAP_CENTER, WOLF_SPAWN_DISTANCE);
    var wolf = wolf_create(wolfTryPos)
    wolfs.push(wolf);
    pushers.push(wolf);
    renderables.push(wolf);
}

function wolfs_update(dt)
{
    if (sheeps.length > 0)
    {
        wolfNextRespawn -= dt;
        if (wolfNextRespawn < 0)
        {
            wolf_spawn();
            wolfNextRespawn = WOLF_SPAWN_TIME;
        }
    }
    for (var i = 0; i < wolfs.length; ++i)
    {
        var wolf = wolfs[i];
        wolf_update(wolf, dt);
    }
}

function wolf_targetAcquisition(wolf, sheep)
{
    if (sheep.dead) return; // Shoudln't happen, but just in case.

    if (wolf.target == null || wolf.target.dead) // if the target was killed by another sheep.
    {
        wolf.target = sheep;
        return;
    }

    var distance = Vector2.distance(sheep.position, wolf.position);
    var currentTargetDistance = Vector2.distance(wolf.target.position, wolf.position);
    if (distance < currentTargetDistance)
    {
        wolf.target = sheep;
    }
}

function wolf_moveToward(wolf, targetPosition, speed, dt)
{
    var distance = Vector2.distance(targetPosition, wolf.position);
    if (distance > 0)
    {
        distance -= speed * dt;
        if (distance < 0)
        {
            distance = 0;
            wolf.position = targetPosition;
            return true;
        }
        var dir = targetPosition.sub(wolf.position).normalize();
        wolf.position = wolf.position.add(dir.mul(speed * dt));
        if (dir.x > .1) wolf.spriteAnim.play("run_e");
        else if (dir.x < -.1) wolf.spriteAnim.play("run_w");
    }
    return true;
}

function wolf_calculateStress(wolf, dt)
{
    for (var i = 0; i < dogs.length; ++i)
    {
        var dog = dogs[i];

        var distance = Vector2.distance(wolf.position, dog.position);

        // barking dogs only stress the wolves if the dog is close enough.
        if (distance <= WOLF_STRESS_DOG_PROXIMITY_RANGE)
        {
            wolf.stress = Math.min(wolf.stress + (dog.fearFactor * WOLF_STRESS_RANGE_CONTRIB_PER_SECOND * dt), WOLF_STRESS_MAX);

            // running away 1000 miles away from the dog. Wolf will most likely never reach this destination because the wolf
            // will turn around as soon as the wolf's stress level comes back to normal.
            var factor = new Vector2(TILE_SIZE);
            wolf.retreatPosition = wolf.position.add(factor.mul(wolf.position.sub(dog.position)));
        }
    }
}

function wolf_update(wolf, dt)
{
    wolf_calculateStress(wolf, dt);

    if (wolf.stress > WOLF_STRESS_THRESHOLD)
    {
        wolf.state = WOLF_STATE_RETREAT;
    }

    switch (wolf.state)
    {
        case WOLF_STATE_HUNTING:
            for (var i = 0; i < sheeps.length; i++)
            {
                wolf_targetAcquisition(wolf, sheeps[i]);
            }
            wolf_moveToward(wolf, wolf.target.position, WOLF_SPEED, dt);
            var targetDistance = +Vector2.distance(wolf.target.position, wolf.position);
            if (targetDistance <= KILL_DISTANCE)
            {
                wolf.state = WOLF_STATE_ATTACKING;
                wolf.position = wolf.target.position;
                wolf.spriteAnim.play("eat_" + wolf.dir);
                wolf.attackTime = 2;
                for (var i = 0; i < 3; ++i)
                {
                    setTimeout(function ()
                    {
                        var emitter = emitParticles(getParticleSystem("blood.pfx"), new Vector3(wolf.position.x, wolf.position.y, 0));
                        emitter.setRenderEnabled(false);
                        particles.push(emitter);
                        playSound("SFX_Dog_Growl_" + Random.randInt(1, 11) + ".wav");
                        splat_spawn(
                            Random.randVector2(
                                new Vector2(wolf.position.x - TILE_SIZE / 2, wolf.position.y - TILE_SIZE / 2),
                                new Vector2(wolf.position.x + TILE_SIZE / 2, wolf.position.y + TILE_SIZE / 2)));
                    }, 500 * (i + 1));
                }
            }
            break;
        case WOLF_STATE_ATTACKING:
            wolf.attackTime -= dt;
            if (wolf.attackTime <= 0)
            {
                sheep_kill_instantly(wolf.target);
                wolf.state = WOLF_STATE_HUNTING;
                wolf.spriteAnim.play("run_" + wolf.dir);
            }
            break;
        case WOLF_STATE_RETREAT:
            if (wolf.wimperCoolDown <= 0)
            {
                playSound("SFX_dog_wimper_" + Random.randInt(1, 2) + ".wav", 0.5);
                wolf.wimperCoolDown = WOLF_WIMP_COOLDOWN_RESET;
            }
            wolf.wimperCoolDown -= dt;
            wolf_moveToward(wolf, wolf.retreatPosition, WOLF_STRESS_RUN_SPEED, dt);
            if (wolf.stress <= WOLF_STRESS_THRESHOLD && sheeps.length > 0)
            {
                wolf.state = WOLF_STATE_HUNTING;
            }
            break;
    }

    // wolf's stress level cools down over time.
    wolf.stress = Math.max(wolf.stress-(WOLF_STRESS_COOLDOWN_PER_SECOND*dt), WOLF_STRESS_MIN);

}<|endoftext|>


<commit_before>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 shortid from 'shortid';
import { selectResultsTab, assertSQLLabResultsAreEqual } from './sqllab.helper';

export default () => {
  describe('SqlLab query panel', () => {
    beforeEach(() => {
      cy.login();
      cy.server();
      cy.visit('/superset/sqllab');

      cy.route('POST', '/superset/sql_json/').as('sqlLabQuery');
    });

    it('supports entering and running a query', () => {
      // row limit has to be < ~10 for us to be able to determine how many rows
      // are fetched below (because React _Virtualized_ does not render all rows)
      const rowLimit = 3;

      cy.get('#brace-editor textarea')
        .clear({ force: true })
        .type(
        `{selectall}{backspace}SELECT ds, gender, name, num FROM main.birth_names LIMIT ${rowLimit}`,
        { force: true },
      );
      cy.get('#js-sql-toolbar button')
        .eq(0)
        .click();

      cy.wait('@sqlLabQuery');

      selectResultsTab()
        .eq(0) // ensures results tab in case preview tab exists
        .then((tableNodes) => {
          const [header, bodyWrapper] = tableNodes[0].childNodes;
          const body = bodyWrapper.childNodes[0];
          const expectedColCount = header.childNodes.length;
          const expectedRowCount = body.childNodes.length;
          expect(expectedColCount).to.equal(4);
          expect(expectedRowCount).to.equal(rowLimit);
        });
    });

    it('successfully saves a query', () => {
      cy.route('savedqueryviewapi/**').as('getSavedQuery');
      cy.route('superset/tables/**').as('getTables');

      const query = 'SELECT ds, gender, name, num FROM main.birth_names ORDER BY name LIMIT 3';
      const savedQueryTitle = `CYPRESS TEST QUERY ${shortid.generate()}`;

      // we will assert that the results of the query we save, and the saved query are the same
      let initialResultsTable = null;
      let savedQueryResultsTable = null;

      cy.get('#brace-editor textarea')
        .clear({ force: true })
        .type(`{selectall}{backspace}${query}`, { force: true })
        .focus() // focus => blur is required for updating the query that is to be saved
        .blur();

      // ctrl + r also runs query
      cy.get('#brace-editor textarea').type('{ctrl}r', { force: true });

      cy.wait('@sqlLabQuery');

      // Save results to check agains below
      selectResultsTab().then((resultsA) => {
        initialResultsTable = resultsA[0];
      });

      cy.get('#js-sql-toolbar button')
        .eq(1) // save query
        .click();

      // Enter name + save into modal
      cy.get('.modal-sm input')
        .clear({ force: true })
        .type(`{selectall}{backspace}${savedQueryTitle}`, {
        force: true,
      });

      cy.get('.modal-sm .modal-body button')
        .eq(0) // save
        .click();

      // visit saved queries
      cy.visit('/sqllab/my_queries/');

      // first row contains most recent link, follow back to SqlLab
      cy.get('table tr:first-child a[href*="savedQueryId"').click();

      // will timeout without explicitly waiting here
      cy.wait(['@getSavedQuery', '@getTables']);

      // run the saved query
      cy.get('#js-sql-toolbar button')
        .eq(0) // run query
        .click();

      cy.wait('@sqlLabQuery');

      // assert the results of the saved query match the initial results
      selectResultsTab().then((resultsB) => {
        savedQueryResultsTable = resultsB[0];

        assertSQLLabResultsAreEqual(initialResultsTable, savedQueryResultsTable);
      });
    });
  });
};
<commit_msg>chore: skip flaky SQL Lab test (#8282)<commit_after>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 shortid from 'shortid';
import { selectResultsTab, assertSQLLabResultsAreEqual } from './sqllab.helper';

export default () => {
  describe('SqlLab query panel', () => {
    beforeEach(() => {
      cy.login();
      cy.server();
      cy.visit('/superset/sqllab');

      cy.route('POST', '/superset/sql_json/').as('sqlLabQuery');
    });

    it('supports entering and running a query', () => {
      // row limit has to be < ~10 for us to be able to determine how many rows
      // are fetched below (because React _Virtualized_ does not render all rows)
      const rowLimit = 3;

      cy.get('#brace-editor textarea')
        .clear({ force: true })
        .type(
        `{selectall}{backspace}SELECT ds, gender, name, num FROM main.birth_names LIMIT ${rowLimit}`,
        { force: true },
      );
      cy.get('#js-sql-toolbar button')
        .eq(0)
        .click();

      cy.wait('@sqlLabQuery');

      selectResultsTab()
        .eq(0) // ensures results tab in case preview tab exists
        .then((tableNodes) => {
          const [header, bodyWrapper] = tableNodes[0].childNodes;
          const body = bodyWrapper.childNodes[0];
          const expectedColCount = header.childNodes.length;
          const expectedRowCount = body.childNodes.length;
          expect(expectedColCount).to.equal(4);
          expect(expectedRowCount).to.equal(rowLimit);
        });
    });

    it.skip('successfully saves a query', () => {
      cy.route('savedqueryviewapi/**').as('getSavedQuery');
      cy.route('superset/tables/**').as('getTables');

      const query = 'SELECT ds, gender, name, num FROM main.birth_names ORDER BY name LIMIT 3';
      const savedQueryTitle = `CYPRESS TEST QUERY ${shortid.generate()}`;

      // we will assert that the results of the query we save, and the saved query are the same
      let initialResultsTable = null;
      let savedQueryResultsTable = null;

      cy.get('#brace-editor textarea')
        .clear({ force: true })
        .type(`{selectall}{backspace}${query}`, { force: true })
        .focus() // focus => blur is required for updating the query that is to be saved
        .blur();

      // ctrl + r also runs query
      cy.get('#brace-editor textarea').type('{ctrl}r', { force: true });

      cy.wait('@sqlLabQuery');

      // Save results to check agains below
      selectResultsTab().then((resultsA) => {
        initialResultsTable = resultsA[0];
      });

      cy.get('#js-sql-toolbar button')
        .eq(1) // save query
        .click();

      // Enter name + save into modal
      cy.get('.modal-sm input')
        .clear({ force: true })
        .type(`{selectall}{backspace}${savedQueryTitle}`, {
        force: true,
      });

      cy.get('.modal-sm .modal-body button')
        .eq(0) // save
        .click();

      // visit saved queries
      cy.visit('/sqllab/my_queries/');

      // first row contains most recent link, follow back to SqlLab
      cy.get('table tr:first-child a[href*="savedQueryId"').click();

      // will timeout without explicitly waiting here
      cy.wait(['@getSavedQuery', '@getTables']);

      // run the saved query
      cy.get('#js-sql-toolbar button')
        .eq(0) // run query
        .click();

      cy.wait('@sqlLabQuery');

      // assert the results of the saved query match the initial results
      selectResultsTab().then((resultsB) => {
        savedQueryResultsTable = resultsB[0];

        assertSQLLabResultsAreEqual(initialResultsTable, savedQueryResultsTable);
      });
    });
  });
};
<|endoftext|>


<commit_before>/**
 * uf-form plugin.  Handles validation and submission for basic UserFrosting forms.
 *
 * This plugin uses the jQueryvalidation plugin (https://jqueryvalidation.org/) to perform instant, client-side form validation.
 * UserFrosting forms must be wrapped in a <form> element, and contain a <button type=submit> element for submission.
 *
 * Forms are then set to submit via AJAX when the submit button is clicked.
 *
 * === USAGE ===
 *
 * uf-form can be initialized on a form element as follows:
 *
 * $('#myForm').ufForm(options);
 *
 * `options` is an object containing any of the following parameters:
 * @param {JSON} validators An object containing two keys, "rules" and "messages", which specify the jQueryvalidation rules to use.
 * @param {Object} msgTarget a jQuery selector specifying the element where any error messages should be inserted.  Defaults to looking for a container with class .js-form-alerts inside this form.
 * @param {Callback} beforeSubmitCallback a callback function to execute immediately after form validation, before the form is submitted.
 * @param {bool} binaryCheckboxes specify whether to submit checkboxes as binary values 0 and 1, instead of omitting unchecked checkboxes from submission.
 *
 * == EVENTS ==
 *
 * ufForm triggers the following events:
 *
 * `submitSuccess.ufForm`: triggered when the form is successfully submitted, after re-enabling the submit button.
 * `submitError.ufForm`: triggered when the form submission (not validation) fails, after re-enabling the submit button
 * and displaying any error messages.
 *
 * UserFrosting https://www.userfrosting.com
 * @author Alexander Weissman <https://alexanderweissman.com>
 *
 * @todo Implement proper fallback for when `set` function isn't supported by FormData.
 */
;(function($, window, document, undefined) {
	'use strict';

    // Define plugin name and defaults.
    var pluginName = 'ufForm',
        defaults = {
            validators: {
                'rules'   : {},
                'messages': {}
            },
            submittingText      : "",
            beforeSubmitCallback: null,
            binaryCheckboxes    : true,     // submit checked/unchecked checkboxes as 0/1 values
            keyupDelay          : 0,
            DEBUG: false
        };

    // Constructor
    function Plugin (element, options) {
        this.element = element[0];
        this.$element = $(this.element);
        var lateDefaults = {
            encType  : (typeof this.$element.attr('enctype') !== 'undefined') ? this.$element.attr('enctype') : '',
            msgTarget: this.$element.find('.js-form-alerts:first')
        };
        this.settings = $.extend(true, {}, defaults, lateDefaults, options);
        this._defaults = $.extend(true, {}, defaults, lateDefaults);
        this._name = pluginName;
        this._debugAjax = (typeof site !== 'undefined') && site.debug.ajax;

        // Detect changes to element attributes
        this.$element.attrchange({
            callback: function (event) {
                this.element = event.target;
            }.bind(this)
        });

        // Setup validator
        this.validator = this.$element.validate({
            rules        : this.settings.validators.rules,
            messages     : this.settings.validators.messages,
            submitHandler: $.proxy(this._submitHandler, this),
            onkeyup      : $.proxy(this._onKeyUp, this)
        });

        return this;
    }

    // Functions

    /**
     * Handles the form submission after successful client-side validation.
     */
    Plugin.prototype._submitHandler = function(form, event) {
        // Execute any "before submit" callback
        if (this.settings.beforeSubmitCallback) {
            this.settings.beforeSubmitCallback();
        }

        var $form = $(form);

        // Set "loading" text for submit button, if it exists, and disable button
        var submitButton = $form.find('button[type=submit]');
        if (submitButton) {
            var submitButtonText = submitButton.html();
            submitButton.prop('disabled', true);
            submitButton.html(this.settings.submittingText);
        }

        // Get basic request parameters.
        var reqParams = {
            converters: {
                // Override jQuery's strict JSON parsing
                'text json': function(result) {
                    try {
                        // First try to use native browser parsing
                        if (typeof JSON === 'object' && typeof JSON.parse === 'function') {
                            return JSON.parse(result);
                        } else {
                            return $.parseJSON(result);
                        }
                    } catch (e) {
                       // statements to handle any exceptions
                       console.warn('Could not parse expected JSON response.');
                       return {};
                    }
                }
            },

            dataType: this._debugAjax ? 'html' : 'json',
            type: this.$element.attr('method'),
            url: this.$element.attr('action')
        };

        // Get the form encoding type from the users HTML, and chose an encoding form.
        if (this.settings.encType.toLowerCase() === 'multipart/form-data') {
            reqParams.data = this._multipartData($form);
            // add additional params to fix jquery errors
            reqParams.cache = false;
            reqParams.contentType = false;
            reqParams.processData = false;
        } else {
            reqParams.data = this._urlencodeData($form);
        }

        // Submit the form via AJAX
        $.ajax(reqParams).then(
            // Submission successful
            $.proxy(function(data, textStatus, jqXHR) {
                // Restore button text and re-enable submit button
                if (submitButton) {
                    submitButton.prop('disabled', false );
                    submitButton.html(submitButtonText);
                }

                this.$element.trigger('submitSuccess.ufForm', [data, textStatus, jqXHR]);
                return jqXHR;
            }, this),
            // Submission failed
            $.proxy(function(jqXHR, textStatus, errorThrown) {
                // Restore button text and re-enable submit button
                if (submitButton) {
                    submitButton.prop('disabled', false );
                    submitButton.html(submitButtonText);
                }
                // Error messages
                if (this._debugAjax && jqXHR.responseText) {
                    this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]);
                    document.write(jqXHR.responseText);
                    document.close();
                } else {
                    if (this.settings.DEBUG) {
                        console.log('Error (' + jqXHR.status + '): ' + jqXHR.responseText );
                    }
                    // Display errors on failure
                    // TODO: ufAlerts widget should have a 'destroy' method
                    if (!this.settings.msgTarget.data('ufAlerts')) {
                        this.settings.msgTarget.ufAlerts();
                    } else {
                        this.settings.msgTarget.ufAlerts('clear');
                    }

                    this.settings.msgTarget.ufAlerts('fetch').ufAlerts('render');
                    this.settings.msgTarget.on('render.ufAlerts', $.proxy(function () {
                        this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]);
                    }, this));
                }
                return jqXHR;
            }, this)
        );
    };

    /**
     * Helper function for encoding data as urlencoded
     */
    Plugin.prototype._urlencodeData = function(form) {
        // Serialize and post to the backend script in ajax mode
        var serializedData;
        if (this.settings.binaryCheckboxes) {
            serializedData = form.find(':input').not(':checkbox').serialize();
            // Get unchecked checkbox values, set them to 0
            form.find('input[type=checkbox]:enabled').each(function() {
                if ($(this).is(':checked')) {
                    serializedData += '&' + encodeURIComponent(this.name) + '=1';
                } else {
                    serializedData += '&' + encodeURIComponent(this.name) + '=0';
                }
            });
        }
        else {
            serializedData = form.find(':input').serialize();
        }

        return serializedData;
    };

    /**
     * Helper function for encoding data as multipart/form-data
     */
    Plugin.prototype._multipartData = function(form) {
        // Use FormData to wrap form contents.
        // https://developer.mozilla.org/en/docs/Web/API/FormData
        var formData = new FormData(form[0]);
        // Serialize and post to the backend script in ajax mode
        if (this.settings.binaryCheckboxes) {
            // Get unchecked checkbox values, set them to 0.
            var checkboxes = form.find('input[type=checkbox]:enabled');
            // Feature detection. Several browsers don't support `set`
            if (typeof formData.set !== 'function') {
                this.settings.msgTarget.ufAlerts('push', 'danger', "Your browser is missing a required feature. This form will still attempt to submit, but if it fails, you'll need to use Chrome for desktop or FireFox for desktop.");
            } else {
                checkboxes.each(function() {
                    if ($(this).is(':checked')) {
                        // this replaces checkbox value with 1 (as we're using binaryCheckboxes).
                        formData.set(this.name, 1);
                        // this explicitly adds unchecked boxes.
                    } else {
                        formData.set(this.name, 0);
                    }
                });
            }
        }

        return formData;
    };

    Plugin.prototype._onKeyUp = function(element, event) {
        var validator = this.validator;
        // See http://stackoverflow.com/questions/41363409/jquery-validate-add-delay-to-keyup-validation
        setTimeout(function() {
            // Avoid revalidate the field when pressing one of the following keys
            // Shift       => 16
            // Ctrl        => 17
            // Alt         => 18
            // Caps lock   => 20
            // End         => 35
            // Home        => 36
            // Left arrow  => 37
            // Up arrow    => 38
            // Right arrow => 39
            // Down arrow  => 40
            // Insert      => 45
            // Num lock    => 144
            // AltGr key   => 225
            var excludedKeys = [
                16, 17, 18, 20, 35, 36, 37,
                38, 39, 40, 45, 144, 225
            ];

            if ( event.which === 9 && validator.elementValue( element ) === '' || $.inArray( event.keyCode, excludedKeys ) !== -1 ) {
                return;
            } else if ( element.name in validator.submitted || element.name in validator.invalid ) {
                validator.element( element );
            }
        }, this.settings.keyupDelay);
    };

    // Handles instantiation and access to non-private methods.
    $.fn[pluginName] = function(methodOrOptions) {
        // Grab plugin instance
        var instance = $(this).data(pluginName);
        // If undefined or object, initalise plugin.
        if (methodOrOptions === undefined || typeof methodOrOptions === 'object') {
            // Only initalise if not previously done.
            if (!instance) {
                $(this).data(pluginName, new Plugin(this, methodOrOptions));
            }
            return this;
        }
        // Otherwise ensure first parameter is a valid string, and is the name of an actual function.
        else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') {
            // Ensure not a private function
            if (methodOrOptions.indexOf('_') !== 0) {
                return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1));
            }
            else {
                console.warn( 'Method ' +  methodOrOptions + ' is private!' );
            }
        }
        else {
            console.warn( 'Method ' +  methodOrOptions + ' does not exist.' );
        }
    };
})(jQuery, window, document);
<commit_msg>refactor ufForm, expose jQuery validation and ajax options<commit_after>/**
 * uf-form plugin.  Handles validation and submission for basic UserFrosting forms.
 *
 * This plugin uses the jQueryvalidation plugin (https://jqueryvalidation.org/) to perform instant, client-side form validation.
 * UserFrosting forms must be wrapped in a <form> element, and contain a <button type=submit> element for submission.
 *
 * Forms are then set to submit via AJAX when the submit button is clicked.
 *
 * === USAGE ===
 *
 * uf-form can be initialized on a form element as follows:
 *
 * $('#myForm').ufForm(options);
 *
 * `options` is an object containing any of the following parameters:
 * @param {JSON} validators An object containing two keys, "rules" and "messages", which specify the jQueryvalidation rules to use.
 * @param {Object} msgTarget a jQuery selector specifying the element where any error messages should be inserted.  Defaults to looking for a container with class .js-form-alerts inside this form.
 * @param {Callback} beforeSubmitCallback a callback function to execute immediately after form validation, before the form is submitted.
 * @param {bool} binaryCheckboxes specify whether to submit checkboxes as binary values 0 and 1, instead of omitting unchecked checkboxes from submission.
 *
 * == EVENTS ==
 *
 * ufForm triggers the following events:
 *
 * `submitSuccess.ufForm`: triggered when the form is successfully submitted, after re-enabling the submit button.
 * `submitError.ufForm`: triggered when the form submission (not validation) fails, after re-enabling the submit button
 * and displaying any error messages.
 *
 * UserFrosting https://www.userfrosting.com
 * @author Alexander Weissman <https://alexanderweissman.com>
 *
 * @todo Implement proper fallback for when `set` function isn't supported by FormData.
 */
;(function($, window, document, undefined) {
	'use strict';

    // Define plugin name and defaults.
    var pluginName = 'ufForm',
        defaults = {
            DEBUG: false,
            site                : site, // global site variables
            submittingText      : "",
            beforeSubmitCallback: null,
            binaryCheckboxes    : true,     // submit checked/unchecked checkboxes as 0/1 values
            keyupDelay          : 0,
            validator: {
                rules        : {},
                messages     : {}
            },
            // Deprecated
            validators: {
                rules        : {},
                messages     : {}
            }
        };

    // Constructor
    function Plugin (element, options) {
        this.element = element[0];
        this.$element = $(this.element);

        var lateDefaults = {
            submitButton : this.$element.find('button[type=submit]'),
            msgTarget    : this.$element.find('.js-form-alerts:first'),
            validator    : {
                submitHandler: $.proxy(this.defaultSubmitHandler, this),
                onkeyup      : $.proxy(this._onKeyUp, this)
            },
            ajax         : {
                // Override jQuery's strict JSON parsing
                converters : {
                    'text json': $.proxy(this._defaultResponseParser, this)
                },
                // Response type
                dataType   : this._debugAjax ? 'html' : 'json',
                // Disable the submit button before sending the request
                beforeSend : $.proxy(this.disableSubmit, this),
                // enable the submit button once the request completes
                complete   : $.proxy(this.enableSubmit, this)
            }
        };

        // Legacy options
        if ('validators' in options) {
            options.validator = options.validators;
        }

        this.settings = $.extend(true, {}, defaults, lateDefaults, options);

        this._defaults = $.extend(true, {}, defaults, lateDefaults);
        this._name = pluginName;
        this._debugAjax = (typeof site !== 'undefined') && site.debug.ajax;

        this.submitButtonText = this.settings.submitButton ? this.settings.submitButton.html() : '';

        // Detect changes to element attributes
        this.$element.attrchange({
            callback: function (event) {
                this.element = event.target;
            }.bind(this)
        });

        // Setup validator
        this.validator = this.$element.validate(this.settings.validator);

        return this;
    }

    // Functions

    /**
     * Set "loading" text for submit button, if it exists, and disable button
     */
    Plugin.prototype.disableSubmit = function() {
        var submitButton = this.settings.submitButton;
        // Do nothing, if the button is already disabled
        if (submitButton.prop('disabled')) {
            return this;
        }

        if (!submitButton) {
            console.error('Submit button not found.');
            return this;
        }

        this.submitButtonText = submitButton.html();
        submitButton.prop('disabled', true);
        submitButton.html(this.settings.submittingText);
        return this;
    };

    /**
     * Restore button text and re-enable submit button
     *
     * @return this
     */
    Plugin.prototype.enableSubmit = function() {
        var submitButton = this.settings.submitButton;

        // Do nothing, if the button is already enabled
        if (!submitButton.prop('disabled')) {
            return this;
        }

        if (!submitButton) {
            console.error('Submit button not found.');
            return this;
        }

        submitButton.prop('disabled', false);
        submitButton.html(this.submitButtonText);
        return this;
    };

    /**
     * Handles the form submission after successful client-side validation.
     *
     * @param {Element} form
     * @param {Event}   event
     */
    Plugin.prototype.defaultSubmitHandler = function(form, event) {
        // Execute any "before submit" callback
        if (this.settings.beforeSubmitCallback) {
            this.settings.beforeSubmitCallback();
        }

        // Get basic request parameters.
        var ajaxParams = this.prepareRequestData(form, this.settings.ajax);

        // Submit the form
        this.submitForm(ajaxParams);
    };

    /**
     * Submit the form via AJAX
     * @param   {object} reqParams
     * @returns {Deferred}
     */
    Plugin.prototype.submitForm = function(reqParams) {
        return $.ajax(reqParams).then(
            // Submission successful
            $.proxy(this._defaultSuccess, this),
            // Submission failed
            $.proxy(this._defaultError, this)
        );
    }

    /**
     * Default handler for AJAX request success.
     *
     * @param {object} data
     * @param {string} textStatus
     * @param {jqXHR}  jqXHR
     * @return {jqXHR}
     */
    Plugin.prototype._defaultSuccess = function(data, textStatus, jqXHR) {
        this.$element.trigger('submitSuccess.ufForm', [data, textStatus, jqXHR]);
        return jqXHR;
    };

    /**
     * Default handler for AJAX request fail/error.
     *
     * @param {jqXHR}  jqXHR
     * @param {string} textStatus
     * @param {string} errorThrown
     * @return {jqXHR}
     */
    Plugin.prototype._defaultError = function(jqXHR, textStatus, errorThrown) {
        // Error messages
        if (this._debugAjax && jqXHR.responseText) {
            this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]);
            document.write(jqXHR.responseText);
            document.close();
        } else {
            if (this.settings.DEBUG) {
                console.error(jqXHR.status + ': ' + jqXHR.responseText);
            }
            // Display errors on failure
            // TODO: ufAlerts widget should have a 'destroy' method
            if (!this.settings.msgTarget.data('ufAlerts')) {
                this.settings.msgTarget.ufAlerts();
            } else {
                this.settings.msgTarget.ufAlerts('clear');
            }

            this.settings.msgTarget.ufAlerts('fetch').ufAlerts('render');
            this.settings.msgTarget.on('render.ufAlerts', $.proxy(function () {
                this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]);
            }, this));
        }
        return jqXHR;
    };

    Plugin.prototype._defaultResponseParser = function(result) {
        try {
            // First try to use native browser parsing
            if (typeof JSON === 'object' && typeof JSON.parse === 'function') {
                return JSON.parse(result);
            } else {
                return $.parseJSON(result);
            }
        } catch (e) {
           // statements to handle any exceptions
           console.warn('Could not parse expected JSON response.');
           return {};
        }
    };

    /**
     * Prepares the ajax request parameters, serializing the form according to its enctype,
     * and then using the serialized data as the ajax `data` parameter.
     *
     * @param {Element} form
     * @param {object}  ajaxParams
     * @return {object}
     */
    Plugin.prototype.prepareRequestData = function(form, ajaxParams) {
        // Set ajax type and url from form method and action, if not otherwise set.
        var ajaxDefaults = {
            type        : this.$element.attr('method'),
            url         : this.$element.attr('action'),
            contentType : (typeof this.$element.attr('enctype') === 'undefined') ?
                            'application/x-www-form-urlencoded; charset=UTF-8' :
                            this.$element.attr('enctype')
        };

        ajaxParams = $.extend(true, {}, ajaxDefaults, ajaxParams);

        // Get the form encoding type from the users HTML, and chose an encoding form.
        if (ajaxParams.contentType.toLowerCase() === 'multipart/form-data') {
            ajaxParams.data = this._multipartData(form);
            // Disable caching
            ajaxParams.cache = false;
            // Prevent serialization of FormData object
            ajaxParams.processData = false;
            // Allow the browser to set the Content-Type header instead,
            // so it can include the boundary value
            ajaxParams.contentType = false;
        } else {
            ajaxParams.data = this._urlencodeData(form);
        }

        return ajaxParams;
    };

    /**
     * Helper function for encoding data as urlencoded
     *
     * @param {Element} form
     * @return {string}
     */
    Plugin.prototype._urlencodeData = function(form) {
        // Serialize and post to the backend script in ajax mode
        var serializedData;
        if (this.settings.binaryCheckboxes) {
            serializedData = $(form).find(':input').not(':checkbox').serialize();
            // Get unchecked checkbox values, set them to 0
            $(form).find('input[type=checkbox]:enabled').each(function() {
                if ($(this).is(':checked')) {
                    serializedData += '&' + encodeURIComponent(this.name) + '=1';
                } else {
                    serializedData += '&' + encodeURIComponent(this.name) + '=0';
                }
            });
        }
        else {
            serializedData = $(form).find(':input').serialize();
        }

        return serializedData;
    };

    /**
     * Helper function for encoding data as multipart/form-data
     *
     * @param {Element} form
     * @return {FormData}
     */
    Plugin.prototype._multipartData = function(form) {
        // Use FormData to wrap form contents.
        // https://developer.mozilla.org/en/docs/Web/API/FormData
        var formData = new FormData(form);
        // Serialize and post to the backend script in ajax mode
        if (this.settings.binaryCheckboxes) {
            // Get unchecked checkbox values, set them to 0.
            var checkboxes = $(form).find('input[type=checkbox]:enabled');
            // Feature detection. Several browsers don't support `set`
            if (typeof formData.set !== 'function') {
                this.settings.msgTarget.ufAlerts('push', 'danger', "Your browser is missing a required feature. This form will still attempt to submit, but if it fails, you'll need to use Chrome for desktop or FireFox for desktop.");
            } else {
                checkboxes.each(function() {
                    if ($(this).is(':checked')) {
                        // this replaces checkbox value with 1 (as we're using binaryCheckboxes).
                        formData.set(this.name, 1);
                        // this explicitly adds unchecked boxes.
                    } else {
                        formData.set(this.name, 0);
                    }
                });
            }
        }

        return formData;
    };

    /**
     * Implements a delay for revalidating the form.
     *
     * @param {Element} form
     * @param {Event}   event
     */
    Plugin.prototype._onKeyUp = function(element, event) {
        var validator = this.validator;
        // See http://stackoverflow.com/questions/41363409/jquery-validate-add-delay-to-keyup-validation
        setTimeout(function() {
            // Avoid revalidate the field when pressing one of the following keys
            // Shift       => 16
            // Ctrl        => 17
            // Alt         => 18
            // Caps lock   => 20
            // End         => 35
            // Home        => 36
            // Left arrow  => 37
            // Up arrow    => 38
            // Right arrow => 39
            // Down arrow  => 40
            // Insert      => 45
            // Num lock    => 144
            // AltGr key   => 225
            var excludedKeys = [
                16, 17, 18, 20, 35, 36, 37,
                38, 39, 40, 45, 144, 225
            ];

            if ( event.which === 9 && validator.elementValue( element ) === '' || $.inArray( event.keyCode, excludedKeys ) !== -1 ) {
                return;
            } else if ( element.name in validator.submitted || element.name in validator.invalid ) {
                validator.element( element );
            }
        }, this.settings.keyupDelay);
    };

    // Handles instantiation and access to non-private methods.
    $.fn[pluginName] = function(methodOrOptions) {
        // Grab plugin instance
        var instance = $(this).data(pluginName);
        // If undefined or object, initalise plugin.
        if (methodOrOptions === undefined || typeof methodOrOptions === 'object') {
            // Only initalise if not previously done.
            if (!instance) {
                $(this).data(pluginName, new Plugin(this, methodOrOptions));
            }
            return this;
        }
        // Otherwise ensure first parameter is a valid string, and is the name of an actual function.
        else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') {
            // Ensure not a private function
            if (methodOrOptions.indexOf('_') !== 0) {
                return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1));
            }
            else {
                console.warn( 'Method ' +  methodOrOptions + ' is private!' );
            }
        }
        else {
            console.warn( 'Method ' +  methodOrOptions + ' does not exist.' );
        }
    };
})(jQuery, window, document);
<|endoftext|>


<commit_before>Meteor.methods({
	// Add to shopping cart collection
	addCartSampleItems: function() {

		var order = {
      userId: Meteor.userId(),
			vendorName: "Missy's Backyard Pride",
      vendorEmail: 'email@vendor.sample',
			vendorUserId: '1',
			vendorBusinessId: null,
      vendorTotal: 75.10,
			products: [{
				quantity: 3,
				name: "Grandma's Heirloom Tomato",
				description: "Grandma went to great lengths to weave her heir on this loom",
        unitType: "pound",
				unitPrice: 4.20,
        photo: {src: "/images/user-images/AuntRubyTomato.png"},
				currency: 'USD',
				itemTotalPrice: 63.60,
			}, {
				quantity: 0.5,
				name: "Dad's Ghost Chilies",
				description: "Ask Dad why he is shivering",
				unitType: "pound",
				unitPrice: 3.00,
				currency: 'USD',
        photo: {src: "/images/user-images/ScarletNantesCarrot.png"},
				itemTotalPrice: 1.50,
			}, {
				quantity: 2,
				name: "Louise's Famous Strawberry Jam",
				description: "Mmmmm so sweet and fruity",
				unitType: "bottle",
				unitPrice: 5.00,
				currency: 'USD',
        photo: {src: "/images/user-images/LittleGemLettuce.png"},
				itemTotalPrice: 10.00,
			}]
		}
		// console.log('adding cart sample order:', order);
		ShoppingCart.insert(order);
  },

	// Adds an item to the shopping cart collection
	addCartItem: function(item) {

	    // TODO vendorEmail won't be exposed to the client (browser)
	    // TODO when not logged in, add session id

			// If vendor doesn't exist in collection
    	if (!Meteor.call('checkForVendor', item)) {
    		// Create the product structure
		    var product = {
				productId: item.productId,
				name: item.name,
				description: item.description,
				photo: item.photo,
				currency: item.currency,
				unitType: item.unitType,
				quantity: item.quantity,
				unitPrice: item.unitPrice,
				itemTotalPrice: item.quantity * item.unitPrice,
		    }

		    var vendorTotal = product.itemTotalPrice;

    		// Create the order structure
		    var order = {
				userId: Meteor.userId(),
				vendorUserId: item.vendorUserId,
				vendorBusinessId: item.vendorBusinessId,
				vendorName: item.vendorName,
				vendorLink: item.vendorLink,
				vendorEmail: item.vendorEmail,
				vendorTotal: vendorTotal,
				products: [ product, ],
		    }

		    // Add to collection
    		ShoppingCart.insert(order);
    	}
    	else {

    		// Get the product array
	    	var completeOrder = ShoppingCart.find({
          userId: Accounts.userId(),
	    		vendorBusinessId: item.vendorBusinessId,
					vendorUserId: item.vendorUserId,
				}, {
					fields: { products: 1, }
				}).fetch();

	    	var orderProducts = completeOrder[0].products;

	    	for (var i = 0, len = orderProducts.length; i < len; i++) {
	    		if (orderProducts[i].productId == item.productId) {

	    			// Calculate the item's total price and vendor total
	    			var itp = orderProducts[i].itemTotalPrice + (item.quantity * item.unitPrice);

	    			// Update document with new quantity
	     			ShoppingCart.update({
            userId: Accounts.userId(),
						vendorBusinessId: item.vendorBusinessId,
						vendorUserId: item.vendorUserId,
						"products.productId": item.productId,
						}, {
							$set: {
								"products.$.quantity": item.quantity,
								"products.$.itemTotalPrice": itp,
							}
						});
						break;
	    		}
	    		else {
	    			// Calculate the total price of the item
	    			var itemTotalPrice = item.quantity * item.unitPrice;

	    			// Add the product to the array
	    			ShoppingCart.update({
              userId: Accounts.userId(),
	    				vendorBusinessId: item.vendorBusinessId,
    					vendorUserId: item.vendorUserId,
    				}, {
    					$addToSet: {
    						products: {
								productId: item.productId,
								name: item.name,
								descrpition: item.description,
								photo: item.photo,
								currency: item.currency,
								unitType: item.unitType,
								quantity: item.quantity,
								unitPrice: item.unitPrice,
								itemTotalPrice: itemTotalPrice,
							}
						},
						$inc: {
							vendorTotal: itemTotalPrice,
						}
					});
					break;
	    		}
	    	}
    	}
	},

	// Increases or decreases the quantity and item's total price by 1
	// Increase: True, Decrease: False
	increaseOrDecrease: function(incOrDec, item) {
		var multiplier;
		if (incOrDec) {
			multiplier = 1;
		}
		else {
			multiplier = -1;
		}

		ShoppingCart.update({
          userId: Accounts.userId(),
          "products.productId": item.productId},
				{$inc: {
					"products.$.quantity": (1 * multiplier),
					"products.$.itemTotalPrice": (item.unitPrice * multiplier)
				}},
			);
	},

	// Checks if vendor exists in collection
	checkForVendor: function(_order) {

		if (ShoppingCart.findOne({userId: Accounts.userId(), vendorUserId: _order.vendorUserId}) ||
			ShoppingCart.findOne({userId: Accounts.userId(), vendorBusinessId: _order.vendorBusinessId})) {
			return true;
		}
		return false;
	},

	// Assigns the userId to all the carts
	assignUserId: function(user) {
		ShoppingCart.update(
			{userId: null},
			{$set: {userId: Accounts.userId()}},
		);
	},

	// Removes the item from the cart
	removeFromCart: function(item) {
		ShoppingCart.update(
			{ userId: Accounts.userId(), },
			{ $pull: { products: item }},
			{ multi: true },
			function() {
        console.log("Item removed");
        // If any products array has 0 items, remove vendor from collection
        ShoppingCart.remove({
            userId: Accounts.userId(),
            products: [],
          },
          function() { console.log("Order removed") }
        );
      }
		);

	},
});
<commit_msg>Remove logging<commit_after>Meteor.methods({
	// Add to shopping cart collection
	addCartSampleItems: function() {

		var order = {
      userId: Meteor.userId(),
			vendorName: "Missy's Backyard Pride",
      vendorEmail: 'email@vendor.sample',
			vendorUserId: '1',
			vendorBusinessId: null,
      vendorTotal: 75.10,
			products: [{
				quantity: 3,
				name: "Grandma's Heirloom Tomato",
				description: "Grandma went to great lengths to weave her heir on this loom",
        unitType: "pound",
				unitPrice: 4.20,
        photo: {src: "/images/user-images/AuntRubyTomato.png"},
				currency: 'USD',
				itemTotalPrice: 63.60,
			}, {
				quantity: 0.5,
				name: "Dad's Ghost Chilies",
				description: "Ask Dad why he is shivering",
				unitType: "pound",
				unitPrice: 3.00,
				currency: 'USD',
        photo: {src: "/images/user-images/ScarletNantesCarrot.png"},
				itemTotalPrice: 1.50,
			}, {
				quantity: 2,
				name: "Louise's Famous Strawberry Jam",
				description: "Mmmmm so sweet and fruity",
				unitType: "bottle",
				unitPrice: 5.00,
				currency: 'USD',
        photo: {src: "/images/user-images/LittleGemLettuce.png"},
				itemTotalPrice: 10.00,
			}]
		}
		// console.log('adding cart sample order:', order);
		ShoppingCart.insert(order);
  },

	// Adds an item to the shopping cart collection
	addCartItem: function(item) {

	    // TODO vendorEmail won't be exposed to the client (browser)
	    // TODO when not logged in, add session id

			// If vendor doesn't exist in collection
    	if (!Meteor.call('checkForVendor', item)) {
    		// Create the product structure
		    var product = {
				productId: item.productId,
				name: item.name,
				description: item.description,
				photo: item.photo,
				currency: item.currency,
				unitType: item.unitType,
				quantity: item.quantity,
				unitPrice: item.unitPrice,
				itemTotalPrice: item.quantity * item.unitPrice,
		    }

		    var vendorTotal = product.itemTotalPrice;

    		// Create the order structure
		    var order = {
				userId: Meteor.userId(),
				vendorUserId: item.vendorUserId,
				vendorBusinessId: item.vendorBusinessId,
				vendorName: item.vendorName,
				vendorLink: item.vendorLink,
				vendorEmail: item.vendorEmail,
				vendorTotal: vendorTotal,
				products: [ product, ],
		    }

		    // Add to collection
    		ShoppingCart.insert(order);
    	}
    	else {

    		// Get the product array
	    	var completeOrder = ShoppingCart.find({
          userId: Accounts.userId(),
	    		vendorBusinessId: item.vendorBusinessId,
					vendorUserId: item.vendorUserId,
				}, {
					fields: { products: 1, }
				}).fetch();

	    	var orderProducts = completeOrder[0].products;

	    	for (var i = 0, len = orderProducts.length; i < len; i++) {
	    		if (orderProducts[i].productId == item.productId) {

	    			// Calculate the item's total price and vendor total
	    			var itp = orderProducts[i].itemTotalPrice + (item.quantity * item.unitPrice);

	    			// Update document with new quantity
	     			ShoppingCart.update({
            userId: Accounts.userId(),
						vendorBusinessId: item.vendorBusinessId,
						vendorUserId: item.vendorUserId,
						"products.productId": item.productId,
						}, {
							$set: {
								"products.$.quantity": item.quantity,
								"products.$.itemTotalPrice": itp,
							}
						});
						break;
	    		}
	    		else {
	    			// Calculate the total price of the item
	    			var itemTotalPrice = item.quantity * item.unitPrice;

	    			// Add the product to the array
	    			ShoppingCart.update({
              userId: Accounts.userId(),
	    				vendorBusinessId: item.vendorBusinessId,
    					vendorUserId: item.vendorUserId,
    				}, {
    					$addToSet: {
    						products: {
								productId: item.productId,
								name: item.name,
								descrpition: item.description,
								photo: item.photo,
								currency: item.currency,
								unitType: item.unitType,
								quantity: item.quantity,
								unitPrice: item.unitPrice,
								itemTotalPrice: itemTotalPrice,
							}
						},
						$inc: {
							vendorTotal: itemTotalPrice,
						}
					});
					break;
	    		}
	    	}
    	}
	},

	// Increases or decreases the quantity and item's total price by 1
	// Increase: True, Decrease: False
	increaseOrDecrease: function(incOrDec, item) {
		var multiplier;
		if (incOrDec) {
			multiplier = 1;
		}
		else {
			multiplier = -1;
		}

		ShoppingCart.update({
          userId: Accounts.userId(),
          "products.productId": item.productId},
				{$inc: {
					"products.$.quantity": (1 * multiplier),
					"products.$.itemTotalPrice": (item.unitPrice * multiplier)
				}},
			);
	},

	// Checks if vendor exists in collection
	checkForVendor: function(_order) {

		if (ShoppingCart.findOne({userId: Accounts.userId(), vendorUserId: _order.vendorUserId}) ||
			ShoppingCart.findOne({userId: Accounts.userId(), vendorBusinessId: _order.vendorBusinessId})) {
			return true;
		}
		return false;
	},

	// Assigns the userId to all the carts
	assignUserId: function(user) {
		ShoppingCart.update(
			{userId: null},
			{$set: {userId: Accounts.userId()}},
		);
	},

	// Removes the item from the cart
	removeFromCart: function(item) {
		ShoppingCart.update(
			{ userId: Accounts.userId(), },
			{ $pull: { products: item }},
			{ multi: true },
			function() {
        // console.log("Item removed");
        // If any products array has 0 items, remove vendor from collection
        ShoppingCart.remove({
            userId: Accounts.userId(),
            products: [],
          },
          // function(err,count) {
            // if (count) {
              // console.log("Order removed");
            // }
          // }
        );
      }
		);

	},
});
<|endoftext|>