<commit_before>/**
 * Placeholders
 *
 * Provides functionality to extract, parse and populate placeholders.
 */

import getValue from 'lodash/get'
import * as Filters from './filters'

/**
 * Extracts placeholders from a string. Placeholders are identified by {}.
 *
 * @param {String} string
 * @returns {Array}
 */
export function extractPlaceholders (string) {

  // get placeholders
  let placeholders = []

  // match placeholders identified by {}
  let regex = /(?![^(]*\)|[^[]*]){([^}]+)}/g
  let match = regex.exec(string)
  while (match) {

    // parse placeholder
    if (match[0].split('{').length - 1 === match[0].split('}').length - 1) {
      let parsedPlaceholder = parsePlaceholder(match[0])

      // add to placeholders array
      placeholders.push(parsedPlaceholder)
    }

    // parse next placeholder
    match = regex.exec(string)
  }

  return placeholders
}

/**
 * Parses the placeholder. The string contains the content of the placeholder
 * without being wrapped in () or {}, e.g. firstName, lastName | & •
 *
 * @param {String} placeholderString
 * @returns {Object}
 *
 * Example placeholders (shown with {}):
 *
 * - {firstName}, {name.first} - John
 * - {firstName, lastName | & • } - John • Doe
 * - {(lastName?, firstName | &, ), DOB | & born on } - Doe, John born on 14/07/1970
 * - {firstName | upper} - JOHN
 * - {firstName | upper | max 2} - JO
 * - {(firstName | upper | max 2), (lastName | max 1) | & • } - JO • D
 * - {keypath?} - The default substitute
 * - {keypath?not available} - not available
 * - {firstName?First name not available, lastName?No last name, DOB? | & - }
 * - TODO: quotation marks {firstName?"First name, not | available \" ,,", lastName?'No, \' | last name', DOB? | & - }
 *   TODO:                '{firstName?"First name, not | available \\" ,,", lastName?\'No, \\\' | last name\', DOB? | & - }'
 *
 * Example nesting:
 * - {(lastName?'', firstName | &, ), DOB | & born on }
 *   - (lastName?'', firstName | &, )
 *     - lastName?''
 *     - firstName
 *   - DOB
 *
 * returned placeholder format: [{
 *   string: {String}, - the original text of the placeholder, e.g. {name.first}
 *   keypath: {String}, - the path to the data, e.g. name.first
 *   filters: {Array}, - filters applied through the pipe character, adding in the correct order
 *   substitute: {String}, - string to use in case the placeholder resolves to an empty string
 *   placeholders: {Array} - nested placeholders with the same format as this placeholder
 * }]
 *
 * Example parsed placeholders:
 *
 * [
 *   {
 *     "string":"{firstName}",
 *     "keypath":"firstName"
 *   },
 *
 *   {
 *     "string":"{name.first}",
 *     "keypath":"name.first"
 *   },
 *
 *   {
 *     "string":"{firstName, lastName | & • }",
 *     "filters":[
 *       {
 *         "command":"&",
 *         "param":" • "
 *       }
 *     ],
 *     "placeholders":[
 *       {
 *         "string":"firstName",
 *         "keypath":"firstName"
 *       },
 *       {
 *         "string":"lastName",
 *         "keypath":"lastName"
 *       }
 *     ]
 *   },
 *   {
 *     "string":"{firstName | upper}",
 *     "filters":[
 *       {
 *         "command":"upper"
 *       }
 *     ],
 *     "keypath":"firstName"
 *   },
 *   {
 *     "string":"{firstName | upper | max 2}",
 *     "filters":[
 *       {
 *         "command":"upper"
 *       },
 *       {
 *         "command":"max",
 *         "param":" 2"
 *       }
 *     ],
 *     "keypath":"firstName"
 *   },
 *   {
 *     "string":"{(firstName | upper | max 2), (lastName | max 1) | & • }",
 *     "filters":[
 *       {
 *         "command":"&",
 *         "param":" • "
 *
 *       }
 *     ],
 *     "placeholders":[
 *       {
 *         "string":"(firstName | upper | max 2)",
 *         "filters":[
 *           {
 *             "command":"upper"
 *           },
 *           {
 *             "command":"max",
 *             "param":" 2"
 *           }
 *         ],
 *         "keypath":"firstName"
 *       },
 *       {
 *         "string":"(lastName | max 1)",
 *         "filters":[
 *           {
 *             "command":"max",
 *             "param":" 1"
 *           }
 *         ],
 *         "keypath":"lastName"
 *       }
 *     ]
 *   },
 *   {
 *     "string":"{keypath?}",
 *     "keypath":"keypath",
 *     "substitute":true
 *   },
 *   {
 *
 *     "string":"{keypath?not available}",
 *     "keypath":"keypath",
 *     "substitute":"not available"
 *   },
 *   {
 *
 *     "string":"{firstName?First name not available, lastName?No last name, DOB? | & - }",
 *     "filters":[
 *       {
 *         "command":"&",
 *         "param":" - "
 *
 *       }
 *     ],
 *     "placeholders":[
 *       {
 *         "string":"firstName?First name not available",
 *         "keypath":"firstName",
 *         "substitute":"First name not available"
 *       },
 *       {
 *         "string":"lastName?No last name",
 *         "keypath":"lastName",
 *         "substitute":"No last name"
 *       },
 *       {
 *         "string":"DOB?",
 *         "keypath":"DOB",
 *         "substitute":true
 *       }
 *     ]
 *   }
 * ]
 */
export function parsePlaceholder (placeholderString) {

  // prepare placeholder
  let placeholder = {
    string: placeholderString
  }

  // get placeholder content
  let placeholderContent = placeholderString
  if (isGroupedPlaceholder(placeholderString) || isRootPlaceholder(placeholderString)) {
    placeholderContent = placeholderContent.substr(1, placeholderContent.length - 2)
  }

  // get filters
  let filters = Filters.extractFilters(placeholderContent)
  if (filters.length) {

    // get placeholder filters
    placeholder.filters = filters

    // remove filters string from placeholder content
    placeholderContent = Filters.removeFiltersString(placeholderContent)
  }

  // get nested placeholders
  let groupingLevel = 0
  let nestedPlaceholders = []
  let buffer = ''
  for (let i = 0; i < placeholderContent.length; i++) {

    // get character of content
    let char = placeholderContent[i]

    // adjust placeholder grouping level
    if (char === '(') groupingLevel++
    if (char === ')') groupingLevel--

    // if comma and not nested or if last character
    if ((char === ',' && groupingLevel === 0) ||
      (i === placeholderContent.length - 1)) {

      // add the character in case it's the last character
      if (char !== ',') buffer += char

      // trim and add placeholder
      nestedPlaceholders.push(buffer.trim())

      // reset placeholder buffer
      buffer = ''

    } else {

      // append the character to buffer
      buffer += char
    }
  }

  // parse nested placeholders if there are more than one or the one is a grouped placeholder
  if (nestedPlaceholders.length > 1 || isGroupedPlaceholder(nestedPlaceholders[0])) {

    // set nested placeholders of the placeholder
    placeholder.placeholders = nestedPlaceholders.map((nestedPlaceholder) => {

      // recur to parse nested placeholder
      return parsePlaceholder(nestedPlaceholder)
    })
  }

  // parse a single ungrouped placeholder, the base case for the recursive function
  else if (nestedPlaceholders[0] && nestedPlaceholders[0].length) {
    let nestedPlaceholder = nestedPlaceholders[0]

    // split into components, dividing into the keypath and substitute
    let substituteMarkerIndex = nestedPlaceholder.indexOf('?')
    let placeholderComponents = [nestedPlaceholder.slice(0, substituteMarkerIndex), nestedPlaceholder.slice(substituteMarkerIndex + 1)]

    // check if has substitute
    if (placeholderComponents.length === 2) {

      // set keypath
      placeholder.keypath = placeholderComponents[0].trim()

      // set substitute
      if (placeholderComponents[1]) {
        placeholder.substitute = placeholderComponents[1].trim()

      } else {

        // set to true to signify that a default substitute should be used
        placeholder.substitute = true
      }

    } else {

      // set keypath to the placeholder itself since there is no substitute
      placeholder.keypath = nestedPlaceholder
    }
  }

  return placeholder
}

/**
 * Populates a placeholder with data, returning the populated string.
 *
 * @param {Object} placeholder
 * @param {Object} data
 * @param {String} defaultSubstitute
 * @returns {String}
 */
export function populatePlaceholder (placeholder, data, defaultSubstitute, xd) {

  // prepare populated string/array
  let populated
  let hasValueForKey = true

  // populate nested placeholders
  if (placeholder.placeholders) {

    // populate and add to array of populated nested placeholders
    populated = placeholder.placeholders.map((nestedPlaceholder) => {
      return populatePlaceholder(nestedPlaceholder, data, defaultSubstitute, xd)
    })
  }

  // no nested placeholders, this is the base case
  else {

    // populate with data for keypath
    populated = getValue(data, placeholder.keypath)

    // check if substitute is needed
    if (!populated) {
      hasValueForKey = false

      // true signifies to use default substitute
      if (placeholder.substitute === true) {
        populated = defaultSubstitute
      }

      // use specified substitute
      else if (placeholder.substitute && placeholder.substitute.length) {

        if (placeholder.substitute[0] === '?') {
          populated = getValue(data, placeholder.substitute.substring(1))

          // use default if placeholder substitute didn't return any data
          if (!populated) populated = defaultSubstitute
        }
        else {
          populated = placeholder.substitute
        }
      }

      // return empty string when no substitute should be used
      else {
        populated = ''
      }
    }
  }

  // apply filters
  if (placeholder.filters) {
    placeholder.filters.forEach((filter) => {
      populated = Filters.applyFilter(filter, populated)
    })
  }

  // make sure that populated is always a string before returning
  // it could be an array if a filter apply function was not found
  if (populated instanceof Array) {
    populated = populated.join(' ')
  }

  if (!xd) {
    return String(populated)
  } else {
    return {
      populated: String(populated),
      hasValueForKey
    }
  }
}

/**
 * Checks if the placeholder is grouped. A placeholder is grouped if it's wrapped
 * within parentheses.
 *
 * @param {String} placeholder
 * @returns {Boolean}
 */
function isGroupedPlaceholder (placeholder) {
  return (placeholder && placeholder[0] === '(' && placeholder[placeholder.length - 1] === ')')
}

/**
 * Checks if the placeholder is a root placeholder. A placeholder is root if it's wrapped
 * within {}.
 *
 * @param {String} placeholder
 * @returns {Boolean}
 */
function isRootPlaceholder (placeholder) {
  return (placeholder && placeholder[0] === '{' && placeholder[placeholder.length - 1] === '}')
}
<commit_msg>Fix image populating<commit_after>/**
 * Placeholders
 *
 * Provides functionality to extract, parse and populate placeholders.
 */

import getValue from 'lodash/get'
import * as Filters from './filters'

/**
 * Extracts placeholders from a string. Placeholders are identified by {}.
 *
 * @param {String} string
 * @returns {Array}
 */
export function extractPlaceholders (string) {

  // get placeholders
  let placeholders = []

  // match placeholders identified by {}
  let regex = /(?![^(]*\)|[^[]*]){([^}]+)}/g
  let match = regex.exec(string)
  while (match) {

    // parse placeholder
    if (match[0].split('{').length - 1 === match[0].split('}').length - 1) {
      let parsedPlaceholder = parsePlaceholder(match[0])

      // add to placeholders array
      placeholders.push(parsedPlaceholder)
    }

    // parse next placeholder
    match = regex.exec(string)
  }

  return placeholders
}

