<commit_before>const changelogHelper = require('./changelog');
const { getPrBody } = require('./pr-body');

module.exports = {
  ensurePr,
  checkAutoMerge,
};
// Ensures that PR exists with matching title/body
async function ensurePr(prConfig) {
  const config = { ...prConfig };

  logger.trace({ config }, 'ensurePr');
  // If there is a group, it will use the config of the first upgrade in the array
  const { branchName, prTitle, upgrades } = config;
  // Check if existing PR exists
  const existingPr = await platform.getBranchPr(branchName);
  if (existingPr) {
    logger.debug('Found existing PR');
  }
  config.upgrades = [];

  if (config.artifactErrors && config.artifactErrors.length) {
    logger.debug('Forcing PR because of artifact errors');
    config.forcePr = true;
  }

  let branchStatus;
  async function getBranchStatus() {
    if (!branchStatus) {
      branchStatus = await platform.getBranchStatus(
        branchName,
        config.requiredStatusChecks
      );
      logger.debug({ branchStatus, branchName }, 'getBranchStatus() result');
    }
    return branchStatus;
  }

  // Only create a PR if a branch automerge has failed
  if (
    config.automerge === true &&
    config.automergeType.startsWith('branch') &&
    !config.forcePr
  ) {
    logger.debug(
      `Branch is configured for branch automerge, branch status) is: ${await getBranchStatus()}`
    );
    if (
      (await getBranchStatus()) === 'pending' ||
      (await getBranchStatus()) === 'running'
    ) {
      logger.debug('Checking how long this branch has been pending');
      const lastCommitTime = await platform.getBranchLastCommitTime(branchName);
      const currentTime = new Date();
      const millisecondsPerHour = 1000 * 60 * 60;
      const elapsedHours = Math.round(
        (currentTime.getTime() - lastCommitTime.getTime()) / millisecondsPerHour
      );
      if (elapsedHours >= config.prNotPendingHours) {
        logger.info('Branch exceeds prNotPending hours - forcing PR creation');
        config.forcePr = true;
      }
    }
    if (config.forcePr || (await getBranchStatus()) === 'failure') {
      logger.debug(`Branch tests failed, so will create PR`);
    } else {
      return null;
    }
  }
  if (config.prCreation === 'status-success') {
    logger.debug('Checking branch combined status');
    if ((await getBranchStatus()) !== 'success') {
      logger.debug(
        `Branch status is "${await getBranchStatus()}" - not creating PR`
      );
      return null;
    }
    logger.debug('Branch status success');
  } else if (
    config.prCreation === 'not-pending' &&
    !existingPr &&
    !config.forcePr
  ) {
    logger.debug('Checking branch combined status');
    if (
      (await getBranchStatus()) === 'pending' ||
      (await getBranchStatus()) === 'running'
    ) {
      logger.debug(
        `Branch status is "${await getBranchStatus()}" - checking timeout`
      );
      const lastCommitTime = await platform.getBranchLastCommitTime(branchName);
      const currentTime = new Date();
      const millisecondsPerHour = 1000 * 60 * 60;
      const elapsedHours = Math.round(
        (currentTime.getTime() - lastCommitTime.getTime()) / millisecondsPerHour
      );
      if (elapsedHours < config.prNotPendingHours) {
        logger.debug(
          `Branch is ${elapsedHours} hours old - skipping PR creation`
        );
        return null;
      }
      logger.debug(
        `prNotPendingHours=${
          config.prNotPendingHours
        } threshold hit - creating PR`
      );
    }
    logger.debug('Branch status success');
  }

  const processedUpgrades = [];
  const commitRepos = [];

  // Get changelog and then generate template strings
  for (const upgrade of upgrades) {
    const upgradeKey = `${upgrade.depType}-${upgrade.depName}-${
      upgrade.manager
    }-${upgrade.fromVersion || upgrade.currentValue}-${upgrade.toVersion}`;
    if (processedUpgrades.includes(upgradeKey)) {
      continue; // eslint-disable-line no-continue
    }
    processedUpgrades.push(upgradeKey);
    upgrade.hasUrls = !!(upgrade.sourceUrl || upgrade.homepage);

    const logJSON = await changelogHelper.getChangeLogJSON({
      manager: upgrade.manager,
      versionScheme: upgrade.versionScheme,
      depType: upgrade.depType,
      depName: upgrade.depName,
      fromVersion: upgrade.fromVersion,
      toVersion: upgrade.toVersion,
      sourceUrl: upgrade.sourceUrl,
      releases: upgrade.releases,
    });

    if (logJSON) {
      upgrade.githubName = logJSON.project ? logJSON.project.github : undefined;
      upgrade.hasReleaseNotes = logJSON.hasReleaseNotes;
      upgrade.releases = [];
      if (
        upgrade.hasReleaseNotes &&
        upgrade.githubName &&
        !commitRepos.includes(upgrade.githubName)
      ) {
        commitRepos.push(upgrade.githubName);
        logJSON.versions.forEach(version => {
          const release = { ...version };
          upgrade.releases.push(release);
        });
      }
    }
    config.upgrades.push(upgrade);
  }

  // Update the config object
  Object.assign(config, upgrades[0]);
  config.hasReleaseNotes = config.upgrades.some(upg => upg.hasReleaseNotes);

  const releaseNoteRepos = [];
  for (const upgrade of config.upgrades) {
    if (upgrade.hasReleaseNotes) {
      if (releaseNoteRepos.includes(upgrade.sourceUrl)) {
        logger.debug(
          { depName: upgrade.depName },
          'Removing duplicate release notes'
        );
        upgrade.hasReleaseNotes = false;
      } else {
        releaseNoteRepos.push(upgrade.sourceUrl);
      }
    }
  }

  const prBody = await getPrBody(config);

  try {
    if (existingPr) {
      logger.debug('Processing existing PR');
      // istanbul ignore if
      if (
        config.automerge &&
        ['failure', 'error', 'failed'].includes(await getBranchStatus())
      ) {
        logger.debug(`Setting assignees and reviewers as status checks failed`);
        await addAssigneesReviewers(config, existingPr);
      }
      // Check if existing PR needs updating
      const reviewableIndex = existingPr.body.indexOf(
        '<!-- Reviewable:start -->'
      );
      let existingPrBody = existingPr.body;
      if (reviewableIndex > -1) {
        logger.debug('Stripping Reviewable content');
        existingPrBody = existingPrBody.slice(0, reviewableIndex);
      }
      existingPrBody = existingPrBody.trim();
      if (
        existingPr.title === prTitle &&
        noWhitespace(existingPrBody) === noWhitespace(prBody)
      ) {
        logger.debug(`${existingPr.displayNumber} does not need updating`);
        return existingPr;
      }
      // PR must need updating
      if (existingPr.title !== prTitle) {
        logger.info(
          {
            branchName,
            oldPrTitle: existingPr.title,
            newPrTitle: prTitle,
          },
          'PR title changed'
        );
      } else if (!config.committedFiles) {
        logger.debug(
          {
            prTitle,
            oldPrBody: existingPrBody,
            newPrBody: prBody,
          },
          'PR body changed'
        );
      }
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would update PR #' + existingPr.number);
      } else {
        await platform.updatePr(existingPr.number, prTitle, prBody);
        logger.info(
          { committedFiles: config.committedFiles, pr: existingPr.number },
          `PR updated`
        );
      }
      return existingPr;
    }
    logger.debug({ branch: branchName, prTitle }, `Creating PR`);
    // istanbul ignore if
    if (config.updateType === 'rollback') {
      logger.info('Creating Rollback PR');
    }
    let pr;
    try {
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would create PR: ' + prTitle);
        pr = { number: 0, displayNumber: 'Dry run PR' };
      } else {
        pr = await platform.createPr(
          branchName,
          prTitle,
          prBody,
          config.labels,
          false,
          config.statusCheckVerify
        );
        logger.info({ branch: branchName, pr: pr.number }, 'PR created');
      }
    } catch (err) {
      logger.warn({ err }, `Failed to create PR`);
      if (err.message === 'Validation Failed (422)') {
        logger.info({ branch: branchName }, 'Deleting invalid branch');
        // istanbul ignore if
        if (config.dryRun) {
          logger.info('DRY-RUN: Would delete branch: ' + config.branchName);
        } else {
          await platform.deleteBranch(branchName);
        }
      }
      // istanbul ignore if
      if (err.statusCode === 502) {
        logger.info(
          { branch: branchName },
          'Deleting branch due to server error'
        );
        // istanbul ignore if
        if (config.dryRun) {
          logger.info('DRY-RUN: Would delete branch: ' + config.branchName);
        } else {
          await platform.deleteBranch(branchName);
        }
      }
      return null;
    }
    if (
      config.branchAutomergeFailureMessage &&
      !config.suppressNotifications.includes('branchAutomergeFailure')
    ) {
      const subject = 'Branch automerge failure';
      let content =
        'This PR was configured for branch automerge, however this is not possible so it has been raised as a PR instead.';
      if (config.branchAutomergeFailureMessage === 'branch status error') {
        content += '\n___\n * Branch has one or more failed status checks';
      }
      logger.info('Adding branch automerge failure message to PR');
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('Would add comment to PR #' + pr.number);
      } else {
        await platform.ensureComment(pr.number, subject, content);
      }
    }
    // Skip assign and review if automerging PR
    if (
      config.automerge &&
      !['failure', 'error', 'failed'].includes(await getBranchStatus())
    ) {
      logger.debug(
        `Skipping assignees and reviewers as automerge=${config.automerge}`
      );
    } else {
      await addAssigneesReviewers(config, pr);
    }
    logger.info(`Created ${pr.displayNumber}`);
    return pr;
  } catch (err) {
    // istanbul ignore if
    if (
      err.message === 'rate-limit-exceeded' ||
      err.message === 'platform-failure' ||
      err.message === 'integration-unauthorized'
    ) {
      logger.debug('Passing error up');
      throw err;
    }
    logger.error({ err }, 'Failed to ensure PR:', err);
  }
  return null;
}

