<commit_before>module.exports = function(app) {
  app.config([
    'formioComponentsProvider',
    function(formioComponentsProvider) {
      formioComponentsProvider.register('resource', {
        onEdit: ['$scope', function($scope) {
          $scope.resources = [];
          $scope.component.project = $scope.formio.projectId;
          $scope.formio.loadForms({params: {
            type: 'resource',
            limit: 4294967295,
            select: '_id,title'
          }}).then(function(resources) {
            $scope.resources = resources;
            if (!$scope.component.resource) {
              $scope.component.resource = resources[0]._id;
            }
          });
        }],
        icon: 'fa fa-files-o',
        views: [
          {
            name: 'Display',
            template: 'formio/components/resource/display.html'
          },
          {
            name: 'Validation',
            template: 'formio/components/resource/validate.html'
          },
          {
            name: 'API',
            template: 'formio/components/common/api.html'
          },
          {
            name: 'Layout',
            template: 'formio/components/common/layout.html'
          },
          {
            name: 'Conditional',
            template: 'formio/components/common/conditional.html'
          }
        ],
        documentation: 'http://help.form.io/userguide/#resource'
      });
    }
  ]);
  app.run([
    '$templateCache',
    function($templateCache) {
      // Create the settings markup.
      $templateCache.put('formio/components/resource/display.html',
        '<ng-form>' +
          '<form-builder-option property="label"></form-builder-option>' +
          '<form-builder-option property="hideLabel"></form-builder-option>' +
          '<form-builder-option-label-position></form-builder-option-label-position>' +
          '<form-builder-option property="placeholder"></form-builder-option>' +
          '<form-builder-option property="tooltip"></form-builder-option>' +
          '
' +
            '{{\'Resource\' | formioTranslate}}' +
            '<select class="form-control" id="resource" name="resource" ng-options="value._id as value.title for value in resources" ng-model="component.resource"></select>' +
          '
' +
          '
' +
            '{{\'Select Fields\' | formioTranslate}}' +
            '<input type="text" class="form-control" id="selectFields" name="selectFields" ng-model="component.selectFields" placeholder="Comma separated list of fields to select." value="{{ component.selectFields }}">' +
          '
' +
          '
' +
            '<label for="searchFields" form-builder-tooltip="A list of search filters based on the fields of the resource. See the Resource.js documentation for the format of these filters.">{{\'Search Fields\' | formioTranslate}}' +
            '<input type="text" class="form-control" id="searchFields" name="searchFields" ng-model="component.searchFields" ng-list placeholder="The fields to query on the server" value="{{ component.searchFields }}">' +
          '
' +
          '
' +
            '{{\'Item Template\' | formioTranslate}}' +
            '<textarea class="form-control" id="template" name="template" ng-model="component.template" rows="3">{{ component.template }}</textarea>' +
          '
' +
          '<form-builder-option property="tooltip"></form-builder-option>' +
          '<form-builder-option property="errorLabel"></form-builder-option>' +
          '<form-builder-option property="customClass"></form-builder-option>' +
          '<form-builder-option property="tabindex"></form-builder-option>' +
          '<form-builder-option property="multiple" label="Allow Multiple Resources"></form-builder-option>' +
          '<form-builder-option property="clearOnHide"></form-builder-option>' +
          '<form-builder-option property="reference"></form-builder-option>' +
          '<form-builder-option property="addResource"></form-builder-option>' +
          '<form-builder-option property="addResourceLabel" ng-if="component.addResource"></form-builder-option>' +
          '<form-builder-option property="disabled"></form-builder-option>' +
          '<form-builder-option property="persistent"></form-builder-option>' +
          '<form-builder-option property="encrypted" class="form-builder-premium"></form-builder-option>' +
          '<form-builder-option property="hidden"></form-builder-option>' +
          '<form-builder-option property="autofocus" type="checkbox" label="Initial Focus" tooltip="Make this field the initially focused element on this form."></form-builder-option>' +
          '<form-builder-option property="dataGridLabel"></form-builder-option>' +
          '<form-builder-option property="tableView"></form-builder-option>' +
        '</ng-form>'
      );

      // Create the API markup.
      $templateCache.put('formio/components/resource/validate.html',
        '<ng-form>' +
          '<form-builder-option property="validate.required"></form-builder-option>' +
          '<form-builder-option property="validate.customMessage"></form-builder-option>' +
        '</ng-form>'
      );
    }
  ]);
};
<commit_msg>Removed double toop tip<commit_after>module.exports = function(app) {
  app.config([
    'formioComponentsProvider',
    function(formioComponentsProvider) {
      formioComponentsProvider.register('resource', {
        onEdit: ['$scope', function($scope) {
          $scope.resources = [];
          $scope.component.project = $scope.formio.projectId;
          $scope.formio.loadForms({params: {
            type: 'resource',
            limit: 4294967295,
            select: '_id,title'
          }}).then(function(resources) {
            $scope.resources = resources;
            if (!$scope.component.resource) {
              $scope.component.resource = resources[0]._id;
            }
          });
        }],
        icon: 'fa fa-files-o',
        views: [
          {
            name: 'Display',
            template: 'formio/components/resource/display.html'
          },
          {
            name: 'Validation',
            template: 'formio/components/resource/validate.html'
          },
          {
            name: 'API',
            template: 'formio/components/common/api.html'
          },
          {
            name: 'Layout',
            template: 'formio/components/common/layout.html'
          },
          {
            name: 'Conditional',
            template: 'formio/components/common/conditional.html'
          }
        ],
        documentation: 'http://help.form.io/userguide/#resource'
      });
    }
  ]);
  app.run([
    '$templateCache',
    function($templateCache) {
      // Create the settings markup.
      $templateCache.put('formio/components/resource/display.html',
        '<ng-form>' +
          '<form-builder-option property="label"></form-builder-option>' +
          '<form-builder-option property="hideLabel"></form-builder-option>' +
          '<form-builder-option-label-position></form-builder-option-label-position>' +
          '<form-builder-option property="placeholder"></form-builder-option>' +
          '<form-builder-option property="tooltip"></form-builder-option>' +
          '
' +
            '{{\'Resource\' | formioTranslate}}' +
            '<select class="form-control" id="resource" name="resource" ng-options="value._id as value.title for value in resources" ng-model="component.resource"></select>' +
          '
' +
          '
' +
            '{{\'Select Fields\' | formioTranslate}}' +
            '<input type="text" class="form-control" id="selectFields" name="selectFields" ng-model="component.selectFields" placeholder="Comma separated list of fields to select." value="{{ component.selectFields }}">' +
          '
' +
          '
' +
            '<label for="searchFields" form-builder-tooltip="A list of search filters based on the fields of the resource. See the Resource.js documentation for the format of these filters.">{{\'Search Fields\' | formioTranslate}}' +
            '<input type="text" class="form-control" id="searchFields" name="searchFields" ng-model="component.searchFields" ng-list placeholder="The fields to query on the server" value="{{ component.searchFields }}">' +
          '
' +
          '
' +
            '{{\'Item Template\' | formioTranslate}}' +
            '<textarea class="form-control" id="template" name="template" ng-model="component.template" rows="3">{{ component.template }}</textarea>' +
          '
' +
          '<form-builder-option property="errorLabel"></form-builder-option>' +
          '<form-builder-option property="customClass"></form-builder-option>' +
          '<form-builder-option property="tabindex"></form-builder-option>' +
          '<form-builder-option property="multiple" label="Allow Multiple Resources"></form-builder-option>' +
          '<form-builder-option property="clearOnHide"></form-builder-option>' +
          '<form-builder-option property="reference"></form-builder-option>' +
          '<form-builder-option property="addResource"></form-builder-option>' +
          '<form-builder-option property="addResourceLabel" ng-if="component.addResource"></form-builder-option>' +
          '<form-builder-option property="disabled"></form-builder-option>' +
          '<form-builder-option property="persistent"></form-builder-option>' +
          '<form-builder-option property="encrypted" class="form-builder-premium"></form-builder-option>' +
          '<form-builder-option property="hidden"></form-builder-option>' +
          '<form-builder-option property="autofocus" type="checkbox" label="Initial Focus" tooltip="Make this field the initially focused element on this form."></form-builder-option>' +
          '<form-builder-option property="dataGridLabel"></form-builder-option>' +
          '<form-builder-option property="tableView"></form-builder-option>' +
        '</ng-form>'
      );

      // Create the API markup.
      $templateCache.put('formio/components/resource/validate.html',
        '<ng-form>' +
          '<form-builder-option property="validate.required"></form-builder-option>' +
          '<form-builder-option property="validate.customMessage"></form-builder-option>' +
        '</ng-form>'
      );
    }
  ]);
};
<|endoftext|>


<commit_before>'use strict';

