<commit_before>odoo.define('tko_coexiste_br_account.reconciliationa', function (require) {
"use strict";
    var core = require('web.core');
    var _t = core._t;
    var reconciliation = require('account.reconciliation');
    var FieldMany2One = core.form_widget_registry.get('many2one');

    reconciliation.abstractReconciliation.include({
        init: function(parent, context) {
            this._super(parent, context);
            this.create_form_fields['expense_type_id']= {
                id: "expense_type_id",
                index: 25,
                corresponding_property: "expense_type_id",
                label: _t("Expense Type"),
                required: false,
                group:"analytic.group_analytic_accounting",
                constructor: FieldMany2One,
                field_properties: {
                    relation: "account.expense.type",
                    string: _t("Expense Type"),
                    type: "many2one",
                },
            }

        },

        fetchPresets: function() {
        var self = this;
        var deferred_last_update = self.model_presets.query(['write_date']).order_by('-write_date').first().then(function (data) {
            self.presets_last_write_date = (data ? data.write_date : undefined);
        });
        var deferred_presets = self.model_presets.query().order_by('-sequence', '-id').all().then(function (data) {
            self.presets = {};
            _(data).each(function(datum){
                var preset = {
                    id: datum.id,
                    name: datum.name,
                    sequence: datum.sequence,
                    lines: [{
                        account_id: datum.account_id,
                        journal_id: datum.journal_id,
                        label: datum.label,
                        amount_type: datum.amount_type,
                        amount: datum.amount,
                        tax_id: datum.tax_id,
                        analytic_account_id: datum.analytic_account_id,
                        expense_type_id: datum.expense_type_id
                    }]
                };
                if (datum.has_second_line) {
                    preset.lines.push({
                        account_id: datum.second_account_id,
                        journal_id: datum.second_journal_id,
                        label: datum.second_label,
                        amount_type: datum.second_amount_type,
                        amount: datum.second_amount,
                        tax_id: datum.second_tax_id,
                        analytic_account_id: datum.second_analytic_account_id,
                        expense_type_id: datum.second_expense_type_id
                    });
                }
                self.presets[datum.id] = preset;
            });
        });
        return $.when(deferred_last_update, deferred_presets);
    },
    });

    /* write expense_type_id to database */

    reconciliation.abstractReconciliationLine.include({
        // Returns an object that can be passed to process_reconciliation()
        prepareCreatedMoveLinesForPersisting: function(lines) {
            lines = _.filter(lines, function(line) { return !line.is_tax_line });
            return _.collect(lines, function(line) {
                var dict = {
                    account_id: line.account_id,
                    name: line.label
                };
                // Use amount_before_tax since the amount of the newly created line is adjusted to
                // reflect tax included in price in account_move_line.create()
                var amount = line.tax_id ? line.amount_before_tax: line.amount;
                dict['credit'] = (amount > 0 ? amount : 0);
                dict['debit'] = (amount < 0 ? -1 * amount : 0);
                if (line.tax_id) dict['tax_ids'] = [[4, line.tax_id, null]];
                if (line.analytic_account_id) dict['analytic_account_id'] = line.analytic_account_id;
                if (line.expense_type_id) dict['expense_type_id'] = line.expense_type_id;
                return dict;
            });
        },
    });
});
<commit_msg>set expense_type required in reconciliation form<commit_after>odoo.define('tko_coexiste_br_account.reconciliationa', function (require) {
"use strict";
    var core = require('web.core');
    var _t = core._t;
    var reconciliation = require('account.reconciliation');
    var FieldMany2One = core.form_widget_registry.get('many2one');

    reconciliation.abstractReconciliation.include({
        init: function(parent, context) {
            this._super(parent, context);
            this.create_form_fields['expense_type_id']= {
                id: "expense_type_id",
                index: 25,
                corresponding_property: "expense_type_id",
                label: _t("Expense Type"),
                required: true,
                group:"analytic.group_analytic_accounting",
                constructor: FieldMany2One,
                field_properties: {
                    relation: "account.expense.type",
                    string: _t("Expense Type"),
                    type: "many2one",
                },
            }

        },

        fetchPresets: function() {
        var self = this;
        var deferred_last_update = self.model_presets.query(['write_date']).order_by('-write_date').first().then(function (data) {
            self.presets_last_write_date = (data ? data.write_date : undefined);
        });
        var deferred_presets = self.model_presets.query().order_by('-sequence', '-id').all().then(function (data) {
            self.presets = {};
            _(data).each(function(datum){
                var preset = {
                    id: datum.id,
                    name: datum.name,
                    sequence: datum.sequence,
                    lines: [{
                        account_id: datum.account_id,
                        journal_id: datum.journal_id,
                        label: datum.label,
                        amount_type: datum.amount_type,
                        amount: datum.amount,
                        tax_id: datum.tax_id,
                        analytic_account_id: datum.analytic_account_id,
                        expense_type_id: datum.expense_type_id
                    }]
                };
                if (datum.has_second_line) {
                    preset.lines.push({
                        account_id: datum.second_account_id,
                        journal_id: datum.second_journal_id,
                        label: datum.second_label,
                        amount_type: datum.second_amount_type,
                        amount: datum.second_amount,
                        tax_id: datum.second_tax_id,
                        analytic_account_id: datum.second_analytic_account_id,
                        expense_type_id: datum.second_expense_type_id
                    });
                }
                self.presets[datum.id] = preset;
            });
        });
        return $.when(deferred_last_update, deferred_presets);
    },
    });

    /* write expense_type_id to database */

    reconciliation.abstractReconciliationLine.include({
        // Returns an object that can be passed to process_reconciliation()
        prepareCreatedMoveLinesForPersisting: function(lines) {
            lines = _.filter(lines, function(line) { return !line.is_tax_line });
            return _.collect(lines, function(line) {
                var dict = {
                    account_id: line.account_id,
                    name: line.label
                };
                // Use amount_before_tax since the amount of the newly created line is adjusted to
                // reflect tax included in price in account_move_line.create()
                var amount = line.tax_id ? line.amount_before_tax: line.amount;
                dict['credit'] = (amount > 0 ? amount : 0);
                dict['debit'] = (amount < 0 ? -1 * amount : 0);
                if (line.tax_id) dict['tax_ids'] = [[4, line.tax_id, null]];
                if (line.analytic_account_id) dict['analytic_account_id'] = line.analytic_account_id;
                if (line.expense_type_id) dict['expense_type_id'] = line.expense_type_id;
                return dict;
            });
        },
    });
});
<|endoftext|>


<commit_before>/**
 * Module Dependencies
 */

"use strict";

const SandGrain = require('sand-grain');
const Q = require('q');
const _ = require('lodash');
const Client = require('./Client');
const Riak = require('basho-riak-client');

/**
 * Expose `Riak`
 */
class RiakGrain extends SandGrain {
  constructor() {
    super();
    this.name = this.configName = 'riak';
    this.defaultConfig = require('./default');
    this.version = require('../package').version;
  }

  bindToContext(ctx) {
    ctx.riak = new Client(this.config);

    ctx.on('end', function() {
      try {
        ctx.riak.stop();
      } catch (e) {}
    });
  }

  get(bucket, key, cb) {
    return wrapCallInPromise.call(this, 'fetchValue', { bucket, key }, cb);
  }

  save(bucket, key, value, cb) {
    return wrapCallInPromise.call(this, 'storeValue', { bucket, key, value }, cb);
  }

  delete(bucket, key, cb) {
    return wrapCallInPromise.call(this, 'deleteValue', { bucket, key }, cb);
  }
}

function wrapCallInPromise(fn, args, cb) {
  let client = new Client(this.config);

  if (this.config.bucketType) {
    args.bucketType = this.config.bucketType;
  }

  if ('function' === typeof cb) {
    return client[fn].call(client, args)
      .then((response) => {
        cb(null, response);
      }).catch((error) => {
        cb(error, response);
      });
  }

  return Q.nfcall(client[fn].bind(client), args);
}

module.exports = RiakGrain;
RiakGrain.Riak = Riak;<commit_msg>fix domain issues<commit_after>/**
 * Module Dependencies
 */

"use strict";

const SandGrain = require('sand-grain');
const Q = require('q');
const _ = require('lodash');
const Client = require('./Client');
const Riak = require('basho-riak-client');

/**
 * Expose `Riak`
 */
class RiakGrain extends SandGrain {
  constructor() {
    super();
    this.name = this.configName = 'riak';
    this.defaultConfig = require('./default');
    this.version = require('../package').version;
  }

  bindToContext(ctx) {
    ctx.riak = new Client(this.config);

    ctx.on('end', function() {
      try {
        ctx.riak.stop();
      } catch (e) {}
    });
  }

  get(bucket, key, cb) {
    return wrapCallInPromise.call(this, 'fetchValue', { bucket, key }, cb);
  }

  save(bucket, key, value, cb) {
    return wrapCallInPromise.call(this, 'storeValue', { bucket, key, value }, cb);
  }

  delete(bucket, key, cb) {
    return wrapCallInPromise.call(this, 'deleteValue', { bucket, key }, cb);
  }
}

function wrapCallInPromise(fn, args, cb) {
  let client = new Client(this.config);

  if (this.config.bucketType) {
    args.bucketType = this.config.bucketType;
  }

  if ('function' === typeof cb) {
    cb = process.domain ? process.domain.bind(cb) : cb;
    return client[fn].call(client, args)
      .then((response) => {
        cb(null, response);
      }).catch((error) => {
        cb(error, response);
      });
  }

  return Q.nfcall(client[fn].bind(client), args);
}

