<commit_before>// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url';
import Button from '../base/button';
import Highlight from '../base/highlight';
import Modal from '../base/modal';
import ModalHeader from '../base/modal-header';
import ModalBody from '../base/modal-body';
import MethodTag from '../tags/method-tag';
import { fuzzyMatchAll } from '../../../common/misc';
import type { BaseModel } from '../../../models';
import * as models from '../../../models';
import type { RequestGroup } from '../../../models/request-group';
import type { Request } from '../../../models/request';
import type { Workspace } from '../../../models/workspace';
import { hotKeyRefs } from '../../../common/hotkeys';
import { executeHotKey } from '../../../common/hotkeys-listener';
import KeydownBinder from '../keydown-binder';
import type { RequestMeta } from '../../../models/request-meta';
import { keyboardKeys } from '../../../common/keyboard-keys';

type Props = {
  handleSetActiveWorkspace: (id: string) => void,
  activateRequest: (id: string) => void,
  activeRequest: ?Request,
  workspaceChildren: Array<Request | RequestGroup>,
  workspace: Workspace,
  workspaces: Array<Workspace>,
  requestMetas: Array<RequestMeta>,
};

type State = {
  searchString: string,
  workspaces: Array<Workspace>,
  matchedRequests: Array<Request>,
  matchedWorkspaces: Array<Workspace>,
  activeIndex: number,
  maxRequests: number,
  maxWorkspaces: number,
  disableInput: boolean,
  selectOnKeyup: boolean,
  hideNeverActiveRequests: boolean,
  title: string | null,
};

@autobind
class RequestSwitcherModal extends React.PureComponent<Props, State> {
  modal: ?Modal;
  _input: ?HTMLInputElement;
  _openTimeout: TimeoutID;

  constructor(props: Props) {
    super(props);

    this.state = {
      searchString: '',
      workspaces: [],
      matchedRequests: [],
      matchedWorkspaces: [],
      activeIndex: -1,
      maxRequests: 20,
      maxWorkspaces: 20,
      disableInput: false,
      selectOnKeyup: false,
      hideNeverActiveRequests: false,
      title: null,
    };
  }

  _handleInputKeydown(e: KeyboardEvent) {
    const keyCode = e.keyCode;

    if (keyCode === 38 || (keyCode === 9 && e.shiftKey)) {
      // Up or Shift+Tab
      this._setActiveIndex(this.state.activeIndex - 1);
    } else if (keyCode === 40 || keyCode === 9) {
      // Down or Tab
      this._setActiveIndex(this.state.activeIndex + 1);
    } else if (keyCode === 13) {
      // Enter
      this._activateCurrentIndex();
    } else {
      return;
    }

    e.preventDefault();
  }

  _setModalRef(n: ?Modal) {
    this.modal = n;
  }

  _setInputRef(n: ?HTMLInputElement) {
    this._input = n;
  }

  _setActiveIndex(activeIndex: number) {
    const maxIndex = this.state.matchedRequests.length + this.state.matchedWorkspaces.length;
    if (activeIndex < 0) {
      activeIndex = this.state.matchedRequests.length - 1;
    } else if (activeIndex >= maxIndex) {
      activeIndex = 0;
    }

    this.setState({ activeIndex });
  }

  async _activateCurrentIndex() {
    const { activeIndex, matchedRequests, matchedWorkspaces, searchString } = this.state;

    if (activeIndex < matchedRequests.length) {
      // Activate the request if there is one
      const request = matchedRequests[activeIndex];
      this._activateRequest(request);
    } else if (activeIndex < matchedRequests.length + matchedWorkspaces.length) {
      // Activate the workspace if there is one
      const index = activeIndex - matchedRequests.length;
      const workspace = matchedWorkspaces[index];
      if (workspace) {
        this._activateWorkspace(workspace);
      }
    } else if (searchString) {
      // Create request if no match
      await this._createRequestFromSearch();
    } else {
      // Do nothing
    }
  }

  async _createRequestFromSearch() {
    const { activeRequest, workspace } = this.props;
    const { searchString } = this.state;

    // Create the request if nothing matched
    const parentId = activeRequest ? activeRequest.parentId : workspace._id;
    const patch = {
      parentId,
      name: searchString,
    };

    const request = await models.request.create(patch);
    this._activateRequest(request);
  }

  _activateWorkspace(workspace: Workspace) {
    this.props.handleSetActiveWorkspace(workspace._id);
    this.modal && this.modal.hide();
  }

  _activateRequest(request: ?Request) {
    if (!request) {
      return;
    }

    this.props.activateRequest(request._id);
    this.modal && this.modal.hide();
  }

  _handleChange(e: SyntheticEvent<HTMLInputElement>) {
    this._handleChangeValue(e.currentTarget.value);
  }

  /** Return array of path segments for given request or folder */
  _groupOf(requestOrRequestGroup: BaseModel): Array<string> {
    const { workspaceChildren } = this.props;
    const requestGroups = workspaceChildren.filter(d => d.type === models.requestGroup.type);
    const matchedGroups = requestGroups.filter(g => g._id === requestOrRequestGroup.parentId);
    const currentGroupName =
      requestOrRequestGroup.type === models.requestGroup.type
        ? `${(requestOrRequestGroup: any).name}`
        : '';

    // It's the final parent
    if (matchedGroups.length === 0) {
      return [currentGroupName];
    }

    // Still has more parents
    if (currentGroupName) {
      return [...this._groupOf(matchedGroups[0]), currentGroupName];
    }

    // It's the child
    return this._groupOf(matchedGroups[0]);
  }

  _isMatch(request: Request, searchStrings: string): number | null {
    let finalUrl = request.url;
    if (request.parameters) {
      finalUrl = joinUrlAndQueryString(finalUrl, buildQueryStringFromParams(request.parameters));
    }

    const match = fuzzyMatchAll(
      searchStrings,
      [request.name, finalUrl, request.method || '', this._groupOf(request).join('/')],
      { splitSpace: true },
    );

    // Match exact Id
    const matchesId = request._id === searchStrings;

    // _id match is the highest;
    if (matchesId) {
      return Infinity;
    }

    if (!match) {
      return null;
    }

    return match.score;
  }

  async _handleChangeValue(searchString: string) {
    const { workspace, workspaceChildren, workspaces, requestMetas, activeRequest } = this.props;
    const { maxRequests, maxWorkspaces, hideNeverActiveRequests } = this.state;

    const lastActiveMap = {};
    for (const meta of requestMetas) {
      lastActiveMap[meta.parentId] = meta.lastActive;
    }

    // OPTIMIZATION: This only filters if we have a filter
    let matchedRequests = workspaceChildren
      .filter(d => d.type === models.request.type)
      .sort((a, b) => {
        const aLA = lastActiveMap[a._id] || 0;
        const bLA = lastActiveMap[b._id] || 0;

        // If lastActive same, go by name
        if (aLA === bLA) {
          return a.name > b.name ? 1 : -1;
        }

        return bLA - aLA;
      });

    if (hideNeverActiveRequests) {
      matchedRequests = matchedRequests.filter(r => lastActiveMap[r._id]);
    }

    if (searchString) {
      matchedRequests = matchedRequests
        .map(r => ({
          request: r,
          score: this._isMatch((r: any), searchString),
        }))
        .filter(v => v.score !== null)
        .sort((a, b) => (a.score || -Infinity) - (b.score || -Infinity))
        .map(v => v.request);
    }

    const matchedWorkspaces = workspaces
      .filter(w => w._id !== workspace._id)
      .filter(w => {
        const name = w.name.toLowerCase();
        const toMatch = searchString.toLowerCase();
        return name.indexOf(toMatch) !== -1;
      });

    // Make sure we select the first item but we don't want to select the currently active
    // one because that wouldn't make any sense.
    const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
    const indexOfFirstNonActiveRequest = matchedRequests.findIndex(r => r._id !== activeRequestId);

    this.setState({
      searchString,
      activeIndex: indexOfFirstNonActiveRequest >= 0 ? indexOfFirstNonActiveRequest : 0,
      matchedRequests: (matchedRequests: Array<any>).slice(0, maxRequests),
      matchedWorkspaces: matchedWorkspaces.slice(0, maxWorkspaces),
    });
  }

  async show(
    options: {
      maxRequests?: number,
      maxWorkspaces?: number,
      disableInput?: boolean,
      selectOnKeyup?: boolean,
      hideNeverActiveRequests?: boolean,
      title?: string,
      openDelay?: number,
    } = {},
  ) {
    if (this.modal && this.modal.isOpen()) {
      return;
    }

    this.setState({
      maxRequests: typeof options.maxRequests === 'number' ? options.maxRequests : 20,
      maxWorkspaces: typeof options.maxWorkspaces === 'number' ? options.maxWorkspaces : 20,
      disableInput: !!options.disableInput,
      hideNeverActiveRequests: !!options.hideNeverActiveRequests,
      selectOnKeyup: !!options.selectOnKeyup,
      title: options.title || null,
    });

    await this._handleChangeValue('');

    this._openTimeout = setTimeout(() => {
      this.modal && this.modal.show();
    }, options.openDelay || 0);

    setTimeout(() => this._input && this._input.focus(), 100);
  }

  hide() {
    clearTimeout(this._openTimeout);
    this.modal && this.modal.hide();
  }

  toggle() {
    if (this.modal && this.modal.isOpen()) {
      this.hide();
    } else {
      this.show();
    }
  }