async function addAssigneesReviewers(config, pr) {
  if (config.assignees.length > 0) {
    try {
      const assignees = config.assignees.map(assignee =>
        assignee.length && assignee[0] === '@' ? assignee.slice(1) : assignee
      );
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would add assignees to PR #', pr.number);
      } else {
        await platform.addAssignees(pr.number, assignees);
        logger.info({ assignees: config.assignees }, 'Added assignees');
      }
    } catch (err) {
      logger.info(
        { assignees: config.assignees, err },
        'Failed to add assignees'
      );
    }
  }
  if (config.reviewers.length > 0) {
    try {
      const reviewers = config.reviewers.map(reviewer =>
        reviewer.length && reviewer[0] === '@' ? reviewer.slice(1) : reviewer
      );
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would add assignees to PR #', pr.number);
      } else {
        await platform.addReviewers(pr.number, reviewers);
        logger.info({ reviewers: config.reviewers }, 'Added reviewers');
      }
    } catch (err) {
      logger.info(
        { assignees: config.assignees, err },
        'Failed to add reviewers'
      );
    }
  }
}

async function checkAutoMerge(pr, config) {
  logger.trace({ config }, 'checkAutoMerge');
  const {
    branchName,
    automerge,
    automergeType,
    automergeComment,
    requiredStatusChecks,
  } = config;
  logger.debug(
    { automerge, automergeType, automergeComment },
    `Checking #${pr.number} for automerge`
  );
  if (automerge) {
    logger.debug('PR is configured for automerge');
    // Return if PR not ready for automerge
    if (pr.isConflicted) {
      logger.info('PR is conflicted');
      logger.debug({ pr });
      return false;
    }
    if (requiredStatusChecks && pr.canMerge !== true) {
      logger.info('PR is not ready for merge');
      return false;
    }
    const branchStatus = await platform.getBranchStatus(
      branchName,
      requiredStatusChecks
    );
    if (branchStatus !== 'success') {
      logger.info(
        `PR is not ready for merge (branch status is ${branchStatus})`
      );
      return false;
    }
    // Check if it's been touched
    if (!pr.canRebase) {
      logger.info('PR is ready for automerge but has been modified');
      return false;
    }
    if (automergeType === 'pr-comment') {
      logger.info(`Applying automerge comment: ${automergeComment}`);
      // istanbul ignore if
      if (config.dryRun) {
        logger.info(
          'DRY-RUN: Would add PR automerge comment to PR #' + pr.number
        );
        return false;
      }
      return platform.ensureComment(pr.number, null, automergeComment);
    }
    // Let's merge this
    logger.debug(`Automerging #${pr.number}`);
    // istanbul ignore if
    if (config.dryRun) {
      logger.info('DRY-RUN: Would merge PR #' + pr.number);
      return false;
    }
    const res = await platform.mergePr(pr.number, branchName);
    if (res) {
      logger.info({ pr: pr.number }, 'PR automerged');
    }
    return res;
  }
  logger.debug('No automerge');
  return false;
}

function noWhitespace(input) {
  return input.replace(/\r?\n|\r|\s/g, '');
}
<commit_msg>refactor(pr): don’t warn if validation fails<commit_after>const changelogHelper = require('./changelog');
const { getPrBody } = require('./pr-body');

module.exports = {
  ensurePr,
  checkAutoMerge,
};
// Ensures that PR exists with matching title/body
async function ensurePr(prConfig) {
  const config = { ...prConfig };

  logger.trace({ config }, 'ensurePr');
  // If there is a group, it will use the config of the first upgrade in the array
  const { branchName, prTitle, upgrades } = config;
  // Check if existing PR exists
  const existingPr = await platform.getBranchPr(branchName);
  if (existingPr) {
    logger.debug('Found existing PR');
  }
  config.upgrades = [];

  if (config.artifactErrors && config.artifactErrors.length) {
    logger.debug('Forcing PR because of artifact errors');
    config.forcePr = true;
  }

  let branchStatus;
  async function getBranchStatus() {
    if (!branchStatus) {
      branchStatus = await platform.getBranchStatus(
        branchName,
        config.requiredStatusChecks
      );
      logger.debug({ branchStatus, branchName }, 'getBranchStatus() result');
    }
    return branchStatus;
  }

  // Only create a PR if a branch automerge has failed
  if (
    config.automerge === true &&
    config.automergeType.startsWith('branch') &&
    !config.forcePr
  ) {
    logger.debug(
      `Branch is configured for branch automerge, branch status) is: ${await getBranchStatus()}`
    );
    if (
      (await getBranchStatus()) === 'pending' ||
      (await getBranchStatus()) === 'running'
    ) {
      logger.debug('Checking how long this branch has been pending');
      const lastCommitTime = await platform.getBranchLastCommitTime(branchName);
      const currentTime = new Date();
      const millisecondsPerHour = 1000 * 60 * 60;
      const elapsedHours = Math.round(
        (currentTime.getTime() - lastCommitTime.getTime()) / millisecondsPerHour
      );
      if (elapsedHours >= config.prNotPendingHours) {
        logger.info('Branch exceeds prNotPending hours - forcing PR creation');
        config.forcePr = true;
      }
    }
    if (config.forcePr || (await getBranchStatus()) === 'failure') {
      logger.debug(`Branch tests failed, so will create PR`);
    } else {
      return null;
    }
  }
  if (config.prCreation === 'status-success') {
    logger.debug('Checking branch combined status');
    if ((await getBranchStatus()) !== 'success') {
      logger.debug(
        `Branch status is "${await getBranchStatus()}" - not creating PR`
      );
      return null;
    }
    logger.debug('Branch status success');
  } else if (
    config.prCreation === 'not-pending' &&
    !existingPr &&
    !config.forcePr
  ) {
    logger.debug('Checking branch combined status');
    if (
      (await getBranchStatus()) === 'pending' ||
      (await getBranchStatus()) === 'running'
    ) {
      logger.debug(
        `Branch status is "${await getBranchStatus()}" - checking timeout`
      );
      const lastCommitTime = await platform.getBranchLastCommitTime(branchName);
      const currentTime = new Date();
      const millisecondsPerHour = 1000 * 60 * 60;
      const elapsedHours = Math.round(
        (currentTime.getTime() - lastCommitTime.getTime()) / millisecondsPerHour
      );
      if (elapsedHours < config.prNotPendingHours) {
        logger.debug(
          `Branch is ${elapsedHours} hours old - skipping PR creation`
        );
        return null;
      }
      logger.debug(
        `prNotPendingHours=${
          config.prNotPendingHours
        } threshold hit - creating PR`
      );
    }
    logger.debug('Branch status success');
  }

  const processedUpgrades = [];
  const commitRepos = [];

  // Get changelog and then generate template strings
  for (const upgrade of upgrades) {
    const upgradeKey = `${upgrade.depType}-${upgrade.depName}-${
      upgrade.manager
    }-${upgrade.fromVersion || upgrade.currentValue}-${upgrade.toVersion}`;
    if (processedUpgrades.includes(upgradeKey)) {
      continue; // eslint-disable-line no-continue
    }
    processedUpgrades.push(upgradeKey);
    upgrade.hasUrls = !!(upgrade.sourceUrl || upgrade.homepage);

    const logJSON = await changelogHelper.getChangeLogJSON({
      manager: upgrade.manager,
      versionScheme: upgrade.versionScheme,
      depType: upgrade.depType,
      depName: upgrade.depName,
      fromVersion: upgrade.fromVersion,
      toVersion: upgrade.toVersion,
      sourceUrl: upgrade.sourceUrl,
      releases: upgrade.releases,
    });

    if (logJSON) {
      upgrade.githubName = logJSON.project ? logJSON.project.github : undefined;
      upgrade.hasReleaseNotes = logJSON.hasReleaseNotes;
      upgrade.releases = [];
      if (
        upgrade.hasReleaseNotes &&
        upgrade.githubName &&
        !commitRepos.includes(upgrade.githubName)
      ) {
        commitRepos.push(upgrade.githubName);
        logJSON.versions.forEach(version => {
          const release = { ...version };
          upgrade.releases.push(release);
        });
      }
    }
    config.upgrades.push(upgrade);
  }

  // Update the config object
  Object.assign(config, upgrades[0]);
  config.hasReleaseNotes = config.upgrades.some(upg => upg.hasReleaseNotes);

  const releaseNoteRepos = [];
  for (const upgrade of config.upgrades) {
    if (upgrade.hasReleaseNotes) {
      if (releaseNoteRepos.includes(upgrade.sourceUrl)) {
        logger.debug(
          { depName: upgrade.depName },
          'Removing duplicate release notes'
        );
        upgrade.hasReleaseNotes = false;
      } else {
        releaseNoteRepos.push(upgrade.sourceUrl);
      }
    }
  }

  const prBody = await getPrBody(config);

  try {
    if (existingPr) {
      logger.debug('Processing existing PR');
      // istanbul ignore if
      if (
        config.automerge &&
        ['failure', 'error', 'failed'].includes(await getBranchStatus())
      ) {
        logger.debug(`Setting assignees and reviewers as status checks failed`);
        await addAssigneesReviewers(config, existingPr);
      }
      // Check if existing PR needs updating
      const reviewableIndex = existingPr.body.indexOf(
        '<!-- Reviewable:start -->'
      );
      let existingPrBody = existingPr.body;
      if (reviewableIndex > -1) {
        logger.debug('Stripping Reviewable content');
        existingPrBody = existingPrBody.slice(0, reviewableIndex);
      }
      existingPrBody = existingPrBody.trim();
      if (
        existingPr.title === prTitle &&
        noWhitespace(existingPrBody) === noWhitespace(prBody)
      ) {
        logger.debug(`${existingPr.displayNumber} does not need updating`);
        return existingPr;
      }
      // PR must need updating
      if (existingPr.title !== prTitle) {
        logger.info(
          {
            branchName,
            oldPrTitle: existingPr.title,
            newPrTitle: prTitle,
          },
          'PR title changed'
        );
      } else if (!config.committedFiles) {
        logger.debug(
          {
            prTitle,
            oldPrBody: existingPrBody,
            newPrBody: prBody,
          },
          'PR body changed'
        );
      }
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would update PR #' + existingPr.number);
      } else {
        await platform.updatePr(existingPr.number, prTitle, prBody);
        logger.info(
          { committedFiles: config.committedFiles, pr: existingPr.number },
          `PR updated`
        );
      }
      return existingPr;
    }
    logger.debug({ branch: branchName, prTitle }, `Creating PR`);
    // istanbul ignore if
    if (config.updateType === 'rollback') {
      logger.info('Creating Rollback PR');
    }
    let pr;
    try {
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would create PR: ' + prTitle);
        pr = { number: 0, displayNumber: 'Dry run PR' };
      } else {
        pr = await platform.createPr(
          branchName,
          prTitle,
          prBody,
          config.labels,
          false,
          config.statusCheckVerify
        );
        logger.info({ branch: branchName, pr: pr.number }, 'PR created');
      }
    } catch (err) /* istanbul ignore next */ {
      if (err.message === 'Validation Failed (422)') {
        logger.info({ branch: branchName }, 'Deleting invalid branch');
        // istanbul ignore if
        if (config.dryRun) {
          logger.info('DRY-RUN: Would delete branch: ' + config.branchName);
        } else {
          await platform.deleteBranch(branchName);
        }
        return null;
      }
      logger.warn({ err }, `Failed to create PR`);
      if (err.statusCode === 502) {
        logger.info(
          { branch: branchName },
          'Deleting branch due to server error'
        );
        // istanbul ignore if
        if (config.dryRun) {
          logger.info('DRY-RUN: Would delete branch: ' + config.branchName);
        } else {
          await platform.deleteBranch(branchName);
        }
      }
      return null;
    }
    if (
      config.branchAutomergeFailureMessage &&
      !config.suppressNotifications.includes('branchAutomergeFailure')
    ) {
      const subject = 'Branch automerge failure';
      let content =
        'This PR was configured for branch automerge, however this is not possible so it has been raised as a PR instead.';
      if (config.branchAutomergeFailureMessage === 'branch status error') {
        content += '\n___\n * Branch has one or more failed status checks';
      }
      logger.info('Adding branch automerge failure message to PR');
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('Would add comment to PR #' + pr.number);
      } else {
        await platform.ensureComment(pr.number, subject, content);
      }
    }
    // Skip assign and review if automerging PR
    if (
      config.automerge &&
      !['failure', 'error', 'failed'].includes(await getBranchStatus())
    ) {
      logger.debug(
        `Skipping assignees and reviewers as automerge=${config.automerge}`
      );
    } else {
      await addAssigneesReviewers(config, pr);
    }
    logger.info(`Created ${pr.displayNumber}`);
    return pr;
  } catch (err) {
    // istanbul ignore if
    if (
      err.message === 'rate-limit-exceeded' ||
      err.message === 'platform-failure' ||
      err.message === 'integration-unauthorized'
    ) {
      logger.debug('Passing error up');
      throw err;
    }
    logger.error({ err }, 'Failed to ensure PR:', err);
  }
  return null;
}

