<commit_before>(function() {

    "use strict";

    angular.module('common.dragdrop', [])

    .factory('DragDropHandler', [function() {
        return {
            dragObject: {},
            addObject: function(object, objects, to) {
                objects.splice(to, 0, object);
            },
            updateObjects: function(objects, to, from) {
                objects.splice(to, 0, objects.splice(from, 1)[0]);
            }
        };
    }])

    .directive('draggable', ['DragDropHandler', function(DragDropHandler) {
        return {
            scope: {
                draggable: '='
            },
            link: function(scope, element, attrs){
                element.draggable({
                    connectToSortable: attrs.draggableTarget,
                    helper: "clone",
                    revert: "invalid",
                    start: function(event, ui) {
                        DragDropHandler.dragObject = scope.draggable;
                    }
                });

                element.disableSelection();
            }
        };
    }])

    .directive('droppable', ['DragDropHandler', function(DragDropHandler) {
        return {
            scope: {
                ngUpdate: '&',
                ngCreate: '&'
            },
            link: function(scope, element, attrs){
                element.sortable();
                element.disableSelection();

                element.on("sortdeactivate", function(event, ui) {
                    var from = angular.element(ui.item).scope().$index;
                    var to = element.children().index(ui.item);

                    if (to >= 0 ){
                        scope.$apply(function(){
                            if (from >= 0) {
                                scope.ngUpdate({
                                    from: from,
                                    to: to
                                });
                            } else {
                                scope.ngCreate({
                                    object: DragDropHandler.dragObject,
                                    to: to
                                });

                                ui.item.remove();
                            }
                        });
                    }
                });
            }
        };
    }])

;})();<commit_msg>clear the service dragdrop rereference<commit_after>(function() {

    "use strict";

    angular.module('common.dragdrop', [])

    .factory('DragDropHandler', [function() {
        return {
            dragObject: {},
            addObject: function(object, objects, to) {
                objects.splice(to, 0, object);
            },
            updateObjects: function(objects, to, from) {
                objects.splice(to, 0, objects.splice(from, 1)[0]);
            }
        };
    }])

    .directive('draggable', ['DragDropHandler', function(DragDropHandler) {
        return {
            scope: {
                draggable: '='
            },
            link: function(scope, element, attrs){
                element.draggable({
                    connectToSortable: attrs.draggableTarget,
                    helper: "clone",
                    revert: "invalid",
                    start: function() {
                        DragDropHandler.dragObject = scope.draggable;
                    },
                    stop: function() {
                        DragDropHandler.dragObject = undefined;
                    }
                });

                element.disableSelection();
            }
        };
    }])

    .directive('droppable', ['DragDropHandler', function(DragDropHandler) {
        return {
            scope: {
                ngUpdate: '&',
                ngCreate: '&'
            },
            link: function(scope, element, attrs){
                element.sortable();
                element.disableSelection();

                element.on("sortdeactivate", function(event, ui) {
                    var from = angular.element(ui.item).scope().$index;
                    var to = element.children().index(ui.item);

                    if (to >= 0 ){
                        scope.$apply(function(){
                            if (from >= 0) {
                                scope.ngUpdate({
                                    from: from,
                                    to: to
                                });
                            } else {
                                scope.ngCreate({
                                    object: DragDropHandler.dragObject,
                                    to: to
                                });

                                ui.item.remove();
                            }
                        });
                    }
                });
            }
        };
    }])

;})();<|endoftext|>


<commit_before>import {Form, form} from 'widget/form/form'
import {Input} from 'widget/input'
import {Layout} from 'widget/layout'
import {ModalConfirmationButton} from 'widget/modalConfirmationButton'
import {Panel} from 'widget/panel/panel'
import {UserStatus} from './userStatus'
import {compose} from 'compose'
import {msg} from 'translate'
import {requestPasswordReset$} from 'user'
import {select} from 'store'
import Confirm from 'widget/confirm'
import Notifications from 'widget/notifications'
import PropTypes from 'prop-types'
import React from 'react'
import styles from './userDetails.module.css'

const isNoMatchingUser = (id, check) => !(select('users') || []).find(user => user.id !== id && check(user))

const fields = {
    id: new Form.Field(),
    username: new Form.Field()
        .notBlank('user.userDetails.form.username.required')
        .match(/^[a-zA-Z_][a-zA-Z0-9]{0,29}$/, 'user.userDetails.form.username.format')
        .predicate((username, {id}) => isNoMatchingUser(id, user => user.username === username), 'user.userDetails.form.username.unique'),
    name: new Form.Field()
        .notBlank('user.userDetails.form.name.required'),
    email: new Form.Field()
        .notBlank('user.userDetails.form.email.required')
        .email('user.userDetails.form.email.required')
        .predicate((email, {id}) => isNoMatchingUser(id, user => user.email === email), 'user.userDetails.form.email.unique'),
    organization: new Form.Field()
        .notBlank('user.userDetails.form.organization.required'),
    intendedUse: new Form.Field(),
    // .notBlank('user.userDetails.form.intendedUse.required'),
    admin: new Form.Field(),
    instanceSpending: new Form.Field()
        .notBlank('user.userDetails.form.monthlyBudget.instanceSpending.atLeast1')
        .int('user.userDetails.form.monthlyBudget.instanceSpending.atLeast1')
        .min(1, 'user.userDetails.form.monthlyBudget.instanceSpending.atLeast1'),
    storageSpending: new Form.Field()
        .notBlank('user.userDetails.form.monthlyBudget.storageSpending.atLeast1')
        .int('user.userDetails.form.monthlyBudget.storageSpending.atLeast1')
        .min(1, 'user.userDetails.form.monthlyBudget.storageSpending.atLeast1'),
    storageQuota: new Form.Field()
        .notBlank('user.userDetails.form.monthlyBudget.storageQuota.atLeast1')
        .int('user.userDetails.form.monthlyBudget.storageQuota.atLeast1')
        .min(1, 'user.userDetails.form.monthlyBudget.storageQuota.atLeast1'),
    userRequestInstanceSpendingState: new Form.Field().notNil(),
    userRequestStorageSpendingState: new Form.Field().notNil(),
    userRequestStorageQuotaState: new Form.Field().notNil()
}

const mapStateToProps = (state, ownProps) => {
    const {userDetails} = ownProps
    const {id, newUser, username, name, email, organization, intendedUse, admin = false} = userDetails
    const {quota: {budget: {instanceSpending, storageSpending, storageQuota} = {}, budgetUpdateRequest}} = userDetails
    const userRequestInstanceSpendingState = budgetUpdateRequest ? null : false
    const userRequestStorageSpendingState = budgetUpdateRequest ? null : false
    const userRequestStorageQuotaState = budgetUpdateRequest ? null : false
    return {
        values: {
            id, newUser, username, name, email, organization, intendedUse, admin, instanceSpending, storageSpending, storageQuota,
            userRequestInstanceSpendingState, userRequestStorageSpendingState, userRequestStorageQuotaState
        }
    }
}

class UserDetails extends React.Component {
    save(userDetails) {
        const {onSave, onCancel} = this.props
        onSave({...userDetails})
        onCancel()
    }

    cancel() {
        const {onCancel} = this.props
        onCancel()
    }

    render() {
        const {form, inputs: {username, name, email, organization, intendedUse, instanceSpending, storageSpending, storageQuota, admin}} = this.props
        const newUser = !this.props.userDetails.username
        return (
            <Form.Panel
                className={styles.panel}
                form={form}
                statePath='userDetails'
                modal
                confirmation={
                    admin.isDirty() ?
                        ({confirm, cancel}) =>
                            <Confirm
                                message={msg('user.userDetails.confirmation.message', {role: msg(`user.role.${admin.value ? 'admin' : 'user'}`)})}
                                onConfirm={confirm}
                                onCancel={cancel}
                            />
                        : null
                }
                onApply={userDetails => this.save(userDetails)}
                onClose={() => this.cancel()}>
                <Panel.Header
                    icon='user'
                    title={msg('user.userDetails.title')}
                    label={this.renderHeaderButtons()}
                />
                <Panel.Content>
                    <Layout>
                        <Form.Input
                            label={msg('user.userDetails.form.username.label')}
                            input={username}
                            disabled={!newUser}
                            autoComplete={false}
                            spellCheck={false}
                            autoFocus={newUser}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.name.label')}
                            input={name}
                            autoComplete={false}
                            spellCheck={false}
                            autoFocus={!newUser}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.email.label')}
                            input={email}
                            autoComplete={false}
                            spellCheck={false}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.organization.label')}
                            input={organization}
                            autoComplete={false}
                            spellCheck={false}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.intendedUse.label')}
                            input={intendedUse}
                            spellCheck={false}
                            textArea
                            minRows={4}
                        />
                        <Form.FieldSet
                            className={styles.monthlyLimits}
                            layout='horizontal'
                            label={msg('user.userDetails.form.monthlyLimits.label')}
                            tooltip={msg('user.userDetails.form.monthlyLimits.tooltip')}
                            tooltipPlacement='topLeft'
                            errorMessage={[instanceSpending, storageSpending, storageQuota]}>
                            <Form.Input
                                label={msg('user.userDetails.form.monthlyBudget.instanceSpending.label')}
                                type='number'
                                input={instanceSpending}
                                spellCheck={false}
                                prefix='US$/mo.'
                                onChange={e => this.onChangeInstanceSpending(e.target.value)}
                            />
                            <Form.Input
                                label={msg('user.userDetails.form.monthlyBudget.storageSpending.label')}
                                type='number'
                                input={storageSpending}
                                spellCheck={false}
                                prefix='US$/mo.'
                                onChange={e => this.onChangeStorageSpending(e.target.value)}
                            />
                            <Form.Input
                                label={msg('user.userDetails.form.monthlyBudget.storageQuota.label')}
                                type='number'
                                input={storageQuota}
                                spellCheck={false}
                                prefix='GB'
                                onChange={e => this.onChangeStorageQuota(e.target.value)}
                            />
                        </Form.FieldSet>
                        {this.isUserRequest() ? this.renderUserRequest() : null}
                    </Layout>
                </Panel.Content>
                <Form.PanelButtons>
                    {this.renderLockUnlockButton()}
                    {this.renderResetPasswordButton()}
                </Form.PanelButtons>
            </Form.Panel>
        )
    }