  _handleKeydown(e: KeyboardEvent) {
    if (e.keyCode === keyboardKeys.esc.keyCode) {
      this.hide();
      return;
    }

    executeHotKey(e, hotKeyRefs.SHOW_RECENT_REQUESTS, () => {
      this._setActiveIndex(this.state.activeIndex + 1);
    });

    executeHotKey(e, hotKeyRefs.SHOW_RECENT_REQUESTS_PREVIOUS, () => {
      this._setActiveIndex(this.state.activeIndex - 1);
    });
  }

  _handleKeyup(e: KeyboardEvent) {
    const { selectOnKeyup } = this.state;

    // Handle selection if unpresses all modifier keys. Ideally this would trigger once
    // the user unpresses the hotkey that triggered this modal but we currently do not
    // have the facilities to do that.
    if (selectOnKeyup && !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey) {
      this._activateCurrentIndex();
      this.hide();
    }
  }

  render() {
    const {
      searchString,
      activeIndex,
      matchedRequests,
      matchedWorkspaces,
      disableInput,
      title,
    } = this.state;

    const { workspaceChildren } = this.props;
    const requestGroups = workspaceChildren.filter(d => d.type === models.requestGroup.type);

    return (
      <KeydownBinder onKeydown={this._handleKeydown} onKeyup={this._handleKeyup}>
        <Modal ref={this._setModalRef} dontFocus={!disableInput}>
          <ModalHeader hideCloseButton>
            {title || (
              <React.Fragment>

                      tab or
                      ↑↓ to navigate
                      ↵  to select
                      esc to dismiss

Quick Switch

              </React.Fragment>
            )}
          </ModalHeader>
          <ModalBody className="request-switcher">
            {!disableInput && (

                  <input
                    type="text"
                    placeholder="Filter by name or folder"
                    ref={this._setInputRef}
                    value={searchString}
                    onChange={this._handleChange}
                  />

            )}

              {matchedRequests.map((r, i) => {
                const requestGroup = requestGroups.find(rg => rg._id === r.parentId);
                const buttonClasses = classnames(
                  'btn btn--expandable-small wide text-left pad-bottom',
                  { focus: activeIndex === i },
                );

                return (

                    <Button onClick={this._activateRequest} value={r} className={buttonClasses}>

                        {requestGroup ? (

                            <Highlight search={searchString} text={this._groupOf(r).join(' / ')} />

                        ) : null}
                        <Highlight search={searchString} text={(r: any).name} />

                        <MethodTag method={(r: any).method} />
                        <Highlight search={searchString} text={(r: any).url} />

                    </Button>

                );
              })}

              {matchedRequests.length > 0 && matchedWorkspaces.length > 0 && (

              )}

              {matchedWorkspaces.map((w, i) => {
                const buttonClasses = classnames('btn btn--super-compact wide text-left', {
                  focus: activeIndex - matchedRequests.length === i,
                });

                return (

                    <Button onClick={this._activateWorkspace} value={w} className={buttonClasses}>

                          Switch to {(w: any).name}
                    </Button>

                );
              })}

            {searchString && matchedRequests.length === 0 && matchedWorkspaces.length === 0 && (

                  No matches found for {searchString}

                <button
                  className="btn btn--outlined btn--compact"
                  disabled={!searchString}
                  onClick={this._activateCurrentIndex}>
                  Create a request named {searchString}
                </button>

            )}
          </ModalBody>
        </Modal>
      </KeydownBinder>
    );
  }
}