async function addAssigneesReviewers(config, pr) {
  if (config.assignees.length > 0) {
    try {
      const assignees = config.assignees.map(assignee =>
        assignee.length && assignee[0] === '@' ? assignee.slice(1) : assignee
      );
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would add assignees to PR #', pr.number);
      } else {
        await platform.addAssignees(pr.number, assignees);
        logger.info({ assignees: config.assignees }, 'Added assignees');
      }
    } catch (err) {
      logger.info(
        { assignees: config.assignees, err },
        'Failed to add assignees'
      );
    }
  }
  if (config.reviewers.length > 0) {
    try {
      const reviewers = config.reviewers.map(reviewer =>
        reviewer.length && reviewer[0] === '@' ? reviewer.slice(1) : reviewer
      );
      // istanbul ignore if
      if (config.dryRun) {
        logger.info('DRY-RUN: Would add assignees to PR #', pr.number);
      } else {
        await platform.addReviewers(pr.number, reviewers);
        logger.info({ reviewers: config.reviewers }, 'Added reviewers');
      }
    } catch (err) {
      logger.info(
        { assignees: config.assignees, err },
        'Failed to add reviewers'
      );
    }
  }
}

async function checkAutoMerge(pr, config) {
  logger.trace({ config }, 'checkAutoMerge');
  const {
    branchName,
    automerge,
    automergeType,
    automergeComment,
    requiredStatusChecks,
  } = config;
  logger.debug(
    { automerge, automergeType, automergeComment },
    `Checking #${pr.number} for automerge`
  );
  if (automerge) {
    logger.debug('PR is configured for automerge');
    // Return if PR not ready for automerge
    if (pr.isConflicted) {
      logger.info('PR is conflicted');
      logger.debug({ pr });
      return false;
    }
    if (requiredStatusChecks && pr.canMerge !== true) {
      logger.info('PR is not ready for merge');
      return false;
    }
    const branchStatus = await platform.getBranchStatus(
      branchName,
      requiredStatusChecks
    );
    if (branchStatus !== 'success') {
      logger.info(
        `PR is not ready for merge (branch status is ${branchStatus})`
      );
      return false;
    }
    // Check if it's been touched
    if (!pr.canRebase) {
      logger.info('PR is ready for automerge but has been modified');
      return false;
    }
    if (automergeType === 'pr-comment') {
      logger.info(`Applying automerge comment: ${automergeComment}`);
      // istanbul ignore if
      if (config.dryRun) {
        logger.info(
          'DRY-RUN: Would add PR automerge comment to PR #' + pr.number
        );
        return false;
      }
      return platform.ensureComment(pr.number, null, automergeComment);
    }
    // Let's merge this
    logger.debug(`Automerging #${pr.number}`);
    // istanbul ignore if
    if (config.dryRun) {
      logger.info('DRY-RUN: Would merge PR #' + pr.number);
      return false;
    }
    const res = await platform.mergePr(pr.number, branchName);
    if (res) {
      logger.info({ pr: pr.number }, 'PR automerged');
    }
    return res;
  }
  logger.debug('No automerge');
  return false;
}

function noWhitespace(input) {
  return input.replace(/\r?\n|\r|\s/g, '');
}
<|endoftext|>


<commit_before>var _ = require('underscore'),
    seq = require('seq'),
    fileUtils = require('../fileUtils'),
    error = require('../error'),
    assets = require('../assets'),
    query = require('../query');