module.exports = RiakGrain;
RiakGrain.Riak = Riak;<|endoftext|>


<commit_before>/*global beforeEach, expect, describe, it, jasmine, $ */
describe("Json Upload Widget", function () {
	'use strict';
	var input, parentForm, hiddenFrame;
	beforeEach(function () {
		input = $('<input value="abc.def" type="file" name="file" />').appendTo("body");
	});
	function readDeps() {
		parentForm = input.parent();
		hiddenFrame = $('iframe[name="' + parentForm.prop('target') + '"]');
	}
	it("wraps the file input into a multi-part form", function () {
		input.json_upload('http://fakeaction/a');
		readDeps();
		expect(parentForm).toBe('form');
		expect(parentForm.prop('enctype')).toBe('multipart/form-data');
		expect(parentForm.prop('method')).toBe('post');
		expect(parentForm.prop('action')).toBe('http://fakeaction/a');
	});
	it("adds a hidden iframe to be the target of the form", function () {
		input.json_upload('#');
		readDeps();
		expect(hiddenFrame.length).toBe(1);
		expect(hiddenFrame.css('display')).toBe('none');
	});
	function fakeUpload(content) {
		readDeps();
		hiddenFrame[0].contentWindow.document.body.innerHTML = content;
		hiddenFrame.load();
	}
	it("submits the form when the value is changed", function () {
		var called = false;
		input.prop('type', 'hidden').json_upload('#');
		readDeps();
		parentForm.submit(function () { called = true; });
		input.change();
		expect(called).toBeTruthy();
	});
	it("executes the start callback with the selected file name when the form is submitted", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('#', spy);
		readDeps();
		input.prop('type', 'hidden').val('/testpath/abc.def');
		parentForm.submit();
		expect(spy).toHaveBeenCalledWith('abc.def');
	});
	it("executes the success callback with the parsed JSON body if the result is JSON", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('#', null, spy);
		fakeUpload('{"a":"b"}');
		expect(spy).toHaveBeenCalledWith({'a' : 'b'});
	});
	it("executes the fail callback with the returned body is not JSON", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('#', null, null, spy);
		fakeUpload('ab');
		expect(spy).toHaveBeenCalledWith('invalid server response', 'ab');
	});
	it("executes the fail callback with the returned body is JSON containing an error message", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('#', null, null, spy);
		fakeUpload('{"error":"fake error"}');
		expect(spy).toHaveBeenCalledWith('fake error');
	});
});
<commit_msg>#url was causing browser to go crazy when executing specrunner, / seems to work ok<commit_after>/*global beforeEach, expect, describe, it, jasmine, $ */
describe("Json Upload Widget", function () {
	'use strict';
	var input, parentForm, hiddenFrame;
	beforeEach(function () {
		input = $('<input value="abc.def" type="file" name="file" />').appendTo("body");
	});
	function readDeps() {
		parentForm = input.parent();
		hiddenFrame = $('iframe[name="' + parentForm.prop('target') + '"]');
	}
	it("wraps the file input into a multi-part form", function () {
		input.json_upload('http://fakeaction/a');
		readDeps();
		expect(parentForm).toBe('form');
		expect(parentForm.prop('enctype')).toBe('multipart/form-data');
		expect(parentForm.prop('method')).toBe('post');
		expect(parentForm.prop('action')).toBe('http://fakeaction/a');
	});
	it("adds a hidden iframe to be the target of the form", function () {
		input.json_upload('/');
		readDeps();
		expect(hiddenFrame.length).toBe(1);
		expect(hiddenFrame.css('display')).toBe('none');
	});
	function fakeUpload(content) {
		readDeps();
		hiddenFrame[0].contentWindow.document.body.innerHTML = content;
		hiddenFrame.load();
	}
	it("submits the form when the value is changed", function () {
		var called = false;
		input.prop('type', 'hidden').json_upload('/');
		readDeps();
		parentForm.submit(function () { called = true; });
		input.change();
		expect(called).toBeTruthy();
	});
	it("executes the start callback with the selected file name when the form is submitted", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('/', spy);
		readDeps();
		input.prop('type', 'hidden').val('/testpath/abc.def');
		parentForm.submit();
		expect(spy).toHaveBeenCalledWith('abc.def');
	});
	it("executes the success callback with the parsed JSON body if the result is JSON", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('/', null, spy);
		fakeUpload('{"a":"b"}');
		expect(spy).toHaveBeenCalledWith({'a' : 'b'});
	});
	it("executes the fail callback with the returned body is not JSON", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('/', null, null, spy);
		fakeUpload('ab');
		expect(spy).toHaveBeenCalledWith('invalid server response', 'ab');
	});
	it("executes the fail callback with the returned body is JSON containing an error message", function () {
		var spy = jasmine.createSpy('called');
		input.json_upload('/', null, null, spy);
		fakeUpload('{"error":"fake error"}');
		expect(spy).toHaveBeenCalledWith('fake error');
	});
});
<|endoftext|>


<commit_before>import { arrayToBinaryString, binaryStringToArray, decodeBase64, encodeBase64 } from 'pmcrypto';
import getRandomValues from 'get-random-values';

/**
 * Partially inspired by http://www.thomasfrank.se/sessionvars.html
 * However, we aim to deliberately be non-persistent. This is useful for
 * data that wants to be preserved across refreshes, but is too sensitive
 * to be safely written to disk. Unfortunately, although sessionStorage is
 * deleted when a session ends, major browsers automatically write it
 * to disk to enable a session recovery feature, so using sessionStorage
 * alone is inappropriate.
 *
 * To achieve this, we do two tricks. The first trick is to delay writing
 * any possibly persistent data until the user is actually leaving the
 * page (onunload). This already prevents any persistence in the face of
 * crashes, and severely limits the lifetime of any data in possibly
 * persistent form on refresh.
 *
 * The second, more important trick is to split sensitive data between
 * window.name and sessionStorage. window.name is a property that, like
 * sessionStorage, is preserved across refresh and navigation within the
 * same tab - however, it seems to never be stored persistently. This
 * provides exactly the lifetime we want. Unfortunately, window.name is
 * readable and transferable between domains, so any sensitive data stored
 * in it would leak to random other websites.
 *
 * To avoid this leakage, we split sensitive data into two shares which
 * xor to the sensitive information but which individually are completely
 * random and give away nothing. One share is stored in window.name, while
 * the other share is stored in sessionStorage. This construction provides
 * security that is the best of both worlds - random websites can't read
 * the data since they can't access sessionStorage, while disk inspections
 * can't read the data since they can't access window.name. The lifetime
 * of the data is therefore the smaller lifetime, that of window.name.
 */

const deserialize = (string) => {
    try {
        return JSON.parse(string);
    } catch (e) {
        return {};
    }
};

const serialize = (data) => JSON.stringify(data);

/**
 * Deserialize an item
 * @param {string} value
 * @return {Uint8Array}
 */
const deserializeItem = (value) => {
    if (!value) {
        return;
    }
    try {
        return binaryStringToArray(decodeBase64(value));
    } catch (e) {
        return undefined;
    }
};

/**
 * Serialize an item
 * @param {Uint8Array} value
 * @return {String}
 */
const serializeItem = (value) => {
    if (!value) {
        return;
    }
    return encodeBase64(arrayToBinaryString(value));
};

/**
 * TODO: Replace this with one key when the other apps have been updated.
 * @param {Array} keys
 * @param {Object} data
 */
const saveSessionStorage = (keys = [], data) => {
    keys.forEach((key) => {
        window.sessionStorage.setItem(key, data[key]);
    }, {});
};

/**
 * TODO: Replace this with one key when the other apps have been updated.
 * @param {Array} keys
 * @return {Object}
 */
const readSessionStorage = (keys = []) => {
    return keys.reduce((acc, key) => {
        acc[key] = window.sessionStorage.getItem(key);
        window.sessionStorage.removeItem(key);
        return acc;
    }, {});
};

/**
 * Parts two parts into an object.
 * @param {Object} share1
 * @param {Object} share2
 * @return {Object}
 */
export const mergeParts = (share1, share2) =>
    Object.keys(share1).reduce((acc, key) => {
        const share1Value = deserializeItem(share1[key]);
        const share2Value = deserializeItem(share2[key]);

        if (!share1Value || !share2Value || share2Value.length !== share1Value.length) {
            return acc;
        }

        const xored = new Array(share2Value.length);

        for (let j = 0; j < share2Value.length; j++) {
            xored[j] = share2Value[j] ^ share1Value[j];
        }

        // Strip off padding
        let unpaddedLength = share2Value.length;
        while (unpaddedLength > 0 && xored[unpaddedLength - 1] === 0) {
            unpaddedLength--;
        }

        acc[key] = arrayToBinaryString(xored.slice(0, unpaddedLength));
        return acc;
    }, {});

/**
 * Separate an object in two parts.
 * @param {Object} data
 * @return {{share1: {}, share2: {}}}
 */
export const separateParts = (data) =>
    Object.keys(data).reduce(
        (acc, key) => {
            const item = binaryStringToArray(data[key]);
            const paddedLength = Math.ceil(item.length / 256) * 256;

            const share1 = getRandomValues(new Uint8Array(paddedLength));
            const share2 = new Uint8Array(share1);

            for (let i = 0; i < item.length; i++) {
                share2[i] ^= item[i];
            }

            acc.share1[key] = serializeItem(share1);
            acc.share2[key] = serializeItem(share2);

            return acc;
        },
        { share1: {}, share2: {} }
    );