export default RequestSwitcherModal;
<commit_msg>Fixes for recent request switcher (Closes #1512)<commit_after>// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url';
import Button from '../base/button';
import Highlight from '../base/highlight';
import Modal from '../base/modal';
import ModalHeader from '../base/modal-header';
import ModalBody from '../base/modal-body';
import MethodTag from '../tags/method-tag';
import { fuzzyMatchAll } from '../../../common/misc';
import type { BaseModel } from '../../../models';
import * as models from '../../../models';
import type { RequestGroup } from '../../../models/request-group';
import type { Request } from '../../../models/request';
import type { Workspace } from '../../../models/workspace';
import { hotKeyRefs } from '../../../common/hotkeys';
import { executeHotKey } from '../../../common/hotkeys-listener';
import KeydownBinder from '../keydown-binder';
import type { RequestMeta } from '../../../models/request-meta';
import { keyboardKeys } from '../../../common/keyboard-keys';

type Props = {
  handleSetActiveWorkspace: (id: string) => void,
  activateRequest: (id: string) => void,
  activeRequest: ?Request,
  workspaceChildren: Array<Request | RequestGroup>,
  workspace: Workspace,
  workspaces: Array<Workspace>,
  requestMetas: Array<RequestMeta>,
};

type State = {
  searchString: string,
  workspaces: Array<Workspace>,
  matchedRequests: Array<Request>,
  matchedWorkspaces: Array<Workspace>,
  activeIndex: number,
  maxRequests: number,
  maxWorkspaces: number,
  disableInput: boolean,
  selectOnKeyup: boolean,
  hideNeverActiveRequests: boolean,
  isModalVisible: boolean,
  title: string | null,
};

@autobind
class RequestSwitcherModal extends React.PureComponent<Props, State> {
  modal: ?Modal;
  _input: ?HTMLInputElement;
  _openTimeout: TimeoutID;

  constructor(props: Props) {
    super(props);

    this.state = {
      searchString: '',
      workspaces: [],
      matchedRequests: [],
      matchedWorkspaces: [],
      activeIndex: -1,
      maxRequests: 20,
      maxWorkspaces: 20,
      disableInput: false,
      selectOnKeyup: false,
      hideNeverActiveRequests: false,
      isModalVisible: true,
      title: null,
    };
  }

  _handleInputKeydown(e: KeyboardEvent) {
    const keyCode = e.keyCode;

    if (keyCode === 38 || (keyCode === 9 && e.shiftKey)) {
      // Up or Shift+Tab
      this._setActiveIndex(this.state.activeIndex - 1);
    } else if (keyCode === 40 || keyCode === 9) {
      // Down or Tab
      this._setActiveIndex(this.state.activeIndex + 1);
    } else if (keyCode === 13) {
      // Enter
      this._activateCurrentIndex();
    } else {
      return;
    }

    e.preventDefault();
  }

  _setModalRef(n: ?Modal) {
    this.modal = n;
  }

  _setInputRef(n: ?HTMLInputElement) {
    this._input = n;
  }

  _setActiveIndex(activeIndex: number) {
    const maxIndex = this.state.matchedRequests.length + this.state.matchedWorkspaces.length;
    if (activeIndex < 0) {
      activeIndex = this.state.matchedRequests.length - 1;
    } else if (activeIndex >= maxIndex) {
      activeIndex = 0;
    }

    this.setState({ activeIndex });
  }

  async _activateCurrentIndex() {
    const { activeIndex, matchedRequests, matchedWorkspaces, searchString } = this.state;

    if (activeIndex < matchedRequests.length) {
      // Activate the request if there is one
      const request = matchedRequests[activeIndex];
      this._activateRequest(request);
    } else if (activeIndex < matchedRequests.length + matchedWorkspaces.length) {
      // Activate the workspace if there is one
      const index = activeIndex - matchedRequests.length;
      const workspace = matchedWorkspaces[index];
      if (workspace) {
        this._activateWorkspace(workspace);
      }
    } else if (searchString) {
      // Create request if no match
      await this._createRequestFromSearch();
    } else {
      // Do nothing
    }
  }

  async _createRequestFromSearch() {
    const { activeRequest, workspace } = this.props;
    const { searchString } = this.state;

    // Create the request if nothing matched
    const parentId = activeRequest ? activeRequest.parentId : workspace._id;
    const patch = {
      parentId,
      name: searchString,
    };

    const request = await models.request.create(patch);
    this._activateRequest(request);
  }

  _activateWorkspace(workspace: Workspace) {
    this.props.handleSetActiveWorkspace(workspace._id);
    this.modal && this.modal.hide();
  }

  _activateRequest(request: ?Request) {
    if (!request) {
      return;
    }

    this.props.activateRequest(request._id);
    this.modal && this.modal.hide();
  }

  _handleChange(e: SyntheticEvent<HTMLInputElement>) {
    this._handleChangeValue(e.currentTarget.value);
  }

  /** Return array of path segments for given request or folder */
  _groupOf(requestOrRequestGroup: BaseModel): Array<string> {
    const { workspaceChildren } = this.props;
    const requestGroups = workspaceChildren.filter(d => d.type === models.requestGroup.type);
    const matchedGroups = requestGroups.filter(g => g._id === requestOrRequestGroup.parentId);
    const currentGroupName =
      requestOrRequestGroup.type === models.requestGroup.type
        ? `${(requestOrRequestGroup: any).name}`
        : '';

    // It's the final parent
    if (matchedGroups.length === 0) {
      return [currentGroupName];
    }

    // Still has more parents
    if (currentGroupName) {
      return [...this._groupOf(matchedGroups[0]), currentGroupName];
    }

    // It's the child
    return this._groupOf(matchedGroups[0]);
  }

  _isMatch(request: Request, searchStrings: string): number | null {
    let finalUrl = request.url;
    if (request.parameters) {
      finalUrl = joinUrlAndQueryString(finalUrl, buildQueryStringFromParams(request.parameters));
    }

    const match = fuzzyMatchAll(
      searchStrings,
      [request.name, finalUrl, request.method || '', this._groupOf(request).join('/')],
      { splitSpace: true },
    );

    // Match exact Id
    const matchesId = request._id === searchStrings;

    // _id match is the highest;
    if (matchesId) {
      return Infinity;
    }

    if (!match) {
      return null;
    }

    return match.score;
  }

  _handleChangeValue(searchString: string) {
    const { workspace, workspaceChildren, workspaces, requestMetas, activeRequest } = this.props;
    const { maxRequests, maxWorkspaces, hideNeverActiveRequests } = this.state;

    const lastActiveMap = {};
    for (const meta of requestMetas) {
      lastActiveMap[meta.parentId] = meta.lastActive;
    }

    // OPTIMIZATION: This only filters if we have a filter
    let matchedRequests = workspaceChildren
      .filter(d => d.type === models.request.type)
      .sort((a, b) => {
        const aLA = lastActiveMap[a._id] || 0;
        const bLA = lastActiveMap[b._id] || 0;

        // If lastActive same, go by name
        if (aLA === bLA) {
          return a.name > b.name ? 1 : -1;
        }

        return bLA - aLA;
      });

    if (hideNeverActiveRequests) {
      matchedRequests = matchedRequests.filter(r => lastActiveMap[r._id]);
    }

    if (searchString) {
      matchedRequests = matchedRequests
        .map(r => ({
          request: r,
          score: this._isMatch((r: any), searchString),
        }))
        .filter(v => v.score !== null)
        .sort((a, b) => (a.score || -Infinity) - (b.score || -Infinity))
        .map(v => v.request);
    }

    const matchedWorkspaces = workspaces
      .filter(w => w._id !== workspace._id)
      .filter(w => {
        const name = w.name.toLowerCase();
        const toMatch = searchString.toLowerCase();
        return name.indexOf(toMatch) !== -1;
      });

    // Make sure we select the first item but we don't want to select the currently active
    // one because that wouldn't make any sense.
    const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
    const indexOfFirstNonActiveRequest = matchedRequests.findIndex(r => r._id !== activeRequestId);

    this.setState({
      searchString,
      activeIndex: indexOfFirstNonActiveRequest >= 0 ? indexOfFirstNonActiveRequest : 0,
      matchedRequests: (matchedRequests: Array<any>).slice(0, maxRequests),
      matchedWorkspaces: matchedWorkspaces.slice(0, maxWorkspaces),
    });
  }

  async show(
    options: {
      maxRequests?: number,
      maxWorkspaces?: number,
      disableInput?: boolean,
      selectOnKeyup?: boolean,
      hideNeverActiveRequests?: boolean,
      title?: string,
      openDelay?: number,
    } = {},
  ) {
    // Don't show if we're already showing
    if (this.modal && this.modal.isOpen()) {
      return;
    }

    const newState = {
      maxRequests: typeof options.maxRequests === 'number' ? options.maxRequests : 20,
      maxWorkspaces: typeof options.maxWorkspaces === 'number' ? options.maxWorkspaces : 20,
      disableInput: !!options.disableInput,
      hideNeverActiveRequests: !!options.hideNeverActiveRequests,
      selectOnKeyup: !!options.selectOnKeyup,
      title: options.title || null,
      isModalVisible: false,
    };

    // Using a visibility toggle here because we want to be able to interact with everything
    // here via keyboard BEFORE the modal shows.
    if (options.openDelay) {
      this._openTimeout = setTimeout(() => {
        this.setState({ isModalVisible: true });
      }, options.openDelay);
    } else {
      newState.isModalVisible = true;
    }

    this.setState(newState, () => {
      // Change value after because it accesses state properties
      this._handleChangeValue('');
    });

    this.modal && this.modal.show();

    setTimeout(() => this._input && this._input.focus(), 100);
  }

  hide() {
    clearTimeout(this._openTimeout);
    this.modal && this.modal.hide();
  }

  toggle() {
    if (this.modal && this.modal.isOpen()) {
      this.hide();
    } else {
      this.show();
    }
  }

  _handleKeydown(e: KeyboardEvent) {
    if (e.keyCode === keyboardKeys.esc.keyCode) {
      this.hide();
      return;
    }

    // Only control up/down with tab if modal is visible
    executeHotKey(e, hotKeyRefs.SHOW_RECENT_REQUESTS, () => {
      if (this.state.isModalVisible) {
        this._setActiveIndex(this.state.activeIndex + 1);
      }
    });

    // Only control up/down with tab if modal is visible
    executeHotKey(e, hotKeyRefs.SHOW_RECENT_REQUESTS_PREVIOUS, () => {
      if (this.state.isModalVisible) {
        this._setActiveIndex(this.state.activeIndex - 1);
      }
    });
  }

  async _handleKeyup(e: KeyboardEvent) {
    const { selectOnKeyup } = this.state;

    // Handle selection if unpresses all modifier keys. Ideally this would trigger once
    // the user unpresses the hotkey that triggered this modal but we currently do not
    // have the facilities to do that.
    const isMetaKeyDown = e.ctrlKey || e.shiftKey || e.metaKey || e.altKey;
    const isActive = this.modal && this.modal.isOpen();
    if (selectOnKeyup && isActive && !isMetaKeyDown) {
      await this._activateCurrentIndex();
      this.hide();
    }
  }

  render() {
    const {
      searchString,
      activeIndex,
      matchedRequests,
      matchedWorkspaces,
      disableInput,
      title,
      isModalVisible,
    } = this.state;

    const { workspaceChildren } = this.props;
    const requestGroups = workspaceChildren.filter(d => d.type === models.requestGroup.type);

    return (
      <KeydownBinder onKeydown={this._handleKeydown} onKeyup={this._handleKeyup}>
        <Modal
          ref={this._setModalRef}
          dontFocus={!disableInput}
          className={isModalVisible ? '' : 'hide'}>
          <ModalHeader hideCloseButton>
            {title || (
              <React.Fragment>

                      tab or
                      ↑↓ to navigate
                      ↵  to select
                      esc to dismiss

Quick Switch

              </React.Fragment>
            )}
          </ModalHeader>
          <ModalBody className="request-switcher">
            {!disableInput && (

                  <input
                    type="text"
                    placeholder="Filter by name or folder"
                    ref={this._setInputRef}
                    value={searchString}
                    onChange={this._handleChange}
                  />

            )}

              {matchedRequests.map((r, i) => {
                const requestGroup = requestGroups.find(rg => rg._id === r.parentId);
                const buttonClasses = classnames(
                  'btn btn--expandable-small wide text-left pad-bottom',
                  { focus: activeIndex === i },
                );

                return (

                    <Button onClick={this._activateRequest} value={r} className={buttonClasses}>

                        {requestGroup ? (

                            <Highlight search={searchString} text={this._groupOf(r).join(' / ')} />

                        ) : null}
                        <Highlight search={searchString} text={(r: any).name} />

                        <MethodTag method={(r: any).method} />
                        <Highlight search={searchString} text={(r: any).url} />

                    </Button>

                );
              })}

              {matchedRequests.length > 0 && matchedWorkspaces.length > 0 && (

              )}

              {matchedWorkspaces.map((w, i) => {
                const buttonClasses = classnames('btn btn--super-compact wide text-left', {
                  focus: activeIndex - matchedRequests.length === i,
                });

                return (

                    <Button onClick={this._activateWorkspace} value={w} className={buttonClasses}>

                          Switch to {(w: any).name}
                    </Button>

                );
              })}

            {searchString && matchedRequests.length === 0 && matchedWorkspaces.length === 0 && (

                  No matches found for {searchString}

                <button
                  className="btn btn--outlined btn--compact"
                  disabled={!searchString}
                  onClick={this._activateCurrentIndex}>
                  Create a request named {searchString}
                </button>

            )}
          </ModalBody>
        </Modal>
      </KeydownBinder>
    );
  }
}

export default RequestSwitcherModal;
<|endoftext|>


<commit_before>import { registerMigration } from './migrationUtils';
import { forEachDocumentBatchInCollection } from '../queryUtil.js';
import { LWEvents } from '../../lib/collections/lwevents'
import { ReadStatuses } from '../../lib/collections/readStatus/collection.js'

registerMigration({
  name: "denormalizeReadStatus",
  idempotent: true,
  action: async () => {
    forEachDocumentBatchInCollection({
      collection: LWEvents,
      batchSize: 10000,
      filter: {name: "post-view"},
      callback: (postViews) => {
        const updates = postViews.map(view => ({
          updateOne: {
            filter: {
              postId: view.docmentId,
              userId: view.userId,
            },
            update: {
              $max: {
                lastUpdated: view.createdAt,
              },
              $set: {
                isRead: true
              },
            },
            upsert: true,
          }
        }));
        ReadStatuses.rawCollection().bulkWrite(updates);
      }
    })
  }
});<commit_msg>Fix read-status migration script<commit_after>import { registerMigration } from './migrationUtils';
import { forEachDocumentBatchInCollection } from '../queryUtil.js';
import { LWEvents } from '../../lib/collections/lwevents'
import { ReadStatuses } from '../../lib/collections/readStatus/collection.js'

registerMigration({
  name: "denormalizeReadStatus",
  idempotent: true,
  action: async () => {
    forEachDocumentBatchInCollection({
      collection: LWEvents,
      batchSize: 10000,
      filter: {name: "post-view"},
      callback: (postViews) => {
        const updates = postViews.map(view => ({
          updateOne: {
            filter: {
              postId: view.documentId,
              userId: view.userId,
            },
            update: {
              $max: {
                lastUpdated: view.createdAt,
              },
              $set: {
                isRead: true
              },
            },
            upsert: true,
          }
        }));
        ReadStatuses.rawCollection().bulkWrite(updates);
      }
    })
  }
});<|endoftext|>


<commit_before>/* Copyright (c) 2010, Sage Software, Inc. 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.
 */

Ext.namespace('Sage.Platform.Mobile.Controls');

(function() {
    var U = Sage.Platform.Mobile.Utility;

    Sage.Platform.Mobile.Controls.LookupField = Ext.extend(Sage.Platform.Mobile.Controls.Field, {
        template: new Simplate([
            '{%: $.label %}',
            '{%: $.lookupText %}',
            '<input type="text" {% if ($.requireSelection) { %}readonly="readonly"{% } %} />'
        ]),
        dependentErrorText: "A value for '{0}' must be selected.",
        disabled: false,
        dirty: false,
        view: false,
        keyProperty: '$key',
        textProperty: '$descriptor',
        textTemplate: null,
        textRenderer: null,
        valueKeyProperty: null,
        valueTextProperty: null,
        requireSelection: true,
        emptyText: '',
        completeText: 'Select',
        lookupText: '...',
        init: function() {
            Sage.Platform.Mobile.Controls.LookupField.superclass.init.apply(this, arguments);

            this.containerEl.on('click', this.onClick, this);

            if (this.isReadOnly())
            {
                this.containerEl.addClass('field-disabled');
                this.el.dom.readOnly = true;
            }
            else if (!this.requireSelection)
            {
                this.el
                    .on('keyup', this.onKeyUp, this)
                    .on('blur', this.onBlur, this);
            }
        },
        enable: function(options) {
            if (this.disabled)
            {
                this.containerEl.removeClass('field-disabled');
                this.el
                    .on('keyup', this.onKeyUp, this)
                    .on('blur', this.onBlur, this);
                this.containerEl.on('click', this.onClick, this);
                this.disabled = false;
                if (!options || (options && options.throughUserAction !== false))
                    this.dirty = !this.dirty;
            }
        },
        disable: function(options) {
            if (!this.disabled)
            {
                this.containerEl.addClass('field-disabled');
                this.el
                    .un('keyup', this.onKeyUp, this)
                    .un('blur', this.onBlur, this);
                this.containerEl.un('click', this.onClick, this);
                this.disabled = true;
                if (!options || (options && options.throughUserAction !== false))
                    this.dirty = !this.dirty;
            }
        },
        isDisabled: function() {
            return this.disabled;
        },
        isReadOnly: function() {
            return !this.view;
        },
        getDependentValue: function() {
            if (this.dependsOn && this.owner)
            {
                var field = this.owner.fields[this.dependsOn];
                if (field) return field.getValue();
            }
        },
        getDependentLabel: function() {
            if (this.dependsOn && this.owner)
            {
                var field = this.owner.fields[this.dependsOn];
                if (field) return field.label;
            }
        },
        expandExpression: function(expression) {
            if (typeof expression === 'function')
                return expression.apply(this, Array.prototype.slice.call(arguments, 1));
            else
                return expression;
        },
        createNavigationOptions: function() {
            var options = {
                selectionOnly: true,
                singleSelect: true,
                resourceKind: this.resourceKind,
                resourcePredicate: this.resourcePredicate,
                where: this.where,
                orderBy: this.orderBy,
                tools: {
                    tbar: [{
                        name: 'complete',
                        title: this.completeText,
                        fn: this.complete,
                        scope: this
                    }]
                }
            };

            var expand = ['resourceKind', 'resourcePredicate', 'where'],
                dependentValue = this.getDependentValue();

            if (this.dependsOn && !dependentValue)
            {
                alert(String.format(this.dependentErrorText, this.getDependentLabel()));
                return false;
            }

            Ext.each(expand, function(item) {
                if (this[item])
                    options[item] = this.dependsOn // only pass dependentValue if there is a dependency
                        ? this.expandExpression(this[item], dependentValue)
                        : this.expandExpression(this[item]);
            }, this);

            options.dependentValue = dependentValue;
            options.title = this.title;

            return options;
        },
        navigateToListView: function() {
            var view = App.getView(this.view),
                options = this.createNavigationOptions();
            if (view && options)
                view.show(options);
        },
        onClick: function(evt, el, o) {
            if (evt.getTarget('a') || this.requireSelection)
            {
                evt.stopEvent();

                this.navigateToListView();
            }
        },
        onKeyUp: function(evt, el, o) {
            if (this.notificationTrigger == 'keyup')
                this.onNotificationTrigger(evt, el, o);
        },
        onBlur: function(evt, el, o) {
            if (this.notificationTrigger == 'blur')
                this.onNotificationTrigger(evt, el, o);
        },
        onNotificationTrigger: function(evt, el, o) {
            var currentValue = this.getValue();

            if (this.previousValue != currentValue)
                this.fireEvent('change', currentValue, this);

            this.previousValue = currentValue;
        },
        setText: function(text) {
            this.el.dom.value = text;

            this.previousValue = text;
        },
        getText: function() {
            return this.el.dom.value;
        },
        complete: function() {
            // todo: should there be a better way?
            var view = App.getActiveView();
            if (view && view.selectionModel)
            {
                var selections = view.selectionModel.getSelections();

                for (var selectionKey in selections)
                {
                    var val = selections[selectionKey].data,
                        success = true;

                    this.setSelection(val, selectionKey);
                    break;
                }

                ReUI.back();

                // if the event is fired before the transition, any XMLHttpRequest created in an event handler and
                // executing during the transition can potentially fail (status 0).  this might only be an issue with CORS
                // requests created in this state (the pre-flight request is made, and the request ends with status 0).
                // wrapping thing in a timeout and placing after the transition starts, mitigates this issue.
                if (success) setTimeout(this.onChange.createDelegate(this), 0);
            }
        },
        onChange: function() {
            this.fireEvent('change', this.currentValue, this);
        },
        isDirty: function() {
            if (this.dirty) return true;

            if (this.originalValue && this.currentValue)
            {
                if (this.originalValue.key != this.currentValue.key)
                    return true;

                if (this.originalValue.text != this.currentValue.text)
                    return true;

                if (!this.requireSelection && !this.textTemplate)
                    if (this.originalValue.text != this.getText())
                        return true;

                return false;
            }

            if (this.originalValue)
            {
                if (!this.requireSelection && !this.textTemplate)
                    if (this.originalValue.text != this.getText())
                        return true;
            }
            else
            {
                if (!this.requireSelection && !this.textTemplate)
                {
                    var text = this.getText();
                    if (text && text.length > 0)
                        return true;
                }
            }

            return (this.originalValue != this.currentValue);
        },
        getSelection: function() {
            return this.currentSelection;
        },
        getValue: function() {
            if (this.disabled) return false;

            var value = null,
                text = this.getText() || '',
                // if valueKeyProperty or valueTextProperty IS NOT EXPLICITLY set to false
                // and IS NOT defined use keyProperty or textProperty in its place.
                keyProperty = this.valueKeyProperty !== false
                    ? this.valueKeyProperty || this.keyProperty
                    : false,
                textProperty = this.valueTextProperty !== false
                    ? this.valueTextProperty || this.textProperty
                    : false;

            if (keyProperty || textProperty)
            {
                if (this.currentValue)
                {
                    if (keyProperty)
                        value = U.setValue(value || {}, keyProperty, this.currentValue.key);

                    // if a text template has been applied there is no way to guarantee a correct
                    // mapping back to the property
                    if (textProperty && !this.textTemplate)
                        value = U.setValue(value || {}, textProperty, this.requireSelection ? this.currentValue.text : text);
                }
                else if (!this.requireSelection)
                {
                    if (keyProperty && text.length > 0)
                        value = U.setValue(value || {}, keyProperty, text);

                    // if a text template has been applied there is no way to guarantee a correct
                    // mapping back to the property
                    if (textProperty && !this.textTemplate && text.length > 0)
                    {
                        value = U.setValue(value || {}, textProperty, text);
                    }
                }
            }
            else
            {
                if (this.currentValue)
                {
                    value = this.requireSelection
                        ? this.currentValue.key
                        : this.currentValue.text != text && !this.textTemplate
                            ? text
                            : this.currentValue.key;
                }
                else if (!this.requireSelection && text.length > 0)
                {
                    value = text;
                }
            }

            return value;
        },
        setSelection: function(val, key) {
            var key = U.getValue(val, this.keyProperty, val) || key, // if we can extract the key as requested, use it instead of the selection key
                text = U.getValue(val, this.textProperty);

            if (text && this.textTemplate)
                text = this.textTemplate.apply(text, this);
            else if (this.textRenderer)
                text = this.textRenderer.call(this, val, key, text);

            this.currentSelection = val;

            this.currentValue = {
                key: key || text,
                text: text || key
            };

            this.setText(text);
        },
        setValue: function(val, initial) {
            if (this.disabled) return;

            // if valueKeyProperty or valueTextProperty IS NOT EXPLICITLY set to false
            // and IS NOT defined use keyProperty or textProperty in its place.
            var key,
                text,
                keyProperty = this.valueKeyProperty !== false
                    ? this.valueKeyProperty || this.keyProperty
                    : false,
                textProperty = this.valueTextProperty !== false
                    ? this.valueTextProperty || this.textProperty
                    : false;

            if (keyProperty || textProperty)
            {
                if (keyProperty)
                    key = U.getValue(val, keyProperty);

                if (textProperty)
                    text = U.getValue(val, textProperty);

                if (text && this.textTemplate)
                    text = this.textTemplate.apply(text, this);
                else if (this.textRenderer)
                    text = this.textRenderer.call(this, val, key, text);

                if (key || text)
                {
                    this.currentValue = {
                        key: key || text,
                        text: text || key
                    };

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(this.currentValue.text);
                }
                else
                {
                    this.currentValue = false;

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(this.requireSelection ? this.emptyText : '');
                }
            }
            else
            {
                // Falsy values are 0, -0, null, undefined, NaN, empty string, false
                // But '0' is evaluated to true.
                // We will allow 0 to pass through
                val = val === 0 ? '0' : val;
                if (val)
                {
                    key = val;
                    text = val;

                    if (text && this.textTemplate)
                        text = this.textTemplate.apply(text, this);
                    else if (this.textRenderer)
                        text = this.textRenderer.call(this, val, key, text);

                    this.currentValue = {
                        key: key,
                        text: text
                    };

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(text);
                }
                else
                {
                    this.currentValue = false;

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(this.requireSelection ? this.emptyText : '');
                }
            }
        },
        clearValue: function() {
            this.setValue(null, true);
            this.dirty = false;
        }
    });

    Sage.Platform.Mobile.Controls.FieldManager.register('lookup', Sage.Platform.Mobile.Controls.LookupField);
})();<commit_msg>reverted 281c37 - the incomming value should not be fundamentally changed.<commit_after>/* Copyright (c) 2010, Sage Software, Inc. 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.
 */

Ext.namespace('Sage.Platform.Mobile.Controls');

(function() {
    var U = Sage.Platform.Mobile.Utility;

    Sage.Platform.Mobile.Controls.LookupField = Ext.extend(Sage.Platform.Mobile.Controls.Field, {
        template: new Simplate([
            '{%: $.label %}',
            '{%: $.lookupText %}',
            '<input type="text" {% if ($.requireSelection) { %}readonly="readonly"{% } %} />'
        ]),
        dependentErrorText: "A value for '{0}' must be selected.",
        disabled: false,
        dirty: false,
        view: false,
        keyProperty: '$key',
        textProperty: '$descriptor',
        textTemplate: null,
        textRenderer: null,
        valueKeyProperty: null,
        valueTextProperty: null,
        requireSelection: true,
        emptyText: '',
        completeText: 'Select',
        lookupText: '...',
        init: function() {
            Sage.Platform.Mobile.Controls.LookupField.superclass.init.apply(this, arguments);

            this.containerEl.on('click', this.onClick, this);

            if (this.isReadOnly())
            {
                this.containerEl.addClass('field-disabled');
                this.el.dom.readOnly = true;
            }
            else if (!this.requireSelection)
            {
                this.el
                    .on('keyup', this.onKeyUp, this)
                    .on('blur', this.onBlur, this);
            }
        },
        enable: function(options) {
            if (this.disabled)
            {
                this.containerEl.removeClass('field-disabled');
                this.el
                    .on('keyup', this.onKeyUp, this)
                    .on('blur', this.onBlur, this);
                this.containerEl.on('click', this.onClick, this);
                this.disabled = false;
                if (!options || (options && options.throughUserAction !== false))
                    this.dirty = !this.dirty;
            }
        },
        disable: function(options) {
            if (!this.disabled)
            {
                this.containerEl.addClass('field-disabled');
                this.el
                    .un('keyup', this.onKeyUp, this)
                    .un('blur', this.onBlur, this);
                this.containerEl.un('click', this.onClick, this);
                this.disabled = true;
                if (!options || (options && options.throughUserAction !== false))
                    this.dirty = !this.dirty;
            }
        },
        isDisabled: function() {
            return this.disabled;
        },
        isReadOnly: function() {
            return !this.view;
        },
        getDependentValue: function() {
            if (this.dependsOn && this.owner)
            {
                var field = this.owner.fields[this.dependsOn];
                if (field) return field.getValue();
            }
        },
        getDependentLabel: function() {
            if (this.dependsOn && this.owner)
            {
                var field = this.owner.fields[this.dependsOn];
                if (field) return field.label;
            }
        },
        expandExpression: function(expression) {
            if (typeof expression === 'function')
                return expression.apply(this, Array.prototype.slice.call(arguments, 1));
            else
                return expression;
        },
        createNavigationOptions: function() {
            var options = {
                selectionOnly: true,
                singleSelect: true,
                resourceKind: this.resourceKind,
                resourcePredicate: this.resourcePredicate,
                where: this.where,
                orderBy: this.orderBy,
                tools: {
                    tbar: [{
                        name: 'complete',
                        title: this.completeText,
                        fn: this.complete,
                        scope: this
                    }]
                }
            };

            var expand = ['resourceKind', 'resourcePredicate', 'where'],
                dependentValue = this.getDependentValue();

            if (this.dependsOn && !dependentValue)
            {
                alert(String.format(this.dependentErrorText, this.getDependentLabel()));
                return false;
            }

            Ext.each(expand, function(item) {
                if (this[item])
                    options[item] = this.dependsOn // only pass dependentValue if there is a dependency
                        ? this.expandExpression(this[item], dependentValue)
                        : this.expandExpression(this[item]);
            }, this);

            options.dependentValue = dependentValue;
            options.title = this.title;

            return options;
        },
        navigateToListView: function() {
            var view = App.getView(this.view),
                options = this.createNavigationOptions();
            if (view && options)
                view.show(options);
        },
        onClick: function(evt, el, o) {
            if (evt.getTarget('a') || this.requireSelection)
            {
                evt.stopEvent();

                this.navigateToListView();
            }
        },
        onKeyUp: function(evt, el, o) {
            if (this.notificationTrigger == 'keyup')
                this.onNotificationTrigger(evt, el, o);
        },
        onBlur: function(evt, el, o) {
            if (this.notificationTrigger == 'blur')
                this.onNotificationTrigger(evt, el, o);
        },
        onNotificationTrigger: function(evt, el, o) {
            var currentValue = this.getValue();

            if (this.previousValue != currentValue)
                this.fireEvent('change', currentValue, this);

            this.previousValue = currentValue;
        },
        setText: function(text) {
            this.el.dom.value = text;

            this.previousValue = text;
        },
        getText: function() {
            return this.el.dom.value;
        },
        complete: function() {
            // todo: should there be a better way?
            var view = App.getActiveView();
            if (view && view.selectionModel)
            {
                var selections = view.selectionModel.getSelections();

                for (var selectionKey in selections)
                {
                    var val = selections[selectionKey].data,
                        success = true;

                    this.setSelection(val, selectionKey);
                    break;
                }

                ReUI.back();

                // if the event is fired before the transition, any XMLHttpRequest created in an event handler and
                // executing during the transition can potentially fail (status 0).  this might only be an issue with CORS
                // requests created in this state (the pre-flight request is made, and the request ends with status 0).
                // wrapping thing in a timeout and placing after the transition starts, mitigates this issue.
                if (success) setTimeout(this.onChange.createDelegate(this), 0);
            }
        },
        onChange: function() {
            this.fireEvent('change', this.currentValue, this);
        },
        isDirty: function() {
            if (this.dirty) return true;

            if (this.originalValue && this.currentValue)
            {
                if (this.originalValue.key != this.currentValue.key)
                    return true;

                if (this.originalValue.text != this.currentValue.text)
                    return true;

                if (!this.requireSelection && !this.textTemplate)
                    if (this.originalValue.text != this.getText())
                        return true;

                return false;
            }

            if (this.originalValue)
            {
                if (!this.requireSelection && !this.textTemplate)
                    if (this.originalValue.text != this.getText())
                        return true;
            }
            else
            {
                if (!this.requireSelection && !this.textTemplate)
                {
                    var text = this.getText();
                    if (text && text.length > 0)
                        return true;
                }
            }

            return (this.originalValue != this.currentValue);
        },
        getSelection: function() {
            return this.currentSelection;
        },
        getValue: function() {
            if (this.disabled) return false;

            var value = null,
                text = this.getText() || '',
                // if valueKeyProperty or valueTextProperty IS NOT EXPLICITLY set to false
                // and IS NOT defined use keyProperty or textProperty in its place.
                keyProperty = this.valueKeyProperty !== false
                    ? this.valueKeyProperty || this.keyProperty
                    : false,
                textProperty = this.valueTextProperty !== false
                    ? this.valueTextProperty || this.textProperty
                    : false;

            if (keyProperty || textProperty)
            {
                if (this.currentValue)
                {
                    if (keyProperty)
                        value = U.setValue(value || {}, keyProperty, this.currentValue.key);

                    // if a text template has been applied there is no way to guarantee a correct
                    // mapping back to the property
                    if (textProperty && !this.textTemplate)
                        value = U.setValue(value || {}, textProperty, this.requireSelection ? this.currentValue.text : text);
                }
                else if (!this.requireSelection)
                {
                    if (keyProperty && text.length > 0)
                        value = U.setValue(value || {}, keyProperty, text);

                    // if a text template has been applied there is no way to guarantee a correct
                    // mapping back to the property
                    if (textProperty && !this.textTemplate && text.length > 0)
                    {
                        value = U.setValue(value || {}, textProperty, text);
                    }
                }
            }
            else
            {
                if (this.currentValue)
                {
                    value = this.requireSelection
                        ? this.currentValue.key
                        : this.currentValue.text != text && !this.textTemplate
                            ? text
                            : this.currentValue.key;
                }
                else if (!this.requireSelection && text.length > 0)
                {
                    value = text;
                }
            }

            return value;
        },
        setSelection: function(val, key) {
            var key = U.getValue(val, this.keyProperty, val) || key, // if we can extract the key as requested, use it instead of the selection key
                text = U.getValue(val, this.textProperty);

            if (text && this.textTemplate)
                text = this.textTemplate.apply(text, this);
            else if (this.textRenderer)
                text = this.textRenderer.call(this, val, key, text);

            this.currentSelection = val;

            this.currentValue = {
                key: key || text,
                text: text || key
            };

            this.setText(text);
        },
        setValue: function(val, initial) {
            if (this.disabled) return;

            // if valueKeyProperty or valueTextProperty IS NOT EXPLICITLY set to false
            // and IS NOT defined use keyProperty or textProperty in its place.
            var key,
                text,
                keyProperty = this.valueKeyProperty !== false
                    ? this.valueKeyProperty || this.keyProperty
                    : false,
                textProperty = this.valueTextProperty !== false
                    ? this.valueTextProperty || this.textProperty
                    : false;

            if (keyProperty || textProperty)
            {
                if (keyProperty)
                    key = U.getValue(val, keyProperty);

                if (textProperty)
                    text = U.getValue(val, textProperty);

                if (text && this.textTemplate)
                    text = this.textTemplate.apply(text, this);
                else if (this.textRenderer)
                    text = this.textRenderer.call(this, val, key, text);

                if (key || text)
                {
                    this.currentValue = {
                        key: key || text,
                        text: text || key
                    };

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(this.currentValue.text);
                }
                else
                {
                    this.currentValue = false;

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(this.requireSelection ? this.emptyText : '');
                }
            }
            else
            {
                if (val)
                {
                    key = val;
                    text = val;

                    if (text && this.textTemplate)
                        text = this.textTemplate.apply(text, this);
                    else if (this.textRenderer)
                        text = this.textRenderer.call(this, val, key, text);

                    this.currentValue = {
                        key: key,
                        text: text
                    };

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(text);
                }
                else
                {
                    this.currentValue = false;

                    if (initial) this.originalValue = this.currentValue;

                    this.setText(this.requireSelection ? this.emptyText : '');
                }
            }
        },
        clearValue: function() {
            this.setValue(null, true);
            this.dirty = false;
        }
    });

    Sage.Platform.Mobile.Controls.FieldManager.register('lookup', Sage.Platform.Mobile.Controls.LookupField);
})();<|endoftext|>


<commit_before>/**
 * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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.
 */

define(['require', 'elementUtils', 'lodash'],
    function (require, ElementUtils, _) {

        /**
         * @class Partition
         * @constructor
         * @class Partition  Creates a Partition
         * @param {Object} options Rendering options for the view
         */
        var Partition = function (options) {
            /*
             Data storing structure as follows.
                id*: ‘’,
                streamList = [];
                queryLists: [
                    {
                        '<queryType>': [{Query JSON},...]
                    },
                    ...
                ],
                partitionWith*: [
                    {
                        streamName*: '',
                        expression*: ''
                    },
                    ...
                ],
                annotationList: {Annotation JSON Array}
            */
            if (options !== undefined) {
                this.id = options.id;
            }
            this.streamList = [];
            this.queryLists = {
                WINDOW_FILTER_PROJECTION: [],
                PATTERN: [],
                SEQUENCE: [],
                JOIN: []
            };
            this.partitionWith = [];
            this.annotationList = [];
        };

        Partition.prototype.addAnnotation = function (annotation) {
            this.annotationList.push(annotation);
        };

        Partition.prototype.addStream = function (stream) {
            this.streamList.push(stream);
        };

        Partition.prototype.addPartitionWith = function (partitionWith) {
            this.partitionWith.push(partitionWith);
        };

        Partition.prototype.addWindowFilterProjectionQuery = function (windowFilterProjectionQuery) {
            this.queryLists.WINDOW_FILTER_PROJECTION.push(windowFilterProjectionQuery);
        };

        Partition.prototype.addPatternQuery = function (patternQuery) {
            this.queryLists.PATTERN.push(patternQuery);
        };

        Partition.prototype.addSequenceQuery = function (sequenceQuery) {
            this.queryLists.SEQUENCE.push(sequenceQuery);
        };

        Partition.prototype.addJoinQuery = function (joinQuery) {
            this.queryLists.JOIN.push(joinQuery);
        };

        Partition.prototype.clearAnnotationList = function () {
            ElementUtils.prototype.removeAllElements(this.annotationList);
        };

        Partition.prototype.clearPartitionWith = function () {
            ElementUtils.prototype.removeAllElements(this.partitionWith);
        };

        Partition.prototype.removePartitionWith = function (partitionWithStreamName) {
            var self = this;
            var index = undefined;
            _.forEach(self.partitionWith, function(partitionWith) {
                if (partitionWith.getStreamName() === partitionWithStreamName) {
                    index = self.partitionWith.indexOf(partitionWith);
                }
            });
            if (index > -1) {
                this.partitionWith.splice(index, 1);
            }
        };

        Partition.prototype.removeStream = function (streamId) {
            ElementUtils.prototype.removeElement(this.streamList, streamId);
        };

        Partition.prototype.removeWindowFilterProjectionQuery = function (windowFilterProjectionQueryId) {
            ElementUtils.prototype
                .removeElement(this.queryLists.WINDOW_FILTER_PROJECTION, windowFilterProjectionQueryId);
        };

        Partition.prototype.removePatternQuery = function (patternQueryId) {
            ElementUtils.prototype.removeElement(this.queryLists.PATTERN, patternQueryId);
        };

        Partition.prototype.removeSequenceQuery = function (sequenceQueryId) {
            ElementUtils.prototype.removeElement(this.queryLists.SEQUENCE, sequenceQueryId);
        };

        Partition.prototype.removeJoinQuery = function (joinQueryId) {
            ElementUtils.prototype.removeElement(this.queryLists.JOIN, joinQueryId);
        };

        Partition.prototype.getId = function () {
            return this.id;
        };

        Partition.prototype.getStream = function (streamId) {
            return ElementUtils.prototype.getElement(this.streamList, streamId);
        };

        Partition.prototype.getWindowFilterProjectionQuery = function (windowFilterProjectionQueryId) {
            return ElementUtils.prototype
                .getElement(this.queryLists.WINDOW_FILTER_PROJECTION, windowFilterProjectionQueryId);
        };

        Partition.prototype.getPatternQuery = function (patternQueryId) {
            return ElementUtils.prototype.getElement(this.queryLists.PATTERN, patternQueryId);
        };

        Partition.prototype.getSequenceQuery = function (sequenceQueryId) {
            return ElementUtils.prototype.getElement(this.queryLists.SEQUENCE, sequenceQueryId);
        };

        Partition.prototype.getJoinQuery = function (joinQueryId) {
            return ElementUtils.prototype.getElement(this.queryLists.JOIN, joinQueryId);
        };

        Partition.prototype.getWindowFilterProjectionQueryList = function () {
            return this.queryLists.WINDOW_FILTER_PROJECTION;
        };

        Partition.prototype.getPatternQueryList = function () {
            return this.queryLists.PATTERN;
        };

        Partition.prototype.getSequenceQueryList = function () {
            return this.queryLists.SEQUENCE;
        };

        Partition.prototype.getJoinQueryList = function () {
            return this.queryLists.JOIN;
        };

        Partition.prototype.getPartitionWith = function () {
            return this.partitionWith;
        };

        Partition.prototype.getAnnotationList = function () {
            return this.annotationList;
        };

        Partition.prototype.setId = function (id) {
            this.id = id;
        };

        Partition.prototype.setPartitionWith = function (partitionWith) {
            this.partitionWith = partitionWith;
        };

        Partition.prototype.setAnnotationList = function (annotationList) {
            this.annotationList = annotationList;
        };

        return Partition;

    });
<commit_msg>Add utility methods regards to partition<commit_after>/**
 * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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.
 */

define(['require', 'elementUtils', 'lodash'],
    function (require, ElementUtils, _) {

        /**
         * @class Partition
         * @constructor
         * @class Partition  Creates a Partition
         * @param {Object} options Rendering options for the view
         */
        var Partition = function (options) {
            /*
             Data storing structure as follows.
                id*: ‘’,
                streamList = [];
                queryLists: [
                    {
                        '<queryType>': [{Query JSON},...]
                    },
                    ...
                ],
                partitionWith*: [
                    {
                        streamName*: '',
                        expression*: ''
                    },
                    ...
                ],
                annotationList: {Annotation JSON Array}
            */
            if (options !== undefined) {
                this.id = options.id;
            }
            this.streamList = [];
            this.queryLists = {
                WINDOW_FILTER_PROJECTION: [],
                PATTERN: [],
                SEQUENCE: [],
                JOIN: []
            };
            this.partitionWith = [];
            this.annotationList = [];
        };

        Partition.prototype.addAnnotation = function (annotation) {
            this.annotationList.push(annotation);
        };

        Partition.prototype.addStream = function (stream) {
            this.streamList.push(stream);
        };

        Partition.prototype.addPartitionWith = function (partitionWith) {
            this.partitionWith.push(partitionWith);
        };

        Partition.prototype.addWindowFilterProjectionQuery = function (windowFilterProjectionQuery) {
            this.queryLists.WINDOW_FILTER_PROJECTION.push(windowFilterProjectionQuery);
        };

        Partition.prototype.addPatternQuery = function (patternQuery) {
            this.queryLists.PATTERN.push(patternQuery);
        };

        Partition.prototype.addSequenceQuery = function (sequenceQuery) {
            this.queryLists.SEQUENCE.push(sequenceQuery);
        };

        Partition.prototype.addJoinQuery = function (joinQuery) {
            this.queryLists.JOIN.push(joinQuery);
        };

        Partition.prototype.clearAnnotationList = function () {
            ElementUtils.prototype.removeAllElements(this.annotationList);
        };

        Partition.prototype.clearPartitionWith = function () {
            ElementUtils.prototype.removeAllElements(this.partitionWith);
        };

        Partition.prototype.removePartitionWith = function (partitionWithStreamName) {
            var self = this;
            var index = undefined;
            _.forEach(self.partitionWith, function(partitionWith) {
                if (partitionWith.getStreamName() === partitionWithStreamName) {
                    index = self.partitionWith.indexOf(partitionWith);
                }
            });
            if (index > -1) {
                this.partitionWith.splice(index, 1);
            }
        };

        Partition.prototype.removeStream = function (streamId) {
            ElementUtils.prototype.removeElement(this.streamList, streamId);
        };

        Partition.prototype.removeWindowFilterProjectionQuery = function (windowFilterProjectionQueryId) {
            ElementUtils.prototype
                .removeElement(this.queryLists.WINDOW_FILTER_PROJECTION, windowFilterProjectionQueryId);
        };

        Partition.prototype.removePatternQuery = function (patternQueryId) {
            ElementUtils.prototype.removeElement(this.queryLists.PATTERN, patternQueryId);
        };

        Partition.prototype.removeSequenceQuery = function (sequenceQueryId) {
            ElementUtils.prototype.removeElement(this.queryLists.SEQUENCE, sequenceQueryId);
        };

        Partition.prototype.removeJoinQuery = function (joinQueryId) {
            ElementUtils.prototype.removeElement(this.queryLists.JOIN, joinQueryId);
        };

        Partition.prototype.getId = function () {
            return this.id;
        };

        Partition.prototype.getStream = function (streamId) {
            return ElementUtils.prototype.getElement(this.streamList, streamId);
        };

        Partition.prototype.getWindowFilterProjectionQuery = function (windowFilterProjectionQueryId) {
            return ElementUtils.prototype
                .getElement(this.queryLists.WINDOW_FILTER_PROJECTION, windowFilterProjectionQueryId);
        };

        Partition.prototype.getPatternQuery = function (patternQueryId) {
            return ElementUtils.prototype.getElement(this.queryLists.PATTERN, patternQueryId);
        };

        Partition.prototype.getSequenceQuery = function (sequenceQueryId) {
            return ElementUtils.prototype.getElement(this.queryLists.SEQUENCE, sequenceQueryId);
        };

        Partition.prototype.getJoinQuery = function (joinQueryId) {
            return ElementUtils.prototype.getElement(this.queryLists.JOIN, joinQueryId);
        };

        Partition.prototype.getWindowFilterProjectionQueryList = function () {
            return this.queryLists.WINDOW_FILTER_PROJECTION;
        };

        Partition.prototype.getPatternQueryList = function () {
            return this.queryLists.PATTERN;
        };

        Partition.prototype.getSequenceQueryList = function () {
            return this.queryLists.SEQUENCE;
        };

        Partition.prototype.getJoinQueryList = function () {
            return this.queryLists.JOIN;
        };

        Partition.prototype.getPartitionWith = function () {
            return this.partitionWith;
        };

        Partition.prototype.getAnnotationList = function () {
            return this.annotationList;
        };

        Partition.prototype.setId = function (id) {
            this.id = id;
        };

        Partition.prototype.setPartitionWith = function (partitionWith) {
            this.partitionWith = partitionWith;
        };

        Partition.prototype.setAnnotationList = function (annotationList) {
            this.annotationList = annotationList;
        };

        Partition.prototype.checkOuterStreamIsAlreadyConnected = function (streamName) {
            var self = this;
            var isStreamConnected = false;
            _.forEach(self.partitionWith, function(partitionWith) {
                if (partitionWith.getStreamName() === streamName) {
                    isStreamConnected = true;
                }
            });
            return isStreamConnected;
        };

        Partition.prototype.getNoOfElementsInPartition = function () {
            var self = this;
            return self.streamList.length + self.queryLists.WINDOW_FILTER_PROJECTION.length +
                self.queryLists.JOIN.length + self.queryLists.PATTERN.length + self.queryLists.SEQUENCE.length;
        };

        return Partition;

    });
<|endoftext|>


<commit_before>var Bluebird = require('bluebird');
var eq = require('deep-equal');
var _ = require('lodash');

var diff = require('./diff');

function Model (collection, attributes, props) {
    this.def('collection', collection);
    this.def('old', {});
    this.def('attributes', {});
    this.def('columns', collection.table.columns);
    this.def('isFromDb', false);

    for (var key in props) {
        this.def(key, props[key]);
    }

    this.bindAccessors();
    _.extend(this, attributes);
}

/**
 * Binds setters and getters for model columns.
 */
Model.prototype.bindAccessors = function () {
    var properties = {};

    _.forEach(this.columns, function (column) {
        var key = column.getName();
        var getter = this.getters[key];
        var setter = this.setters[key];

        properties[key] = {
            configurable: true,
            enumerable: true,
            get: function () {
                if (typeof getter === 'undefined') {
                    return this.attributes[key];
                } else {
                    return getter.call(this, this.attributes[key]);
                }
            },
            set: function (value) {
                if (typeof setter === 'undefined') {
                    this.attributes[key] = value;
                } else {
                    this.attributes[key] = setter.call(this, value);
                }
            }
        };
    }, this);

    Object.defineProperties(this, properties);
};

/**
 * Defines a new non-enumerable property on the model.
 *
 * @param {String}   name
 * @param {*} value
 */
Model.prototype.def = function (name, value) {
    Object.defineProperty(this, name, {
        configurable: true,
        writable: true,
        enumerable: false,
        value: value
    });
};

/**
 * Wipes the sync state and attributes. Mainly for testing ease, but meh,
 * you might find this useful too.
 */
Model.prototype.reset = function () {
    this.old = {};
    this.isFromDb = false;
};

/**
 * Fixes the casing of a column name to match the model definition, since
 * Cassandra columns are case-insensitive but attributes are sensitive.
 * @param  {String} column
 * @return {String}
 */
Model.prototype.fixCasing = function (column) {
    for (var i = 0, l = this.columns.length; i < l; i++) {
        var name = this.columns[i].toString();
        if (name.toLowerCase() === column.toLowerCase()) {
            return name;
        }
    }

    return column;
};

/**
 * Updates the "old" and existing properties. Should be called after the
 * model is updated or read from the database.
 *
 * @param  {Object} attributes
 * @return {Model}
 */
Model.prototype.sync = function (attributes) {
    this.isFromDb = true;

    var self = this;
    Object.keys(attributes).forEach(function (key) {
        var value = attributes[key];
        var name = self.fixCasing(key);

        self.attributes[name] = _.cloneDeep(value);
        self.old[name] = _.cloneDeep(value);
    });

    return this;
};

/**
 * Extends this model's data with anything passed in the arguments. Functions
 * in the same way to lodash's "extend".
 * @return {Model}
 */
Model.prototype.extend = function () {
    var args = new Array(arguments.length + 1);
    var data = args[0] = {};

    var i, l;
    for (i = 0, l = arguments.length; i < l; i++) {
        args[i + 1] = arguments[i];
    }

    // Compact all the arguments to our single "data" argument
    _.extend.apply(null, args);

    // Then apply those over this model.
    for (i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i].toString();
        var item = data[column];
        if (typeof item !== 'undefined') {
            this[column] = item;
        }
    }

    return this;
};