/**
 * Parses the placeholder. The string contains the content of the placeholder
 * without being wrapped in () or {}, e.g. firstName, lastName | & •
 *
 * @param {String} placeholderString
 * @returns {Object}
 *
 * Example placeholders (shown with {}):
 *
 * - {firstName}, {name.first} - John
 * - {firstName, lastName | & • } - John • Doe
 * - {(lastName?, firstName | &, ), DOB | & born on } - Doe, John born on 14/07/1970
 * - {firstName | upper} - JOHN
 * - {firstName | upper | max 2} - JO
 * - {(firstName | upper | max 2), (lastName | max 1) | & • } - JO • D
 * - {keypath?} - The default substitute
 * - {keypath?not available} - not available
 * - {firstName?First name not available, lastName?No last name, DOB? | & - }
 * - TODO: quotation marks {firstName?"First name, not | available \" ,,", lastName?'No, \' | last name', DOB? | & - }
 *   TODO:                '{firstName?"First name, not | available \\" ,,", lastName?\'No, \\\' | last name\', DOB? | & - }'
 *
 * Example nesting:
 * - {(lastName?'', firstName | &, ), DOB | & born on }
 *   - (lastName?'', firstName | &, )
 *     - lastName?''
 *     - firstName
 *   - DOB
 *
 * returned placeholder format: [{
 *   string: {String}, - the original text of the placeholder, e.g. {name.first}
 *   keypath: {String}, - the path to the data, e.g. name.first
 *   filters: {Array}, - filters applied through the pipe character, adding in the correct order
 *   substitute: {String}, - string to use in case the placeholder resolves to an empty string
 *   placeholders: {Array} - nested placeholders with the same format as this placeholder
 * }]
 *
 * Example parsed placeholders:
 *
 * [
 *   {
 *     "string":"{firstName}",
 *     "keypath":"firstName"
 *   },
 *
 *   {
 *     "string":"{name.first}",
 *     "keypath":"name.first"
 *   },
 *
 *   {
 *     "string":"{firstName, lastName | & • }",
 *     "filters":[
 *       {
 *         "command":"&",
 *         "param":" • "
 *       }
 *     ],
 *     "placeholders":[
 *       {
 *         "string":"firstName",
 *         "keypath":"firstName"
 *       },
 *       {
 *         "string":"lastName",
 *         "keypath":"lastName"
 *       }
 *     ]
 *   },
 *   {
 *     "string":"{firstName | upper}",
 *     "filters":[
 *       {
 *         "command":"upper"
 *       }
 *     ],
 *     "keypath":"firstName"
 *   },
 *   {
 *     "string":"{firstName | upper | max 2}",
 *     "filters":[
 *       {
 *         "command":"upper"
 *       },
 *       {
 *         "command":"max",
 *         "param":" 2"
 *       }
 *     ],
 *     "keypath":"firstName"
 *   },
 *   {
 *     "string":"{(firstName | upper | max 2), (lastName | max 1) | & • }",
 *     "filters":[
 *       {
 *         "command":"&",
 *         "param":" • "
 *
 *       }
 *     ],
 *     "placeholders":[
 *       {
 *         "string":"(firstName | upper | max 2)",
 *         "filters":[
 *           {
 *             "command":"upper"
 *           },
 *           {
 *             "command":"max",
 *             "param":" 2"
 *           }
 *         ],
 *         "keypath":"firstName"
 *       },
 *       {
 *         "string":"(lastName | max 1)",
 *         "filters":[
 *           {
 *             "command":"max",
 *             "param":" 1"
 *           }
 *         ],
 *         "keypath":"lastName"
 *       }
 *     ]
 *   },
 *   {
 *     "string":"{keypath?}",
 *     "keypath":"keypath",
 *     "substitute":true
 *   },
 *   {
 *
 *     "string":"{keypath?not available}",
 *     "keypath":"keypath",
 *     "substitute":"not available"
 *   },
 *   {
 *
 *     "string":"{firstName?First name not available, lastName?No last name, DOB? | & - }",
 *     "filters":[
 *       {
 *         "command":"&",
 *         "param":" - "
 *
 *       }
 *     ],
 *     "placeholders":[
 *       {
 *         "string":"firstName?First name not available",
 *         "keypath":"firstName",
 *         "substitute":"First name not available"
 *       },
 *       {
 *         "string":"lastName?No last name",
 *         "keypath":"lastName",
 *         "substitute":"No last name"
 *       },
 *       {
 *         "string":"DOB?",
 *         "keypath":"DOB",
 *         "substitute":true
 *       }
 *     ]
 *   }
 * ]
 */
export function parsePlaceholder (placeholderString) {

  // prepare placeholder
  let placeholder = {
    string: placeholderString
  }

  // get placeholder content
  let placeholderContent = placeholderString
  if (isGroupedPlaceholder(placeholderString) || isRootPlaceholder(placeholderString)) {
    placeholderContent = placeholderContent.substr(1, placeholderContent.length - 2)
  }

  // get filters
  let filters = Filters.extractFilters(placeholderContent)
  if (filters.length) {

    // get placeholder filters
    placeholder.filters = filters

    // remove filters string from placeholder content
    placeholderContent = Filters.removeFiltersString(placeholderContent)
  }

  // get nested placeholders
  let groupingLevel = 0
  let nestedPlaceholders = []
  let buffer = ''
  for (let i = 0; i < placeholderContent.length; i++) {

    // get character of content
    let char = placeholderContent[i]

    // adjust placeholder grouping level
    if (char === '(') groupingLevel++
    if (char === ')') groupingLevel--

    // if comma and not nested or if last character
    if ((char === ',' && groupingLevel === 0) ||
      (i === placeholderContent.length - 1)) {

      // add the character in case it's the last character
      if (char !== ',') buffer += char

      // trim and add placeholder
      nestedPlaceholders.push(buffer.trim())

      // reset placeholder buffer
      buffer = ''

    } else {

      // append the character to buffer
      buffer += char
    }
  }

  // parse nested placeholders if there are more than one or the one is a grouped placeholder
  if (nestedPlaceholders.length > 1 || isGroupedPlaceholder(nestedPlaceholders[0])) {

    // set nested placeholders of the placeholder
    placeholder.placeholders = nestedPlaceholders.map((nestedPlaceholder) => {

      // recur to parse nested placeholder
      return parsePlaceholder(nestedPlaceholder)
    })
  }

  // parse a single ungrouped placeholder, the base case for the recursive function
  else if (nestedPlaceholders[0] && nestedPlaceholders[0].length) {
    let nestedPlaceholder = nestedPlaceholders[0]

    // split into components, dividing into the keypath and substitute
    let substituteMarkerIndex = nestedPlaceholder.indexOf('?')
    let placeholderComponents = (substituteMarkerIndex === -1) ? [nestedPlaceholder] : [nestedPlaceholder.slice(0, substituteMarkerIndex), nestedPlaceholder.slice(substituteMarkerIndex + 1)]

    // check if has substitute
    if (placeholderComponents.length === 2) {

      // set keypath
      placeholder.keypath = placeholderComponents[0].trim()

      // set substitute
      if (placeholderComponents[1]) {
        placeholder.substitute = placeholderComponents[1].trim()

      } else {

        // set to true to signify that a default substitute should be used
        placeholder.substitute = true
      }

    } else {

      // set keypath to the placeholder itself since there is no substitute
      placeholder.keypath = nestedPlaceholder
    }
  }

  return placeholder
}

/**
 * Populates a placeholder with data, returning the populated string.
 *
 * @param {Object} placeholder
 * @param {Object} data
 * @param {String} defaultSubstitute
 * @returns {String}
 */
export function populatePlaceholder (placeholder, data, defaultSubstitute, xd) {

  // prepare populated string/array
  let populated
  let hasValueForKey = true

  // populate nested placeholders
  if (placeholder.placeholders) {

    // populate and add to array of populated nested placeholders
    populated = placeholder.placeholders.map((nestedPlaceholder) => {
      return populatePlaceholder(nestedPlaceholder, data, defaultSubstitute, xd)
    })
  }

  // no nested placeholders, this is the base case
  else {

    // populate with data for keypath
    populated = getValue(data, placeholder.keypath)

    // check if substitute is needed
    if (!populated) {
      hasValueForKey = false

      // true signifies to use default substitute
      if (placeholder.substitute === true) {
        populated = defaultSubstitute
      }

      // use specified substitute
      else if (placeholder.substitute && placeholder.substitute.length) {

        if (placeholder.substitute[0] === '?') {
          populated = getValue(data, placeholder.substitute.substring(1))

          // use default if placeholder substitute didn't return any data
          if (!populated) populated = defaultSubstitute
        }
        else {
          populated = placeholder.substitute
        }
      }

      // return empty string when no substitute should be used
      else {
        populated = ''
      }
    }
  }

  // apply filters
  if (placeholder.filters) {
    placeholder.filters.forEach((filter) => {
      populated = Filters.applyFilter(filter, populated)
    })
  }

  // make sure that populated is always a string before returning
  // it could be an array if a filter apply function was not found
  if (populated instanceof Array) {
    populated = populated.join(' ')
  }

  if (!xd) {
    return String(populated)
  } else {
    return {
      populated: String(populated),
      hasValueForKey
    }
  }
}

/**
 * Checks if the placeholder is grouped. A placeholder is grouped if it's wrapped
 * within parentheses.
 *
 * @param {String} placeholder
 * @returns {Boolean}
 */
function isGroupedPlaceholder (placeholder) {
  return (placeholder && placeholder[0] === '(' && placeholder[placeholder.length - 1] === ')')
}

/**
 * Checks if the placeholder is a root placeholder. A placeholder is root if it's wrapped
 * within {}.
 *
 * @param {String} placeholder
 * @returns {Boolean}
 */
function isRootPlaceholder (placeholder) {
  return (placeholder && placeholder[0] === '{' && placeholder[placeholder.length - 1] === '}')
}
<|endoftext|>