/**
 * Save data to name storage and session storage.
 * @param {Array} keys
 * @param {Object} data
 */
export const save = (keys, data) => {
    const { share1, share2 } = separateParts(data);

    window.name = serialize(share1);
    saveSessionStorage(keys, share2);
};

/**
 * Load data from name storage and session storage.
 * @param {Array} keys
 * @return {Object}
 */
export const load = (keys) => {
    const nameStorage = deserialize(window.name);
    window.name = '';

    const sessionData = readSessionStorage(keys);

    return mergeParts(nameStorage, sessionData);
};
<commit_msg>Allow undefined values in secureSessionStorage<commit_after>import { arrayToBinaryString, binaryStringToArray, decodeBase64, encodeBase64 } from 'pmcrypto';
import getRandomValues from 'get-random-values';

/**
 * Partially inspired by http://www.thomasfrank.se/sessionvars.html
 * However, we aim to deliberately be non-persistent. This is useful for
 * data that wants to be preserved across refreshes, but is too sensitive
 * to be safely written to disk. Unfortunately, although sessionStorage is
 * deleted when a session ends, major browsers automatically write it
 * to disk to enable a session recovery feature, so using sessionStorage
 * alone is inappropriate.
 *
 * To achieve this, we do two tricks. The first trick is to delay writing
 * any possibly persistent data until the user is actually leaving the
 * page (onunload). This already prevents any persistence in the face of
 * crashes, and severely limits the lifetime of any data in possibly
 * persistent form on refresh.
 *
 * The second, more important trick is to split sensitive data between
 * window.name and sessionStorage. window.name is a property that, like
 * sessionStorage, is preserved across refresh and navigation within the
 * same tab - however, it seems to never be stored persistently. This
 * provides exactly the lifetime we want. Unfortunately, window.name is
 * readable and transferable between domains, so any sensitive data stored
 * in it would leak to random other websites.
 *
 * To avoid this leakage, we split sensitive data into two shares which
 * xor to the sensitive information but which individually are completely
 * random and give away nothing. One share is stored in window.name, while
 * the other share is stored in sessionStorage. This construction provides
 * security that is the best of both worlds - random websites can't read
 * the data since they can't access sessionStorage, while disk inspections
 * can't read the data since they can't access window.name. The lifetime
 * of the data is therefore the smaller lifetime, that of window.name.
 */

const deserialize = (string) => {
    try {
        return JSON.parse(string);
    } catch (e) {
        return {};
    }
};

const serialize = (data) => JSON.stringify(data);

/**
 * Deserialize an item
 * @param {string} value
 * @return {Uint8Array}
 */
const deserializeItem = (value) => {
    if (!value) {
        return;
    }
    try {
        return binaryStringToArray(decodeBase64(value));
    } catch (e) {
        return undefined;
    }
};

/**
 * Serialize an item
 * @param {Uint8Array} value
 * @return {String}
 */
const serializeItem = (value) => {
    return encodeBase64(arrayToBinaryString(value));
};

/**
 * TODO: Replace this with one key when the other apps have been updated.
 * @param {Array} keys
 * @param {Object} data
 */
const saveSessionStorage = (keys = [], data) => {
    keys.forEach((key) => {
        window.sessionStorage.setItem(key, data[key]);
    }, {});
};

/**
 * TODO: Replace this with one key when the other apps have been updated.
 * @param {Array} keys
 * @return {Object}
 */
const readSessionStorage = (keys = []) => {
    return keys.reduce((acc, key) => {
        acc[key] = window.sessionStorage.getItem(key);
        window.sessionStorage.removeItem(key);
        return acc;
    }, {});
};

/**
 * Parts two parts into an object.
 * @param {Object} share1
 * @param {Object} share2
 * @return {Object}
 */
export const mergeParts = (share1, share2) =>
    Object.keys(share1).reduce((acc, key) => {
        const share1Value = deserializeItem(share1[key]);
        const share2Value = deserializeItem(share2[key]);

        if (!share1Value || !share2Value || share2Value.length !== share1Value.length) {
            return acc;
        }

        const xored = new Array(share2Value.length);

        for (let j = 0; j < share2Value.length; j++) {
            xored[j] = share2Value[j] ^ share1Value[j];
        }

        // Strip off padding
        let unpaddedLength = share2Value.length;
        while (unpaddedLength > 0 && xored[unpaddedLength - 1] === 0) {
            unpaddedLength--;
        }

        acc[key] = arrayToBinaryString(xored.slice(0, unpaddedLength));
        return acc;
    }, {});

/**
 * Separate an object in two parts.
 * @param {Object} data
 * @return {{share1: {}, share2: {}}}
 */
export const separateParts = (data) =>
    Object.keys(data).reduce(
        (acc, key) => {
            const value = data[key];
            if (!value) {
                return acc;
            }

            const item = binaryStringToArray(value);
            const paddedLength = Math.ceil(item.length / 256) * 256;

            const share1 = getRandomValues(new Uint8Array(paddedLength));
            const share2 = new Uint8Array(share1);

            for (let i = 0; i < item.length; i++) {
                share2[i] ^= item[i];
            }

            acc.share1[key] = serializeItem(share1);
            acc.share2[key] = serializeItem(share2);

            return acc;
        },
        { share1: {}, share2: {} }
    );

/**
 * Save data to name storage and session storage.
 * @param {Array} keys
 * @param {Object} data
 */
export const save = (keys, data) => {
    const { share1, share2 } = separateParts(data);

    window.name = serialize(share1);
    saveSessionStorage(keys, share2);
};

/**
 * Load data from name storage and session storage.
 * @param {Array} keys
 * @return {Object}
 */
export const load = (keys) => {
    const nameStorage = deserialize(window.name);
    window.name = '';

    const sessionData = readSessionStorage(keys);

    return mergeParts(nameStorage, sessionData);
};
<|endoftext|>