/**
 * Iterates over the model's columns, typecasting each one.
 */
Model.prototype.fixTypes = function () {
    for (var i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i];
        var value = this.attributes[column.toString()];

        this.attributes[column.toString()] = this.collection.typecast(column, value);
    }
};

/**
 * Returns whether the property has changed since it was synced to the
 * database.
 * @param  {String}  property
 * @return {Boolean}
 */
Model.prototype.isDirty = function (property) {
    this.fixTypes();

    return !eq(this.attributes[property], this.old[property]);
};

/**
 * Returns true if every property is synced with the db. False if any
 * are different.
 *
 * @return {Boolean}
 */
Model.prototype.isSynced = function () {
    for (var i = 0, l = this.columns.length; i < l; i++) {
        if (this.isDirty(this.columns[i].toString())) {
            return false;
        }
    }

    return true;
};

/**
 * Converts the model to a plain object
 *
 * @param  {Boolean} useGetters
 * @return {Object}
 */
Model.prototype.toObject = function (useGetters) {
    this.fixTypes();

    var output = {};
    for (var i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i].toString();
        if (typeof useGetters === 'undefined' || useGetters === true) {
            output[column] = this[column];
        } else {
            output[column] = this.attributes[column];
        }
    }

    return output;
};

/**
 * Converts the record to a JSON representation.
 *
 * @return {String}
 */
