<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import { generateUUID } from 'react-native-database';

import { formatDateAndTime } from '../../utilities';
import { NUMBER_SEQUENCE_KEYS } from './constants';
import { generalStrings } from '../../localization';

// Get the next highest number in an existing number sequence.
export const getNextNumber = (database, sequenceKey) => {
  const numberSequence = getNumberSequence(database, sequenceKey);
  const number = numberSequence.getNextNumber(database);
  database.save('NumberSequence', numberSequence);
  return String(number);
};

// Put a number back into a sequence for reuse.
export const reuseNumber = (database, sequenceKey, number) => {
  const numberSequence = getNumberSequence(database, sequenceKey);
  numberSequence.reuseNumber(database, parseInt(number, 10));
  database.save('NumberSequence', numberSequence);
};

export const getNumberSequence = (database, sequenceKey) => {
  const sequenceResults = database
    .objects('NumberSequence')
    .filtered('sequenceKey == $0', sequenceKey);
  if (sequenceResults.length > 1) {
    throw new Error(`More than one ${sequenceKey} sequence`);
  }
  if (sequenceResults.length <= 0) {
    return createRecord(database, 'NumberSequence', sequenceKey);
  }
  return sequenceResults[0];
};

/**
 * Create a customer invoice associated with a given customer.
 *
 * @param   {Realm}        database
 * @param   {Name}         customer  Customer associated with invoice.
 * @return  {Transaction}
 */
const createCustomerInvoice = (database, customer, user) => {
  const { CUSTOMER_INVOICE_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const currentDate = new Date();
  const invoice = database.create('Transaction', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, CUSTOMER_INVOICE_NUMBER),
    entryDate: currentDate,
    confirmDate: currentDate,
    type: 'customer_invoice',
    // Mobile customer invoices are always 'confirmed' for easy stock tracking.
    status: 'confirmed',
    comment: '',
    otherParty: customer,
    enteredBy: user,
  });

  database.save('Transaction', invoice);
  customer.addTransaction(invoice);
  database.save('Name', customer);

  return invoice;
};

/**
 * Create a new number sequence.
 *
 * @param   {Realm}           database
 * @param   {string}          sequenceKey
 * @return  {NumberSequence}
 */
const createNumberSequence = (database, sequenceKey) =>
  database.create('NumberSequence', {
    id: generateUUID(),
    sequenceKey,
  });

/**
 * Create a number attached to a sequence.
 *
 * @param  {Realm}           database
 * @param  {NumberSequence}  numberSequence  Sequence to attach the number to.
 * @param  {number}          number
 */
const createNumberToReuse = (database, numberSequence, number) => {
  const numberToReuse = database.create('NumberToReuse', {
    id: generateUUID(),
    numberSequence,
    number,
  });

  numberSequence.addNumberToReuse(numberToReuse);
};

/**
 * Create a transaction representing an inventory adjustment.
 *
 * @param   {Realm}        database    Local database.
 * @param   {Name}         user        Currently logged in user.
 * @param   {Date}         date        Current date.
 * @param   {bool}         isAddition  If true, adjustment is an increase,
 *                                     else, adjustment is a decrease.
 * @return  {Transaction}
 */
const createInventoryAdjustment = (database, user, date, isAddition) => {
  const { INVENTORY_ADJUSTMENT_SERIAL_NUMBER } = NUMBER_SEQUENCE_KEYS;
  return database.create('Transaction', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, INVENTORY_ADJUSTMENT_SERIAL_NUMBER),
    entryDate: date,
    confirmDate: date,
    type: isAddition ? 'supplier_invoice' : 'supplier_credit',
    status: 'confirmed',
    comment: '',
    enteredBy: user,
    otherParty: database.objects('Name').find(name => name.type === 'inventory_adjustment'),
  });
};

/**
 * Create a new empty batch for a given item.
 *
 * @param  {Realm}      database
 * @param  {Item}       item         Item assocated with new batch.
 * @param  {string}     batchString
 * @return {ItemBatch}
 */
const createItemBatch = (database, item, batchString) => {
  // Handle cross-reference items.
  const { realItem } = item;

  const itemBatch = database.create('ItemBatch', {
    id: generateUUID(),
    item: realItem,
    batch: batchString,
    packSize: 1,
    numberOfPacks: 0,
    costPrice: realItem.defaultPrice ? realItem.defaultPrice : 0,
    sellPrice: realItem.defaultPrice ? realItem.defaultPrice : 0,
  });

  realItem.addBatch(itemBatch);
  database.save('Item', realItem);

  return itemBatch;
};

/**
 * Create a new requisition.
 *
 * @param   {Realm}        database
 * @param   {User}         user            User creating requisition.
 * @param   {Name}         values?  TODO: this param+description is wrong
 * @return  {Requisition}
 */
const createRequisition = (
  database,
  user,
  { otherStoreName, program, period, orderType = {}, monthsLeadTime = 0 }
) => {
  const { REQUISITION_REQUESTER_REFERENCE, REQUISITION_SERIAL_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const { name: orderTypeName, maxMOS, thresholdMOS } = orderType || {};
  const regimenData =
    program && program.parsedProgramSettings ? program.parsedProgramSettings.regimenData : null;
  const daysToSupply = ((monthsLeadTime || 0) + (maxMOS || 1)) * 30;

  const requisition = database.create('Requisition', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, REQUISITION_SERIAL_NUMBER),
    requesterReference: getNextNumber(database, REQUISITION_REQUESTER_REFERENCE),
    status: 'suggested',
    type: 'request',
    entryDate: new Date(),
    daysToSupply,
    enteredBy: user,
    otherStoreName,
    program,
    orderType: orderTypeName,
    thresholdMOS,
    period,
    customData: regimenData && JSON.stringify({ regimenData }),
  });

  if (period) {
    period.addRequisitionIfUnique(requisition);
    database.save('Period', period);
  }

  return requisition;
};

/**
 * Create a new requisition item.
 *
 * @param   {Database}        database
 * @param   {Requisition}     requisition  Parent requisition.
 * @param   {Item}            item         Item to add to requisition.
 * @param   {double}          dailyUsage   Daily usage of item.
 * @return  {RequisitionItem}
 */
const createRequisitionItem = (database, requisition, item, dailyUsage) => {
  // Handle cross reference items.
  const { realItem } = item;

  const requisitionItem = database.create('RequisitionItem', {
    id: generateUUID(),
    item: realItem,
    requisition,
    stockOnHand: realItem.totalQuantity,
    dailyUsage: dailyUsage || realItem.dailyUsage,
    requiredQuantity: 0,
    comment: '',
    sortIndex: requisition.items.length + 1,
  });

  requisition.addItem(requisitionItem);
  database.save('Requisition', requisition);

  return requisitionItem;
};

/**
 * Create a new stocktake.
 *
 * @param   {Realm}      database
 * @param   {User}       user             User creating the stocktake.
 * @param   {string}     stocktakeName    What to name the stocktake
 * @param   {Program}    program          Program record to associate with the stocktake
 * @return  {Stocktake}
 */
const createStocktake = (database, user, stocktakeName, program) => {
  const { STOCKTAKE_SERIAL_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const date = new Date();
  const serialNumber = getNextNumber(database, STOCKTAKE_SERIAL_NUMBER);
  const title = program ? program.name : generalStrings.stocktake;
  const defaultName = `${title} - ${formatDateAndTime(date, 'slashes')}`;
  return database.create('Stocktake', {
    id: generateUUID(),
    serialNumber,
    name: stocktakeName || defaultName,
    createdDate: date,
    stocktakeDate: date,
    status: 'suggested',
    comment: '',
    createdBy: user,
    program,
  });
};

/**
 * Create a new stocktake item.
 *
 * @param   {Realm}          database
 * @param   {Stocktake}      stocktake  Parent stocktake.
 * @param   {Item}           item       Real item to create stocktake item from.
 * @return  {StocktakeItem}
 */
const createStocktakeItem = (database, stocktake, item) => {
  // Handle cross reference items.
  const { realItem } = item;

  const stocktakeItem = database.create('StocktakeItem', {
    id: generateUUID(),
    item: realItem,
    stocktake,
  });

  stocktake.items.push(stocktakeItem);
  database.save('Stocktake', stocktake);

  return stocktakeItem;
};

/**
 * Create a new stocktake batch.
 *
 * @param   {Realm}          database
 * @param   {Item}           stocktakeItem  Batch item.
 * @param   {ItemBatch}      itemBatch      Item batch to associate with stocktake batch.
 * @return  {StocktakeBatch}
 */
const createStocktakeBatch = (database, stocktakeItem, itemBatch) => {
  const { numberOfPacks, packSize, expiryDate, batch, costPrice, sellPrice } = itemBatch;

  const stocktakeBatch = database.create('StocktakeBatch', {
    id: generateUUID(),
    stocktake: stocktakeItem.stocktake,
    itemBatch,
    snapshotNumberOfPacks: numberOfPacks,
    packSize,
    expiryDate,
    batch,
    costPrice,
    sellPrice,
    sortIndex: (stocktakeItem?.stocktake?.numberOfBatches || 0) + 1 || 1,
  });

  stocktakeItem.addBatch(stocktakeBatch);
  database.save('StocktakeItem', stocktakeItem);

  return stocktakeBatch;
};

/**
 * Create a new supplier invoice.
 *
 * @param   {Realm}       database
 * @param   {Name}        supplier   Name of supplier being invoiced.
 * @param   {User}        enteredBy  User who creating invoice.
 * @return  {Transaction}
 */
const createSupplierInvoice = (database, supplier, user) => {
  const { SUPPLIER_INVOICE_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const currentDate = new Date();

  const invoice = database.create('Transaction', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, SUPPLIER_INVOICE_NUMBER),
    entryDate: currentDate,
    type: 'supplier_invoice',
    status: 'new',
    comment: '',
    otherParty: supplier,
    enteredBy: user,
  });

  database.save('Transaction', invoice);
  supplier.addTransaction(invoice);

  return invoice;
};

/**
 * Create a new transaction batch.
 *
 * @param   {Realm}             database
 * @param   {TransactionItem}   transactionItem  Batch item.
 * @param   {ItemBatch}         itemBatch        Item batch to associate with transaction batch.
 * @return  {TransactionBatch}
 */
const createTransactionBatch = (database, transactionItem, itemBatch) => {
  const { item, batch, expiryDate, packSize, costPrice, sellPrice, donor } = itemBatch;

  const transactionBatch = database.create('TransactionBatch', {
    id: generateUUID(),
    itemId: item.id,
    itemName: item.name,
    itemBatch,
    batch,
    expiryDate,
    packSize,
    numberOfPacks: 0,
    costPrice,
    sellPrice,
    donor,
    transaction: transactionItem.transaction,
    sortIndex: (transactionItem?.transaction?.numberOfBatches || 0) + 1 || 1,
  });

  transactionItem.addBatch(transactionBatch);
  database.save('TransactionItem', transactionItem);
  itemBatch.addTransactionBatch(transactionBatch);
  database.save('ItemBatch', itemBatch);

  return transactionBatch;
};

/**
 * Create a new transaction item.
 *
 * @param   {Realm}            database
 * @param   {Transaction}      transaction  Parent transaction.
 * @param   {Item}             item         Real item to create transaction item from.
 * @return  {TransactionItem}
 */
const createTransactionItem = (database, transaction, item) => {
  // Handle cross reference items.
  const { realItem } = item;

  const transactionItem = database.create('TransactionItem', {
    id: generateUUID(),
    item: realItem,
    transaction,
  });

  transaction.addItem(transactionItem);
  database.save('Transaction', transaction);

  return transactionItem;
};

/**
 * Create a record of the given type, taking care of linkages, generating IDs, serial
 * numbers, current dates, and inserting sensible defaults.
 *
 * @param   {Realm}         database
 * @param   {string}        type      The type of record to create (not 1:1 with schema types).
 * @param   {args}          ...args   Any specific arguments the record type will need.
 * @return  {Realm.Object}            The new database record.
 */