    renderResetPasswordButton() {
        const {userDetails: {status}, inputs: {email}, form} = this.props
        return (
            <ModalConfirmationButton
                label={msg('user.userDetails.resetPassword.label')}
                icon='key'
                tooltip={msg('user.userDetails.resetPassword.tooltip')}
                message={msg('user.userDetails.resetPassword.message')}
                disabled={UserStatus.isLocked(status) || email.isInvalid() || form.isDirty()}
                onConfirm={() => this.requestPasswordReset(email.value)}
            />
        )
    }

    renderLockUnlockButton() {
        const {userDetails: {status}} = this.props
        return UserStatus.isLocked(status)
            ? this.renderUnlockButton()
            : this.renderLockButton()
    }

    renderLockButton() {
        const {form} = this.props
        return (
            <ModalConfirmationButton
                label={msg('user.userDetails.lock.label')}
                icon='lock'
                tooltip={msg('user.userDetails.lock.tooltip')}
                message={msg('user.userDetails.lock.message')}
                disabled={form.isDirty()}
                onConfirm={() => this.lock()}
            />
        )
    }

    renderUnlockButton() {
        const {inputs: {email}, form} = this.props
        return (
            <ModalConfirmationButton
                label={msg('user.userDetails.unlock.label')}
                icon='lock-open'
                tooltip={msg('user.userDetails.unlock.tooltip')}
                message={msg('user.userDetails.unlock.message')}
                disabled={email.isInvalid() || form.isDirty()}
                onConfirm={() => this.unlock()}
            />
        )
    }

    onChangeInstanceSpending(instanceSpending) {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs: {userRequestInstanceSpendingState}} = this.props
        if (budgetUpdateRequest) {
            const approved = instanceSpending >= budgetUpdateRequest.instanceSpending
            userRequestInstanceSpendingState.set(approved)
        }
    }

    onChangeStorageSpending(storageSpending) {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs: {userRequestStorageSpendingState}} = this.props
        if (budgetUpdateRequest) {
            const approved = storageSpending >= budgetUpdateRequest.storageSpending
            userRequestStorageSpendingState.set(approved)
        }
    }

    onChangeStorageQuota(storageQuota) {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs: {userRequestStorageQuotaState}} = this.props
        if (budgetUpdateRequest) {
            const approved = storageQuota >= budgetUpdateRequest.storageQuota
            userRequestStorageQuotaState.set(approved)
        }
    }

    renderHeaderButtons() {
        return (
            <Layout type='horizontal-nowrap'>
                {this.renderStatus()}
                {this.renderUserRoleButtons()}
            </Layout>
        )
    }

    renderStatus() {
        const {userDetails: {status}} = this.props
        return (
            <UserStatus status={status}/>
        )
    }

    renderUserRoleButtons() {
        const {inputs: {admin}} = this.props
        return (
            <Form.Buttons
                input={admin}
                multiple={false}
                options={[{
                    value: false,
                    label: msg('user.role.user')
                }, {
                    value: true,
                    label: msg('user.role.admin')
                }]}
            />
        )
    }

    renderUserRequest() {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs} = this.props
        const {instanceSpending, storageSpending, storageQuota, userRequestInstanceSpendingState, userRequestStorageSpendingState, userRequestStorageQuotaState} = inputs
        return (
            <Form.FieldSet
                className={styles.monthlyLimits}
                layout='vertical'
                label={msg('user.userDetails.form.budgetUpdateRequest.label')}>
                <div className={styles.message}>
                    {budgetUpdateRequest.message}
                </div>
                <Layout type='horizontal'>
                    <Layout type='vertical'>
                        <Input
                            label={msg('user.userDetails.form.monthlyBudget.instanceSpending.label')}
                            type='number'
                            value={budgetUpdateRequest.instanceSpending}
                            readOnly
                            prefix='US$/mo.'
                        />
                        {this.renderAcceptDeclineButtons(userRequestInstanceSpendingState, instanceSpending, budgetUpdateRequest.instanceSpending)}
                    </Layout>
                    <Layout type='vertical'>
                        <Input
                            label={msg('user.userDetails.form.monthlyBudget.storageSpending.label')}
                            type='number'
                            value={budgetUpdateRequest.storageSpending}
                            readOnly
                            prefix='US$/mo.'
                        />
                        {this.renderAcceptDeclineButtons(userRequestStorageSpendingState, storageSpending, budgetUpdateRequest.storageSpending)}
                    </Layout>
                    <Layout type='vertical'>
                        <Input
                            label={msg('user.userDetails.form.monthlyBudget.storageQuota.label')}
                            type='number'
                            value={budgetUpdateRequest.storageQuota}
                            readOnly
                            prefix='GB'
                        />
                        {this.renderAcceptDeclineButtons(userRequestStorageQuotaState, storageQuota, budgetUpdateRequest.storageQuota)}
                    </Layout>
                </Layout>
            </Form.FieldSet>
        )
    }

    renderAcceptDeclineButtons(input, valueInput, userRequestValue) {
        const options = [{
            look: 'add',
            icon: 'check',
            value: true
        }, {
            look: 'cancel',
            icon: 'times',
            value: false
        }]
        return (
            <Form.Buttons
                alignment='distribute'
                options={options}
                input={input}
                onChange={value => value
                    ? valueInput.set(userRequestValue)
                    : valueInput.resetValue()
                }
            />
        )
    }

    isUserRequest() {
        const {userDetails: {quota: {budgetUpdateRequest}}} = this.props
        return budgetUpdateRequest
    }

    requestPasswordReset(email) {
        this.props.stream('REQUEST_PASSWORD_RESET',
            requestPasswordReset$({email, optional: false}),
            () => Notifications.success({message: msg('landing.forgot-password.success', {email})})
        )
    }

    lock() {
        const {userDetails: {username}, onLock} = this.props
        onLock(username)
    }

    unlock() {
        const {userDetails: {username}, onUnlock} = this.props
        onUnlock(username)
    }
}

UserDetails.propTypes = {
    userDetails: PropTypes.object.isRequired,
    onCancel: PropTypes.func.isRequired,
    onLock: PropTypes.func.isRequired,
    onSave: PropTypes.func.isRequired,
    onUnlock: PropTypes.func.isRequired,
}

export default compose(
    UserDetails,
    form({fields, mapStateToProps})
)
<commit_msg>Allow 0 budget to be set by admin.<commit_after>import {Form, form} from 'widget/form/form'
import {Input} from 'widget/input'
import {Layout} from 'widget/layout'
import {ModalConfirmationButton} from 'widget/modalConfirmationButton'
import {Panel} from 'widget/panel/panel'
import {UserStatus} from './userStatus'
import {compose} from 'compose'
import {msg} from 'translate'
import {requestPasswordReset$} from 'user'
import {select} from 'store'
import Confirm from 'widget/confirm'
import Notifications from 'widget/notifications'
import PropTypes from 'prop-types'
import React from 'react'
import styles from './userDetails.module.css'

const isNoMatchingUser = (id, check) => !(select('users') || []).find(user => user.id !== id && check(user))

const fields = {
    id: new Form.Field(),
    username: new Form.Field()
        .notBlank('user.userDetails.form.username.required')
        .match(/^[a-zA-Z_][a-zA-Z0-9]{0,29}$/, 'user.userDetails.form.username.format')
        .predicate((username, {id}) => isNoMatchingUser(id, user => user.username === username), 'user.userDetails.form.username.unique'),
    name: new Form.Field()
        .notBlank('user.userDetails.form.name.required'),
    email: new Form.Field()
        .notBlank('user.userDetails.form.email.required')
        .email('user.userDetails.form.email.required')
        .predicate((email, {id}) => isNoMatchingUser(id, user => user.email === email), 'user.userDetails.form.email.unique'),
    organization: new Form.Field()
        .notBlank('user.userDetails.form.organization.required'),
    intendedUse: new Form.Field(),
    // .notBlank('user.userDetails.form.intendedUse.required'),
    admin: new Form.Field(),
    instanceSpending: new Form.Field()
        .notBlank('user.userDetails.form.monthlyBudget.instanceSpending.atLeast1')
        .int('user.userDetails.form.monthlyBudget.instanceSpending.atLeast1')
        .min(0, 'user.userDetails.form.monthlyBudget.instanceSpending.atLeast1'),
    storageSpending: new Form.Field()
        .notBlank('user.userDetails.form.monthlyBudget.storageSpending.atLeast1')
        .int('user.userDetails.form.monthlyBudget.storageSpending.atLeast1')
        .min(0, 'user.userDetails.form.monthlyBudget.storageSpending.atLeast1'),
    storageQuota: new Form.Field()
        .notBlank('user.userDetails.form.monthlyBudget.storageQuota.atLeast1')
        .int('user.userDetails.form.monthlyBudget.storageQuota.atLeast1')
        .min(0, 'user.userDetails.form.monthlyBudget.storageQuota.atLeast1'),
    userRequestInstanceSpendingState: new Form.Field().notNil(),
    userRequestStorageSpendingState: new Form.Field().notNil(),
    userRequestStorageQuotaState: new Form.Field().notNil()
}