module.exports = function (includeRelationQuery, initialAssetsQuery) { // includeRelationQuery and initialAssetsQuery are optional
    var includeRelationMatcher = query.queryObjToMatcherFunction(includeRelationQuery);
    return function populate(err, assetGraph, cb) {
        if (err) {
            throw err;
        }
        var assetQueue = assetGraph.findAssets(initialAssetsQuery),
            maxWaitingCallbacks = 100,
            numWaitingCallbacks = 0,
            callbackCalled = false;

        cb = error.onlyCallOnce(cb);

        (function proceed() {
            while (assetQueue.length && numWaitingCallbacks < maxWaitingCallbacks) {
                numWaitingCallbacks += 1;
                processAsset(assetQueue.shift(), function (err) {
                    if (err) {
                        cb(err);
                    }
                    numWaitingCallbacks -= 1;
                    proceed();
                });
            }
            if (!numWaitingCallbacks) {
                cb();
            }
        })();

        function processAsset(asset, cb) {
            var lastSeenRelation;

            function initializeAndAddRelation(relation) {
                if (relation.to.url && relation.to.url in assetGraph.urlIndex) {
                    relation.to = assetGraph.urlIndex[relation.to.url];
                } else {
                    relation.to = assets.create(relation.to);
                    assetGraph.addAsset(relation.to);
                    assetQueue.push(relation.to);
                }
                if (lastSeenRelation) {
                    assetGraph.addRelation(relation, 'after', lastSeenRelation);
                } else {
                    assetGraph.addRelation(relation, 'first');
                }
                lastSeenRelation = relation;
            }

            seq()
                .seq(function () {
                    asset.getOriginalRelations(this);
                })
                .seq(function (originalRelations) {
                    this(null, originalRelations.filter(includeRelationMatcher));
                })
                .flatten()
                .parEach(function (originalRelation) {
                    assetGraph.resolver.resolve(originalRelation.to, assetGraph.getBaseAssetForRelation(originalRelation).url, this.into(originalRelation.id));
                })
                .parEach(function (originalRelation) {
                    var resolvedAssetConfigs = this.vars[originalRelation.id],
                        lastSeenRelation;
                    if (!_.isArray(resolvedAssetConfigs)) {
                        // Simple case
                        resolvedAssetConfigs = [resolvedAssetConfigs];
                    }
                    if (resolvedAssetConfigs.length === 0) {
                        asset.detachRelation(originalRelation);
                    } else if (resolvedAssetConfigs.length === 1) {
                        originalRelation.to = resolvedAssetConfigs[0];
                        initializeAndAddRelation(originalRelation);
                    } else if (asset.attachRelation) {
                        resolvedAssetConfigs.forEach(function (resolvedAssetConfig) {
                            var relation = new originalRelation.constructor({
                                from: asset,
                                to: resolvedAssetConfig
                            });
                            initializeAndAddRelation(relation);
                            relation.from.attachRelation(relation, 'before', originalRelation);
                        });
                        asset.detachRelation(originalRelation);
                    } else {
                        cb(new Error("assetConfig resolved to multiple, but " + originalRelation.type + " doesn't support attachRelation"));
                    }
                    this();
                })
                .seq(function () {
                    cb();
                })
                ['catch'](cb);
        }
    };
};
<commit_msg>transforms.populate: Minor bugfix in error propagation.<commit_after>var _ = require('underscore'),
    seq = require('seq'),
    fileUtils = require('../fileUtils'),
    error = require('../error'),
    assets = require('../assets'),
    query = require('../query');

module.exports = function (includeRelationQuery, initialAssetsQuery) { // includeRelationQuery and initialAssetsQuery are optional
    var includeRelationMatcher = query.queryObjToMatcherFunction(includeRelationQuery);
    return function populate(err, assetGraph, cb) {
        if (err) {
            throw err;
        }
        var assetQueue = assetGraph.findAssets(initialAssetsQuery),
            maxWaitingCallbacks = 100,
            numWaitingCallbacks = 0,
            callbackCalled = false;

        cb = error.onlyCallOnce(cb);

        (function proceed() {
            while (assetQueue.length && numWaitingCallbacks < maxWaitingCallbacks) {
                numWaitingCallbacks += 1;
                processAsset(assetQueue.shift(), function (err) {
                    if (err) {
                        cb(err);
                    }
                    numWaitingCallbacks -= 1;
                    proceed();
                });
            }
            if (!numWaitingCallbacks) {
                cb();
            }
        })();

        function processAsset(asset, cb) {
            var lastSeenRelation;

            function initializeAndAddRelation(relation) {
                if (relation.to.url && relation.to.url in assetGraph.urlIndex) {
                    relation.to = assetGraph.urlIndex[relation.to.url];
                } else {
                    relation.to = assets.create(relation.to);
                    assetGraph.addAsset(relation.to);
                    assetQueue.push(relation.to);
                }
                if (lastSeenRelation) {
                    assetGraph.addRelation(relation, 'after', lastSeenRelation);
                } else {
                    assetGraph.addRelation(relation, 'first');
                }
                lastSeenRelation = relation;
            }

            seq()
                .seq(function () {
                    asset.getOriginalRelations(this);
                })
                .seq(function (originalRelations) {
                    this(null, originalRelations.filter(includeRelationMatcher));
                })
                .flatten()
                .parEach(function (originalRelation) {
                    assetGraph.resolver.resolve(originalRelation.to, assetGraph.getBaseAssetForRelation(originalRelation).url, this.into(originalRelation.id));
                })
                .parEach(function (originalRelation) {
                    var resolvedAssetConfigs = this.vars[originalRelation.id],
                        lastSeenRelation;
                    if (!_.isArray(resolvedAssetConfigs)) {
                        // Simple case
                        resolvedAssetConfigs = [resolvedAssetConfigs];
                    }
                    if (resolvedAssetConfigs.length === 0) {
                        asset.detachRelation(originalRelation);
                    } else if (resolvedAssetConfigs.length === 1) {
                        originalRelation.to = resolvedAssetConfigs[0];
                        initializeAndAddRelation(originalRelation);
                    } else if (asset.attachRelation) {
                        resolvedAssetConfigs.forEach(function (resolvedAssetConfig) {
                            var relation = new originalRelation.constructor({
                                from: asset,
                                to: resolvedAssetConfig
                            });
                            initializeAndAddRelation(relation);
                            relation.from.attachRelation(relation, 'before', originalRelation);
                        });
                        asset.detachRelation(originalRelation);
                    } else {
                        return cb(new Error("assetConfig resolved to multiple, but " + originalRelation.type + " doesn't support attachRelation"));
                    }
                    this();
                })
                .seq(function () {
                    cb();
                })
                ['catch'](cb);
        }
    };
};
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import isEmpty from 'lodash/isEmpty';

// Services
import WidgetsService from 'services/WidgetsService';
import DatasetsService from 'services/DatasetsService';
import LayersService from 'services/LayersService';

import { toastr } from 'react-redux-toastr';

// Redux
import { connect } from 'react-redux';

import {
  setFilters,
  setColor,
  setCategory,
  setValue,
  setSize,
  setOrderBy,
  setAggregateFunction,
  setLimit,
  setChartType,
  setBand,
  setVisualizationType,
  setLayer,
  setTitle
} from 'components/widgets/editor/redux/widgetEditor';

// Constants
import { STATE_DEFAULT, FORM_ELEMENTS } from 'components/admin/widgets/form/constants';

// Components
import Navigation from 'components/form/Navigation';
import Step1 from 'components/admin/widgets/form/steps/Step1';
import Spinner from 'components/ui/Spinner';

class WidgetsForm extends React.Component {
  constructor(props) {
    super(props);

    const formObj = props.dataset ?
      Object.assign({}, STATE_DEFAULT.form, { dataset: props.dataset }) :
      STATE_DEFAULT.form;

    this.state = Object.assign({}, STATE_DEFAULT, {
      id: props.id,
      loading: !!props.id,
      form: formObj,
      mode: 'editor'
    });

    // BINDINGS
    this.onSubmit = this.onSubmit.bind(this);
    this.onChange = this.onChange.bind(this);
    this.handleModeChange = this.handleModeChange.bind(this);
    this.onStepChange = this.onStepChange.bind(this);

    this.service = new WidgetsService({
      authorization: props.authorization
    });

    this.datasetsService = new DatasetsService({
      authorization: props.authorization
    });
  }

  componentDidMount() {
    const { id } = this.state;

    const promises = [
      this.datasetsService.fetchAllData({})
    ];

    // Add the dashboard promise if the id exists
    if (id) {
      promises.push(this.service.fetchData({ id }));
    }

    Promise.all(promises)
      .then((response) => {
        const datasets = response[0];
        const current = response[1];

        // Set advanced mode if paramsConfig doesn't exist or if it's empty
        const mode = (
          current &&
          (!current.widgetConfig.paramsConfig || isEmpty(current.widgetConfig.paramsConfig))
        ) ? 'advanced' : 'editor';

        this.setState({
          // CURRENT DASHBOARD
          form: (id) ? this.setFormFromParams(current) : this.state.form,
          loading: false,
          mode,
          // OPTIONS
          datasets: datasets.map(p => ({ label: p.name, value: p.id }))
        }, () => this.loadWidgetIntoRedux());
      })
      .catch((err) => {
        toastr.error(err);
      });
  }