export const createRecord = (database, type, ...args) => {
  switch (type) {
    case 'CustomerInvoice':
      return createCustomerInvoice(database, ...args);
    case 'NumberSequence':
      return createNumberSequence(database, ...args);
    case 'NumberToReuse':
      return createNumberToReuse(database, ...args);
    case 'ItemBatch':
      return createItemBatch(database, ...args);
    case 'Requisition':
      return createRequisition(database, ...args);
    case 'RequisitionItem':
      return createRequisitionItem(database, ...args);
    case 'Stocktake':
      return createStocktake(database, ...args);
    case 'StocktakeItem':
      return createStocktakeItem(database, ...args);
    case 'StocktakeBatch':
      return createStocktakeBatch(database, ...args);
    case 'SupplierInvoice':
      return createSupplierInvoice(database, ...args);
    case 'InventoryAdjustment':
      return createInventoryAdjustment(database, ...args);
    case 'TransactionItem':
      return createTransactionItem(database, ...args);
    case 'TransactionBatch':
      return createTransactionBatch(database, ...args);
    default:
      throw new Error(`Cannot create a record with unsupported type: ${type}`);
  }
};

export default createRecord;
<commit_msg>Add createRecord for Message<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import { generateUUID } from 'react-native-database';

import { formatDateAndTime } from '../../utilities';
import { NUMBER_SEQUENCE_KEYS } from './constants';
import { generalStrings } from '../../localization';
import { SETTINGS_KEYS } from '../../settings/index';

// Get the next highest number in an existing number sequence.
export const getNextNumber = (database, sequenceKey) => {
  const numberSequence = getNumberSequence(database, sequenceKey);
  const number = numberSequence.getNextNumber(database);
  database.save('NumberSequence', numberSequence);
  return String(number);
};

// Put a number back into a sequence for reuse.
export const reuseNumber = (database, sequenceKey, number) => {
  const numberSequence = getNumberSequence(database, sequenceKey);
  numberSequence.reuseNumber(database, parseInt(number, 10));
  database.save('NumberSequence', numberSequence);
};

export const getNumberSequence = (database, sequenceKey) => {
  const sequenceResults = database
    .objects('NumberSequence')
    .filtered('sequenceKey == $0', sequenceKey);
  if (sequenceResults.length > 1) {
    throw new Error(`More than one ${sequenceKey} sequence`);
  }
  if (sequenceResults.length <= 0) {
    return createRecord(database, 'NumberSequence', sequenceKey);
  }
  return sequenceResults[0];
};

/**
 * Create a customer invoice associated with a given customer.
 *
 * @param   {Realm}        database
 * @param   {Name}         customer  Customer associated with invoice.
 * @return  {Transaction}
 */
const createCustomerInvoice = (database, customer, user) => {
  const { CUSTOMER_INVOICE_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const currentDate = new Date();
  const invoice = database.create('Transaction', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, CUSTOMER_INVOICE_NUMBER),
    entryDate: currentDate,
    confirmDate: currentDate,
    type: 'customer_invoice',
    // Mobile customer invoices are always 'confirmed' for easy stock tracking.
    status: 'confirmed',
    comment: '',
    otherParty: customer,
    enteredBy: user,
  });

  database.save('Transaction', invoice);
  customer.addTransaction(invoice);
  database.save('Name', customer);

  return invoice;
};

/**
 * Create a new number sequence.
 *
 * @param   {Realm}           database
 * @param   {string}          sequenceKey
 * @return  {NumberSequence}
 */
const createNumberSequence = (database, sequenceKey) =>
  database.create('NumberSequence', {
    id: generateUUID(),
    sequenceKey,
  });

/**
 * Create a number attached to a sequence.
 *
 * @param  {Realm}           database
 * @param  {NumberSequence}  numberSequence  Sequence to attach the number to.
 * @param  {number}          number
 */
const createNumberToReuse = (database, numberSequence, number) => {
  const numberToReuse = database.create('NumberToReuse', {
    id: generateUUID(),
    numberSequence,
    number,
  });

  numberSequence.addNumberToReuse(numberToReuse);
};

/**
 * Create a transaction representing an inventory adjustment.
 *
 * @param   {Realm}        database    Local database.
 * @param   {Name}         user        Currently logged in user.
 * @param   {Date}         date        Current date.
 * @param   {bool}         isAddition  If true, adjustment is an increase,
 *                                     else, adjustment is a decrease.
 * @return  {Transaction}
 */
const createInventoryAdjustment = (database, user, date, isAddition) => {
  const { INVENTORY_ADJUSTMENT_SERIAL_NUMBER } = NUMBER_SEQUENCE_KEYS;
  return database.create('Transaction', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, INVENTORY_ADJUSTMENT_SERIAL_NUMBER),
    entryDate: date,
    confirmDate: date,
    type: isAddition ? 'supplier_invoice' : 'supplier_credit',
    status: 'confirmed',
    comment: '',
    enteredBy: user,
    otherParty: database.objects('Name').find(name => name.type === 'inventory_adjustment'),
  });
};

/**
 * Create a new empty batch for a given item.
 *
 * @param  {Realm}      database
 * @param  {Item}       item         Item assocated with new batch.
 * @param  {string}     batchString
 * @return {ItemBatch}
 */
const createItemBatch = (database, item, batchString) => {
  // Handle cross-reference items.
  const { realItem } = item;

  const itemBatch = database.create('ItemBatch', {
    id: generateUUID(),
    item: realItem,
    batch: batchString,
    packSize: 1,
    numberOfPacks: 0,
    costPrice: realItem.defaultPrice ? realItem.defaultPrice : 0,
    sellPrice: realItem.defaultPrice ? realItem.defaultPrice : 0,
  });

  realItem.addBatch(itemBatch);
  database.save('Item', realItem);

  return itemBatch;
};

/**
 * Create a new requisition.
 *
 * @param   {Realm}        database
 * @param   {User}         user            User creating requisition.
 * @param   {Name}         values?  TODO: this param+description is wrong
 * @return  {Requisition}
 */
const createRequisition = (
  database,
  user,
  { otherStoreName, program, period, orderType = {}, monthsLeadTime = 0 }
) => {
  const { REQUISITION_REQUESTER_REFERENCE, REQUISITION_SERIAL_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const { name: orderTypeName, maxMOS, thresholdMOS } = orderType || {};
  const regimenData =
    program && program.parsedProgramSettings ? program.parsedProgramSettings.regimenData : null;
  const daysToSupply = ((monthsLeadTime || 0) + (maxMOS || 1)) * 30;

  const requisition = database.create('Requisition', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, REQUISITION_SERIAL_NUMBER),
    requesterReference: getNextNumber(database, REQUISITION_REQUESTER_REFERENCE),
    status: 'suggested',
    type: 'request',
    entryDate: new Date(),
    daysToSupply,
    enteredBy: user,
    otherStoreName,
    program,
    orderType: orderTypeName,
    thresholdMOS,
    period,
    customData: regimenData && JSON.stringify({ regimenData }),
  });

  if (period) {
    period.addRequisitionIfUnique(requisition);
    database.save('Period', period);
  }

  return requisition;
};

/**
 * Create a new requisition item.
 *
 * @param   {Database}        database
 * @param   {Requisition}     requisition  Parent requisition.
 * @param   {Item}            item         Item to add to requisition.
 * @param   {double}          dailyUsage   Daily usage of item.
 * @return  {RequisitionItem}
 */
const createRequisitionItem = (database, requisition, item, dailyUsage) => {
  // Handle cross reference items.
  const { realItem } = item;

  const requisitionItem = database.create('RequisitionItem', {
    id: generateUUID(),
    item: realItem,
    requisition,
    stockOnHand: realItem.totalQuantity,
    dailyUsage: dailyUsage || realItem.dailyUsage,
    requiredQuantity: 0,
    comment: '',
    sortIndex: requisition.items.length + 1,
  });

  requisition.addItem(requisitionItem);
  database.save('Requisition', requisition);

  return requisitionItem;
};

/**
 * Create a new stocktake.
 *
 * @param   {Realm}      database
 * @param   {User}       user             User creating the stocktake.
 * @param   {string}     stocktakeName    What to name the stocktake
 * @param   {Program}    program          Program record to associate with the stocktake
 * @return  {Stocktake}
 */