const mapStateToProps = (state, ownProps) => {
    const {userDetails} = ownProps
    const {id, newUser, username, name, email, organization, intendedUse, admin = false} = userDetails
    const {quota: {budget: {instanceSpending, storageSpending, storageQuota} = {}, budgetUpdateRequest}} = userDetails
    const userRequestInstanceSpendingState = budgetUpdateRequest ? null : false
    const userRequestStorageSpendingState = budgetUpdateRequest ? null : false
    const userRequestStorageQuotaState = budgetUpdateRequest ? null : false
    return {
        values: {
            id, newUser, username, name, email, organization, intendedUse, admin, instanceSpending, storageSpending, storageQuota,
            userRequestInstanceSpendingState, userRequestStorageSpendingState, userRequestStorageQuotaState
        }
    }
}

class UserDetails extends React.Component {
    save(userDetails) {
        const {onSave, onCancel} = this.props
        onSave({...userDetails})
        onCancel()
    }

    cancel() {
        const {onCancel} = this.props
        onCancel()
    }

    render() {
        const {form, inputs: {username, name, email, organization, intendedUse, instanceSpending, storageSpending, storageQuota, admin}} = this.props
        const newUser = !this.props.userDetails.username
        return (
            <Form.Panel
                className={styles.panel}
                form={form}
                statePath='userDetails'
                modal
                confirmation={
                    admin.isDirty() ?
                        ({confirm, cancel}) =>
                            <Confirm
                                message={msg('user.userDetails.confirmation.message', {role: msg(`user.role.${admin.value ? 'admin' : 'user'}`)})}
                                onConfirm={confirm}
                                onCancel={cancel}
                            />
                        : null
                }
                onApply={userDetails => this.save(userDetails)}
                onClose={() => this.cancel()}>
                <Panel.Header
                    icon='user'
                    title={msg('user.userDetails.title')}
                    label={this.renderHeaderButtons()}
                />
                <Panel.Content>
                    <Layout>
                        <Form.Input
                            label={msg('user.userDetails.form.username.label')}
                            input={username}
                            disabled={!newUser}
                            autoComplete={false}
                            spellCheck={false}
                            autoFocus={newUser}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.name.label')}
                            input={name}
                            autoComplete={false}
                            spellCheck={false}
                            autoFocus={!newUser}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.email.label')}
                            input={email}
                            autoComplete={false}
                            spellCheck={false}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.organization.label')}
                            input={organization}
                            autoComplete={false}
                            spellCheck={false}
                            errorMessage
                        />
                        <Form.Input
                            label={msg('user.userDetails.form.intendedUse.label')}
                            input={intendedUse}
                            spellCheck={false}
                            textArea
                            minRows={4}
                        />
                        <Form.FieldSet
                            className={styles.monthlyLimits}
                            layout='horizontal'
                            label={msg('user.userDetails.form.monthlyLimits.label')}
                            tooltip={msg('user.userDetails.form.monthlyLimits.tooltip')}
                            tooltipPlacement='topLeft'
                            errorMessage={[instanceSpending, storageSpending, storageQuota]}>
                            <Form.Input
                                label={msg('user.userDetails.form.monthlyBudget.instanceSpending.label')}
                                type='number'
                                input={instanceSpending}
                                spellCheck={false}
                                prefix='US$/mo.'
                                onChange={e => this.onChangeInstanceSpending(e.target.value)}
                            />
                            <Form.Input
                                label={msg('user.userDetails.form.monthlyBudget.storageSpending.label')}
                                type='number'
                                input={storageSpending}
                                spellCheck={false}
                                prefix='US$/mo.'
                                onChange={e => this.onChangeStorageSpending(e.target.value)}
                            />
                            <Form.Input
                                label={msg('user.userDetails.form.monthlyBudget.storageQuota.label')}
                                type='number'
                                input={storageQuota}
                                spellCheck={false}
                                prefix='GB'
                                onChange={e => this.onChangeStorageQuota(e.target.value)}
                            />
                        </Form.FieldSet>
                        {this.isUserRequest() ? this.renderUserRequest() : null}
                    </Layout>
                </Panel.Content>
                <Form.PanelButtons>
                    {this.renderLockUnlockButton()}
                    {this.renderResetPasswordButton()}
                </Form.PanelButtons>
            </Form.Panel>
        )
    }

    renderResetPasswordButton() {
        const {userDetails: {status}, inputs: {email}, form} = this.props
        return (
            <ModalConfirmationButton
                label={msg('user.userDetails.resetPassword.label')}
                icon='key'
                tooltip={msg('user.userDetails.resetPassword.tooltip')}
                message={msg('user.userDetails.resetPassword.message')}
                disabled={UserStatus.isLocked(status) || email.isInvalid() || form.isDirty()}
                onConfirm={() => this.requestPasswordReset(email.value)}
            />
        )
    }

    renderLockUnlockButton() {
        const {userDetails: {status}} = this.props
        return UserStatus.isLocked(status)
            ? this.renderUnlockButton()
            : this.renderLockButton()
    }

    renderLockButton() {
        const {form} = this.props
        return (
            <ModalConfirmationButton
                label={msg('user.userDetails.lock.label')}
                icon='lock'
                tooltip={msg('user.userDetails.lock.tooltip')}
                message={msg('user.userDetails.lock.message')}
                disabled={form.isDirty()}
                onConfirm={() => this.lock()}
            />
        )
    }

    renderUnlockButton() {
        const {inputs: {email}, form} = this.props
        return (
            <ModalConfirmationButton
                label={msg('user.userDetails.unlock.label')}
                icon='lock-open'
                tooltip={msg('user.userDetails.unlock.tooltip')}
                message={msg('user.userDetails.unlock.message')}
                disabled={email.isInvalid() || form.isDirty()}
                onConfirm={() => this.unlock()}
            />
        )
    }

    onChangeInstanceSpending(instanceSpending) {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs: {userRequestInstanceSpendingState}} = this.props
        if (budgetUpdateRequest) {
            const approved = instanceSpending >= budgetUpdateRequest.instanceSpending
            userRequestInstanceSpendingState.set(approved)
        }
    }

    onChangeStorageSpending(storageSpending) {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs: {userRequestStorageSpendingState}} = this.props
        if (budgetUpdateRequest) {
            const approved = storageSpending >= budgetUpdateRequest.storageSpending
            userRequestStorageSpendingState.set(approved)
        }
    }

    onChangeStorageQuota(storageQuota) {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs: {userRequestStorageQuotaState}} = this.props
        if (budgetUpdateRequest) {
            const approved = storageQuota >= budgetUpdateRequest.storageQuota
            userRequestStorageQuotaState.set(approved)
        }
    }

    renderHeaderButtons() {
        return (
            <Layout type='horizontal-nowrap'>
                {this.renderStatus()}
                {this.renderUserRoleButtons()}
            </Layout>
        )
    }

    renderStatus() {
        const {userDetails: {status}} = this.props
        return (
            <UserStatus status={status}/>
        )
    }

    renderUserRoleButtons() {
        const {inputs: {admin}} = this.props
        return (
            <Form.Buttons
                input={admin}
                multiple={false}
                options={[{
                    value: false,
                    label: msg('user.role.user')
                }, {
                    value: true,
                    label: msg('user.role.admin')
                }]}
            />
        )
    }

    renderUserRequest() {
        const {userDetails: {quota: {budgetUpdateRequest}}, inputs} = this.props
        const {instanceSpending, storageSpending, storageQuota, userRequestInstanceSpendingState, userRequestStorageSpendingState, userRequestStorageQuotaState} = inputs
        return (
            <Form.FieldSet
                className={styles.monthlyLimits}
                layout='vertical'
                label={msg('user.userDetails.form.budgetUpdateRequest.label')}>
                <div className={styles.message}>
                    {budgetUpdateRequest.message}
                </div>
                <Layout type='horizontal'>
                    <Layout type='vertical'>
                        <Input
                            label={msg('user.userDetails.form.monthlyBudget.instanceSpending.label')}
                            type='number'
                            value={budgetUpdateRequest.instanceSpending}
                            readOnly
                            prefix='US$/mo.'
                        />
                        {this.renderAcceptDeclineButtons(userRequestInstanceSpendingState, instanceSpending, budgetUpdateRequest.instanceSpending)}
                    </Layout>
                    <Layout type='vertical'>
                        <Input
                            label={msg('user.userDetails.form.monthlyBudget.storageSpending.label')}
                            type='number'
                            value={budgetUpdateRequest.storageSpending}
                            readOnly
                            prefix='US$/mo.'
                        />
                        {this.renderAcceptDeclineButtons(userRequestStorageSpendingState, storageSpending, budgetUpdateRequest.storageSpending)}
                    </Layout>
                    <Layout type='vertical'>
                        <Input
                            label={msg('user.userDetails.form.monthlyBudget.storageQuota.label')}
                            type='number'
                            value={budgetUpdateRequest.storageQuota}
                            readOnly
                            prefix='GB'
                        />
                        {this.renderAcceptDeclineButtons(userRequestStorageQuotaState, storageQuota, budgetUpdateRequest.storageQuota)}
                    </Layout>
                </Layout>
            </Form.FieldSet>
        )
    }

    renderAcceptDeclineButtons(input, valueInput, userRequestValue) {
        const options = [{
            look: 'add',
            icon: 'check',
            value: true
        }, {
            look: 'cancel',
            icon: 'times',
            value: false
        }]
        return (
            <Form.Buttons
                alignment='distribute'
                options={options}
                input={input}
                onChange={value => value
                    ? valueInput.set(userRequestValue)
                    : valueInput.resetValue()
                }
            />
        )
    }

    isUserRequest() {
        const {userDetails: {quota: {budgetUpdateRequest}}} = this.props
        return budgetUpdateRequest
    }

    requestPasswordReset(email) {
        this.props.stream('REQUEST_PASSWORD_RESET',
            requestPasswordReset$({email, optional: false}),
            () => Notifications.success({message: msg('landing.forgot-password.success', {email})})
        )
    }

    lock() {
        const {userDetails: {username}, onLock} = this.props
        onLock(username)
    }

    unlock() {
        const {userDetails: {username}, onUnlock} = this.props
        onUnlock(username)
    }
}