app.directive('accessGrantForm', [
  '$timeout', 'constantService', 'modelService', 'messageService', 'displayService',
  function ($timeout, constants, models, messages, display) {
    var link = function ($scope) {
      var volume = $scope.volume;
      var access = $scope.access;
      var form = $scope.accessGrantForm;

      form.data = access;
      form.data.extend = (access.children == access.individual);
      if (!access.individual) {
        form.$setDirty();
        $timeout(function () {
          // hack! calling $validate() doesn't work!
          form['access-'+access.party.id+'-individual'].$setValidity('required', false);
        });
      }

      form.canGrantAccess = function (p) {
        return p == constants.permission.READ ||
          p == constants.permission.EDIT ||
          p == constants.permission.ADMIN ||
          models.Login.checkAuthorization(constants.permission.ADMIN);
      };

      form.save = function () {
        messages.clear(form);
        form.data.children = form.data.extend ? form.data.individual : 0;

        form.$setSubmitted();
        volume.accessSave(access.party.id, form.data).then(function () {
          form.$setUnsubmitted();
          messages.add({
            body: constants.message('access.grant.save.success'),
            type: 'green',
            owner: form
          });

          delete access.new;
          form.$setPristine();
        }, function (res) {
          form.$setUnsubmitted();
          messages.addError({
            body: constants.message('access.grant.save.error'),
            report: res,
            owner: form
          });

          display.scrollTo(form.$element);
        });
      };

      form.remove = function () {
        messages.clear(form);
        if (access.new) {
          form.removeSuccessFn(access);
          return;
        }
        form.$setSubmitted();
        volume.accessRemove(access.party.id).then(function () {
          messages.add({
            body: constants.message('access.grant.remove.success'),
            type: 'green',
            owner: form
          });

          form.$setPristine();
          form.removeSuccessFn(access);
        }, function (res) {
          form.$setUnsubmitted();
          messages.addError({
            body: constants.message('access.grant.remove.error'),
            report: res,
            owner: form
          });
          display.scrollTo(form.$element);
        });
      };

      //

      $scope.$emit('accessGrantForm-init', form, $scope);
    };

    //

    return {
      restrict: 'E',
      templateUrl: 'volume/accessGrant.html',
      link: link
    };
  }
]);
<commit_msg>Only re-sort access on save<commit_after>'use strict';

app.directive('accessGrantForm', [
  '$timeout', 'constantService', 'modelService', 'messageService', 'displayService',
  function ($timeout, constants, models, messages, display) {
    var link = function ($scope) {
      var volume = $scope.volume;
      var access = $scope.access;
      var form = $scope.accessGrantForm;

      form.data = {
        individual: access.individual,
        extend: access.children == access.individual,
        sort: access.sort
      };
      if (!access.individual) {
        form.$setDirty();
        $timeout(function () {
          // hack! calling $validate() doesn't work!
          form['access-'+access.party.id+'-individual'].$setValidity('required', false);
        });
      }

      form.canGrantAccess = function (p) {
        return p == constants.permission.READ ||
          p == constants.permission.EDIT ||
          p == constants.permission.ADMIN ||
          models.Login.checkAuthorization(constants.permission.ADMIN);
      };

      form.save = function () {
        messages.clear(form);
        form.data.children = form.data.extend ? form.data.individual : 0;

        form.$setSubmitted();
        volume.accessSave(access.party.id, form.data).then(function () {
          form.$setUnsubmitted();
          messages.add({
            body: constants.message('access.grant.save.success'),
            type: 'green',
            owner: form
          });

          delete access.new;
          access.individual = form.data.individual;
          access.sort = form.data.sort;
          form.$setPristine();
        }, function (res) {
          form.$setUnsubmitted();
          messages.addError({
            body: constants.message('access.grant.save.error'),
            report: res,
            owner: form
          });

          display.scrollTo(form.$element);
        });
      };

      form.remove = function () {
        messages.clear(form);
        if (access.new) {
          form.removeSuccessFn(access);
          return;
        }
        form.$setSubmitted();
        volume.accessRemove(access.party.id).then(function () {
          form.$setUnsubmitted();
          messages.add({
            body: constants.message('access.grant.remove.success'),
            type: 'green',
            owner: form
          });

          form.$setPristine();
          form.removeSuccessFn(access);
        }, function (res) {
          form.$setUnsubmitted();
          messages.addError({
            body: constants.message('access.grant.remove.error'),
            report: res,
            owner: form
          });
          display.scrollTo(form.$element);
        });
      };

      //

      $scope.$emit('accessGrantForm-init', form, $scope);
    };

    //

    return {
      restrict: 'E',
      templateUrl: 'volume/accessGrant.html',
      link: link
    };
  }
]);
<|endoftext|>


<commit_before>/*
 Copyright [2016] [Relevance Lab]
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

var logger = require('_pr/logger')(module)
var async = require('async')
var appConfig = require('_pr/config')
var Cryptography = require('_pr/lib/utils/cryptography')
var S3 = require('_pr/lib/s3.js')
var AWSProvider = require('_pr/model/classes/masters/cloudprovider/awsCloudProvider.js')
var MasterUtils = require('_pr/lib/utils/masterUtil.js')
var resourceService = require('_pr/services/resourceService')
var CatalystCronJob = require('_pr/cronjobs/CatalystCronJob')
var dateUtil = require('_pr/lib/utils/dateUtil')
var AdmZip = require('adm-zip')

var AWSResourceCostsAggregation = Object.create(CatalystCronJob)
AWSResourceCostsAggregation.interval = '0 * * * *'
AWSResourceCostsAggregation.execute = aggregateAWSResourceCosts

var date = new Date()
var currentMonth = date.getMonth() + 1
if(currentMonth < 10) {
    currentMonth = '0' + currentMonth
}
var currentYear = date.getFullYear()
// current time - cron interval
AWSResourceCostsAggregation.previousCronRunTime = dateUtil.getDateInUTC(date.setHours(date.getHours() - 1))
AWSResourceCostsAggregation.currentCronRunTime = dateUtil.getDateInUTC(date)
AWSResourceCostsAggregation.csvFileName = appConfig.aws.s3AccountNumber
    + appConfig.aws.s3CSVFileName + currentYear + '-' + currentMonth + '.csv'
AWSResourceCostsAggregation.fullKey = AWSResourceCostsAggregation.csvFileName + '.zip'

AWSResourceCostsAggregation.aggregateAWSResourceCostsForProvider = aggregateAWSResourceCostsForProvider
AWSResourceCostsAggregation.downloadLatestBill = downloadLatestBill
AWSResourceCostsAggregation.updateResourceCosts = updateResourceCosts
AWSResourceCostsAggregation.aggregateEntityCosts = aggregateEntityCosts

AWSResourceCostsAggregation.execute()

module.exports = AWSResourceCostsAggregation

function aggregateAWSResourceCosts() {
    async.waterfall([
        function(next) {
            MasterUtils.getAllActiveOrg(next)
        },
        function(orgs, next) {
            // Gets providers for all orgs in the list
            AWSProvider.getAWSProvidersForOrg(orgs, function(err, providers) {
                if(err) {
                    next(err)
                } else {
                    next(null, orgs, providers)
                }
            })
        },
        /*function(orgs, providers, next) {
            async.forEach(providers, AWSResourceCostsAggregation.aggregateAWSResourceCostsForProvider,
                function(err) {
                    if(err) {
                        next(err)
                    } else {
                        next(null, orgs, providers)
                    }
                })
        },*/
        function(orgs, providers, next) {
            // aggregate  cost across catalyst entities
            async.forEach(orgs, AWSResourceCostsAggregation.aggregateEntityCosts,
                function(err, results) {
                    if(err) {
                        next(err)
                    } else {
                        next()
                    }
                })
        }
    ], function(err) {
        if (err) {
            logger.error(err)
        } else {
            logger.info('Resource costs aggregation for all organizations complete')
        }
    })
}

function aggregateAWSResourceCostsForProvider(provider, callback) {
    async.waterfall([
        function(next) {
            AWSResourceCostsAggregation.downloadLatestBill(provider, next)
        },
        function(downloadedCSVPath, next) {
            AWSResourceCostsAggregation.updateResourceCosts(provider, downloadedCSVPath, next)
        }
    ], function(err) {
        if (err) {
            callback(err)
        } else {
            logger.info('Resource cost aggregation complete for provider ' + provider._id)
            callback()
        }
    })
}