const createStocktake = (database, user, stocktakeName, program) => {
  const { STOCKTAKE_SERIAL_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const date = new Date();
  const serialNumber = getNextNumber(database, STOCKTAKE_SERIAL_NUMBER);
  const title = program ? program.name : generalStrings.stocktake;
  const defaultName = `${title} - ${formatDateAndTime(date, 'slashes')}`;
  return database.create('Stocktake', {
    id: generateUUID(),
    serialNumber,
    name: stocktakeName || defaultName,
    createdDate: date,
    stocktakeDate: date,
    status: 'suggested',
    comment: '',
    createdBy: user,
    program,
  });
};

/**
 * Create a new stocktake item.
 *
 * @param   {Realm}          database
 * @param   {Stocktake}      stocktake  Parent stocktake.
 * @param   {Item}           item       Real item to create stocktake item from.
 * @return  {StocktakeItem}
 */
const createStocktakeItem = (database, stocktake, item) => {
  // Handle cross reference items.
  const { realItem } = item;

  const stocktakeItem = database.create('StocktakeItem', {
    id: generateUUID(),
    item: realItem,
    stocktake,
  });

  stocktake.items.push(stocktakeItem);
  database.save('Stocktake', stocktake);

  return stocktakeItem;
};

/**
 * Create a new stocktake batch.
 *
 * @param   {Realm}          database
 * @param   {Item}           stocktakeItem  Batch item.
 * @param   {ItemBatch}      itemBatch      Item batch to associate with stocktake batch.
 * @return  {StocktakeBatch}
 */
const createStocktakeBatch = (database, stocktakeItem, itemBatch) => {
  const { numberOfPacks, packSize, expiryDate, batch, costPrice, sellPrice } = itemBatch;

  const stocktakeBatch = database.create('StocktakeBatch', {
    id: generateUUID(),
    stocktake: stocktakeItem.stocktake,
    itemBatch,
    snapshotNumberOfPacks: numberOfPacks,
    packSize,
    expiryDate,
    batch,
    costPrice,
    sellPrice,
    sortIndex: (stocktakeItem?.stocktake?.numberOfBatches || 0) + 1 || 1,
  });

  stocktakeItem.addBatch(stocktakeBatch);
  database.save('StocktakeItem', stocktakeItem);

  return stocktakeBatch;
};

/**
 * Create a new supplier invoice.
 *
 * @param   {Realm}       database
 * @param   {Name}        supplier   Name of supplier being invoiced.
 * @param   {User}        enteredBy  User who creating invoice.
 * @return  {Transaction}
 */
const createSupplierInvoice = (database, supplier, user) => {
  const { SUPPLIER_INVOICE_NUMBER } = NUMBER_SEQUENCE_KEYS;
  const currentDate = new Date();

  const invoice = database.create('Transaction', {
    id: generateUUID(),
    serialNumber: getNextNumber(database, SUPPLIER_INVOICE_NUMBER),
    entryDate: currentDate,
    type: 'supplier_invoice',
    status: 'new',
    comment: '',
    otherParty: supplier,
    enteredBy: user,
  });

  database.save('Transaction', invoice);
  supplier.addTransaction(invoice);

  return invoice;
};

/**
 * Create a new transaction batch.
 *
 * @param   {Realm}             database
 * @param   {TransactionItem}   transactionItem  Batch item.
 * @param   {ItemBatch}         itemBatch        Item batch to associate with transaction batch.
 * @return  {TransactionBatch}
 */
const createTransactionBatch = (database, transactionItem, itemBatch) => {
  const { item, batch, expiryDate, packSize, costPrice, sellPrice, donor } = itemBatch;

  const transactionBatch = database.create('TransactionBatch', {
    id: generateUUID(),
    itemId: item.id,
    itemName: item.name,
    itemBatch,
    batch,
    expiryDate,
    packSize,
    numberOfPacks: 0,
    costPrice,
    sellPrice,
    donor,
    transaction: transactionItem.transaction,
    sortIndex: (transactionItem?.transaction?.numberOfBatches || 0) + 1 || 1,
  });

  transactionItem.addBatch(transactionBatch);
  database.save('TransactionItem', transactionItem);
  itemBatch.addTransactionBatch(transactionBatch);
  database.save('ItemBatch', itemBatch);

  return transactionBatch;
};

/**
 * Create a new transaction item.
 *
 * @param   {Realm}            database
 * @param   {Transaction}      transaction  Parent transaction.
 * @param   {Item}             item         Real item to create transaction item from.
 * @return  {TransactionItem}
 */
const createTransactionItem = (database, transaction, item) => {
  // Handle cross reference items.
  const { realItem } = item;

  const transactionItem = database.create('TransactionItem', {
    id: generateUUID(),
    item: realItem,
    transaction,
  });

  transaction.addItem(transactionItem);
  database.save('Transaction', transaction);

  return transactionItem;
};

/**
 * Create a Message record. This will be sent to the server and requests tables
 * when an app is upgraded from some version to another.
 *
 * @param {Realm}  database    App-wide database interface
 * @param {String} fromVersion Which version the app is being upgraded from.
 * @param {String} toVersion   Which version the app is being upgraded too.
 */

const createMessage = (database, fromVersion, toVersion) => {
  const syncSiteId = database.getSetting(SETTINGS_KEYS.SYNC_SITE_ID);

  const body = {
    fromVersion: String(fromVersion),
    toVersion: String(toVersion),
    syncSiteId: Number(syncSiteId),
  };

  const message = database.create('Message', {
    id: generateUUID(),
    type: 'mobile_upgrade',
  });

  message.body = body;

  database.save('Message', message);

  return message;
};

/**
 * Create a record of the given type, taking care of linkages, generating IDs, serial
 * numbers, current dates, and inserting sensible defaults.
 *
 * @param   {Realm}         database
 * @param   {string}        type      The type of record to create (not 1:1 with schema types).
 * @param   {args}          ...args   Any specific arguments the record type will need.
 * @return  {Realm.Object}            The new database record.
 */
export const createRecord = (database, type, ...args) => {
  switch (type) {
    case 'CustomerInvoice':
      return createCustomerInvoice(database, ...args);
    case 'NumberSequence':
      return createNumberSequence(database, ...args);
    case 'NumberToReuse':
      return createNumberToReuse(database, ...args);
    case 'ItemBatch':
      return createItemBatch(database, ...args);
    case 'Requisition':
      return createRequisition(database, ...args);
    case 'RequisitionItem':
      return createRequisitionItem(database, ...args);
    case 'Stocktake':
      return createStocktake(database, ...args);
    case 'StocktakeItem':
      return createStocktakeItem(database, ...args);
    case 'StocktakeBatch':
      return createStocktakeBatch(database, ...args);
    case 'SupplierInvoice':
      return createSupplierInvoice(database, ...args);
    case 'InventoryAdjustment':
      return createInventoryAdjustment(database, ...args);
    case 'TransactionItem':
      return createTransactionItem(database, ...args);
    case 'TransactionBatch':
      return createTransactionBatch(database, ...args);
    case 'Message':
      return createMessage(database, ...args);
    default:
      throw new Error(`Cannot create a record with unsupported type: ${type}`);
  }
};

export default createRecord;
<|endoftext|>


<commit_before>  console.log("ayy lmao");
define(['threejs/build/three'], function (THREE) {
  var scene, renderer;

  function _drawBox(){
    var geometry = new THREE.BoxGeometry(200, 200, 200);
    var material = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true});

    var urls = [
    "Assets/desert_lf.png",
    "Assets/desert_rt.png",
    "Assets/desert_up.png",
    "Assets/desert_dn.png",
    "Assets/desert_bk.png",
    "Assets/desert_ft.png"
    ];

    var cubemap = THREE.ImageUtils.loadTextureCube(urls); // load textures
    cubemap.format = THREE.RGBFormat;

    var shader = THREE.ShaderLib['cube']; // init cube shader from built-in lib
    shader.uniforms['tCube'].value = cubemap; // apply textures to shader

    // create shader material
    var skyBoxMaterial = new THREE.ShaderMaterial( {
      fragmentShader: shader.fragmentShader,
      vertexShader: shader.vertexShader,
      uniforms: shader.uniforms,
      depthWrite: false,
      side: THREE.BackSide
    });

    // create skybox mesh
    var skybox = new THREE.Mesh(
      new THREE.CubeGeometry(10000, 10000, 10000),
      skyBoxMaterial
    );

    scene.add(skybox);
    var cubemap = THREE.ImageUtils.loadTextureCube(urls); // load textures
    cubemap.format = THREE.RGBFormat;
    var material = new THREE.MeshLambertMaterial({
      color: 0xffffff,
      envMap: cubemap
    });
    return mesh = new THREE.Mesh(geometry, material);

  }
  function _init() {

    console.log("ayy lmao");
    scene = new THREE.Scene();

    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
    camera.position.z = 1000;

    _drawBox();

    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);

    document.body.appendChild(renderer.domElement);

  }

  function _animate() {
    requestAnimationFrame(_animate);

    camera.rotation.x += 0.001;
    camera.rotation.y += 0.002;

    renderer.render(scene, camera);
  }

  function run(){
    _init();
    _animate();
  }

  return {
    run : run
  };
});
<commit_msg>Reordered skybox<commit_after>define(['threejs/build/three'], function (THREE) {
  var scene, renderer;

  function _drawBox(){
    var geometry = new THREE.BoxGeometry(200, 200, 200);
    var material = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true});

    var urls = [
    "Assets/desert_rt.png",
    "Assets/desert_lf.png",
    "Assets/desert_up.png",
    "Assets/desert_dn.png",
    "Assets/desert_bk.png",
    "Assets/desert_ft.png"
    ];

    var cubemap = THREE.ImageUtils.loadTextureCube(urls); // load textures
    cubemap.format = THREE.RGBFormat;

    var shader = THREE.ShaderLib['cube']; // init cube shader from built-in lib
    shader.uniforms['tCube'].value = cubemap; // apply textures to shader

    // create shader material
    var skyBoxMaterial = new THREE.ShaderMaterial( {
      fragmentShader: shader.fragmentShader,
      vertexShader: shader.vertexShader,
      uniforms: shader.uniforms,
      depthWrite: false,
      side: THREE.BackSide
    });

    // create skybox mesh
    var skybox = new THREE.Mesh(
      new THREE.CubeGeometry(10000, 10000, 10000),
      skyBoxMaterial
    );

    scene.add(skybox);
    var cubemap = THREE.ImageUtils.loadTextureCube(urls); // load textures
    cubemap.format = THREE.RGBFormat;
    var material = new THREE.MeshLambertMaterial({
      color: 0xffffff,
      envMap: cubemap
    });
  }
  function _init() {

    console.log("ayy lmao");
    scene = new THREE.Scene();

    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
    camera.position.z = 1000;

    _drawBox();

    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);

    document.body.appendChild(renderer.domElement);

  }

  function _animate() {
    requestAnimationFrame(_animate);

    //camera.rotation.x += 0.001;
   // camera.rotation.y += 0.002;
    camera.rotation.z += 0.002;

    renderer.render(scene, camera);
  }

  function run(){
    _init();
    _animate();
  }

  return {
    run : run
  };
});
<|endoftext|>


<commit_before>/* Web Gremlin
 * Tartan Hacks 2015
 * CMU
 * Carson, Adam, Billy and Spencer
 */

//---------------------------------------------
// Gremlin engine
//---------------------------------------------

function WebGremlin() {

    this.AE = new AnimationEngine();

    this.start = function() {
        console.log('Your web gremlin is awake');
        this.AE.animate(this.AE.MOVEMENT);
    };

};

//---------------------------------------------
// Animation engine
//---------------------------------------------

function AnimationEngine() {
    // Defined animations
    this.MOVEMENT = "ANIMATION_MOVE";

    // TODO create div for sprite, add sprite to body
    // TODO animate sprite (per animation)

    //----------- Animations -----------------------

    // Animation object used to play animation
    this.animate = function(animation) {

        // Case on animation type
        switch(animation.type) {
            case this.MOVEMENT:
                this.runMove(animation);
                break;
            default:
                console.log('Unrecognized animation type [' +
                    animation.type + ']');
                break;
        }
    };

    // In place animation
    this.runInPlace = function(animation) {
        // TODO
    };

    // Move across screen in straight line
    this.runMove = function(animation) {
        console.log(MOVEMENT);
    };

}

//---------------------------------------------
// Run
//---------------------------------------------

var webGremlin = new WebGremlin();
webGremlin.start();
<commit_msg>Random action engine working<commit_after>/* Web Gremlin
 * Tartan Hacks 2015
 * CMU
 * Carson, Adam, Billy and Spencer
 */

//---------------------------------------------
// Gremlin engine
//---------------------------------------------

function WebGremlin() {

    // Constants
    this.MAX_DELAY = 1000;

    this.AE = new AnimationEngine();

    // Perform actions randomly
    this.start = function() {
        console.log('Your web gremlin is awake');
        console.log(this.AE);

        var delayMs = Math.floor(Math.random() * this.MAX_DELAY);
        setTimeout(function() {
            this.AE.animate({'type':this.AE.MOVEMENT});
        }.bind(this), delayMs)

    };

};

//---------------------------------------------
// Animation engine
//---------------------------------------------

function AnimationEngine() {
    // Defined animations
    this.MOVEMENT = "ANIMATION_MOVE";

    // TODO create div for sprite, add sprite to body
    // TODO animate sprite (per animation)

    //----------- Animations -----------------------

    // Animation object used to play animation
    this.animate = function(animation) {

        // Case on animation type
        switch(animation.type) {
            case this.MOVEMENT:
                this.runMove(animation);
                break;
            default:
                console.log('Unrecognized animation type [' +
                    animation.type + ']');
                break;
        }
    };

    // In place animation
    this.runInPlace = function(animation) {
        // TODO
    };

    // Move across screen in straight line
    this.runMove = function(animation) {
        console.log(this.MOVEMENT);
    };

}

//---------------------------------------------
// Run
//---------------------------------------------

var webGremlin = new WebGremlin();
webGremlin.start();
<|endoftext|>


<commit_before>import DOM from "../dom/floatz.dom.dom.js";
import Easing from "../animation/floatz.animation.easing.js"
import {DOMElement} from "../dom/floatz.dom.dom.js";
import {EVENT_SCROLL} from "../dom/floatz.dom.events.js";

/**
 * Notes:
 * ------
 * document.body.scrollTop / scrollLeft
 * containerElement.scrollTop / scrollLeft
 * childelement.offsetTop / offsetLeft => Position relative to document (fixed)
 * childelement.getClientBoundingRect.top / left => Position relative to _container (variable)
 */

// Constants for events
// Note: Symbols can´t be used because closure compiler will change names
export const SCROLL_EVENT_BEFORENAVGIATE = "flz-event-before-navigate";
export const SCROLL_EVENT_AFTERNAVGIATE = "flz-event-after-navigate";

/**
 * Scroll direction enum.
 *
 * @type {Object}
 */
export const Direction = Object.freeze({
	HORIZONTAL: Symbol("horizontal"),
	VERTICAL: Symbol("vertical")
});

/**
 * Scroll manager.
 */
export class Scroller {
	/**
	 * Constructor.
	 *
	 * @param {?(DOMElement|Object|string)} container Scroll container (default is window)
	 * @param {?Object} options Scroll container options
	 */
	constructor(container = window, options = {}) {
		this._options = options;
		this._options.direction = options.direction || Direction.VERTICAL;
		this._options.offset = options.offset || 0;
		this._plugins = [];
		this._handlers = [];
		this._scrollHandler = null;
		this._container = container;
		this._scrolling = false;

		if (DOM.isWindow(container)) {
			// Note: document.body does not work since Chrome 61
			this._options.scrollable = document.scrollingElement || document.documentElement;
		} else if (container instanceof DOMElement) {
			this._container = container.origNode();
			this._options.scrollable = this._container;
		} else {
			this._options.scrollable = this._container;
		}
		this._prevScrollPos = this.scrollPos();
	}