UserDetails.propTypes = {
    userDetails: PropTypes.object.isRequired,
    onCancel: PropTypes.func.isRequired,
    onLock: PropTypes.func.isRequired,
    onSave: PropTypes.func.isRequired,
    onUnlock: PropTypes.func.isRequired,
}

export default compose(
    UserDetails,
    form({fields, mapStateToProps})
)
<|endoftext|>


<commit_before>import React, { Component } from 'react';
//import Suggest from './Suggest.js';
import Select from 'react-select';
import Slider, {Handle} from 'rc-slider';
import Tooltip from 'rc-tooltip';
const queryString = require('query-string');

import 'react-select/dist/react-select.css';
import 'rc-slider/assets/index.css';

import './App.css';

var isLoadingExternally = true;

const updateHash = (param, value) => {
  var hash = queryString.parse(location.hash);
  hash[param] = value;
  location.hash = "#" + queryString.stringify(hash);
}

const handle = (props) => {
  const { value, dragging, index, ...restProps } = props;
  return (
    <Tooltip
      prefixCls="rc-slider-tooltip"
      overlay={value}
      visible={dragging}
      placement="top"
      key={index}
    >
      <Handle {...restProps} />
    </Tooltip>
  );
};

const getSimfiles = (input) => {
  return fetch(`/api/v1/simfiles`)
    .then((response) => {
      return response.json();
    }).then((json) => {
      return { options: json };
    });
}

class SongInfo extends Component {
  renderStops() {
    return this.props.songInfo.result.stops ? (

        Stops: {this.props.songInfo.result.stops}

    ) : null;
  }
  renderSpeedChanges() {
    return this.props.songInfo.result.speed_changes.length ? (

        {
          this.props.songInfo.result.speed_changes.map(function(message) {
            return (

                {message}

            );
          })
        }

    ) : null;
  }
  render() {
    return this.props.songInfo ? (

        {this.renderStops()}
        {
          this.props.songInfo.result.bpm_list.map(function(message) {
            return

              {message}

;
          })
        }

{this.props.songInfo.result.suggestion}

        {this.renderSpeedChanges()}

    ) : null;
  }
}

class App extends Component {
  constructor() {
    super();
    var hash = queryString.parse(location.hash);
    this.state = {
      selectedSong: null,
      songInfo: null,
      preferredReadSpeed: (hash.readSpeed) ? hash.readSpeed : 573,
    };

    if (hash.song) {
      this.fetchSuggestions({'label': hash.song});
    }

    this.fetchSuggestions = this.fetchSuggestions.bind(this);
    this.onSliderChange = this.onSliderChange.bind(this);
    this.onSliderSelect = this.onSliderSelect.bind(this);
  }

  onSliderChange(value) {
    this.setState({'preferredReadSpeed': value});
  }

  onSliderSelect(value) {
    if (this.state.selectedSong) {
      this.fetchSuggestions(this.state.selectedSong);
    }
  }

  fetchSuggestions(song) {
    this.setState({'selectedSong': song});
    updateHash('song', song.label);
    updateHash('readSpeed', this.state.preferredReadSpeed);
    return fetch(`/api/v1/simfiles/` + song.label + `?style=Single&difficulty=Hard&preferred_rate=` + this.state.preferredReadSpeed + `&speed_change_threshold=4`)
      .then((response) => {
        return response.json();
      }).then(function(info) {
        this.setState({'songInfo': info});
      }.bind(this));
  }

  render() {
    return (

true BPM

            figure out the actual BPM of a chart on DDR.

          preferred read speed:
          <Slider
            min={50}
            max={800}
            defaultValue={573}
            value={this.state.preferredReadSpeed}
		    handle={handle}
            step={5}
            onChange={this.onSliderChange}
            onAfterChange={this.onSliderSelect}
          />

          <Select.Async
            name="form-field-name"
            value="one"
            loadOptions={getSimfiles}
            isLoading={isLoadingExternally}
            onChange={this.fetchSuggestions}
          />

        <SongInfo songInfo={this.state.songInfo} />

          <a href='https://github.com/zachwalton/truebpm/issues/new<Paste>'>problem?

    );
  }
}

export default App;
<commit_msg>fix bad link<commit_after>import React, { Component } from 'react';
//import Suggest from './Suggest.js';
import Select from 'react-select';
import Slider, {Handle} from 'rc-slider';
import Tooltip from 'rc-tooltip';
const queryString = require('query-string');

import 'react-select/dist/react-select.css';
import 'rc-slider/assets/index.css';

import './App.css';

var isLoadingExternally = true;

const updateHash = (param, value) => {
  var hash = queryString.parse(location.hash);
  hash[param] = value;
  location.hash = "#" + queryString.stringify(hash);
}

const handle = (props) => {
  const { value, dragging, index, ...restProps } = props;
  return (
    <Tooltip
      prefixCls="rc-slider-tooltip"
      overlay={value}
      visible={dragging}
      placement="top"
      key={index}
    >
      <Handle {...restProps} />
    </Tooltip>
  );
};

const getSimfiles = (input) => {
  return fetch(`/api/v1/simfiles`)
    .then((response) => {
      return response.json();
    }).then((json) => {
      return { options: json };
    });
}

class SongInfo extends Component {
  renderStops() {
    return this.props.songInfo.result.stops ? (

        Stops: {this.props.songInfo.result.stops}

    ) : null;
  }
  renderSpeedChanges() {
    return this.props.songInfo.result.speed_changes.length ? (

        {
          this.props.songInfo.result.speed_changes.map(function(message) {
            return (

                {message}

            );
          })
        }

    ) : null;
  }
  render() {
    return this.props.songInfo ? (

        {this.renderStops()}
        {
          this.props.songInfo.result.bpm_list.map(function(message) {
            return

              {message}

;
          })
        }

{this.props.songInfo.result.suggestion}

        {this.renderSpeedChanges()}

    ) : null;
  }
}

class App extends Component {
  constructor() {
    super();
    var hash = queryString.parse(location.hash);
    this.state = {
      selectedSong: null,
      songInfo: null,
      preferredReadSpeed: (hash.readSpeed) ? hash.readSpeed : 573,
    };

    if (hash.song) {
      this.fetchSuggestions({'label': hash.song});
    }

    this.fetchSuggestions = this.fetchSuggestions.bind(this);
    this.onSliderChange = this.onSliderChange.bind(this);
    this.onSliderSelect = this.onSliderSelect.bind(this);
  }

  onSliderChange(value) {
    this.setState({'preferredReadSpeed': value});
  }

  onSliderSelect(value) {
    if (this.state.selectedSong) {
      this.fetchSuggestions(this.state.selectedSong);
    }
  }

  fetchSuggestions(song) {
    this.setState({'selectedSong': song});
    updateHash('song', song.label);
    updateHash('readSpeed', this.state.preferredReadSpeed);
    return fetch(`/api/v1/simfiles/` + song.label + `?style=Single&difficulty=Hard&preferred_rate=` + this.state.preferredReadSpeed + `&speed_change_threshold=4`)
      .then((response) => {
        return response.json();
      }).then(function(info) {
        this.setState({'songInfo': info});
      }.bind(this));
  }

  render() {
    return (

true BPM

            figure out the actual BPM of a chart on DDR.

          preferred read speed:
          <Slider
            min={50}
            max={800}
            defaultValue={573}
            value={this.state.preferredReadSpeed}
		    handle={handle}
            step={5}
            onChange={this.onSliderChange}
            onAfterChange={this.onSliderSelect}
          />

          <Select.Async
            name="form-field-name"
            value="one"
            loadOptions={getSimfiles}
            isLoading={isLoadingExternally}
            onChange={this.fetchSuggestions}
          />

        <SongInfo songInfo={this.state.songInfo} />

          problem?

    );
  }
}

export default App;
<|endoftext|>