function downloadLatestBill(provider, callback) {
    var cryptoConfig = appConfig.cryptoSettings
    var cryptography = new Cryptography(cryptoConfig.algorithm, cryptoConfig.password)
    var decryptedAccessKey = cryptography.decryptText(provider.accessKey,
        cryptoConfig.decryptionEncoding, cryptoConfig.encryptionEncoding)
    var decryptedSecretKey = cryptography.decryptText(provider.secretKey,
        cryptoConfig.decryptionEncoding, cryptoConfig.encryptionEncoding)
    var s3Config = {
        access_key: decryptedAccessKey,
        secret_key: decryptedSecretKey,
        region: 'us-east-1'
    }
    var s3 = new S3(s3Config);
    var params = {
        Bucket: provider.s3BucketName,
        Key: AWSResourceCostsAggregation.fullKey
    }

    async.waterfall([
        function(next) {
            s3.getObject(params, 'time', next)
        },
        function(lastBillUpdateTime, next) {
            //@TODO Last update time to be updated from db
            console.log('Previous run' + AWSResourceCostsAggregation.previousCronRunTime)
            if(AWSResourceCostsAggregation.previousCronRunTime <  lastBillUpdateTime) {
                s3.getObject(params, 'file', next)
            } else {
                next(false)
            }
        },
        function(billDownloaded, next) {
            if(billDownloaded) {
                var downloadedZipPath = appConfig.aws.s3BucketDownloadFileLocation
                    + appConfig.aws.s3BucketFileName
                var zip = new AdmZip(downloadedZipPath)
                zip.extractAllTo(appConfig.aws.s3BucketDownloadFileLocation, true)

                var downloadedCSVPath = appConfig.aws.s3BucketDownloadFileLocation
                    + AWSResourceCostsAggregation.csvFileName
                next(null, downloadedCSVPath)
            } else {
                next(null, null)
            }
        }
    ],
    function(err, downloadedCSVPath) {
        if(err) {
            logger.error(err)
            callback(new Error('Error in cost aggregation for provider ' + provider._id))
        } else {
            callback(null, downloadedCSVPath)
        }
    })
}

function updateResourceCosts(provider, downloadedCSVPath, callback) {
    async.waterfall([
        function(next) {
            resourceService.getAllResourcesForProvider(provider, next)
        },
        function(resources, next) {
            resourceService.updateAWSResourceCostsFromCSV(provider, resources, downloadedCSVPath,
                AWSResourceCostsAggregation.currentCronRunTime, next)
        }
    ],
    function(err) {
        if(err) {
            callback(err)
        } else {
            callback()
        }
    })
}

function aggregateEntityCosts(org, callback) {
    var catalystEntityHierarchy = appConfig.catalystEntityHierarchy

    async.forEach(Object.keys(catalystEntityHierarchy), function(entity, next) {
            var parentEntity = {
                name: entity
            }
            var childEntity = {}
            switch(entity) {
                case 'organization':
                    resourceService.aggregateEntityCosts(entity, org.rowid, {'organizationId': org.rowid},
                        AWSResourceCostsAggregation.currentCronRunTime, 'month', next)
                    break
                case 'provider':
                    // get all providers for the organization
                    // aggregate for all children for all periods
                    break
                case 'businessGroup':
                    // get all business groups for the organization
                    // aggregate for all children for all periods
                    break
            }
        },
        function(err) {
            if(err) {
                logger.error('Costs aggregation for organizaion ' + org.rowid + ' failed')
                logger.error(err)
            } else {
                logger.info('Costs aggregation complete for organizaion ' + org.rowid)
            }
            callback()
        }
    )
}<commit_msg>cost aggregation cron run on startup commented<commit_after>/*
 Copyright [2016] [Relevance Lab]
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

var logger = require('_pr/logger')(module)
var async = require('async')
var appConfig = require('_pr/config')
var Cryptography = require('_pr/lib/utils/cryptography')
var S3 = require('_pr/lib/s3.js')
var AWSProvider = require('_pr/model/classes/masters/cloudprovider/awsCloudProvider.js')
var MasterUtils = require('_pr/lib/utils/masterUtil.js')
var resourceService = require('_pr/services/resourceService')
var CatalystCronJob = require('_pr/cronjobs/CatalystCronJob')
var dateUtil = require('_pr/lib/utils/dateUtil')
var AdmZip = require('adm-zip')

var AWSResourceCostsAggregation = Object.create(CatalystCronJob)
AWSResourceCostsAggregation.interval = '0 * * * *'
AWSResourceCostsAggregation.execute = aggregateAWSResourceCosts

var date = new Date()
var currentMonth = date.getMonth() + 1
if(currentMonth < 10) {
    currentMonth = '0' + currentMonth
}
var currentYear = date.getFullYear()
// current time - cron interval
AWSResourceCostsAggregation.previousCronRunTime = dateUtil.getDateInUTC(date.setHours(date.getHours() - 1))
AWSResourceCostsAggregation.currentCronRunTime = dateUtil.getDateInUTC(date)
AWSResourceCostsAggregation.csvFileName = appConfig.aws.s3AccountNumber
    + appConfig.aws.s3CSVFileName + currentYear + '-' + currentMonth + '.csv'
AWSResourceCostsAggregation.fullKey = AWSResourceCostsAggregation.csvFileName + '.zip'

AWSResourceCostsAggregation.aggregateAWSResourceCostsForProvider = aggregateAWSResourceCostsForProvider
AWSResourceCostsAggregation.downloadLatestBill = downloadLatestBill
AWSResourceCostsAggregation.updateResourceCosts = updateResourceCosts
AWSResourceCostsAggregation.aggregateEntityCosts = aggregateEntityCosts

// AWSResourceCostsAggregation.execute()

module.exports = AWSResourceCostsAggregation

function aggregateAWSResourceCosts() {
    async.waterfall([
        function(next) {
            MasterUtils.getAllActiveOrg(next)
        },
        function(orgs, next) {
            // Gets providers for all orgs in the list
            AWSProvider.getAWSProvidersForOrg(orgs, function(err, providers) {
                if(err) {
                    next(err)
                } else {
                    next(null, orgs, providers)
                }
            })
        },
        /*function(orgs, providers, next) {
            async.forEach(providers, AWSResourceCostsAggregation.aggregateAWSResourceCostsForProvider,
                function(err) {
                    if(err) {
                        next(err)
                    } else {
                        next(null, orgs, providers)
                    }
                })
        },*/
        function(orgs, providers, next) {
            // aggregate  cost across catalyst entities
            async.forEach(orgs, AWSResourceCostsAggregation.aggregateEntityCosts,
                function(err, results) {
                    if(err) {
                        next(err)
                    } else {
                        next()
                    }
                })
        }
    ], function(err) {
        if (err) {
            logger.error(err)
        } else {
            logger.info('Resource costs aggregation for all organizations complete')
        }
    })
}

function aggregateAWSResourceCostsForProvider(provider, callback) {
    async.waterfall([
        function(next) {
            AWSResourceCostsAggregation.downloadLatestBill(provider, next)
        },
        function(downloadedCSVPath, next) {
            AWSResourceCostsAggregation.updateResourceCosts(provider, downloadedCSVPath, next)
        }
    ], function(err) {
        if (err) {
            callback(err)
        } else {
            logger.info('Resource cost aggregation complete for provider ' + provider._id)
            callback()
        }
    })
}

function downloadLatestBill(provider, callback) {
    var cryptoConfig = appConfig.cryptoSettings
    var cryptography = new Cryptography(cryptoConfig.algorithm, cryptoConfig.password)
    var decryptedAccessKey = cryptography.decryptText(provider.accessKey,
        cryptoConfig.decryptionEncoding, cryptoConfig.encryptionEncoding)
    var decryptedSecretKey = cryptography.decryptText(provider.secretKey,
        cryptoConfig.decryptionEncoding, cryptoConfig.encryptionEncoding)
    var s3Config = {
        access_key: decryptedAccessKey,
        secret_key: decryptedSecretKey,
        region: 'us-east-1'
    }
    var s3 = new S3(s3Config);
    var params = {
        Bucket: provider.s3BucketName,
        Key: AWSResourceCostsAggregation.fullKey
    }

    async.waterfall([
        function(next) {
            s3.getObject(params, 'time', next)
        },
        function(lastBillUpdateTime, next) {
            //@TODO Last update time to be updated from db
            console.log('Previous run' + AWSResourceCostsAggregation.previousCronRunTime)
            if(AWSResourceCostsAggregation.previousCronRunTime <  lastBillUpdateTime) {
                s3.getObject(params, 'file', next)
            } else {
                next(false)
            }
        },
        function(billDownloaded, next) {
            if(billDownloaded) {
                var downloadedZipPath = appConfig.aws.s3BucketDownloadFileLocation
                    + appConfig.aws.s3BucketFileName
                var zip = new AdmZip(downloadedZipPath)
                zip.extractAllTo(appConfig.aws.s3BucketDownloadFileLocation, true)

                var downloadedCSVPath = appConfig.aws.s3BucketDownloadFileLocation
                    + AWSResourceCostsAggregation.csvFileName
                next(null, downloadedCSVPath)
            } else {
                next(null, null)
            }
        }
    ],
    function(err, downloadedCSVPath) {
        if(err) {
            logger.error(err)
            callback(new Error('Error in cost aggregation for provider ' + provider._id))
        } else {
            callback(null, downloadedCSVPath)
        }
    })
}

function updateResourceCosts(provider, downloadedCSVPath, callback) {
    async.waterfall([
        function(next) {
            resourceService.getAllResourcesForProvider(provider, next)
        },
        function(resources, next) {
            resourceService.updateAWSResourceCostsFromCSV(provider, resources, downloadedCSVPath,
                AWSResourceCostsAggregation.currentCronRunTime, next)
        }
    ],
    function(err) {
        if(err) {
            callback(err)
        } else {
            callback()
        }
    })
}