	/**
	 * Inject scroll plugin.
	 *
	 * @param plugin {ScrollPlugin} Scroll plugin
	 * @returns {Scroller} Scroller for chaining
	 */
	plugin(plugin) {
		if (!(plugin instanceof ScrollPlugin)) {
			throw "Plugin must extend class ScrollPlugin";
		}

		plugin.scroller(this);
		this._plugins.push(plugin);
		this.onScroll(() => {
			plugin.onScroll(this);
		});
		this.onScrollBackward(() => {
			plugin.onScrollBackward(this);
		});
		this.onScrollForward(() => {
			plugin.onScrollForward(this);
		});
		return this;
	}

	/**
	 * Get scroll container.
	 *
	 * @returns {Object} Scroll container
	 */
	container() {
		return this._container;
	}

	/**
	 * Get scroll options.
	 *
	 * @returns Scroll options
	 */
	options() {
		return this._options;
	}

	/**
	 * Get / set scroll offset correction.
	 *
	 * @param {number=} offset Scroll offset correction
	 * @returns Scroller for chaining when used as setter
	 */
	offset(offset) {
		if (offset === undefined) {
			return this._options.offset;
		} else {
			this._options.offset = offset;
			return this;
		}
	}

	/**
	 * Scroll handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScroll(handler) {
		if(! this._scrollHandler) {
			this._scrollHandler = () => {
				// Adjust events to maximum of 60fps
				if (!this._scrolling) {
					window.requestAnimationFrame(() => {
						this._handlers.forEach((handler) => {
							handler(this);
						});

						// Set new position AFTER firing handlers!
						this._prevScrollPos = this.scrollPos();
						this._scrolling = false;
					});
					this._scrolling = true;
				}
			};
			DOM.addEvent(this._container, EVENT_SCROLL, this._scrollHandler);
		}

		this._handlers.push(() => {
			handler(this);
		});
		return this;
	}

	/**
	 * Scroll forward handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScrollForward(handler) {
		this._handlers.push(() => {
			if (this._prevScrollPos < this.scrollPos()) {
				handler(this);
			}
		});
		return this;
	}

	/**
	 * Scroll backward handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScrollBackward(handler) {
		this._handlers.push(() => {
			if (this._prevScrollPos > this.scrollPos()) {
				handler(this);
			}
		});
		return this;
	}

	/**
	 * Scroll to
	 * @param {(number|Object|string)} target Target element or position
	 * @param {Object=} options Scroll options
	 * @returns {Scroller} Scroller for chaining
	 */
	scrollTo(target, options = {}) {
		this._options.duration = options.duration || 600;
		this._options.easing = options.easing || Easing.easeInOutQuad;
		this._options.complete = options.complete || null;
		new ScrollAnimation(this._container, target, this._options);
		return this;
	}

	/**
	 * Get scroll direction configure via the constructor.
	 *
	 * @return {Object} direction Scroll Direction
	 */
	direction() {
		return this._options.direction;
	}

	/**
	 * Get scroll position.
	 *
	 * @param {number=} position Optional scroll position
	 * @returns {number|Scroller} Scroll position in px or scroller for chaining if used as setter
	 */
	scrollPos(position) {
		if (position) {
			if (this.direction() === Direction.VERTICAL) {
				this._options.scrollable.scrollTop = position;
			} else {
				this._options.scrollable.scrollLeft = position;
			}
			return this;
		} else {
			return this.direction() === Direction.VERTICAL ? this._options.scrollable.scrollTop : this._options.scrollable.scrollLeft;
		}
	}

	/**
	 * Get previous scroll position.
	 *
	 * @returns {*|number} Previous scroll position in px
	 */
	prevScrollPos() {
		return this._prevScrollPos;
	}

	/**
	 * Get size of scroll container (including all its scroll sections)
	 *
	 * @returns {number} Scroll container size in px
	 */
	scrollSize() {
		if (this.direction() === Direction.VERTICAL) {
			return this._options.scrollable.scrollHeight;
		} else {
			return this._options.scrollable.scrollWidth;
		}
	}

	/**
	 * Get size of scroll container viewport.
	 * @returns {number} Scroll container viewport size in px
	 */
	viewportSize() {
		if (this.direction() === Direction.VERTICAL) {
			if (DOM.isWindow(this._container)) {
				return this._container.innerHeight;
			} else {
				return this._container.getBoundingClientRect().height;
			}
		} else {
			if (DOM.isWindow(this._container)) {
				return this._container.innerWidth;
			} else {
				return this._container.getBoundingClientRect().width;
			}
		}
	}

	/**
	 * Determine if target is visible within the viewport of the scroll container.
	 *
	 * @param target
	 */
	visible(target) {
		// TODO
		// overlap top
		// overlap bottom
		// overlap top and bottom

	}
}

/**
 * Scroll animation.
 *
 * Inspired by:
 * http://callmecavs.com/jump.js/
 */
export class ScrollAnimation {

	/**
	 * Constructor.
	 *
	 * @param {Object} container Scroll container
	 * @param {(number|string|Object)} target Target element or position
	 * @param {Object} options Scroll options
	 */
	constructor(container, target, options) {
		this._container = null;      // Scroll container
		this._options = null;        // Scroll configuration
		this._element = null;        // Scroll target DOMElement
		this._start = null;          // Scroll start position in px
		this._stop = null;           // Scroll stop position in px
		this._distance = null;       // Scroll distance in px
		this._timeStart = null;      // Scroll start time in ms
		this._timeElapsed = null;    // Scroll time already elapsed ms
		this._next = null;           // Next scroll position in px

		this._container = container;
		this._options = options;

		// Convert target to DOMElement
		this._element = this.element(target);

		// Get start position
		this._start = this.startPos();

		// Get stop position
		this._stop = this.stopPos(target);

		// Get distance
		this._distance = this._stop - this._start + this._options.offset;
		// console.debug(`target: ${target}, stop: ${this._stop}, start: ${this._start}, offset: ${this._options.offset}, distance: ${this._distance}`);

		// Start scroll animation
		// Note: the arrow function sets context for usage of this in animate
		window.requestAnimationFrame((t) => this.animate(t));
	}

	/**
	 * Get start position.
	 *
	 * @returns {(number)} Start position
	 */
	startPos() {
		// Get scroll _start position depending on scroll direction
		if (this._options.direction === Direction.VERTICAL) {
			return this._options.scrollable.scrollTop;
		} else {
			return this._options.scrollable.scrollLeft;
		}
	}

	/**
	 * Get stop position
	 *
	 * @param {(DOMElement|Object|string|number)} target Scroll target
	 * @returns {number} Stop position in px
	 */
	stopPos(target) {
		if (typeof target === 'number') {
			// Just use the px position of the target
			return target;
		} else {
			// Get scroll stop position depending on scroll direction
			if (this._options.direction === Direction.VERTICAL) {
				return this._element.origNode().getBoundingClientRect().top + this._start;
			} else {
				return this._element.origNode().getBoundingClientRect().left + this._start;
			}
		}
	}

	// noinspection JSMethodCanBeStatic
	/**
	 * Convert target to DOMElement.
	 *
	 * @param {(DOMElement|Object|string|number)} target Target element or position
	 * @returns {*} DOMElement
	 */
	element(target) {
		let element = null;
		switch (typeof target) {
			case 'DOMElement':
				// Just use the DOMElement
				element = target;
				break;
			case 'number':
				// Target position is directly used later on
				break;
			case 'object':
				// Convert _element into DOMElement
				element = new DOMElement(target);
				break;
			case 'string':
				// Query DOMElement by id or class
				element = DOM.queryUnique(target);
				break;
		}
		return element;
	}

	/**
	 * Run scroll animation.
	 *
	 * @param {number} timeCurrent Current time from in µs
	 */
	animate(timeCurrent) {

		// Remember time when scrolling started
		if (!this._timeStart) {
			this._timeStart = timeCurrent;
		}

		// Determine time spent for scrolling so far
		this._timeElapsed = timeCurrent - this._timeStart;

		// Calculate _next scroll position
		this._next = this._options.easing(this._timeElapsed, this._start, this._distance,
			this._options.duration);

		// Change scroll position
		this.scroll(this._next);

		// Check progress
		if (this._timeElapsed < this._options.duration) {
			// Continue scroll animation
			// Note: the arrow function sets context for usage of this in animate
			window.requestAnimationFrame((t) => this.animate(t));
		} else {
			// Finish scroll animation
			this.done();
		}
	}

	/**
	 * Scroll to position.
	 *
	 * @param {number} position Scroll position
	 */
	scroll(position) {
		if (this._options.direction === Direction.VERTICAL) {
			this._options.scrollable.scrollTop = position;
		} else {
			this._options.scrollable.scrollLeft = position;
		}
	}

	/**
	 * Finish scroll animation.
	 */
	done() {
		// Account for time rounding inaccuracies in requestAnimationFrame
		this.scroll(this._start + this._distance);

		// Reset time for _next animation
		this._timeStart = false;

		// Run custom complete function if available
		if (this._options.complete !== null) {
			this._options.complete();
		}
	}
}

/**
 * Scroller plugin.
 */
export class ScrollPlugin {

	/**
	 * Constructor.
	 * @param {Object=} options Options
	 */
	constructor(options = {}) {
		this._scroller = null;
		this._options = options;
	}

	/**
	 * Get / set scroller.
	 *
	 * @param {Scroller=} scroller
	 * @returns {(Scroller|ScrollPlugin)} Scroller or ScrollPlugin for chaining when used as setter
	 */
	scroller(scroller) {
		if (scroller) {
			this._scroller = scroller;
			return this;
		} else {
			return this._scroller;
		}
	}

	/**
	 * Get options.
	 *
	 * @returns {Object}
	 */
	options() {
		return this._options;
	}

	/**
	 * Scroll handler.
	 */
	onScroll() {
	}

	/**
	 * Scroll backward handler.
	 */
	onScrollBackward() {
	}

	/**
	 * Scroll forward handler.
	 */
	onScrollForward() {
	}
}<commit_msg>Adding scrollStart and scrollEnd event.<commit_after>import DOM from "../dom/floatz.dom.dom.js";
import Easing from "../animation/floatz.animation.easing.js"
import {DOMElement} from "../dom/floatz.dom.dom.js";
import {EVENT_SCROLL} from "../dom/floatz.dom.events.js";

/**
 * Notes:
 * ------
 * document.body.scrollTop / scrollLeft
 * containerElement.scrollTop / scrollLeft
 * childelement.offsetTop / offsetLeft => Position relative to document (fixed)
 * childelement.getClientBoundingRect.top / left => Position relative to _container (variable)
 */

// Constants for events
// Note: Symbols can´t be used because closure compiler will change names
export const SCROLL_EVENT_BEFORENAVGIATE = "flz-event-before-navigate";
export const SCROLL_EVENT_AFTERNAVGIATE = "flz-event-after-navigate";

/**
 * Scroll direction enum.
 *
 * @type {Object}
 */
export const Direction = Object.freeze({
	HORIZONTAL: Symbol("horizontal"),
	VERTICAL: Symbol("vertical")
});

/**
 * Scroll manager.
 */
export class Scroller {
	/**
	 * Constructor.
	 *
	 * @param {?(DOMElement|Object|string)} container Scroll container (default is window)
	 * @param {?Object} options Scroll container options
	 */
	constructor(container = window, options = {}) {
		this._options = options;
		this._options.direction = options.direction || Direction.VERTICAL;
		this._options.offset = options.offset || 0;
		this._plugins = [];
		this._handlers = [];
		this._scrollStartHandlers = [];
		this._scrollEndHandlers = [];
		this._scrollHandler = null;
		this._container = container;
		this._scrolling = false;

		if (DOM.isWindow(container)) {
			// Note: document.body does not work since Chrome 61
			this._options.scrollable = document.scrollingElement || document.documentElement;
		} else if (container instanceof DOMElement) {
			this._container = container.origNode();
			this._options.scrollable = this._container;
		} else {
			this._options.scrollable = this._container;
		}
		this._prevScrollPos = this.scrollPos();
	}