  /**
   * UI EVENTS
   * - onSubmit
   * - onChange
   * - handleModeChange
  */
  onSubmit(event) {
    const { submitting, stepLength, step, form, mode } = this.state;
    const { widgetEditor } = this.props;
    const {
      limit,
      value,
      category,
      color,
      size,
      orderBy,
      aggregateFunction,
      chartType,
      filters,
      areaIntersection,
      visualizationType,
      band,
      layer,
      title
    } = widgetEditor;

    event.preventDefault();

    // Validate the form
    FORM_ELEMENTS.validate(step);

    // Set a timeout due to the setState function of react
    setTimeout(() => {
      // Validate all the inputs on the current step
      const validWidgetConfig = (mode === 'editor') ? this.validateWidgetConfig() : true;
      const valid = FORM_ELEMENTS.isValid(step) && validWidgetConfig;
      if (valid) {
        // if we are in the last step we will submit the form
        if (step === stepLength && !submitting) {
          const { id } = this.state;

          // Start the submitting
          this.setState({ submitting: true });

          // The name of the widget is the title property of the
          // widgetEditor reducer
          let formObj = Object.assign({}, form, { name: title || '' });

          if (mode === 'editor') {
            const newWidgetConfig = {
              widgetConfig: Object.assign(
                {},
                // If the widget is a map, we want to add some extra info
                // in widgetConfig so the widget is compatible with other
                // apps that use the same API
                // This info is not necessary for the editor because it is
                // already saved in widgetConfig.paramsConfig
                (
                  visualizationType === 'map'
                    ? { type: 'map', layer_id: layer && layer.id }
                    : {}
                ),
                {
                  paramsConfig: {
                    visualizationType,
                    limit,
                    value,
                    category,
                    color,
                    size,
                    orderBy,
                    aggregateFunction,
                    chartType,
                    filters,
                    areaIntersection,
                    band: band && { name: band.name },
                    layer: layer && layer.id
                  }
                },
                formObj.widgetConfig
              )
            };

            formObj = Object.assign({}, formObj, newWidgetConfig);
          }

          const obj = {
            dataset: form.dataset,
            id: id || '',
            type: (id) ? 'PATCH' : 'POST',
            body: formObj
          };

          if (obj.body.sourceUrl === '') {
            delete obj.body.sourceUrl;
          }

          // Save data
          this.service.saveData(obj)
            .then((data) => {
              toastr.success('Success', `The widget "${data.id}" - "${data.name}" has been uploaded correctly`);

              if (this.props.onSubmit) this.props.onSubmit();
            })
            .catch((errors) => {
              this.setState({ submitting: false });

              try {
                errors.forEach(er =>
                  toastr.error('Error', er.detail)
                );
              } catch (e) {
                toastr.error('Error', 'Oops! There was an error, try again.');
              }
            });
        } else {
          this.setState({
            step: this.state.step + 1
          });
        }
      } else {
        if (!validWidgetConfig && mode === 'editor') {
          return this.errorValidationWidgetConfig();
        }

        toastr.error('Error', 'Fill all the required fields or correct the invalid values');
      }
    }, 0);
  }

  onChange(obj) {
    const form = Object.assign({}, this.state.form, obj);
    this.setState({ form });
  }

  onStepChange(step) {
    this.setState({ step });
  }

  // HELPERS
  setFormFromParams(params) {
    const newForm = {};

    Object.keys(params).forEach((f) => {
      switch (f) {
        default: {
          if ((typeof params[f] !== 'undefined' || params[f] !== null) ||
              (typeof this.state.form[f] !== 'undefined' || this.state.form[f] !== null)) {
            newForm[f] = params[f] || this.state.form[f];
          }
        }
      }
    });

    return newForm;
  }

  validateWidgetConfig() {
    const { value, category, chartType, visualizationType, layer } = this.props.widgetEditor;

    switch (visualizationType) {
      case 'chart':
        return !!chartType && !!category && !!value;
      case 'table':
        return !!chartType && !!category && !!value;
      case 'map':
        return !!layer;
      default:
        return false;
    }
  }

  errorValidationWidgetConfig() {
    const { visualizationType } = this.props.widgetEditor;

    switch (visualizationType) {
      case 'chart':
        return toastr.error('Error', 'Value, Category and Chart type are mandatory fields for a widget visualization.');
      case 'table':
        return toastr.error('Error', 'Value, Category and Chart type are mandatory fields for a widget visualization.');
      case 'map':
        return toastr.error('Error', 'Layer is mandatory field for a widget visualization.');
      default:
        return false;
    }
  }

  loadWidgetIntoRedux() {
    const { paramsConfig } = this.state.form.widgetConfig;
    if (paramsConfig) {
      const {
        visualizationType,
        band,
        value,
        category,
        color,
        size,
        aggregateFunction,
        orderBy,
        filters,
        limit,
        chartType,
        layer
      } = paramsConfig;

      // We restore the type of visualization
      // We default to "chart" to maintain the compatibility with previously created
      // widgets (at that time, only "chart" widgets could be created)
      this.props.setVisualizationType(visualizationType || 'chart');

      if (band) this.props.setBand(band);
      if (layer) this.props.setLayer(layer);
      if (aggregateFunction) this.props.setAggregateFunction(aggregateFunction);
      if (value) this.props.setValue(value);
      if (size) this.props.setSize(size);
      if (color) this.props.setColor(color);
      if (orderBy) this.props.setOrderBy(orderBy);
      if (category) this.props.setCategory(category);
      if (filters) this.props.setFilters(filters);
      if (limit) this.props.setLimit(limit);
      if (chartType) this.props.setChartType(chartType);
      if (this.state.form.name) this.props.setTitle(this.state.form.name);
    }
  }

  handleModeChange(value) {
    // We have to set the defaultEditableWidget to false if the mode has been changed
    // to 'advanced'
    const newForm = (value === 'advanced') ?
      Object.assign({}, this.state.form, { defaultEditableWidget: false })
      : this.state.form;

    this.setState({
      form: newForm,
      mode: value
    });
  }

  render() {
    return (
      <form className="c-form" onSubmit={this.onSubmit} noValidate>
        <Spinner isLoading={this.state.loading} className="-light" />

        {(this.state.step === 1 && !this.state.loading) &&
          <Step1
            id={this.state.id}
            form={this.state.form}
            partners={this.state.partners}
            datasets={this.state.datasets}
            mode={this.state.mode}
            onChange={value => this.onChange(value)}
            onModeChange={this.handleModeChange}
          />
        }

        {!this.state.loading &&
          <Navigation
            step={this.state.step}
            stepLength={this.state.stepLength}
            submitting={this.state.submitting}
            onStepChange={this.onStepChange}
          />
        }
      </form>
    );
  }
}

WidgetsForm.propTypes = {
  authorization: PropTypes.string,
  id: PropTypes.string,
  onSubmit: PropTypes.func,
  dataset: PropTypes.string, // ID of the dataset that should be pre-selected
  // Store
  widgetEditor: PropTypes.object,
  // ACTIONS
  setFilters: PropTypes.func.isRequired,
  setSize: PropTypes.func.isRequired,
  setColor: PropTypes.func.isRequired,
  setCategory: PropTypes.func.isRequired,
  setValue: PropTypes.func.isRequired,
  setOrderBy: PropTypes.func.isRequired,
  setAggregateFunction: PropTypes.func.isRequired,
  setLimit: PropTypes.func.isRequired,
  setChartType: PropTypes.func.isRequired,
  setVisualizationType: PropTypes.func.isRequired,
  setBand: PropTypes.func.isRequired,
  setLayer: PropTypes.func.isRequired,
  setTitle: PropTypes.func.isRequired
};

const mapDispatchToProps = dispatch => ({
  setFilters: filter => dispatch(setFilters(filter)),
  setColor: color => dispatch(setColor(color)),
  setSize: size => dispatch(setSize(size)),
  setCategory: category => dispatch(setCategory(category)),
  setValue: value => dispatch(setValue(value)),
  setOrderBy: value => dispatch(setOrderBy(value)),
  setAggregateFunction: value => dispatch(setAggregateFunction(value)),
  setLimit: value => dispatch(setLimit(value)),
  setChartType: value => dispatch(setChartType(value)),
  setVisualizationType: vis => dispatch(setVisualizationType(vis)),
  setBand: band => dispatch(setBand(band)),
  setTitle: title => dispatch(setTitle(title)),
  setLayer: (layerId) => {
    new LayersService()
      .fetchData({ id: layerId })
      .then(layer => dispatch(setLayer(layer)))
      // TODO: better handling of the error
      .catch(err => toastr.error('Error', err));
  }
});

const mapStateToProps = state => ({
  widgetEditor: state.widgetEditor
});

export default connect(mapStateToProps, mapDispatchToProps)(WidgetsForm);
<commit_msg>Widgets: form object assign fixed<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import isEmpty from 'lodash/isEmpty';

// Services
import WidgetsService from 'services/WidgetsService';
import DatasetsService from 'services/DatasetsService';
import LayersService from 'services/LayersService';

import { toastr } from 'react-redux-toastr';

// Redux
import { connect } from 'react-redux';

import {
  setFilters,
  setColor,
  setCategory,
  setValue,
  setSize,
  setOrderBy,
  setAggregateFunction,
  setLimit,
  setChartType,
  setBand,
  setVisualizationType,
  setLayer,
  setTitle
} from 'components/widgets/editor/redux/widgetEditor';

// Constants
import { STATE_DEFAULT, FORM_ELEMENTS } from 'components/admin/widgets/form/constants';

// Components
import Navigation from 'components/form/Navigation';
import Step1 from 'components/admin/widgets/form/steps/Step1';
import Spinner from 'components/ui/Spinner';

class WidgetsForm extends React.Component {
  constructor(props) {
    super(props);

    const formObj = props.dataset ?
      Object.assign({}, STATE_DEFAULT.form, { dataset: props.dataset }) :
      STATE_DEFAULT.form;

    this.state = Object.assign({}, STATE_DEFAULT, {
      id: props.id,
      loading: !!props.id,
      form: formObj,
      mode: 'editor'
    });

    // BINDINGS
    this.onSubmit = this.onSubmit.bind(this);
    this.onChange = this.onChange.bind(this);
    this.handleModeChange = this.handleModeChange.bind(this);
    this.onStepChange = this.onStepChange.bind(this);

    this.service = new WidgetsService({
      authorization: props.authorization
    });

    this.datasetsService = new DatasetsService({
      authorization: props.authorization
    });
  }

