From 9a71b714ab967dbfcb60774dc1bd4ea194b6d6ee Mon Sep 17 00:00:00 2001 From: Carlos Alessandro Ribeiro Date: Wed, 19 Aug 2020 21:30:42 -0300 Subject: [PATCH] Introducing User Preferences and Multi-project selection (#148) * Adding User Preferences panel * User Preferences logic * Moving things around to support multiple projects * Multi-project selection support * Minor UI tweaks * fixing SonarQube issue * More fixes * Fixing inconsistency setState * Another fix * Restore filters to original state onDismiss * Changing major version to 2.0 * Removing unnecessary state value --- .azure-pipelines/azure-pipelines.dev.yml | 2 +- .azure-pipelines/azure-pipelines.yml | 2 +- package-lock.json | 345 +++++++++++++---- package.json | 2 +- src/common.tsx | 28 +- src/components/Columns.tsx | 29 +- src/components/FilterBarHub.tsx | 4 +- src/components/UserPreferencesPanel.scss | 103 ++++++ src/components/UserPreferencesPanel.tsx | 260 +++++++++++++ src/index.tsx | 153 ++++++-- src/models/PullRequestModel.tsx | 7 +- src/models/UserPreferences.tsx | 58 +++ src/tabs/PulRequestsTabData.tsx | 133 ++----- src/tabs/PullRequestsTab.tsx | 447 +++++++++++------------ tsconfig.json | 2 +- vss-extension.json | 2 +- 16 files changed, 1109 insertions(+), 468 deletions(-) create mode 100644 src/components/UserPreferencesPanel.scss create mode 100644 src/components/UserPreferencesPanel.tsx create mode 100644 src/models/UserPreferences.tsx diff --git a/.azure-pipelines/azure-pipelines.dev.yml b/.azure-pipelines/azure-pipelines.dev.yml index 40d89ac..9d7d256 100644 --- a/.azure-pipelines/azure-pipelines.dev.yml +++ b/.azure-pipelines/azure-pipelines.dev.yml @@ -55,4 +55,4 @@ steps: fileType: 'vsix' vsixFile: 'azure-pull-request-hub-dev.vsix' updateTasksVersionType: 'patch' - extensionVersion: '1.$(Build.BuildNumber)' + extensionVersion: '2.$(Build.BuildNumber)' diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index ee0f0e4..d805151 100644 --- a/.azure-pipelines/azure-pipelines.yml +++ b/.azure-pipelines/azure-pipelines.yml @@ -59,4 +59,4 @@ steps: fileType: 'vsix' vsixFile: 'azure-pull-request-hub.vsix' updateTasksVersionType: 'patch' - extensionVersion: '1.$(Build.BuildNumber)' + extensionVersion: '2.$(Build.BuildNumber)' diff --git a/package-lock.json b/package-lock.json index a7ffeb3..78e51ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1182,6 +1182,128 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "@fluentui/date-time-utilities": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-7.4.0.tgz", + "integrity": "sha512-8zaFJ5I1AikQmoi5aWv/mustCf8UAFYUjJrrnlXwvXOe2HlC+wLZH236qRZPi4Wat8qG151vE24nTqzGMVldRQ==", + "dev": true, + "requires": { + "@uifabric/set-version": "^7.0.19", + "tslib": "^1.10.0" + }, + "dependencies": { + "@uifabric/set-version": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + } + } + }, + "@fluentui/keyboard-key": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.2.8.tgz", + "integrity": "sha512-GJW3NjDdigTddYuxoOuBGhOs5Egweqs6iPTDSUN+oAtXI/poYHVtgjxaFQx1OeAzD8wLXofGneAe/03ZW+TESA==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "@fluentui/react-focus": { + "version": "7.12.30", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.12.30.tgz", + "integrity": "sha512-UV9StYMID9cRB2TQRE1u134sT8AOaOs0A2ZW4oM82/lzQ8XxL3aREFY0ohhmblYhEGzkb349y51h4eSqW5EpPA==", + "dev": true, + "requires": { + "@fluentui/keyboard-key": "^0.2.8", + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "@uifabric/styling": "^7.14.10", + "@uifabric/utilities": "^7.26.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "@uifabric/merge-styles": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.4.tgz", + "integrity": "sha512-OhOEtwYD74AARf4VZQJPan97QEvtTYcxBGVQfdE7YxFnvR1VdfMxOsV+9CAjAIFM+Xu5ibeKkEE/ZmJYnHkqsQ==", + "dev": true, + "requires": { + "@uifabric/set-version": "^7.0.19", + "tslib": "^1.10.0" + } + }, + "@uifabric/set-version": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "@uifabric/utilities": { + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.26.1.tgz", + "integrity": "sha512-FX/Gu4XY6YlvBEyTyEeXUOtPpgTy1irHpSAE/vDbDZQlksVNv4FPnVingQZI9T/rA96ivP4q1PUutrb3X3hfsw==", + "dev": true, + "requires": { + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" + } + } + } + }, + "@fluentui/react-icons": { + "version": "0.1.45", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-0.1.45.tgz", + "integrity": "sha512-xDE7dgbh3JgUt2uFUW66ut4V9T5irmxi+S5IGJKkXLUf3MHBysknx42BkX1Yc1Uczz3r5va1oWWwlXNiN/CjsA==", + "dev": true, + "requires": { + "@microsoft/load-themed-styles": "^1.10.26", + "@uifabric/set-version": "^7.0.19", + "@uifabric/utilities": "^7.26.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "@uifabric/merge-styles": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.4.tgz", + "integrity": "sha512-OhOEtwYD74AARf4VZQJPan97QEvtTYcxBGVQfdE7YxFnvR1VdfMxOsV+9CAjAIFM+Xu5ibeKkEE/ZmJYnHkqsQ==", + "dev": true, + "requires": { + "@uifabric/set-version": "^7.0.19", + "tslib": "^1.10.0" + } + }, + "@uifabric/set-version": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "@uifabric/utilities": { + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.26.1.tgz", + "integrity": "sha512-FX/Gu4XY6YlvBEyTyEeXUOtPpgTy1irHpSAE/vDbDZQlksVNv4FPnVingQZI9T/rA96ivP4q1PUutrb3X3hfsw==", + "dev": true, + "requires": { + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" + } + } + } + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -1416,9 +1538,9 @@ } }, "@microsoft/load-themed-styles": { - "version": "1.9.15", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.9.15.tgz", - "integrity": "sha512-bhm4T+tZ/OcrpehW1q1tuElGvxT7WT6s+1Aqz7Yg7UZDA9xyZkQDNb/BY9f4GjJ8t82EedwJxYJQwaGQfTngfg==", + "version": "1.10.66", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.66.tgz", + "integrity": "sha512-w1NCJQOrr5Ko5Og1ay7NO0vFUwxksddnLmVuY2bBi7DkgbFYtiRFQGkc+HMDpy2x6Fcy/iUTitbR674aX5TJhw==", "dev": true }, "@mrmlnc/readdir-enhanced": { @@ -1845,24 +1967,70 @@ } } }, + "@uifabric/foundation": { + "version": "7.7.44", + "resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.7.44.tgz", + "integrity": "sha512-0YBZTGsVxQEd1+IYXawQcOd263auoXCQo2KE8CaL2aug330+9LIqloIyjnhDH+yJQ9JZZmzzlJ2clOD7NCT97g==", + "dev": true, + "requires": { + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "@uifabric/styling": "^7.14.10", + "@uifabric/utilities": "^7.26.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "@uifabric/merge-styles": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.4.tgz", + "integrity": "sha512-OhOEtwYD74AARf4VZQJPan97QEvtTYcxBGVQfdE7YxFnvR1VdfMxOsV+9CAjAIFM+Xu5ibeKkEE/ZmJYnHkqsQ==", + "dev": true, + "requires": { + "@uifabric/set-version": "^7.0.19", + "tslib": "^1.10.0" + } + }, + "@uifabric/set-version": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "@uifabric/utilities": { + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.26.1.tgz", + "integrity": "sha512-FX/Gu4XY6YlvBEyTyEeXUOtPpgTy1irHpSAE/vDbDZQlksVNv4FPnVingQZI9T/rA96ivP4q1PUutrb3X3hfsw==", + "dev": true, + "requires": { + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" + } + } + } + }, "@uifabric/icons": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-6.5.4.tgz", - "integrity": "sha512-yR9FlXiR3QsY8hkFhdWgzQxBUxxIqarwI/CVfE0N/aNqqlcfDs8fMRKRgpa5ZpPEtnF9VOkrNzcUI6dE2UjXJA==", + "version": "7.3.70", + "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.3.70.tgz", + "integrity": "sha512-ibAyKU02TFH6wdqrnu7keea9MaM29EGrMTkz24DyNlWfM+H9z3JL6UiWUeldgyok8HXDWI9GRQWB7f3YhJi60Q==", "dev": true, "requires": { - "@uifabric/set-version": "^1.1.3", - "@uifabric/styling": "^6.50.3", - "tslib": "^1.7.1" + "@uifabric/set-version": "^7.0.19", + "@uifabric/styling": "^7.14.10", + "tslib": "^1.10.0" }, "dependencies": { "@uifabric/set-version": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-1.1.3.tgz", - "integrity": "sha512-IYpwVIuN7MJOeiWzZzr9AmFSvA5zc6gJn4fNHtEFIQnNB8WVWIcYrvx8Tbf7wWj9MvhdHYp70F054zZlHbL/Ag==", + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", "dev": true, "requires": { - "tslib": "^1.7.1" + "tslib": "^1.10.0" } } } @@ -1876,6 +2044,50 @@ "tslib": "^1.10.0" } }, + "@uifabric/react-hooks": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/@uifabric/react-hooks/-/react-hooks-7.7.3.tgz", + "integrity": "sha512-WAhMcnQSRgSQr0wkw8paESqxHCDWL2vdgTcDNKSj550GpAzD9BgZSBp3v3yln/qA4pN/nfjeU0CX9HujidSbMA==", + "dev": true, + "requires": { + "@uifabric/set-version": "^7.0.19", + "@uifabric/utilities": "^7.26.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "@uifabric/merge-styles": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.4.tgz", + "integrity": "sha512-OhOEtwYD74AARf4VZQJPan97QEvtTYcxBGVQfdE7YxFnvR1VdfMxOsV+9CAjAIFM+Xu5ibeKkEE/ZmJYnHkqsQ==", + "dev": true, + "requires": { + "@uifabric/set-version": "^7.0.19", + "tslib": "^1.10.0" + } + }, + "@uifabric/set-version": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "@uifabric/utilities": { + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.26.1.tgz", + "integrity": "sha512-FX/Gu4XY6YlvBEyTyEeXUOtPpgTy1irHpSAE/vDbDZQlksVNv4FPnVingQZI9T/rA96ivP4q1PUutrb3X3hfsw==", + "dev": true, + "requires": { + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" + } + } + } + }, "@uifabric/set-version": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.15.tgz", @@ -1885,47 +2097,47 @@ } }, "@uifabric/styling": { - "version": "6.50.3", - "resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-6.50.3.tgz", - "integrity": "sha512-k7ZIInlUz+oIaD1cXZi2a6s3Ms3VjIqdt7A9oT/SlTSrGcec5punjiUajETUgHpOlp39ZTTqB91Qeqrk9yA9Gg==", + "version": "7.14.10", + "resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.14.10.tgz", + "integrity": "sha512-hEmXCJJUVr+ykvPVXyvTHS5f2/GMCh1PObajuXgtpZVpJRzA+Rwgg5gBxWYeRNYw+3WZV62f0zAbmYh8ZCRAhQ==", "dev": true, "requires": { - "@microsoft/load-themed-styles": "^1.7.13", - "@uifabric/merge-styles": "^6.19.3", - "@uifabric/set-version": "^1.1.3", - "@uifabric/utilities": "^6.41.6", - "tslib": "^1.7.1" + "@microsoft/load-themed-styles": "^1.10.26", + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "@uifabric/utilities": "^7.26.1", + "tslib": "^1.10.0" }, "dependencies": { "@uifabric/merge-styles": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-6.19.3.tgz", - "integrity": "sha512-89JXEvl6bIQQqQLJ8T3fXcmKpvJfzqer/noFcAHHq/Gbo1wT8pixiDgUQm9vpTB7lpyjfSfw3PAh5Xt5UFFYOg==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.4.tgz", + "integrity": "sha512-OhOEtwYD74AARf4VZQJPan97QEvtTYcxBGVQfdE7YxFnvR1VdfMxOsV+9CAjAIFM+Xu5ibeKkEE/ZmJYnHkqsQ==", "dev": true, "requires": { - "@uifabric/set-version": "^1.1.3", - "tslib": "^1.7.1" + "@uifabric/set-version": "^7.0.19", + "tslib": "^1.10.0" } }, "@uifabric/set-version": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-1.1.3.tgz", - "integrity": "sha512-IYpwVIuN7MJOeiWzZzr9AmFSvA5zc6gJn4fNHtEFIQnNB8WVWIcYrvx8Tbf7wWj9MvhdHYp70F054zZlHbL/Ag==", + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", "dev": true, "requires": { - "tslib": "^1.7.1" + "tslib": "^1.10.0" } }, "@uifabric/utilities": { - "version": "6.41.6", - "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-6.41.6.tgz", - "integrity": "sha512-xD2/Jy8OMACZSyRoN3dwk3Ds9QcU4jkf9GDc5mJcSqCyYp3ShlO6ggx4u28OFB5qhsIljtzop31hTvL6PH23Cg==", + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.26.1.tgz", + "integrity": "sha512-FX/Gu4XY6YlvBEyTyEeXUOtPpgTy1irHpSAE/vDbDZQlksVNv4FPnVingQZI9T/rA96ivP4q1PUutrb3X3hfsw==", "dev": true, "requires": { - "@uifabric/merge-styles": "^6.19.3", - "@uifabric/set-version": "^1.1.3", - "prop-types": "^15.5.10", - "tslib": "^1.7.1" + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" } } } @@ -10322,50 +10534,55 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, "office-ui-fabric-react": { - "version": "6.105.0", - "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-6.105.0.tgz", - "integrity": "sha512-xcKObGYd9cRrww5dl89rBTRxOxfS358ohvglnlL/n1+tWzsqtx3C393mlBV8d67/D3LUDyCFK4BIaXcjsgOu4w==", + "version": "7.126.2", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.126.2.tgz", + "integrity": "sha512-1ETz4x5AsBZ36PBLIbL3T++Aso+9WudIsu3Z331UDNwSJF4LjOM50+PY/i7DUb+LSDzmwMwydCpU5yzEndLRBw==", "dev": true, "requires": { - "@microsoft/load-themed-styles": "^1.7.13", - "@uifabric/icons": ">=6.1.2 <7.0.0", - "@uifabric/merge-styles": ">=6.15.0 <7.0.0", - "@uifabric/set-version": ">=1.1.3 <2.0.0", - "@uifabric/styling": ">=6.35.0 <7.0.0", - "@uifabric/utilities": ">=6.27.0 <7.0.0", - "prop-types": "^15.5.10", - "tslib": "^1.7.1" + "@fluentui/date-time-utilities": "^7.4.0", + "@fluentui/react-focus": "^7.12.30", + "@fluentui/react-icons": "^0.1.45", + "@microsoft/load-themed-styles": "^1.10.26", + "@uifabric/foundation": "^7.7.44", + "@uifabric/icons": "^7.3.70", + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/react-hooks": "^7.7.3", + "@uifabric/set-version": "^7.0.19", + "@uifabric/styling": "^7.14.10", + "@uifabric/utilities": "^7.26.1", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" }, "dependencies": { "@uifabric/merge-styles": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-6.19.3.tgz", - "integrity": "sha512-89JXEvl6bIQQqQLJ8T3fXcmKpvJfzqer/noFcAHHq/Gbo1wT8pixiDgUQm9vpTB7lpyjfSfw3PAh5Xt5UFFYOg==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.4.tgz", + "integrity": "sha512-OhOEtwYD74AARf4VZQJPan97QEvtTYcxBGVQfdE7YxFnvR1VdfMxOsV+9CAjAIFM+Xu5ibeKkEE/ZmJYnHkqsQ==", "dev": true, "requires": { - "@uifabric/set-version": "^1.1.3", - "tslib": "^1.7.1" + "@uifabric/set-version": "^7.0.19", + "tslib": "^1.10.0" } }, "@uifabric/set-version": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-1.1.3.tgz", - "integrity": "sha512-IYpwVIuN7MJOeiWzZzr9AmFSvA5zc6gJn4fNHtEFIQnNB8WVWIcYrvx8Tbf7wWj9MvhdHYp70F054zZlHbL/Ag==", + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.19.tgz", + "integrity": "sha512-p52z9Z5Kfl0kAU3DiPNPg+0vCdSAxlkRZEtEa+RwM6fh9XSo91n4C56FFdKDW7HJVuhGjMK7UEXuU6ELY1W7fg==", "dev": true, "requires": { - "tslib": "^1.7.1" + "tslib": "^1.10.0" } }, "@uifabric/utilities": { - "version": "6.41.6", - "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-6.41.6.tgz", - "integrity": "sha512-xD2/Jy8OMACZSyRoN3dwk3Ds9QcU4jkf9GDc5mJcSqCyYp3ShlO6ggx4u28OFB5qhsIljtzop31hTvL6PH23Cg==", + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.26.1.tgz", + "integrity": "sha512-FX/Gu4XY6YlvBEyTyEeXUOtPpgTy1irHpSAE/vDbDZQlksVNv4FPnVingQZI9T/rA96ivP4q1PUutrb3X3hfsw==", "dev": true, "requires": { - "@uifabric/merge-styles": "^6.19.3", - "@uifabric/set-version": "^1.1.3", - "prop-types": "^15.5.10", - "tslib": "^1.7.1" + "@uifabric/merge-styles": "^7.16.4", + "@uifabric/set-version": "^7.0.19", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" } } } diff --git a/package.json b/package.json index f333a0a..255256b 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "core-js": "^3.1.4", "del-cli": "^2.0.0", "node-sass": "^4.14.1", - "office-ui-fabric-react": "^6.105.0", + "office-ui-fabric-react": "^7.123.4", "react-router-dom": "^4.3.1", "redux": "^4.0.4", "rename-webpack-plugin": "^2.0.0", diff --git a/src/common.tsx b/src/common.tsx index 14abf66..0be3396 100644 --- a/src/common.tsx +++ b/src/common.tsx @@ -3,6 +3,7 @@ import "./common.scss"; import * as React from "react"; import * as ReactDOM from "react-dom"; import { MessageCard, MessageCardSeverity } from "azure-devops-ui/MessageCard"; +import { UserPreferences } from "./models/UserPreferences"; export function showRootComponent(component: React.ReactElement) { ReactDOM.render(component, document.getElementById("root")); @@ -19,34 +20,9 @@ export function isLocalStorageAvailable(){ } } -export class UsertSettings { - constructor(public lastVisit: Date = new Date()) { - - } - - save = () => { - this.lastVisit = new Date(); - localStorage.setItem(USER_SETTINGS_STORE_KEY, JSON.stringify(this)); - } - - load = () => { - const cachedInstance = localStorage.getItem(USER_SETTINGS_STORE_KEY); - - if (!cachedInstance || cachedInstance.length === 0) - { - return; - } - - const cachedUserSettings: UsertSettings = JSON.parse(cachedInstance); - const savedDate = new Date(cachedUserSettings.lastVisit.toString()); - - this.lastVisit = savedDate; - } -} - export const USER_SETTINGS_STORE_KEY: string = "PRMH_USER_SETTINGS_KEY"; -export const UsertSettingsInstance: UsertSettings = new UsertSettings(); +export const UserPreferencesInstance: UserPreferences = new UserPreferences(); export function ShowErrorMessage(props: any) { return ( diff --git a/src/components/Columns.tsx b/src/components/Columns.tsx index d7fbd81..244585b 100644 --- a/src/components/Columns.tsx +++ b/src/components/Columns.tsx @@ -22,9 +22,10 @@ import { PillGroup } from "azure-devops-ui/PillGroup"; import { Pill, PillSize, PillVariant } from "azure-devops-ui/Pill"; import { ConditionalChildren } from "azure-devops-ui/ConditionalChildren"; import { Observer } from "azure-devops-ui/Observer"; +import { UserPreferencesInstance } from "../common"; export function openNewWindowTab(targetUrl: string): void { - window.open(targetUrl, "_blank"); + window.open(targetUrl, UserPreferencesInstance.openPRNewWindow ? "_blank" : "_top"); } export function StatusColumn( @@ -295,7 +296,7 @@ export function DetailsColumn( - + - + {tableItem.gitPullRequest.reviewers - .sort(Data.sortMethod) + .sort(Data.sortBranchOrIdentity) .map((reviewer, i) => { return ( {" "}
-
{reviewer.votedFor.map((r) => { return ( ) : null} + {reviewer.isContainer && reviewer.isContainer === true ? ( + + +
+ Voted by: +
{" "} +
+ {tableItem.gitPullRequest.reviewers.filter(rv => rv.votedFor && rv.votedFor.some(vf => vf.id === reviewer.id)).map((r) => { + return ( + + {" "} + - {r.displayName}
+
+ ); + })} +
+ ) : null} diff --git a/src/components/FilterBarHub.tsx b/src/components/FilterBarHub.tsx index 8fb0329..9776efa 100644 --- a/src/components/FilterBarHub.tsx +++ b/src/components/FilterBarHub.tsx @@ -26,7 +26,6 @@ import { Status } from "azure-devops-ui/Status"; import { getStatusSizeValue, getStatusIcon } from "../models/constants"; import { PullRequestModel } from "../models/PullRequestModel"; import { Spinner } from "office-ui-fabric-react"; -import { IProjectInfo } from "azure-devops-extension-api"; export const myApprovalStatuses: IListBoxItem[] = Object.keys( Data.ReviewerVoteOption @@ -50,7 +49,6 @@ export const alternateStatusPr: IListBoxItem[] = Object.keys( export interface IFilterHubProps { filterPullRequests: () => void; - currentProject: IProjectInfo | TeamProjectReference; pullRequests: PullRequestModel[]; projects: TeamProjectReference[]; filter: Filter; @@ -93,7 +91,7 @@ export function FilterBarHub(props: IFilterHubProps): JSX.Element { void; + onSave: () => void; + projects: TeamProjectReference[]; +} + +export function UserPreferencesPanel(props: IUserSettingsProps): JSX.Element { + const sortDirectionItems: IListBoxItem[] = [ + { id: "desc", text: "Oldest First", iconProps: { iconName: "SortDown" } }, + { id: "asc", text: "Newest First", iconProps: { iconName: "SortUp" } }, + ]; + + const showFilterByDefault = new ObservableValue( + UserPreferencesInstance.showFilterByDefault + ); + const openPRNewWindow = new ObservableValue( + UserPreferencesInstance.openPRNewWindow + ); + const userSettingsSelectedProjects = new DropdownMultiSelection(); + + if ( + UserPreferencesInstance.selectedProjects && + UserPreferencesInstance.selectedProjects.length + ) { + UserPreferencesInstance.selectedProjects.forEach((i) => { + const foundIndex = props.projects.findIndex((p) => p.id === i); + + if (foundIndex >= 0) { + userSettingsSelectedProjects.select(foundIndex); + } + }); + } + + const topNumberCompletedAbandoned = new ObservableValue( + UserPreferencesInstance.topNumberCompletedAbandoned.toString() + ); + const selectedDefaultSorting = new ObservableValue( + UserPreferencesInstance.selectedDefaultSorting + ); + + const restoreDefaults = (): void => { + UserPreferencesInstance.restoreToDefaults(); + reloadValues(); + }; + + const reloadValues = (): void => { + userSettingsSelectedProjects.clear(); + showFilterByDefault.value = UserPreferencesInstance.showFilterByDefault; + openPRNewWindow.value = UserPreferencesInstance.openPRNewWindow; + + if ( + UserPreferencesInstance.selectedProjects && + UserPreferencesInstance.selectedProjects.length + ) { + UserPreferencesInstance.selectedProjects.forEach((i) => { + const foundIndex = props.projects.findIndex((p) => p.id === i); + + if (foundIndex >= 0) { + userSettingsSelectedProjects.select(foundIndex); + } + }); + } + + topNumberCompletedAbandoned.value = UserPreferencesInstance.topNumberCompletedAbandoned.toString(); + + selectedDefaultSorting.value = + UserPreferencesInstance.selectedDefaultSorting; + }; + + return ( + { + props.onDismiss(); + }} + titleProps={{ text: "User Preferences" }} + description={ + "Configure your preferences for making this extension even better for you." + } + footerButtonProps={[ + { + text: "Cancel", + onClick: () => { + UserPreferencesInstance.load(); + props.onDismiss(); + }, + }, + { + text: "Restore Defaults", + onClick: () => { + restoreDefaults(); + }, + }, + { + text: "Save", + primary: true, + onClick: () => { + UserPreferencesInstance.save(); + props.onDismiss(); + props.onSave(); + }, + }, + ]} + > +
+
+
+
+
+ Show Filters by Default +
+ { + showFilterByDefault.value = value; + UserPreferencesInstance.showFilterByDefault = value; + }} + /> +
+
+ Turn On/Off the Filter Bar by default. +
+
+
+
+
+ Open PR in a new Window +
+ { + openPRNewWindow.value = value; + UserPreferencesInstance.openPRNewWindow = value; + }} + /> +
+
+ Enable or disable opening the PR in a new Window or keep on the + same. +
+
+ {false &&
+
+
+ Default Selected Projects +
+ { + userSettingsSelectedProjects.clear(); + }, + }, + ]} + items={props.projects.map((p) => { + return { + id: p.id, + text: p.name, + }; + })} + selection={userSettingsSelectedProjects} + placeholder="Select your preferred projects" + showFilterBox={true} + onSelect={( + event: React.SyntheticEvent, + item: IListBoxItem<{}> + ) => { + const itemIndex = UserPreferencesInstance.selectedProjects.findIndex( + (i) => i === item.id + ); + + if (itemIndex < 0) { + UserPreferencesInstance.selectedProjects.push(item.id); + } else { + UserPreferencesInstance.selectedProjects.splice( + itemIndex, + 1 + ); + } + + console.log(UserPreferencesInstance.selectedProjects); + }} + /> +
+
+ Will keep selected by default when the extension loads again. +
+
} +
+
+
Default PR Sorting
+ { + selectedDefaultSorting.value = selectedId; + UserPreferencesInstance.selectedDefaultSorting = selectedId; + }} + selectedButtonId={selectedDefaultSorting} + direction={RadioButtonGroupDirection.Horizontal} + > + {sortDirectionItems.map((i) => { + return ; + })} + +
+
+ Select which preferred sorting you desire when the PRs are loaded +
+
+
+
+
+ Top Completed/Abandoned PRs +
+ { + topNumberCompletedAbandoned.value = newValue; + UserPreferencesInstance.topNumberCompletedAbandoned = parseInt( + newValue + ); + }} + placeholder="" + readOnly={false} + width={TextFieldWidth.auto} + /> +
+
+ Set the max number of Completed/Abandoned PRs (tabs). +
+
+
+
+
+ ); +} diff --git a/src/index.tsx b/src/index.tsx index c189a2f..3a74799 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ +import "./index.scss"; + import * as DevOps from "azure-devops-extension-sdk"; import * as React from "react"; -import "./index.scss"; import { Surface } from "azure-devops-ui/Surface"; import { Header, TitleSize } from "azure-devops-ui/Header"; import { IHeaderCommandBarItem } from "azure-devops-ui/HeaderCommandBar"; @@ -8,7 +9,7 @@ import { Page } from "azure-devops-ui/Page"; import { Tab, TabBar, TabSize } from "azure-devops-ui/Tabs"; import { showRootComponent, - UsertSettingsInstance, + UserPreferencesInstance, ShowErrorMessage, isLocalStorageAvailable, } from "./common"; @@ -17,38 +18,72 @@ import { addPolyFills } from "./polyfills"; import { PullRequestStatus } from "azure-devops-extension-api/Git/Git"; import { ObservableValue } from "azure-devops-ui/Core/Observable"; import { Observer } from "azure-devops-ui/Observer"; +import { UserPreferencesPanel } from "./components/UserPreferencesPanel"; +import { Toast } from "azure-devops-ui/Toast"; +import { TeamProjectReference } from "azure-devops-extension-api/Core/Core"; +import { CoreRestClient } from "azure-devops-extension-api/Core/CoreClient"; +import { getClient } from "azure-devops-extension-api"; +import * as Data from "./tabs/PulRequestsTabData"; +import { Spinner, SpinnerSize } from "office-ui-fabric-react"; interface IHubContentState { errorMessage: string; + showUserPreferencesPanel: boolean; + showToastMessage: boolean; + toastMessageToShow: string; + projects: TeamProjectReference[]; + loading: boolean; } addPolyFills(); export class App extends React.Component<{}, IHubContentState> { + private toastRef: React.RefObject = React.createRef(); private selectedTabId: ObservableValue; private activeCount: ObservableValue; private completedCount: ObservableValue; private abandonedCount: ObservableValue; + private readonly coreClient: CoreRestClient; private onUnload = (e: BeforeUnloadEvent) => {}; constructor(props: {}) { super(props); + UserPreferencesInstance.load(); + + this.coreClient = getClient(CoreRestClient); + this.selectedTabId = new ObservableValue("active"); this.activeCount = new ObservableValue(0); this.completedCount = new ObservableValue(0); this.abandonedCount = new ObservableValue(0); + this.toggleUserPreferencesPanel = this.toggleUserPreferencesPanel.bind( + this + ); + this.showToastMessage = this.showToastMessage.bind(this); + this.state = { errorMessage: "", + showUserPreferencesPanel: false, + showToastMessage: false, + toastMessageToShow: "", + projects: [], + loading: true }; } public async componentWillMount() { try { - DevOps.init(); - UsertSettingsInstance.load(); + await DevOps.init(); + + await this.getTeamProjects(); + + this.setState({ + loading: false + }); + } catch (error) { this.handleError(error); } @@ -57,10 +92,10 @@ export class App extends React.Component<{}, IHubContentState> { public componentDidMount() { window.addEventListener("beforeunload", this.onUnload); - if (!isLocalStorageAvailable()) - { + if (!isLocalStorageAvailable()) { this.setState({ - errorMessage: "Your browser is blocking 'localStorage' API. Save current filters and last visit on PR will not work as expected." + errorMessage: + "Your browser is blocking 'localStorage' API. Save current filters and last visit on PR will not work as expected.", }); } } @@ -69,15 +104,16 @@ export class App extends React.Component<{}, IHubContentState> { window.removeEventListener("beforeunload", this.onUnload); } - private handleError(error: any): void { - console.log(error); - this.setState({ - errorMessage: "There was an error during the extension load: " + error, - }); - } - public render(): JSX.Element { - const { errorMessage } = this.state; + const { errorMessage, showToastMessage, toastMessageToShow, loading } = this.state; + + if (loading === true) { + return ( +
+ +
+ ); + } return ( @@ -88,6 +124,10 @@ export class App extends React.Component<{}, IHubContentState> { titleSize={TitleSize.Medium} /> + {showToastMessage && ( + + )} + { key="active" prType={PullRequestStatus.Active} onCountChange={this.onCountChangeActive} + showToastMessage={this.showToastMessage} + projects={this.state.projects} /> ); } else if (props.selectedTabId === "completed") { @@ -140,6 +182,8 @@ export class App extends React.Component<{}, IHubContentState> { key="completed" prType={PullRequestStatus.Completed} onCountChange={this.onCountChangeCompleted} + showToastMessage={this.showToastMessage} + projects={this.state.projects} /> ); } else if (props.selectedTabId === "abandoned") { @@ -148,17 +192,51 @@ export class App extends React.Component<{}, IHubContentState> { key="abandoned" prType={PullRequestStatus.Abandoned} onCountChange={this.onCountChangeAbandoned} + showToastMessage={this.showToastMessage} + projects={this.state.projects} /> ); } }} + + {this.state.showUserPreferencesPanel && ( + + )} ); } + private getTeamProjects = async (): Promise => { + const projects = (await this.coreClient.getProjects(undefined, 1000)).sort( + Data.sortTagRepoTeamProject + ); + + this.setState({ + projects + }); + }; + + private showToastMessage = (message: string): void => { + this.setState({ showToastMessage: true, toastMessageToShow: message }); + + setTimeout(() => { + this.toastRef.current!.fadeOut().promise.then(() => { + this.setState({ showToastMessage: false, toastMessageToShow: message }); + }); + }, 5000); + }; + + private saveUserPreferences = (): void => { + this.showToastMessage("User Preferences successfully saved! Please refresh for the changes to take effect."); + }; + private onCountChangeActive = (count: number): void => { this.activeCount.value = count; }; @@ -175,24 +253,37 @@ export class App extends React.Component<{}, IHubContentState> { this.selectedTabId.value = newTabId; }; - private getCommandBarItems(): IHeaderCommandBarItem[] { + private handleError = (error: any): void => { + console.log(error); + this.setState({ + errorMessage: "There was an error during the extension load: " + error, + }); + }; + + private toggleUserPreferencesPanel = (): void => { + this.setState({ + showUserPreferencesPanel: !this.state.showUserPreferencesPanel, + }); + }; + + private getCommandBarItems = (): IHeaderCommandBarItem[] => { return [ - // { - // id: "configuration", - // text: "Configuration", - // onActivate: () => { - // this.onPanelClick(); - // }, - // iconProps: { - // iconName: "fabric-icon ms-Icon--Settings" - // }, - // isPrimary: true, - // tooltipProps: { - // text: "Open the Pull Request Manager tab" - // } - // } + { + id: "preferences", + text: "Preferences", + onActivate: () => { + this.toggleUserPreferencesPanel(); + }, + iconProps: { + iconName: "fabric-icon ms-Icon--Settings", + }, + isPrimary: true, + tooltipProps: { + text: "Open the Pull Request Manager User settings", + }, + }, ]; - } + }; } showRootComponent(); diff --git a/src/models/PullRequestModel.tsx b/src/models/PullRequestModel.tsx index 297f4e9..707d5f9 100644 --- a/src/models/PullRequestModel.tsx +++ b/src/models/PullRequestModel.tsx @@ -420,7 +420,7 @@ export class PullRequestModel { p.configuration.isEnabled === true && p.configuration.isBlocking === true ) - .map((p) => { + .forEach((p) => { const pullRequestPolicy = new PullRequestPolicy(); pullRequestPolicy.id = p.evaluationId; pullRequestPolicy.displayName = `${p.configuration.type.displayName}`; @@ -480,15 +480,14 @@ export class PullRequestModel { public static getModels( pullRequestList: GitPullRequest[] | undefined, - projectName: string, baseUrl: string, callbackState: (pullRequestModel: PullRequestModel) => void ): PullRequestModel[] { const modelList: PullRequestModel[] = []; - pullRequestList!.map((pr) => { + pullRequestList!.forEach((pr) => { modelList.push( - new PullRequestModel(pr, projectName, baseUrl, callbackState) + new PullRequestModel(pr, pr.repository.project.name, baseUrl, callbackState) ); return pr; diff --git a/src/models/UserPreferences.tsx b/src/models/UserPreferences.tsx new file mode 100644 index 0000000..bcbdc3f --- /dev/null +++ b/src/models/UserPreferences.tsx @@ -0,0 +1,58 @@ +import { USER_SETTINGS_STORE_KEY } from "../common"; + +export class UserPreferences { + public showFilterByDefault: boolean = true; + public openPRNewWindow: boolean = true; + public selectedProjects: string[] = []; + public topNumberCompletedAbandoned: number = 25; + public selectedDefaultSorting: string = "desc"; + + constructor(public lastVisit: Date = new Date()) { + this.restoreToDefaults(); + } + + restoreToDefaults = (): void => { + this.showFilterByDefault = true; + this.openPRNewWindow = true; + this.selectedProjects = []; + this.topNumberCompletedAbandoned = 25; + this.selectedDefaultSorting = "desc"; + }; + + save = () => { + this.lastVisit = new Date(); + localStorage.setItem(USER_SETTINGS_STORE_KEY, JSON.stringify(this)); + }; + + load = () => { + try { + const cachedInstance = localStorage.getItem(USER_SETTINGS_STORE_KEY); + + if (!cachedInstance || cachedInstance.length === 0) { + return; + } + + const cachedUserSettings: UserPreferences = JSON.parse(cachedInstance); + const savedDate = new Date(cachedUserSettings.lastVisit.toString()); + + this.lastVisit = savedDate; + this.selectedDefaultSorting = cachedUserSettings.selectedDefaultSorting !== undefined + ? cachedUserSettings.selectedDefaultSorting + : this.selectedDefaultSorting; + this.openPRNewWindow = cachedUserSettings.openPRNewWindow !== undefined + ? cachedUserSettings.openPRNewWindow + : this.openPRNewWindow; + this.selectedProjects = cachedUserSettings.selectedProjects !== undefined + ? cachedUserSettings.selectedProjects + : this.selectedProjects; + this.showFilterByDefault = cachedUserSettings.showFilterByDefault !== undefined + ? cachedUserSettings.showFilterByDefault + : this.showFilterByDefault; + this.topNumberCompletedAbandoned = cachedUserSettings.topNumberCompletedAbandoned !== undefined + ? cachedUserSettings.topNumberCompletedAbandoned + : this.topNumberCompletedAbandoned; + } catch (error) { + this.restoreToDefaults(); + } + }; +} diff --git a/src/tabs/PulRequestsTabData.tsx b/src/tabs/PulRequestsTabData.tsx index 0a1b2fd..48c4186 100644 --- a/src/tabs/PulRequestsTabData.tsx +++ b/src/tabs/PulRequestsTabData.tsx @@ -6,21 +6,13 @@ import { } from "azure-devops-extension-api/Git/Git"; import { IStatusProps } from "azure-devops-ui/Status"; import { IColor } from "azure-devops-ui/Utilities/Color"; -import { IProjectInfo } from "azure-devops-extension-api/Common/CommonServices"; import { IdentityRef } from "azure-devops-extension-api/WebApi/WebApi"; import { TeamProjectReference, WebApiTagDefinition, } from "azure-devops-extension-api/Core/Core"; -import { ITableColumn, SortOrder, TableColumnStyle } from "azure-devops-ui/Table"; -import { - StatusColumn, - TitleColumn, - DetailsColumn, - ReviewersColumn, - DateColumn, -} from "../components/Columns"; import { PullRequestModel } from "../models/PullRequestModel"; +import { UserPreferencesInstance } from "../common"; export const refsPreffix = "refs/heads/"; @@ -93,7 +85,7 @@ export enum AlternateStatusPr { NotConflicts = "Not Conflicts", NotIsDraft = "Not Draft", NotReadyForCompletion = "Not Ready for Completion", - ReadForCompletion = "Ready for Completion" + ReadForCompletion = "Ready for Completion", } export class BranchDropDownItem { @@ -109,50 +101,6 @@ export class BranchDropDownItem { } } -export const columns: ITableColumn[] = [ - { - id: "status", - name: "", - renderCell: StatusColumn, - readonly: true, - width: -4, - minWidth: -4, - columnStyle: TableColumnStyle.Primary - }, - { - id: "title", - name: "Pull Request", - renderCell: TitleColumn, - readonly: true, - width: -46, - }, - { - className: "pipelines-two-line-cell", - id: "details", - name: "Details", - renderCell: DetailsColumn, - width: -20, - }, - { - id: "time", - name: "When", - readonly: true, - renderCell: DateColumn, - width: -10, - sortProps: { - ariaLabelAscending: "Sorted new to older", - ariaLabelDescending: "Sorted older to new", - sortOrder: SortOrder.descending - } - }, - { - id: "reviewers", - name: "Reviewers", - renderCell: ReviewersColumn, - width: -20, - }, -]; - export class PullRequestPolicy { public id: string = ""; public displayName: string = ""; @@ -224,7 +172,6 @@ export const pullRequestCriteria: GitPullRequestSearchCriteria = { export interface IPullRequestsTabState { projects: TeamProjectReference[]; - currentProject: IProjectInfo | TeamProjectReference | undefined; pullRequests: PullRequestModel[]; repositories: GitRepository[]; createdByList: IdentityRef[]; @@ -235,48 +182,44 @@ export interface IPullRequestsTabState { loading: boolean; errorMessage: string; pullRequestCount: number; - showToastMessage: boolean; - toastMessageToShow: string; + savedProjects: string[]; } -export function sortMethod( - a: - | BranchDropDownItem - | IdentityRef - | WebApiTagDefinition - | GitRepository - | TeamProjectReference, - b: - | BranchDropDownItem - | IdentityRef - | WebApiTagDefinition - | GitRepository - | TeamProjectReference -) { - if (a.hasOwnProperty("displayName")) { - const convertedA = a as BranchDropDownItem | IdentityRef; - const convertedB = b as BranchDropDownItem | IdentityRef; - if (convertedA.displayName! < convertedB.displayName!) { - return -1; - } - if (convertedA.displayName! > convertedB.displayName!) { - return 1; - } - } else if (a.hasOwnProperty("name")) { - const convertedA = a as - | WebApiTagDefinition - | GitRepository - | TeamProjectReference; - const convertedB = b as - | WebApiTagDefinition - | GitRepository - | TeamProjectReference; - if (convertedA.name < convertedB.name) { - return -1; - } - if (convertedA.name > convertedB.name) { - return 1; - } +export function sortBranchOrIdentity( + a: BranchDropDownItem | IdentityRef, + b: BranchDropDownItem | IdentityRef +): number { + if (a.displayName! < b.displayName!) { + return -1; + } + if (a.displayName! > b.displayName!) { + return 1; + } + + return 0; +} + +export function sortTagRepoTeamProject( + a: WebApiTagDefinition | GitRepository | TeamProjectReference, + b: WebApiTagDefinition | GitRepository | TeamProjectReference +): number { + if (a.name! < b.name!) { + return -1; } + if (a.name! > b.name!) { + return 1; + } + return 0; } + +export function sortPullRequests( + a: PullRequestModel, + b: PullRequestModel +) { + return UserPreferencesInstance.selectedDefaultSorting === "asc" + ? b.gitPullRequest.creationDate.getTime() - + a.gitPullRequest.creationDate.getTime() + : a.gitPullRequest.creationDate.getTime() - + b.gitPullRequest.creationDate.getTime(); +} diff --git a/src/tabs/PullRequestsTab.tsx b/src/tabs/PullRequestsTab.tsx index b57f70c..214a4d3 100644 --- a/src/tabs/PullRequestsTab.tsx +++ b/src/tabs/PullRequestsTab.tsx @@ -23,7 +23,6 @@ import * as DevOps from "azure-devops-extension-sdk"; // Azure DevOps API import { IProjectPageService, getClient } from "azure-devops-extension-api"; -import { CoreRestClient } from "azure-devops-extension-api/Core/CoreClient"; import { GitRestClient } from "azure-devops-extension-api/Git/GitClient"; import { IdentityRefWithVote, @@ -32,15 +31,10 @@ import { } from "azure-devops-extension-api/Git/Git"; // Azure DevOps UI -import { Toast } from "azure-devops-ui/Toast"; import { ListSelection } from "azure-devops-ui/List"; import { Observer } from "azure-devops-ui/Observer"; import { Dialog } from "azure-devops-ui/Dialog"; -import { - Filter, - FILTER_CHANGE_EVENT, - IFilterItemState, -} from "azure-devops-ui/Utilities/Filter"; +import { Filter, FILTER_CHANGE_EVENT } from "azure-devops-ui/Utilities/Filter"; import { DropdownMultiSelection, DropdownSelection, @@ -56,33 +50,42 @@ import { ColumnSorting, SortOrder, sortItems, + ITableColumn, + TableColumnStyle, } from "azure-devops-ui/Table"; import { ZeroData } from "azure-devops-ui/ZeroData"; import { IdentityRef } from "azure-devops-extension-api/WebApi/WebApi"; import { ObservableValue } from "azure-devops-ui/Core/Observable"; -import { IProjectInfo } from "azure-devops-extension-api/Common/CommonServices"; import { TeamProjectReference, - ProjectInfo, WebApiTagDefinition, + ProjectInfo, } from "azure-devops-extension-api/Core/Core"; -import { IListBoxItem } from "azure-devops-ui/ListBox"; import { FilterBarHub } from "../components/FilterBarHub"; import { hasPullRequestFailure } from "../models/constants"; import { ContentSize } from "azure-devops-ui/Callout"; import { IHeaderCommandBarItem } from "azure-devops-ui/HeaderCommandBar"; -import { ShowErrorMessage } from "../common"; +import { ShowErrorMessage, UserPreferencesInstance } from "../common"; +import { + StatusColumn, + TitleColumn, + DetailsColumn, + DateColumn, + ReviewersColumn, +} from "../components/Columns"; +import { IListBoxItem } from "azure-devops-ui/ListBox"; export interface IPullRequestTabProps { prType: PullRequestStatus; + projects: TeamProjectReference[]; onCountChange: (count: number) => void; + showToastMessage: (message: string) => void; } export class PullRequestsTab extends React.Component< IPullRequestTabProps, Data.IPullRequestsTabState > { - private toastRef: React.RefObject = React.createRef(); private baseUrl: string = ""; private prRowSelecion = new ListSelection({ selectOnFocus: true, @@ -90,7 +93,7 @@ export class PullRequestsTab extends React.Component< }); private isDialogOpen = new ObservableValue(false); private filter: Filter; - private selectedProject = new DropdownSelection(); + private selectedProjects = new DropdownMultiSelection(); private selectedAuthors = new DropdownMultiSelection(); private selectedRepos = new DropdownMultiSelection(); private selectedSourceBranches = new DropdownMultiSelection(); @@ -105,7 +108,6 @@ export class PullRequestsTab extends React.Component< >(); private readonly gitClient: GitRestClient; - private readonly coreClient: CoreRestClient; constructor(props: IPullRequestTabProps) { super(props); @@ -113,13 +115,11 @@ export class PullRequestsTab extends React.Component< this.selectedProjectChanged = this.selectedProjectChanged.bind(this); this.gitClient = getClient(GitRestClient); - this.coreClient = getClient(CoreRestClient); this.state = { - projects: [], + projects: props.projects, pullRequests: [], repositories: [], - currentProject: { id: "", name: "" }, createdByList: [], sourceBranchList: [], targetBranchList: [], @@ -128,8 +128,7 @@ export class PullRequestsTab extends React.Component< loading: true, errorMessage: "", pullRequestCount: 0, - showToastMessage: false, - toastMessageToShow: "", + savedProjects: [], }; this.filter = new Filter(); @@ -165,35 +164,17 @@ export class PullRequestsTab extends React.Component< }); } - private showToastMessage(message: string) { - this.setState({ showToastMessage: true, toastMessageToShow: message }); - - setTimeout(() => { - this.toastRef.current!.fadeOut().promise.then(() => { - this.setState({ showToastMessage: false, toastMessageToShow: message }); - }); - }, 5000); - } - - private getCurrentFilterNameKey(projectId?: string): string { - const { currentProject } = this.state; - const currentProjectId = - projectId !== undefined ? projectId : currentProject!.id; - const filterKey = `${currentProjectId}_${FILTER_STORE_KEY_NAME}`; - + private getCurrentFilterNameKey(): string { + const filterKey = `MY_${FILTER_STORE_KEY_NAME}`; return filterKey; } private saveCurrentFilters() { try { - const { currentProject } = this.state; const filterKey = this.getCurrentFilterNameKey(); const currentFilter = this.filter.getState(); - localStorage.setItem(FILTER_STORE_KEY_NAME, currentProject!.id); localStorage.setItem(filterKey, JSON.stringify(currentFilter)); - this.showToastMessage( - `Current selected filters have been saved - ${currentProject!.name}.` - ); + this.props.showToastMessage(`Current selected filters have been saved.`); } catch (error) { this.handleError(error); } @@ -201,34 +182,23 @@ export class PullRequestsTab extends React.Component< private clearSavedFilter() { try { - const { currentProject } = this.state; const filterKey = this.getCurrentFilterNameKey(); - this.showToastMessage( - `Saved filters have been removed of selected project - ${ - currentProject!.name - }.` - ); + this.props.showToastMessage(`Saved filters have been removed.`); localStorage.removeItem(FILTER_STORE_KEY_NAME); localStorage.removeItem(filterKey); - this.filter.reset(); - this.refresh(); } catch (error) { this.handleError(error); } } - private loadSavedFilter(storedSavedCurrentProjectId: string | null): void { + private loadSavedFilter(): void { try { - if (storedSavedCurrentProjectId != null) { - const saveFilterKeyName = this.getCurrentFilterNameKey( - storedSavedCurrentProjectId - ); - const storedSavedFilter = localStorage.getItem(saveFilterKeyName); + const saveFilterKeyName = this.getCurrentFilterNameKey(); + const storedSavedFilter = localStorage.getItem(saveFilterKeyName); - if (storedSavedFilter && storedSavedFilter.length > 0) { - const savedFilterState = JSON.parse(storedSavedFilter); - this.filter.setState(savedFilterState); - } + if (storedSavedFilter && storedSavedFilter.length > 0) { + const savedFilterState = JSON.parse(storedSavedFilter); + this.filter.setState(savedFilterState); } } catch (error) { this.handleError(error); @@ -236,43 +206,79 @@ export class PullRequestsTab extends React.Component< } private async initializePage() { - const self = this; + let { savedProjects } = this.state; + this.setState({ + pullRequests: [], + }); + this.getOrganizationBaseUrl() .then(async () => { const projectService = await DevOps.getService( getCommonServiceIdsValue("ProjectPageService") ); + this.loadSavedFilter(); + const currentProjectId = localStorage.getItem(FILTER_STORE_KEY_NAME); + const savedProjectsFilter = this.filter.getFilterItemValue( + "selectedProjects" + ); - this.loadSavedFilter(currentProjectId); - - this.getTeamProjects() - .then(async (projects) => { - this.setState({ - projects, - }); - - const currentProject = - currentProjectId && currentProjectId.length > 0 - ? currentProjectId - : (await projectService.getProject())!.id; - - const projectIndex = self.changeProjectSelectionTo(currentProject); - - this.getRepositories(projects[projectIndex]) - .then(() => { - this.getAllPullRequests().catch((error) => - this.handleError(error) - ); - }) - .catch((error) => { - this.handleError(error); - }); - }) - .catch((error) => { - this.handleError(error); - }); + if ( + savedProjectsFilter !== undefined && + savedProjectsFilter.length > 0 + ) { + savedProjects = savedProjectsFilter; + } + + if (savedProjects.length === 0) { + const currentProject = + currentProjectId && currentProjectId.length > 0 + ? currentProjectId + : (await projectService.getProject())!.id; + + savedProjects.push(...[currentProject.toString()]); + + this.selectedProjects.select( + this.state.projects.findIndex((p) => p.id === currentProject) + ); + } + + this.setState({ + savedProjects, + }); + + await this.loadAllProjects(); + }) + .catch((error) => { + this.handleError(error); + }); + } + + private async loadAllProjects(): Promise { + const { savedProjects } = this.state; + this.setState({ + pullRequests: [], + }); + + savedProjects.forEach(async (p) => { + const projectIndex = this.state.projects.findIndex( + (item) => item.id === p + ); + this.selectedProjects.select(projectIndex); + + this.loadProject(p); + }); + } + + private async loadProject(projectId: string): Promise { + const self = this; + return self + .getRepositories(projectId) + .then((repos) => { + this.getAllPullRequests(repos).catch((error) => + this.handleError(error) + ); }) .catch((error) => { this.handleError(error); @@ -287,21 +293,15 @@ export class PullRequestsTab extends React.Component< }); } - private async getRepositories( - project: IProjectInfo | TeamProjectReference - ): Promise { - this.setState({ - currentProject: project, - }); - - this.loadSavedFilter(project.id); + private async getRepositories(projectId: string): Promise { + const repos = await this.gitClient.getRepositories(projectId, true); - const repos = ( - await this.gitClient.getRepositories(project.name, true) - ).sort(Data.sortMethod); + let currentRepos = this.state.repositories; + currentRepos.push(...repos); + currentRepos = currentRepos.sort(Data.sortTagRepoTeamProject); this.setState({ - repositories: repos, + repositories: currentRepos, }); return repos; @@ -362,19 +362,10 @@ export class PullRequestsTab extends React.Component< this.props.onCountChange(newList.length); } - private async getTeamProjects(): Promise { - const projects = (await this.coreClient.getProjects(undefined, 1000)).sort( - Data.sortMethod - ); - return projects; - } - - private async getAllPullRequests() { + private async getAllPullRequests(repositories: GitRepository[]) { const self = this; this.setState({ loading: true }); - const { currentProject, repositories, pullRequests } = this.state; - - this.changeProjectSelectionTo(currentProject!.id); + let { pullRequests } = this.state; let newPullRequestList = Object.assign([], pullRequests); @@ -393,7 +384,7 @@ export class PullRequestsTab extends React.Component< const top = this.props.prType === PullRequestStatus.Completed || this.props.prType === PullRequestStatus.Abandoned - ? 25 + ? UserPreferencesInstance.topNumberCompletedAbandoned : 0; const loadedPullRequests = await this.gitClient.getPullRequests( @@ -409,7 +400,7 @@ export class PullRequestsTab extends React.Component< }) ) .then((loadedPullRequests) => { - loadedPullRequests.map((pr) => { + loadedPullRequests.forEach((pr) => { if (!pr || pr.length === 0) { return pr; } @@ -417,15 +408,14 @@ export class PullRequestsTab extends React.Component< newPullRequestList.push( ...PullRequestModel.PullRequestModel.getModels( pr, - this.state.currentProject!.name, this.baseUrl, (updatedPr) => { let { tagList } = self.state; updatedPr.labels .filter((t) => !this.hasFilterValue(tagList, t.id)) - .map((t) => { + .forEach((t) => { tagList.push(t); - tagList = tagList.sort(Data.sortMethod); + tagList = tagList.sort(Data.sortTagRepoTeamProject); return tagList; }); @@ -434,7 +424,6 @@ export class PullRequestsTab extends React.Component< tagList, }); - //this.pullRequestItemProvider.splice(0, this.pullRequestItemProvider.length, ...pullRequests); this.filterPullRequests(); } ) @@ -447,24 +436,11 @@ export class PullRequestsTab extends React.Component< }) .finally(() => { if (newPullRequestList.length > 0) { - newPullRequestList = newPullRequestList.sort( - ( - a: PullRequestModel.PullRequestModel, - b: PullRequestModel.PullRequestModel - ) => { - return ( - a.gitPullRequest.creationDate.getTime() - - b.gitPullRequest.creationDate.getTime() - ); - } - ); + pullRequests.push(...newPullRequestList); + pullRequests = pullRequests.sort(Data.sortPullRequests); this.setState({ - pullRequests: newPullRequestList, - }); - } else { - this.setState({ - pullRequests: [], + pullRequests, }); } @@ -475,12 +451,15 @@ export class PullRequestsTab extends React.Component< private loadLists() { const { pullRequests } = this.state; - this.reloadPullRequestItemProvider([]); + this.setState({ + loading: false + }); + this.reloadPullRequestItemProvider([]); this.pullRequestItemProvider.push(...pullRequests); - this.populateFilterBarFields(pullRequests); - this.setState({ loading: false }); + + this.loadSavedFilter(); this.filterPullRequests(); } @@ -488,6 +467,10 @@ export class PullRequestsTab extends React.Component< private filterPullRequests() { const { pullRequests } = this.state; + const selectedProjectsFilter = this.filter.getFilterItemValue( + "selectedProjects" + ); + const repositoriesFilter = this.filter.getFilterItemValue( "selectedRepos" ); @@ -518,6 +501,16 @@ export class PullRequestsTab extends React.Component< let filteredPullRequest = pullRequests; + if (selectedProjectsFilter && selectedProjectsFilter.length > 0) { + filteredPullRequest = filteredPullRequest.filter((pr) => { + const found = selectedProjectsFilter!.some((r) => { + return pr.gitPullRequest.repository.project.id === r; + }); + + return found; + }); + } + if (filterPullRequestTitle && filterPullRequestTitle.length > 0) { filteredPullRequest = pullRequests.filter((pr) => { const found = @@ -613,9 +606,10 @@ export class PullRequestsTab extends React.Component< (pr.isAllPoliciesOk === true && item === Data.AlternateStatusPr.ReadForCompletion && pr.hasFailures === false) || - (item === Data.AlternateStatusPr.NotReadyForCompletion && ( - pr.hasFailures === true || pr.isAllPoliciesOk === false)) || - (item === Data.AlternateStatusPr.HasNewChanges && pr.hasNewChanges()) + (item === Data.AlternateStatusPr.NotReadyForCompletion && + (pr.hasFailures === true || pr.isAllPoliciesOk === false)) || + (item === Data.AlternateStatusPr.HasNewChanges && + pr.hasNewChanges()) ); }); return found; @@ -701,7 +695,7 @@ export class PullRequestsTab extends React.Component< createdByList = []; reviewerList = []; - pullRequests.map((pr) => { + pullRequests.forEach((pr) => { let found = this.hasFilterValue( createdByList, pr.gitPullRequest.createdBy.id @@ -747,53 +741,10 @@ export class PullRequestsTab extends React.Component< return pr; }); - sourceBranchList = sourceBranchList.sort(Data.sortMethod); - targetBranchList = targetBranchList.sort(Data.sortMethod); - createdByList = createdByList.sort(Data.sortMethod); - reviewerList = reviewerList.sort(Data.sortMethod); - - const selectionObjectList = [ - "selectedAuthors", - "selectedReviewers", - "selectedSourceBranches", - "selectedTargetBranches", - ]; - const selectionFilterObjects = [ - this.selectedAuthors, - this.selectedReviewers, - this.selectedSourceBranches, - this.selectedTargetBranches, - ]; - const selectedItemsObjectList = [ - createdByList, - reviewerList, - sourceBranchList, - targetBranchList, - ]; - - selectionObjectList.forEach((objectKey, index) => { - const filterItemState = this.filter.getFilterItemState(objectKey); - - if (!filterItemState) { - return; - } - - const filterStateValueList: string[] = (filterItemState as IFilterItemState) - .value; - - filterStateValueList.map((item, itemIndex) => { - const found = this.hasFilterValue(selectedItemsObjectList[index], item); - - if (!found) { - filterStateValueList.splice(itemIndex, 1); - selectionFilterObjects[index].clear(); - } - - return found; - }); - - this.filter.setFilterItemState(objectKey, filterItemState); - }); + sourceBranchList = sourceBranchList.sort(Data.sortBranchOrIdentity); + targetBranchList = targetBranchList.sort(Data.sortBranchOrIdentity); + createdByList = createdByList.sort(Data.sortBranchOrIdentity); + reviewerList = reviewerList.sort(Data.sortBranchOrIdentity); this.setState({ sourceBranchList, @@ -804,7 +755,7 @@ export class PullRequestsTab extends React.Component< }; refresh = async () => { - await this.getAllPullRequests().catch((error) => this.handleError(error)); + await this.loadAllProjects(); }; onHelpDismiss = () => { @@ -813,7 +764,6 @@ export class PullRequestsTab extends React.Component< public render(): JSX.Element { const { - currentProject, pullRequests, projects, repositories, @@ -837,14 +787,14 @@ export class PullRequestsTab extends React.Component< return (
{ - this.refresh(); + this.initializePage(); + this.props.showToastMessage(`Filters have been restored to its original state.`); }} pullRequests={pullRequests} filter={this.filter} selectedProjectChanged={this.selectedProjectChanged} - selectedProject={this.selectedProject} + selectedProject={this.selectedProjects} projects={projects} repositories={repositories} selectedRepos={this.selectedRepos} @@ -887,56 +837,39 @@ export class PullRequestsTab extends React.Component< _event: React.SyntheticEvent, item: IListBoxItem ) { - const { projects } = this.state; - this.filter.reset(); - const projectIndex = this.changeProjectSelectionTo(item.id); - - this.getRepositories(projects[projectIndex]).then(() => { - this.refresh(); - }); - } + let { savedProjects } = this.state; + const foundIndex = savedProjects.findIndex((p) => p === item.id); - changeProjectSelectionTo(id: string): number { - const { projects } = this.state; - const projectIndex = projects.findIndex((p) => { - return p.id === id; - }); + if (foundIndex < 0) { + savedProjects.push(item.id); - this.selectedProject.select(projectIndex); + this.setState({ + savedProjects, + }); - return projectIndex; + await this.loadProject(item.id); + } } getRenderContent() { - const { - pullRequestCount, - pullRequests, - showToastMessage, - toastMessageToShow, - } = this.state; + const { pullRequestCount, pullRequests } = this.state; // Create the sorting behavior (delegate that is called when a column is sorted). const sortingBehavior = new ColumnSorting< PullRequestModel.PullRequestModel - >( - ( - columnIndex: number, - proposedSortOrder: SortOrder, - event: React.KeyboardEvent | React.MouseEvent - ) => { - this.pullRequestItemProvider.splice( - 0, - this.pullRequestItemProvider.length, - ...sortItems( - columnIndex, - proposedSortOrder, - this.sortFunctions, - Data.columns, - pullRequests - ) - ); - } - ); + >((columnIndex: number, proposedSortOrder: SortOrder) => { + this.pullRequestItemProvider.splice( + 0, + this.pullRequestItemProvider.length, + ...sortItems( + columnIndex, + proposedSortOrder, + this.sortFunctions, + this.columns, + pullRequests + ) + ); + }); if ( pullRequestCount === 0 && @@ -965,14 +898,11 @@ export class PullRequestsTab extends React.Component< contentProps={{ contentPadding: false }} headerCommandBarItems={this.listHeaderColumns} > - {showToastMessage && ( - - )} key={this.props.prType} behaviors={[sortingBehavior]} - columns={Data.columns} + columns={this.columns} itemProvider={this.pullRequestItemProvider} showLines={true} selection={this.prRowSelecion} @@ -1091,6 +1021,53 @@ export class PullRequestsTab extends React.Component< null, // Reviewers column ]; + columns: ITableColumn[] = [ + { + id: "status", + name: "", + renderCell: StatusColumn, + readonly: true, + width: -4, + minWidth: -4, + columnStyle: TableColumnStyle.Primary, + }, + { + id: "title", + name: "Pull Request", + renderCell: TitleColumn, + readonly: true, + width: -46, + }, + { + className: "pipelines-two-line-cell", + id: "details", + name: "Details", + renderCell: DetailsColumn, + width: -20, + }, + { + id: "time", + name: "When", + readonly: true, + renderCell: DateColumn, + width: -10, + sortProps: { + ariaLabelAscending: "Sorted new to older", + ariaLabelDescending: "Sorted older to new", + sortOrder: + UserPreferencesInstance.selectedDefaultSorting === "asc" + ? SortOrder.ascending + : SortOrder.descending, + }, + }, + { + id: "reviewers", + name: "Reviewers", + renderCell: ReviewersColumn, + width: -20, + }, + ]; + private listHeaderColumns: IHeaderCommandBarItem[] = [ { id: "refresh", diff --git a/tsconfig.json b/tsconfig.json index 57f7ef9..61900f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "ES2019", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/vss-extension.json b/vss-extension.json index 92dc406..d3bedd6 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "pull-request-manager-hub", "publisher": "caribeiro84", - "version": "1.0.0", + "version": "2.0.0", "name": "Pull Request Manager Hub", "description": "A complete and friendly Pull Request Hub to manage all your repositories' PRs with must have filters at one simplified view.", "public": true,