<commit_before>(function ($) {
    $(document).ready(function () {
        initPage(document);
    });
    $(document).on("opencontent.change", function (event, element) {
        initPage(element);
    });
    var template = '';
    function initPage(element) {
        $(".jplist", element).each(function () {
			var moduleid = $(this).attr('data-moduleid');
            var moduleScope = $(this),
                self = moduleScope,
                sf = $.ServicesFramework(moduleid);

            var $list = $('#demo .list'),
                template = Handlebars.compile($('#jplist-template').html());

            $(this).jplist({
                itemsBox: ".list",
				itemPath: ".list-item",
				panelPath: ".jplist-panel",
				deepLinking: true,
				dataSource: {
                    type: 'server',
					server: {
                        ajax: {
                            data: {},
							url: sf.getServiceRoot('OpenContent') + "JplistAPI/List",
							dataType: 'json',
							type: 'POST',
							beforeSend: sf.setModuleHeaders
                        }
                    },
					render: function (dataItem, statuses) {
						$list.html(template(dataItem.content));
						var logs = dataItem.content.Logs;
                        $.fn.openContent.printLogs('Module ' + moduleid + ' - jplist webapi', logs);
                    }
                }
            });
        });

        $(".flexslider.flex-carousel", element).each(function () {
            $(this).flexslider({
                'animationLoop': $(this).attr("data-animationloop") ? $(this).data("animationloop") : false,
                'slideshow': $(this).attr("data-slideshow") ? $(this).data("slideshow") : false,
                'animation': "slide",
                'touch': $(this).attr("data-touch") ? $(this).data("touch") : false,
                'controlNav': $(this).attr("data-controlnav") ? $(this).data("controlnav") : false,
                'directionNav': $(this).attr("data-directionnav") ? $(this).data("directionnav") : false,
                'itemWidth': $(this).attr("data-itemwidth") ? $(this).data("itemwidth") : 210,
                'itemMargin': $(this).attr("data-itemmargin") ? $(this).data("itemmargin") : 5,
                'minItems': $(this).attr("data-minitems") ? $(this).data("minitems") : 0,
                'maxItems': $(this).attr("data-maxitems") ? $(this).data("maxitems") : 0,
                'move': $(this).attr("data-move") ? $(this).data("move") : 0,
                'asNavFor': $(this).attr("data-asnavfor") ? $(this).data("asnavfor") : ""
            });
        });
        $(".flexslider.flex-slider", element).each(function () {
            $(this).flexslider({
                'animationLoop': $(this).attr("data-animationloop") ? $(this).data("animationloop") : false,
                'slideshow': $(this).attr("data-slideshow") ? $(this).data("slideshow") : false,

                'animation': $(this).attr("data-animation") ? $(this).data("animation") : "slide",
                'touch': $(this).attr("data-touch") ? $(this).data("touch") : false,
                'controlNav': $(this).attr("data-controlnav") ? $(this).data("controlnav") : false,
                'directionNav': $(this).attr("data-directionnav") ? $(this).data("directionnav") : false,
                'sync': $(this).attr("data-sync") ? $(this).data("sync") : ""

            });
        });
        $(".jplist-detail", element).each(function () {

            var moduleId = $(this).attr('data-moduleid');
            var itemId = $(this).attr('data-id');
            var itemTitle = $(this).attr('data-title');

            var sf = $.ServicesFramework(moduleId);

            var $submitButton = $('#submit-' + moduleId);
            var $modal = $('#modal-' + moduleId);
            var $contactForm = $('.contactform', $modal);

            var form = $contactForm.openContentForm({
                servicesFramework: sf,
                onSubmit: function (data) {
                    data.Title = itemTitle;
                },
                onSubmited: function (data) {
                    form.destroy();
                    $contactForm.text(data.message);
                    $submitButton.hide();
                    //$modal.modal('hide');
                }
            });
            $submitButton.click(function () {
                form.submit(itemId);
            });
        });
    }
    if (typeof Handlebars != 'undefined') {
        Handlebars.registerHelper('formatDateTime', function (context, format) {
            if (window.moment && context && moment(context).isValid()) {
                var f = format || "DD/MM/YYYY";
                return moment(context).format(f);
            } else {
                return context;   //  moment plugin is not available, context does not have a truthy value, or context is not a valid date
            }
        });
    }
}(jQuery));<commit_msg>show errors<commit_after>(function ($) {
    $(document).ready(function () {
        initPage(document);
    });
    $(document).on("opencontent.change", function (event, element) {
        initPage(element);
    });
    var template = '';
    function initPage(element) {
        $(".jplist", element).each(function () {
			var moduleid = $(this).attr('data-moduleid');
            var moduleScope = $(this),
                self = moduleScope,
                sf = $.ServicesFramework(moduleid);

            var $list = $('#demo .list'),
                template = Handlebars.compile($('#jplist-template').html());

            $(this).jplist({
                itemsBox: ".list",
				itemPath: ".list-item",
				panelPath: ".jplist-panel",
				deepLinking: true,
				dataSource: {
                    type: 'server',
					server: {
                        ajax: {
                            data: {},
							url: sf.getServiceRoot('OpenContent') + "JplistAPI/List",
							dataType: 'json',
							type: 'POST',
							beforeSend: sf.setModuleHeaders
                        }
                    },
					render: function (dataItem, statuses) {
						$list.html(template(dataItem.content));
						var logs = dataItem.content.Logs;
                        $.fn.openContent.printLogs('Module ' + moduleid + ' - jplist webapi', logs);
                    }
                }
            });
        });

        $(".flexslider.flex-carousel", element).each(function () {
            $(this).flexslider({
                'animationLoop': $(this).attr("data-animationloop") ? $(this).data("animationloop") : false,
                'slideshow': $(this).attr("data-slideshow") ? $(this).data("slideshow") : false,
                'animation': "slide",
                'touch': $(this).attr("data-touch") ? $(this).data("touch") : false,
                'controlNav': $(this).attr("data-controlnav") ? $(this).data("controlnav") : false,
                'directionNav': $(this).attr("data-directionnav") ? $(this).data("directionnav") : false,
                'itemWidth': $(this).attr("data-itemwidth") ? $(this).data("itemwidth") : 210,
                'itemMargin': $(this).attr("data-itemmargin") ? $(this).data("itemmargin") : 5,
                'minItems': $(this).attr("data-minitems") ? $(this).data("minitems") : 0,
                'maxItems': $(this).attr("data-maxitems") ? $(this).data("maxitems") : 0,
                'move': $(this).attr("data-move") ? $(this).data("move") : 0,
                'asNavFor': $(this).attr("data-asnavfor") ? $(this).data("asnavfor") : ""
            });
        });
        $(".flexslider.flex-slider", element).each(function () {
            $(this).flexslider({
                'animationLoop': $(this).attr("data-animationloop") ? $(this).data("animationloop") : false,
                'slideshow': $(this).attr("data-slideshow") ? $(this).data("slideshow") : false,

                'animation': $(this).attr("data-animation") ? $(this).data("animation") : "slide",
                'touch': $(this).attr("data-touch") ? $(this).data("touch") : false,
                'controlNav': $(this).attr("data-controlnav") ? $(this).data("controlnav") : false,
                'directionNav': $(this).attr("data-directionnav") ? $(this).data("directionnav") : false,
                'sync': $(this).attr("data-sync") ? $(this).data("sync") : ""

            });
        });
        $(".jplist-detail", element).each(function () {

            var moduleId = $(this).attr('data-moduleid');
            var itemId = $(this).attr('data-id');
            var itemTitle = $(this).attr('data-title');

            var sf = $.ServicesFramework(moduleId);

            var $submitButton = $('#submit-' + moduleId);
            var $modal = $('#modal-' + moduleId);
            var $contactForm = $('.contactform', $modal);

            var form = $contactForm.openContentForm({
                servicesFramework: sf,
                onSubmit: function (data) {
                    data.Title = itemTitle;
                },
                onSubmited: function (data) {
                    form.destroy();
		    if (data.errors && data.errors.length) {
                        $contactForm.text(data.errors.join(", "));
                    } else {
                        $contactForm.text(data.message);
                    }
                    $submitButton.hide();
                    //$modal.modal('hide');
                }
            });
            $submitButton.click(function () {
                form.submit(itemId);
            });
        });
    }
    if (typeof Handlebars != 'undefined') {
        Handlebars.registerHelper('formatDateTime', function (context, format) {
            if (window.moment && context && moment(context).isValid()) {
                var f = format || "DD/MM/YYYY";
                return moment(context).format(f);
            } else {
                return context;   //  moment plugin is not available, context does not have a truthy value, or context is not a valid date
            }
        });
    }
}(jQuery));
<|endoftext|>


<commit_before>const test = require('tape');
const nvar = require('../lib');

// Helper to initialise new instances for each test.
function nv(source) {return nvar({source, target: Object.create(null)});}

// Note: tests are less unit and more accumulative - successive sections may build upon functionality established in the preceding sections.
// E.g. after backslash-escaping has been introduced and tested, the subsequent single-quotes section may then test for the case of backslashes appearing within quotes.

test('Unquoted values', function (t) {
  t.deepEqual(nv('toast=kaya'), {toast: 'kaya'}, 'Handles basic unquoted assignment.');
  t.deepEqual(nv('toast=kaya nope'), {toast: 'kaya'}, 'Ignores non-assignments.');
  t.deepEqual(nv('toast=kaya eggs=hardboiled nope nope nope'), {toast: 'kaya', eggs: 'hardboiled'}, 'Accepts multiple assignments at the start of a single line.');
  t.deepEqual(nv('toast=kaya nope sorry=darling'), {toast: 'kaya'}, 'Ignores assignment-like statements once a non-assignment has been encountered.');

  t.end();
});

// Canon: https://www.gnu.org/software/bash/manual/html_node/Escape-Character.html
test('Escaping with backslashes', function (t) {
  t.deepEqual(nv('toast=ka\\\nya'), {toast: 'kaya'}, 'Removes backslash-newline combination.');
  t.deepEqual(nv('science=`\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\_\\+\\[\\]\\\\\\{\\}\\|\\;\\:\\\'\\"\\,\\.\\/\\<\\>\\?\\a\\b\\c\\1\\2\\3'), {science: 'science=`~!@#$%^&*()-=_+[]\\{}|;:\'",./<>?abc123'}, 'Preserves literal value of character following a backslash.');
  t.deepEqual(nv('toast=ka\\ ya'), {toast: 'ka ya'}, 'Allows escaping of the space character in an unquoted value.');

  t.end();
});

// Canon: https://www.gnu.org/software/bash/manual/html_node/Single-Quotes.html
test('Escaping with single-quotes', function (t) {
  t.deepEqual(nv("toast='kaya'"), {toast: 'kaya'}, 'Does not include surrounding quotes in final value.');
  t.deepEqual(nv("toast='  kaya toast\t'"), {toast: '  kaya toast\t'}, 'Treats spaces and tabs literally.');
  t.deepEqual(nv("toast='kaya\ntoast'"), {toast: 'kaya\ntoast'}, 'Treats newlines literally.');
  t.deepEqual(nv("science='\\$\\`\\\n\\\\'"), {science: '\\$\\`\\\n\\\\'}, 'Prevents backslash-escaping.');
  t.deepEqual(nv("science='`~!@#$%^&*()-=_+[]\\{}|;:\",./<>?'"), {science: '`~!@#$%^&*()-=_+[]\\{}|;:",./<>?'}, 'Treats all other symbol characters (except single-quotes) literally.');

  t.end();
});
<commit_msg>Spec parameter expansion (basic only).<commit_after>const test = require('tape');
const nvar = require('../lib');

// Helper to initialise new instances for each test.
function nv(source) {return nvar({source, target: Object.create(null)});}

// Note: tests are less unit and more accumulative - successive sections may build upon functionality established in the preceding sections.
// E.g. after backslash-escaping has been introduced and tested, the subsequent single-quotes section may then test for the case of backslashes appearing within quotes.

test('Unquoted values', function (t) {
  t.deepEqual(nv('toast=kaya'), {toast: 'kaya'}, 'Handles basic unquoted assignment.');
  t.deepEqual(nv('toast=kaya nope'), {toast: 'kaya'}, 'Ignores non-assignments.');
  t.deepEqual(nv('toast=kaya eggs=hardboiled nope nope nope'), {toast: 'kaya', eggs: 'hardboiled'}, 'Accepts multiple assignments at the start of a single line.');
  t.deepEqual(nv('toast=kaya nope sorry=darling'), {toast: 'kaya'}, 'Ignores assignment-like statements once a non-assignment has been encountered.');

  t.end();
});

