Convert files to typescript
The change converts the following files to typescript:
* elements/admin/gr-permission/gr-permission.ts
* elements/admin/gr-repo-access/gr-repo-access.ts
* elements/admin/gr-access-section/gr-access-section.ts
Change-Id: I14bddac6260c41897baf6eb1c9aa79349fc1684a
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index c5d24db..3c155f85 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -14,18 +14,35 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '@polymer/iron-input/iron-input.js';
-import '../../../styles/gr-form-styles.js';
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-icons/gr-icons.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-permission/gr-permission.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {htmlTemplate} from './gr-access-section_html.js';
-import {AccessPermissions, toSortedPermissionsArray} from '../../../utils/access-util.js';
+import '@polymer/iron-input/iron-input';
+import '../../../styles/gr-form-styles';
+import '../../../styles/shared-styles';
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icons/gr-icons';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../gr-permission/gr-permission';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {htmlTemplate} from './gr-access-section_html';
+import {
+ AccessPermissions,
+ PermissionArray,
+ PermissionArrayItem,
+ toSortedPermissionsArray,
+} from '../../../utils/access-util';
+import {customElement, property} from '@polymer/decorators';
+import {
+ EditablePermissionInfo,
+ PermissionAccessSection,
+ EditableProjectAccessGroups,
+} from '../gr-repo-access/gr-repo-access-interfaces';
+import {
+ CapabilityInfoMap,
+ GitRef,
+ LabelNameToLabelTypeInfoMap,
+} from '../../../types/common';
+import {PolymerDomRepeatEvent} from '../../../types/types';
/**
* Fired when the section has been modified or removed.
@@ -47,81 +64,98 @@
const ON_BEHALF_OF = '(On Behalf Of)';
const LABEL = 'Label';
-/**
- * @extends PolymerElement
- */
-class GrAccessSection extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
+export interface GrAccessSection {
+ $: {
+ permissionSelect: HTMLSelectElement;
+ };
+}
- static get is() { return 'gr-access-section'; }
-
- static get properties() {
- return {
- capabilities: Object,
- /** @type {?} */
- section: {
- type: Object,
- notify: true,
- observer: '_updateSection',
- },
- groups: Object,
- labels: Object,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- canUpload: Boolean,
- ownerOf: Array,
- _originalId: String,
- _editingRef: {
- type: Boolean,
- value: false,
- },
- _deleted: {
- type: Boolean,
- value: false,
- },
- _permissions: Array,
- };
+@customElement('gr-access-section')
+export class GrAccessSection extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
}
+ @property({type: Object})
+ capabilities?: CapabilityInfoMap;
+
+ @property({type: Object, notify: true, observer: '_updateSection'})
+ section?: PermissionAccessSection;
+
+ @property({type: Object})
+ groups?: EditableProjectAccessGroups;
+
+ @property({type: Object})
+ labels?: LabelNameToLabelTypeInfoMap;
+
+ @property({type: Boolean, observer: '_handleEditingChanged'})
+ editing = false;
+
+ @property({type: Boolean})
+ canUpload?: boolean;
+
+ @property({type: Array})
+ ownerOf?: GitRef[];
+
+ @property({type: String})
+ _originalId?: GitRef;
+
+ @property({type: Boolean})
+ _editingRef = false;
+
+ @property({type: Boolean})
+ _deleted = false;
+
+ @property({type: Array})
+ _permissions?: PermissionArray<EditablePermissionInfo>;
+
/** @override */
created() {
super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
+ this.addEventListener('access-saved', () => this._handleAccessSaved());
}
- _updateSection(section) {
+ _updateSection(section: PermissionAccessSection) {
this._permissions = toSortedPermissionsArray(section.value.permissions);
- this._originalId = section.id;
+ this._originalId = section.id as GitRef;
}
_handleAccessSaved() {
+ if (!this.section) {
+ return;
+ }
// Set a new 'original' value to keep track of after the value has been
// saved.
this._updateSection(this.section);
}
_handleValueChange() {
+ if (!this.section) {
+ return;
+ }
if (!this.section.value.added) {
this.section.value.modified = this.section.id !== this._originalId;
// Allows overall access page to know a change has been made.
// For a new section, this is not fired because new permissions and
// rules have to be added in order to save, modifying the ref is not
// enough.
- this.dispatchEvent(new CustomEvent(
- 'access-modified', {bubbles: true, composed: true}));
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true})
+ );
}
this.section.value.updatedId = this.section.id;
}
- _handleEditingChanged(editing, editingOld) {
+ _handleEditingChanged(editing: boolean, editingOld: boolean) {
// Ignore when editing gets set initially.
- if (!editingOld) { return; }
+ if (!editingOld) {
+ return;
+ }
+ if (!this.section || !this._permissions) {
+ return;
+ }
// Restore original values if no longer editing.
if (!editing) {
this._editingRef = false;
@@ -139,9 +173,14 @@
}
}
- _computePermissions(name, capabilities, labels) {
+ _computePermissions(
+ name: string,
+ capabilities?: CapabilityInfoMap,
+ labels?: LabelNameToLabelTypeInfoMap
+ ) {
let allPermissions;
- if (!this.section || !this.section.value) {
+ const section = this.section;
+ if (!section || !section.value) {
return [];
}
if (name === GLOBAL_NAME) {
@@ -149,25 +188,33 @@
} else {
const labelOptions = this._computeLabelOptions(labels);
allPermissions = labelOptions.concat(
- toSortedPermissionsArray(AccessPermissions));
+ toSortedPermissionsArray(AccessPermissions)
+ );
}
- return allPermissions
- .filter(permission => !this.section.value.permissions[permission.id]);
+ return allPermissions.filter(
+ permission => !section.value.permissions[permission.id]
+ );
}
- _computeHideEditClass(section) {
+ _computeHideEditClass(section: PermissionAccessSection) {
return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
}
- _handleAddedPermissionRemoved(e) {
+ _handleAddedPermissionRemoved(e: PolymerDomRepeatEvent) {
+ if (!this._permissions) {
+ return;
+ }
const index = e.model.index;
- this._permissions = this._permissions.slice(0, index).concat(
- this._permissions.slice(index + 1, this._permissions.length));
+ this._permissions = this._permissions
+ .slice(0, index)
+ .concat(this._permissions.slice(index + 1, this._permissions.length));
}
- _computeLabelOptions(labels) {
+ _computeLabelOptions(labels?: LabelNameToLabelTypeInfoMap) {
const labelOptions = [];
- if (!labels) { return []; }
+ if (!labels) {
+ return [];
+ }
for (const labelName of Object.keys(labels)) {
labelOptions.push({
id: 'label-' + labelName,
@@ -187,7 +234,11 @@
return labelOptions;
}
- _computePermissionName(name, permission, capabilities) {
+ _computePermissionName(
+ name: string,
+ permission: PermissionArrayItem<EditablePermissionInfo>,
+ capabilities: CapabilityInfoMap
+ ) {
if (name === GLOBAL_NAME) {
return capabilities[permission.id].name;
} else if (AccessPermissions[permission.id]) {
@@ -199,9 +250,10 @@
}
return `${LABEL} ${permission.value.label}${behalfOf}`;
}
+ return undefined;
}
- _computeSectionName(name) {
+ _computeSectionName(name: string) {
// When a new section is created, it doesn't yet have a ref. Set into
// edit mode so that the user can input one.
if (!name) {
@@ -220,25 +272,38 @@
}
_handleRemoveReference() {
+ if (!this.section) {
+ return;
+ }
if (this.section.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-section-removed', {bubbles: true, composed: true}));
+ this.dispatchEvent(
+ new CustomEvent('added-section-removed', {
+ bubbles: true,
+ composed: true,
+ })
+ );
}
this._deleted = true;
this.section.value.deleted = true;
this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ new CustomEvent('access-modified', {bubbles: true, composed: true})
+ );
}
_handleUndoRemove() {
+ if (!this.section) {
+ return;
+ }
this._deleted = false;
delete this.section.value.deleted;
}
editRefInput() {
- return this.root.querySelector(PolymerElement ?
- 'iron-input.editRefInput' :
- 'input[is=iron-input].editRefInput');
+ return this.root!.querySelector(
+ PolymerElement
+ ? 'iron-input.editRefInput'
+ : 'input[is=iron-input].editRefInput'
+ ) as HTMLInputElement;
}
editReference() {
@@ -246,14 +311,27 @@
this.editRefInput().focus();
}
- _isEditEnabled(canUpload, ownerOf, sectionId) {
+ _isEditEnabled(
+ canUpload: boolean | undefined,
+ ownerOf: GitRef[] | undefined,
+ sectionId: GitRef
+ ) {
return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
}
- _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
+ _computeSectionClass(
+ editing: boolean,
+ canUpload: boolean | undefined,
+ ownerOf: GitRef[] | undefined,
+ editingRef: boolean,
+ deleted: boolean
+ ) {
const classList = [];
- if (editing
- && this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
+ if (
+ editing &&
+ this.section &&
+ this._isEditEnabled(canUpload, ownerOf, this.section.id as GitRef)
+ ) {
classList.push('editing');
}
if (editingRef) {
@@ -265,13 +343,13 @@
return classList.join(' ');
}
- _computeEditBtnClass(name) {
+ _computeEditBtnClass(name: string) {
return name === GLOBAL_NAME ? 'global' : '';
}
_handleAddPermission() {
const value = this.$.permissionSelect.value;
- const permission = {
+ const permission: PermissionArrayItem<EditablePermissionInfo> = {
id: value,
value: {rules: {}, added: true},
};
@@ -292,15 +370,19 @@
// When we add a new item, we have to push the new permission in the same
// format as the ones that have been returned by the API.
if (value.startsWith('label')) {
- permission.value.label =
- value.replace('label-', '').replace('labelAs-', '');
+ permission.value.label = value
+ .replace('label-', '')
+ .replace('labelAs-', '');
}
// Add to the end of the array (used in dom-repeat) and also to the
// section object that is two way bound with its parent element.
this.push('_permissions', permission);
- this.set(['section.value.permissions', permission.id],
- permission.value);
+ this.set(['section.value.permissions', permission.id], permission.value);
}
}
-customElements.define(GrAccessSection.is, GrAccessSection);
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-access-section': GrAccessSection;
+ }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index 9675c92..27e6f32 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -15,27 +15,75 @@
* limitations under the License.
*/
-import '@polymer/paper-toggle-button/paper-toggle-button.js';
-import '../../../styles/gr-form-styles.js';
-import '../../../styles/gr-menu-page-styles.js';
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-autocomplete/gr-autocomplete.js';
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-rule-editor/gr-rule-editor.js';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-permission_html.js';
-import {toSortedPermissionsArray} from '../../../utils/access-util.js';
+import '@polymer/paper-toggle-button/paper-toggle-button';
+import '../../../styles/gr-form-styles';
+import '../../../styles/gr-menu-page-styles';
+import '../../../styles/shared-styles';
+import '../../shared/gr-autocomplete/gr-autocomplete';
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../gr-rule-editor/gr-rule-editor';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-permission_html';
+import {
+ toSortedPermissionsArray,
+ PermissionArrayItem,
+ PermissionArray,
+} from '../../../utils/access-util';
+import {customElement, property, observe} from '@polymer/decorators';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {hasOwnProperty} from '../../../utils/common-util';
+import {
+ LabelNameToLabelTypeInfoMap,
+ LabelTypeInfoValues,
+ GroupInfo,
+ ProjectAccessGroups,
+ GroupId,
+ GitRef,
+} from '../../../types/common';
+import {
+ AutocompleteQuery,
+ GrAutocomplete,
+ AutocompleteSuggestion,
+ AutocompleteCommitEvent,
+} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {
+ EditablePermissionInfo,
+ EditablePermissionRuleInfo,
+ EditableProjectAccessGroups,
+} from '../gr-repo-access/gr-repo-access-interfaces';
+import {PolymerDomRepeatEvent} from '../../../types/types';
const MAX_AUTOCOMPLETE_RESULTS = 20;
-const RANGE_NAMES = [
- 'QUERY LIMIT',
- 'BATCH CHANGES LIMIT',
-];
+const RANGE_NAMES = ['QUERY LIMIT', 'BATCH CHANGES LIMIT'];
+
+type GroupsWithRulesMap = {[ruleId: string]: boolean};
+
+export interface GrPermission {
+ $: {
+ restAPI: RestApiService & Element;
+ groupAutocomplete: GrAutocomplete;
+ };
+}
+
+interface ComputedLabelValue {
+ value: number;
+ text: string;
+}
+
+interface ComputedLabel {
+ name: string;
+ values: ComputedLabelValue[];
+}
+
+interface GroupSuggestion {
+ name: string;
+ value: GroupInfo;
+}
/**
* Fired when the permission has been modified or removed.
@@ -46,64 +94,63 @@
* Fired when a permission that was previously added was removed.
*
* @event added-permission-removed
- * @extends PolymerElement
*/
-class GrPermission extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-permission'; }
-
- static get properties() {
- return {
- labels: Object,
- name: String,
- /** @type {?} */
- permission: {
- type: Object,
- observer: '_sortPermission',
- notify: true,
- },
- groups: Object,
- section: String,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- _label: {
- type: Object,
- computed: '_computeLabel(permission, labels)',
- },
- _groupFilter: String,
- _query: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- _rules: Array,
- _groupsWithRules: Object,
- _deleted: {
- type: Boolean,
- value: false,
- },
- _originalExclusiveValue: Boolean,
- };
+@customElement('gr-permission')
+export class GrPermission extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
}
- static get observers() {
- return [
- '_handleRulesChanged(_rules.splices)',
- ];
+ @property({type: Object})
+ labels?: LabelNameToLabelTypeInfoMap;
+
+ @property({type: String})
+ name?: string;
+
+ @property({type: Object, observer: '_sortPermission', notify: true})
+ permission?: PermissionArrayItem<EditablePermissionInfo>;
+
+ @property({type: Object})
+ groups?: EditableProjectAccessGroups;
+
+ @property({type: String})
+ section?: GitRef;
+
+ @property({type: Boolean, observer: '_handleEditingChanged'})
+ editing = false;
+
+ @property({type: Object, computed: '_computeLabel(permission, labels)'})
+ _label?: ComputedLabel;
+
+ @property({type: String})
+ _groupFilter?: string;
+
+ @property({type: Object})
+ _query: AutocompleteQuery;
+
+ @property({type: Array})
+ _rules?: PermissionArray<EditablePermissionRuleInfo>;
+
+ @property({type: Object})
+ _groupsWithRules?: GroupsWithRulesMap;
+
+ @property({type: Boolean})
+ _deleted = false;
+
+ @property({type: Boolean})
+ _originalExclusiveValue?: boolean;
+
+ constructor() {
+ super();
+ this._query = () => this._getGroupSuggestions();
}
/** @override */
created() {
super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
+ this.addEventListener('access-saved', () => this._handleAccessSaved());
}
/** @override */
@@ -113,7 +160,9 @@
}
_setupValues() {
- if (!this.permission) { return; }
+ if (!this.permission) {
+ return;
+ }
this._originalExclusiveValue = !!this.permission.value.exclusive;
flush();
}
@@ -124,13 +173,19 @@
this._setupValues();
}
- _permissionIsOwnerOrGlobal(permissionId, section) {
+ _permissionIsOwnerOrGlobal(permissionId: string, section: string) {
return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
}
- _handleEditingChanged(editing, editingOld) {
+ _handleEditingChanged(editing: boolean, editingOld: boolean) {
// Ignore when editing gets set initially.
- if (!editingOld) { return; }
+ if (!editingOld) {
+ return;
+ }
+ if (!this.permission || !this._rules) {
+ return;
+ }
+
// Restore original values if no longer editing.
if (!editing) {
this._deleted = false;
@@ -144,45 +199,67 @@
}
// Restore exclusive bit to original.
- this.set(['permission', 'value', 'exclusive'],
- this._originalExclusiveValue);
+ this.set(
+ ['permission', 'value', 'exclusive'],
+ this._originalExclusiveValue
+ );
}
}
- _handleAddedRuleRemoved(e) {
+ _handleAddedRuleRemoved(e: PolymerDomRepeatEvent) {
+ if (!this._rules) {
+ return;
+ }
const index = e.model.index;
- this._rules = this._rules.slice(0, index)
- .concat(this._rules.slice(index + 1, this._rules.length));
+ this._rules = this._rules
+ .slice(0, index)
+ .concat(this._rules.slice(index + 1, this._rules.length));
}
_handleValueChange() {
+ if (!this.permission) {
+ return;
+ }
this.permission.value.modified = true;
// Allows overall access page to know a change has been made.
this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ new CustomEvent('access-modified', {bubbles: true, composed: true})
+ );
}
_handleRemovePermission() {
+ if (!this.permission) {
+ return;
+ }
if (this.permission.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-permission-removed', {bubbles: true, composed: true}));
+ this.dispatchEvent(
+ new CustomEvent('added-permission-removed', {
+ bubbles: true,
+ composed: true,
+ })
+ );
}
this._deleted = true;
this.permission.value.deleted = true;
this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ new CustomEvent('access-modified', {bubbles: true, composed: true})
+ );
}
- _handleRulesChanged(changeRecord) {
+ @observe('_rules.splices')
+ _handleRulesChanged() {
+ if (!this._rules) {
+ return;
+ }
// Update the groups to exclude in the autocomplete.
this._groupsWithRules = this._computeGroupsWithRules(this._rules);
}
- _sortPermission(permission) {
+ _sortPermission(permission: PermissionArrayItem<EditablePermissionInfo>) {
this._rules = toSortedPermissionsArray(permission.value.rules);
}
- _computeSectionClass(editing, deleted) {
+ _computeSectionClass(editing: boolean, deleted: boolean) {
const classList = [];
if (editing) {
classList.push('editing');
@@ -194,33 +271,51 @@
}
_handleUndoRemove() {
+ if (!this.permission) {
+ return;
+ }
this._deleted = false;
delete this.permission.value.deleted;
}
- _computeLabel(permission, labels) {
- if (!labels || !permission ||
- !permission.value || !permission.value.label) { return; }
+ _computeLabel(
+ permission?: PermissionArrayItem<EditablePermissionInfo>,
+ labels?: LabelNameToLabelTypeInfoMap
+ ): ComputedLabel | undefined {
+ if (
+ !labels ||
+ !permission ||
+ !permission.value ||
+ !permission.value.label
+ ) {
+ return;
+ }
const labelName = permission.value.label;
// It is possible to have a label name that is not included in the
// 'labels' object. In this case, treat it like anything else.
- if (!labels[labelName]) { return; }
+ if (!labels[labelName]) {
+ return;
+ }
return {
name: labelName,
values: this._computeLabelValues(labels[labelName].values),
};
}
- _computeLabelValues(values) {
- const valuesArr = [];
- const keys = Object.keys(values)
- .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
+ _computeLabelValues(values: LabelTypeInfoValues): ComputedLabelValue[] {
+ const valuesArr: ComputedLabelValue[] = [];
+ const keys = Object.keys(values).sort(
+ // TODO(TS): change parseInto to Number(...) according to typescript style guide
+ (a, b) => parseInt(a, 10) - parseInt(b, 10)
+ );
for (const key of keys) {
let text = values[key];
- if (!text) { text = ''; }
+ if (!text) {
+ text = '';
+ }
// The value from the server being used to choose which item is
// selected is in integer form, so this must be converted.
valuesArr.push({value: parseInt(key, 10), text});
@@ -228,58 +323,71 @@
return valuesArr;
}
- /**
- * @param {!Array} rules
- * @return {!Object} Object with groups with rues as keys, and true as
- * value.
- */
- _computeGroupsWithRules(rules) {
- const groups = {};
+ _computeGroupsWithRules(
+ rules: PermissionArray<EditablePermissionRuleInfo>
+ ): GroupsWithRulesMap {
+ const groups: GroupsWithRulesMap = {};
for (const rule of rules) {
groups[rule.id] = true;
}
return groups;
}
- _computeGroupName(groups, groupId) {
- return groups && groups[groupId] && groups[groupId].name ?
- groups[groupId].name : groupId;
+ _computeGroupName(groups: ProjectAccessGroups, groupId: GroupId) {
+ return groups && groups[groupId] && groups[groupId].name
+ ? groups[groupId].name
+ : groupId;
}
- _getGroupSuggestions() {
- return this.$.restAPI.getSuggestedGroups(
- this._groupFilter,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: response[key],
- });
+ _getGroupSuggestions(): Promise<AutocompleteSuggestion[]> {
+ return this.$.restAPI
+ .getSuggestedGroups(this._groupFilter || '', MAX_AUTOCOMPLETE_RESULTS)
+ .then(response => {
+ const groups: GroupSuggestion[] = [];
+ for (const key in response) {
+ if (!hasOwnProperty(response, key)) {
+ continue;
}
- // Does not return groups in which we already have rules for.
- return groups
- .filter(group => !this._groupsWithRules[group.value.id]);
- });
+ groups.push({
+ name: key,
+ value: response[key],
+ });
+ }
+ // Does not return groups in which we already have rules for.
+ return groups
+ .filter(
+ group =>
+ this._groupsWithRules && !this._groupsWithRules[group.value.id]
+ )
+ .map((group: GroupSuggestion) => {
+ const autocompleteSuggestion: AutocompleteSuggestion = {
+ name: group.name,
+ value: group.value.id,
+ };
+ return autocompleteSuggestion;
+ });
+ });
}
/**
* Handles adding a skeleton item to the dom-repeat.
* gr-rule-editor handles setting the default values.
*/
- _handleAddRuleItem(e) {
+ _handleAddRuleItem(e: AutocompleteCommitEvent) {
+ if (!this.permission || !this._rules) {
+ return;
+ }
+
// The group id is encoded, but have to decode in order for the access
// API to work as expected.
- const groupId = decodeURIComponent(e.detail.value.id)
- .replace(/\+/g, ' ');
+ const groupId = decodeURIComponent(e.detail.value).replace(/\+/g, ' ');
// We cannot use "this.set(...)" here, because groupId may contain dots,
// and dots in property path names are totally unsupported by Polymer.
// Apparently Polymer picks up this change anyway, otherwise we should
// have looked at using MutableData:
// https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
- this.permission.value.rules[groupId] = {};
+ // Actual value assigned below, after the flush
+ this.permission.value.rules[groupId] = {} as EditablePermissionRuleInfo;
// Purposely don't recompute sorted array so that the newly added rule
// is the last item of the array.
@@ -303,11 +411,14 @@
// See comment above for why we cannot use "this.set(...)" here.
this.permission.value.rules[groupId] = value;
this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ new CustomEvent('access-modified', {bubbles: true, composed: true})
+ );
}
- _computeHasRange(name) {
- if (!name) { return false; }
+ _computeHasRange(name: string) {
+ if (!name) {
+ return false;
+ }
return RANGE_NAMES.includes(name.toUpperCase());
}
@@ -315,9 +426,13 @@
/**
* Work around a issue on iOS when clicking turns into double tap
*/
- _onTapExclusiveToggle(e) {
+ _onTapExclusiveToggle(e: Event) {
e.preventDefault();
}
}
-customElements.define(GrPermission.is, GrPermission);
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-permission': GrPermission;
+ }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
index 835c90a..d0d04f4 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
@@ -193,10 +193,10 @@
assert.deepEqual(groups, [
{
name: 'Administrators',
- value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
+ value: '4c97682e6ce61b7247f3381b6f1789356666de7f',
}, {
name: 'Anonymous Users',
- value: {id: 'global%3AAnonymous-Users'},
+ value: 'global%3AAnonymous-Users',
},
]);
done();
@@ -211,7 +211,7 @@
element._getGroupSuggestions().then(groups => {
assert.deepEqual(groups, [{
name: 'Anonymous Users',
- value: {id: 'global%3AAnonymous-Users'},
+ value: 'global%3AAnonymous-Users',
}]);
done();
});
@@ -293,9 +293,7 @@
element.$.groupAutocomplete.text = 'ldap/tests te.st';
const e = {
detail: {
- value: {
- id: 'ldap:CN=test+te.st',
- },
+ value: 'ldap:CN=test+te.st',
},
};
element.editing = true;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
new file mode 100644
index 0000000..40a1e0a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+/**
+ * @fileOverview This file contains interfaces shared between gr-repo-access
+ * and nested elements (gr-access-section, gr-permission)
+ */
+
+import {
+ AccessSectionInfo,
+ GroupInfo,
+ PermissionInfo,
+ PermissionRuleInfo,
+} from '../../../types/common';
+import {PermissionArrayItem} from '../../../utils/access-util';
+
+export type PrimitiveValue = string | boolean | number | undefined;
+
+export interface PropertyTreeNode {
+ [propName: string]: PropertyTreeNode | PrimitiveValue;
+ deleted?: boolean;
+ modified?: boolean;
+ added?: boolean;
+ updatedId?: string;
+}
+
+/**
+ * EditableLocalAccessSectionInfo is exactly the same as LocalAccessSectionInfo,
+ * but with additional properties: each nested object additionally implements
+ * interface PropertyTreeNode
+ */
+
+export type EditableLocalAccessSectionInfo = {
+ [ref: string]: EditableAccessSectionInfo;
+};
+
+export interface EditableAccessSectionInfo
+ extends AccessSectionInfo,
+ PropertyTreeNode {
+ permissions: EditableAccessPermissionsMap;
+}
+
+export type EditableAccessPermissionsMap = {
+ [permissionName: string]: EditablePermissionInfo;
+};
+
+export interface EditablePermissionInfo
+ extends PermissionInfo,
+ PropertyTreeNode {
+ rules: EditablePermissionInfoRules;
+}
+
+export type EditablePermissionInfoRules = {
+ [groupUUID: string]: EditablePermissionRuleInfo;
+};
+
+export interface EditablePermissionRuleInfo
+ extends PermissionRuleInfo,
+ PropertyTreeNode {}
+
+export type PermissionAccessSection = PermissionArrayItem<
+ EditableAccessSectionInfo
+>;
+
+export interface NewlyAddedGroupInfo {
+ name: string;
+}
+export type EditableProjectAccessGroups = {
+ [uuid: string]: GroupInfo | NewlyAddedGroupInfo;
+};
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 086d817..b96ec2c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -14,206 +14,219 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-menu-page-styles.js';
-import '../../../styles/gr-subpage-styles.js';
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-access-section/gr-access-section.js';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-repo-access_html.js';
+import '../../../styles/gr-menu-page-styles';
+import '../../../styles/gr-subpage-styles';
+import '../../../styles/shared-styles';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../gr-access-section/gr-access-section';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-repo-access_html';
+import {encodeURL, getBaseUrl, singleDecodeURL} from '../../../utils/url-util';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {toSortedPermissionsArray} from '../../../utils/access-util';
+import {customElement, property} from '@polymer/decorators';
import {
- encodeURL,
- getBaseUrl,
- singleDecodeURL,
-} from '../../../utils/url-util.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {toSortedPermissionsArray} from '../../../utils/access-util.js';
-
-const Defs = {};
+ RepoName,
+ ProjectInfo,
+ CapabilityInfoMap,
+ LabelNameToLabelTypeInfoMap,
+ ProjectAccessInput,
+ GitRef,
+ UrlEncodedRepoName,
+ ProjectAccessGroups,
+} from '../../../types/common';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {hasOwnProperty} from '../../../utils/common-util';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrAccessSection} from '../gr-access-section/gr-access-section';
+import {
+ AutocompleteQuery,
+ AutocompleteSuggestion,
+} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {
+ EditableLocalAccessSectionInfo,
+ PermissionAccessSection,
+ PropertyTreeNode,
+ PrimitiveValue,
+} from './gr-repo-access-interfaces';
const NOTHING_TO_SAVE = 'No changes to save.';
const MAX_AUTOCOMPLETE_RESULTS = 50;
+export interface GrRepoAccess {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+
/**
* Fired when save is a no-op
*
* @event show-alert
*/
+@customElement('gr-repo-access')
+export class GrRepoAccess extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
-/**
- * @typedef {{
- * value: !Object,
- * }}
- */
-Defs.rule;
+ @property({type: String, observer: '_repoChanged'})
+ repo?: RepoName;
-/**
- * @typedef {{
- * rules: !Object<string, Defs.rule>
- * }}
- */
-Defs.permission;
+ @property({type: String})
+ path?: string;
-/**
- * Can be an empty object or consist of permissions.
- *
- * @typedef {{
- * permissions: !Object<string, Defs.permission>
- * }}
- */
-Defs.permissions;
+ @property({type: Boolean})
+ _canUpload?: boolean = false; // restAPI can return undefined
-/**
- * Can be an empty object or consist of permissions.
- *
- * @typedef {!Object<string, Defs.permissions>}
- */
-Defs.sections;
+ @property({type: String})
+ _inheritFromFilter?: RepoName;
-/**
- * @typedef {{
- * remove: !Defs.sections,
- * add: !Defs.sections,
- * }}
- */
-Defs.projectAccessInput;
+ @property({type: Object})
+ _query: AutocompleteQuery;
-/**
- * @extends PolymerElement
- */
-class GrRepoAccess extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
+ @property({type: Array})
+ _ownerOf?: GitRef[];
- static get is() { return 'gr-repo-access'; }
+ @property({type: Object})
+ _capabilities?: CapabilityInfoMap;
- static get properties() {
- return {
- repo: {
- type: String,
- observer: '_repoChanged',
- },
- // The current path
- path: String,
+ @property({type: Object})
+ _groups?: ProjectAccessGroups;
- _canUpload: {
- type: Boolean,
- value: false,
- },
- _inheritFromFilter: String,
- _query: {
- type: Function,
- value() {
- return this._getInheritFromSuggestions.bind(this);
- },
- },
- _ownerOf: Array,
- _capabilities: Object,
- _groups: Object,
- /** @type {?} */
- _inheritsFrom: Object,
- _labels: Object,
- _local: Object,
- _editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- _modified: {
- type: Boolean,
- value: false,
- },
- _sections: Array,
- _weblinks: Array,
- _loading: {
- type: Boolean,
- value: true,
- },
- };
+ @property({type: Object})
+ _inheritsFrom?: ProjectInfo | null | {};
+
+ @property({type: Object})
+ _labels?: LabelNameToLabelTypeInfoMap;
+
+ @property({type: Object})
+ _local?: EditableLocalAccessSectionInfo;
+
+ @property({type: Boolean, observer: '_handleEditingChanged'})
+ _editing = false;
+
+ @property({type: Boolean})
+ _modified = false;
+
+ @property({type: Array})
+ _sections?: PermissionAccessSection[];
+
+ @property({type: Array})
+ _weblinks?: string[];
+
+ @property({type: Boolean})
+ _loading = true;
+
+ private _originalInheritsFrom?: ProjectInfo | null;
+
+ constructor() {
+ super();
+ this._query = () => this._getInheritFromSuggestions();
}
/** @override */
created() {
super.created();
- this.addEventListener('access-modified',
- () =>
- this._handleAccessModified());
+ this.addEventListener('access-modified', () =>
+ this._handleAccessModified()
+ );
}
_handleAccessModified() {
this._modified = true;
}
- /**
- * @param {string} repo
- * @return {!Promise}
- */
- _repoChanged(repo) {
+ _repoChanged(repo: RepoName) {
this._loading = true;
- if (!repo) { return Promise.resolve(); }
+ if (!repo) {
+ return Promise.resolve();
+ }
return this._reload(repo);
}
- _reload(repo) {
- const promises = [];
-
- const errFn = response => {
- this.dispatchEvent(new CustomEvent('page-error', {
- detail: {response},
- composed: true, bubbles: true,
- }));
+ _reload(repo: RepoName) {
+ const errFn = (response?: Response | null) => {
+ this.dispatchEvent(
+ new CustomEvent('page-error', {
+ detail: {response},
+ composed: true,
+ bubbles: true,
+ })
+ );
};
this._editing = false;
// Always reset sections when a project changes.
this._sections = [];
- promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
+ const sectionsPromises = this.$.restAPI
+ .getRepoAccessRights(repo, errFn)
+ .then(res => {
+ if (!res) {
+ return Promise.resolve(undefined);
+ }
- // Keep a copy of the original inherit from values separate from
- // the ones data bound to gr-autocomplete, so the original value
- // can be restored if the user cancels.
- this._inheritsFrom = res.inherits_from ? ({
- ...res.inherits_from}) : null;
- this._originalInheritsFrom = res.inherits_from ? ({
- ...res.inherits_from}) : null;
- // Initialize the filter value so when the user clicks edit, the
- // current value appears. If there is no parent repo, it is
- // initialized as an empty string.
- this._inheritFromFilter = res.inherits_from ?
- this._inheritsFrom.name : '';
- this._local = res.local;
- this._groups = res.groups;
- this._weblinks = res.config_web_links || [];
- this._canUpload = res.can_upload;
- this._ownerOf = res.owner_of || [];
- return toSortedPermissionsArray(this._local);
- }));
+ // Keep a copy of the original inherit from values separate from
+ // the ones data bound to gr-autocomplete, so the original value
+ // can be restored if the user cancels.
+ this._inheritsFrom = res.inherits_from
+ ? {
+ ...res.inherits_from,
+ }
+ : null;
+ this._originalInheritsFrom = res.inherits_from
+ ? {
+ ...res.inherits_from,
+ }
+ : null;
+ // Initialize the filter value so when the user clicks edit, the
+ // current value appears. If there is no parent repo, it is
+ // initialized as an empty string.
+ this._inheritFromFilter = res.inherits_from
+ ? res.inherits_from.name
+ : ('' as RepoName);
+ // 'as EditableLocalAccessSectionInfo' is required because res.local
+ // type doesn't have index signature
+ this._local = res.local as EditableLocalAccessSectionInfo;
+ this._groups = res.groups;
+ this._weblinks = res.config_web_links || [];
+ this._canUpload = res.can_upload;
+ this._ownerOf = res.owner_of || [];
+ return toSortedPermissionsArray(this._local);
+ });
- promises.push(this.$.restAPI.getCapabilities(errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
+ const capabilitiesPromises = this.$.restAPI
+ .getCapabilities(errFn)
+ .then(res => {
+ if (!res) {
+ return Promise.resolve(undefined);
+ }
- return res;
- }));
+ return res;
+ });
- promises.push(this.$.restAPI.getRepo(repo, errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
+ const labelsPromises = this.$.restAPI.getRepo(repo, errFn).then(res => {
+ if (!res) {
+ return Promise.resolve(undefined);
+ }
- return res.labels;
- }));
+ return res.labels;
+ });
- return Promise.all(promises).then(([sections, capabilities, labels]) => {
+ return Promise.all([
+ sectionsPromises,
+ capabilitiesPromises,
+ labelsPromises,
+ ]).then(([sections, capabilities, labels]) => {
this._capabilities = capabilities;
this._labels = labels;
this._sections = sections;
@@ -221,33 +234,42 @@
});
}
- _handleUpdateInheritFrom(e) {
+ _handleUpdateInheritFrom(e: CustomEvent<{value: string}>) {
+ const parentProject: ProjectInfo = {
+ id: e.detail.value as UrlEncodedRepoName,
+ name: this._inheritFromFilter,
+ };
if (!this._inheritsFrom) {
- this._inheritsFrom = {};
+ this._inheritsFrom = parentProject;
+ } else {
+ // TODO(TS): replace with
+ // this._inheritsFrom = {...this._inheritsFrom, ...parentProject};
+ const projectInfo = this._inheritsFrom as ProjectInfo;
+ projectInfo.id = parentProject.id;
+ projectInfo.name = parentProject.name;
}
- this._inheritsFrom.id = e.detail.value;
- this._inheritsFrom.name = this._inheritFromFilter;
this._handleAccessModified();
}
- _getInheritFromSuggestions() {
- return this.$.restAPI.getRepos(
- this._inheritFromFilter,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(response => {
- const projects = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- projects.push({
- name: response[key].name,
- value: response[key].id,
- });
- }
+ _getInheritFromSuggestions(): Promise<AutocompleteSuggestion[]> {
+ return this.$.restAPI
+ .getRepos(this._inheritFromFilter, MAX_AUTOCOMPLETE_RESULTS)
+ .then(response => {
+ const projects: AutocompleteSuggestion[] = [];
+ if (!response) {
return projects;
- });
+ }
+ for (const item of response) {
+ projects.push({
+ name: item.name,
+ value: item.id,
+ });
+ }
+ return projects;
+ });
}
- _computeLoadingClass(loading) {
+ _computeLoadingClass(loading: boolean) {
return loading ? 'loading' : '';
}
@@ -255,27 +277,37 @@
this._editing = !this._editing;
}
- _editOrCancel(editing) {
+ _editOrCancel(editing: boolean) {
return editing ? 'Cancel' : 'Edit';
}
- _computeWebLinkClass(weblinks) {
+ _computeWebLinkClass(weblinks?: string[]) {
return weblinks && weblinks.length ? 'show' : '';
}
- _computeShowInherit(inheritsFrom) {
+ _computeShowInherit(inheritsFrom?: RepoName) {
return inheritsFrom ? 'show' : '';
}
- _handleAddedSectionRemoved(e) {
- const index = e.model.index;
- this._sections = this._sections.slice(0, index)
- .concat(this._sections.slice(index + 1, this._sections.length));
+ // TODO(TS): Unclear what is model here, provide a better explanation
+ _handleAddedSectionRemoved(e: CustomEvent & {model: {index: string}}) {
+ if (!this._sections) {
+ return;
+ }
+ const index = Number(e.model.index);
+ if (isNaN(index)) {
+ return;
+ }
+ this._sections = this._sections
+ .slice(0, index)
+ .concat(this._sections.slice(index + 1, this._sections.length));
}
- _handleEditingChanged(editing, editingOld) {
+ _handleEditingChanged(editing: boolean, editingOld: boolean) {
// Ignore when editing gets set initially.
- if (!editingOld || editing) { return; }
+ if (!editingOld || editing) {
+ return;
+ }
// Remove any unsaved but added refs.
if (this._sections) {
this._sections = this._sections.filter(p => !p.value.added);
@@ -283,7 +315,11 @@
// Restore inheritFrom.
if (this._inheritsFrom) {
this._inheritsFrom = {...this._originalInheritsFrom};
- this._inheritFromFilter = this._inheritsFrom.name;
+ this._inheritFromFilter =
+ 'name' in this._inheritsFrom ? this._inheritsFrom.name : undefined;
+ }
+ if (!this._local) {
+ return;
}
for (const key of Object.keys(this._local)) {
if (this._local[key].added) {
@@ -292,18 +328,11 @@
}
}
- /**
- * @param {!Defs.projectAccessInput} addRemoveObj
- * @param {!Array} path
- * @param {string} type add or remove
- * @param {!Object=} opt_value value to add if the type is 'add'
- * @return {!Defs.projectAccessInput}
- */
- _updateAddRemoveObj(addRemoveObj, path, type, opt_value) {
- let curPos = addRemoveObj[type];
+ _updateRemoveObj(addRemoveObj: {remove: PropertyTreeNode}, path: string[]) {
+ let curPos: PropertyTreeNode = addRemoveObj.remove;
for (const item of path) {
if (!curPos[item]) {
- if (item === path[path.length - 1] && type === 'remove') {
+ if (item === path[path.length - 1]) {
if (path[path.length - 2] === 'permissions') {
curPos[item] = {rules: {}};
} else if (path.length === 1) {
@@ -311,13 +340,36 @@
} else {
curPos[item] = {};
}
- } else if (item === path[path.length - 1] && type === 'add') {
- curPos[item] = opt_value;
} else {
curPos[item] = {};
}
}
- curPos = curPos[item];
+ // The last item can be a PrimitiveValue, but we don't use it
+ // All intermediate items are PropertyTreeNode
+ // TODO(TS): rewrite this loop and process the last item explicitly
+ curPos = curPos[item] as PropertyTreeNode;
+ }
+ return addRemoveObj;
+ }
+
+ _updateAddObj(
+ addRemoveObj: {add: PropertyTreeNode},
+ path: string[],
+ value: PropertyTreeNode | PrimitiveValue
+ ) {
+ let curPos: PropertyTreeNode = addRemoveObj.add;
+ for (const item of path) {
+ if (!curPos[item]) {
+ if (item === path[path.length - 1]) {
+ curPos[item] = value;
+ } else {
+ curPos[item] = {};
+ }
+ }
+ // The last item can be a PrimitiveValue, but we don't use it
+ // All intermediate items are PropertyTreeNode
+ // TODO(TS): rewrite this loop and process the last item explicitly
+ curPos = curPos[item] as PropertyTreeNode;
}
return addRemoveObj;
}
@@ -325,58 +377,69 @@
/**
* Used to recursively remove any objects with a 'deleted' bit.
*/
- _recursivelyRemoveDeleted(obj) {
+ _recursivelyRemoveDeleted(obj: PropertyTreeNode) {
for (const k in obj) {
- if (!obj.hasOwnProperty(k)) { continue; }
-
- if (typeof obj[k] == 'object') {
- if (obj[k].deleted) {
+ if (!hasOwnProperty(obj, k)) {
+ continue;
+ }
+ const node = obj[k];
+ if (typeof node === 'object') {
+ if (node.deleted) {
delete obj[k];
return;
}
- this._recursivelyRemoveDeleted(obj[k]);
+ this._recursivelyRemoveDeleted(node);
}
}
}
- _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
+ _recursivelyUpdateAddRemoveObj(
+ obj: PropertyTreeNode,
+ addRemoveObj: {
+ add: PropertyTreeNode;
+ remove: PropertyTreeNode;
+ },
+ path: string[] = []
+ ) {
for (const k in obj) {
- if (!obj.hasOwnProperty(k)) { continue; }
- if (typeof obj[k] == 'object') {
- const updatedId = obj[k].updatedId;
+ if (!hasOwnProperty(obj, k)) {
+ continue;
+ }
+ const node = obj[k];
+ if (typeof node === 'object') {
+ const updatedId = node.updatedId;
const ref = updatedId ? updatedId : k;
- if (obj[k].deleted) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(k), 'remove');
+ if (node.deleted) {
+ this._updateRemoveObj(addRemoveObj, path.concat(k));
continue;
- } else if (obj[k].modified) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(k), 'remove');
- this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
- obj[k]);
+ } else if (node.modified) {
+ this._updateRemoveObj(addRemoveObj, path.concat(k));
+ this._updateAddObj(addRemoveObj, path.concat(ref), node);
/* Special case for ref changes because they need to be added and
removed in a different way. The new ref needs to include all
changes but also the initial state. To do this, instead of
continuing with the same recursion, just remove anything that is
deleted in the current state. */
if (updatedId && updatedId !== k) {
- this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
+ this._recursivelyRemoveDeleted(
+ addRemoveObj.add[updatedId] as PropertyTreeNode
+ );
}
continue;
- } else if (obj[k].added) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(ref), 'add', obj[k]);
+ } else if (node.added) {
+ this._updateAddObj(addRemoveObj, path.concat(ref), node);
/**
* As add / delete both can happen in the new section,
* so here to make sure it will remove the deleted ones.
*
* @see Issue 11339
*/
- this._recursivelyRemoveDeleted(addRemoveObj.add[k]);
+ this._recursivelyRemoveDeleted(
+ addRemoveObj.add[k] as PropertyTreeNode
+ );
continue;
}
- this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
- path.concat(k));
+ this._recursivelyUpdateAddRemoveObj(node, addRemoveObj, path.concat(k));
}
}
}
@@ -384,28 +447,40 @@
/**
* Returns an object formatted for saving or submitting access changes for
* review
- *
- * @return {!Defs.projectAccessInput}
*/
_computeAddAndRemove() {
- const addRemoveObj = {
+ const addRemoveObj: {
+ add: PropertyTreeNode;
+ remove: PropertyTreeNode;
+ parent?: string | null;
+ } = {
add: {},
remove: {},
};
- const originalInheritsFromId = this._originalInheritsFrom ?
- singleDecodeURL(this._originalInheritsFrom.id) : null;
- const inheritsFromId = this._inheritsFrom ?
- singleDecodeURL(this._inheritsFrom.id) : null;
+ const originalInheritsFromId = this._originalInheritsFrom
+ ? singleDecodeURL(this._originalInheritsFrom.id)
+ : null;
+ // TODO(TS): this._inheritsFrom as ProjectInfo might be a mistake.
+ // _inheritsFrom can be {}
+ const inheritsFromId = this._inheritsFrom
+ ? singleDecodeURL((this._inheritsFrom as ProjectInfo).id)
+ : null;
const inheritFromChanged =
- // Inherit from changed
- (originalInheritsFromId &&
- originalInheritsFromId !== inheritsFromId) ||
- // Inherit from added (did not have one initially);
- (!originalInheritsFromId && inheritsFromId);
+ // Inherit from changed
+ (originalInheritsFromId && originalInheritsFromId !== inheritsFromId) ||
+ // Inherit from added (did not have one initially);
+ (!originalInheritsFromId && inheritsFromId);
- this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
+ if (!this._local) {
+ return addRemoveObj;
+ }
+
+ this._recursivelyUpdateAddRemoveObj(
+ (this._local as unknown) as PropertyTreeNode,
+ addRemoveObj
+ );
if (inheritFromChanged) {
addRemoveObj.parent = inheritsFromId;
@@ -414,6 +489,9 @@
}
_handleCreateSection() {
+ if (!this._local) {
+ return;
+ }
let newRef = 'refs/for/*';
// Avoid using an already used key for the placeholder, since it
// immediately gets added to an object.
@@ -424,83 +502,105 @@
this.push('_sections', {id: newRef, value: section});
this.set(['_local', newRef], section);
flush();
- this.root.querySelector('gr-access-section:last-of-type')
- .editReference();
+ // Template already instantiated at this point
+ (this.root!.querySelector(
+ 'gr-access-section:last-of-type'
+ ) as GrAccessSection).editReference();
}
- _getObjforSave() {
+ _getObjforSave(): ProjectAccessInput | undefined {
const addRemoveObj = this._computeAddAndRemove();
// If there are no changes, don't actually save.
- if (!Object.keys(addRemoveObj.add).length &&
- !Object.keys(addRemoveObj.remove).length &&
- !addRemoveObj.parent) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: NOTHING_TO_SAVE},
- bubbles: true,
- composed: true,
- }));
+ if (
+ !Object.keys(addRemoveObj.add).length &&
+ !Object.keys(addRemoveObj.remove).length &&
+ !addRemoveObj.parent
+ ) {
+ this.dispatchEvent(
+ new CustomEvent('show-alert', {
+ detail: {message: NOTHING_TO_SAVE},
+ bubbles: true,
+ composed: true,
+ })
+ );
return;
}
- const obj = {
+ const obj: ProjectAccessInput = ({
add: addRemoveObj.add,
remove: addRemoveObj.remove,
- };
+ } as unknown) as ProjectAccessInput;
if (addRemoveObj.parent) {
obj.parent = addRemoveObj.parent;
}
return obj;
}
- _handleSave(e) {
+ _handleSave(e: Event) {
const obj = this._getObjforSave();
- if (!obj) { return; }
- const button = e && e.target;
+ if (!obj) {
+ return;
+ }
+ const button = e && (e.target as GrButton);
if (button) {
button.loading = true;
}
- return this.$.restAPI.setRepoAccessRights(this.repo, obj)
- .then(() => {
- this._reload(this.repo);
- })
- .finally(() => {
- this._modified = false;
- if (button) {
- button.loading = false;
- }
- });
- }
-
- _handleSaveForReview(e) {
- const obj = this._getObjforSave();
- if (!obj) { return; }
- const button = e && e.target;
- if (button) {
- button.loading = true;
+ const repo = this.repo;
+ if (!repo) {
+ return Promise.resolve();
}
return this.$.restAPI
- .setRepoAccessRightsForReview(this.repo, obj)
- .then(change => {
- GerritNav.navigateToChange(change);
- })
- .finally(() => {
- this._modified = false;
- if (button) {
- button.loading = false;
- }
- });
+ .setRepoAccessRights(repo, obj)
+ .then(() => {
+ this._reload(repo);
+ })
+ .finally(() => {
+ this._modified = false;
+ if (button) {
+ button.loading = false;
+ }
+ });
}
- _computeSaveReviewBtnClass(canUpload) {
+ _handleSaveForReview(e: Event) {
+ const obj = this._getObjforSave();
+ if (!obj) {
+ return;
+ }
+ const button = e && (e.target as GrButton);
+ if (button) {
+ button.loading = true;
+ }
+ if (!this.repo) {
+ return;
+ }
+ return this.$.restAPI
+ .setRepoAccessRightsForReview(this.repo, obj)
+ .then(change => {
+ GerritNav.navigateToChange(change);
+ })
+ .finally(() => {
+ this._modified = false;
+ if (button) {
+ button.loading = false;
+ }
+ });
+ }
+
+ _computeSaveReviewBtnClass(canUpload?: boolean) {
return !canUpload ? 'invisible' : '';
}
- _computeSaveBtnClass(ownerOf) {
+ _computeSaveBtnClass(ownerOf?: GitRef[]) {
return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
}
- _computeMainClass(ownerOf, canUpload, editing) {
+ _computeMainClass(
+ ownerOf: GitRef[] | undefined,
+ canUpload: boolean,
+ editing: boolean
+ ) {
const classList = [];
- if (ownerOf && ownerOf.length > 0 || canUpload) {
+ if ((ownerOf && ownerOf.length > 0) || canUpload) {
classList.push('admin');
}
if (editing) {
@@ -509,10 +609,13 @@
return classList.join(' ');
}
- _computeParentHref(repoName) {
- return getBaseUrl() +
- `/admin/repos/${encodeURL(repoName, true)},access`;
+ _computeParentHref(repoName: RepoName) {
+ return getBaseUrl() + `/admin/repos/${encodeURL(repoName, true)},access`;
}
}
-customElements.define(GrRepoAccess.is, GrRepoAccess);
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-repo-access': GrRepoAccess;
+ }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
index c60a1fe..af87254 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
@@ -586,7 +586,7 @@
.querySelector('gr-access-section').shadowRoot
.querySelector('gr-permission')
._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
+ {detail: {value: 'Maintainers'}});
flushAsynchronousOperations();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
@@ -686,7 +686,7 @@
.querySelector('gr-access-section').root).querySelectorAll(
'gr-permission')[2];
newPermission._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
+ {detail: {value: 'Maintainers'}});
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Modify a section reference.
@@ -806,7 +806,7 @@
newSection.shadowRoot
.querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
+ {detail: {value: 'Maintainers'}});
flushAsynchronousOperations();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
@@ -921,7 +921,7 @@
.querySelector('gr-access-section').root).querySelectorAll(
'gr-permission')[1];
readPermission._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
+ {detail: {value: 'Maintainers'}});
expectedInput = {
add: {
@@ -997,7 +997,7 @@
flushAsynchronousOperations();
newSection.shadowRoot
.querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
+ {detail: {value: 'Maintainers'}});
// Modify a the reference from the default value.
element._local['refs/for/*'].updatedId = 'refs/for/new';
@@ -1071,7 +1071,7 @@
flushAsynchronousOperations();
newSection.shadowRoot
.querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
+ {detail: {value: 'Maintainers'}});
// Modify a the reference from the default value.
element._local['refs/for/**'].updatedId = 'refs/for/new2';
expectedInput = {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 721c7a2..2eb407b 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -61,6 +61,15 @@
value?: string;
text?: string;
}
+
+export interface AutocompleteCommitEventDetail {
+ value: string;
+}
+
+export type AutocompleteCommitEvent = CustomEvent<
+ AutocompleteCommitEventDetail
+>;
+
@customElement('gr-autocomplete')
export class GrAutocomplete extends KeyboardShortcutMixin(
GestureEventListeners(LegacyElementMixin(PolymerElement))
@@ -490,7 +499,7 @@
if (!silent) {
this.dispatchEvent(
new CustomEvent('commit', {
- detail: {value},
+ detail: {value} as AutocompleteCommitEventDetail,
composed: true,
bubbles: true,
})
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index 8923a48..1040631 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -124,6 +124,9 @@
GpgKeysInput,
DocResult,
EmailInfo,
+ ProjectAccessInfo,
+ CapabilityInfoMap,
+ ProjectInfoWithName,
} from '../../../types/common';
import {
CancelConditionCallback,
@@ -1681,7 +1684,11 @@
);
}
- _getReposUrl(filter: string, reposPerPage: number, offset?: number) {
+ _getReposUrl(
+ filter: string | undefined,
+ reposPerPage: number,
+ offset?: number
+ ) {
const defaultFilter = 'state:active OR state:read-only';
const namePartDelimiters = /[@.\-\s/_]/g;
offset = offset || 0;
@@ -1735,18 +1742,18 @@
}
getRepos(
- filter: string,
+ filter: string | undefined,
reposPerPage: number,
offset?: number
- ): Promise<ProjectInfo | undefined> {
+ ): Promise<ProjectInfoWithName[] | undefined> {
const url = this._getReposUrl(filter, reposPerPage, offset);
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._fetchSharedCacheURL({
- url,
+ url, // The url contains query,so the response is an array, not map
anonymizedUrl: '/projects/?*',
- }) as Promise<ProjectInfo | undefined>;
+ }) as Promise<ProjectInfoWithName[] | undefined>;
}
setRepoHead(repo: RepoName, ref: GitRef) {
@@ -1820,17 +1827,23 @@
});
}
- getRepoAccessRights(repoName: RepoName, errFn?: ErrorCallback) {
+ getRepoAccessRights(
+ repoName: RepoName,
+ errFn?: ErrorCallback
+ ): Promise<ProjectAccessInfo | undefined> {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._restApiHelper.fetchJSON({
url: `/projects/${encodeURIComponent(repoName)}/access`,
errFn,
anonymizedUrl: '/projects/*/access',
- });
+ }) as Promise<ProjectAccessInfo | undefined>;
}
- setRepoAccessRights(repoName: RepoName, repoInfo: ProjectAccessInput) {
+ setRepoAccessRights(
+ repoName: RepoName,
+ repoInfo: ProjectAccessInput
+ ): Promise<Response> {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._restApiHelper.send({
@@ -3096,12 +3109,14 @@
});
}
- getCapabilities(errFn?: ErrorCallback) {
+ getCapabilities(
+ errFn?: ErrorCallback
+ ): Promise<CapabilityInfoMap | undefined> {
return this._restApiHelper.fetchJSON({
url: '/config/server/capabilities',
errFn,
reportUrlAsIs: true,
- });
+ }) as Promise<CapabilityInfoMap | undefined>;
}
getTopMenus(errFn?: ErrorCallback) {
diff --git a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
index 3fd56d0..c8942e5 100644
--- a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
@@ -42,6 +42,11 @@
GpgKeyInfo,
PreferencesInfo,
EmailInfo,
+ ProjectAccessInfo,
+ CapabilityInfoMap,
+ ProjectAccessInput,
+ ChangeInfo,
+ ProjectInfoWithName,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {HttpMethod} from '../../../constants/constants';
@@ -126,10 +131,10 @@
getExternalIds(): Promise<AccountExternalIdInfo[] | undefined>;
deleteAccountIdentity(id: string[]): Promise<unknown>;
getRepos(
- filter: string,
+ filter: string | undefined,
reposPerPage: number,
offset?: number
- ): Promise<ProjectInfo | undefined>;
+ ): Promise<ProjectInfoWithName[] | undefined>;
send(
method: HttpMethod,
@@ -249,4 +254,28 @@
patchNum: PatchSetNum,
query: string
): Promise<string[] | undefined>;
+
+ getRepoAccessRights(
+ repoName: RepoName,
+ errFn?: ErrorCallback
+ ): Promise<ProjectAccessInfo | undefined>;
+
+ getRepo(
+ repo: RepoName,
+ errFn?: ErrorCallback
+ ): Promise<ProjectInfo | undefined>;
+
+ getCapabilities(
+ errFn?: ErrorCallback
+ ): Promise<CapabilityInfoMap | undefined>;
+
+ setRepoAccessRights(
+ repoName: RepoName,
+ repoInfo: ProjectAccessInput
+ ): Promise<Response>;
+
+ setRepoAccessRightsForReview(
+ projectName: RepoName,
+ projectInfo: ProjectAccessInput
+ ): Promise<ChangeInfo>;
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 688082b..56991d9 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -111,6 +111,7 @@
export type Hashtag = BrandType<string, '_hashtag'>;
export type StarLabel = BrandType<string, '_startLabel'>;
export type CommitId = BrandType<string, '_commitId'>;
+export type LabelName = BrandType<string, '_labelName'>;
// The UUID of the group
export type GroupId = BrandType<string, '_groupId'>;
@@ -676,6 +677,8 @@
name: string;
}
+export type CapabilityInfoMap = {[id: string]: CapabilityInfo};
+
/**
* The ChangeConfigInfo entity contains information about Gerrit configuration
* from the change section.
@@ -1072,12 +1075,17 @@
state?: ProjectState;
branches?: {[branchName: string]: CommitId};
// labels is filled for Create Project and Get Project calls.
- labels?: {[labelName: string]: LabelTypeInfo};
+ labels?: LabelNameToLabelTypeInfoMap;
// Links to the project in external sites
web_links?: WebLinkInfo[];
}
+export interface ProjectInfoWithName extends ProjectInfo {
+ name: RepoName;
+}
+
export type NameToProjectInfoMap = {[projectName: string]: ProjectInfo};
+export type LabelNameToLabelTypeInfoMap = {[labelName: string]: LabelTypeInfo};
/**
* The LabelTypeInfo entity contains metadata about the labels that a project
@@ -1085,10 +1093,12 @@
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#label-type-info
*/
export interface LabelTypeInfo {
- values: {[value: string]: string};
+ values: LabelTypeInfoValues;
default_value: number;
}
+export type LabelTypeInfoValues = {[value: string]: string};
+
/**
* The DiffContent entity contains information about the content differences in a file.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#diff-content
@@ -1346,7 +1356,7 @@
can_add_tags?: boolean;
config_visible?: boolean;
groups: ProjectAccessGroups;
- configWebLinks: string[];
+ config_web_links: string[];
}
export type ProjectAccessInfoMap = {[projectName: string]: ProjectAccessInfo};
@@ -1486,12 +1496,14 @@
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-access-input
*/
export interface ProjectAccessInput {
- remove?: ProjectAccessInfo[];
- add?: ProjectAccessInfo[];
+ remove?: RefToProjectAccessInfoMap;
+ add?: RefToProjectAccessInfoMap;
message?: string;
parent?: string;
}
+export type RefToProjectAccessInfoMap = {[refName: string]: ProjectAccessInfo};
+
/**
* Represent a file in a base64 encoding
*/
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 39e23ec..94cc435 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -51,13 +51,27 @@
* Event type for an event fired by Polymer for an element generated from a
* dom-repeat template.
*/
-export interface PolymerDomRepeatEvent<T> extends CustomEvent {
- model: PolymerDomRepeatEventModel<T>;
+export interface PolymerDomRepeatEvent<TModel = unknown> extends Event {
+ model: PolymerDomRepeatEventModel<TModel>;
+}
+
+/**
+ * Event type for an event fired by Polymer for an element generated from a
+ * dom-repeat template.
+ */
+export interface PolymerDomRepeatCustomEvent<
+ TModel = unknown,
+ TDetail = unknown
+> extends CustomEvent<TDetail> {
+ model: PolymerDomRepeatEventModel<TModel>;
}
/**
* Model containing additional information about the dom-repeat element
* that fired an event.
+ *
+ * Note: This interface is valid only if both dom-repeat properties 'as' and
+ * 'indexAs' have default values ('item' and 'index' correspondingly)
*/
export interface PolymerDomRepeatEventModel<T> {
/**
diff --git a/polygerrit-ui/app/utils/access-util.ts b/polygerrit-ui/app/utils/access-util.ts
index 041cab3..5ef4155 100644
--- a/polygerrit-ui/app/utils/access-util.ts
+++ b/polygerrit-ui/app/utils/access-util.ts
@@ -14,8 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {LabelName} from '../types/common';
-export const AccessPermissions = {
+export type AccessPermissionMap = {[id: string]: AccessPermission};
+
+export const AccessPermissions: AccessPermissionMap = {
abandon: {
id: 'abandon',
name: 'Abandon',
@@ -122,18 +125,26 @@
},
};
-interface AccessPermission {
+export interface AccessPermission {
id: string;
name: string;
+ label?: LabelName;
}
+export interface PermissionArrayItem<T> {
+ id: string;
+ value: T;
+}
+
+export type PermissionArray<T> = Array<PermissionArrayItem<T>>;
+
/**
* @return a sorted array sorted by the id of the original
* object.
*/
-export function toSortedPermissionsArray(
- obj: Record<string, AccessPermission>
-) {
+export function toSortedPermissionsArray<T>(obj?: {
+ [permissionId: string]: T;
+}): PermissionArray<T> {
if (!obj) {
return [];
}