	/**
	 * Inject scroll plugin.
	 *
	 * @param plugin {ScrollPlugin} Scroll plugin
	 * @returns {Scroller} Scroller for chaining
	 */
	plugin(plugin) {
		if (!(plugin instanceof ScrollPlugin)) {
			throw "Plugin must extend class ScrollPlugin";
		}

		plugin.scroller(this);
		this._plugins.push(plugin);
		this.onScroll(() => {
			plugin.onScroll(this);
		});
		this.onScrollBackward(() => {
			plugin.onScrollBackward(this);
		});
		this.onScrollForward(() => {
			plugin.onScrollForward(this);
		});
		return this;
	}

	/**
	 * Get scroll container.
	 *
	 * @returns {Object} Scroll container
	 */
	container() {
		return this._container;
	}

	/**
	 * Get scroll options.
	 *
	 * @returns Scroll options
	 */
	options() {
		return this._options;
	}

	/**
	 * Get / set scroll offset correction.
	 *
	 * @param {number=} offset Scroll offset correction
	 * @returns Scroller for chaining when used as setter
	 */
	offset(offset) {
		if (offset === undefined) {
			return this._options.offset;
		} else {
			this._options.offset = offset;
			return this;
		}
	}

	/**
	 * Scroll handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScroll(handler) {
		if (!this._scrollHandler) {
			this._scrollHandler = () => {
				// Adjust events to maximum of 60fps
				if (!this._scrolling) {
					window.requestAnimationFrame(() => {
						this._handlers.forEach((handler) => {
							handler(this);
						});

						// Set new position AFTER firing handlers!
						this._prevScrollPos = this.scrollPos();
						this._scrolling = false;
					});
					this._scrolling = true;
				}
			};
			DOM.addEvent(this._container, EVENT_SCROLL, this._scrollHandler);
		}

		this._handlers.push(() => {
			handler(this);
		});
		return this;
	}

	/**
	 * Scroll forward handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScrollForward(handler) {
		this._handlers.push(() => {
			if (this._prevScrollPos < this.scrollPos()) {
				handler(this);
			}
		});
		return this;
	}

	/**
	 * Scroll backward handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScrollBackward(handler) {
		this._handlers.push(() => {
			if (this._prevScrollPos > this.scrollPos()) {
				handler(this);
			}
		});
		return this;
	}

	/**
	 * Scroll start handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScrollStart(handler) {
		_registerScrollStartEndHandler(this);
		this._scrollStartHandlers.push(() => {
			handler(this);
		});
		return this;
	}

	/**
	 * Scroll end handler.
	 *
	 * @param handler Custom handler
	 * @returns {Scroller} Scroller for chaining
	 */
	onScrollEnd(handler) {
		_registerScrollStartEndHandler(this);
		this._scrollEndHandlers.push(() => {
			handler(this);
		});
		return this;
	}

	/**
	 * Scroll to
	 * @param {(number|Object|string)} target Target element or position
	 * @param {Object=} options Scroll options
	 * @returns {Scroller} Scroller for chaining
	 */
	scrollTo(target, options = {}) {
		this._options.duration = options.duration || 600;
		this._options.easing = options.easing || Easing.easeInOutQuad;
		this._options.complete = options.complete || null;
		new ScrollAnimation(this._container, target, this._options);
		return this;
	}

	/**
	 * Get scroll direction configure via the constructor.
	 *
	 * @return {Object} direction Scroll Direction
	 */
	direction() {
		return this._options.direction;
	}

	/**
	 * Get scroll position.
	 *
	 * @param {number=} position Optional scroll position
	 * @returns {number|Scroller} Scroll position in px or scroller for chaining if used as setter
	 */
	scrollPos(position) {
		if (position) {
			if (this.direction() === Direction.VERTICAL) {
				this._options.scrollable.scrollTop = position;
			} else {
				this._options.scrollable.scrollLeft = position;
			}
			return this;
		} else {
			return this.direction() === Direction.VERTICAL ? this._options.scrollable.scrollTop : this._options.scrollable.scrollLeft;
		}
	}

	/**
	 * Get previous scroll position.
	 *
	 * @returns {*|number} Previous scroll position in px
	 */
	prevScrollPos() {
		return this._prevScrollPos;
	}

	/**
	 * Get size of scroll container (including all its scroll sections)
	 *
	 * @returns {number} Scroll container size in px
	 */
	scrollSize() {
		if (this.direction() === Direction.VERTICAL) {
			return this._options.scrollable.scrollHeight;
		} else {
			return this._options.scrollable.scrollWidth;
		}
	}

	/**
	 * Get size of scroll container viewport.
	 * @returns {number} Scroll container viewport size in px
	 */
	viewportSize() {
		if (this.direction() === Direction.VERTICAL) {
			if (DOM.isWindow(this._container)) {
				return this._container.innerHeight;
			} else {
				return this._container.getBoundingClientRect().height;
			}
		} else {
			if (DOM.isWindow(this._container)) {
				return this._container.innerWidth;
			} else {
				return this._container.getBoundingClientRect().width;
			}
		}
	}

	/**
	 * Determine if target is visible within the viewport of the scroll container.
	 *
	 * @param target
	 */
	visible(target) {
		// TODO
		// overlap top
		// overlap bottom
		// overlap top and bottom
	}
}

/**
 * Scroll animation.
 *
 * Inspired by:
 * http://callmecavs.com/jump.js/
 */
export class ScrollAnimation {

	/**
	 * Constructor.
	 *
	 * @param {Object} container Scroll container
	 * @param {(number|string|Object)} target Target element or position
	 * @param {Object} options Scroll options
	 */
	constructor(container, target, options) {
		this._container = null;      // Scroll container
		this._options = null;        // Scroll configuration
		this._element = null;        // Scroll target DOMElement
		this._start = null;          // Scroll start position in px
		this._stop = null;           // Scroll stop position in px
		this._distance = null;       // Scroll distance in px
		this._timeStart = null;      // Scroll start time in ms
		this._timeElapsed = null;    // Scroll time already elapsed ms
		this._next = null;           // Next scroll position in px

		this._container = container;
		this._options = options;

		// Convert target to DOMElement
		this._element = this.element(target);

		// Get start position
		this._start = this.startPos();

		// Get stop position
		this._stop = this.stopPos(target);

		// Get distance
		this._distance = this._stop - this._start + this._options.offset;
		// console.debug(`target: ${target}, stop: ${this._stop}, start: ${this._start}, offset: ${this._options.offset}, distance: ${this._distance}`);

		// Start scroll animation
		// Note: the arrow function sets context for usage of this in animate
		window.requestAnimationFrame((t) => this.animate(t));
	}

	/**
	 * Get start position.
	 *
	 * @returns {(number)} Start position
	 */
	startPos() {
		// Get scroll _start position depending on scroll direction
		if (this._options.direction === Direction.VERTICAL) {
			return this._options.scrollable.scrollTop;
		} else {
			return this._options.scrollable.scrollLeft;
		}
	}

	/**
	 * Get stop position
	 *
	 * @param {(DOMElement|Object|string|number)} target Scroll target
	 * @returns {number} Stop position in px
	 */
	stopPos(target) {
		if (typeof target === 'number') {
			// Just use the px position of the target
			return target;
		} else {
			// Get scroll stop position depending on scroll direction
			if (this._options.direction === Direction.VERTICAL) {
				return this._element.origNode().getBoundingClientRect().top + this._start;
			} else {
				return this._element.origNode().getBoundingClientRect().left + this._start;
			}
		}
	}

	// noinspection JSMethodCanBeStatic
	/**
	 * Convert target to DOMElement.
	 *
	 * @param {(DOMElement|Object|string|number)} target Target element or position
	 * @returns {*} DOMElement
	 */
	element(target) {
		let element = null;
		switch (typeof target) {
			case 'DOMElement':
				// Just use the DOMElement
				element = target;
				break;
			case 'number':
				// Target position is directly used later on
				break;
			case 'object':
				// Convert _element into DOMElement
				element = new DOMElement(target);
				break;
			case 'string':
				// Query DOMElement by id or class
				element = DOM.queryUnique(target);
				break;
		}
		return element;
	}

	/**
	 * Run scroll animation.
	 *
	 * @param {number} timeCurrent Current time from in µs
	 */
	animate(timeCurrent) {

		// Remember time when scrolling started
		if (!this._timeStart) {
			this._timeStart = timeCurrent;
		}

		// Determine time spent for scrolling so far
		this._timeElapsed = timeCurrent - this._timeStart;

		// Calculate _next scroll position
		this._next = this._options.easing(this._timeElapsed, this._start, this._distance,
			this._options.duration);

		// Change scroll position
		this.scroll(this._next);

		// Check progress
		if (this._timeElapsed < this._options.duration) {
			// Continue scroll animation
			// Note: the arrow function sets context for usage of this in animate
			window.requestAnimationFrame((t) => this.animate(t));
		} else {
			// Finish scroll animation
			this.done();
		}
	}

	/**
	 * Scroll to position.
	 *
	 * @param {number} position Scroll position
	 */
	scroll(position) {
		if (this._options.direction === Direction.VERTICAL) {
			this._options.scrollable.scrollTop = position;
		} else {
			this._options.scrollable.scrollLeft = position;
		}
	}

	/**
	 * Finish scroll animation.
	 */
	done() {
		// Account for time rounding inaccuracies in requestAnimationFrame
		this.scroll(this._start + this._distance);

		// Reset time for _next animation
		this._timeStart = false;

		// Run custom complete function if available
		if (this._options.complete !== null) {
			this._options.complete();
		}
	}
}

/**
 * Scroller plugin.
 */
export class ScrollPlugin {

	/**
	 * Constructor.
	 * @param {Object=} options Options
	 */
	constructor(options = {}) {
		this._scroller = null;
		this._options = options;
	}

	/**
	 * Get / set scroller.
	 *
	 * @param {Scroller=} scroller
	 * @returns {(Scroller|ScrollPlugin)} Scroller or ScrollPlugin for chaining when used as setter
	 */
	scroller(scroller) {
		if (scroller) {
			this._scroller = scroller;
			return this;
		} else {
			return this._scroller;
		}
	}

	/**
	 * Get options.
	 *
	 * @returns {Object}
	 */
	options() {
		return this._options;
	}

	/**
	 * Scroll handler.
	 */
	onScroll() {
	}

	/**
	 * Scroll backward handler.
	 */
	onScrollBackward() {
	}

	/**
	 * Scroll forward handler.
	 */
	onScrollForward() {
	}
}

/**
 * Register handler that is necessary for scroll start / end events.
 *
 * @param scroller Scroller
 * @see https://gomakethings.com/detecting-when-a-visitor-has-stopped-scrolling-with-vanilla-javascript/
 * @private
 */
function _registerScrollStartEndHandler(scroller) {
	if (scroller._scrollEndHandlers.length === 0 && scroller._scrollStartHandlers.length === 0) {
		let isScrolling = null;
		DOM.addEvent(scroller._container, EVENT_SCROLL, () => {

			// Run scroll start handlers
			if (isScrolling === null) {
				scroller._scrollStartHandlers.forEach((handler) => {
					handler(scroller);
				});
			}

			// Clear our timeout throughout the scroll
			window.clearTimeout(isScrolling);

			// Set a timeout to run after scrolling ends
			isScrolling = setTimeout(() => {

				// Run scroll end handlers
				scroller._scrollEndHandlers.forEach((handler) => {
					handler(scroller);
				});
				isScrolling = null;

			}, 66);
		});
	}
}
<|endoftext|>