  componentDidMount() {
    const { id } = this.state;

    const promises = [
      this.datasetsService.fetchAllData({})
    ];

    // Add the dashboard promise if the id exists
    if (id) {
      promises.push(this.service.fetchData({ id }));
    }

    Promise.all(promises)
      .then((response) => {
        const datasets = response[0];
        const current = response[1];

        // Set advanced mode if paramsConfig doesn't exist or if it's empty
        const mode = (
          current &&
          (!current.widgetConfig.paramsConfig || isEmpty(current.widgetConfig.paramsConfig))
        ) ? 'advanced' : 'editor';

        this.setState({
          // CURRENT DASHBOARD
          form: (id) ? this.setFormFromParams(current) : this.state.form,
          loading: false,
          mode,
          // OPTIONS
          datasets: datasets.map(p => ({ label: p.name, value: p.id }))
        }, () => this.loadWidgetIntoRedux());
      })
      .catch((err) => {
        toastr.error(err);
      });
  }

  /**
   * UI EVENTS
   * - onSubmit
   * - onChange
   * - handleModeChange
  */
  onSubmit(event) {
    const { submitting, stepLength, step, form, mode } = this.state;
    const { widgetEditor } = this.props;
    const {
      limit,
      value,
      category,
      color,
      size,
      orderBy,
      aggregateFunction,
      chartType,
      filters,
      areaIntersection,
      visualizationType,
      band,
      layer,
      title
    } = widgetEditor;

    event.preventDefault();

    // Validate the form
    FORM_ELEMENTS.validate(step);

    // Set a timeout due to the setState function of react
    setTimeout(() => {
      // Validate all the inputs on the current step
      const validWidgetConfig = (mode === 'editor') ? this.validateWidgetConfig() : true;
      const valid = FORM_ELEMENTS.isValid(step) && validWidgetConfig;
      if (valid) {
        // if we are in the last step we will submit the form
        if (step === stepLength && !submitting) {
          const { id } = this.state;

          // Start the submitting
          this.setState({ submitting: true });

          // The name of the widget is the title property of the
          // widgetEditor reducer
          let formObj = Object.assign({}, form, { name: title || '' });

          if (mode === 'editor') {
            const newWidgetConfig = {
              widgetConfig: Object.assign(
                {},
                formObj.widgetConfig,
                // If the widget is different from chart, we want to add the type
                (
                  visualizationType !== 'chart'
                    ? { type: visualizationType }
                    : {}
                ),
                {
                  paramsConfig: {
                    visualizationType,
                    limit,
                    value,
                    category,
                    color,
                    size,
                    orderBy,
                    aggregateFunction,
                    chartType,
                    filters,
                    areaIntersection,
                    band: band && { name: band.name },
                    layer: layer && layer.id
                  }
                }
              )
            };

            formObj = Object.assign({}, formObj, newWidgetConfig);
          }

          const obj = {
            dataset: form.dataset,
            id: id || '',
            type: (id) ? 'PATCH' : 'POST',
            body: formObj
          };

          if (obj.body.sourceUrl === '') {
            delete obj.body.sourceUrl;
          }

          // Save data
          this.service.saveData(obj)
            .then((data) => {
              toastr.success('Success', `The widget "${data.id}" - "${data.name}" has been uploaded correctly`);

              if (this.props.onSubmit) this.props.onSubmit();
            })
            .catch((errors) => {
              this.setState({ submitting: false });

              try {
                errors.forEach(er =>
                  toastr.error('Error', er.detail)
                );
              } catch (e) {
                toastr.error('Error', 'Oops! There was an error, try again.');
              }
            });
        } else {
          this.setState({
            step: this.state.step + 1
          });
        }
      } else {
        if (!validWidgetConfig && mode === 'editor') {
          return this.errorValidationWidgetConfig();
        }

        toastr.error('Error', 'Fill all the required fields or correct the invalid values');
      }
    }, 0);
  }

  onChange(obj) {
    const form = Object.assign({}, this.state.form, obj);
    this.setState({ form });
  }

  onStepChange(step) {
    this.setState({ step });
  }

  // HELPERS
  setFormFromParams(params) {
    const newForm = {};

    Object.keys(params).forEach((f) => {
      switch (f) {
        default: {
          if ((typeof params[f] !== 'undefined' || params[f] !== null) ||
              (typeof this.state.form[f] !== 'undefined' || this.state.form[f] !== null)) {
            newForm[f] = params[f] || this.state.form[f];
          }
        }
      }
    });

    return newForm;
  }

  validateWidgetConfig() {
    const { value, category, chartType, visualizationType, layer } = this.props.widgetEditor;

    switch (visualizationType) {
      case 'chart':
        return !!chartType && !!category && !!value;
      case 'table':
        return !!chartType && !!category && !!value;
      case 'map':
        return !!layer;
      default:
        return false;
    }
  }

  errorValidationWidgetConfig() {
    const { visualizationType } = this.props.widgetEditor;

    switch (visualizationType) {
      case 'chart':
        return toastr.error('Error', 'Value, Category and Chart type are mandatory fields for a widget visualization.');
      case 'table':
        return toastr.error('Error', 'Value, Category and Chart type are mandatory fields for a widget visualization.');
      case 'map':
        return toastr.error('Error', 'Layer is mandatory field for a widget visualization.');
      default:
        return false;
    }
  }

  loadWidgetIntoRedux() {
    const { paramsConfig } = this.state.form.widgetConfig;
    if (paramsConfig) {
      const {
        visualizationType,
        band,
        value,
        category,
        color,
        size,
        aggregateFunction,
        orderBy,
        filters,
        limit,
        chartType,
        layer
      } = paramsConfig;

      // We restore the type of visualization
      // We default to "chart" to maintain the compatibility with previously created
      // widgets (at that time, only "chart" widgets could be created)
      this.props.setVisualizationType(visualizationType || 'chart');

      if (band) this.props.setBand(band);
      if (layer) this.props.setLayer(layer);
      if (aggregateFunction) this.props.setAggregateFunction(aggregateFunction);
      if (value) this.props.setValue(value);
      if (size) this.props.setSize(size);
      if (color) this.props.setColor(color);
      if (orderBy) this.props.setOrderBy(orderBy);
      if (category) this.props.setCategory(category);
      if (filters) this.props.setFilters(filters);
      if (limit) this.props.setLimit(limit);
      if (chartType) this.props.setChartType(chartType);
      if (this.state.form.name) this.props.setTitle(this.state.form.name);
    }
  }

  handleModeChange(value) {
    // We have to set the defaultEditableWidget to false if the mode has been changed
    // to 'advanced'
    const newForm = (value === 'advanced') ?
      Object.assign({}, this.state.form, { defaultEditableWidget: false })
      : this.state.form;

    this.setState({
      form: newForm,
      mode: value
    });
  }

  render() {
    return (
      <form className="c-form" onSubmit={this.onSubmit} noValidate>
        <Spinner isLoading={this.state.loading} className="-light" />

        {(this.state.step === 1 && !this.state.loading) &&
          <Step1
            id={this.state.id}
            form={this.state.form}
            partners={this.state.partners}
            datasets={this.state.datasets}
            mode={this.state.mode}
            onChange={value => this.onChange(value)}
            onModeChange={this.handleModeChange}
          />
        }

        {!this.state.loading &&
          <Navigation
            step={this.state.step}
            stepLength={this.state.stepLength}
            submitting={this.state.submitting}
            onStepChange={this.onStepChange}
          />
        }
      </form>
    );
  }
}

WidgetsForm.propTypes = {
  authorization: PropTypes.string,
  id: PropTypes.string,
  onSubmit: PropTypes.func,
  dataset: PropTypes.string, // ID of the dataset that should be pre-selected
  // Store
  widgetEditor: PropTypes.object,
  // ACTIONS
  setFilters: PropTypes.func.isRequired,
  setSize: PropTypes.func.isRequired,
  setColor: PropTypes.func.isRequired,
  setCategory: PropTypes.func.isRequired,
  setValue: PropTypes.func.isRequired,
  setOrderBy: PropTypes.func.isRequired,
  setAggregateFunction: PropTypes.func.isRequired,
  setLimit: PropTypes.func.isRequired,
  setChartType: PropTypes.func.isRequired,
  setVisualizationType: PropTypes.func.isRequired,
  setBand: PropTypes.func.isRequired,
  setLayer: PropTypes.func.isRequired,
  setTitle: PropTypes.func.isRequired
};

const mapDispatchToProps = dispatch => ({
  setFilters: filter => dispatch(setFilters(filter)),
  setColor: color => dispatch(setColor(color)),
  setSize: size => dispatch(setSize(size)),
  setCategory: category => dispatch(setCategory(category)),
  setValue: value => dispatch(setValue(value)),
  setOrderBy: value => dispatch(setOrderBy(value)),
  setAggregateFunction: value => dispatch(setAggregateFunction(value)),
  setLimit: value => dispatch(setLimit(value)),
  setChartType: value => dispatch(setChartType(value)),
  setVisualizationType: vis => dispatch(setVisualizationType(vis)),
  setBand: band => dispatch(setBand(band)),
  setTitle: title => dispatch(setTitle(title)),
  setLayer: (layerId) => {
    new LayersService()
      .fetchData({ id: layerId })
      .then(layer => dispatch(setLayer(layer)))
      // TODO: better handling of the error
      .catch(err => toastr.error('Error', err));
  }
});

const mapStateToProps = state => ({
  widgetEditor: state.widgetEditor
});