// Canon: https://www.gnu.org/software/bash/manual/html_node/Escape-Character.html
test('Escaping with backslashes', function (t) {
  t.deepEqual(nv('toast=ka\\\nya'), {toast: 'kaya'}, 'Removes backslash-newline combination.');
  t.deepEqual(nv('science=`\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\_\\+\\[\\]\\\\\\{\\}\\|\\;\\:\\\'\\"\\,\\.\\/\\<\\>\\?\\a\\b\\c\\1\\2\\3'), {science: 'science=`~!@#$%^&*()-=_+[]\\{}|;:\'",./<>?abc123'}, 'Preserves literal value of character following a backslash.');
  t.deepEqual(nv('toast=ka\\ ya'), {toast: 'ka ya'}, 'Allows escaping of the space character in an unquoted value.');

  t.end();
});

// Canon: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
test('Basic Parameter expansion', function (t) {
  // Define custom fixture-maker in this case, as parameter expansion relies on pre-existing variables.
  function nv(source) {
    const target = Object.create(null);
    target.style = 'retro'; // Simulates a pre-existing variable.
    return nvar({source, target});
  }
  t.equal(nv('genre=$style').genre, 'retro', 'Handles basic parameter expansion (unbraced).');
  t.equal(nv('genre=${style}').genre, 'retro', 'Handles basic parameter expansion (braced).');
  t.equal(nv('superstyle=super$style').superstyle, 'superretro', 'Handles unbraced parameter expansion with literal prefix.');
  t.equal(nv('direction=${style}grade').direction, 'retrograde', 'Handles braced parameter expansion with literal suffix.');
  t.equal(nv('direction=$style\\grade').direction, 'retrograde', 'Handles demarcating the end of a variable name with a backslash in an unbraced expansion.');
  t.equal(nv('dollarstyle=\\$style').dollarstyle, '$style', 'Does not expand if $-char is escaped.');
  t.equal(nv('dollarbracestyle=$\\{style').dollarbracestyle, '$style', 'Does not expand if opening brace is escaped.');

  t.end();
});

// MAYBE-TODO: Advanced Parameter expansion, e.g. all the forms listed in https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html

// Canon: https://www.gnu.org/software/bash/manual/html_node/Single-Quotes.html
test('Escaping with single-quotes', function (t) {
  t.deepEqual(nv("toast='kaya'"), {toast: 'kaya'}, 'Does not include surrounding quotes in final value.');
  t.deepEqual(nv("toast='  kaya toast\t'"), {toast: '  kaya toast\t'}, 'Treats spaces and tabs literally.');
  t.deepEqual(nv("toast='kaya\ntoast'"), {toast: 'kaya\ntoast'}, 'Treats newlines literally.');
  t.deepEqual(nv("science='\\$\\`\\\n\\\\'"), {science: '\\$\\`\\\n\\\\'}, 'Prevents backslash-escaping.');
  t.deepEqual(nv("science='`~!@#$%^&*()-=_+[]\\{}|;:\",./<>?'"), {science: '`~!@#$%^&*()-=_+[]\\{}|;:",./<>?'}, 'Treats all other symbol characters (except single-quotes) literally.');

  t.end();
});
<|endoftext|>


<commit_before>import _ from 'lodash';

import treeherder from '../treeherder';
import { getApiUrl } from '../../helpers/url';
import { thRepoGroupOrder } from "../constants";
import TreeStatusModel from '../../models/treeStatus';

treeherder.factory('ThRepositoryModel', [
    '$http', '$rootScope', '$interval', '$q',
    function (
        $http, $rootScope, $interval, $q) {

        const repos = {};
        const watchedRepos = [];
        const orderedRepoGroups = {};
        const maxWatchedRepos = 3;

        // get the repositories (aka trees)
        // sample: 'resources/menu.json'
        const getByName = function (name) {
            if ($rootScope.repos !== undefined) {
                for (let i = 0; i < $rootScope.repos.length; i++) {
                    const repo = $rootScope.repos[i];
                    if (repo.name === name) {
                        return repo;
                    }
                }
            }
            return null;
        };

        const getOrderedRepoGroups = function () {
            if (!Object.keys(orderedRepoGroups).length) {
                // group repos by 'repository_group.name'
                const groups = $rootScope.repos.reduce((acc, repo, idx, arr, group = repo => repo.repository_group.name) => (
                    { ...acc, [group(repo)]: [...acc[group(repo)] || [], repo] }
                ), {});
                _.each(groups, function (reposAr, gName) {
                    orderedRepoGroups[thRepoGroupOrder[gName] || gName] = { name: gName, repos: reposAr };
                });
            }
            return orderedRepoGroups;
        };

        /**
         * if the repo isn't supported by treestatus, then these are the generic
         * values to use for it.
         * setting the value to 'unsupported' means that it won't bother checking
         * treestatus again for that repo when the interval does the updates.
         */
        const getUnsupportedTreeStatus = function (repoName) {
            return {
                status: "unsupported",
                message_of_the_day: `${repoName} is not listed on <a href="https://mozilla-releng.net/treestatus">TreeStatus</a>`,
                reason: "",
                tree: repoName
            };
        };

        /**
         * if there's an error fetching data from treestatus, make that obvious
         * in the treestatus field in treeherder
         */
        const getErrorTreeStatus = function (repoName) {
            return {
                status: "error",
                message_of_the_day: 'Unable to connect to the TreeStatus API',
                reason: '',
                tree: repoName
            };
        };

        /**
         * Update the status for ``repoName``.  If it's not passed in,
         * then update all ``watchedRepos`` status.
         * @param repoName
         * @returns a promise
         */
        const updateTreeStatus = function (repoName) {
            // The $interval will pass in the number of times it was called,
            // rather than a ``repoName``.  So repoName would equal 1, 2, 3.  So
            // if repoName isn't a valid watched repo, we update all.
            let repoNames = watchedRepos.indexOf(repoName) !== -1 ? [repoName] : watchedRepos;

            // filter out non-watched and unsupported repos to prevent repeatedly
            // hitting an endpoint we know will never work.
            repoNames = repoNames.filter(repo => watchedRepos.indexOf(repo) !== -1 && repos[repo].treeStatus.status !== 'unsupported');
            const newStatuses = {};

            const updateStatusesIfDone = function () {
                if (Object.keys(newStatuses).length === repoNames.length) {
                    // we've received all the statuses we expect to
                    _.defer(function () {
                        _.each(newStatuses, function (status) {
                            repos[TreeStatusModel.getRepoName(status.tree)].treeStatus = status;
                        });
                    });
                }
            };

            const getStatus = function (repo) {
                TreeStatusModel.get(repo).then(
                    function (data) {
                        newStatuses[repo] = data.result;
                        updateStatusesIfDone();
                    },
                    function (data) {
                        if (data !== null) {
                            newStatuses[repo] = getUnsupportedTreeStatus(repo);
                        } else {
                            newStatuses[repo] = getErrorTreeStatus(repo);
                        }
                        updateStatusesIfDone();
                    });
            };

            _.each(repoNames, getStatus);
        };

        const addRepoAsUnwatched = function (repo) {
            repos[repo.name] = {
                treeStatus: null,
                unclassifiedFailureCount: 0,
                groupName: repo.repository_group.name
            };
        };

        const loadWatchedRepos = function () {
            try {
                return JSON.parse(localStorage.getItem("thWatchedRepos"));
            } catch (e) {
                // localStorage is disabled/not supported.
                return [];
            }
        };

        const saveWatchedRepos = function () {
            try {
                localStorage.setItem("thWatchedRepos", JSON.stringify(watchedRepos));
            } catch (e) {
                // localStorage is disabled/not supported.
            }
        };

        /**
         * We want to add this repo as watched, but we also
         * want to get the treestatus for it
         */
        const watchRepo = function (name) {
            // Safeguard: Don't allow duplicates in the watch list
            // Also, only add items for which we have data for
            if (watchedRepos.indexOf(name) !== -1 || !repos[name]) {
                return;
            }
            _.extend(repos[name], {
                treeStatus: { status: "not retrieved yet", message_of_the_day: "" },
                unclassifiedFailureCount: 0,
                groupName: repos[name].groupName
            });
            watchedRepos.unshift(name);
            updateTreeStatus(name);
            // Limit to maxiumum saved repos at a time
            if (watchedRepos.length > maxWatchedRepos) {
                watchedRepos.length = maxWatchedRepos;
            }
            saveWatchedRepos();
        };

        const unwatchRepo = function (name) {
            const pos = watchedRepos.indexOf(name);
            if (pos > -1) {
                watchedRepos.splice(pos, 1);
            }
            saveWatchedRepos();
        };

        const get_uri = function () {
            return getApiUrl("/repository/");
        };

        const get_list = function () {
            return $http.get(get_uri(), { cache: true });
        };

        const getCurrent = function () {
            return $rootScope.currentRepo;
        };

        const setCurrent = function (name) {
            if (!$rootScope.currentRepo || $rootScope.currentRepo.name !== name) {
                $rootScope.currentRepo = getByName(name);

                // don't want to just replace the watchedRepos object because
                // controllers, etc, are watching the reference to it, which would
                // be lost by replacing.
                if (watchedRepos.length <= 1) {
                    _.each(watchedRepos, function (r, rname) {
                        unwatchRepo(rname);
                    });
                }
                if (!_.has(watchedRepos, name)) {
                    watchRepo(name);
                }

            }
        };

        const load = function (options) {
            options = options || {};

            if (!$rootScope.repos) {
                // this is the first time this was called, so initiate the interval
                // update the repo status (treestatus) in an interval of every 2 minutes
                $interval(updateTreeStatus, 2 * 60 * 1000);

                // return the promise of getting the repos
                return get_list()
                    .then(({ data }) => {
                        // FIXME: only supporting github + hg for now for pushlog
                        // + revision info (we also assume dvcs_type git===github)
                        function Repo(props) {
                            Object.assign(this, props);
                            if (this.dvcs_type === 'git') {
                                // FIXME: assuming master branch, which may not
                                // always be right -- unfortunately fixing this
                                // requires backend changes as we're not storing
                                // such info explicitly right now
                                this.pushlogURL = this.url + "/commits/master";
                            } else {
                                this.pushlogURL = this.url + "/pushloghtml";
                            }
                        }
                        Repo.prototype = {
                            getRevisionHref: function (revision) {
                                if (this.dvcs_type === 'git') {
                                    return this.url + '/commit/' + revision;
                                }
                                return this.url + '/rev/' + revision;
                            },
                            getPushLogHref: function (arg) {
                                if (this.dvcs_type === 'git') {
                                    // if git, assume github
                                    if (typeof (arg) === 'string') {
                                        return this.getRevisionHref(arg);
                                    } else if (arg && arg.from && arg.to) {
                                        return this.url + '/compare/' + arg.from + '...' +
                                            arg.to;
                                    }
                                } else if (this.dvcs_type === 'hg') {
                                    if (typeof (arg) === 'string') {
                                        return this.pushlogURL + '?changeset=' + arg;
                                    } else if (arg && arg.from && arg.to) {
                                        return this.pushlogURL + '?fromchange=' +
                                            arg.from +'&tochange=' + arg.to;
                                    }
                                }

                                // if neither git nor mercurial, undefined
                                return "";
                            }
                        };

                        $rootScope.repos = _.map(data, function (datum) {
                            return new Repo(datum);
                        });

                        _.each(data, addRepoAsUnwatched);

                        // This needs to be done before `setCurrent` because
                        // `setCurrent` overwrites the entire listing
                        // with only the default repo
                        let storedWatched;
                        if (options.watchRepos) {
                            storedWatched = loadWatchedRepos();
                        }

                        if (options.name) {
                            setCurrent(options.name);
                        }
                        if (options.watchRepos) {
                            if (Array.isArray(storedWatched)) {

                                // To get the current repo to display first, we must
                                // ensure it's added to the array last, as per normal user interaction
                                storedWatched = storedWatched.filter(value => (value !== options.name));
                                unwatchRepo(options.name);

                                // Add the repos in reverse order, like the user would (oldest to newest)
                                storedWatched.reverse();
                                storedWatched.push(options.name);

                                _.each(storedWatched, function (repo) {
                                    watchRepo(repo);
                                });
                            }
                            saveWatchedRepos();
                        }
                    });
            }

            if (options.name) {
                setCurrent(options.name);
            }
            return $q(function (resolve) {
                resolve('Already loaded');
            });
        };

        const toggleWatched = function (repoName) {
            if (watchedRepos[repoName]) {
                unwatchRepo(repoName);
            } else {
                watchRepo(repoName);
            }

        };

        const getCurrentTreeStatus = function () {
            try {
                return repos[$rootScope.repoName].treeStatus.status;
            } catch (Exception) {
                return "unavailable";
            }
        };

        return {
            // load the list of repos into $rootScope, and set the current repo.
            load: load,

            get_list: get_list,

            // return the currently selected repo
            getCurrent: getCurrent,

            // set the current repo to one in the repos list
            setCurrent: setCurrent,

            // get a repo object without setting anything
            getRepo: getByName,

            getOrderedRepoGroups: getOrderedRepoGroups,

            getCurrentTreeStatus: getCurrentTreeStatus,

            repos: repos,

            watchedRepos: watchedRepos,

            loadWatchedRepos: loadWatchedRepos,

            saveWatchedRepos: saveWatchedRepos,

            unwatchRepo: unwatchRepo,

            toggleWatched: toggleWatched

        };
    }]);