<commit_before>// import { Grid } from "ag-grid-community/main";
// import { formatDate } from "../../utilities/date-utilities";
// import { escapeHtml, newElement } from "../../utilities/html-utilities";
// import { download } from "../../utilities/file.utilities";
// import {
//   getPrincipalUserSingleSampleAnalysisOutputs,
//   getProjectAutomatedSingleSampleAnalysisOutputs,
//   getProjectSharedSingleSampleAnalysisOutputs,
//   prepareAnalysisOutputsDownload
// } from "../../apis/analysis/analysis";
// import { getIridaWorkflowDescription } from "../../apis/pipelines/pipelines";
//
// import "ag-grid-community/dist/styles/ag-grid.css";
// import "ag-grid-community/dist/styles/ag-theme-balham.css";
//
// /**
//  * Internationalized messages
//  * @type {Object} map of messages key name to i18n text
//  */
// let I18N = {
//   "sample.sampleName": "SAMPLE NAME",
//   "bc.file": "FILE",
//   "analysis.table.type": "ANALYSIS TYPE",
//   pipeline: "PIPELINE",
//   "analysis-submission": "ANALYSIS SUBMISSION",
//   "analysis.date-created": "CREATED",
//   "form.download": "DOWNLOAD",
//   "project.export.submitter": "SUBMITTER",
//   "error.request.status-code": "STATUS CODE",
//   "error.request.url": "REQUEST URL",
//   "error.request.status-text": "STATUS TEXT",
//   "analysis.batch-download.ajax.error": "REQUEST ERROR",
//   "analysis.batch-download.preparing": "PREPARING DOWNLOAD",
//   "analysis.automated-analyses": "AUTOMATED ANALYSES",
//   "analysis.shared-analyses": "SHARED ANALYSES",
//   "analysis.batch-download.help-info": "SEND HELP"
// };
// I18N = Object.assign(I18N, window.PAGE.i18n);
//
// const helpInfoIcon = ``;
//
// /**
//  * Analysis output file path regex to capture filename with extension
//  * @type {RegExp}
//  */
// const FILENAME_REGEX = /.*\/(.+\.\w+)/;
//
// /**
//  * Base URL for AJAX requests.
//  * @type {string}
//  */
// const BASE_URL = window.PAGE.URLS.base;
//
// /**
//  * Project id if on Project Analysis Outputs page; null if on User Analysis Outputs page
//  * @type {?number}
//  */
// const PROJECT_ID = (() => {
//   try {
//     return window.project.id;
//   } catch (e) {
//     // No window.project.id? Then we must be on the User Analysis Outputs page!
//     return null;
//   }
// })();
//
// /**
//  * HTML container for dynamically generated table and other UI elements
//  * @type {HTMLElement}
//  */
// const $app = document.getElementById("app");
//
// /**
//  * ag-grid Grid instance object
//  * @type {Grid}
//  */
// let grid;
//
// /**
//  * Get filename from path
//  * @param path File path
//  */
// function getFilename(path) {
//   return path.replace(FILENAME_REGEX, "$1");
// }
//
// /**
//  * Download analysis output files (AOFs) for selected rows in ag-grid table
//  *
//  * A single selected AOF will be downloaded unzipped.
//  * Multiple files are downloaded in the same request as a Zipped response
//  * stream.
//  *
//  * @param {Object} api ag-grid grid options API object
//  * @param {HTMLElement} $dlButton
//  */
// async function downloadSelected($dlButton, api) {
//   /**
//    * Selected rows in the ag-grid Grid corresponding to AOFs
//    * @type {*|RowNode[]}
//    */
//   const selectedNodes = api.getSelectedNodes();
//   setDownloadButtonHtml($dlButton, selectedNodes.length, true, true);
//   if (selectedNodes.length === 1) {
//     const {
//       analysisSubmissionId,
//       analysisOutputFileId,
//       sampleName,
//       sampleId,
//       filePath
//     } = selectedNodes[0].data;
//     let url = `${BASE_URL}analysis/ajax/download/${analysisSubmissionId}/file/${analysisOutputFileId}`;
//     const downloadName = `${sampleName}-sampleId-${sampleId}-analysisSubmissionId-${analysisSubmissionId}-${getFilename(
//       filePath
//     )}`;
//     url += "?" + $.param({ filename: downloadName });
//     download(url);
//   } else if (selectedNodes.length > 1) {
//     const outputs = selectedNodes.map(node => node.data);
//     const { data, error } = await prepareAnalysisOutputsDownload(outputs);
//     if (error) {
//       console.error(I18N["analysis.batch-download.ajax.error"], error);
//       return;
//     }
//     const { selectionSize } = data;
//     const projectOrUser = PROJECT_ID ? `projectId-${PROJECT_ID}` : `user`;
//     const downloadUrl = `${BASE_URL}analysis/ajax/download/selection?filename=${projectOrUser}-batch-download-${selectionSize}-analysis-output-files`;
//     download(downloadUrl);
//   }
//   setDownloadButtonHtml(
//     $dlButton,
//     selectedNodes.length,
//     selectedNodes.length === 0
//   );
// }
//
// /**
//  * Set analysis output file download button inner HTML
//  * @param {HTMLElement} $dlButton Download button element
//  * @param {number} selectionLength Number of rows selected for download
//  * @param {boolean} isDisabled Disable the download button?
//  * @param {boolean} isPreparing Is the download being prepared? It's necessary to prepare downloads for large collections of analysis output files.
//  */
// function setDownloadButtonHtml(
//   $dlButton,
//   selectionLength,
//   isDisabled = false,
//   isPreparing = false
// ) {
//   if (isDisabled) {
//     $dlButton.setAttribute("disabled", "disabled");
//   } else {
//     $dlButton.removeAttribute("disabled");
//   }
//
//   const badge = selectionLength
//     ? `<span class="badge">${selectionLength}</span>`
//     : "";
//   $dlButton.innerHTML = ` ${
//     isPreparing
//       ? I18N["analysis.batch-download.preparing"]
//       : I18N["form.download"]
//   } ${badge}`;
// }
//
// /**
//  * Initialize ag-grid Grid
//  * @param {HTMLElement} $grid Element to create Grid in
//  * @param {Array<Object<string>>} headers Table fields
//  * @param {Array<Object>} rows Table data
//  * @param {jQuery|HTMLElement} $dlButton Download button
//  * @return {Grid} ag-grid object
//  */
// function initAgGrid($grid, headers, rows, $dlButton) {
//   const gridOptions = {
//     enableColResize: true,
//     columnDefs: headers,
//     rowData: rows,
//     rowDeselection: true,
//     enableSorting: true,
//     enableFilter: true,
//     rowSelection: "multiple",
//     onSelectionChanged: e => {
//       const selectedNodes = e.api.getSelectedNodes();
//       const selectionLength = selectedNodes.length;
//       setDownloadButtonHtml($dlButton, selectionLength, selectionLength === 0);
//     }
//   };
//   const grid = new Grid($grid, gridOptions);
//   gridOptions.api.sizeColumnsToFit();
//   $dlButton.addEventListener("click", e => {
//     e.preventDefault();
//     downloadSelected($dlButton, gridOptions.api);
//   });
//   return grid;
// }
//
// /**
//  * Filter for single sample analysis outputs.
//  * @param data
//  * @returns {Array<Object>}
//  */
// function filterSingleSampleOutputs(data) {
//   // group analysis output file (AOF) info by AOF id
//   const groupedDataByAofId = data.reduce((acc, x) => {
//     const { analysisOutputFileId } = x;
//     if (acc.hasOwnProperty(analysisOutputFileId)) {
//       acc[analysisOutputFileId].push(x);
//     } else {
//       acc[analysisOutputFileId] = [x];
//     }
//     return acc;
//   }, {});
//   // return AOF info that only has one Sample assoc with the AOF id, i.e. single
//   // sample AOFs
//   return Object.keys(groupedDataByAofId)
//     .filter(x => groupedDataByAofId[x].length === 1)
//     .map(x => groupedDataByAofId[x][0]);
// }
//
// /**
//  * Get workflow/pipeline info and save to `workflowIds` map.
//  * @param {Array<Object>} singleSampleOutputs Single sample analysis output file infos
//  * @returns {Object<Object>} Map of workflow id to workflow info map.
//  */
// function getWorkflowInfo(singleSampleOutputs) {
//   const workflowIds = singleSampleOutputs.reduce(
//     (acc, x) => Object.assign(acc, { [x.workflowId]: null }),
//     {}
//   );
//   Object.keys(workflowIds).forEach(async function(workflowId) {
//     const { data, error } = await getIridaWorkflowDescription(workflowId);
//     if (!error) {
//       workflowIds[workflowId] = data;
//       if (grid) {
//         grid.gridOptions.api.redrawRows();
//       }
//     }
//   });
//   return workflowIds;
// }
//
// function displayErrorAlert(error) {
//   const { message, request } = error;
//   const { responseURL, statusText, status } = request;
//   $app.innerHTML = "";
//   const $alert = newElement(
//     `

//
${I18N["analysis.batch-download.ajax.error"]}

//
${I18N["error.request.status-text"]}: ${message}; ${statusText}

//
${I18N["error.request.status-code"]}: ${status}