function aggregateEntityCosts(org, callback) {
    var catalystEntityHierarchy = appConfig.catalystEntityHierarchy

    async.forEach(Object.keys(catalystEntityHierarchy), function(entity, next) {
            var parentEntity = {
                name: entity
            }
            var childEntity = {}
            switch(entity) {
                case 'organization':
                    resourceService.aggregateEntityCosts(entity, org.rowid, {'organizationId': org.rowid},
                        AWSResourceCostsAggregation.currentCronRunTime, 'month', next)
                    break
                case 'provider':
                    // get all providers for the organization
                    // aggregate for all children for all periods
                    break
                case 'businessGroup':
                    // get all business groups for the organization
                    // aggregate for all children for all periods
                    break
            }
        },
        function(err) {
            if(err) {
                logger.error('Costs aggregation for organizaion ' + org.rowid + ' failed')
                logger.error(err)
            } else {
                logger.info('Costs aggregation complete for organizaion ' + org.rowid)
            }
            callback()
        }
    )
}<|endoftext|>


<commit_before>import profiler from '../util/profiler'

const MANDATORY_OBJECTIVES = [
  ['UnpopularGoalsNotConsidered'],
]

const WEIGHTED_OBJECTIVES = [
  ['TeamSizesMatchRecommendation', 100],
  ['PlayersGotTheirVote', 10],
  ['PlayersGetTeammatesTheyGaveGoodFeedback', 30],
]

function load(objective) {
  const module = require(`./${objective}Appraiser`)
  return module.default || module
}

export default class ObjectiveAppraiser {
  constructor(pool) {
    this.pool = pool
    this.objectives = {
      mandatory: MANDATORY_OBJECTIVES,
      weighted: WEIGHTED_OBJECTIVES,
      ...pool.objectives
    }
    const allObjectives = [...this.objectives.weighted, ...this.objectives.mandatory]
    this.appraisers = allObjectives.reduce((result, [objectiveName]) => {
      const ObjectiveClass = load(objectiveName)
      result.set(objectiveName, new ObjectiveClass(pool))
      return result
    }, new Map())
  }

  score(teamFormationPlan, {teamsAreIncomplete} = {}) {
    profiler.start('ObjectiveAppraiser.score')

    if (this.objectives.mandatory.length > 0) {
      const mandatoryObjectivesScore = this._getScore(this.objectives.mandatory, teamFormationPlan, {teamsAreIncomplete})
      if (mandatoryObjectivesScore !== 1) {
        return 0
      }
    }

    if (this.objectives.weighted.length === 0) {
      return 1
    }

    const score = this._getScore(this.objectives.weighted, teamFormationPlan, {teamsAreIncomplete})

    profiler.pause('ObjectiveAppraiser.score')
    return score
  }

  objectiveScores(teamFormationPlan, {teamsAreIncomplete} = {}) {
    const result = []
    this.objectives.mandatory.forEach(objective => {
      result.push({
        mandatory: true,
        objective,
        score: this._getScore([objective], teamFormationPlan, {teamsAreIncomplete})
      })
    })

    this.objectives.weighted.forEach(objective => {
      result.push({
        objective,
        score: this._getScore([objective], teamFormationPlan, {teamsAreIncomplete})
      })
    })

    return result
  }

  _getScore(objectives, teamFormationPlan, {teamsAreIncomplete} = {}) {
    const self = this
    const scores = objectives.map(([objectiveName]) => {
      try {
        profiler.start(objectiveName)
        const score = self.appraisers.get(objectiveName).score(teamFormationPlan, {teamsAreIncomplete})
        profiler.pause(objectiveName)
        return score
      } catch (err) {
        if (err.code && err.code === 'MODULE_NOT_FOUND') {
          throw new Error(`Could not load project formation algorithm objective [${objectiveName}]: ${err}`)
        }
        throw err
      }
    })
    const weights = objectives.map(([_, weight]) => weight || 1)
    const rawScore = this._getWeightedSum(scores, weights)
    const maxPossibleRawScore = this._getMaxPossibleRawScore(weights)

    return rawScore / maxPossibleRawScore
  }

  _getMaxPossibleRawScore(weights) {
    return this._getWeightedSum(Array.from({length: weights.length}, () => 1), weights)
  }

  _getWeightedSum(scores, weights) {
    return scores.reduce((sum, next, i) => {
      const weight = weights[i]
      return sum + (next * weight)
    }, 0)
  }
}
<commit_msg>swap the priority of team size and getting your vote<commit_after>import profiler from '../util/profiler'

const MANDATORY_OBJECTIVES = [
  ['UnpopularGoalsNotConsidered'],
]

const WEIGHTED_OBJECTIVES = [
  ['TeamSizesMatchRecommendation', 10],
  ['PlayersGotTheirVote', 100],
  ['PlayersGetTeammatesTheyGaveGoodFeedback', 30],
]

function load(objective) {
  const module = require(`./${objective}Appraiser`)
  return module.default || module
}

export default class ObjectiveAppraiser {
  constructor(pool) {
    this.pool = pool
    this.objectives = {
      mandatory: MANDATORY_OBJECTIVES,
      weighted: WEIGHTED_OBJECTIVES,
      ...pool.objectives
    }
    const allObjectives = [...this.objectives.weighted, ...this.objectives.mandatory]
    this.appraisers = allObjectives.reduce((result, [objectiveName]) => {
      const ObjectiveClass = load(objectiveName)
      result.set(objectiveName, new ObjectiveClass(pool))
      return result
    }, new Map())
  }

  score(teamFormationPlan, {teamsAreIncomplete} = {}) {
    profiler.start('ObjectiveAppraiser.score')

    if (this.objectives.mandatory.length > 0) {
      const mandatoryObjectivesScore = this._getScore(this.objectives.mandatory, teamFormationPlan, {teamsAreIncomplete})
      if (mandatoryObjectivesScore !== 1) {
        return 0
      }
    }

    if (this.objectives.weighted.length === 0) {
      return 1
    }

    const score = this._getScore(this.objectives.weighted, teamFormationPlan, {teamsAreIncomplete})

    profiler.pause('ObjectiveAppraiser.score')
    return score
  }

  objectiveScores(teamFormationPlan, {teamsAreIncomplete} = {}) {
    const result = []
    this.objectives.mandatory.forEach(objective => {
      result.push({
        mandatory: true,
        objective,
        score: this._getScore([objective], teamFormationPlan, {teamsAreIncomplete})
      })
    })

    this.objectives.weighted.forEach(objective => {
      result.push({
        objective,
        score: this._getScore([objective], teamFormationPlan, {teamsAreIncomplete})
      })
    })

    return result
  }

  _getScore(objectives, teamFormationPlan, {teamsAreIncomplete} = {}) {
    const self = this
    const scores = objectives.map(([objectiveName]) => {
      try {
        profiler.start(objectiveName)
        const score = self.appraisers.get(objectiveName).score(teamFormationPlan, {teamsAreIncomplete})
        profiler.pause(objectiveName)
        return score
      } catch (err) {
        if (err.code && err.code === 'MODULE_NOT_FOUND') {
          throw new Error(`Could not load project formation algorithm objective [${objectiveName}]: ${err}`)
        }
        throw err
      }
    })
    const weights = objectives.map(([_, weight]) => weight || 1)
    const rawScore = this._getWeightedSum(scores, weights)
    const maxPossibleRawScore = this._getMaxPossibleRawScore(weights)

    return rawScore / maxPossibleRawScore
  }

  _getMaxPossibleRawScore(weights) {
    return this._getWeightedSum(Array.from({length: weights.length}, () => 1), weights)
  }

  _getWeightedSum(scores, weights) {
    return scores.reduce((sum, next, i) => {
      const weight = weights[i]
      return sum + (next * weight)
    }, 0)
  }
}
<|endoftext|>