<commit_before>define([ "jquery", "message-bus", "typeahead" ], function($, bus) {
	bus.listen("ui-autocomplete:create", function(e, msg) {
		var div = $("<div/>").attr("id", msg.div);

		var input = $("<input/>");
		input.attr("type", "text");
		input.attr("placeholder", msg.placeholder);

		var icon = $("<div/>").addClass("autocomplete-icon")

		div.append(input);
		div.append(icon);
		$("#" + msg.parentDiv).append(div);

		div.addClass(msg.css);
		div.addClass("autocomplete");
		input.addClass("typeahead");
		input.addClass("autocomplete-input");
		icon.addClass("autocomplete-icon");

		input.typeahead({
			hint : true,
			highlight : true,
			minLength : 1,
			autoselect : true
		}, {
			source : function(q, cb) {
				var matches = [];
				var regex = new RegExp(q, 'i');

				$.each(msg.options, function(i, option) {
					if (regex.test(option)) {
						matches.push({
							value : option
						});
					}
				});

				cb(matches.slice(0,5));
			}
		});

		var eventName = "ui-autocomplete:" + msg.div + ":selected";

		input.keypress(function(event) {
			if (event.which == 13) {
				bus.send(eventName, input.typeahead("val"));
			}
		});

		icon.click(function() {
			bus.send(eventName, input.typeahead("val"));
		});

		input.on('typeahead:selected', function(event, selection) {
			bus.send(eventName, selection.value);
		});
	});
});<commit_msg>Modify autocomplete control so several options can be configured and it's possible to show it on focus without writing anything. Use it from query-manager. Refs #1732<commit_after>define([ "jquery", "message-bus", "typeahead" ], function($, bus) {
	bus.listen("ui-autocomplete:create", function(e, msg) {
		var div = $("<div/>").attr("id", msg.div);

		if (msg.label) {
			var label = $("<label/>").text(msg.label).addClass("autocomplete-label");
			div.append(label);
		}

		var input = $("<input/>");
		input.attr("type", "text");
		input.attr("placeholder", msg.placeholder);

		var icon = $("<div/>").addClass("autocomplete-icon");
		var options = msg.options;

		div.append(input);
		div.append(icon);
		$("#" + msg.parentDiv).append(div);

		div.addClass(msg.css);
		div.addClass("autocomplete");
		input.addClass("typeahead");
		input.addClass("autocomplete-input");
		icon.addClass("autocomplete-icon");

		var inputTypeahead = input.typeahead({
			hint : msg.hint,
			highlight : true,
			minLength : msg.minQueryLength,
			autoselect : true
		}, {
			source : function(q, cb) {
				if (!options) {
					cb([]);
					return;
				}

				var matches = [];
				var pattern = msg.searchMode == "startsWith" ? "^" + q : q;
				var regex = new RegExp(pattern, 'i');
				$.each(options, function(i, option) {
					if (regex.test(option)) {
						matches.push({
							value : option
						});
					}
				});

				if (msg.maxResults > 0) {
					matches = matches.slice(0, msg.maxResults);
				}
				cb(matches);
			}
		});
		var eventName = "ui-autocomplete:" + msg.div + ":selected";

		input.keypress(function(event) {
			if (event.which == 13) {
				bus.send(eventName, input.typeahead("val"));
			}
		});

		icon.click(function() {
			bus.send(eventName, input.typeahead("val"));
		});

		input.on('typeahead:selected', function(event, selection) {
			bus.send(eventName, selection.value);
		});

		if (msg.showOnFocus || minQueryLength == 0) {
			// Show dropdown on input focus
			input.focus(function() {
				// This is a bit obscure, but the only way I found to do it
				var query = (input.val() || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
				var data = input.data("ttTypeahead");
				data.dropdown.update(query);
				data.dropdown.open();
			});
		}

		bus.listen("ui-autocomplete:" + msg.div + ":set-values", function(e, values) {
			options = values;
		});

		bus.listen(msg.div + "-field-value-fill", function(e, message) {
			message[msg.div] = input.val();
		});
	});
});<|endoftext|>


<commit_before>/*
This file is part of the Juju GUI, which lets users view and manage Juju
environments within a graphical interface (https://launchpad.net/juju-gui).
Copyright (C) 2016 Canonical Ltd.

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License version 3, as published by
the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
General Public License for more details.

You should have received a copy of the GNU Affero General Public License along
with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

'use strict';

YUI.add('create-model-button', function() {

  juju.components.CreateModelButton = React.createClass({
    propTypes: {
      action: React.PropTypes.func,
      changeState: React.PropTypes.func.isRequired,
      disabled: React.PropTypes.bool,
      switchModel: React.PropTypes.func.isRequired,
      title: React.PropTypes.string,
      type: React.PropTypes.string
    },

    getDefaultProps: function() {
      return {
        type: 'inline-neutral',
        title: 'Create new'
      };
    },

    _createNewModel: function() {
      const props = this.props;
      if (props.disabled) {
        return;
      }
      // We want to explicitly close the profile when switching to a new
      // model to resolve a race condition with the new model setup.
      props.changeState({profile: null});
      props.switchModel(null);
      if (this.props.action) {
        this.props.action();
      }
    },

    render: function() {
      const disabled = this.props.disabled || false;
      return (

          <juju.components.GenericButton
            action={this._createNewModel}
            disabled={disabled}
            type={this.props.type}
            title={this.props.title} />

      );
    }
  });

}, '', {
  requires: [
    'generic-button'
  ]
});
<commit_msg>Update CreateModelButton component.<commit_after>/*
This file is part of the Juju GUI, which lets users view and manage Juju
environments within a graphical interface (https://launchpad.net/juju-gui).
Copyright (C) 2016 Canonical Ltd.

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License version 3, as published by
the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
General Public License for more details.

You should have received a copy of the GNU Affero General Public License along
with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

'use strict';

const CreateModelButton = React.createClass({
  propTypes: {
    action: React.PropTypes.func,
    changeState: React.PropTypes.func.isRequired,
    disabled: React.PropTypes.bool,
    switchModel: React.PropTypes.func.isRequired,
    title: React.PropTypes.string,
    type: React.PropTypes.string
  },

  getDefaultProps: function() {
    return {
      type: 'inline-neutral',
      title: 'Create new'
    };
  },

  _createNewModel: function() {
    const props = this.props;
    if (props.disabled) {
      return;
    }
    // We want to explicitly close the profile when switching to a new
    // model to resolve a race condition with the new model setup.
    props.changeState({profile: null});
    props.switchModel(null);
    if (this.props.action) {
      this.props.action();
    }
  },

  render: function() {
    const disabled = this.props.disabled || false;
    return (

        <juju.components.GenericButton
          action={this._createNewModel}
          disabled={disabled}
          type={this.props.type}
          title={this.props.title} />

    );
  }
});

YUI.add('create-model-button', function() {
  juju.components.CreateModelButton = CreateModelButton;
}, '', {
  requires: [
    'generic-button'
  ]
});
<|endoftext|>


<commit_before>import htmlGlobals from 'driveback-utils/htmlGlobals';
import semver from 'driveback-utils/semver';
import getQueryParam from 'driveback-utils/getQueryParam';
import cleanObject from 'driveback-utils/cleanObject';
import { getProp, setProp } from 'driveback-utils/dotProp';
import { camelize } from 'driveback-utils/camelize';
import { warn } from 'driveback-utils/safeConsole';
import uuid from 'uuid/v1';
import UAParser from 'ua-parser-js';
import {
  SUBSCRIBED,
  COMPLETED_TRANSACTION,
  UPDATED_CART,
  ADDED_PRODUCT,
  REMOVED_PRODUCT,
  SESSION_STARTED,
  ASSIGNED_SEGMENT,
} from '../events/semanticEvents';
import {
  SDK_EVENT_SOURCE, SDK_CHANGE_SOURCE,
} from '../constants';

class DigitalDataEnricher {
  constructor(digitalData, ddListener, ddStorage, options) {
    this.digitalData = digitalData;
    this.ddListener = ddListener;
    this.ddStorage = ddStorage;
    this.options = Object.assign({
      sessionLength: 3600,
    }, options);

    this.listenToEvents();
  }

  setDigitalData(digitalData) {
    this.digitalData = digitalData;
  }

  setDDListener(ddListener) {
    this.ddListener = ddListener;
  }

  setDDStorage(ddStorage) {
    this.ddStorage = ddStorage;
  }

  setOption(key, value) {
    this.options[key] = value;
  }

  enrichDigitalData() {
    // define required digitalData structure
    this.enrichStructure();

    // fire session started event if this is new session
    this.fireSessionStarted();

    // enrich with default data
    this.enrichContextData();
    this.enrichPageData();
    this.enrichLegacyVersions();

    // persist some default behaviours
    this.persistUserData();

    // should be after all default enrichments
    this.enrichDDStorageData();

    // enrich required fields if still not defined
    this.enrichDefaultUserData();
    this.enrichIsReturningStatus();

    this.ddStorage.setLastEventTimestamp(Date.now());
  }

  addToSegment(event) {
    const segment = getProp(event, 'segment');
    if (segment) {
      const { id, name, value } = segment;
      const segmentName = id || camelize(name);

      const userSegment = getProp(this.digitalData, 'user.segment') || {};
      userSegment[segmentName] = value;
      this.digitalData.changes.push(['user.segment', userSegment, SDK_CHANGE_SOURCE]);
      this.ddStorage.persist('user.segment');
    }
  }

  listenToEvents() {
    // enrich DDL based on semantic events
    this.ddListener.push(['on', 'event', (event) => {
      this.enrichIsReturningStatus();
      this.ddStorage.setLastEventTimestamp(Date.now());

      if (event.name === SUBSCRIBED) {
        const email = getProp(event, 'user.email');
        this.enrichHasSubscribed(email);
      } else if (event.name === COMPLETED_TRANSACTION) {
        this.enrichHasTransacted();
        if (this.options.unpersistContextCampaignOnTransaction !== false){
          this.ddStorage.unpersist('context.campaign');
        }
      } else if (event.name === UPDATED_CART && semver.cmp(this.digitalData.version, '1.1.3') >= 0) {
        this.fireAddRemoveProduct(event);
        this.digitalData.changes.push(['cart', event.cart, SDK_CHANGE_SOURCE]);
      } else if (event.name === ASSIGNED_SEGMENT) {
        this.addToSegment(event);
      }
    }]);
  }

  fireAddRemoveProduct(event) {
    const lineItems = getProp(event, 'cart.lineItems') || [];
    const oldLineItems = getProp(this.digitalData, 'cart.lineItems') || [];
    const addedQuantityDiff = {};
    const removedQuantityDiff = {};

    const productKey = (id, skuCode) => ((id && skuCode) ? [id, skuCode].join('_') : id);

    const comparerUnique = otherLineItems => (
      current => (!otherLineItems.some(other => (
        productKey(getProp(current, 'product.id'), getProp(current, 'product.skuCode'))
          === productKey(getProp(other, 'product.id'), getProp(other, 'product.skuCode'))
      )))
    );

    const comparerQuantity = (otherLineItems, diff) => (
      current => (otherLineItems.some((other) => {
        const currentKey = productKey(getProp(current, 'product.id'), getProp(current, 'product.skuCode'));
        const otherKey = productKey(getProp(other, 'product.id'), getProp(other, 'product.skuCode'));
        const currentQuantity = getProp(current, 'quantity');
        const otherQuantity = getProp(other, 'quantity');
        if (currentKey === otherKey && currentQuantity > otherQuantity) {
          diff[currentKey] = currentQuantity - otherQuantity;
          return true;
        }
        return false;
      }))
    );

    const fireEvent = (eventName, diff) => (
      (lineItem) => {
        const id = getProp(lineItem, 'product.id');
        const skuCode = getProp(lineItem, 'product.skuCode');
        this.digitalData.events.push({
          name: eventName,
          product: lineItem.product,
          quantity: diff[productKey(id, skuCode)] || lineItem.quantity,
          source: SDK_EVENT_SOURCE,
        });
      }
    );

    const addedNew = lineItems.filter(comparerUnique(oldLineItems));
    const removedNew = oldLineItems.filter(comparerUnique(lineItems));

    const addedQuantity = lineItems.filter(comparerQuantity(oldLineItems, addedQuantityDiff));
    const removedQuantity = oldLineItems.filter(comparerQuantity(lineItems, removedQuantityDiff));

    addedNew.concat(addedQuantity).forEach(fireEvent(ADDED_PRODUCT, addedQuantityDiff));
    removedNew.concat(removedQuantity).forEach(fireEvent(REMOVED_PRODUCT, removedQuantityDiff));
  }

  fireSessionStarted() {
    const lastEventTimestamp = this.ddStorage.getLastEventTimestamp();
    if (
      !lastEventTimestamp
      || (Date.now() - lastEventTimestamp) > this.options.sessionLength * 1000
    ) {
      this.digitalData.events.push({
        name: SESSION_STARTED,
        source: SDK_EVENT_SOURCE,
        includeIntegrations: [], // do not send this event to any integration
      });
    }
  }

  enrichDefaultUserData() {
    const { user } = this.digitalData;

    if (user.isReturning === undefined) {
      user.isReturning = false;
    }

    if (user.isLoggedIn !== undefined && user.everLoggedIn === undefined) {
      user.everLoggedIn = false;
    }

    // set user.anonymousId
    if (!user.anonymousId) {
      user.anonymousId = uuid();
      this.ddStorage.persist('user.anonymousId');
    }
  }

  persistUserData() {
    const { user } = this.digitalData;

    // persist user.everLoggedIn
    if (user.isLoggedIn && !user.everLoggedIn) {
      user.everLoggedIn = true;
      this.ddStorage.persist('user.everLoggedIn');
    }
    // persist user.email
    if (user.email) {
      this.ddStorage.persist('user.email');
    }
    // persist user.isSubscribed
    if (user.isSubscribed) {
      this.ddStorage.persist('user.isSubscribed');
    }
    // persist user.hasTransacted
    if (user.hasTransacted) {
      this.ddStorage.persist('user.hasTransacted');
    }
    // persist user.lastTransactionDate
    if (user.lastTransactionDate) {
      this.ddStorage.persist('user.lastTransactionDate');
    }
  }

  enrichIsReturningStatus() {
    const lastEventTimestamp = this.ddStorage.getLastEventTimestamp();
    const { user } = this.digitalData;
    if (
      !user.isReturning && lastEventTimestamp
      && (Date.now() - lastEventTimestamp) > this.options.sessionLength * 1000
    ) {
      this.digitalData.user.isReturning = true;
      this.ddStorage.persist('user.isReturning');
    }
  }

  enrichHasSubscribed(email) {
    const { user } = this.digitalData;
    if (!user.isSubscribed) {
      user.isSubscribed = true;
      this.ddStorage.persist('user.isSubscribed');
    }
    if (!user.email && email) {
      user.email = email;
      this.ddStorage.persist('user.email');
    }
  }

  enrichHasTransacted() {
    const { user } = this.digitalData;
    if (!user.hasTransacted) {
      user.hasTransacted = true;
      this.ddStorage.persist('user.hasTransacted');
    }
    if (!user.lastTransactionDate) {
      user.lastTransactionDate = (new Date()).toISOString();
      this.ddStorage.persist('user.lastTransactionDate');
    }
  }

  enrichStructure() {
    this.digitalData.website = this.digitalData.website || {};
    this.digitalData.page = this.digitalData.page || {};
    this.digitalData.user = this.digitalData.user || {};
    this.digitalData.context = this.digitalData.context || {};
    if (this.digitalData.page.type && this.digitalData.page.type === 'confirmation') {
      this.digitalData.transaction = this.digitalData.transaction || {};
    }
    this.digitalData.cart = this.digitalData.cart || { lineItems: [], subtotal: 0, total: 0 };
    this.digitalData.events = this.digitalData.events || [];
    this.digitalData.changes = this.digitalData.changes || [];
  }

  enrichPageData() {
    const { page } = this.digitalData;
    const document = this.getHtmlGlobals().getDocument();
    const location = this.getHtmlGlobals().getLocation();
    const { title } = document;

    let path = location.pathname;
    let queryString = location.search;
    let url = location.href;
    let { hash } = location;
    let { referrer } = document;

    try { path = decodeURIComponent(path); } catch (e) { warn(e); }
    try { referrer = decodeURI(referrer); } catch (e) { warn(e); }
    try { queryString = decodeURIComponent(queryString); } catch (e) { warn(e); }
    try { url = decodeURI(url); } catch (e) { warn(e); }
    try { hash = decodeURIComponent(hash); } catch (e) { warn(e); }

    page.path = page.path || path;
    page.referrer = page.referrer || referrer;
    page.queryString = page.queryString || queryString;
    page.title = page.title || title;
    page.url = page.url || url;
    page.hash = page.hash || hash;
  }

  enrichContextData() {
    const { context } = this.digitalData;
    const { userAgent } = this.getHtmlGlobals().getNavigator();
    context.userAgent = userAgent;
    const uaParser = new UAParser();
    context.browser = uaParser.getBrowser();
    context.device = { type: 'desktop', ...cleanObject(uaParser.getDevice()) };
    context.os = uaParser.getOS();
    if (!context.campaign) {
      const gclid = getQueryParam('gclid');
      const utmCampaign = getQueryParam('utm_campaign');
      let utmSource = getQueryParam('utm_source');
      let utmMedium = getQueryParam('utm_medium');
      utmSource = gclid ? utmSource || 'google' : utmSource;
      utmMedium = gclid ? utmMedium || 'cpc' : utmMedium;
      if (utmSource || utmMedium || utmCampaign) {
        context.campaign = cleanObject({
          name: utmCampaign,
          source: utmSource,
          medium: utmMedium,
        });
        this.ddStorage.persist('context.campaign', 7776000); // 90 days
      }
    }
  }

  enrichDDStorageData() {
    const persistedKeys = this.ddStorage.getPersistedKeys();
    persistedKeys.forEach((key) => {
      const value = this.ddStorage.get(key);
      if (value === undefined) {
        return;
      }
      if (getProp(this.digitalData, key) === undefined) {
        setProp(this.digitalData, key, value);
      }
    });
  }

  enrichLegacyVersions() {
    // compatibility with version <1.1.1
    if (this.digitalData.version && semver.cmp(this.digitalData.version, '1.1.1') < 0) {
      // enrich listing.listId
      const { listing } = this.digitalData;
      if (listing && listing.listName && !listing.listId) {
        listing.listId = listing.listName;
      }
      // enrich recommendation[].listId
      let recommendations = this.digitalData.recommendation || [];
      if (!Array.isArray(recommendations)) {
        recommendations = [recommendations];
      }
      recommendations.forEach((recommendation) => {
        if (recommendation && recommendation.listName && !recommendation.listId) {
          recommendation.listId = recommendation.listName;
        }
      });
    }

    // compatibility with version <1.1.0
    if (this.digitalData.version && semver.cmp(this.digitalData.version, '1.1.0') < 0) {
      // enrich listing.categoryId
      const { page } = this.digitalData;
      if (page.type === 'category' && page.categoryId) {
        const listing = this.digitalData.listing = this.digitalData.listing || {};
        listing.categoryId = page.categoryId;
      }
    }
  }

  /**
   * Can be overriden for test purposes
   * @returns {{getDocument, getLocation, getNavigator}}
   */
  getHtmlGlobals() {
    return htmlGlobals;
  }
}