Model.prototype.toJson = function () {
    return JSON.stringify(this.toObject());
};

/**
 * Persists the model to the database.
 *
 * @param {Boolean=false} force
 * @return {Promise}
 */
Model.prototype.save = function (force) {
    if (!force && this.isSynced()) {
        return Bluebird.resolve(this);
    }

    this.fixTypes();
    if (this.isFromDb) {
        return this.updateExisting();
    } else {
        return this.createNew();
    }
};

/**
 * Deletes this model's record from the database.
 *
 * @param {Boolean=false} force
 * @return {Promise}
 */
Model.prototype.delete = wrapAround('beforeDelete', function (force) {
    if (!this.isFromDb && !force) {
        return Bluebird.resolve();
    }

    return this.addWhereSelf(this.collection.delete()).execute();
}, 'afterDelete');

/**
 * Adds a "where" to the query so that it targets only this model.
 * @param {BaseQuery} query
 * @return {BaseQuery}
 */
Model.prototype.addWhereSelf = function (query) {
    var keys = this.collection.table.keys;
    var pks = keys.partition.concat(keys.compound);

    if (!pks.length) {
        throw new Error('A primary key is required on all models.');
    }

    for (var i = 0, l = pks.length; i < l; i++) {
        query.where(pks[i], '=', this.old[pks[i].toString()]);
    }

    return query;
};