//
${
//            I18N["error.request.url"]
//          }: ${responseURL}

//
`
//   );
//   $app.appendChild($alert);
// }
//
// /**
//  * Get analysis output file (AOF) table information and create table.
//  * @param {boolean} isShared If project analyses to be shown, show outputs shared with project, otherwise show automated analyses.
//  */
// async function getTableData(isShared = true) {
//   const { data, error } = await (PROJECT_ID === null
//     ? getPrincipalUserSingleSampleAnalysisOutputs()
//     : isShared
//     ? getProjectSharedSingleSampleAnalysisOutputs(PROJECT_ID)
//     : getProjectAutomatedSingleSampleAnalysisOutputs(PROJECT_ID));
//   if (error) {
//     displayErrorAlert(error);
//     return;
//   }
//
//   $app.innerHTML = "";
//   const singleSampleOutputs = filterSingleSampleOutputs(data);
//   const workflowIds = getWorkflowInfo(singleSampleOutputs);
//   /**
//    * ag-grid Grid header definitions
//    * @type {*[]}
//    */
//   const HEADERS = [
//     {
//       field: "sampleName",
//       headerName: I18N["sample.sampleName"],
//       checkboxSelection: true,
//       headerCheckboxSelection: true,
//       headerCheckboxSelectionFilteredOnly: true,
//       cellRenderer: p => {
//         const { sampleId, sampleName } = p.data;
//         const projectUrlPrefix = PROJECT_ID ? `projects/${PROJECT_ID}/` : "";
//         return `<a href="${BASE_URL}${projectUrlPrefix}samples/${sampleId}/details" target="_blank">${sampleName}</a>`;
//       }
//     },
//     {
//       field: "filePath",
//       headerName: I18N["bc.file"],
//       cellRenderer: p => {
//         const {
//           filePath,
//           analysisOutputFileKey,
//           analysisOutputFileId
//         } = p.data;
//         const REGEX = /^\d+\/\d+\/(.+)$/;
//         const groups = REGEX.exec(filePath);
//         if (groups === null) return filePath;
//         const filename = groups[1];
//         return `${filename} <small>(${analysisOutputFileKey}, id=${analysisOutputFileId})</small>`;
//       }
//     },
//     {
//       field: "analysisType",
//       headerName: I18N["analysis.table.type"],
//       valueGetter: function(p) {
//         return p.data.analysisType.type;
//       }
//     },
//     {
//       field: "workflowId",
//       headerName: I18N["pipeline"],
//       valueGetter: function(p) {
//         const wfInfo = workflowIds[p.data.workflowId];
//         if (wfInfo === null) return p.data.workflowId;
//         const version =
//           wfInfo.version === "unknown"
//             ? I18N["analysis.table.version.unknown"]
//             : "v" + wfInfo.version;
//         const name =
//           wfInfo.name === "unknown"
//             ? I18N["analysis.table.name.unknown"]
//             : wfInfo.name;
//         return `${name} (${version})`;
//       }
//     },
//     {
//       field: "analysisSubmissionName",
//       headerName: I18N["analysis-submission"],
//       cellRenderer: p =>
//         `<a href="${BASE_URL}analysis/${p.data.analysisSubmissionId}" target="_blank">${p.data.analysisSubmissionName}</a>`
//     },
//     PROJECT_ID
//       ? {
//           field: "userId",
//           headerName: I18N["project.export.submitter"],
//           cellRenderer: p => `${p.data.userFirstName} ${p.data.userLastName}`
//         }
//       : null,
//     {
//       field: "createdDate",
//       headerName: I18N["analysis.date-created"],
//       cellRenderer: p => formatDate({ date: p.data.createdDate })
//     }
//   ].filter(header => header !== null);
//   const $grid = newElement(
//     `<div id="grid-outputs" class="ag-theme-balham" style="height: 600px; width: 100%; resize: both;"/>`
//   );
//   const $dlButton = newElement(
//     `<button type="button" class="btn spaced-bottom" disabled="disabled"></button>`
//   );
//   const $helpIcon = newElement(helpInfoIcon);
//   setDownloadButtonHtml($dlButton, 0, true);
//   $app.appendChild($dlButton);
//   $app.appendChild($helpIcon);
//   $app.appendChild($grid);
//
//   /**
//    * Update the ag-grid container element height on window resize to fill out
//    * the available vertical space
//    */
//   const updateHeight = () => {
//     const BOTTOM_PADDING = 20;
//     const top = $grid.children[0].getBoundingClientRect().top;
//     let height = window.innerHeight - top - BOTTOM_PADDING;
//     // prevent table from getting too small
//     if (height < 300) {
//       height = 300;
//     }
//     $grid.style.height = `${height}px`;
//   };
//   window.addEventListener("resize", updateHeight);
//   /**
//    * Set `grid` to initialized ag-grid Grid for access to Grid API.
//    * @type {Grid}
//    */
//   grid = initAgGrid($grid, HEADERS, singleSampleOutputs, $dlButton);
//   // initialize height of ag-grid table
//   updateHeight();
// }
//
// // init table on page load with outputs shared to project
// getTableData(window.PAGE.page === "shared");
<commit_msg>Work on analysis-outputs-table.js<commit_after>import { Grid } from "ag-grid-community";
import { formatDate } from "../../utilities/date-utilities";
import { escapeHtml, newElement } from "../../utilities/html-utilities";
import { download } from "../../utilities/file.utilities";
import {
  getPrincipalUserSingleSampleAnalysisOutputs,
  getProjectAutomatedSingleSampleAnalysisOutputs,
  getProjectSharedSingleSampleAnalysisOutputs,
  prepareAnalysisOutputsDownload
} from "../../apis/analysis/analysis";
import { getIridaWorkflowDescription } from "../../apis/pipelines/pipelines";

import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-balham.css";

/**
 * Internationalized messages
 * @type {Object} map of messages key name to i18n text
 */
let I18N = {
  "sample.sampleName": "SAMPLE NAME",
  "bc.file": "FILE",
  "analysis.table.type": "ANALYSIS TYPE",
  pipeline: "PIPELINE",
  "analysis-submission": "ANALYSIS SUBMISSION",
  "analysis.date-created": "CREATED",
  "form.download": "DOWNLOAD",
  "project.export.submitter": "SUBMITTER",
  "error.request.status-code": "STATUS CODE",
  "error.request.url": "REQUEST URL",
  "error.request.status-text": "STATUS TEXT",
  "analysis.batch-download.ajax.error": "REQUEST ERROR",
  "analysis.batch-download.preparing": "PREPARING DOWNLOAD",
  "analysis.automated-analyses": "AUTOMATED ANALYSES",
  "analysis.shared-analyses": "SHARED ANALYSES",
  "analysis.batch-download.help-info": "SEND HELP"
};
I18N = Object.assign(I18N, window.PAGE.i18n);

const helpInfoIcon = ``;

/**
 * Analysis output file path regex to capture filename with extension
 * @type {RegExp}
 */
const FILENAME_REGEX = /.*\/(.+\.\w+)/;

/**
 * Base URL for AJAX requests.
 * @type {string}
 */
const BASE_URL = window.PAGE.URLS.base;

/**
 * Project id if on Project Analysis Outputs page; null if on User Analysis Outputs page
 * @type {?number}
 */
const PROJECT_ID = (() => {
  try {
    return window.project.id;
  } catch (e) {
    // No window.project.id? Then we must be on the User Analysis Outputs page!
    return null;
  }
})();

/**
 * HTML container for dynamically generated table and other UI elements
 * @type {HTMLElement}
 */
const $app = document.getElementById("app");

/**
 * ag-grid Grid instance object
 * @type {Grid}
 */
let grid;

/**
 * Get filename from path
 * @param path File path
 */
function getFilename(path) {
  return path.replace(FILENAME_REGEX, "$1");
}

/**
 * Download analysis output files (AOFs) for selected rows in ag-grid table
 *
 * A single selected AOF will be downloaded unzipped.
 * Multiple files are downloaded in the same request as a Zipped response
 * stream.
 *
 * @param {Object} api ag-grid grid options API object
 * @param {HTMLElement} $dlButton
 */
async function downloadSelected($dlButton, api) {
  /**
   * Selected rows in the ag-grid Grid corresponding to AOFs
   * @type {*|RowNode[]}
   */
  const selectedNodes = api.getSelectedNodes();
  setDownloadButtonHtml($dlButton, selectedNodes.length, true, true);
  if (selectedNodes.length === 1) {
    const {
      analysisSubmissionId,
      analysisOutputFileId,
      sampleName,
      sampleId,
      filePath
    } = selectedNodes[0].data;
    let url = `${BASE_URL}analysis/ajax/download/${analysisSubmissionId}/file/${analysisOutputFileId}`;
    const downloadName = `${sampleName}-sampleId-${sampleId}-analysisSubmissionId-${analysisSubmissionId}-${getFilename(
      filePath
    )}`;
    url += "?" + $.param({ filename: downloadName });
    download(url);
  } else if (selectedNodes.length > 1) {
    const outputs = selectedNodes.map(node => node.data);
    const { data, error } = await prepareAnalysisOutputsDownload(outputs);
    if (error) {
      console.error(I18N["analysis.batch-download.ajax.error"], error);
      return;
    }
    const { selectionSize } = data;
    const projectOrUser = PROJECT_ID ? `projectId-${PROJECT_ID}` : `user`;
    const downloadUrl = `${BASE_URL}analysis/ajax/download/selection?filename=${projectOrUser}-batch-download-${selectionSize}-analysis-output-files`;
    download(downloadUrl);
  }
  setDownloadButtonHtml(
    $dlButton,
    selectedNodes.length,
    selectedNodes.length === 0
  );
}

/**
 * Set analysis output file download button inner HTML
 * @param {HTMLElement} $dlButton Download button element
 * @param {number} selectionLength Number of rows selected for download
 * @param {boolean} isDisabled Disable the download button?
 * @param {boolean} isPreparing Is the download being prepared? It's necessary to prepare downloads for large collections of analysis output files.
 */
function setDownloadButtonHtml(
  $dlButton,
  selectionLength,
  isDisabled = false,
  isPreparing = false
) {
  if (isDisabled) {
    $dlButton.setAttribute("disabled", "disabled");
  } else {
    $dlButton.removeAttribute("disabled");
  }

  const badge = selectionLength
    ? `<span class="badge">${selectionLength}</span>`
    : "";
  $dlButton.innerHTML = ` ${
    isPreparing
      ? I18N["analysis.batch-download.preparing"]
      : I18N["form.download"]
  } ${badge}`;
}

/**
 * Initialize ag-grid Grid
 * @param {HTMLElement} $grid Element to create Grid in
 * @param {Array<Object<string>>} headers Table fields
 * @param {Array<Object>} rows Table data
 * @param {jQuery|HTMLElement} $dlButton Download button
 * @return {Grid} ag-grid object
 */
function initAgGrid($grid, headers, rows, $dlButton) {
  const gridOptions = {
    enableColResize: true,
    columnDefs: headers,
    rowData: rows,
    rowDeselection: true,
    sorting: true,
    filter: true,
    rowSelection: "multiple",
    onSelectionChanged: e => {
      const selectedNodes = e.api.getSelectedNodes();
      const selectionLength = selectedNodes.length;
      setDownloadButtonHtml($dlButton, selectionLength, selectionLength === 0);
    }
  };
  const grid = new Grid($grid, gridOptions);
  gridOptions.api.sizeColumnsToFit();
  $dlButton.addEventListener("click", e => {
    e.preventDefault();
    downloadSelected($dlButton, gridOptions.api);
  });
  return grid;
}

/**
 * Filter for single sample analysis outputs.
 * @param data
 * @returns {Array<Object>}
 */
function filterSingleSampleOutputs(data) {
  // group analysis output file (AOF) info by AOF id
  const groupedDataByAofId = data.reduce((acc, x) => {
    const { analysisOutputFileId } = x;
    if (acc.hasOwnProperty(analysisOutputFileId)) {
      acc[analysisOutputFileId].push(x);
    } else {
      acc[analysisOutputFileId] = [x];
    }
    return acc;
  }, {});
  // return AOF info that only has one Sample assoc with the AOF id, i.e. single
  // sample AOFs
  return Object.keys(groupedDataByAofId)
    .filter(x => groupedDataByAofId[x].length === 1)
    .map(x => groupedDataByAofId[x][0]);
}

/**
 * Get workflow/pipeline info and save to `workflowIds` map.
 * @param {Array<Object>} singleSampleOutputs Single sample analysis output file infos
 * @returns {Object<Object>} Map of workflow id to workflow info map.
 */
function getWorkflowInfo(singleSampleOutputs) {
  const workflowIds = singleSampleOutputs.reduce(
    (acc, x) => Object.assign(acc, { [x.workflowId]: null }),
    {}
  );
  Object.keys(workflowIds).forEach(async function(workflowId) {
    const { data, error } = await getIridaWorkflowDescription(workflowId);
    if (!error) {
      workflowIds[workflowId] = data;
      if (grid) {
        grid.gridOptions.api.redrawRows();
      }
    }
  });
  return workflowIds;
}

function displayErrorAlert(error) {
  const { message, request } = error;
  const { responseURL, statusText, status } = request;
  $app.innerHTML = "";
  const $alert = newElement(
    `

${I18N["analysis.batch-download.ajax.error"]}

${I18N["error.request.status-text"]}: ${message}; ${statusText}

${I18N["error.request.status-code"]}: ${status}

${
           I18N["error.request.url"]
         }: ${responseURL}

`
  );
  $app.appendChild($alert);
}

/**
 * Get analysis output file (AOF) table information and create table.
 * @param {boolean} isShared If project analyses to be shown, show outputs shared with project, otherwise show automated analyses.
 */
async function getTableData(isShared = true) {
  const { data, error } = await (PROJECT_ID === null
    ? getPrincipalUserSingleSampleAnalysisOutputs()
    : isShared
    ? getProjectSharedSingleSampleAnalysisOutputs(PROJECT_ID)
    : getProjectAutomatedSingleSampleAnalysisOutputs(PROJECT_ID));
  if (error) {
    displayErrorAlert(error);
    return;
  }

  $app.innerHTML = "";
  const singleSampleOutputs = filterSingleSampleOutputs(data);
  const workflowIds = getWorkflowInfo(singleSampleOutputs);
  /**
   * ag-grid Grid header definitions
   * @type {*[]}
   */
  const HEADERS = [
    {
      field: "sampleName",
      headerName: I18N["sample.sampleName"],
      checkboxSelection: true,
      headerCheckboxSelection: true,
      headerCheckboxSelectionFilteredOnly: true,
      cellRenderer: p => {
        const { sampleId, sampleName } = p.data;
        const projectUrlPrefix = PROJECT_ID ? `projects/${PROJECT_ID}/` : "";
        return `<a href="${BASE_URL}${projectUrlPrefix}samples/${sampleId}/details" target="_blank">${sampleName}</a>`;
      }
    },
    {
      field: "filePath",
      headerName: I18N["bc.file"],
      cellRenderer: p => {
        const {
          filePath,
          analysisOutputFileKey,
          analysisOutputFileId
        } = p.data;
        const REGEX = /^\d+\/\d+\/(.+)$/;
        const groups = REGEX.exec(filePath);
        if (groups === null) return filePath;
        const filename = groups[1];
        return `${filename} <small>(${analysisOutputFileKey}, id=${analysisOutputFileId})</small>`;
      }
    },
    {
      field: "analysisType",
      headerName: I18N["analysis.table.type"],
      valueGetter: function(p) {
        return p.data.analysisType.type;
      }
    },
    {
      field: "workflowId",
      headerName: I18N["pipeline"],
      valueGetter: function(p) {
        const wfInfo = workflowIds[p.data.workflowId];
        if (wfInfo === null) return p.data.workflowId;
        const version =
          wfInfo.version === "unknown"
            ? I18N["analysis.table.version.unknown"]
            : "v" + wfInfo.version;
        const name =
          wfInfo.name === "unknown"
            ? I18N["analysis.table.name.unknown"]
            : wfInfo.name;
        return `${name} (${version})`;
      }
    },
    {
      field: "analysisSubmissionName",
      headerName: I18N["analysis-submission"],
      cellRenderer: p =>
        `<a href="${BASE_URL}analysis/${p.data.analysisSubmissionId}" target="_blank">${p.data.analysisSubmissionName}</a>`
    },
    PROJECT_ID
      ? {
          field: "userId",
          headerName: I18N["project.export.submitter"],
          cellRenderer: p => `${p.data.userFirstName} ${p.data.userLastName}`
        }
      : null,
    {
      field: "createdDate",
      headerName: I18N["analysis.date-created"],
      cellRenderer: p => formatDate({ date: p.data.createdDate })
    }
  ].filter(header => header !== null);
  const $grid = newElement(
    `<div id="grid-outputs" class="ag-theme-balham" style="height: 600px; width: 100%; resize: both;"/>`
  );
  const $dlButton = newElement(
    `<button type="button" class="btn spaced-bottom" disabled="disabled"></button>`
  );
  const $helpIcon = newElement(helpInfoIcon);
  setDownloadButtonHtml($dlButton, 0, true);
  $app.appendChild($dlButton);
  $app.appendChild($helpIcon);
  $app.appendChild($grid);

  /**
   * Update the ag-grid container element height on window resize to fill out
   * the available vertical space
   */
  const updateHeight = () => {
    const BOTTOM_PADDING = 20;
    const top = $grid.children[0].getBoundingClientRect().top;
    let height = window.innerHeight - top - BOTTOM_PADDING;
    // prevent table from getting too small
    if (height < 300) {
      height = 300;
    }
    $grid.style.height = `${height}px`;
  };
  window.addEventListener("resize", updateHeight);
  /**
   * Set `grid` to initialized ag-grid Grid for access to Grid API.
   * @type {Grid}
   */
  grid = initAgGrid($grid, HEADERS, singleSampleOutputs, $dlButton);
  // initialize height of ag-grid table
  updateHeight();
}

// init table on page load with outputs shared to project
getTableData(window.PAGE.page === "shared");
<|endoftext|>


<commit_before>// @flow

import {} from '../../lib/storage'

import {
	trackChannelSubscribe,
	trackChannelUnsubscribe,
	trackNotificationsDisable,
	trackNotificationsEnable,
} from '@frogpond/analytics'
import OneSignal, {
	type SubscriptionState,
	type TagsObject,
} from 'react-native-onesignal'
import pify from 'pify'

const getOneSignalTags: () => Promise<?TagsObject> = pify(OneSignal.getTags, {
	errorFirst: false,
})
const getOneSignalPermissions: () => Promise<SubscriptionState> = pify(
	OneSignal.getPermissionSubscriptionState,
	{errorFirst: false},
)
const promptOneSignalPushPermission: () => Promise<boolean> = pify(
	OneSignal.promptForPushNotificationsWithUserResponse,
	{errorFirst: false},
)

import {type ReduxState} from '../index'

type Dispatch<A: Action> = (action: A | Promise<A> | ThunkAction<A>) => any
type GetState = () => ReduxState
type ThunkAction<A: Action> = (dispatch: Dispatch<A>, getState: GetState) => any

export type NotificationChannelName =
	| 'channel:campus/emergency'
	| 'channel:cage/late-night-specials'
	| 'channel:pause/monthly-special'
	| 'channel:multimedia/weekly-movie'
	| 'channel:post-office/packages'
	| 'channel:sga/announcements'

const CHANNEL_SUBSCRIBE = 'notifications/CHANNEL_SUBSCRIBE'
const CHANNEL_UNSUBSCRIBE = 'notifications/CHANNEL_UNSUBSCRIBE'
const CHANNELS_HYDRATE = 'notifications/CHANNELS_HYDRATE'
const DISABLE = 'notifications/DISABLE'
const ENABLE = 'notifications/ENABLE'

type ChannelSubscribeAction = {|
	type: 'notifications/CHANNEL_SUBSCRIBE',
	payload: NotificationChannelName,
|}
export function subscribe(
	channelName: NotificationChannelName,
): ChannelSubscribeAction {
	OneSignal.sendTag(channelName, 'active')
	trackChannelSubscribe(channelName)
	return {type: CHANNEL_SUBSCRIBE, payload: channelName}
}

type ChannelUnsubscribeAction = {|
	type: 'notifications/CHANNEL_UNSUBSCRIBE',
	payload: NotificationChannelName,
|}
export function unsubscribe(
	channelName: NotificationChannelName,
): ChannelUnsubscribeAction {
	OneSignal.deleteTag(channelName)
	trackChannelUnsubscribe(channelName)
	return {type: CHANNEL_UNSUBSCRIBE, payload: channelName}
}

type ToggleChannelSubscriptionAction =
	| ChannelSubscribeAction
	| ChannelUnsubscribeAction
export function toggleSubscription(
	channelName: NotificationChannelName,
): ThunkAction<ToggleChannelSubscriptionAction> {
	return (dispatch, getState) => {
		let {notifications: {channels = new Set()} = {}} = getState()

		if (channels.has(channelName)) {
			dispatch(unsubscribe(channelName))
		} else {
			dispatch(subscribe(channelName))
		}
	}
}

type HydrateChannelsAction = {|
	type: 'notifications/CHANNELS_HYDRATE',
	payload: {
		channels: Set<NotificationChannelName>,
		permissions: SubscriptionState,
	},
|}
export function hydrate(): ThunkAction<HydrateChannelsAction> {
	return async dispatch => {
		let [tags, permissions] = await Promise.all([
			getOneSignalTags(),
			getOneSignalPermissions(),
		])
		let channels = new Set()
		if (tags) {
			channels = new Set(Object.keys(tags))
		}
		dispatch({type: CHANNELS_HYDRATE, payload: {channels, permissions}})
	}
}

type DisableNotificationsAction = {|
	type: 'notifications/DISABLE',
|}
function disable(): DisableNotificationsAction {
	OneSignal.setSubscription(false)
	trackNotificationsDisable()
	return {type: DISABLE}
}

type EnableNotificationsAction = {|
	type: 'notifications/ENABLE',
|}
function enable(): EnableNotificationsAction {
	OneSignal.setSubscription(true)
	trackNotificationsEnable()
	return {type: ENABLE}
}

export function prompt(): ThunkAction<
	EnableNotificationsAction | DisableNotificationsAction,
> {
	return async dispatch => {
		const permissionResult = await promptOneSignalPushPermission()
		dispatch(permissionResult ? enable() : disable())
	}
}

type Action =
	| ChannelSubscribeAction
	| ChannelUnsubscribeAction
	| HydrateChannelsAction
	| EnableNotificationsAction
	| DisableNotificationsAction

export type State = {|
	+enabled: boolean,
	+channels: Set<NotificationChannelName>,
	+hasPrompted: boolean,
|}

const initialState = {
	enabled: false,
	channels: new Set(),
	hasPrompted: false,
}

export function notifications(state: State = initialState, action: Action) {
	switch (action.type) {
		case CHANNEL_SUBSCRIBE: {
			let channels: Set<NotificationChannelName> = new Set(state.channels)
			channels.add(action.payload)
			return {...state, channels}
		}

		case CHANNEL_UNSUBSCRIBE: {
			let channels: Set<NotificationChannelName> = new Set(state.channels)
			channels.delete(action.payload)
			return {...state, channels}
		}

		case CHANNELS_HYDRATE:
			return {
				...state,
				channels: action.payload.channels,
				enabled: action.payload.permissions.notificationsEnabled,
				hasPrompted: action.payload.permissions.hasPrompted,
			}

		case ENABLE:
			return {...state, enabled: true, hasPrompted: true}

		case DISABLE:
			return {...state, enabled: false, hasPrompted: true}

		default:
			return state
	}
}
<commit_msg>rework notifications/hydrate action creator<commit_after>// @flow

import {
	trackChannelSubscribe,
	trackChannelUnsubscribe,
	trackNotificationsDisable,
	trackNotificationsEnable,
} from '@frogpond/analytics'
import OneSignal from 'react-native-onesignal'

import {type ReduxState} from '../index'

type Dispatch<A: Action> = (action: A | Promise<A> | ThunkAction<A>) => any
type GetState = () => ReduxState
type ThunkAction<A: Action> = (dispatch: Dispatch<A>, getState: GetState) => any

export type NotificationChannelName =
	| 'channel:campus/emergency'
	| 'channel:cage/late-night-specials'
	| 'channel:pause/monthly-special'
	| 'channel:multimedia/weekly-movie'
	| 'channel:post-office/packages'
	| 'channel:sga/announcements'

const CHANNEL_SUBSCRIBE = 'notifications/CHANNEL_SUBSCRIBE'
const CHANNEL_UNSUBSCRIBE = 'notifications/CHANNEL_UNSUBSCRIBE'
const SET_CHANNELS = 'notifications/SET_CHANNELS'
const SET_PERMISSIONS = 'notifications/SET_PERMISSIONS'
const DISABLE = 'notifications/DISABLE'
const ENABLE = 'notifications/ENABLE'

type ChannelSubscribeAction = {|
	type: 'notifications/CHANNEL_SUBSCRIBE',
	payload: NotificationChannelName,
|}
export function subscribe(
	channelName: NotificationChannelName,
): ChannelSubscribeAction {
	OneSignal.sendTag(channelName, 'active')
	trackChannelSubscribe(channelName)
	return {type: CHANNEL_SUBSCRIBE, payload: channelName}
}

type ChannelUnsubscribeAction = {|
	type: 'notifications/CHANNEL_UNSUBSCRIBE',
	payload: NotificationChannelName,
|}
export function unsubscribe(
	channelName: NotificationChannelName,
): ChannelUnsubscribeAction {
	OneSignal.deleteTag(channelName)
	trackChannelUnsubscribe(channelName)
	return {type: CHANNEL_UNSUBSCRIBE, payload: channelName}
}

type ToggleChannelSubscriptionAction =
	| ChannelSubscribeAction
	| ChannelUnsubscribeAction
export function toggleSubscription(
	channelName: NotificationChannelName,
): ThunkAction<ToggleChannelSubscriptionAction> {
	return (dispatch, getState) => {
		let {notifications: {channels = new Set()} = {}} = getState()

		if (channels.has(channelName)) {
			dispatch(unsubscribe(channelName))
		} else {
			dispatch(subscribe(channelName))
		}
	}
}

type SetPermissionsAction = {|
	type: 'notifications/SET_PERMISSIONS',
	payload: {enabled: boolean, hasPrompted: boolean},
|}
type SetChannelsAction = {|
	type: 'notifications/SET_CHANNELS',
	payload: Set<NotificationChannelName>,
|}
export function hydrate(): ThunkAction<SetPermissionsAction | SetChannelsAction> {
	return dispatch => {
		OneSignal.getPermissionSubscriptionState(permissions => {
			dispatch({type: SET_PERMISSIONS, payload: {
				enabled: permissions.notificationsEnabled,
				hasPrompted: permissions.hasPrompted,
			}})
		})

		OneSignal.getTags(tags => {
			if (!tags) {
				return;
			}

			if (tags.stack && tags.message) {
				// it's an error
				return;
			}

			let channels: Set<any> = new Set(Object.keys(tags))
			dispatch({type: SET_CHANNELS, payload: channels})
		})
	}
}

type DisableNotificationsAction = {|
	type: 'notifications/DISABLE',
|}
function disable(): DisableNotificationsAction {
	OneSignal.setSubscription(false)
	trackNotificationsDisable()
	return {type: DISABLE}
}

type EnableNotificationsAction = {|
	type: 'notifications/ENABLE',
|}
function enable(): EnableNotificationsAction {
	OneSignal.setSubscription(true)
	trackNotificationsEnable()
	return {type: ENABLE}
}

export function prompt(): ThunkAction<
	EnableNotificationsAction | DisableNotificationsAction,
> {
	return dispatch => {
		OneSignal.promptForPushNotificationsWithUserResponse(result => {
			dispatch(result ? enable() : disable())
		})
	}
}

type Action =
	| ChannelSubscribeAction
	| ChannelUnsubscribeAction
	| SetPermissionsAction
	| SetChannelsAction
	| EnableNotificationsAction
	| DisableNotificationsAction

export type State = {|
	+enabled: boolean,
	+channels: Set<NotificationChannelName>,
	+hasPrompted: boolean,
|}

const initialState = {
	enabled: false,
	channels: new Set(),
	hasPrompted: false,
}

export function notifications(state: State = initialState, action: Action) {
	switch (action.type) {
		case CHANNEL_SUBSCRIBE: {
			let channels: Set<NotificationChannelName> = new Set(state.channels)
			channels.add(action.payload)
			return {...state, channels}
		}

		case CHANNEL_UNSUBSCRIBE: {
			let channels: Set<NotificationChannelName> = new Set(state.channels)
			channels.delete(action.payload)
			return {...state, channels}
		}

		case SET_CHANNELS: {
			return {...state, channels: action.payload}
		}

		case SET_PERMISSIONS:
			return {
				...state,
				enabled: action.payload.enabled,
				hasPrompted: action.payload.hasPrompted,
			}

		case ENABLE:
			return {...state, enabled: true, hasPrompted: true}

		case DISABLE:
			return {...state, enabled: false, hasPrompted: true}

		default:
			return state
	}
}
<|endoftext|>


<commit_before>import {csvParseRows} from 'd3-dsv'

import mapResource from './MapResource'

import populationCsv from '../../data/population-by-state.csv'
import electoralCollegeCsv from '../../data/electoral-college-votes-by-state.csv'
import gdpCsv from '../../data/gdp-by-state.csv'

class DatasetResource {
  constructor() {
    this._datasets = [
      {
        label: 'U.S. Population 2016',
        data: this.parseCsv(populationCsv),
      },
      {
        label: 'U.S. Electoral College 2016',
        data: this.parseCsv(electoralCollegeCsv),
      },
      {
        label: 'U.S. GDP 2015 (Millions)',
        data: this.parseCsv(gdpCsv),
      },
    ]
    this._selectedDatasetIndex = 2
  }

  parseCsv(csv) {
    const features = mapResource.getUniqueFeatureIds()
    const badMapIds = []
    const badValueIds = []
    const parsed = csvParseRows(csv, d => [d[0], parseFloat(d[1])]).filter(row => {
      const hasId = features.indexOf(row[0]) > -1
      if (!hasId) {
        badMapIds.push(row[0])
      }
      if (row[1] <= 0 || isNaN(row[1])) {
        badValueIds.push(row[0])
      }
      return hasId && row[1] > 0
    })
    if (badMapIds.length || badValueIds.length) {
      this._warnDataErrors(badMapIds, badValueIds)
    }
    return parsed
  }

  _warnDataErrors(badMapIds, badValueIds) {
    const mapIdString = badMapIds.join(', ')
    const valueIdString = badValueIds.join(', ')
    let alertString = ''
    if (mapIdString) {
      alertString += `There is no associated map data associated with id(s): ${mapIdString}.`
    }
    if (valueIdString) {
      alertString += ` Ids ${valueIdString} have zero or negative value.`
    }
    alertString += ' This data has been pruned.'
    // eslint-disable-next-line no-alert
    alert(alertString)
  }

  getLabels() {
    return this._datasets.map(dataset => dataset.label)
  }

  getDataset(index) {
    return this._datasets[index].data
  }

  buildDatasetFromTiles(tiles) {
    const datasetMap = {}
    tiles.forEach((tile) => {
      datasetMap[tile.id] = [tile.id, tile.tilegramValue]
    })
    return Object.keys(datasetMap).map((row) => datasetMap[row])
  }
}

export default new DatasetResource()
<commit_msg>don't require leading 0 for fips codes, don't warn for empty rows (#74)<commit_after>import {csvParseRows} from 'd3-dsv'

import mapResource from './MapResource'

import populationCsv from '../../data/population-by-state.csv'
import electoralCollegeCsv from '../../data/electoral-college-votes-by-state.csv'
import gdpCsv from '../../data/gdp-by-state.csv'

class DatasetResource {
  constructor() {
    this._datasets = [
      {
        label: 'U.S. Population 2016',
        data: this.parseCsv(populationCsv),
      },
      {
        label: 'U.S. Electoral College 2016',
        data: this.parseCsv(electoralCollegeCsv),
      },
      {
        label: 'U.S. GDP 2015 (Millions)',
        data: this.parseCsv(gdpCsv),
      },
    ]
    this._selectedDatasetIndex = 2
  }

  _validateFips(fips) {
    return fips && fips.length < 2 ? `0${fips}` : fips
  }

  parseCsv(csv) {
    const features = mapResource.getUniqueFeatureIds()
    const badMapIds = []
    const badValueIds = []
    const parsed = csvParseRows(csv, d => [this._validateFips(d[0]), parseFloat(d[1])])
      .filter(row => {
        const hasId = (features.indexOf(row[0]) > -1)
        if (!hasId) {
          badMapIds.push(row[0])
        }
        if (row[1] <= 0 || isNaN(row[1])) {
          badValueIds.push(row[0])
        }
        return hasId && row[1] > 0
      })
    if (badMapIds.length || badValueIds.length) {
      this._warnDataErrors(badMapIds, badValueIds)
    }
    return parsed
  }

  _warnDataErrors(badMapIds, badValueIds) {
    const mapIdString = badMapIds.map(id => `"${id}"`).join(', ')
    const valueIdString = badValueIds.map(id => `"${id}"`).join(', ')
    let alertString = ''
    if (mapIdString) {
      alertString += `There is no associated map data associated with id(s): ${mapIdString}.`
    }
    if (valueIdString) {
      alertString += ` Ids ${valueIdString} have zero or negative value.`
    }
    alertString += ' This data has been pruned.'
    // eslint-disable-next-line no-alert
    alert(alertString)
  }

  getLabels() {
    return this._datasets.map(dataset => dataset.label)
  }

  getDataset(index) {
    return this._datasets[index].data
  }

  buildDatasetFromTiles(tiles) {
    const datasetMap = {}
    tiles.forEach((tile) => {
      datasetMap[tile.id] = [tile.id, tile.tilegramValue]
    })
    return Object.keys(datasetMap).map((row) => datasetMap[row])
  }
}

export default new DatasetResource()
<|endoftext|>


<commit_before>// Functions for tooltips.
	(function() {
		/**
		 * Storage for the autoshow tootips functions.
		 *
		 * @type	{object}
		 */
		var _go_autoshowTooltips = new function() {
			/**
			 * General function for autowhow.
			 *
			 * @param	{string}	onclickWrapperClass
			 *   The class of the wrapper which has the onclik attribute set.
			 */
			var _lf_autoshowTooltipsGeneral = function(onclickWrapperClass) {
				IC.myGM.$$('.' + onclickWrapperClass).forEach(function(onclickWrapper) {
					var onclickFunction		= onclickWrapper.onclick;
					onclickWrapper.onclick	= 'return false;';

					var newHandler = IC.myGM.$('.magnify_icon', onclickWrapper);
					if(!newHandler)
						newHandler = onclickWrapper;

					newHandler.addEventListener('mouseover', function(e) { IC.ika.controller.captureMousePosition(e); onclickFunction(e); }, true);
				});

				IC.myGM.$('.templateView .mainContent').addEventListener('click', function() { IC.win.$(document).trigger("closeExclusiveInfo"); }, true);
			};

			/**
			 * Autmatically show tooltips in alliance member lists.
			 */
			var _lf_doAutoshowTooltipsAlliance = function() {
				_lf_autoshowTooltipsGeneral('cityInfo');
			};

			/**
			 * Autmatically show tooltips in military advisor.
			 */
			var _lf_doAutoshowTooltipsMilitaryAdvisor = function() {
				if(IC.Options.getOption('moduleOptions', 'showDirectMilitaryTooltips') === true)
					return;

				_lf_autoshowTooltipsGeneral('spyMilitary');
			};

			/**
			 * Update the settings to execute the callback or delete the handler.
			 *
			 * @param	{boolean}	autoshowTooltips
			 *   If the user selected the checkbox to autoshow tooltips.
			 */
			this.updateSettings = function(autoshowTooltips) {
				var alliancePopupIds = ['diplomacyAllyMemberlist', 'embassy'];

				if(autoshowTooltips === true) {
					IC.RefreshHandler.add('militaryAdvisor', 'autoshowTooltips', _lf_doAutoshowTooltipsMilitaryAdvisor);
					IC.RefreshHandler.add(alliancePopupIds, 'autoshowTooltips', _lf_doAutoshowTooltipsAlliance);
					return;
				}

				IC.RefreshHandler.remove('militaryAdvisor', 'autoshowTooltips');
				IC.RefreshHandler.remove(alliancePopupIds, 'autoshowTooltips');
			};
		};

		/**
		 * Storage for the direct military tooltip functions.
		 *
		 * @type	{object}
		 */
		var _go_directMilitaryTooltips = new function() {
			/**
			 * Storage for the class ids and their corresponding translations. Each class is a property of the object with the translation stored in it.
			 *
			 * @type	{object}
			 */
			var _lo_idTranslation = {};

			/**
			 * Fill the id translation object.
			 */
			var _lf_fillIdTranslation = function() {
				if(_lo_idTranslation.length === 0)
					return;

				var la_troops		= ['swordsman', 'phalanx', 'archer', 'marksman', 'mortar', 'slinger', 'catapult', 'ram', 'steamgiant', 'bombardier', 'cook', 'medic', 'gyrocopter', 'spearman', 'spartan'];
				var la_ships		= ['balliasta', 'catapult', 'flamethrower', 'mortar', 'ram', 'steamboat', 'rocketship', 'submarine', 'paddlespeedship', 'balloncarrier', 'tender', 'transport', 'premium_transport'];
				var la_resources	= IC.Ikariam.resourceNames;
				la_resources.push('gold');

				la_troops.forEach(function(is_troopName) {
					_lo_idTranslation[is_troopName] = IC.Language.$('diverse.name.unit.' + is_troopName);
				});

				la_ships.forEach(function(is_shipName) {
					_lo_idTranslation['ship_' + is_shipName] = IC.Language.$('diverse.name.ship.' + is_shipName.replace('premium_', ''));
				});

				la_resources.forEach(function(is_resourceName) {
					_lo_idTranslation[is_resourceName] = IC.Language.$('diverse.name.resource.' + is_resourceName);
				});
			};

			/**
			 * Hide the number of ships for the own peaceful missions (trade / transport).
			 */
			var _lf_hideShipNumberOwnPeacefulMissions = function() {
				var la_ownEventTableRows = IC.myGM.$$('#js_MilitaryMovementsFleetMovementsTable .military_event_table tr.own');

				la_ownEventTableRows.forEach(function(ie_tableRow) {
					var le_missionDiv		= IC.myGM.$('td:nth-child(1) div.mission_icon', ie_tableRow);
					var lb_peacefulMission	= le_missionDiv.classList.contains('transport') || le_missionDiv.classList.contains('trade');

					if(lb_peacefulMission === true) {
						IC.myGM.$('td:nth-child(4) div', ie_tableRow).classList.add('invisible');
					}
				});
			};

			/**
			 * Show the military tooltips directly.
			 */
			var _lf_doShowDirectMilitaryTooltips = function() {
				_lf_fillIdTranslation();

				_lf_hideShipNumberOwnPeacefulMissions();

				IC.myGM.$$('.spyMilitary').forEach(function(ie_movementWrapper) {
					ie_movementWrapper.onclick = 'return false;';
				});

				var le_movementsTable = IC.myGM.$('#js_MilitaryMovementsFleetMovementsTable');

				IC.myGM.forEach(_lo_idTranslation, function(is_classId, is_translation) {
					IC.myGM.$$('.icon40.' + is_classId, le_movementsTable).forEach(function(ie_detailIcon) {
						ie_detailIcon.title = is_translation;
					});
				});
			};

			/**
			 * Update the settings to execute the callback or delete the handler.
			 *
			 * @param	{boolean}	ib_showDirectMilitaryTooltips
			 *   If the user selected the checkbox to show the military tooltips directly.
			 */
			this.updateSettings = function(ib_showDirectMilitaryTooltips) {
				if(ib_showDirectMilitaryTooltips === true) {
					IC.RefreshHandler.add('militaryAdvisor', 'showDirectMilitaryTooltips', _lf_doShowDirectMilitaryTooltips);
					IC.myGM.addStyle(
						'#js_MilitaryMovementsFleetMovementsTable .military_event_table .magnify_icon				{ background-image: none; cursor: default; width: 240px; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .magnify_icon .infoTip		{ display: inline; position: relative; padding: 0px; border: none; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .magnify_icon .infoTip h5	{ display: none; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .icon40						{ background-size: 25px 25px; background-color: transparent; padding: 26px 3px 0px 3px; width: 30px; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .icon40.resource_icon		{ background-size: 20px 16px; }',
						'showDirectMilitaryTooltips'
					);
					return;
				}

				IC.RefreshHandler.remove('militaryAdvisor', 'showDirectMilitaryTooltips');
				IC.myGM.removeStyle('showDirectMilitaryTooltips');
			};
		};

		// Show alliance / military tooltips directly.
		IC.Options.addCheckbox('autoshowTooltips', 'diverseOptions', 1, true, IC.Language.$('tooltips.options.autoshow'), { changeCallback: _go_autoshowTooltips.updateSettings });
		// Show military tooltips directly.
		IC.Options.addCheckbox('showDirectMilitaryTooltips', 'diverseOptions', 1, false, IC.Language.$('tooltips.options.showDirectInMilitaryAdvisor'), { changeCallback: _go_directMilitaryTooltips.updateSettings });
		// Add a divider line.
		IC.Options.addLine('diverseOptions', 1);
	})();<commit_msg>Fix #23: Formatting of direct tooltip military advisor broken.<commit_after>// Functions for tooltips.
	(function() {
		/**
		 * Storage for the autoshow tootips functions.
		 *
		 * @type	{object}
		 */
		var _go_autoshowTooltips = new function() {
			/**
			 * General function for autowhow.
			 *
			 * @param	{string}	onclickWrapperClass
			 *   The class of the wrapper which has the onclik attribute set.
			 */
			var _lf_autoshowTooltipsGeneral = function(onclickWrapperClass) {
				IC.myGM.$$('.' + onclickWrapperClass).forEach(function(onclickWrapper) {
					var onclickFunction		= onclickWrapper.onclick;
					onclickWrapper.onclick	= 'return false;';

					var newHandler = IC.myGM.$('.magnify_icon', onclickWrapper);
					if(!newHandler)
						newHandler = onclickWrapper;

					newHandler.addEventListener('mouseover', function(e) { IC.ika.controller.captureMousePosition(e); onclickFunction(e); }, true);
				});

				IC.myGM.$('.templateView .mainContent').addEventListener('click', function() { IC.win.$(document).trigger("closeExclusiveInfo"); }, true);
			};

			/**
			 * Autmatically show tooltips in alliance member lists.
			 */
			var _lf_doAutoshowTooltipsAlliance = function() {
				_lf_autoshowTooltipsGeneral('cityInfo');
			};

			/**
			 * Autmatically show tooltips in military advisor.
			 */
			var _lf_doAutoshowTooltipsMilitaryAdvisor = function() {
				if(IC.Options.getOption('moduleOptions', 'showDirectMilitaryTooltips') === true)
					return;

				_lf_autoshowTooltipsGeneral('spyMilitary');
			};

			/**
			 * Update the settings to execute the callback or delete the handler.
			 *
			 * @param	{boolean}	autoshowTooltips
			 *   If the user selected the checkbox to autoshow tooltips.
			 */
			this.updateSettings = function(autoshowTooltips) {
				var alliancePopupIds = ['diplomacyAllyMemberlist', 'embassy'];

				if(autoshowTooltips === true) {
					IC.RefreshHandler.add('militaryAdvisor', 'autoshowTooltips', _lf_doAutoshowTooltipsMilitaryAdvisor);
					IC.RefreshHandler.add(alliancePopupIds, 'autoshowTooltips', _lf_doAutoshowTooltipsAlliance);
					return;
				}

				IC.RefreshHandler.remove('militaryAdvisor', 'autoshowTooltips');
				IC.RefreshHandler.remove(alliancePopupIds, 'autoshowTooltips');
			};
		};

		/**
		 * Storage for the direct military tooltip functions.
		 *
		 * @type	{object}
		 */
		var _go_directMilitaryTooltips = new function() {
			/**
			 * Storage for the class ids and their corresponding translations. Each class is a property of the object with the translation stored in it.
			 *
			 * @type	{object}
			 */
			var _lo_idTranslation = {};

			/**
			 * Fill the id translation object.
			 */
			var _lf_fillIdTranslation = function() {
				if(_lo_idTranslation.length === 0)
					return;

				var la_troops		= ['swordsman', 'phalanx', 'archer', 'marksman', 'mortar', 'slinger', 'catapult', 'ram', 'steamgiant', 'bombardier', 'cook', 'medic', 'gyrocopter', 'spearman', 'spartan'];
				var la_ships		= ['balliasta', 'catapult', 'flamethrower', 'mortar', 'ram', 'steamboat', 'rocketship', 'submarine', 'paddlespeedship', 'balloncarrier', 'tender', 'transport', 'premium_transport'];
				var la_resources	= IC.Ikariam.resourceNames;
				la_resources.push('gold');

				la_troops.forEach(function(is_troopName) {
					_lo_idTranslation[is_troopName] = IC.Language.$('diverse.name.unit.' + is_troopName);
				});

				la_ships.forEach(function(is_shipName) {
					_lo_idTranslation['ship_' + is_shipName] = IC.Language.$('diverse.name.ship.' + is_shipName.replace('premium_', ''));
				});

				la_resources.forEach(function(is_resourceName) {
					_lo_idTranslation[is_resourceName] = IC.Language.$('diverse.name.resource.' + is_resourceName);
				});
			};

			/**
			 * Hide the number of ships for the own peaceful missions (trade / transport).
			 */
			var _lf_hideShipNumberOwnPeacefulMissions = function() {
				var la_ownEventTableRows = IC.myGM.$$('#js_MilitaryMovementsFleetMovementsTable .military_event_table tr.own');

				la_ownEventTableRows.forEach(function(ie_tableRow) {
					var le_missionDiv		= IC.myGM.$('td:nth-child(1) div.mission_icon', ie_tableRow);
					var lb_peacefulMission	= le_missionDiv.classList.contains('transport') || le_missionDiv.classList.contains('trade');

					if(lb_peacefulMission === true) {
						IC.myGM.$('td:nth-child(4) div', ie_tableRow).classList.add('invisible');
					}
				});
			};

			/**
			 * Show the military tooltips directly.
			 */
			var _lf_doShowDirectMilitaryTooltips = function() {
				_lf_fillIdTranslation();

				_lf_hideShipNumberOwnPeacefulMissions();

				IC.myGM.$$('.spyMilitary').forEach(function(ie_movementWrapper) {
					ie_movementWrapper.onclick = 'return false;';
				});

				var le_movementsTable = IC.myGM.$('#js_MilitaryMovementsFleetMovementsTable');

				IC.myGM.forEach(_lo_idTranslation, function(is_classId, is_translation) {
					IC.myGM.$$('.icon40.' + is_classId, le_movementsTable).forEach(function(ie_detailIcon) {
						ie_detailIcon.title = is_translation;
					});
				});
			};

			/**
			 * Update the settings to execute the callback or delete the handler.
			 *
			 * @param	{boolean}	ib_showDirectMilitaryTooltips
			 *   If the user selected the checkbox to show the military tooltips directly.
			 */
			this.updateSettings = function(ib_showDirectMilitaryTooltips) {
				if(ib_showDirectMilitaryTooltips === true) {
					IC.RefreshHandler.add('militaryAdvisor', 'showDirectMilitaryTooltips', _lf_doShowDirectMilitaryTooltips);
					IC.myGM.addStyle(
						'#js_MilitaryMovementsFleetMovementsTable .military_event_table .magnify_icon				{ background-image: none; cursor: default; width: 240px; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .magnify_icon .infoTip		{ display: inline; position: relative; padding: 0px; border: none; top: 0px; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .magnify_icon .infoTip h5	{ display: none; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .icon40						{ background-size: 25px 25px; background-color: transparent; padding: 26px 3px 0px 3px; width: 30px; } \
						 #js_MilitaryMovementsFleetMovementsTable .military_event_table .icon40.resource_icon		{ background-size: 20px 16px; }',
						'showDirectMilitaryTooltips'
					);
					return;
				}

				IC.RefreshHandler.remove('militaryAdvisor', 'showDirectMilitaryTooltips');
				IC.myGM.removeStyle('showDirectMilitaryTooltips');
			};
		};

		// Show alliance / military tooltips directly.
		IC.Options.addCheckbox('autoshowTooltips', 'diverseOptions', 1, true, IC.Language.$('tooltips.options.autoshow'), { changeCallback: _go_autoshowTooltips.updateSettings });
		// Show military tooltips directly.
		IC.Options.addCheckbox('showDirectMilitaryTooltips', 'diverseOptions', 1, false, IC.Language.$('tooltips.options.showDirectInMilitaryAdvisor'), { changeCallback: _go_directMilitaryTooltips.updateSettings });
		// Add a divider line.
		IC.Options.addLine('diverseOptions', 1);
	})();<|endoftext|>


<commit_before>var editor;
$(document).ready(function() {
	editor = new CollectionEditor();
	$("#btn-add-description").click(function() { editor.triggerAddTableElement(); return false;});
});

var CollectionEditor = function() {};

CollectionEditor.prototype.triggerAddTableElement = function() {
	$.ajax({
        url: __util.getBaseUrl() + "collections/includes/editDescription",
        type: "GET",
        dataType: "html",
        success: function(data) {

        	// Hide placeholder row
        	$("#tbl-collection-description-sets .collection-editor-table-empty-placeholder").hide();

        	// Hide all existing form-rows
        	$("#tbl-collection-description-sets tr.edit").hide();

        	$("#tbl-collection-description-sets tbody").append(data);
        },
        error: function(textStatus) { }
	});
};

CollectionEditor.prototype.editEntry = function(btn) {
	var expand = $(btn).closest("tr").next().css("display")=="none";

	$(btn).closest("table").find("tr.edit").hide();

	if (expand) {
		$(btn).closest("tr").next().show();
	}
};

CollectionEditor.prototype.removeEntry = function(btn) {
	$(btn).closest("tr").next().remove();
	$(btn).closest("tr").remove();
};<commit_msg>3: Implement collection editor / viewer (mantis 452) <commit_after>var editor;
$(document).ready(function() {
	editor = new CollectionEditor();
	$("#btn-add-description").click(function() { editor.triggerAddTableElement(); return false;});

	$("form").submit(function(event) { editor.submit(event); });
});

var CollectionEditor = function() {};

CollectionEditor.prototype.triggerAddTableElement = function() {
	$.ajax({
        url: __util.getBaseUrl() + "collections/includes/editDescription",
        type: "GET",
        dataType: "html",
        success: function(data) {

        	// Hide placeholder row
        	$("#tbl-collection-description-sets .collection-editor-table-empty-placeholder").hide();

        	// Hide all existing form-rows
        	$("#tbl-collection-description-sets tr.edit").hide();

        	$("#tbl-collection-description-sets tbody").append(data);
        },
        error: function(textStatus) { }
	});
};

CollectionEditor.prototype.editEntry = function(btn) {
	var expand = $(btn).closest("tr").next().css("display")=="none";

	$(btn).closest("table").find("tr.edit").hide();

	if (expand) {
		$(btn).closest("tr").next().show();
	}
};

CollectionEditor.prototype.removeEntry = function(btn) {
	$(btn).closest("tr").next().remove();
	$(btn).closest("tr").remove();
};

CollectionEditor.prototype.submit = function(event) {

	event.preventDefault();	// Assume error for now
	return false;
};<|endoftext|>


<commit_before>/**
 * $params is a JSON object initiliazed by the /assets/js/marknotes.js file.
 */
function fnPluginButtonEdit($params) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor');
	}
	/*<!-- endbuild -->*/

	if (marknotes.note.url == '') {
		Noty({
			message: $.i18n('error_select_first'),
			type: 'error'
		});

	} else {

		ajaxify({
			task: 'task.edit.form',
			param: marknotes.note.md5,
			callback: 'afterEdit($data)',
			useStore: false,
			target: 'CONTENT'
		});
	}

	return true;
}

/**
 * EDIT MODE - Render the textarea in an editor
 */
function afterEdit($data) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - afterEdit');
	}
	/*<!-- endbuild -->*/

	if (document.getElementById("sourceMarkDown") !== null) {
		afterEditInitMDE($data);
	} else {
		Noty({
			message: $.i18n('not_authenticated'),
			type: 'error'
		});
	}

	return true;
}

function afterEditInitMDE($data) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - afterEdit');
		console.log($data);
	}
	/*<!-- endbuild -->*/

	filename = $data.param;

	// Create the Simple Markdown Editor
	// @link https://github.com/NextStepWebs/simplemde-markdown-editor

	var simplemde = new SimpleMDE({
		autoDownloadFontAwesome: false,
		autofocus: true,
		autosave: {
			enabled: false
		},
		codeSyntaxHighlighting: true,
		element: document.getElementById("sourceMarkDown"),
		indentWithTabs: true,
		insertTexts: {
			horizontalRule: ["", "\n\n---\n\n"],
			image: ["![](https://", ")"],
			link: ["[", "](https://)"],
			table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| --- | --- | --- |\n| Text | Text | Text |\n\n"],
		},
		spellChecker: true,
		status: ["autosave", "lines", "words", "cursor"], // Optional usage
		styleSelectedText: false,
		tabSize: 4,
		toolbar: [
			{
				// Add a custom button for saving
				name: "Save",
				action: function customFunction(editor) {
					buttonSave(filename, simplemde.value());
				},
				className: "fa fa-floppy-o",
				title: $.i18n('button_save')
			},
			{
				// Encrypt
				name: "Encrypt",
				action: function customFunction(editor) {
					buttonEncrypt(editor);
				},
				className: "fa fa-user-secret",
				title: $.i18n('button_encrypt')
			},
			{
				// Table of content
				name: "AddTOC",
				action: function customFunction(editor) {
					buttonAddTOC(editor);
				},
				className: "fa fa-map-o",
				title: $.i18n('button_addTOC')
			},
			"|",
			{
				// Add a custom button for saving
				name: "Exit",
				action: function customFunction(editor) {
					$('#sourceMarkDown').parent().hide();
					ajaxify({
						task: 'task.export.html',
						param: filename,
						callback: 'afterDisplay($data.param)',
						target: 'CONTENT'
					});
				},
				className: "fa fa-sign-out",
				title: $.i18n('button_exit_edit_mode')
			},
			"|", "preview", "side-by-side", "fullscreen", "|",
			"bold", "italic", "strikethrough", "|", "heading", "heading-smaller", "heading-bigger", "|", "heading-1", "heading-2", "heading-3", "|",
			"code", "quote", "unordered-list", "ordered-list", "clean-block", "|", "link", "image", "table", "horizontal-rule"
		] // toolbar
	});

	//	$('.editor-toolbar').addClass('fa-2x');

	return true;
}

/**
 * EDIT MODE - Save the new content.  Called by the "Save" button
 * of the simplemde editor, initialized in the afterEdit function)
 *
 * @param {type} $fname        Filename
 * @param {type} $markdown     The new content
 * @returns {boolean}
 */
function buttonSave($fname, $markdown) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - Save');
	}
	/*<!-- endbuild -->*/

	var $data = {};
	$data.task = 'task.edit.save';
	$data.param = $fname;
	$data.markdown = window.btoa(encodeURIComponent(JSON.stringify($markdown)));

	$.ajax({
		async: true,
		// GET can't be used because note's content can be too big for URLs
		type: 'POST',
		url: marknotes.url,
		data: $data,
		datatype: 'json',
		success: function (data) {
			Noty({
				message: data.message,
				type: (data.status == 1 ? 'success' : 'error')
			});

			var $useStore = (typeof store === 'object');
			if ($useStore) {
				// Be sure the localStorage array is up-to-date and willn't
				// contains the previous content
				fnPluginTaskOptimizeStore_Remove({
					"name": $fname
				});
			}
		}
	}); // $.ajax()

	return true;

}

/**
 * EDIT MODE - Encrypt the selection.  Add the <encrypt> tag
 *
 * @returns {boolean}
 */
function buttonEncrypt(editor) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - Encrypt');
	}
	/*<!-- endbuild -->*/

	var cm = editor.codemirror;
	var output = '';
	var selectedText = cm.getSelection();
	var text = selectedText || 'your_confidential_info';

	output = '<encrypt>' + text + '</encrypt>';
	cm.replaceSelection(output);

}

/**
 * ADD TOC - Add the %TOC_3% tag
 *
 * @returns {boolean}
 */
function buttonAddTOC(editor) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - Add TOC tag');
	}
	/*<!-- endbuild -->*/

	var cm = editor.codemirror;
	// Just add the tag where the cursor is located
	cm.replaceSelection('%TOC_5%');
}
<commit_msg>Solve bug editor - You should be connected<commit_after>/**
 * $params is a JSON object initiliazed by the /assets/js/marknotes.js file.
 */
function fnPluginButtonEdit($params) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor');
	}
	/*<!-- endbuild -->*/

	if (marknotes.note.url == '') {
		Noty({
			message: $.i18n('error_select_first'),
			type: 'error'
		});

	} else {

		ajaxify({
			task: 'task.edit.form',
			param: marknotes.note.md5,
			callback: 'afterEdit($data, data)',
			useStore: false,
			target: 'CONTENT'
		});
	}

	return true;
}

/**
 * EDIT MODE - Render the textarea in an editor
 */
function afterEdit($ajax_request, $form) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - afterEdit');
	}
	/*<!-- endbuild -->*/

	$('#CONTENT').html($form);

	if (document.getElementById("sourceMarkDown") !== null) {
		afterEditInitMDE($ajax_request);
	} else {
		Noty({
			message: $.i18n('not_authenticated'),
			type: 'error'
		});
	}

	return true;
}

function afterEditInitMDE($data) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - afterEdit');
		console.log($data);
	}
	/*<!-- endbuild -->*/

	filename = $data.param;

	// Create the Simple Markdown Editor
	// @link https://github.com/NextStepWebs/simplemde-markdown-editor

	var simplemde = new SimpleMDE({
		autoDownloadFontAwesome: false,
		autofocus: true,
		autosave: {
			enabled: false
		},
		codeSyntaxHighlighting: true,
		element: document.getElementById("sourceMarkDown"),
		indentWithTabs: true,
		insertTexts: {
			horizontalRule: ["", "\n\n---\n\n"],
			image: ["![](https://", ")"],
			link: ["[", "](https://)"],
			table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| --- | --- | --- |\n| Text | Text | Text |\n\n"],
		},
		spellChecker: true,
		status: ["autosave", "lines", "words", "cursor"], // Optional usage
		styleSelectedText: false,
		tabSize: 4,
		toolbar: [
			{
				// Add a custom button for saving
				name: "Save",
				action: function customFunction(editor) {
					buttonSave(filename, simplemde.value());
				},
				className: "fa fa-floppy-o",
				title: $.i18n('button_save')
			},
			{
				// Encrypt
				name: "Encrypt",
				action: function customFunction(editor) {
					buttonEncrypt(editor);
				},
				className: "fa fa-user-secret",
				title: $.i18n('button_encrypt')
			},
			{
				// Table of content
				name: "AddTOC",
				action: function customFunction(editor) {
					buttonAddTOC(editor);
				},
				className: "fa fa-map-o",
				title: $.i18n('button_addTOC')
			},
			"|",
			{
				// Add a custom button for saving
				name: "Exit",
				action: function customFunction(editor) {
					$('#sourceMarkDown').parent().hide();
					ajaxify({
						task: 'task.export.html',
						param: filename,
						callback: 'afterDisplay($data.param)',
						target: 'CONTENT'
					});
				},
				className: "fa fa-sign-out",
				title: $.i18n('button_exit_edit_mode')
			},
			"|", "preview", "side-by-side", "fullscreen", "|",
			"bold", "italic", "strikethrough", "|", "heading", "heading-smaller", "heading-bigger", "|", "heading-1", "heading-2", "heading-3", "|",
			"code", "quote", "unordered-list", "ordered-list", "clean-block", "|", "link", "image", "table", "horizontal-rule"
		] // toolbar
	});

	//	$('.editor-toolbar').addClass('fa-2x');

	return true;
}

/**
 * EDIT MODE - Save the new content.  Called by the "Save" button
 * of the simplemde editor, initialized in the afterEdit function)
 *
 * @param {type} $fname        Filename
 * @param {type} $markdown     The new content
 * @returns {boolean}
 */
function buttonSave($fname, $markdown) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - Save');
	}
	/*<!-- endbuild -->*/

	var $data = {};
	$data.task = 'task.edit.save';
	$data.param = $fname;
	$data.markdown = window.btoa(encodeURIComponent(JSON.stringify($markdown)));

	$.ajax({
		async: true,
		// GET can't be used because note's content can be too big for URLs
		type: 'POST',
		url: marknotes.url,
		data: $data,
		datatype: 'json',
		success: function (data) {
			Noty({
				message: data.message,
				type: (data.status == 1 ? 'success' : 'error')
			});

			var $useStore = (typeof store === 'object');
			if ($useStore) {
				// Be sure the localStorage array is up-to-date and willn't
				// contains the previous content
				fnPluginTaskOptimizeStore_Remove({
					"name": $fname
				});
			}
		}
	}); // $.ajax()

	return true;

}

/**
 * EDIT MODE - Encrypt the selection.  Add the <encrypt> tag
 *
 * @returns {boolean}
 */
function buttonEncrypt(editor) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - Encrypt');
	}
	/*<!-- endbuild -->*/

	var cm = editor.codemirror;
	var output = '';
	var selectedText = cm.getSelection();
	var text = selectedText || 'your_confidential_info';

	output = '<encrypt>' + text + '</encrypt>';
	cm.replaceSelection(output);

}

/**
 * ADD TOC - Add the %TOC_3% tag
 *
 * @returns {boolean}
 */
function buttonAddTOC(editor) {

	/*<!-- build:debug -->*/
	if (marknotes.settings.debug) {
		console.log('      Plugin Page html - Editor - Add TOC tag');
	}
	/*<!-- endbuild -->*/

	var cm = editor.codemirror;
	// Just add the tag where the cursor is located
	cm.replaceSelection('%TOC_5%');
}
<|endoftext|>