export default connect(mapStateToProps, mapDispatchToProps)(WidgetsForm);
<|endoftext|>


<commit_before>import React from 'react';
import { ThemeProvider } from 'styled-components';
import EVENTS from 'api/constants/events';
import LatestRequestNotifications from 'ui/components/LatestRequestNotifications';
import MimicControls from 'ui/components/BottomBar/MimicControls';
import QuickEdit from 'ui/components/QuickEdit/QuickEdit';
import Mocks from 'ui/components/Mocks/Mocks';
import RequestLog from 'ui/components/RequestLogs/RequestLog';
import Settings from 'ui/components/Settings/Settings';
import styled from 'styled-components';
import API from 'api';
import UIState from 'ui/states/UIState';
import SettingsState from 'ui/states/SettingsState';
import QuickEditState from 'ui/states/QuickEditState';
import { connectToState } from 'ui/states/connector';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';

const Container = styled.div`
  display: flex;
  position: fixed;
  bottom: 0;
  width: 100%;
  z-index: 2147483646;
  background-color: white;
  font-size: 13px;
  line-height: 20px;
  font-family: -apple-system, BlinkMacSystemFont, Arial, sans-serif;
`;

const theme = {
  blue: '#2d7bd1',
  selectionBlue: '#cbddf5',
  mockedBlue: '#4b82d5',
  red: '#ba3a00',
  lightGray: '#f0f0f0',
  darkGray: '#dedede',
  lightBorder: '1px solid #f0f0f0',
  darkBorder: '1px solid #dedede'
};

class Mimic extends React.Component {

  selectRequest = (requestId) => {
    QuickEditState.selectRequest(requestId);
    this.setLatestRequest({ requestId });
  };

  setLatestRequest = ({ requestId }) => {
    UIState.updateLatestRequest(API.getCapturedRequest(requestId));
  };

  toggleOnHotKey = (event) => {
    if (event.altKey && event.code === 'KeyM') {
      UIState.setViewMode(UIState.viewMode === 'closed' ? 'requests' : 'closed');
    }
  };

  componentDidMount() {
    window.addEventListener('keypress', this.toggleOnHotKey);
    API.on(EVENTS.REQUEST_CAPTURED, this.setLatestRequest);
    API.on(EVENTS.RESPONSE_RECEIVED, this.setLatestRequest);
  }

  componentWillUnmount() {
    window.removeEventListener('keypress', this.toggleOnHotKey);
    API.off(EVENTS.REQUEST_CAPTURED, this.setLatestRequest);
    API.off(EVENTS.RESPONSE_RECEIVED, this.setLatestRequest);
  }

  render() {
    return (
      <ThemeProvider theme={ theme }>
        <Container>
          {
            (UIState.viewMode !== 'closed' ||
            SettingsState.alwaysShowMimicButtons) &&
            <MimicControls/>
          }

          { UIState.viewMode === 'closed' && <LatestRequestNotifications/> }

          {
            UIState.viewMode === 'quickEdit' &&
            <QuickEdit
              selectedRequestId={ QuickEditState.selectedRequestId }
              onSelectRequest={ this.selectRequest }/>
          }

          { UIState.viewMode === 'mocks' && <Mocks/> }
          { UIState.viewMode === 'requests' && <RequestLog/> }
          { UIState.viewMode === 'settings' && <Settings/> }
        </Container>
      </ThemeProvider>
    );
  }
}

export default connectToState(UIState, DragDropContext(HTML5Backend)(Mimic));
<commit_msg>fix font antialiasing on mac<commit_after>import React from 'react';
import { ThemeProvider } from 'styled-components';
import EVENTS from 'api/constants/events';
import LatestRequestNotifications from 'ui/components/LatestRequestNotifications';
import MimicControls from 'ui/components/BottomBar/MimicControls';
import QuickEdit from 'ui/components/QuickEdit/QuickEdit';
import Mocks from 'ui/components/Mocks/Mocks';
import RequestLog from 'ui/components/RequestLogs/RequestLog';
import Settings from 'ui/components/Settings/Settings';
import styled from 'styled-components';
import API from 'api';
import UIState from 'ui/states/UIState';
import SettingsState from 'ui/states/SettingsState';
import QuickEditState from 'ui/states/QuickEditState';
import { connectToState } from 'ui/states/connector';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';

const Container = styled.div`
  display: flex;
  position: fixed;
  bottom: 0;
  width: 100%;
  z-index: 2147483646;
  background-color: white;
  font-size: 13px;
  line-height: 20px;
  font-family: -apple-system, BlinkMacSystemFont, Arial, sans-serif;
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
`;

const theme = {
  blue: '#2d7bd1',
  selectionBlue: '#cbddf5',
  mockedBlue: '#4b82d5',
  red: '#ba3a00',
  lightGray: '#f0f0f0',
  darkGray: '#dedede',
  lightBorder: '1px solid #f0f0f0',
  darkBorder: '1px solid #dedede'
};

class Mimic extends React.Component {

  selectRequest = (requestId) => {
    QuickEditState.selectRequest(requestId);
    this.setLatestRequest({ requestId });
  };

  setLatestRequest = ({ requestId }) => {
    UIState.updateLatestRequest(API.getCapturedRequest(requestId));
  };

  toggleOnHotKey = (event) => {
    if (event.altKey && event.code === 'KeyM') {
      UIState.setViewMode(UIState.viewMode === 'closed' ? 'requests' : 'closed');
    }
  };

  componentDidMount() {
    window.addEventListener('keypress', this.toggleOnHotKey);
    API.on(EVENTS.REQUEST_CAPTURED, this.setLatestRequest);
    API.on(EVENTS.RESPONSE_RECEIVED, this.setLatestRequest);
  }

  componentWillUnmount() {
    window.removeEventListener('keypress', this.toggleOnHotKey);
    API.off(EVENTS.REQUEST_CAPTURED, this.setLatestRequest);
    API.off(EVENTS.RESPONSE_RECEIVED, this.setLatestRequest);
  }

  render() {
    return (
      <ThemeProvider theme={ theme }>
        <Container>
          {
            (UIState.viewMode !== 'closed' ||
            SettingsState.alwaysShowMimicButtons) &&
            <MimicControls/>
          }

          { UIState.viewMode === 'closed' && <LatestRequestNotifications/> }

          {
            UIState.viewMode === 'quickEdit' &&
            <QuickEdit
              selectedRequestId={ QuickEditState.selectedRequestId }
              onSelectRequest={ this.selectRequest }/>
          }

          { UIState.viewMode === 'mocks' && <Mocks/> }
          { UIState.viewMode === 'requests' && <RequestLog/> }
          { UIState.viewMode === 'settings' && <Settings/> }
        </Container>
      </ThemeProvider>
    );
  }
}

export default connectToState(UIState, DragDropContext(HTML5Backend)(Mimic));
<|endoftext|>


<commit_before>var Provoke = require('../util/provoke.js');

var MergeTemplateService = function() {

  //***** private methods *****//

  //***** public methods *****//

  this.create = function(template) {
    return Provoke('MergeTemplateService', 'create', template.toConfig());
  };

  this.getAll = function() {
    return Provoke('MergeTemplateService', 'getAll');
  };

  this.getOne = function(id) {
    return Provoke('MergeTemplateService', 'getByID', id);
  };

  this.update = function(template) {
    return Provoke('MergeTemplateService', 'update', template.toConfig());
  };

  this.delete = function(template) {
    return Provoke('MergeTemplateService', 'delete', template.getID());
  }
};

module.exports = MergeTemplateService;
<commit_msg>Fixed the delete method. It referenced the wrong server-side method.<commit_after>var Provoke = require('../util/provoke.js');

var MergeTemplateService = function() {

  //***** private methods *****//

  //***** public methods *****//

  this.create = function(template) {
    return Provoke('MergeTemplateService', 'create', template.toConfig());
  };

  this.getAll = function() {
    return Provoke('MergeTemplateService', 'getAll');
  };

  this.getOne = function(id) {
    return Provoke('MergeTemplateService', 'getByID', id);
  };

  this.update = function(template) {
    return Provoke('MergeTemplateService', 'update', template.toConfig());
  };

  this.delete = function(template) {
    return Provoke('MergeTemplateService', 'deleteByID', template.getID());
  }
};

module.exports = MergeTemplateService;
<|endoftext|>


<commit_before>/*
 * Plunge.js
 * v0.2.2
 */