/**
 * INSERTs the model into the database.
 *
 * @return {Promise}
 */
Model.prototype.createNew = wrapAround('beforeCreate', function () {
    var properties = this.toObject(false);
    var self = this;

    return this.collection.insert()
        .data(properties)
        .then(function () {
            self.sync(properties);
            return self;
        });
}, 'afterCreate');

/**
 * UPDATEs a record in the database with the changed properties.
 *
 * @return {Promise}
 */
Model.prototype.updateExisting = wrapAround('beforeUpdate', function () {
    var query = this.addWhereSelf(this.collection.update());
    var self = this;

    for (var i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i];
        var name = column.getName();

        diff(query, column, this.old[name], this.attributes[name]);
    }

    return query.then(function () {
        self.sync(self.toObject(false));
        return self;
    });
}, 'afterUpdate');

/**
 * Wraps the function so that the "before" and "after" middleware
 * is run "around" it.
 *
 * @param  {String}   before
 * @param  {Function} fn
 * @param  {String}   after
 * @return {Promise}
 */
function wrapAround (before, fn, after) {
    return function () {
        var self = this;
        return self.collection
            .run(before, self)
            .then(fn.bind(self))
            .then(function () {
                var resolved = Bluebird.resolve.apply(Bluebird, arguments);

                return self.collection.run(after, self).then(function () {
                    return resolved;
                });
            });
    };
}