<commit_before>var dP = require('./_object-dp').f;
var createDesc = require('./_property-desc');
var has = require('./_has');
var FProto = Function.prototype;
var nameRE = /^\s*function ([^ (]*)/;
var NAME = 'name';

var isExtensible = Object.isExtensible || function () {
  return true;
};

// 19.2.4.2 name
NAME in FProto || require('./_descriptors') && dP(FProto, NAME, {
  configurable: true,
  get: function () {
    try {
      var that = this;
      var name = ('' + that).match(nameRE)[1];
      has(that, NAME) || !isExtensible(that) || dP(that, NAME, createDesc(5, name));
      return name;
    } catch (e) {
      return '';
    }
  }
});
<commit_msg>remove caching `Function#name`, fix #296<commit_after>var dP = require('./_object-dp').f;
var createDesc = require('./_property-desc');
var has = require('./_has');
var FProto = Function.prototype;
var nameRE = /^\s*function ([^ (]*)/;
var NAME = 'name';

var isExtensible = Object.isExtensible || function () {
  return true;
};

// 19.2.4.2 name
NAME in FProto || require('./_descriptors') && dP(FProto, NAME, {
  configurable: true,
  get: function () {
    try {
      return ('' + this).match(nameRE)[1];
    } catch (e) {
      return '';
    }
  }
});
<|endoftext|>


<commit_before>import { h, Component } 				from 'preact/preact';

import ButtonBase						from '../button-base/base';

import NavSpinner						from 'com/nav-spinner/spinner';
import NavLink 							from 'com/nav-link/link';
import ContentMore						from 'com/content-more/more';

import NotificationsBase				from 'com/content-notifications/base';
import Notification						from 'com/content-notifications/notification';

import $Notification					from '../../shrub/js/notification/notification';

export default class NotificationsFeed extends NotificationsBase {
	constructor( props ) {
		super(props);

		this.state = {
			maxReadId: 0,
			offset: 0,
			limit: 30,
			count: 0,
			notifications: null,
			notificationIds: [],
			status: null,
			highestRead: -1,
		};
		this.fetchMore = this.fetchMore.bind(this);
	}

	componentDidMount() {

		$Notification.GetFeedAll(this.state.offset, this.state.limit ).then((r) => {
			this.processNotificationFeed(r);
		});

	}

	fetchMore() {
		const offset = this.state.offset + this.state.feedSize;
		$Notification.GetFeedAll(offset, this.state.limit ).then((r) => {
			this.processNotificationFeed(r);
		});
		this.setState({offset:offset});
	}

	render( props, state ) {

		const maxReadId = state.highestRead;
		const processing = state.status === null || this.isLoading();
		const hasMore = !processing && state.offset + state.feedSize < state.count;
		//console.log(processing, state.offset, state.feedSize, state.count);
		const hasUnread = this.getHighestNotificationInFeed() > maxReadId;
		let ShowNotifications = [];
		const caller_id = state.caller_id;
		const notifications = state.notifications;
		const notificationsOrder = this.getNotificationsOrder();
		notificationsOrder.forEach((identifier) => {
			let notification = notifications.get(identifier);
			ShowNotifications.push((
				<Notification caller_id={caller_id} notification={notification} class={cN("-item -notification",(notification.notification[0].id>maxReadId)?'-new-comment':'')} id={'notification-' + identifier} />
			));
		});

		const ShowGetMore = hasMore ? (<ContentMore onclick={this.fetchMore} />) : null;

		const ShowSetAllRead = hasUnread ? (
			<ButtonBase
				class="-button -light focusable"
				id="button-mark-read"
				onclick={(e) => {this.markReadHighest();}}>
				Mark all notifications as read
			</ButtonBase>) : null;

		const ShowSpinner = processing ? <NavSpinner /> : null;

		return (

NOTIFICATIONS

				{ShowSetAllRead}
				{ShowNotifications}
				{ShowGetMore}
				{ShowSpinner}

		);
	}

}
<commit_msg>"More" in notification feed now works correctly.<commit_after>import { h, Component } 				from 'preact/preact';

import ButtonBase						from '../button-base/base';

import NavSpinner						from 'com/nav-spinner/spinner';
import NavLink 							from 'com/nav-link/link';
import ContentMore						from 'com/content-more/more';

import NotificationsBase				from 'com/content-notifications/base';
import Notification						from 'com/content-notifications/notification';

import $Notification					from '../../shrub/js/notification/notification';

export default class NotificationsFeed extends NotificationsBase {
	constructor( props ) {
		super(props);

		this.state = {
			maxReadId: 0,
			offset: 0,
			limit: 30,
			count: 0,
			notifications: null,
			notificationIds: [],
			status: null,
			highestRead: -1,
		};
		this.fetchMore = this.fetchMore.bind(this);
	}

	componentDidMount() {

		$Notification.GetFeedAll(this.state.offset, this.state.limit ).then((r) => {
			this.processNotificationFeed(r);
		});

	}

	fetchMore() {
		const offset = this.state.offset + this.state.limit;
		$Notification.GetFeedAll(offset, this.state.limit ).then((r) => {
			this.processNotificationFeed(r);
		});
		this.setState({offset:offset});
	}

	render( props, state ) {

		const maxReadId = state.highestRead;
		const processing = state.status === null || this.isLoading();
		const hasMore = !processing && ((state.offset + this.state.limit) < state.count);
		//console.log(processing, state.offset, state.feedSize, state.count);
		const hasUnread = this.getHighestNotificationInFeed() > maxReadId;
		let ShowNotifications = [];
		const caller_id = state.caller_id;
		const notifications = state.notifications;
		const notificationsOrder = this.getNotificationsOrder();
		notificationsOrder.forEach((identifier) => {
			let notification = notifications.get(identifier);
			ShowNotifications.push((
				<Notification caller_id={caller_id} notification={notification} class={cN("-item -notification",(notification.notification[0].id>maxReadId)?'-new-comment':'')} id={'notification-' + identifier} />
			));
		});

		const ShowGetMore = hasMore ? (<ContentMore onclick={this.fetchMore} />) : null;

		const ShowSetAllRead = hasUnread ? (
			<ButtonBase
				class="-button -light focusable"
				id="button-mark-read"
				onclick={(e) => {this.markReadHighest();}}>
				Mark all notifications as read
			</ButtonBase>) : null;

		const ShowSpinner = processing ? <NavSpinner /> : null;

		return (

NOTIFICATIONS

				{ShowSetAllRead}
				{ShowNotifications}
				{ShowGetMore}
				{ShowSpinner}

		);
	}

}
<|endoftext|>


<commit_before>"use strict"

var util = require('./util');
var view = require('./view');
var backendQuery = require('./backendQuery');
import Cookies from './lib/js-cookie';

export var a = Cookies;

var REWARDS = [1,100,10000]

export var courses = {}
var loadedCourseCodes = {}
var selectedOptions = {}

loadFromCookies();
view.initHandlebars(selectedOptions);
view.renderCourseList(courses)

export function clearAll() {
    if(window.confirm("Opravdu smazat vše?")) {
        courses = {}
        loadedCourseCodes = {}
        selectedOptions = {}
        saveToCookies()
    }
    view.renderCourseList();
    return false
}

export function addCourse() {
    var courseCode = document.getElementById("course_code").value
    if (loadedCourseCodes[courseCode]) {
        view.setStatusMessage("Předmět " + courseCode + " už je přidán")
        return false
    }
    backendQuery.addCourse(courseCode, function(res, err) {
        if (err) {
            view.setStatusMessage("Chyba: " + err)
        } else {
            res.forEach(addGroup)
            view.renderCourseList(courses)
            loadedCourseCodes[courseCode] = true
            saveToCookies()
            view.setStatusMessage("Přidán předmět " + courseCode)
        }
    })
    view.setStatusMessage("Hledám předmět " + courseCode)

    return false // prevents default form submission behavior (refreshing the page)
}

function addGroup(group) {
    if (group.length == 0) {
        return
    }
    var type = group[0].type
    var name = group[0].name
    var id = type + ";" + name

    if (courses[id] === undefined) {
        courses[id] = {
            id: id,
            name: name + " (" + ((type==='P') ? "přednáška" : "seminář") + ")",
            options: [],
            allowed: true,
        }
    }
    group.allowed = true
    // We remember the index so that we can get back to this group when passing a filtered version
    // of options to the solver
    group.optionId = courses[id].options.length
    courses[id].options.push(group)
}

// Called when the "sestavit rozvrh" button is pressed
export function createSchedule() {
    view.setStatusMessage("Sestavuji rozvrh")

    var queryArray =
        Object.values(courses).filter(function(c) {
            return c.allowed && c.options.some(function(o){return o.allowed})}
        ).map(function(c) {
            return {
                id: c.id,
                name: c.name,
                reward: REWARDS[(c.priority || 2) - 1],
                options: c.options.filter(function(o) {
                    return o.allowed
                })
            }
        })

    backendQuery.createSchedule(queryArray, function(res, err) {
        if (err) {
            view.setStatusMessage("Chyba: " + err)
        } else {
            var nSelected = res.filter(function(x){return x !== null}).length
            view.setStatusMessage("Rozvrh sestaven (počet předmětů: " + nSelected + ")")
            for (var course in courses) {
                delete selectedOptions[course]
            }
            for (var i = 0; i < queryArray.length; i++) {
                if (res[i] !== null) { // if null, the course is not selected
                    selectedOptions[queryArray[i].id] = queryArray[i].options[res[i]].optionId
                }
            }
            view.renderCourseList(courses)
            view.renderSchedule(getNonOverlappingGroups())
        }
    })

    return false // prevents default form submission behavior (refreshing the page)
}

export function handleCheckbox(checkboxId, courseId, index) {
    if (index === undefined) { // Checkbox for the whole course
        // If something is checked, uncheck everything, otherwise check everything
        var checkAll = !courses[courseId].options.some(function(o) {return o.allowed})

        courses[courseId].options.forEach(function(o, i) {
            document.getElementById(checkboxId + "-" + i).checked = checkAll
            courses[courseId].options[i].allowed = checkAll
        })
        document.getElementById(checkboxId).checked = checkAll
    } else {
        courses[courseId].options[index].allowed = document.getElementById(checkboxId).checked
        document.getElementById(checkboxId.substring(0, checkboxId.length - 2)).checked =
            courses[courseId].options.some(function(o) {return o.allowed})
    }
    saveToCookies()
}

export function handlePriorityChange(courseId, priority) {
    courses[courseId].priority = priority
    saveToCookies()
}

function getNonOverlappingGroups() {
    // For rendering purposes, we split the events into groups with no overlap.
    // Perhaps premature optimization, this is in case we want to allow the solver
    // to let some events overlap.
    var eventsByDay = [[],[],[],[],[]]

    for (var course in selectedOptions) {
        courses[course].options[selectedOptions[course]].forEach(function(event) {
            eventsByDay[event.day].push(event)
        })
    }

    return eventsByDay.map(function(eventsOfDay) {
        eventsOfDay.sort(function(a, b) {
            return util.timeToInt(a.time_from) - util.timeToInt(b.time_from)
        })
        var groups = []
        eventsOfDay.forEach(function(event) {
            var validGroup = null
            groups.forEach(function(group) {
                if (util.timeToInt(group[group.length - 1].time_to) <= util.timeToInt(event.time_from)) {
                    validGroup = group
                }
            })
            if (validGroup) {
                validGroup.push(event)
            } else {
                groups.push([event])
            }
        })
        return groups
    })
}

function loadFromCookies() {
    var loadedFromCookies = Cookies.getJSON("loadedCourseCodes")
    if (loadFromCookies === undefined) {
        return
    }
    loadedCourseCodes = loadedFromCookies

    var after = function() {
        var priorities = Cookies.getJSON("priorities")
        var allowed = Cookies.getJSON("allowed")
        if(priorities === undefined) return
        if(allowed === undefined) return

        var sorted = util.sortCourses(courses)
        sorted.forEach(function(c, i) {
            c.priority = priorities[i]
            c.options.forEach(function(o, j) {
                o.allowed = allowed[i][j]
            })
        })
    }

    var remain = Object.keys(loadedCourseCodes).length // A primitive Promise substitute.
    for (var code in loadedCourseCodes) {
        backendQuery.addCourse(code, function(res, err) {
            remain--
            if (!err) {
                res.forEach(addGroup)
            } else {
                console.error("Error in loading from cookies")
                console.error(err)
            }
            if (remain == 0) {
                after()
            }
            view.renderCourseList(courses)
        })
    }
}

function saveToCookies() {
    var COOKIE_DAYS = 100

    Cookies.set("loadedCourseCodes", loadedCourseCodes, { expires: COOKIE_DAYS })
    var sorted = util.sortCourses(courses)
    var priorities = sorted.map(function(c) { return c.priority || 2 })
    Cookies.set("priorities", priorities, { expires: COOKIE_DAYS })

    var allowed = sorted.map(function(c) {
        return c.options.map(function(o) { return o.allowed })
    })
    Cookies.set("allowed", allowed, { expires: COOKIE_DAYS })
}
<commit_msg>app.js: fix a typo in variable name<commit_after>"use strict"

var util = require('./util');
var view = require('./view');
var backendQuery = require('./backendQuery');
import Cookies from './lib/js-cookie';

export var a = Cookies;

var REWARDS = [1,100,10000]

export var courses = {}
var loadedCourseCodes = {}
var selectedOptions = {}

loadFromCookies();
view.initHandlebars(selectedOptions);
view.renderCourseList(courses)

export function clearAll() {
    if(window.confirm("Opravdu smazat vše?")) {
        courses = {}
        loadedCourseCodes = {}
        selectedOptions = {}
        saveToCookies()
    }
    view.renderCourseList();
    return false
}

export function addCourse() {
    var courseCode = document.getElementById("course_code").value
    if (loadedCourseCodes[courseCode]) {
        view.setStatusMessage("Předmět " + courseCode + " už je přidán")
        return false
    }
    backendQuery.addCourse(courseCode, function(res, err) {
        if (err) {
            view.setStatusMessage("Chyba: " + err)
        } else {
            res.forEach(addGroup)
            view.renderCourseList(courses)
            loadedCourseCodes[courseCode] = true
            saveToCookies()
            view.setStatusMessage("Přidán předmět " + courseCode)
        }
    })
    view.setStatusMessage("Hledám předmět " + courseCode)

    return false // prevents default form submission behavior (refreshing the page)
}

function addGroup(group) {
    if (group.length == 0) {
        return
    }
    var type = group[0].type
    var name = group[0].name
    var id = type + ";" + name

    if (courses[id] === undefined) {
        courses[id] = {
            id: id,
            name: name + " (" + ((type==='P') ? "přednáška" : "seminář") + ")",
            options: [],
            allowed: true,
        }
    }
    group.allowed = true
    // We remember the index so that we can get back to this group when passing a filtered version
    // of options to the solver
    group.optionId = courses[id].options.length
    courses[id].options.push(group)
}

// Called when the "sestavit rozvrh" button is pressed
export function createSchedule() {
    view.setStatusMessage("Sestavuji rozvrh")

    var queryArray =
        Object.values(courses).filter(function(c) {
            return c.allowed && c.options.some(function(o){return o.allowed})}
        ).map(function(c) {
            return {
                id: c.id,
                name: c.name,
                reward: REWARDS[(c.priority || 2) - 1],
                options: c.options.filter(function(o) {
                    return o.allowed
                })
            }
        })

    backendQuery.createSchedule(queryArray, function(res, err) {
        if (err) {
            view.setStatusMessage("Chyba: " + err)
        } else {
            var nSelected = res.filter(function(x){return x !== null}).length
            view.setStatusMessage("Rozvrh sestaven (počet předmětů: " + nSelected + ")")
            for (var course in courses) {
                delete selectedOptions[course]
            }
            for (var i = 0; i < queryArray.length; i++) {
                if (res[i] !== null) { // if null, the course is not selected
                    selectedOptions[queryArray[i].id] = queryArray[i].options[res[i]].optionId
                }
            }
            view.renderCourseList(courses)
            view.renderSchedule(getNonOverlappingGroups())
        }
    })

    return false // prevents default form submission behavior (refreshing the page)
}

export function handleCheckbox(checkboxId, courseId, index) {
    if (index === undefined) { // Checkbox for the whole course
        // If something is checked, uncheck everything, otherwise check everything
        var checkAll = !courses[courseId].options.some(function(o) {return o.allowed})

        courses[courseId].options.forEach(function(o, i) {
            document.getElementById(checkboxId + "-" + i).checked = checkAll
            courses[courseId].options[i].allowed = checkAll
        })
        document.getElementById(checkboxId).checked = checkAll
    } else {
        courses[courseId].options[index].allowed = document.getElementById(checkboxId).checked
        document.getElementById(checkboxId.substring(0, checkboxId.length - 2)).checked =
            courses[courseId].options.some(function(o) {return o.allowed})
    }
    saveToCookies()
}

export function handlePriorityChange(courseId, priority) {
    courses[courseId].priority = priority
    saveToCookies()
}

function getNonOverlappingGroups() {
    // For rendering purposes, we split the events into groups with no overlap.
    // Perhaps premature optimization, this is in case we want to allow the solver
    // to let some events overlap.
    var eventsByDay = [[],[],[],[],[]]

    for (var course in selectedOptions) {
        courses[course].options[selectedOptions[course]].forEach(function(event) {
            eventsByDay[event.day].push(event)
        })
    }

    return eventsByDay.map(function(eventsOfDay) {
        eventsOfDay.sort(function(a, b) {
            return util.timeToInt(a.time_from) - util.timeToInt(b.time_from)
        })
        var groups = []
        eventsOfDay.forEach(function(event) {
            var validGroup = null
            groups.forEach(function(group) {
                if (util.timeToInt(group[group.length - 1].time_to) <= util.timeToInt(event.time_from)) {
                    validGroup = group
                }
            })
            if (validGroup) {
                validGroup.push(event)
            } else {
                groups.push([event])
            }
        })
        return groups
    })
}

function loadFromCookies() {
    var loadedFromCookies = Cookies.getJSON("loadedCourseCodes")
    if (loadedFromCookies === undefined) {
        return
    }
    loadedCourseCodes = loadedFromCookies

    var after = function() {
        var priorities = Cookies.getJSON("priorities")
        var allowed = Cookies.getJSON("allowed")
        if(priorities === undefined) return
        if(allowed === undefined) return

        var sorted = util.sortCourses(courses)
        sorted.forEach(function(c, i) {
            c.priority = priorities[i]
            c.options.forEach(function(o, j) {
                o.allowed = allowed[i][j]
            })
        })
    }

    var remain = Object.keys(loadedCourseCodes).length // A primitive Promise substitute.
    for (var code in loadedCourseCodes) {
        backendQuery.addCourse(code, function(res, err) {
            remain--
            if (!err) {
                res.forEach(addGroup)
            } else {
                console.error("Error in loading from cookies")
                console.error(err)
            }
            if (remain == 0) {
                after()
            }
            view.renderCourseList(courses)
        })
    }
}

function saveToCookies() {
    var COOKIE_DAYS = 100

    Cookies.set("loadedCourseCodes", loadedCourseCodes, { expires: COOKIE_DAYS })
    var sorted = util.sortCourses(courses)
    var priorities = sorted.map(function(c) { return c.priority || 2 })
    Cookies.set("priorities", priorities, { expires: COOKIE_DAYS })

    var allowed = sorted.map(function(c) {
        return c.options.map(function(o) { return o.allowed })
    })
    Cookies.set("allowed", allowed, { expires: COOKIE_DAYS })
}
<|endoftext|>


<commit_before>/*
 * JBoss, Home of Professional Open Source
 * Copyright 2012 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @authors tag. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Date filter.
 * @author Lukas Vlcek (lvlcek@redhat.com)
 */

goog.provide('org.jboss.search.page.filter.DateFilter');

goog.require('org.jboss.search.visualization.Histogram');
goog.require('org.jboss.search.visualization.IntervalSelected');
goog.require('org.jboss.search.LookUp');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.KeyHandler');
goog.require('goog.events.KeyHandler.EventType');
goog.require('goog.array');
goog.require('goog.Disposable');

/**
 *
 * @param {!HTMLElement} element to host the date filter
 * @param {!HTMLElement} date_histogram_element to host the date histogram chart
 * @param {function(): boolean} opt_isCollapsed a function that is used to learn if filter is collapsed
 * @param {Function=} opt_expandFilter a function that is used to show/expand the filter DOM elements
 * @param {Function=} opt_collapseFilter a function that is used to hide/collapse the filter DOM elements
 * @constructor
 * @extends {goog.Disposable}
 */
org.jboss.search.page.filter.DateFilter = function(element, date_histogram_element, opt_isCollapsed, opt_expandFilter, opt_collapseFilter) {
    goog.Disposable.call(this);

    /**
     * @type {HTMLElement}
     * @private
     */
    this.element_ = element;

    /**
     * @type {HTMLElement}
     * @private
     */
    this.date_histogram_element_ = date_histogram_element;

    /**
     * @type {!Function}
     * @private
     */
    this.expandFilter_ = /** @type {!Function} */ (goog.isFunction(opt_expandFilter) ? opt_expandFilter : goog.nullFunction());

    /**
     * @type {!Function}
     * @private
     */
    this.collpaseFilter_ = /** @type {!Function} */ (goog.isFunction(opt_collapseFilter) ? opt_collapseFilter : goog.nullFunction());

    /**
     * @type {function(): boolean}
     * @private
     */
    this.isCollapsed_ = /** @type {function(): boolean} */ (goog.isFunction(opt_isCollapsed) ? opt_isCollapsed : function(){ return true });

    /** @private */
    this.keyHandler_ = new goog.events.KeyHandler(goog.dom.getDocument());
    // We can not use this.element_ but document - because <DIV> may not be focused.
    // this.keyHandler_ = new goog.events.KeyHandler(this.element_);

    // listen for key strokes
    this.keyListenerId_ = goog.events.listen(this.keyHandler_,
        goog.events.KeyHandler.EventType.KEY,
        goog.bind(function(e) {
            var keyEvent = /** @type {goog.events.KeyEvent} */ (e);
            if (!keyEvent.repeat) {
                if (!this.isCollapsed_()) {
                    if (keyEvent.keyCode == goog.events.KeyCodes.ESC) {
//                        keyEvent.preventDefault();
                        this.collapseFilter();
                    }
                }
            }
        }, this)
    );

    /**
     * @type {org.jboss.search.visualization.Histogram}
     * @private
     */
    this.histogram_chart_ = new org.jboss.search.visualization.Histogram(this.date_histogram_element_);
    this.histogram_chart_.initialize('histogram', 420, 200); // TODO add size to configuration
};
goog.inherits(org.jboss.search.page.filter.DateFilter, goog.Disposable);

/** @inheritDoc */
org.jboss.search.page.filter.DateFilter.prototype.disposeInternal = function() {
    org.jboss.search.page.filter.ProjectFilter.superClass_.disposeInternal.call(this);

    goog.dispose(this.histogram_chart_);
    goog.dispose(this.keyHandler_);

    goog.events.unlistenByKey(this.keyListenerId_);

    this.element_ = null;
    this.date_histogram_element_ = null;
    delete this.expandFilter_;
    delete this.collpaseFilter_;
    delete this.isCollapsed_;
};

/**
 * Calls opt_expandFilter function.
 * @see constructor
 */
org.jboss.search.page.filter.DateFilter.prototype.expandFilter = function() {
    this.expandFilter_();
    this.refreshChart(false);
};

/**
 * Refresh chart (meaning update to the latest search result histogram facet data). By default
 * this does nothing if the filter is collapsed.
 * @param {boolean=} opt_force refresh even if filter is collapsed. Defaults to false.
 */
org.jboss.search.page.filter.DateFilter.prototype.refreshChart = function(opt_force) {
    var force = !!(opt_force || false);
    if (!this.isCollapsed_() || force) {
        var data = org.jboss.search.LookUp.getInstance().getRecentQueryResultData();
        if (data && data.activity_dates_histogram_interval && data.facets &&
            data.facets.activity_dates_histogram && data.facets.activity_dates_histogram.entries) {

            /** @type {Array.<{time: number, count: number}>} */
            var entries_ = data.facets.activity_dates_histogram.entries;

            var requestParams = org.jboss.search.LookUp.getInstance().getRequestParams();
            var from_ = goog.isDateLike(requestParams.getFrom()) ? requestParams.getFrom().getTime() : null;
            var to_ = goog.isDateLike(requestParams.getTo()) ? requestParams.getTo().getTime() : null;
            if (!goog.isNull(from_) || !goog.isNull(to_)) {
                entries_ = goog.array.filter(entries_, function(entry){
                    if (!goog.isNull(to_) && entry.time > to_) { return false }
                    if (!goog.isNull(from_) && entry.time < from_) { return false }
                    return true;
                });
            }
            this.histogram_chart_.update(entries_, data.activity_dates_histogram_interval);
        } else {
            this.histogram_chart_.update([],"month");
        }
    }
};

/**
 * Calls opt_collapseFilter function.
 * @see constructor
 */
org.jboss.search.page.filter.DateFilter.prototype.collapseFilter = function() {
    this.collpaseFilter_();
};

/**
 * @return {org.jboss.search.visualization.Histogram}
 */
org.jboss.search.page.filter.DateFilter.prototype.getHistogramChart = function() {
    return this.histogram_chart_;
};<commit_msg>[fix] exclude invalid data from date chart<commit_after>/*
 * JBoss, Home of Professional Open Source
 * Copyright 2012 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @authors tag. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Date filter.
 * @author Lukas Vlcek (lvlcek@redhat.com)
 */

goog.provide('org.jboss.search.page.filter.DateFilter');

goog.require('org.jboss.search.visualization.Histogram');
goog.require('org.jboss.search.visualization.IntervalSelected');
goog.require('org.jboss.search.LookUp');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.KeyHandler');
goog.require('goog.events.KeyHandler.EventType');
goog.require('goog.array');
goog.require('goog.Disposable');

/**
 *
 * @param {!HTMLElement} element to host the date filter
 * @param {!HTMLElement} date_histogram_element to host the date histogram chart
 * @param {function(): boolean} opt_isCollapsed a function that is used to learn if filter is collapsed
 * @param {Function=} opt_expandFilter a function that is used to show/expand the filter DOM elements
 * @param {Function=} opt_collapseFilter a function that is used to hide/collapse the filter DOM elements
 * @constructor
 * @extends {goog.Disposable}
 */
org.jboss.search.page.filter.DateFilter = function(element, date_histogram_element, opt_isCollapsed, opt_expandFilter, opt_collapseFilter) {
    goog.Disposable.call(this);

    /**
     * @type {HTMLElement}
     * @private
     */
    this.element_ = element;

    /**
     * @type {HTMLElement}
     * @private
     */
    this.date_histogram_element_ = date_histogram_element;

    /**
     * @type {!Function}
     * @private
     */
    this.expandFilter_ = /** @type {!Function} */ (goog.isFunction(opt_expandFilter) ? opt_expandFilter : goog.nullFunction());

    /**
     * @type {!Function}
     * @private
     */
    this.collpaseFilter_ = /** @type {!Function} */ (goog.isFunction(opt_collapseFilter) ? opt_collapseFilter : goog.nullFunction());

    /**
     * @type {function(): boolean}
     * @private
     */
    this.isCollapsed_ = /** @type {function(): boolean} */ (goog.isFunction(opt_isCollapsed) ? opt_isCollapsed : function(){ return true });

    /** @private */
    this.keyHandler_ = new goog.events.KeyHandler(goog.dom.getDocument());
    // We can not use this.element_ but document - because <DIV> may not be focused.
    // this.keyHandler_ = new goog.events.KeyHandler(this.element_);

    // listen for key strokes
    this.keyListenerId_ = goog.events.listen(this.keyHandler_,
        goog.events.KeyHandler.EventType.KEY,
        goog.bind(function(e) {
            var keyEvent = /** @type {goog.events.KeyEvent} */ (e);
            if (!keyEvent.repeat) {
                if (!this.isCollapsed_()) {
                    if (keyEvent.keyCode == goog.events.KeyCodes.ESC) {
//                        keyEvent.preventDefault();
                        this.collapseFilter();
                    }
                }
            }
        }, this)
    );

    /**
     * @type {org.jboss.search.visualization.Histogram}
     * @private
     */
    this.histogram_chart_ = new org.jboss.search.visualization.Histogram(this.date_histogram_element_);
    this.histogram_chart_.initialize('histogram', 420, 200); // TODO add size to configuration
};
goog.inherits(org.jboss.search.page.filter.DateFilter, goog.Disposable);

/** @inheritDoc */
org.jboss.search.page.filter.DateFilter.prototype.disposeInternal = function() {
    org.jboss.search.page.filter.ProjectFilter.superClass_.disposeInternal.call(this);

    goog.dispose(this.histogram_chart_);
    goog.dispose(this.keyHandler_);

    goog.events.unlistenByKey(this.keyListenerId_);

    this.element_ = null;
    this.date_histogram_element_ = null;
    delete this.expandFilter_;
    delete this.collpaseFilter_;
    delete this.isCollapsed_;
};

/**
 * Calls opt_expandFilter function.
 * @see constructor
 */
org.jboss.search.page.filter.DateFilter.prototype.expandFilter = function() {
    this.expandFilter_();
    this.refreshChart(false);
};

/**
 * Refresh chart (meaning update to the latest search result histogram facet data). By default
 * this does nothing if the filter is collapsed.
 * @param {boolean=} opt_force refresh even if filter is collapsed. Defaults to false.
 */
org.jboss.search.page.filter.DateFilter.prototype.refreshChart = function(opt_force) {
    var force = !!(opt_force || false);
    if (!this.isCollapsed_() || force) {
        var data = org.jboss.search.LookUp.getInstance().getRecentQueryResultData();
        if (data && data.activity_dates_histogram_interval && data.facets &&
            data.facets.activity_dates_histogram && data.facets.activity_dates_histogram.entries) {

            /** @type {Array.<{time: number, count: number}>} */
            var entries_ = data.facets.activity_dates_histogram.entries;
            // TODO: move this check to response data normalization
            entries_ = goog.array.filter(entries_, function(entry){
                return entry.time == 0 ? false : true;
            });

            var requestParams = org.jboss.search.LookUp.getInstance().getRequestParams();
            var from_ = goog.isDateLike(requestParams.getFrom()) ? requestParams.getFrom().getTime() : null;
            var to_ = goog.isDateLike(requestParams.getTo()) ? requestParams.getTo().getTime() : null;
            if (!goog.isNull(from_) || !goog.isNull(to_)) {
                entries_ = goog.array.filter(entries_, function(entry){
                    if (!goog.isNull(to_) && entry.time > to_) { return false }
                    if (!goog.isNull(from_) && entry.time < from_) { return false }
                    return true;
                });
            }
            this.histogram_chart_.update(entries_, data.activity_dates_histogram_interval);
        } else {
            this.histogram_chart_.update([],"month");
        }
    }
};

/**
 * Calls opt_collapseFilter function.
 * @see constructor
 */
org.jboss.search.page.filter.DateFilter.prototype.collapseFilter = function() {
    this.collpaseFilter_();
};

/**
 * @return {org.jboss.search.visualization.Histogram}
 */
org.jboss.search.page.filter.DateFilter.prototype.getHistogramChart = function() {
    return this.histogram_chart_;
};<|endoftext|>


<commit_before>/**
 * All Paladin azerite powers go in here.
 * You need to do this manually, usually an easy way to do this is by opening a WCL report and clicking the icons of spells to open the relevant Wowhead pages, here you can get the icon name by clicking the icon, copy the name of the spell and the ID is in the URL.
 * You can access these entries like other entries in the spells files by importing `common/SPELLS` and using the assigned property on the SPELLS object. Please try to avoid abbreviating properties.
 */

export default {
  STALWART_PROTECTOR: {
    id: 274395,
    name: 'Stalwart Protector',
    icon: 'spell_holy_divineshield',
  },
  RADIANT_INCANDESCENCE: {
    id: 278147,
    name: 'Radiant Incandescence',
    icon: 'spell_holy_searinglight',
  },
  RADIANT_INCANDESCENCE_DAMAGE: {
    id: 278145,
    name: 'Radiant Incandescence',
    icon: 'spell_holy_searinglight',
  },
  RADIANT_INCANDESCENCE_TRAIT: {
    id: 277674,
    name: 'Radiant Incandescence',
    icon: 'spell_holy_searinglight',
  },

  // Holy
  GRACE_OF_THE_JUSTICAR_TRAIT: {
    id: 278593,
    name: 'Grace of the Justicar',
    icon: 'spell_holy_healingaura',
  },
  GRACE_OF_THE_JUSTICAR: {
    id: 278785,
    name: 'Grace of the Justicar',
    icon: 'spell_holy_healingaura',
  },
  GLIMMER_OF_LIGHT: {
    id: 287286,
    name: 'Glimmer of Light',
    icon: 'ability_paladin_toweroflight',
  },

  // Retribution
  EXPURGATION: {
    id: 273473,
    name: 'Expurgation',
    icon: 'ability_paladin_bladeofjustice',
  },
  RELENTLESS_INQUISITOR: {
    id: 278617,
    name: 'Relentless Inquisitor',
    icon: 'spell_holy_divinepurpose',
  },
  AVENGERS_MIGHT: {
    id: 272898,
    name: `Avenger's Might`,
    icon: 'spell_holy_avenginewrath',
  },
  INDOMITABLE_JUSTICE: {
    id: 275496,
    name: 'Indomitable Justice',
    icon: 'spell_holy_righteousfury',
  },
  LIGHTS_DECREE: {
    id: 286229,
    name: `Light's Decree`,
    icon: 'spell_holy_blessedresillience',
  },
  EMPYREAN_POWER: {
    id: 286390,
    name: 'Empyrean_power',
    icon: 'ability_paladin_sheathoflight',
  },

  // Retribution Trait Effects
  EXPURGATION_DAMAGE: {
    id: 273481,
    name: 'Expurgation',
    icon: 'ability_paladin_bladeofjustice',
  },
  RELENTLESS_INQUISITOR_BUFF: {
    id: 279204,
    name: 'Relentless Inquisitor',
    icon: 'spell_holy_divinepurpose',
  },
  AVENGERS_MIGHT_BUFF: {
    id: 272903,
    name: `Avenger's Might`,
    icon: 'spell_holy_avenginewrath',
  },
  LIGHTS_DECREE_DAMAGE: {
    id: 286232,
    name: `Light's Decree`,
    icon: 'spell_holy_blessedresillience',
  },
  EMPYREAN_POWER_BUFF: {
    id: 286393,
    name: 'Empyrean Power',
    icon: 'ability_paladin_sheathoflight',
  },

  // Protection
  INSPIRING_VANGUARD: {
    id: 278609,
    name: 'Inspiring Vanguard',
    icon: 'inv_helmet_74',
  },
  INSPIRING_VANGUARD_BUFF: {
    id: 279397,
    name: 'Inspiring Vanguard',
    icon: 'inv_helmet_74',
  },
};
<commit_msg>Changed trait id and added buff id.<commit_after>/**
 * All Paladin azerite powers go in here.
 * You need to do this manually, usually an easy way to do this is by opening a WCL report and clicking the icons of spells to open the relevant Wowhead pages, here you can get the icon name by clicking the icon, copy the name of the spell and the ID is in the URL.
 * You can access these entries like other entries in the spells files by importing `common/SPELLS` and using the assigned property on the SPELLS object. Please try to avoid abbreviating properties.
 */

export default {
  STALWART_PROTECTOR: {
    id: 274395,
    name: 'Stalwart Protector',
    icon: 'spell_holy_divineshield',
  },
  RADIANT_INCANDESCENCE: {
    id: 278147,
    name: 'Radiant Incandescence',
    icon: 'spell_holy_searinglight',
  },
  RADIANT_INCANDESCENCE_DAMAGE: {
    id: 278145,
    name: 'Radiant Incandescence',
    icon: 'spell_holy_searinglight',
  },
  RADIANT_INCANDESCENCE_TRAIT: {
    id: 277674,
    name: 'Radiant Incandescence',
    icon: 'spell_holy_searinglight',
  },

  // Holy
  GRACE_OF_THE_JUSTICAR_TRAIT: {
    id: 278593,
    name: 'Grace of the Justicar',
    icon: 'spell_holy_healingaura',
  },
  GRACE_OF_THE_JUSTICAR: {
    id: 278785,
    name: 'Grace of the Justicar',
    icon: 'spell_holy_healingaura',
  },
  GLIMMER_OF_LIGHT: {
    id: 287286,
    name: 'Glimmer of Light',
    icon: 'ability_paladin_toweroflight',
  },
  GLIMMER_OF_LIGHT_BUFF: {
    id: 287280,
    name: 'Glimmer of Light',
    icon: 'ability_paladin_toweroflight',
  },

  // Retribution
  EXPURGATION: {
    id: 273473,
    name: 'Expurgation',
    icon: 'ability_paladin_bladeofjustice',
  },
  RELENTLESS_INQUISITOR: {
    id: 278617,
    name: 'Relentless Inquisitor',
    icon: 'spell_holy_divinepurpose',
  },
  AVENGERS_MIGHT: {
    id: 272898,
    name: `Avenger's Might`,
    icon: 'spell_holy_avenginewrath',
  },
  INDOMITABLE_JUSTICE: {
    id: 275496,
    name: 'Indomitable Justice',
    icon: 'spell_holy_righteousfury',
  },
  LIGHTS_DECREE: {
    id: 286229,
    name: `Light's Decree`,
    icon: 'spell_holy_blessedresillience',
  },
  EMPYREAN_POWER: {
    id: 286390,
    name: 'Empyrean_power',
    icon: 'ability_paladin_sheathoflight',
  },

  // Retribution Trait Effects
  EXPURGATION_DAMAGE: {
    id: 273481,
    name: 'Expurgation',
    icon: 'ability_paladin_bladeofjustice',
  },
  RELENTLESS_INQUISITOR_BUFF: {
    id: 279204,
    name: 'Relentless Inquisitor',
    icon: 'spell_holy_divinepurpose',
  },
  AVENGERS_MIGHT_BUFF: {
    id: 272903,
    name: `Avenger's Might`,
    icon: 'spell_holy_avenginewrath',
  },
  LIGHTS_DECREE_DAMAGE: {
    id: 286232,
    name: `Light's Decree`,
    icon: 'spell_holy_blessedresillience',
  },
  EMPYREAN_POWER_BUFF: {
    id: 286393,
    name: 'Empyrean Power',
    icon: 'ability_paladin_sheathoflight',
  },

  // Protection
  INSPIRING_VANGUARD: {
    id: 278609,
    name: 'Inspiring Vanguard',
    icon: 'inv_helmet_74',
  },
  INSPIRING_VANGUARD_BUFF: {
    id: 279397,
    name: 'Inspiring Vanguard',
    icon: 'inv_helmet_74',
  },
};
<|endoftext|>