<commit_msg>Bug 1468533 - Fix race condition throwing an error about .repos (#3659)<commit_after>import _ from 'lodash';

import treeherder from '../treeherder';
import { getApiUrl } from '../../helpers/url';
import { thRepoGroupOrder } from "../constants";
import TreeStatusModel from '../../models/treeStatus';

treeherder.factory('ThRepositoryModel', [
    '$http', '$rootScope', '$interval', '$q',
    function (
        $http, $rootScope, $interval, $q) {

        const repos = {};
        const watchedRepos = [];
        const orderedRepoGroups = {};
        const maxWatchedRepos = 3;

        // get the repositories (aka trees)
        // sample: 'resources/menu.json'
        const getByName = function (name) {
            if ($rootScope.repos !== undefined) {
                for (let i = 0; i < $rootScope.repos.length; i++) {
                    const repo = $rootScope.repos[i];
                    if (repo.name === name) {
                        return repo;
                    }
                }
            }
            return null;
        };

        const getOrderedRepoGroups = function () {
            if (!Object.keys(orderedRepoGroups).length) {
                // group repos by 'repository_group.name'
                if ($rootScope.repos) {
                  const groups = $rootScope.repos.reduce((acc, repo, idx, arr, group = repo => repo.repository_group.name) => (
                      { ...acc, [group(repo)]: [...acc[group(repo)] || [], repo] }
                  ), {});
                  _.each(groups, function (reposAr, gName) {
                      orderedRepoGroups[thRepoGroupOrder[gName] || gName] = { name: gName, repos: reposAr };
                  });
                }
            }
            return orderedRepoGroups;
        };

        /**
         * if the repo isn't supported by treestatus, then these are the generic
         * values to use for it.
         * setting the value to 'unsupported' means that it won't bother checking
         * treestatus again for that repo when the interval does the updates.
         */
        const getUnsupportedTreeStatus = function (repoName) {
            return {
                status: "unsupported",
                message_of_the_day: `${repoName} is not listed on <a href="https://mozilla-releng.net/treestatus">TreeStatus</a>`,
                reason: "",
                tree: repoName
            };
        };

        /**
         * if there's an error fetching data from treestatus, make that obvious
         * in the treestatus field in treeherder
         */
        const getErrorTreeStatus = function (repoName) {
            return {
                status: "error",
                message_of_the_day: 'Unable to connect to the TreeStatus API',
                reason: '',
                tree: repoName
            };
        };

        /**
         * Update the status for ``repoName``.  If it's not passed in,
         * then update all ``watchedRepos`` status.
         * @param repoName
         * @returns a promise
         */
        const updateTreeStatus = function (repoName) {
            // The $interval will pass in the number of times it was called,
            // rather than a ``repoName``.  So repoName would equal 1, 2, 3.  So
            // if repoName isn't a valid watched repo, we update all.
            let repoNames = watchedRepos.indexOf(repoName) !== -1 ? [repoName] : watchedRepos;

            // filter out non-watched and unsupported repos to prevent repeatedly
            // hitting an endpoint we know will never work.
            repoNames = repoNames.filter(repo => watchedRepos.indexOf(repo) !== -1 && repos[repo].treeStatus.status !== 'unsupported');
            const newStatuses = {};

            const updateStatusesIfDone = function () {
                if (Object.keys(newStatuses).length === repoNames.length) {
                    // we've received all the statuses we expect to
                    _.defer(function () {
                        _.each(newStatuses, function (status) {
                            repos[TreeStatusModel.getRepoName(status.tree)].treeStatus = status;
                        });
                    });
                }
            };

            const getStatus = function (repo) {
                TreeStatusModel.get(repo).then(
                    function (data) {
                        newStatuses[repo] = data.result;
                        updateStatusesIfDone();
                    },
                    function (data) {
                        if (data !== null) {
                            newStatuses[repo] = getUnsupportedTreeStatus(repo);
                        } else {
                            newStatuses[repo] = getErrorTreeStatus(repo);
                        }
                        updateStatusesIfDone();
                    });
            };

            _.each(repoNames, getStatus);
        };

        const addRepoAsUnwatched = function (repo) {
            repos[repo.name] = {
                treeStatus: null,
                unclassifiedFailureCount: 0,
                groupName: repo.repository_group.name
            };
        };

        const loadWatchedRepos = function () {
            try {
                return JSON.parse(localStorage.getItem("thWatchedRepos"));
            } catch (e) {
                // localStorage is disabled/not supported.
                return [];
            }
        };

        const saveWatchedRepos = function () {
            try {
                localStorage.setItem("thWatchedRepos", JSON.stringify(watchedRepos));
            } catch (e) {
                // localStorage is disabled/not supported.
            }
        };

        /**
         * We want to add this repo as watched, but we also
         * want to get the treestatus for it
         */
        const watchRepo = function (name) {
            // Safeguard: Don't allow duplicates in the watch list
            // Also, only add items for which we have data for
            if (watchedRepos.indexOf(name) !== -1 || !repos[name]) {
                return;
            }
            _.extend(repos[name], {
                treeStatus: { status: "not retrieved yet", message_of_the_day: "" },
                unclassifiedFailureCount: 0,
                groupName: repos[name].groupName
            });
            watchedRepos.unshift(name);
            updateTreeStatus(name);
            // Limit to maxiumum saved repos at a time
            if (watchedRepos.length > maxWatchedRepos) {
                watchedRepos.length = maxWatchedRepos;
            }
            saveWatchedRepos();
        };

        const unwatchRepo = function (name) {
            const pos = watchedRepos.indexOf(name);
            if (pos > -1) {
                watchedRepos.splice(pos, 1);
            }
            saveWatchedRepos();
        };

        const get_uri = function () {
            return getApiUrl("/repository/");
        };

        const get_list = function () {
            return $http.get(get_uri(), { cache: true });
        };

        const getCurrent = function () {
            return $rootScope.currentRepo;
        };

        const setCurrent = function (name) {
            if (!$rootScope.currentRepo || $rootScope.currentRepo.name !== name) {
                $rootScope.currentRepo = getByName(name);

                // don't want to just replace the watchedRepos object because
                // controllers, etc, are watching the reference to it, which would
                // be lost by replacing.
                if (watchedRepos.length <= 1) {
                    _.each(watchedRepos, function (r, rname) {
                        unwatchRepo(rname);
                    });
                }
                if (!_.has(watchedRepos, name)) {
                    watchRepo(name);
                }

            }
        };

        const load = function (options) {
            options = options || {};

            if (!$rootScope.repos) {
                // this is the first time this was called, so initiate the interval
                // update the repo status (treestatus) in an interval of every 2 minutes
                $interval(updateTreeStatus, 2 * 60 * 1000);

                // return the promise of getting the repos
                return get_list()
                    .then(({ data }) => {
                        // FIXME: only supporting github + hg for now for pushlog
                        // + revision info (we also assume dvcs_type git===github)
                        function Repo(props) {
                            Object.assign(this, props);
                            if (this.dvcs_type === 'git') {
                                // FIXME: assuming master branch, which may not
                                // always be right -- unfortunately fixing this
                                // requires backend changes as we're not storing
                                // such info explicitly right now
                                this.pushlogURL = this.url + "/commits/master";
                            } else {
                                this.pushlogURL = this.url + "/pushloghtml";
                            }
                        }
                        Repo.prototype = {
                            getRevisionHref: function (revision) {
                                if (this.dvcs_type === 'git') {
                                    return this.url + '/commit/' + revision;
                                }
                                return this.url + '/rev/' + revision;
                            },
                            getPushLogHref: function (arg) {
                                if (this.dvcs_type === 'git') {
                                    // if git, assume github
                                    if (typeof (arg) === 'string') {
                                        return this.getRevisionHref(arg);
                                    } else if (arg && arg.from && arg.to) {
                                        return this.url + '/compare/' + arg.from + '...' +
                                            arg.to;
                                    }
                                } else if (this.dvcs_type === 'hg') {
                                    if (typeof (arg) === 'string') {
                                        return this.pushlogURL + '?changeset=' + arg;
                                    } else if (arg && arg.from && arg.to) {
                                        return this.pushlogURL + '?fromchange=' +
                                            arg.from +'&tochange=' + arg.to;
                                    }
                                }

                                // if neither git nor mercurial, undefined
                                return "";
                            }
                        };

                        $rootScope.repos = _.map(data, function (datum) {
                            return new Repo(datum);
                        });

                        _.each(data, addRepoAsUnwatched);

                        // This needs to be done before `setCurrent` because
                        // `setCurrent` overwrites the entire listing
                        // with only the default repo
                        let storedWatched;
                        if (options.watchRepos) {
                            storedWatched = loadWatchedRepos();
                        }

                        if (options.name) {
                            setCurrent(options.name);
                        }
                        if (options.watchRepos) {
                            if (Array.isArray(storedWatched)) {

                                // To get the current repo to display first, we must
                                // ensure it's added to the array last, as per normal user interaction
                                storedWatched = storedWatched.filter(value => (value !== options.name));
                                unwatchRepo(options.name);

                                // Add the repos in reverse order, like the user would (oldest to newest)
                                storedWatched.reverse();
                                storedWatched.push(options.name);

                                _.each(storedWatched, function (repo) {
                                    watchRepo(repo);
                                });
                            }
                            saveWatchedRepos();
                        }
                    });
            }

            if (options.name) {
                setCurrent(options.name);
            }
            return $q(function (resolve) {
                resolve('Already loaded');
            });
        };

        const toggleWatched = function (repoName) {
            if (watchedRepos[repoName]) {
                unwatchRepo(repoName);
            } else {
                watchRepo(repoName);
            }

        };

        const getCurrentTreeStatus = function () {
            try {
                return repos[$rootScope.repoName].treeStatus.status;
            } catch (Exception) {
                return "unavailable";
            }
        };

        return {
            // load the list of repos into $rootScope, and set the current repo.
            load: load,

            get_list: get_list,

            // return the currently selected repo
            getCurrent: getCurrent,

            // set the current repo to one in the repos list
            setCurrent: setCurrent,

            // get a repo object without setting anything
            getRepo: getByName,

            getOrderedRepoGroups: getOrderedRepoGroups,

            getCurrentTreeStatus: getCurrentTreeStatus,

            repos: repos,

            watchedRepos: watchedRepos,

            loadWatchedRepos: loadWatchedRepos,

            saveWatchedRepos: saveWatchedRepos,

            unwatchRepo: unwatchRepo,

            toggleWatched: toggleWatched

        };
    }]);
<|endoftext|>


<commit_before>/**
 * Chill of the day -
 * There is no shelter from aging and death. Knowing this inevitability, Seek joy instead in the goodness of your actions - Buddha
 *
 * @author - heartit pirates
 */

"use strict";

import React, { Component } from "react";
import { connect } from "react-redux";
import * as actionCreators from "../badlee__redux/action_creators";

import {
  StyleProvider,
  Content,
  TabHeading,
  Tabs,
  Tab,
  Text
} from "native-base";
import getTheme from "../theme/components";

import Icon from "../components/Icon";

import Badlees from "./main/Badlees";
import UserProfile from "./account/User";

class Main extends Component {
  constructor() {
    super();
    this.state = {
      initial_tab: 0
    };
  }
  componentDidMount() {}
  render() {
    return (
      <StyleProvider style={getTheme()}>
        <Content style={{ flex: 1 }} contentContainerStyle={{ flex: 1 }}>
          <Tabs
            initialPage={this.state.initial_tab}
            tabBarUnderlineStyle={{ borderBottomWidth: 0 }}
          >
            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="home"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <Badlees />
            </Tab>
            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="messages"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <Text>Wingardium Leviousa</Text>
            </Tab>

            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="notifications"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <Text>One Piece is hidden somewhere in the app</Text>
            </Tab>
            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="user"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <UserProfile />
            </Tab>
          </Tabs>
        </Content>
      </StyleProvider>
    );
  }
}

var styles = {};

const _Wrapped = connect(
  state => ({ user: state.get("user") }),
  actionCreators
)(Main);

export default _Wrapped;
<commit_msg>added description and fixed paths<commit_after>/**
 * Chill of the day -
 * There is no shelter from aging and death. Knowing this inevitability, Seek joy instead in the goodness of your actions - Buddha
 *
 * @name- Container.JS
 * @description- This file is the starting point of the badlee Authenticated section. Here, we are loading other modules and make main tab structures. We can also do things like Websocket here.
 * @author- heartit pirates
 */

"use strict";

import React, { Component } from "react";
import { connect } from "react-redux";
import * as actionCreators from "../../badlee__redux/action_creators";

import {
  StyleProvider,
  Content,
  TabHeading,
  Tabs,
  Tab,
  Text
} from "native-base";
import getTheme from "../../theme/components";

import Icon from "../../components/Icon";

// import Badlees from "./main/Badlees";
// import UserProfile from "./account/User";

class AuthContainer extends Component {
  constructor() {
    super();
    this.state = {
      initial_tab: 0
    };
  }
  componentDidMount() {}
  render() {
    return (
      <StyleProvider style={getTheme()}>
        <Content style={{ flex: 1 }} contentContainerStyle={{ flex: 1 }}>
          <Tabs
            initialPage={this.state.initial_tab}
            tabBarUnderlineStyle={{ borderBottomWidth: 0 }}
          >
            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="home"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <Text>Petrificious Totalus</Text>
            </Tab>
            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="messages"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <Text>Wingardium Leviousa</Text>
            </Tab>

            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="notifications"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <Text>One Piece is hidden somewhere in the app</Text>
            </Tab>
            <Tab
              heading={
                <TabHeading>
                  <Icon
                    name="user"
                    viewBox="0 0 60 60"
                    width="22"
                    height="22"
                  />
                </TabHeading>
              }
            >
              <Text>Userrr profile</Text>
            </Tab>
          </Tabs>
        </Content>
      </StyleProvider>
    );
  }
}

var styles = {};

const _Wrapped = connect(
  state => ({ user: state.get("user") }),
  actionCreators
)(AuthContainer);

export default _Wrapped;
<|endoftext|>


<commit_before>// @flow
import * as ACTIONS from 'constants/action_types';
import { buildURI, doResolveUris, batchActions } from 'lbry-redux';
import {
  makeSelectSearchUris,
  selectSuggestions,
  makeSelectQueryWithOptions,
  selectSearchValue,
} from 'redux/selectors/search';
import debounce from 'util/debounce';
import handleFetchResponse from 'util/handle-fetch';

const DEBOUNCED_SEARCH_SUGGESTION_MS = 300;
type Dispatch = (action: any) => any;
type GetState = () => { search: SearchState };

type SearchOptions = {
  size?: number,
  from?: number,
  related_to?: string,
  nsfw?: boolean,
  isBackgroundSearch?: boolean,
};

// We can't use env's because they aren't passed into node_modules
let CONNECTION_STRING = 'https://lighthouse.lbry.com/';

export const setSearchApi = (endpoint: string) => {
  CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end;
};

export const getSearchSuggestions = (value: string) => (dispatch: Dispatch, getState: GetState) => {
  const query = value.trim();

  // strip out any basic stuff for more accurate search results
  let searchValue = query.replace(/lbry:\/\//g, '').replace(/-/g, ' ');
  if (searchValue.includes('#')) {
    // This should probably be more robust, but I think it's fine for now
    // Remove everything after # to get rid of the claim id
    searchValue = searchValue.substring(0, searchValue.indexOf('#'));
  }

  const suggestions = selectSuggestions(getState());
  if (suggestions[searchValue]) {
    return;
  }

  fetch(`${CONNECTION_STRING}autocomplete?s=${encodeURIComponent(searchValue)}`)
    .then(handleFetchResponse)
    .then(apiSuggestions => {
      dispatch({
        type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
        data: {
          query: searchValue,
          suggestions: apiSuggestions,
        },
      });
    })
    .catch(() => {
      // If the fetch fails, do nothing
      // Basic search suggestions are already populated at this point
    });
};

const throttledSearchSuggestions = debounce((dispatch, query) => {
  dispatch(getSearchSuggestions(query));
}, DEBOUNCED_SEARCH_SUGGESTION_MS);

export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boolean) => (dispatch: Dispatch) => {
  dispatch({
    type: ACTIONS.UPDATE_SEARCH_QUERY,
    data: { query },
  });

  // Don't fetch new suggestions if the user just added a space
  if (!query.endsWith(' ') || !shouldSkipSuggestions) {
    throttledSearchSuggestions(dispatch, query);
  }
};

export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
  const isBackgroundSearch = (searchOptions && searchOptions.isBackgroundSearch) || false;

  if (!query) {
    dispatch({
      type: ACTIONS.SEARCH_FAIL,
    });
    return;
  }

  const state = getState();

  let queryWithOptions = makeSelectQueryWithOptions(query, searchOptions)(state);

  // If we have already searched for something, we don't need to do anything
  const urisForQuery = makeSelectSearchUris(queryWithOptions)(state);
  if (urisForQuery && !!urisForQuery.length) {
    return;
  }

  dispatch({
    type: ACTIONS.SEARCH_START,
  });

  // If the user is on the file page with a pre-populated uri and they select
  // the search option without typing anything, searchQuery will be empty
  // We need to populate it so the input is filled on the search page
  // isBackgroundSearch means the search is happening in the background, don't update the search query
  if (!state.search.searchQuery && !isBackgroundSearch) {
    dispatch(doUpdateSearchQuery(query));
  }

  fetch(`${CONNECTION_STRING}search?${queryWithOptions}`)
    .then(handleFetchResponse)
    .then((data: Array<{ name: string, claimId: string }>) => {
      const uris = [];
      const actions = [];

      data.forEach(result => {
        if (result) {
          const { name, claimId } = result;
          const urlObj: LbryUrlObj = {};

          if (name.startsWith('@')) {
            urlObj.channelName = name;
            urlObj.channelClaimId = claimId;
          } else {
            urlObj.streamName = name;
            urlObj.streamClaimId = claimId;
          }

          const url = buildURI(urlObj);
          uris.push(url);
        }
      });

      actions.push(doResolveUris(uris));

      actions.push({
        type: ACTIONS.SEARCH_SUCCESS,
        data: {
          query: queryWithOptions,
          uris,
        },
      });
      dispatch(batchActions(...actions));
    })
    .catch(e => {
      dispatch({
        type: ACTIONS.SEARCH_FAIL,
      });
    });
};