module.exports = Model;
<commit_msg>bump model instantiation back up to 100k+ ops/second<commit_after>var Bluebird = require('bluebird');
var eq = require('deep-equal');
var _ = require('lodash');

var diff = require('./diff');

function Model (collection, attributes, props) {
    this.def('collection', collection);
    this.def('old', {});
    this.def('attributes', {});
    this.def('columns', collection.table.columns);
    this.def('isFromDb', false);

    for (var key in props) {
        this.def(key, props[key]);
    }

    this.bindAccessors();
    _.extend(this, attributes);
}

/**
 * Binds setters and getters for model columns. We only add accessords
 * for columns
 */
Model.prototype.bindAccessors = function () {
    var properties = {};

    this.def('accessored', _.union(_.keys(this.getters), _.keys(this.setters)));

    _.forEach(this.accessored, function (key) {
        var getter = this.getters[key];
        var setter = this.setters[key];

        properties[key] = {
            configurable: true,
            enumerable: true,
            get: function () {
                if (typeof getter === 'undefined') {
                    return this.attributes[key];
                } else {
                    return getter.call(this, this.attributes[key]);
                }
            },
            set: function (value) {
                if (typeof setter === 'undefined') {
                    this.attributes[key] = value;
                } else {
                    this.attributes[key] = setter.call(this, value);
                }
            }
        };
    }, this);

    if (this.accessored.length > 0) {
        Object.defineProperties(this, properties);
    }
};