export default DigitalDataEnricher;
<commit_msg>=== true -> !== false<commit_after>import htmlGlobals from 'driveback-utils/htmlGlobals';
import semver from 'driveback-utils/semver';
import getQueryParam from 'driveback-utils/getQueryParam';
import cleanObject from 'driveback-utils/cleanObject';
import { getProp, setProp } from 'driveback-utils/dotProp';
import { camelize } from 'driveback-utils/camelize';
import { warn } from 'driveback-utils/safeConsole';
import uuid from 'uuid/v1';
import UAParser from 'ua-parser-js';
import {
  SUBSCRIBED,
  COMPLETED_TRANSACTION,
  UPDATED_CART,
  ADDED_PRODUCT,
  REMOVED_PRODUCT,
  SESSION_STARTED,
  ASSIGNED_SEGMENT,
} from '../events/semanticEvents';
import {
  SDK_EVENT_SOURCE, SDK_CHANGE_SOURCE,
} from '../constants';

class DigitalDataEnricher {
  constructor(digitalData, ddListener, ddStorage, options) {
    this.digitalData = digitalData;
    this.ddListener = ddListener;
    this.ddStorage = ddStorage;
    this.options = Object.assign({
      sessionLength: 3600,
    }, options);

    this.listenToEvents();
  }

  setDigitalData(digitalData) {
    this.digitalData = digitalData;
  }

  setDDListener(ddListener) {
    this.ddListener = ddListener;
  }

  setDDStorage(ddStorage) {
    this.ddStorage = ddStorage;
  }

  setOption(key, value) {
    this.options[key] = value;
  }

  enrichDigitalData() {
    // define required digitalData structure
    this.enrichStructure();

    // fire session started event if this is new session
    this.fireSessionStarted();

    // enrich with default data
    this.enrichContextData();
    this.enrichPageData();
    this.enrichLegacyVersions();

    // persist some default behaviours
    this.persistUserData();

    // should be after all default enrichments
    this.enrichDDStorageData();

    // enrich required fields if still not defined
    this.enrichDefaultUserData();
    this.enrichIsReturningStatus();

    this.ddStorage.setLastEventTimestamp(Date.now());
  }

  addToSegment(event) {
    const segment = getProp(event, 'segment');
    if (segment) {
      const { id, name, value } = segment;
      const segmentName = id || camelize(name);

      const userSegment = getProp(this.digitalData, 'user.segment') || {};
      userSegment[segmentName] = value;
      this.digitalData.changes.push(['user.segment', userSegment, SDK_CHANGE_SOURCE]);
      this.ddStorage.persist('user.segment');
    }
  }

  listenToEvents() {
    // enrich DDL based on semantic events
    this.ddListener.push(['on', 'event', (event) => {
      this.enrichIsReturningStatus();
      this.ddStorage.setLastEventTimestamp(Date.now());

      if (event.name === SUBSCRIBED) {
        const email = getProp(event, 'user.email');
        this.enrichHasSubscribed(email);
      } else if (event.name === COMPLETED_TRANSACTION) {
        this.enrichHasTransacted();
        if (this.options.unpersistContextCampaignOnTransaction !== false) {
          this.ddStorage.unpersist('context.campaign');
        }
      } else if (event.name === UPDATED_CART && semver.cmp(this.digitalData.version, '1.1.3') >= 0) {
        this.fireAddRemoveProduct(event);
        this.digitalData.changes.push(['cart', event.cart, SDK_CHANGE_SOURCE]);
      } else if (event.name === ASSIGNED_SEGMENT) {
        this.addToSegment(event);
      }
    }]);
  }

  fireAddRemoveProduct(event) {
    const lineItems = getProp(event, 'cart.lineItems') || [];
    const oldLineItems = getProp(this.digitalData, 'cart.lineItems') || [];
    const addedQuantityDiff = {};
    const removedQuantityDiff = {};

    const productKey = (id, skuCode) => ((id && skuCode) ? [id, skuCode].join('_') : id);

    const comparerUnique = otherLineItems => (
      current => (!otherLineItems.some(other => (
        productKey(getProp(current, 'product.id'), getProp(current, 'product.skuCode'))
          === productKey(getProp(other, 'product.id'), getProp(other, 'product.skuCode'))
      )))
    );

    const comparerQuantity = (otherLineItems, diff) => (
      current => (otherLineItems.some((other) => {
        const currentKey = productKey(getProp(current, 'product.id'), getProp(current, 'product.skuCode'));
        const otherKey = productKey(getProp(other, 'product.id'), getProp(other, 'product.skuCode'));
        const currentQuantity = getProp(current, 'quantity');
        const otherQuantity = getProp(other, 'quantity');
        if (currentKey === otherKey && currentQuantity > otherQuantity) {
          diff[currentKey] = currentQuantity - otherQuantity;
          return true;
        }
        return false;
      }))
    );

    const fireEvent = (eventName, diff) => (
      (lineItem) => {
        const id = getProp(lineItem, 'product.id');
        const skuCode = getProp(lineItem, 'product.skuCode');
        this.digitalData.events.push({
          name: eventName,
          product: lineItem.product,
          quantity: diff[productKey(id, skuCode)] || lineItem.quantity,
          source: SDK_EVENT_SOURCE,
        });
      }
    );

    const addedNew = lineItems.filter(comparerUnique(oldLineItems));
    const removedNew = oldLineItems.filter(comparerUnique(lineItems));

    const addedQuantity = lineItems.filter(comparerQuantity(oldLineItems, addedQuantityDiff));
    const removedQuantity = oldLineItems.filter(comparerQuantity(lineItems, removedQuantityDiff));

    addedNew.concat(addedQuantity).forEach(fireEvent(ADDED_PRODUCT, addedQuantityDiff));
    removedNew.concat(removedQuantity).forEach(fireEvent(REMOVED_PRODUCT, removedQuantityDiff));
  }