export const doFocusSearchInput = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SEARCH_FOCUS,
  });

export const doBlurSearchInput = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SEARCH_BLUR,
  });

export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptions: SearchOptions) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const state = getState();
  const searchValue = selectSearchValue(state);

  dispatch({
    type: ACTIONS.UPDATE_SEARCH_OPTIONS,
    data: newOptions,
  });

  if (searchValue) {
    // After updating, perform a search with the new options
    dispatch(doSearch(searchValue, additionalOptions));
  }
};
<commit_msg>Don't autocomplete for empty string search<commit_after>// @flow
import * as ACTIONS from 'constants/action_types';
import { buildURI, doResolveUris, batchActions } from 'lbry-redux';
import {
  makeSelectSearchUris,
  selectSuggestions,
  makeSelectQueryWithOptions,
  selectSearchValue,
} from 'redux/selectors/search';
import debounce from 'util/debounce';
import handleFetchResponse from 'util/handle-fetch';

const DEBOUNCED_SEARCH_SUGGESTION_MS = 300;
type Dispatch = (action: any) => any;
type GetState = () => { search: SearchState };

type SearchOptions = {
  size?: number,
  from?: number,
  related_to?: string,
  nsfw?: boolean,
  isBackgroundSearch?: boolean,
};

// We can't use env's because they aren't passed into node_modules
let CONNECTION_STRING = 'https://lighthouse.lbry.com/';