/**
 * Defines a new non-enumerable property on the model.
 *
 * @param {String}   name
 * @param {*} value
 */
Model.prototype.def = function (name, value) {
    Object.defineProperty(this, name, {
        configurable: true,
        writable: true,
        enumerable: false,
        value: value
    });
};

/**
 * Wipes the sync state and attributes. Mainly for testing ease, but meh,
 * you might find this useful too.
 */
Model.prototype.reset = function () {
    this.old = {};
    this.isFromDb = false;
};

/**
 * Fixes the casing of a column name to match the model definition, since
 * Cassandra columns are case-insensitive but attributes are sensitive.
 * @param  {String} column
 * @return {String}
 */
Model.prototype.fixCasing = function (column) {
    for (var i = 0, l = this.columns.length; i < l; i++) {
        var name = this.columns[i].toString();
        if (name.toLowerCase() === column.toLowerCase()) {
            return name;
        }
    }

    return column;
};

/**
 * Updates the "old" and existing properties. Should be called after the
 * model is updated or read from the database.
 *
 * @param  {Object} attributes
 * @return {Model}
 */
Model.prototype.sync = function (attributes) {
    this.isFromDb = true;

    var self = this;
    Object.keys(attributes).forEach(function (key) {
        var value = attributes[key];
        var name = self.fixCasing(key);

        if (self.accessored.indexOf(name) === -1) {
            self[name] = _.cloneDeep(value);
        }

        self.attributes[name] = _.cloneDeep(value);
        self.old[name] = _.cloneDeep(value);
    });

    return this;
};

/**
 * Extends this model's data with anything passed in the arguments. Functions
 * in the same way to lodash's "extend".
 * @return {Model}
 */
Model.prototype.extend = function () {
    var args = new Array(arguments.length + 1);
    var data = args[0] = {};

    var i, l;
    for (i = 0, l = arguments.length; i < l; i++) {
        args[i + 1] = arguments[i];
    }

    // Compact all the arguments to our single "data" argument
    _.extend.apply(null, args);

    // Then apply those over this model.
    for (i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i].toString();
        var item = data[column];
        if (typeof item !== 'undefined') {
            this[column] = item;
        }
    }

    return this;
};

/**
 * Iterates over the model's columns, typecasting each one and updating
 * the "attributes" for columns without getters/setters.
 */
Model.prototype.fixAttributes = function () {
    for (var i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i];
        var name = column.getName();
        var value;

        if (this.accessored.indexOf(name) !== -1) {
            value = this.attributes[name];
            this.attributes[name] = this.collection.typecast(column, value);
        } else {
            value = this.collection.typecast(column, this[name]);
            this.attributes[name] = value;
            this[name] = value;
        }
    }
};

/**
 * Returns whether the property has changed since it was synced to the
 * database.
 * @param  {String}  property
 * @return {Boolean}
 */
Model.prototype.isDirty = function (property) {
    this.fixAttributes();

    return !eq(this.attributes[property], this.old[property]);
};

/**
 * Returns true if every property is synced with the db. False if any
 * are different.
 *
 * @return {Boolean}
 */
Model.prototype.isSynced = function () {
    for (var i = 0, l = this.columns.length; i < l; i++) {
        if (this.isDirty(this.columns[i].toString())) {
            return false;
        }
    }

    return true;
};

/**
 * Converts the model to a plain object
 *
 * @param  {Boolean} useGetters
 * @return {Object}
 */
Model.prototype.toObject = function (useGetters) {
    this.fixAttributes();

    var output = {};
    for (var i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i].toString();
        if (typeof useGetters === 'undefined' || useGetters === true) {
            output[column] = this[column];
        } else {
            output[column] = this.attributes[column];
        }
    }

    return output;
};

/**
 * Converts the record to a JSON representation.
 *
 * @return {String}
 */
Model.prototype.toJson = function () {
    return JSON.stringify(this.toObject());
};

/**
 * Persists the model to the database.
 *
 * @param {Boolean=false} force
 * @return {Promise}
 */
Model.prototype.save = function (force) {
    if (!force && this.isSynced()) {
        return Bluebird.resolve(this);
    }

    this.fixAttributes();
    if (this.isFromDb) {
        return this.updateExisting();
    } else {
        return this.createNew();
    }
};

/**
 * Deletes this model's record from the database.
 *
 * @param {Boolean=false} force
 * @return {Promise}
 */
Model.prototype.delete = wrapAround('beforeDelete', function (force) {
    if (!this.isFromDb && !force) {
        return Bluebird.resolve();
    }

    return this.addWhereSelf(this.collection.delete()).execute();
}, 'afterDelete');

/**
 * Adds a "where" to the query so that it targets only this model.
 * @param {BaseQuery} query
 * @return {BaseQuery}
 */
Model.prototype.addWhereSelf = function (query) {
    var keys = this.collection.table.keys;
    var pks = keys.partition.concat(keys.compound);

    if (!pks.length) {
        throw new Error('A primary key is required on all models.');
    }

    for (var i = 0, l = pks.length; i < l; i++) {
        query.where(pks[i], '=', this.old[pks[i].toString()]);
    }

    return query;
};

/**
 * INSERTs the model into the database.
 *
 * @return {Promise}
 */
Model.prototype.createNew = wrapAround('beforeCreate', function () {
    var properties = this.toObject(false);
    var self = this;

    return this.collection.insert()
        .data(properties)
        .then(function () {
            self.sync(properties);
            return self;
        });
}, 'afterCreate');

/**
 * UPDATEs a record in the database with the changed properties.
 *
 * @return {Promise}
 */
Model.prototype.updateExisting = wrapAround('beforeUpdate', function () {
    var query = this.addWhereSelf(this.collection.update());
    var self = this;

    for (var i = 0, l = this.columns.length; i < l; i++) {
        var column = this.columns[i];
        var name = column.getName();

        diff(query, column, this.old[name], this.attributes[name]);
    }

    return query.then(function () {
        self.sync(self.toObject(false));
        return self;
    });
}, 'afterUpdate');

/**
 * Wraps the function so that the "before" and "after" middleware
 * is run "around" it.
 *
 * @param  {String}   before
 * @param  {Function} fn
 * @param  {String}   after
 * @return {Promise}
 */
function wrapAround (before, fn, after) {
    return function () {
        var self = this;
        return self.collection
            .run(before, self)
            .then(fn.bind(self))
            .then(function () {
                var resolved = Bluebird.resolve.apply(Bluebird, arguments);

                return self.collection.run(after, self).then(function () {
                    return resolved;
                });
            });
    };
}

module.exports = Model;
<|endoftext|>