;(function() {
  'use strict';

  var assignHandlers = function() {
    if (!document.querySelector('[data-pl-trigger]')) {
      return;
    }
    document.querySelector('body').addEventListener('click', dropdown.open, false);
  };

  var findAncestor = function(el, cls) {
    if (el.classList.contains(cls)) {
      return el;
    }
    while ((el = el.parentElement) && !el.classList.contains(cls));
    return el;
  };

  var findDropElement = function(el) {
    var element = document.querySelectorAll('[data-pl-id="' + el + '"]');
    element[0].classList.add('pl-active');
  };

  var escapeExit = function(event) {
    if (event.which == '27') {
      dropdown.closeAll();
    }
  };

  var dropdown = {
    open: function(event) {
      var root = document.documentElement;
      var clicked = findAncestor(event.target, 'dropdown');
      var dropdownEl = document.querySelector('[data-pl-id=' + event.target.dataset.plTrigger + ']');
      var pos = event.target.dataset.plPosition || 'auto';

      if (event.target.dataset.plTrigger && !event.target.classList.contains('pl-trigger-active')) {
        event.stopPropagation();
        // close all the dropdowns that might be open still
        dropdown.closeAll();
        // add the keyup event listener for the escape key
        document.querySelector('body').addEventListener('keyup', escapeExit, false);
        // add the classes to the html element
        root.classList.add('pl-active');
        // add an active class to the trigger
        event.target.classList.add('pl-trigger-active');
        // find the element that needs to plunge based on the indentifier in the data-pl-trigger attribute
        findDropElement(event.target.dataset.plTrigger);
        // set the position
        dropdown.position[pos](dropdownEl, event.target.getBoundingClientRect(), dropdownEl.getBoundingClientRect(), event);
      }
      else if (root.classList.contains('pl-active') && clicked === null) {
        dropdown.closeAll();
      }
      else {
        return;
      }
    },
    position: {
      'auto': function(dropdownEl, triggerRect, dropdownRect, event) {
        var spaceBelow = window.innerHeight - (event.target.offsetTop + triggerRect.height);

        if (spaceBelow >= dropdownRect.height) {
          dropdown.position.bottom(dropdownEl, event.target.getBoundingClientRect(), dropdownEl.getBoundingClientRect());
        }
        else {
          dropdown.position.top(dropdownEl, event.target.getBoundingClientRect(), dropdownEl.getBoundingClientRect());
        }
      },
      'top': function(dropdownEl, triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top - dropdownRect.height) + 'px';
        dropdownEl.style.left = (triggerRect.left) + 'px';
        dropdownEl.style.width = triggerRect.width + 'px';
      },
      'right': function(dropdownEl,triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top - (dropdownRect.height / 2) + (triggerRect.height / 2)) + 'px';
        dropdownEl.style.left = (triggerRect.left + triggerRect.width) + 'px';
      },
      'bottom': function(dropdownEl, triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top + triggerRect.height) + 'px';
        dropdownEl.style.left = (triggerRect.left) + 'px';
        dropdownEl.style.width = triggerRect.width + 'px';
      },
      'left': function(dropdownEl, triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top - (dropdownRect.height / 2) + (triggerRect.height / 2)) + 'px';
        dropdownEl.style.left = (triggerRect.left - dropdownRect.width) + 'px';
      }
    },
    close: function(element) {
      element.parentNode.removeChild(element);
    },

    hide: function(element) {
      element.classList.remove('pl-active');
      element.classList.remove('pl-trigger-active');
      element.blur();
    },

    closeAll: function() {
      document.querySelectorAll('.dropdown, .pl-trigger-active').forEach(dropdown.hide);

      document.documentElement.classList.remove('pl-active');
      document.querySelector('body').removeEventListener('keyup', escapeExit, false);
    }
  };

  assignHandlers();
})();
<commit_msg>added preventDefault, changed the forEach to a array forEach call so it plays nice with Firefox and Safari<commit_after>/*
 * Plunge.js
 * v0.2.4
 */

;(function() {
  'use strict';

  var assignHandlers = function() {
    if (!document.querySelector('[data-pl-trigger]')) {
      return;
    }
    document.querySelector('body').addEventListener('click', dropdown.open, false);
  };

  var findAncestor = function(el, cls) {
    if (el.classList.contains(cls)) {
      return el;
    }
    while ((el = el.parentElement) && !el.classList.contains(cls));
    return el;
  };

  var findDropElement = function(el) {
    var element = document.querySelectorAll('[data-pl-id="' + el + '"]');
    element[0].classList.add('pl-active');
  };

  var escapeExit = function(event) {
    if (event.which == '27') {
      dropdown.closeAll();
    }
  };

  var dropdown = {
    open: function(event) {
      event.preventDefault();
      var root = document.documentElement;
      var clicked = findAncestor(event.target, 'dropdown');
      var dropdownEl = document.querySelector('[data-pl-id=' + event.target.dataset.plTrigger + ']');
      var pos = event.target.dataset.plPosition || 'auto';

      if (event.target.dataset.plTrigger && !event.target.classList.contains('pl-trigger-active')) {
        event.stopPropagation();
        // close all the dropdowns that might be open still
        dropdown.closeAll();
        // add the keyup event listener for the escape key
        document.querySelector('body').addEventListener('keyup', escapeExit, false);
        // add the classes to the html element
        root.classList.add('pl-active');
        // add an active class to the trigger
        event.target.classList.add('pl-trigger-active');
        // find the element that needs to plunge based on the indentifier in the data-pl-trigger attribute
        findDropElement(event.target.dataset.plTrigger);
        // set the position
        dropdown.position[pos](dropdownEl, event.target.getBoundingClientRect(), dropdownEl.getBoundingClientRect(), event);
      }
      else if (root.classList.contains('pl-active') && clicked === null) {
        dropdown.closeAll();
      }
      else {
        return;
      }
    },
    position: {
      'auto': function(dropdownEl, triggerRect, dropdownRect, event) {
        var spaceBelow = window.innerHeight - (event.target.offsetTop + triggerRect.height);

        if (spaceBelow >= dropdownRect.height) {
          dropdown.position.bottom(dropdownEl, event.target.getBoundingClientRect(), dropdownEl.getBoundingClientRect());
        }
        else {
          dropdown.position.top(dropdownEl, event.target.getBoundingClientRect(), dropdownEl.getBoundingClientRect());
        }
      },
      'top': function(dropdownEl, triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top - dropdownRect.height) + 'px';
        dropdownEl.style.left = (triggerRect.left) + 'px';
        dropdownEl.style.width = triggerRect.width + 'px';
      },
      'right': function(dropdownEl,triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top - (dropdownRect.height / 2) + (triggerRect.height / 2)) + 'px';
        dropdownEl.style.left = (triggerRect.left + triggerRect.width) + 'px';
      },
      'bottom': function(dropdownEl, triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top + triggerRect.height) + 'px';
        dropdownEl.style.left = (triggerRect.left) + 'px';
        dropdownEl.style.width = triggerRect.width + 'px';
      },
      'left': function(dropdownEl, triggerRect, dropdownRect, event) {
        dropdownEl.style.top = (triggerRect.top - (dropdownRect.height / 2) + (triggerRect.height / 2)) + 'px';
        dropdownEl.style.left = (triggerRect.left - dropdownRect.width) + 'px';
      }
    },
    close: function(element) {
      element.parentNode.removeChild(element);
    },

    hide: function(element) {
      element.classList.remove('pl-active');
      element.classList.remove('pl-trigger-active');
      element.blur();
    },

    closeAll: function() {
      [].forEach.call(document.querySelectorAll('.dropdown, .pl-trigger-active'), dropdown.hide);

      document.documentElement.classList.remove('pl-active');
      document.querySelector('body').removeEventListener('keyup', escapeExit, false);
    }
  };

  assignHandlers();
})();
<|endoftext|>


<commit_before>'use babel';

let InteractiveConfigurationPanel = null;
let RandomTipPanel = null;

function initializeInteractiveConfiguration() {
  if (InteractiveConfigurationPanel !== null) return ;

  InteractiveConfigurationPanel = require('./interactive-configuration-panel');
}
function initializeRandomTips() {
  if (RandomTipPanel !== null) return;
  RandomTipPanel = require('./random-tip-panel');
}

export default class UserSupportHelper {
  constructor() {
    this.tips = []
  }

  destroy() {
    if (this.configurationPanel !== null) {
      this.configurationPanel.destroy();
    }
    for (const tip of tips) tip.destory()
  }

  getInteractiveConfigurationPanel() {
    initializeInteractiveConfiguration();

    if (!this.configurationPanel) {
      this.configurationPanel = new InteractiveConfigurationPanel()
    }
    return this.configurationPanel;
  }

  createRandomTipPanel(configurationPrefix, words) {
    initializeRandomTips();

    const panel = new RandomTipPanel(configurationPrefix, words);
    this.tips.push(panel)

    return panel;
  }
};
<commit_msg>Add initialize function to user-support-helper<commit_after>'use babel';

let InteractiveConfigurationPanel = null;
let RandomTipPanel = null;
let isInitialized = false

function initialize() {
  if (isInitialized) return
  atom.themes.requireStylesheet(require.resolve('../styles/user-support-helper.less'));

  isInitialied = true
}

function initializeInteractiveConfiguration() {
  initialize()

  if (InteractiveConfigurationPanel !== null) return ;
  InteractiveConfigurationPanel = require('./interactive-configuration-panel');
}
function initializeRandomTips() {
  initialize()

  if (RandomTipPanel !== null) return;
  RandomTipPanel = require('./random-tip-panel');
}

export default class UserSupportHelper {
  constructor() {
    this.tips = []
  }

  destroy() {
    if (this.configurationPanel !== null) {
      this.configurationPanel.destroy();
    }
    for (const tip of tips) tip.destory()
  }

  getInteractiveConfigurationPanel() {
    initializeInteractiveConfiguration();

    if (!this.configurationPanel) {
      this.configurationPanel = new InteractiveConfigurationPanel()
    }
    return this.configurationPanel;
  }

  createRandomTipPanel(configurationPrefix, words) {
    initializeRandomTips();

    const panel = new RandomTipPanel(configurationPrefix, words);
    this.tips.push(panel)

    return panel;
  }
};
<|endoftext|>