export const setSearchApi = (endpoint: string) => {
  CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end;
};

export const getSearchSuggestions = (value: string) => (dispatch: Dispatch, getState: GetState) => {
  const query = value.trim();

  // strip out any basic stuff for more accurate search results
  let searchValue = query.replace(/lbry:\/\//g, '').replace(/-/g, ' ');
  if (searchValue.includes('#')) {
    // This should probably be more robust, but I think it's fine for now
    // Remove everything after # to get rid of the claim id
    searchValue = searchValue.substring(0, searchValue.indexOf('#'));
  }

  const suggestions = selectSuggestions(getState());
  if (suggestions[searchValue]) {
    return;
  }

  fetch(`${CONNECTION_STRING}autocomplete?s=${encodeURIComponent(searchValue)}`)
    .then(handleFetchResponse)
    .then(apiSuggestions => {
      dispatch({
        type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
        data: {
          query: searchValue,
          suggestions: apiSuggestions,
        },
      });
    })
    .catch(() => {
      // If the fetch fails, do nothing
      // Basic search suggestions are already populated at this point
    });
};

const throttledSearchSuggestions = debounce((dispatch, query) => {
  dispatch(getSearchSuggestions(query));
}, DEBOUNCED_SEARCH_SUGGESTION_MS);

export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boolean) => (dispatch: Dispatch) => {
  dispatch({
    type: ACTIONS.UPDATE_SEARCH_QUERY,
    data: { query },
  });

  if (!query) return;

  // Don't fetch new suggestions if the user just added a space
  if (!query.endsWith(' ') || !shouldSkipSuggestions) {
    throttledSearchSuggestions(dispatch, query);
  }
};

export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
  const isBackgroundSearch = (searchOptions && searchOptions.isBackgroundSearch) || false;

  if (!query) {
    dispatch({
      type: ACTIONS.SEARCH_FAIL,
    });
    return;
  }

  const state = getState();

  let queryWithOptions = makeSelectQueryWithOptions(query, searchOptions)(state);

  // If we have already searched for something, we don't need to do anything
  const urisForQuery = makeSelectSearchUris(queryWithOptions)(state);
  if (urisForQuery && !!urisForQuery.length) {
    return;
  }

  dispatch({
    type: ACTIONS.SEARCH_START,
  });

  // If the user is on the file page with a pre-populated uri and they select
  // the search option without typing anything, searchQuery will be empty
  // We need to populate it so the input is filled on the search page
  // isBackgroundSearch means the search is happening in the background, don't update the search query
  if (!state.search.searchQuery && !isBackgroundSearch) {
    dispatch(doUpdateSearchQuery(query));
  }

  fetch(`${CONNECTION_STRING}search?${queryWithOptions}`)
    .then(handleFetchResponse)
    .then((data: Array<{ name: string, claimId: string }>) => {
      const uris = [];
      const actions = [];

      data.forEach(result => {
        if (result) {
          const { name, claimId } = result;
          const urlObj: LbryUrlObj = {};

          if (name.startsWith('@')) {
            urlObj.channelName = name;
            urlObj.channelClaimId = claimId;
          } else {
            urlObj.streamName = name;
            urlObj.streamClaimId = claimId;
          }

          const url = buildURI(urlObj);
          uris.push(url);
        }
      });

      actions.push(doResolveUris(uris));

      actions.push({
        type: ACTIONS.SEARCH_SUCCESS,
        data: {
          query: queryWithOptions,
          uris,
        },
      });
      dispatch(batchActions(...actions));
    })
    .catch(e => {
      dispatch({
        type: ACTIONS.SEARCH_FAIL,
      });
    });
};

export const doFocusSearchInput = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SEARCH_FOCUS,
  });

export const doBlurSearchInput = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SEARCH_BLUR,
  });

export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptions: SearchOptions) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const state = getState();
  const searchValue = selectSearchValue(state);

  dispatch({
    type: ACTIONS.UPDATE_SEARCH_OPTIONS,
    data: newOptions,
  });

  if (searchValue) {
    // After updating, perform a search with the new options
    dispatch(doSearch(searchValue, additionalOptions));
  }
};
<|endoftext|>