  fireSessionStarted() {
    const lastEventTimestamp = this.ddStorage.getLastEventTimestamp();
    if (
      !lastEventTimestamp
      || (Date.now() - lastEventTimestamp) > this.options.sessionLength * 1000
    ) {
      this.digitalData.events.push({
        name: SESSION_STARTED,
        source: SDK_EVENT_SOURCE,
        includeIntegrations: [], // do not send this event to any integration
      });
    }
  }

  enrichDefaultUserData() {
    const { user } = this.digitalData;

    if (user.isReturning === undefined) {
      user.isReturning = false;
    }

    if (user.isLoggedIn !== undefined && user.everLoggedIn === undefined) {
      user.everLoggedIn = false;
    }

    // set user.anonymousId
    if (!user.anonymousId) {
      user.anonymousId = uuid();
      this.ddStorage.persist('user.anonymousId');
    }
  }

  persistUserData() {
    const { user } = this.digitalData;

    // persist user.everLoggedIn
    if (user.isLoggedIn && !user.everLoggedIn) {
      user.everLoggedIn = true;
      this.ddStorage.persist('user.everLoggedIn');
    }
    // persist user.email
    if (user.email) {
      this.ddStorage.persist('user.email');
    }
    // persist user.isSubscribed
    if (user.isSubscribed) {
      this.ddStorage.persist('user.isSubscribed');
    }
    // persist user.hasTransacted
    if (user.hasTransacted) {
      this.ddStorage.persist('user.hasTransacted');
    }
    // persist user.lastTransactionDate
    if (user.lastTransactionDate) {
      this.ddStorage.persist('user.lastTransactionDate');
    }
  }

  enrichIsReturningStatus() {
    const lastEventTimestamp = this.ddStorage.getLastEventTimestamp();
    const { user } = this.digitalData;
    if (
      !user.isReturning && lastEventTimestamp
      && (Date.now() - lastEventTimestamp) > this.options.sessionLength * 1000
    ) {
      this.digitalData.user.isReturning = true;
      this.ddStorage.persist('user.isReturning');
    }
  }

  enrichHasSubscribed(email) {
    const { user } = this.digitalData;
    if (!user.isSubscribed) {
      user.isSubscribed = true;
      this.ddStorage.persist('user.isSubscribed');
    }
    if (!user.email && email) {
      user.email = email;
      this.ddStorage.persist('user.email');
    }
  }

  enrichHasTransacted() {
    const { user } = this.digitalData;
    if (!user.hasTransacted) {
      user.hasTransacted = true;
      this.ddStorage.persist('user.hasTransacted');
    }
    if (!user.lastTransactionDate) {
      user.lastTransactionDate = (new Date()).toISOString();
      this.ddStorage.persist('user.lastTransactionDate');
    }
  }

  enrichStructure() {
    this.digitalData.website = this.digitalData.website || {};
    this.digitalData.page = this.digitalData.page || {};
    this.digitalData.user = this.digitalData.user || {};
    this.digitalData.context = this.digitalData.context || {};
    if (this.digitalData.page.type && this.digitalData.page.type === 'confirmation') {
      this.digitalData.transaction = this.digitalData.transaction || {};
    }
    this.digitalData.cart = this.digitalData.cart || { lineItems: [], subtotal: 0, total: 0 };
    this.digitalData.events = this.digitalData.events || [];
    this.digitalData.changes = this.digitalData.changes || [];
  }

  enrichPageData() {
    const { page } = this.digitalData;
    const document = this.getHtmlGlobals().getDocument();
    const location = this.getHtmlGlobals().getLocation();
    const { title } = document;

    let path = location.pathname;
    let queryString = location.search;
    let url = location.href;
    let { hash } = location;
    let { referrer } = document;

    try { path = decodeURIComponent(path); } catch (e) { warn(e); }
    try { referrer = decodeURI(referrer); } catch (e) { warn(e); }
    try { queryString = decodeURIComponent(queryString); } catch (e) { warn(e); }
    try { url = decodeURI(url); } catch (e) { warn(e); }
    try { hash = decodeURIComponent(hash); } catch (e) { warn(e); }

    page.path = page.path || path;
    page.referrer = page.referrer || referrer;
    page.queryString = page.queryString || queryString;
    page.title = page.title || title;
    page.url = page.url || url;
    page.hash = page.hash || hash;
  }

  enrichContextData() {
    const { context } = this.digitalData;
    const { userAgent } = this.getHtmlGlobals().getNavigator();
    context.userAgent = userAgent;
    const uaParser = new UAParser();
    context.browser = uaParser.getBrowser();
    context.device = { type: 'desktop', ...cleanObject(uaParser.getDevice()) };
    context.os = uaParser.getOS();
    if (!context.campaign) {
      const gclid = getQueryParam('gclid');
      const utmCampaign = getQueryParam('utm_campaign');
      let utmSource = getQueryParam('utm_source');
      let utmMedium = getQueryParam('utm_medium');
      utmSource = gclid ? utmSource || 'google' : utmSource;
      utmMedium = gclid ? utmMedium || 'cpc' : utmMedium;
      if (utmSource || utmMedium || utmCampaign) {
        context.campaign = cleanObject({
          name: utmCampaign,
          source: utmSource,
          medium: utmMedium,
        });
        this.ddStorage.persist('context.campaign', 7776000); // 90 days
      }
    }
  }

  enrichDDStorageData() {
    const persistedKeys = this.ddStorage.getPersistedKeys();
    persistedKeys.forEach((key) => {
      const value = this.ddStorage.get(key);
      if (value === undefined) {
        return;
      }
      if (getProp(this.digitalData, key) === undefined) {
        setProp(this.digitalData, key, value);
      }
    });
  }

  enrichLegacyVersions() {
    // compatibility with version <1.1.1
    if (this.digitalData.version && semver.cmp(this.digitalData.version, '1.1.1') < 0) {
      // enrich listing.listId
      const { listing } = this.digitalData;
      if (listing && listing.listName && !listing.listId) {
        listing.listId = listing.listName;
      }
      // enrich recommendation[].listId
      let recommendations = this.digitalData.recommendation || [];
      if (!Array.isArray(recommendations)) {
        recommendations = [recommendations];
      }
      recommendations.forEach((recommendation) => {
        if (recommendation && recommendation.listName && !recommendation.listId) {
          recommendation.listId = recommendation.listName;
        }
      });
    }

    // compatibility with version <1.1.0
    if (this.digitalData.version && semver.cmp(this.digitalData.version, '1.1.0') < 0) {
      // enrich listing.categoryId
      const { page } = this.digitalData;
      if (page.type === 'category' && page.categoryId) {
        const listing = this.digitalData.listing = this.digitalData.listing || {};
        listing.categoryId = page.categoryId;
      }
    }
  }

  /**
   * Can be overriden for test purposes
   * @returns {{getDocument, getLocation, getNavigator}}
   */
  getHtmlGlobals() {
    return htmlGlobals;
  }
}

export default DigitalDataEnricher;
<|endoftext|>


<commit_before>/**
 * @author mrdoob / http://mrdoob.com/
 * based on http://papervision3d.googlecode.com/svn/trunk/as3/trunk/src/org/papervision3d/objects/primitives/Plane.as
 */

THREE.PlaneGeometry = function ( width, height, widthSegments, heightSegments ) {

	console.log( 'THREE.PlaneGeometry: Consider using THREE.PlaneBufferGeometry for lower memory footprint.' );

	THREE.Geometry.call( this );

	this.type = 'PlaneGeometry';

	this.parameters = {
		width: width,
		height: height,
		widthSegments: widthSegments,
		heightSegments: heightSegments
	};

	this.fromBufferGeometry( new THREE.PlaneBufferGeometry( width, height, widthSegments, heightSegments ) );

};

THREE.PlaneGeometry.prototype = Object.create( THREE.Geometry.prototype );
THREE.PlaneGeometry.prototype.constructor = THREE.PlaneGeometry;
<commit_msg>Remove console log from PlaneGeometry<commit_after>/**
 * @author mrdoob / http://mrdoob.com/
 * based on http://papervision3d.googlecode.com/svn/trunk/as3/trunk/src/org/papervision3d/objects/primitives/Plane.as
 */

THREE.PlaneGeometry = function ( width, height, widthSegments, heightSegments ) {

	THREE.Geometry.call( this );

	this.type = 'PlaneGeometry';

	this.parameters = {
		width: width,
		height: height,
		widthSegments: widthSegments,
		heightSegments: heightSegments
	};

	this.fromBufferGeometry( new THREE.PlaneBufferGeometry( width, height, widthSegments, heightSegments ) );

};

THREE.PlaneGeometry.prototype = Object.create( THREE.Geometry.prototype );
THREE.PlaneGeometry.prototype.constructor = THREE.PlaneGeometry;
<|endoftext|>


<commit_before>Ext.define('doweown.model.History', {
    extend: 'Ext.data.Model',
    config: {
        fields: [
                {name: 'title',         type: 'string'},
                {name: 'author',        type: 'string'},
                {name: 'publisher',     type: 'string'},
		{name: 'date',          type: 'string'},
		{name: 'thumbnail',     type: 'string', defaultValue: doweown.config.Config.getNoThumbnailUrl() },
		{name: 'barcode', mapping: 'barcode'}
        ]
    }
});
<commit_msg>removed barcode mapping<commit_after>Ext.define('doweown.model.History', {
    extend: 'Ext.data.Model',
    config: {
        fields: [
                {name: 'title',         type: 'string'},
                {name: 'author',        type: 'string'},
                {name: 'publisher',     type: 'string'},
		{name: 'date',          type: 'string'},
		{name: 'thumbnail',     type: 'string', defaultValue: doweown.config.Config.getNoThumbnailUrl() },
		{name: 'barcode', 	type: 'string'}
        ]
    }
});
<|endoftext|>


<commit_before>import WwwcRegionTool from './wwwcRegionTool.js';

describe('wwwcRegionTool.js', () => {
  describe('default values', () => {
    it('has a default name of "wwwcRegion"', () => {
      const defaultName = 'wwwcRegion';
      const instantiatedTool = new WwwcRegionTool();

      expect(instantiatedTool.name).toEqual(defaultName);
    });

    it('can be created with a custom tool name', () => {
      const customToolName = 'customToolName';
      const instantiatedTool = new WwwcRegionTool(customToolName);

      expect(instantiatedTool.name).toEqual(customToolName);
    });

    it('sets a default configuration with an orientation of 0', () => {
      const instantiatedTool = new WwwcRegionTool();

      expect(instantiatedTool.configuration.orientation).toBe(0);
    });
  });
});
<commit_msg>Fix unit test<commit_after>import WwwcRegionTool from './wwwcRegionTool.js';

describe('wwwcRegionTool.js', () => {
  describe('default values', () => {
    it('has a default name of "wwwcRegion"', () => {
      const defaultName = 'wwwcRegion';
      const instantiatedTool = new WwwcRegionTool();

      expect(instantiatedTool.name).toEqual(defaultName);
    });

    it('can be created with a custom tool name', () => {
      const customToolName = 'customToolName';
      const instantiatedTool = new WwwcRegionTool(customToolName);

      expect(instantiatedTool.name).toEqual(customToolName);
    });

    it('sets a default configuration with an minWindowWidth of 10', () => {
      const instantiatedTool = new WwwcRegionTool();

      expect(instantiatedTool.configuration.minWindowWidth).toBe(10);
    });
  });
});
<|endoftext|>