<commit_before>/**
 * Openfiles plugin for Cloud9 IDE
 *
 * @copyright 2011, Ajax.org B.V.
 * @license GPLv3 <http://www.gnu.org/licenses/gpl.txt>
 */

 define(function(require, exports, module) {
    main.consumes = [
        "plugin", "tabs", "menus", "commands", "settings", "tree", "fs", "ui"
    ];
    main.provides = ["openfiles"];
    return main;

    function main(options, imports, register) {
        var Plugin   = imports.plugin;
        var tabs     = imports.tabs;
        var menus    = imports.menus;
        var commands = imports.commands;
        var settings = imports.settings;
        var tree     = imports.tree;
        var fs       = imports.fs;
        var ui       = imports.ui;

        var Tree     = require("ace_tree/tree");
        var TreeData = require("./openfilesdp");

        /***** Initialization *****/

        var plugin        = new Plugin("Ajax.org", main.consumes);
        var emit          = plugin.getEmitter();
        var staticPrefix  = options.staticPrefix;

        // tree maximum height
        var MAX_HEIGHT    = window.outerHeight / 5;
        var showOpenFiles = true;

        // UI Elements
        var ofDataProvider, ofTree, treeParent, winFileTree;

        var loaded = false;
        function load(){
            if (loaded) return false;
            loaded = true;

            // Hook events to get the focussed page
            tabs.on("focus.sync", updateOpenFiles);
            tabs.on("page.destroy", updateOpenFiles);
            tabs.on("page.order", updateOpenFiles);

            commands.addCommand({
                name: "toggleOpenfiles",
                exec: function() {
                    settings.set("user/openfiles/@show", !showOpenFiles);
                    toggleOpenfiles(!showOpenFiles);
                }
            }, plugin);

            menus.addItemByPath("View/Open Files", new ui.item({
                type    : "check",
                checked : "[{settings.model}::user/openfiles/@show]",
                command : "toggleOpenfiles"
            }), 200, plugin);

            settings.on("read", function(e){
                // Defaults
                settings.setDefaults("user/openfiles", [["show", "true"]]);

                toggleOpenfiles(settings.getBool("user/openfiles/@show"));
            }, plugin);

            draw();
        }

        var drawn = false;
        function draw(){
            if (drawn) return;
            drawn = true;

            // ace_tree customization '.openfiles'
            // ui.insertCss(require("text!./openfiles.css"), staticPrefix, plugin);

            tree.getElement("winOpenfiles", function(winOpenfiles) {
                treeParent = winOpenfiles;

                tree.getElement("winFileTree", function(winFileTreeL) {
                    winFileTree = winFileTreeL;
                });

                // Create the Ace Tree
                ofTree = new Tree(treeParent.$int);
                ofDataProvider = new TreeData();
                ofTree.renderer.setScrollMargin(0, 10);
                // Assign the dataprovider
                ofTree.setDataProvider(ofDataProvider);
                // Some global render metadata
                ofDataProvider.staticPrefix = staticPrefix;

                ofDataProvider.on("select", function(){
                    setTimeout(onSelect, 40);
                });

                if (showOpenFiles)
                    updateOpenFiles();
                else
                    hideOpenFiles();

                var splitter = treeParent.parentNode.$handle;
                splitter.on("dragmove", updateOpenFiles);
                splitter.on("dragdrop", updateOpenFiles);

                emit("draw");
            });
        }

        /***** Methods *****/

        function updateOpenFiles() {
            if (!showOpenFiles)
                return;

            draw();
            var activeTabs   = tabs.getTabs();
            var focussedPage = tabs.focussedPage;
            var selected;

            root = activeTabs.map(function (tab, i) {
                return {
                    // name: tab.name (tab0 ...)
                    items: tab.getPages()
                        .filter(function(page){ return page.path && page.loaded; })
                        .map(function (page) {
                        var node = {
                            name : fs.getFilename(page.path),
                            path : page.path,
                            items: [],
                            page : page
                         };
                         if (page === focussedPage)
                            selected = node;
                        return node;
                    })
                };
            }).filter(function (tab) {
                return !!tab.items.length;
            }).map(function (node, i) {
                node.name = "GROUP " + (i+1);
                return node;
            });

            // Hide the openfiles
            if (!root.length)
                return hideOpenFiles();

            treeParent.show();

            if (root.length === 1)
                root = root[0];

            ofDataProvider.setRoot(root, selected);
            ofTree.resize(true);

            var treeHeight = ofTree.renderer.layerConfig.maxHeight + 3;
            var parentHeight = treeParent.getHeight();

            if (parentHeight < MAX_HEIGHT) {
                if (treeHeight < parentHeight)
                    treeParent.setHeight(treeHeight);
                else
                    treeParent.setHeight(Math.min(treeHeight, MAX_HEIGHT));
            }
            else
                treeParent.setHeight(treeParent.getHeight());

            ofTree.resize(true);
            // ofTree.renderer.scrollCaretIntoView(ofDataProvider.$selectedNode, 0.5);
        }

        function onSelect() {
            var node = ofDataProvider.$selectedNode;
            tabs.focusPage(node.path);
        }

        function hideOpenFiles() {
            treeParent && treeParent.hide();
        }

        function toggleOpenfiles(show) {
            if (show === showOpenFiles)
                return;
            showOpenFiles = show;

            if (!show)
                hideOpenFiles();
            else
                updateOpenFiles();

            emit("visible", {value: show});
        }

        /***** Lifecycle *****/
        plugin.on("load", function(){
            load();
        });
        plugin.on("enable", function(){

        });
        plugin.on("disable", function(){

        });
        plugin.on("unload", function(){
            loaded = false;
            drawn  = false;
        });

        /***** Register and define API *****/
        /**
         **/
        plugin.freezePublicAPI({
        });

        register(null, {
            openfiles: plugin
        });
    }
});
<commit_msg>Converting events to not use a dot (.)<commit_after>/**
 * Openfiles plugin for Cloud9 IDE
 *
 * @copyright 2011, Ajax.org B.V.
 * @license GPLv3 <http://www.gnu.org/licenses/gpl.txt>
 */

 define(function(require, exports, module) {
    main.consumes = [
        "plugin", "tabs", "menus", "commands", "settings", "tree", "fs", "ui"
    ];
    main.provides = ["openfiles"];
    return main;

    function main(options, imports, register) {
        var Plugin   = imports.plugin;
        var tabs     = imports.tabs;
        var menus    = imports.menus;
        var commands = imports.commands;
        var settings = imports.settings;
        var tree     = imports.tree;
        var fs       = imports.fs;
        var ui       = imports.ui;

        var Tree     = require("ace_tree/tree");
        var TreeData = require("./openfilesdp");

        /***** Initialization *****/

        var plugin        = new Plugin("Ajax.org", main.consumes);
        var emit          = plugin.getEmitter();
        var staticPrefix  = options.staticPrefix;

        // tree maximum height
        var MAX_HEIGHT    = window.outerHeight / 5;
        var showOpenFiles = true;

        // UI Elements
        var ofDataProvider, ofTree, treeParent, winFileTree;

        var loaded = false;
        function load(){
            if (loaded) return false;
            loaded = true;

            // Hook events to get the focussed page
            tabs.on("focus.sync", updateOpenFiles);
            tabs.on("pageDestroy", updateOpenFiles);
            tabs.on("pageOrder", updateOpenFiles);

            commands.addCommand({
                name: "toggleOpenfiles",
                exec: function() {
                    settings.set("user/openfiles/@show", !showOpenFiles);
                    toggleOpenfiles(!showOpenFiles);
                }
            }, plugin);

            menus.addItemByPath("View/Open Files", new ui.item({
                type    : "check",
                checked : "[{settings.model}::user/openfiles/@show]",
                command : "toggleOpenfiles"
            }), 200, plugin);

            settings.on("read", function(e){
                // Defaults
                settings.setDefaults("user/openfiles", [["show", "true"]]);

                toggleOpenfiles(settings.getBool("user/openfiles/@show"));
            }, plugin);

            draw();
        }

        var drawn = false;
        function draw(){
            if (drawn) return;
            drawn = true;

            // ace_tree customization '.openfiles'
            // ui.insertCss(require("text!./openfiles.css"), staticPrefix, plugin);

            tree.getElement("winOpenfiles", function(winOpenfiles) {
                treeParent = winOpenfiles;

                tree.getElement("winFileTree", function(winFileTreeL) {
                    winFileTree = winFileTreeL;
                });

                // Create the Ace Tree
                ofTree = new Tree(treeParent.$int);
                ofDataProvider = new TreeData();
                ofTree.renderer.setScrollMargin(0, 10);
                // Assign the dataprovider
                ofTree.setDataProvider(ofDataProvider);
                // Some global render metadata
                ofDataProvider.staticPrefix = staticPrefix;

                ofDataProvider.on("select", function(){
                    setTimeout(onSelect, 40);
                });

                if (showOpenFiles)
                    updateOpenFiles();
                else
                    hideOpenFiles();

                var splitter = treeParent.parentNode.$handle;
                splitter.on("dragmove", updateOpenFiles);
                splitter.on("dragdrop", updateOpenFiles);

                emit("draw");
            });
        }

        /***** Methods *****/

        function updateOpenFiles() {
            if (!showOpenFiles)
                return;

            draw();
            var activeTabs   = tabs.getTabs();
            var focussedPage = tabs.focussedPage;
            var selected;

            root = activeTabs.map(function (tab, i) {
                return {
                    // name: tab.name (tab0 ...)
                    items: tab.getPages()
                        .filter(function(page){ return page.path && page.loaded; })
                        .map(function (page) {
                        var node = {
                            name : fs.getFilename(page.path),
                            path : page.path,
                            items: [],
                            page : page
                         };
                         if (page === focussedPage)
                            selected = node;
                        return node;
                    })
                };
            }).filter(function (tab) {
                return !!tab.items.length;
            }).map(function (node, i) {
                node.name = "GROUP " + (i+1);
                return node;
            });

            // Hide the openfiles
            if (!root.length)
                return hideOpenFiles();

            treeParent.show();

            if (root.length === 1)
                root = root[0];

            ofDataProvider.setRoot(root, selected);
            ofTree.resize(true);

            var treeHeight = ofTree.renderer.layerConfig.maxHeight + 3;
            var parentHeight = treeParent.getHeight();

            if (parentHeight < MAX_HEIGHT) {
                if (treeHeight < parentHeight)
                    treeParent.setHeight(treeHeight);
                else
                    treeParent.setHeight(Math.min(treeHeight, MAX_HEIGHT));
            }
            else
                treeParent.setHeight(treeParent.getHeight());

            ofTree.resize(true);
            // ofTree.renderer.scrollCaretIntoView(ofDataProvider.$selectedNode, 0.5);
        }

        function onSelect() {
            var node = ofDataProvider.$selectedNode;
            tabs.focusPage(node.path);
        }

        function hideOpenFiles() {
            treeParent && treeParent.hide();
        }

        function toggleOpenfiles(show) {
            if (show === showOpenFiles)
                return;
            showOpenFiles = show;

            if (!show)
                hideOpenFiles();
            else
                updateOpenFiles();

            emit("visible", {value: show});
        }

        /***** Lifecycle *****/
        plugin.on("load", function(){
            load();
        });
        plugin.on("enable", function(){

        });
        plugin.on("disable", function(){

        });
        plugin.on("unload", function(){
            loaded = false;
            drawn  = false;
        });

        /***** Register and define API *****/
        /**
         **/
        plugin.freezePublicAPI({
        });

        register(null, {
            openfiles: plugin
        });
    }
});
<|endoftext|>


