Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/reducers/projectV0.js b/static_src/reducers/projectV0.js
new file mode 100644
index 0000000..5101ff8
--- /dev/null
+++ b/static_src/reducers/projectV0.js
@@ -0,0 +1,586 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {combineReducers} from 'redux';
+import {createSelector} from 'reselect';
+import {createReducer, createRequestReducer} from './redux-helpers.js';
+import * as permissions from 'reducers/permissions.js';
+import {fieldTypes, SITEWIDE_DEFAULT_COLUMNS, defaultIssueFieldMap,
+ parseColSpec, stringValuesForIssueField} from 'shared/issue-fields.js';
+import {hasPrefix, removePrefix} from 'shared/helpers.js';
+import {fieldNameToLabelPrefix,
+ labelNameToLabelPrefixes, labelNameToLabelValue, fieldDefToName,
+ restrictionLabelsForPermissions} from 'shared/convertersV0.js';
+import {prpcClient} from 'prpc-client-instance.js';
+import 'shared/typedef.js';
+
+/** @typedef {import('redux').AnyAction} AnyAction */
+
+// Actions
+export const SELECT = 'projectV0/SELECT';
+
+const FETCH_CONFIG_START = 'projectV0/FETCH_CONFIG_START';
+export const FETCH_CONFIG_SUCCESS = 'projectV0/FETCH_CONFIG_SUCCESS';
+const FETCH_CONFIG_FAILURE = 'projectV0/FETCH_CONFIG_FAILURE';
+
+export const FETCH_PRESENTATION_CONFIG_START =
+ 'projectV0/FETCH_PRESENTATION_CONFIG_START';
+export const FETCH_PRESENTATION_CONFIG_SUCCESS =
+ 'projectV0/FETCH_PRESENTATION_CONFIG_SUCCESS';
+export const FETCH_PRESENTATION_CONFIG_FAILURE =
+ 'projectV0/FETCH_PRESENTATION_CONFIG_FAILURE';
+
+export const FETCH_CUSTOM_PERMISSIONS_START =
+ 'projectV0/FETCH_CUSTOM_PERMISSIONS_START';
+export const FETCH_CUSTOM_PERMISSIONS_SUCCESS =
+ 'projectV0/FETCH_CUSTOM_PERMISSIONS_SUCCESS';
+export const FETCH_CUSTOM_PERMISSIONS_FAILURE =
+ 'projectV0/FETCH_CUSTOM_PERMISSIONS_FAILURE';
+
+
+export const FETCH_VISIBLE_MEMBERS_START =
+ 'projectV0/FETCH_VISIBLE_MEMBERS_START';
+export const FETCH_VISIBLE_MEMBERS_SUCCESS =
+ 'projectV0/FETCH_VISIBLE_MEMBERS_SUCCESS';
+export const FETCH_VISIBLE_MEMBERS_FAILURE =
+ 'projectV0/FETCH_VISIBLE_MEMBERS_FAILURE';
+
+const FETCH_TEMPLATES_START = 'projectV0/FETCH_TEMPLATES_START';
+export const FETCH_TEMPLATES_SUCCESS = 'projectV0/FETCH_TEMPLATES_SUCCESS';
+const FETCH_TEMPLATES_FAILURE = 'projectV0/FETCH_TEMPLATES_FAILURE';
+
+/* State Shape
+{
+ name: string,
+
+ configs: Object<string, Config>,
+ presentationConfigs: Object<string, PresentationConfig>,
+ customPermissions: Object<string, Array<string>>,
+ visibleMembers:
+ Object<string, {userRefs: Array<UserRef>, groupRefs: Array<UserRef>}>,
+ templates: Object<string, Array<TemplateDef>>,
+ presentationConfigsLoaded: Object<string, boolean>,
+
+ requests: {
+ fetchConfig: ReduxRequestState,
+ fetchMembers: ReduxRequestState
+ fetchCustomPermissions: ReduxRequestState,
+ fetchPresentationConfig: ReduxRequestState,
+ fetchTemplates: ReduxRequestState,
+ },
+}
+*/
+
+// Reducers
+export const nameReducer = createReducer(null, {
+ [SELECT]: (_state, {projectName}) => projectName,
+});
+
+export const configsReducer = createReducer({}, {
+ [FETCH_CONFIG_SUCCESS]: (state, {projectName, config}) => ({
+ ...state,
+ [projectName]: config,
+ }),
+});
+
+export const presentationConfigsReducer = createReducer({}, {
+ [FETCH_PRESENTATION_CONFIG_SUCCESS]:
+ (state, {projectName, presentationConfig}) => ({
+ ...state,
+ [projectName]: presentationConfig,
+ }),
+});
+
+/**
+ * Adds custom permissions to Redux in a normalized state.
+ * @param {Object<string, Array<String>>} state Redux state.
+ * @param {AnyAction} Action
+ * @return {Object<string, Array<String>>}
+ */
+export const customPermissionsReducer = createReducer({}, {
+ [FETCH_CUSTOM_PERMISSIONS_SUCCESS]:
+ (state, {projectName, permissions}) => ({
+ ...state,
+ [projectName]: permissions,
+ }),
+});
+
+export const visibleMembersReducer = createReducer({}, {
+ [FETCH_VISIBLE_MEMBERS_SUCCESS]: (state, {projectName, visibleMembers}) => ({
+ ...state,
+ [projectName]: visibleMembers,
+ }),
+});
+
+export const templatesReducer = createReducer({}, {
+ [FETCH_TEMPLATES_SUCCESS]: (state, {projectName, templates}) => ({
+ ...state,
+ [projectName]: templates,
+ }),
+});
+
+const requestsReducer = combineReducers({
+ fetchConfig: createRequestReducer(
+ FETCH_CONFIG_START, FETCH_CONFIG_SUCCESS, FETCH_CONFIG_FAILURE),
+ fetchMembers: createRequestReducer(
+ FETCH_VISIBLE_MEMBERS_START,
+ FETCH_VISIBLE_MEMBERS_SUCCESS,
+ FETCH_VISIBLE_MEMBERS_FAILURE),
+ fetchCustomPermissions: createRequestReducer(
+ FETCH_CUSTOM_PERMISSIONS_START,
+ FETCH_CUSTOM_PERMISSIONS_SUCCESS,
+ FETCH_CUSTOM_PERMISSIONS_FAILURE),
+ fetchPresentationConfig: createRequestReducer(
+ FETCH_PRESENTATION_CONFIG_START,
+ FETCH_PRESENTATION_CONFIG_SUCCESS,
+ FETCH_PRESENTATION_CONFIG_FAILURE),
+ fetchTemplates: createRequestReducer(
+ FETCH_TEMPLATES_START, FETCH_TEMPLATES_SUCCESS, FETCH_TEMPLATES_FAILURE),
+});
+
+export const reducer = combineReducers({
+ name: nameReducer,
+ configs: configsReducer,
+ customPermissions: customPermissionsReducer,
+ presentationConfigs: presentationConfigsReducer,
+ visibleMembers: visibleMembersReducer,
+ templates: templatesReducer,
+ requests: requestsReducer,
+});
+
+// Selectors
+export const project = (state) => state.projectV0 || {};
+
+export const viewedProjectName =
+ createSelector(project, (project) => project.name || null);
+
+export const configs =
+ createSelector(project, (project) => project.configs || {});
+export const presentationConfigs =
+ createSelector(project, (project) => project.presentationConfigs || {});
+export const customPermissions =
+ createSelector(project, (project) => project.customPermissions || {});
+export const visibleMembers =
+ createSelector(project, (project) => project.visibleMembers || {});
+export const templates =
+ createSelector(project, (project) => project.templates || {});
+
+export const viewedConfig = createSelector(
+ [viewedProjectName, configs],
+ (projectName, configs) => configs[projectName] || {});
+export const viewedPresentationConfig = createSelector(
+ [viewedProjectName, presentationConfigs],
+ (projectName, configs) => configs[projectName] || {});
+
+// TODO(crbug.com/monorail/7080): Come up with a more clear and
+// consistent pattern for determining when data is loaded.
+export const viewedPresentationConfigLoaded = createSelector(
+ [viewedProjectName, presentationConfigs],
+ (projectName, configs) => !!configs[projectName]);
+export const viewedCustomPermissions = createSelector(
+ [viewedProjectName, customPermissions],
+ (projectName, permissions) => permissions[projectName] || []);
+export const viewedVisibleMembers = createSelector(
+ [viewedProjectName, visibleMembers],
+ (projectName, visibleMembers) => visibleMembers[projectName] || {});
+export const viewedTemplates = createSelector(
+ [viewedProjectName, templates],
+ (projectName, templates) => templates[projectName] || []);
+
+/**
+ * Get the default columns for the currently viewed project.
+ */
+export const defaultColumns = createSelector(viewedPresentationConfig,
+ ({defaultColSpec}) =>{
+ if (defaultColSpec) {
+ return parseColSpec(defaultColSpec);
+ }
+ return SITEWIDE_DEFAULT_COLUMNS;
+ });
+
+
+/**
+ * Get the default query for the currently viewed project.
+ */
+export const defaultQuery = createSelector(viewedPresentationConfig,
+ (config) => config.defaultQuery || '');
+
+// Look up components by path.
+export const componentsMap = createSelector(
+ viewedConfig,
+ (config) => {
+ if (!config || !config.componentDefs) return new Map();
+ const acc = new Map();
+ for (const v of config.componentDefs) {
+ acc.set(v.path, v);
+ }
+ return acc;
+ },
+);
+
+export const fieldDefs = createSelector(
+ viewedConfig, (config) => ((config && config.fieldDefs) || []),
+);
+
+export const fieldDefMap = createSelector(
+ fieldDefs, (fieldDefs) => {
+ const map = new Map();
+ fieldDefs.forEach((fd) => {
+ map.set(fd.fieldRef.fieldName.toLowerCase(), fd);
+ });
+ return map;
+ },
+);
+
+export const labelDefs = createSelector(
+ [viewedConfig, viewedCustomPermissions],
+ (config, permissions) => [
+ ...((config && config.labelDefs) || []),
+ ...restrictionLabelsForPermissions(permissions),
+ ],
+);
+
+// labelDefs stored in an easily findable format with label names as keys.
+export const labelDefMap = createSelector(
+ labelDefs, (labelDefs) => {
+ const map = new Map();
+ labelDefs.forEach((ld) => {
+ map.set(ld.label.toLowerCase(), ld);
+ });
+ return map;
+ },
+);
+
+/**
+ * A selector that builds a map where keys are label prefixes
+ * and values equal to sets of possible values corresponding to the prefix
+ * @param {Object} state Current Redux state.
+ * @return {Map}
+ */
+export const labelPrefixValueMap = createSelector(labelDefs, (labelDefs) => {
+ const prefixMap = new Map();
+ labelDefs.forEach((ld) => {
+ const prefixes = labelNameToLabelPrefixes(ld.label);
+
+ prefixes.forEach((prefix) => {
+ if (prefixMap.has(prefix)) {
+ prefixMap.get(prefix).add(labelNameToLabelValue(ld.label, prefix));
+ } else {
+ prefixMap.set(prefix, new Set(
+ [labelNameToLabelValue(ld.label, prefix)]));
+ }
+ });
+ });
+
+ return prefixMap;
+});
+
+/**
+ * A selector that builds an array of label prefixes, keeping casing intact
+ * Some labels are implicitly used as custom fields in the grid and list view.
+ * Only labels with more than one option are included, to reduce noise.
+ * @param {Object} state Current Redux state.
+ * @return {Array}
+ */
+export const labelPrefixFields = createSelector(
+ labelPrefixValueMap, (map) => {
+ const prefixes = [];
+
+ map.forEach((options, prefix) => {
+ // Ignore label prefixes with only one value.
+ if (options.size > 1) {
+ prefixes.push(prefix);
+ }
+ });
+
+ return prefixes;
+ },
+);
+
+/**
+ * A selector that wraps labelPrefixFields arrays as set for fast lookup.
+ * @param {Object} state Current Redux state.
+ * @return {Set}
+ */
+export const labelPrefixSet = createSelector(
+ labelPrefixFields, (fields) => new Set(fields.map(
+ (field) => field.toLowerCase())),
+);
+
+export const enumFieldDefs = createSelector(
+ fieldDefs,
+ (fieldDefs) => {
+ return fieldDefs.filter(
+ (fd) => fd.fieldRef.type === fieldTypes.ENUM_TYPE);
+ },
+);
+
+/**
+ * A selector that builds a function that's used to compute the value of
+ * a given field name on a given issue. This function abstracts the difference
+ * between custom fields, built-in fields, and implicit fields created
+ * from labels and considers these values in the context of the current
+ * project configuration.
+ * @param {Object} state Current Redux state.
+ * @return {function(Issue, string): Array<string>} A function that processes a
+ * given issue and field name to find the string value for that field, in
+ * the issue.
+ */
+export const extractFieldValuesFromIssue = createSelector(
+ viewedProjectName,
+ (projectName) => (issue, fieldName) =>
+ stringValuesForIssueField(issue, fieldName, projectName),
+);
+
+/**
+ * A selector that builds a function that's used to compute the type of a given
+ * field name.
+ * @param {Object} state Current Redux state.
+ * @return {function(string): string}
+ */
+export const extractTypeForFieldName = createSelector(fieldDefMap,
+ (fieldDefMap) => {
+ return (fieldName) => {
+ const key = fieldName.toLowerCase();
+
+ // If the field is a built in field. Default fields have precedence
+ // over custom fields.
+ if (defaultIssueFieldMap.hasOwnProperty(key)) {
+ return defaultIssueFieldMap[key].type;
+ }
+
+ // If the field is a custom field. Custom fields have precedence
+ // over label prefixes.
+ if (fieldDefMap.has(key)) {
+ return fieldDefMap.get(key).fieldRef.type;
+ }
+
+ // Default to STR_TYPE, including for label fields.
+ return fieldTypes.STR_TYPE;
+ };
+ },
+);
+
+export const optionsPerEnumField = createSelector(
+ enumFieldDefs,
+ labelDefs,
+ (fieldDefs, labelDefs) => {
+ const map = new Map(fieldDefs.map(
+ (fd) => [fd.fieldRef.fieldName.toLowerCase(), []]));
+ labelDefs.forEach((ld) => {
+ const labelName = ld.label;
+
+ const fd = fieldDefs.find((fd) => hasPrefix(
+ labelName, fieldNameToLabelPrefix(fd.fieldRef.fieldName)));
+ if (fd) {
+ const key = fd.fieldRef.fieldName.toLowerCase();
+ map.get(key).push({
+ ...ld,
+ optionName: removePrefix(labelName,
+ fieldNameToLabelPrefix(fd.fieldRef.fieldName)),
+ });
+ }
+ });
+ return map;
+ },
+);
+
+export const fieldDefsForPhases = createSelector(
+ fieldDefs,
+ (fieldDefs) => {
+ if (!fieldDefs) return [];
+ return fieldDefs.filter((f) => f.isPhaseField);
+ },
+);
+
+export const fieldDefsByApprovalName = createSelector(
+ fieldDefs,
+ (fieldDefs) => {
+ if (!fieldDefs) return new Map();
+ const acc = new Map();
+ for (const fd of fieldDefs) {
+ if (fd.fieldRef && fd.fieldRef.approvalName) {
+ if (acc.has(fd.fieldRef.approvalName)) {
+ acc.get(fd.fieldRef.approvalName).push(fd);
+ } else {
+ acc.set(fd.fieldRef.approvalName, [fd]);
+ }
+ }
+ }
+ return acc;
+ },
+);
+
+export const fetchingConfig = (state) => {
+ return state.projectV0.requests.fetchConfig.requesting;
+};
+
+/**
+ * Shorthand method for detecting whether we are currently
+ * fetching presentationConcifg
+ * @param {Object} state Current Redux state.
+ * @return {boolean}
+ */
+export const fetchingPresentationConfig = (state) => {
+ return state.projectV0.requests.fetchPresentationConfig.requesting;
+};
+
+// Action Creators
+/**
+ * Action creator to set the currently viewed Project.
+ * @param {string} projectName The name of the Project to select.
+ * @return {function(function): Promise<void>}
+ */
+export const select = (projectName) => {
+ return (dispatch) => dispatch({type: SELECT, projectName});
+};
+
+/**
+ * Fetches data required to view project.
+ * @param {string} projectName
+ * @return {function(function): Promise<void>}
+ */
+export const fetch = (projectName) => async (dispatch) => {
+ const configPromise = dispatch(fetchConfig(projectName));
+ const visibleMembersPromise = dispatch(fetchVisibleMembers(projectName));
+
+ dispatch(fetchPresentationConfig(projectName));
+ dispatch(fetchTemplates(projectName));
+
+ const customPermissionsPromise = dispatch(
+ fetchCustomPermissions(projectName));
+
+ // TODO(crbug.com/monorail/5828): Remove window.TKR_populateAutocomplete once
+ // the old autocomplete code is deprecated.
+ const [config, visibleMembers, customPermissions] = await Promise.all([
+ configPromise,
+ visibleMembersPromise,
+ customPermissionsPromise]);
+ config.labelDefs = [...config.labelDefs,
+ ...restrictionLabelsForPermissions(customPermissions)];
+ dispatch(fetchFieldPerms(config.projectName, config.fieldDefs));
+ // eslint-disable-next-line new-cap
+ window.TKR_populateAutocomplete(config, visibleMembers, customPermissions);
+};
+
+/**
+ * Fetches project configuration including things like the custom fields in a
+ * project, the statuses, etc.
+ * @param {string} projectName
+ * @return {function(function): Promise<Config>}
+ */
+const fetchConfig = (projectName) => async (dispatch) => {
+ dispatch({type: FETCH_CONFIG_START});
+
+ const getConfig = prpcClient.call(
+ 'monorail.Projects', 'GetConfig', {projectName});
+
+ try {
+ const config = await getConfig;
+ dispatch({type: FETCH_CONFIG_SUCCESS, projectName, config});
+ return config;
+ } catch (error) {
+ dispatch({type: FETCH_CONFIG_FAILURE, error});
+ }
+};
+
+export const fetchPresentationConfig = (projectName) => async (dispatch) => {
+ dispatch({type: FETCH_PRESENTATION_CONFIG_START});
+
+ try {
+ const presentationConfig = await prpcClient.call(
+ 'monorail.Projects', 'GetPresentationConfig', {projectName});
+ dispatch({
+ type: FETCH_PRESENTATION_CONFIG_SUCCESS,
+ projectName,
+ presentationConfig,
+ });
+ } catch (error) {
+ dispatch({type: FETCH_PRESENTATION_CONFIG_FAILURE, error});
+ }
+};
+
+/**
+ * Fetches custom permissions defined for a project.
+ * @param {string} projectName
+ * @return {function(function): Promise<Array<string>>}
+ */
+export const fetchCustomPermissions = (projectName) => async (dispatch) => {
+ dispatch({type: FETCH_CUSTOM_PERMISSIONS_START});
+
+ try {
+ const {permissions} = await prpcClient.call(
+ 'monorail.Projects', 'GetCustomPermissions', {projectName});
+ dispatch({
+ type: FETCH_CUSTOM_PERMISSIONS_SUCCESS,
+ projectName,
+ permissions,
+ });
+ return permissions;
+ } catch (error) {
+ dispatch({type: FETCH_CUSTOM_PERMISSIONS_FAILURE, error});
+ }
+};
+
+/**
+ * Fetches the project members that the user is able to view.
+ * @param {string} projectName
+ * @return {function(function): Promise<GetVisibleMembersResponse>}
+ */
+export const fetchVisibleMembers = (projectName) => async (dispatch) => {
+ dispatch({type: FETCH_VISIBLE_MEMBERS_START});
+
+ try {
+ const visibleMembers = await prpcClient.call(
+ 'monorail.Projects', 'GetVisibleMembers', {projectName});
+ dispatch({
+ type: FETCH_VISIBLE_MEMBERS_SUCCESS,
+ projectName,
+ visibleMembers,
+ });
+ return visibleMembers;
+ } catch (error) {
+ dispatch({type: FETCH_VISIBLE_MEMBERS_FAILURE, error});
+ }
+};
+
+const fetchTemplates = (projectName) => async (dispatch) => {
+ dispatch({type: FETCH_TEMPLATES_START});
+
+ const listTemplates = prpcClient.call(
+ 'monorail.Projects', 'ListProjectTemplates', {projectName});
+
+ // TODO(zhangtiff): Remove (see above TODO).
+ if (!listTemplates) return;
+
+ try {
+ const resp = await listTemplates;
+ dispatch({
+ type: FETCH_TEMPLATES_SUCCESS,
+ projectName,
+ templates: resp.templates,
+ });
+ } catch (error) {
+ dispatch({type: FETCH_TEMPLATES_FAILURE, error});
+ }
+};
+
+// Helpers
+
+/**
+ * Helper to fetch field permissions.
+ * @param {string} projectName The name of the project where the fields are.
+ * @param {Array<FieldDef>} fieldDefs
+ * @return {function(function): Promise<void>}
+ */
+export const fetchFieldPerms = (projectName, fieldDefs) => async (dispatch) => {
+ const fieldDefsNames = [];
+ if (fieldDefs) {
+ fieldDefs.forEach((fd) => {
+ const fieldDefName = fieldDefToName(projectName, fd);
+ fieldDefsNames.push(fieldDefName);
+ });
+ }
+ await dispatch(permissions.batchGet(fieldDefsNames));
+};