<commit_before>/**
 * User Input Questionnaire.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * WordPress dependencies
 */
import { useCallback, Fragment, useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import ProgressBar from '../ProgressBar';
import UserInputQuestionWrapper from './UserInputQuestionWrapper';
import UserInputSelectOptions from './UserInputSelectOptions';
import UserInputKeywords from './UserInputKeywords';
import UserInputPreview from './UserInputPreview';
import {
	USER_INPUT_QUESTIONS_LIST,
	USER_INPUT_QUESTION_ROLE,
	USER_INPUT_QUESTION_POST_FREQUENCY,
	USER_INPUT_QUESTION_GOALS,
	USER_INPUT_QUESTION_HELP_NEEDED,
	USER_INPUT_QUESTION_SEARCH_TERMS,
	getUserInputAnwsers,
} from './util/constants';
import useQueryArg from '../../hooks/useQueryArg';
import { CORE_USER } from '../../googlesitekit/datastore/user/constants';
import { CORE_SITE } from '../../googlesitekit/datastore/site/constants';
import { CORE_LOCATION } from '../../googlesitekit/datastore/location/constants';
import { Cell, Row } from '../../material-components';
const { useSelect, useDispatch } = Data;

export default function UserInputQuestionnaire() {
	const steps = [ ...USER_INPUT_QUESTIONS_LIST, 'preview' ];

	const [ activeSlug, setActiveSlug ] = useQueryArg( 'question', steps[ 0 ] );
	const [ shouldScrollToActiveQuestion, setShouldScrollToActiveQuestion ] = useState( false );
	const [ redirectURL ] = useQueryArg( 'redirect_url' );
	const [ single, setSingle ] = useQueryArg( 'single', false );

	const activeSlugIndex = steps.indexOf( activeSlug );
	if ( activeSlugIndex === -1 ) {
		setActiveSlug( steps[ 0 ] );
	}

	const { saveUserInputSettings } = useDispatch( CORE_USER );
	const { navigateTo } = useDispatch( CORE_LOCATION );

	const isNavigating = useSelect( ( select ) => select( CORE_LOCATION ).isNavigating() );
	const dashboardURL = useSelect( ( select ) => select( CORE_SITE ).getAdminURL( 'googlesitekit-dashboard' ) );
	const { isSavingSettings, error, answeredUntilIndex } = useSelect( ( select ) => {
		const userInputSettings = select( CORE_USER ).getUserInputSettings();

		return {
			isSavingSettings: select( CORE_USER ).isSavingUserInputSettings( userInputSettings ),
			error: select( CORE_USER ).getErrorForAction( 'saveUserInputSettings', [] ),
			answeredUntilIndex: USER_INPUT_QUESTIONS_LIST.findIndex( ( question ) => userInputSettings?.[ question ]?.values?.length === 0 ),
		};
	} );

	useEffect( () => {
		if ( answeredUntilIndex === -1 ) {
			return;
		}
		if ( activeSlugIndex > answeredUntilIndex ) {
			setActiveSlug( steps[ answeredUntilIndex ] );
		}
	}, [ answeredUntilIndex, activeSlugIndex ] );

	const {
		USER_INPUT_ANSWERS_GOALS,
		USER_INPUT_ANSWERS_HELP_NEEDED,
		USER_INPUT_ANSWERS_POST_FREQUENCY,
		USER_INPUT_ANSWERS_ROLE,
	} = getUserInputAnwsers();

	const next = useCallback( () => {
		setActiveSlug( steps[ activeSlugIndex + 1 ] );
	}, [ activeSlugIndex ] );

	const goTo = useCallback( ( num = 1, singleType = false ) => {
		// If we're going to a single question to edit it, set the query string here.
		// We can't currently set it in the child component because the useQueryArg hook doesn't update in the parent.
		setSingle( singleType );
		if ( steps.length >= num && num > 0 ) {
			setActiveSlug( steps[ num - 1 ] );
		}
	}, [ activeSlugIndex ] );

	const back = useCallback( () => {
		setActiveSlug( steps[ activeSlugIndex - 1 ] );
	}, [ activeSlugIndex ] );

	const submitChanges = useCallback( async () => {
		const response = await saveUserInputSettings();
		if ( ! response.error ) {
			const url = new URL( redirectURL || dashboardURL );

			// Here we don't use `addQueryArgs` due to a bug with how it handles hashes
			// See https://github.com/WordPress/gutenberg/issues/16655
			url.searchParams.set( 'notification', 'user_input_success' );

			navigateTo( url.toString() );
		}
	}, [ dashboardURL ] );

	const goToPreview = useCallback( () => {
		setActiveSlug( steps[ steps.length - 1 ] );
	}, [ activeSlugIndex ] );

	useEffect( () => {
		if ( ! shouldScrollToActiveQuestion ) {
			setShouldScrollToActiveQuestion( true );
			return;
		}

		global.document.querySelector( '#googlesitekit-user-input-container' )?.scrollIntoView( { behavior: 'smooth' } );
	}, [ activeSlug ] );

	// Update the callbacks and labels for the questions if the user is editing a *single question*.
	let backCallback = back;
	let nextCallback = next;
	let nextLabel;

	if ( single === 'user-input' ) {
		backCallback = undefined;
		// When the user is editing a single question in the user-input screen send them back to the preview when they click Update.
		nextCallback = goToPreview;
		nextLabel = __( 'Update', 'google-site-kit' );
	} else if ( single === 'settings' ) {
		backCallback = undefined;
		// When the user is editing a single question from the settings screen, submit changes and send them back to the settings pages when they click Submit.
		nextCallback = submitChanges;
		nextLabel = __( 'Submit', 'google-site-kit' );
	}

	const settingsProgress = (
		<ProgressBar
			height={ 0 }
			indeterminate={ false }
			progress={ ( activeSlugIndex + 1 ) / USER_INPUT_QUESTIONS_LIST.length }
		/>
	);

	if ( isSavingSettings || isNavigating ) {
		return (
			<Fragment>
				{ settingsProgress }

					<Row>
						<Cell lgSize={ 12 } mdSize={ 8 } smSize={ 4 }>
							<ProgressBar />
						</Cell>
					</Row>

			</Fragment>
		);
	}

	return (

			{ settingsProgress }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_ROLE ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_ROLE }
					isActive={ activeSlug === USER_INPUT_QUESTION_ROLE }
					questionNumber={ 1 }
					title={ __( 'Which best describes your team/role in relation to this site?', 'google-site-kit' ) }
					description={ __( 'This will help Site Kit show tips that help you specifically in your role.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_ROLE }
						options={ USER_INPUT_ANSWERS_ROLE }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_POST_FREQUENCY ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_POST_FREQUENCY }
					isActive={ activeSlug === USER_INPUT_QUESTION_POST_FREQUENCY }
					questionNumber={ 2 }
					title={ __( 'How often do you create new posts for this site?', 'google-site-kit' ) }
					description={ __( 'Based on your answer, Site Kit will suggest new features for your dashboard related to content creation.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_POST_FREQUENCY }
						options={ USER_INPUT_ANSWERS_POST_FREQUENCY }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_GOALS ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_GOALS }
					isActive={ activeSlug === USER_INPUT_QUESTION_GOALS }
					questionNumber={ 3 }
					title={ __( 'What are the goals of this site?', 'google-site-kit' ) }
					description={ __( 'Based on your answer, Site Kit will tailor the metrics you see on your dashboard to help you track how close you’re getting to your specific goals.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_GOALS }
						max={ 2 }
						options={ USER_INPUT_ANSWERS_GOALS }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_HELP_NEEDED ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_HELP_NEEDED }
					isActive={ activeSlug === USER_INPUT_QUESTION_HELP_NEEDED }
					questionNumber={ 4 }
					title={ __( 'What do you need help most with for this site?', 'google-site-kit' ) }
					description={ __( 'Based on your answers, Site Kit will tailor the metrics and advice you see on your dashboard to help you make progress in these areas.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_HELP_NEEDED }
						max={ 3 }
						options={ USER_INPUT_ANSWERS_HELP_NEEDED }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_SEARCH_TERMS ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_SEARCH_TERMS }
					isActive={ activeSlug === USER_INPUT_QUESTION_SEARCH_TERMS }
					questionNumber={ 5 }
					title={ __( 'To help us identify opportunities for your site, enter the top three search terms that best describe your site’s content.', 'google-site-kit' ) }
					description={ __( 'Site Kit will keep you informed if people start finding you in Search for these terms.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel === undefined ? __( 'Preview', 'google-site-kit' ) : nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputKeywords
						slug={ USER_INPUT_QUESTION_SEARCH_TERMS }
						max={ 3 }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlug === 'preview' && (
				<UserInputPreview
					submitChanges={ submitChanges }
					goTo={ goTo }
					error={ error }
				/>
			) }

	);
}
<commit_msg>Use refs instead of querySelector.<commit_after>/**
 * User Input Questionnaire.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * WordPress dependencies
 */
import { useCallback, Fragment, useEffect, useState, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import ProgressBar from '../ProgressBar';
import UserInputQuestionWrapper from './UserInputQuestionWrapper';
import UserInputSelectOptions from './UserInputSelectOptions';
import UserInputKeywords from './UserInputKeywords';
import UserInputPreview from './UserInputPreview';
import {
	USER_INPUT_QUESTIONS_LIST,
	USER_INPUT_QUESTION_ROLE,
	USER_INPUT_QUESTION_POST_FREQUENCY,
	USER_INPUT_QUESTION_GOALS,
	USER_INPUT_QUESTION_HELP_NEEDED,
	USER_INPUT_QUESTION_SEARCH_TERMS,
	getUserInputAnwsers,
} from './util/constants';
import useQueryArg from '../../hooks/useQueryArg';
import { CORE_USER } from '../../googlesitekit/datastore/user/constants';
import { CORE_SITE } from '../../googlesitekit/datastore/site/constants';
import { CORE_LOCATION } from '../../googlesitekit/datastore/location/constants';
import { Cell, Row } from '../../material-components';
const { useSelect, useDispatch } = Data;

export default function UserInputQuestionnaire() {
	const steps = [ ...USER_INPUT_QUESTIONS_LIST, 'preview' ];

	const [ activeSlug, setActiveSlug ] = useQueryArg( 'question', steps[ 0 ] );
	const [ shouldScrollToActiveQuestion, setShouldScrollToActiveQuestion ] = useState( false );
	const [ redirectURL ] = useQueryArg( 'redirect_url' );
	const [ single, setSingle ] = useQueryArg( 'single', false );

	const containerRef = useRef();

	const activeSlugIndex = steps.indexOf( activeSlug );
	if ( activeSlugIndex === -1 ) {
		setActiveSlug( steps[ 0 ] );
	}

	const { saveUserInputSettings } = useDispatch( CORE_USER );
	const { navigateTo } = useDispatch( CORE_LOCATION );

	const isNavigating = useSelect( ( select ) => select( CORE_LOCATION ).isNavigating() );
	const dashboardURL = useSelect( ( select ) => select( CORE_SITE ).getAdminURL( 'googlesitekit-dashboard' ) );
	const { isSavingSettings, error, answeredUntilIndex } = useSelect( ( select ) => {
		const userInputSettings = select( CORE_USER ).getUserInputSettings();

		return {
			isSavingSettings: select( CORE_USER ).isSavingUserInputSettings( userInputSettings ),
			error: select( CORE_USER ).getErrorForAction( 'saveUserInputSettings', [] ),
			answeredUntilIndex: USER_INPUT_QUESTIONS_LIST.findIndex( ( question ) => userInputSettings?.[ question ]?.values?.length === 0 ),
		};
	} );

	useEffect( () => {
		if ( answeredUntilIndex === -1 ) {
			return;
		}
		if ( activeSlugIndex > answeredUntilIndex ) {
			setActiveSlug( steps[ answeredUntilIndex ] );
		}
	}, [ answeredUntilIndex, activeSlugIndex ] );

	const {
		USER_INPUT_ANSWERS_GOALS,
		USER_INPUT_ANSWERS_HELP_NEEDED,
		USER_INPUT_ANSWERS_POST_FREQUENCY,
		USER_INPUT_ANSWERS_ROLE,
	} = getUserInputAnwsers();

	const next = useCallback( () => {
		setActiveSlug( steps[ activeSlugIndex + 1 ] );
	}, [ activeSlugIndex ] );

	const goTo = useCallback( ( num = 1, singleType = false ) => {
		// If we're going to a single question to edit it, set the query string here.
		// We can't currently set it in the child component because the useQueryArg hook doesn't update in the parent.
		setSingle( singleType );
		if ( steps.length >= num && num > 0 ) {
			setActiveSlug( steps[ num - 1 ] );
		}
	}, [ activeSlugIndex ] );

	const back = useCallback( () => {
		setActiveSlug( steps[ activeSlugIndex - 1 ] );
	}, [ activeSlugIndex ] );

	const submitChanges = useCallback( async () => {
		const response = await saveUserInputSettings();
		if ( ! response.error ) {
			const url = new URL( redirectURL || dashboardURL );

			// Here we don't use `addQueryArgs` due to a bug with how it handles hashes
			// See https://github.com/WordPress/gutenberg/issues/16655
			url.searchParams.set( 'notification', 'user_input_success' );

			navigateTo( url.toString() );
		}
	}, [ dashboardURL ] );

	const goToPreview = useCallback( () => {
		setActiveSlug( steps[ steps.length - 1 ] );
	}, [ activeSlugIndex ] );

	useEffect( () => {
		if ( ! shouldScrollToActiveQuestion ) {
			setShouldScrollToActiveQuestion( true );
			return;
		}

		containerRef.current?.scrollIntoView( { behavior: 'smooth' } );
	}, [ activeSlug ] );

	// Update the callbacks and labels for the questions if the user is editing a *single question*.
	let backCallback = back;
	let nextCallback = next;
	let nextLabel;

	if ( single === 'user-input' ) {
		backCallback = undefined;
		// When the user is editing a single question in the user-input screen send them back to the preview when they click Update.
		nextCallback = goToPreview;
		nextLabel = __( 'Update', 'google-site-kit' );
	} else if ( single === 'settings' ) {
		backCallback = undefined;
		// When the user is editing a single question from the settings screen, submit changes and send them back to the settings pages when they click Submit.
		nextCallback = submitChanges;
		nextLabel = __( 'Submit', 'google-site-kit' );
	}

	const settingsProgress = (
		<ProgressBar
			height={ 0 }
			indeterminate={ false }
			progress={ ( activeSlugIndex + 1 ) / USER_INPUT_QUESTIONS_LIST.length }
		/>
	);

	if ( isSavingSettings || isNavigating ) {
		return (
			<Fragment>
				{ settingsProgress }

					<Row>
						<Cell lgSize={ 12 } mdSize={ 8 } smSize={ 4 }>
							<ProgressBar />
						</Cell>
					</Row>

			</Fragment>
		);
	}

	return (

			{ settingsProgress }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_ROLE ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_ROLE }
					isActive={ activeSlug === USER_INPUT_QUESTION_ROLE }
					questionNumber={ 1 }
					title={ __( 'Which best describes your team/role in relation to this site?', 'google-site-kit' ) }
					description={ __( 'This will help Site Kit show tips that help you specifically in your role.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_ROLE }
						options={ USER_INPUT_ANSWERS_ROLE }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_POST_FREQUENCY ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_POST_FREQUENCY }
					isActive={ activeSlug === USER_INPUT_QUESTION_POST_FREQUENCY }
					questionNumber={ 2 }
					title={ __( 'How often do you create new posts for this site?', 'google-site-kit' ) }
					description={ __( 'Based on your answer, Site Kit will suggest new features for your dashboard related to content creation.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_POST_FREQUENCY }
						options={ USER_INPUT_ANSWERS_POST_FREQUENCY }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_GOALS ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_GOALS }
					isActive={ activeSlug === USER_INPUT_QUESTION_GOALS }
					questionNumber={ 3 }
					title={ __( 'What are the goals of this site?', 'google-site-kit' ) }
					description={ __( 'Based on your answer, Site Kit will tailor the metrics you see on your dashboard to help you track how close you’re getting to your specific goals.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_GOALS }
						max={ 2 }
						options={ USER_INPUT_ANSWERS_GOALS }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_HELP_NEEDED ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_HELP_NEEDED }
					isActive={ activeSlug === USER_INPUT_QUESTION_HELP_NEEDED }
					questionNumber={ 4 }
					title={ __( 'What do you need help most with for this site?', 'google-site-kit' ) }
					description={ __( 'Based on your answers, Site Kit will tailor the metrics and advice you see on your dashboard to help you make progress in these areas.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputSelectOptions
						slug={ USER_INPUT_QUESTION_HELP_NEEDED }
						max={ 3 }
						options={ USER_INPUT_ANSWERS_HELP_NEEDED }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlugIndex <= steps.indexOf( USER_INPUT_QUESTION_SEARCH_TERMS ) && (
				<UserInputQuestionWrapper
					slug={ USER_INPUT_QUESTION_SEARCH_TERMS }
					isActive={ activeSlug === USER_INPUT_QUESTION_SEARCH_TERMS }
					questionNumber={ 5 }
					title={ __( 'To help us identify opportunities for your site, enter the top three search terms that best describe your site’s content.', 'google-site-kit' ) }
					description={ __( 'Site Kit will keep you informed if people start finding you in Search for these terms.', 'google-site-kit' ) }
					next={ nextCallback }
					nextLabel={ nextLabel === undefined ? __( 'Preview', 'google-site-kit' ) : nextLabel }
					back={ backCallback }
					error={ error }
				>
					<UserInputKeywords
						slug={ USER_INPUT_QUESTION_SEARCH_TERMS }
						max={ 3 }
					/>
				</UserInputQuestionWrapper>
			) }

			{ activeSlug === 'preview' && (
				<UserInputPreview
					submitChanges={ submitChanges }
					goTo={ goTo }
					error={ error }
				/>
			) }

	);
}
<|endoftext|>


<commit_before>/**
 * `core/user` data store: dismissed items
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { STORE_NAME } from './constants';
import { createFetchStore } from '../../data/create-fetch-store';
import { createValidatedAction } from '../../data/utils';

const { createRegistrySelector, commonActions } = Data;
const { getRegistry } = commonActions;

function reducerCallback( state, dismissedItems ) {
	return {
		...state,
		dismissedItems: Array.isArray( dismissedItems ) ? dismissedItems : [],
	};
}

const fetchGetDismissedItemsStore = createFetchStore( {
	baseName: 'getDismissedItems',
	controlCallback: () => API.get( 'core', 'user', 'dismissed-items', {}, { useCache: false } ),
	reducerCallback,
} );

const fetchDismissItemStore = createFetchStore( {
	baseName: 'dismissItem',
	controlCallback: ( { slug, expiresInSeconds } ) => API.set( 'core', 'user', 'dismiss-item', { slug, expiresInSeconds } ),
	reducerCallback,
	argsToParams: ( slug, expiresInSeconds = 0 ) => {
		return ( { slug, expiresInSeconds } );
	},
	validateParams: ( { slug, expiresInSeconds } = {} ) => {
		invariant( slug, 'slug is required.' );
		invariant( Number.isInteger( expiresInSeconds ), 'expiresInSeconds must be an integer.' );
	},
} );

const baseInitialState = {
	dismissedItems: undefined,
};

const baseActions = {
	/**
	 * Dismisses the given item by slug.
	 *
	 * @since 1.37.0
	 *
	 * @param {string} slug                       Item slug to dismiss.
	 * @param {Object} options                    Dismiss item options.
	 * @param {number} [options.expiresInSeconds] Optional. An integer number of seconds for expiry. 0 denotes permanent dismissal.
	 * @return {Object} Generator instance.
	 */
	dismissItem: createValidatedAction(
		( slug, options = {} ) => {
			const { expiresInSeconds = 0 } = options;
			invariant( slug, 'A tour slug is required to dismiss a tour.' );
			invariant( Number.isInteger( expiresInSeconds ), 'expiresInSeconds must be an integer.' );
		},
		function* ( slug, options = {} ) {
			const { expiresInSeconds = 0 } = options;
			return yield fetchDismissItemStore.actions.fetchDismissItem( slug, expiresInSeconds );
		},
	),
};

const baseResolvers = {
	*getDismissedItems() {
		const { select } = yield getRegistry();
		const dismissedItems = select( STORE_NAME ).getDismissedItems();
		if ( dismissedItems === undefined ) {
			yield fetchGetDismissedItemsStore.actions.fetchGetDismissedItems();
		}
	},
};

const baseSelectors = {
	/**
	 * Gets the list of dismissed items.
	 *
	 * @since 1.37.0
	 *
	 * @param {Object} state Data store's state.
	 * @return {(string[]|undefined)} Array of dismissed item slugs, `undefined` if not resolved yet.
	 */
	getDismissedItems( state ) {
		return state.dismissedItems;
	},

	/**
	 * Determines whether the item is dismissed or not.
	 *
	 * @since 1.37.0
	 *
	 * @param {Object} state Data store's state.
	 * @param {string} slug  Item slug.
	 * @return {(boolean|undefined)} TRUE if dismissed, otherwise FALSE, `undefined` if not resolved yet.
	 */
	isItemDismissed: createRegistrySelector( ( select ) => ( state, slug ) => {
		return select( STORE_NAME ).getDismissedItems()?.includes( slug );
	} ),
};

export const {
	actions,
	controls,
	initialState,
	reducer,
	resolvers,
	selectors,
} = Data.combineStores(
	{
		initialState: baseInitialState,
		actions: baseActions,
		resolvers: baseResolvers,
		selectors: baseSelectors,
	},
	fetchDismissItemStore,
	fetchGetDismissedItemsStore,
);

export default {
	actions,
	controls,
	initialState,
	reducer,
	resolvers,
	selectors,
};
<commit_msg>Match param to endpoint.<commit_after>/**
 * `core/user` data store: dismissed items
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { STORE_NAME } from './constants';
import { createFetchStore } from '../../data/create-fetch-store';
import { createValidatedAction } from '../../data/utils';

const { createRegistrySelector, commonActions } = Data;
const { getRegistry } = commonActions;

function reducerCallback( state, dismissedItems ) {
	return {
		...state,
		dismissedItems: Array.isArray( dismissedItems ) ? dismissedItems : [],
	};
}

const fetchGetDismissedItemsStore = createFetchStore( {
	baseName: 'getDismissedItems',
	controlCallback: () => API.get( 'core', 'user', 'dismissed-items', {}, { useCache: false } ),
	reducerCallback,
} );

const fetchDismissItemStore = createFetchStore( {
	baseName: 'dismissItem',
	controlCallback: ( { slug, expiresInSeconds } ) => API.set( 'core', 'user', 'dismiss-item', { slug, expiration: expiresInSeconds } ),
	reducerCallback,
	argsToParams: ( slug, expiresInSeconds = 0 ) => {
		return ( { slug, expiresInSeconds } );
	},
	validateParams: ( { slug, expiresInSeconds } = {} ) => {
		invariant( slug, 'slug is required.' );
		invariant( Number.isInteger( expiresInSeconds ), 'expiresInSeconds must be an integer.' );
	},
} );

const baseInitialState = {
	dismissedItems: undefined,
};

const baseActions = {
	/**
	 * Dismisses the given item by slug.
	 *
	 * @since 1.37.0
	 *
	 * @param {string} slug                       Item slug to dismiss.
	 * @param {Object} options                    Dismiss item options.
	 * @param {number} [options.expiresInSeconds] Optional. An integer number of seconds for expiry. 0 denotes permanent dismissal.
	 * @return {Object} Generator instance.
	 */
	dismissItem: createValidatedAction(
		( slug, options = {} ) => {
			const { expiresInSeconds = 0 } = options;
			invariant( slug, 'A tour slug is required to dismiss a tour.' );
			invariant( Number.isInteger( expiresInSeconds ), 'expiresInSeconds must be an integer.' );
		},
		function* ( slug, options = {} ) {
			const { expiresInSeconds = 0 } = options;
			return yield fetchDismissItemStore.actions.fetchDismissItem( slug, expiresInSeconds );
		},
	),
};

const baseResolvers = {
	*getDismissedItems() {
		const { select } = yield getRegistry();
		const dismissedItems = select( STORE_NAME ).getDismissedItems();
		if ( dismissedItems === undefined ) {
			yield fetchGetDismissedItemsStore.actions.fetchGetDismissedItems();
		}
	},
};

const baseSelectors = {
	/**
	 * Gets the list of dismissed items.
	 *
	 * @since 1.37.0
	 *
	 * @param {Object} state Data store's state.
	 * @return {(string[]|undefined)} Array of dismissed item slugs, `undefined` if not resolved yet.
	 */
	getDismissedItems( state ) {
		return state.dismissedItems;
	},

	/**
	 * Determines whether the item is dismissed or not.
	 *
	 * @since 1.37.0
	 *
	 * @param {Object} state Data store's state.
	 * @param {string} slug  Item slug.
	 * @return {(boolean|undefined)} TRUE if dismissed, otherwise FALSE, `undefined` if not resolved yet.
	 */
	isItemDismissed: createRegistrySelector( ( select ) => ( state, slug ) => {
		return select( STORE_NAME ).getDismissedItems()?.includes( slug );
	} ),
};

export const {
	actions,
	controls,
	initialState,
	reducer,
	resolvers,
	selectors,
} = Data.combineStores(
	{
		initialState: baseInitialState,
		actions: baseActions,
		resolvers: baseResolvers,
		selectors: baseSelectors,
	},
	fetchDismissItemStore,
	fetchGetDismissedItemsStore,
);

export default {
	actions,
	controls,
	initialState,
	reducer,
	resolvers,
	selectors,
};
<|endoftext|>