@plusscommunities/pluss-maintenance-web-feedback 1.1.37-beta.0 → 1.1.37-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +149 -64
- package/dist/index.esm.js +149 -64
- package/dist/index.umd.js +149 -64
- package/package.json +1 -1
- package/src/components/JobList.js +137 -68
package/dist/index.cjs.js
CHANGED
|
@@ -811,12 +811,26 @@ class JobList extends React.Component {
|
|
|
811
811
|
return params;
|
|
812
812
|
});
|
|
813
813
|
/**
|
|
814
|
-
* Minimum number of items to
|
|
815
|
-
* DynamoDB
|
|
816
|
-
*
|
|
817
|
-
*
|
|
814
|
+
* Minimum number of items to auto-fill before stopping background fetch.
|
|
815
|
+
* Because DynamoDB pages are unfiltered and the backend filters after
|
|
816
|
+
* query, a single page may yield very few matching results. We keep
|
|
817
|
+
* fetching in the background until we have this many items to display.
|
|
818
818
|
*/
|
|
819
|
-
_defineProperty__default["default"](this, "MIN_PAGE_SIZE",
|
|
819
|
+
_defineProperty__default["default"](this, "MIN_PAGE_SIZE", 20);
|
|
820
|
+
/**
|
|
821
|
+
* Monotonically increasing ID used to ignore stale fetch results.
|
|
822
|
+
* When a filter changes, fetchJobs() increments this counter. Any
|
|
823
|
+
* in-flight fetch or autoFillPages loop checks the counter before
|
|
824
|
+
* applying results — if it doesn't match, the results are discarded.
|
|
825
|
+
*
|
|
826
|
+
* Alternative: AbortController would actually cancel the HTTP request
|
|
827
|
+
* (axios supports it via the `signal` config option). That would save
|
|
828
|
+
* bandwidth and server load but requires threading `signal` through
|
|
829
|
+
* authedFunction → getJobs2 → fetchPage (3 layers). The fetchId
|
|
830
|
+
* approach is simpler and sufficient — stale requests still complete
|
|
831
|
+
* in the background but their results are ignored.
|
|
832
|
+
*/
|
|
833
|
+
_defineProperty__default["default"](this, "_fetchId", 0);
|
|
820
834
|
/**
|
|
821
835
|
* Fetch a single page from the server using the given lastKey.
|
|
822
836
|
*/
|
|
@@ -829,39 +843,38 @@ class JobList extends React.Component {
|
|
|
829
843
|
return maintenanceActions$1.getJobs2(auth.site, filters.status, filters.type, filters.priority, filters.assignee, filters.startTime, filters.endTime, filters.search, lastKey);
|
|
830
844
|
});
|
|
831
845
|
/**
|
|
832
|
-
* Fetch the first page
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
846
|
+
* Fetch the first page and render immediately.
|
|
847
|
+
* If fewer than MIN_PAGE_SIZE items returned and there's more data,
|
|
848
|
+
* kicks off a background loop that keeps fetching and appending
|
|
849
|
+
* so results appear progressively.
|
|
836
850
|
*/
|
|
837
851
|
_defineProperty__default["default"](this, "fetchJobs", async () => {
|
|
852
|
+
const fetchId = ++this._fetchId;
|
|
838
853
|
this.setState({
|
|
839
|
-
loading: true
|
|
854
|
+
loading: true,
|
|
855
|
+
loadingMore: false
|
|
840
856
|
}, async () => {
|
|
841
857
|
try {
|
|
842
|
-
|
|
843
|
-
|
|
858
|
+
const res = await this.fetchPage(null);
|
|
859
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
844
860
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const res = await this.fetchPage(lastKey);
|
|
848
|
-
const items = res.data.Items || [];
|
|
849
|
-
lastKey = res.data.LastKey || null;
|
|
850
|
-
allJobs = [...allJobs, ...items];
|
|
851
|
-
} while (lastKey && allJobs.length < this.MIN_PAGE_SIZE);
|
|
861
|
+
const items = res.data.Items || [];
|
|
862
|
+
const lastKey = res.data.LastKey || null;
|
|
852
863
|
this.setState({
|
|
853
|
-
jobs:
|
|
864
|
+
jobs: items,
|
|
854
865
|
lastKey,
|
|
855
866
|
hasMore: !!lastKey,
|
|
856
867
|
loading: false
|
|
857
868
|
});
|
|
869
|
+
this.props.jobsLoaded(items);
|
|
870
|
+
this.setRequesters(items);
|
|
858
871
|
|
|
859
|
-
//
|
|
860
|
-
this.
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
this.setRequesters(allJobs);
|
|
872
|
+
// Auto-fill in background if first page was sparse
|
|
873
|
+
if (lastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
874
|
+
this.autoFillPages(items, lastKey, fetchId);
|
|
875
|
+
}
|
|
864
876
|
} catch (error) {
|
|
877
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
865
878
|
console.error('fetchJobs', error);
|
|
866
879
|
this.setState({
|
|
867
880
|
loading: false
|
|
@@ -870,40 +883,87 @@ class JobList extends React.Component {
|
|
|
870
883
|
});
|
|
871
884
|
});
|
|
872
885
|
/**
|
|
873
|
-
*
|
|
874
|
-
*
|
|
886
|
+
* Background loop: keep fetching pages and appending results
|
|
887
|
+
* until we have MIN_PAGE_SIZE items or run out of data.
|
|
888
|
+
* Each page is rendered as it arrives so the user sees
|
|
889
|
+
* results appearing progressively.
|
|
890
|
+
*/
|
|
891
|
+
_defineProperty__default["default"](this, "autoFillPages", async (existingJobs, startKey, fetchId) => {
|
|
892
|
+
this.setState({
|
|
893
|
+
loadingMore: true
|
|
894
|
+
});
|
|
895
|
+
let currentJobs = existingJobs;
|
|
896
|
+
let currentLastKey = startKey;
|
|
897
|
+
try {
|
|
898
|
+
while (currentLastKey && currentJobs.length < this.MIN_PAGE_SIZE) {
|
|
899
|
+
const res = await this.fetchPage(currentLastKey);
|
|
900
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
901
|
+
|
|
902
|
+
const items = res.data.Items || [];
|
|
903
|
+
currentLastKey = res.data.LastKey || null;
|
|
904
|
+
currentJobs = [...currentJobs, ...items];
|
|
905
|
+
|
|
906
|
+
// Append to UI immediately after each page arrives
|
|
907
|
+
this.setState({
|
|
908
|
+
jobs: currentJobs,
|
|
909
|
+
lastKey: currentLastKey,
|
|
910
|
+
hasMore: !!currentLastKey
|
|
911
|
+
});
|
|
912
|
+
if (items.length > 0) {
|
|
913
|
+
this.props.jobsLoaded(items);
|
|
914
|
+
this.setRequesters(currentJobs);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
919
|
+
console.error('autoFillPages', error);
|
|
920
|
+
} finally {
|
|
921
|
+
if (this._fetchId === fetchId) {
|
|
922
|
+
this.setState({
|
|
923
|
+
loadingMore: false
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
/**
|
|
929
|
+
* Load the next batch of jobs when the user clicks "Load More".
|
|
930
|
+
* Fetches one page and also auto-fills in background if sparse.
|
|
875
931
|
*/
|
|
876
932
|
_defineProperty__default["default"](this, "loadMore", async () => {
|
|
933
|
+
const fetchId = ++this._fetchId;
|
|
877
934
|
const {
|
|
878
|
-
lastKey
|
|
935
|
+
lastKey,
|
|
936
|
+
jobs
|
|
879
937
|
} = this.state;
|
|
880
938
|
this.setState({
|
|
881
939
|
loadingMore: true
|
|
882
940
|
}, async () => {
|
|
883
941
|
try {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const existingCount = this.state.jobs.length;
|
|
942
|
+
const res = await this.fetchPage(lastKey);
|
|
943
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
887
944
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
loadingMore: false
|
|
900
|
-
}));
|
|
945
|
+
const items = res.data.Items || [];
|
|
946
|
+
const newLastKey = res.data.LastKey || null;
|
|
947
|
+
const updatedJobs = [...jobs, ...items];
|
|
948
|
+
this.setState({
|
|
949
|
+
jobs: updatedJobs,
|
|
950
|
+
lastKey: newLastKey,
|
|
951
|
+
hasMore: !!newLastKey
|
|
952
|
+
});
|
|
953
|
+
if (items.length > 0) {
|
|
954
|
+
this.props.jobsLoaded(items);
|
|
955
|
+
}
|
|
901
956
|
|
|
902
|
-
//
|
|
903
|
-
if (
|
|
904
|
-
this.
|
|
957
|
+
// Auto-fill in background if this page was sparse
|
|
958
|
+
if (newLastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
959
|
+
this.autoFillPages(updatedJobs, newLastKey, fetchId);
|
|
960
|
+
} else {
|
|
961
|
+
this.setState({
|
|
962
|
+
loadingMore: false
|
|
963
|
+
});
|
|
905
964
|
}
|
|
906
965
|
} catch (error) {
|
|
966
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
907
967
|
console.error('loadMore', error);
|
|
908
968
|
this.setState({
|
|
909
969
|
loadingMore: false
|
|
@@ -1116,14 +1176,20 @@ class JobList extends React.Component {
|
|
|
1116
1176
|
return r.userID === this.state.selectedRequesterFilter;
|
|
1117
1177
|
});
|
|
1118
1178
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1179
|
+
|
|
1180
|
+
// Skip sorting while auto-fill is appending results to avoid jumpiness.
|
|
1181
|
+
// Sort is re-applied when the user clicks a column header or when
|
|
1182
|
+
// auto-fill completes.
|
|
1183
|
+
if (!this.state.loadingMore) {
|
|
1184
|
+
source = ___default["default"].sortBy(source, event => {
|
|
1185
|
+
if (this.state.sortColumn === 'assigned') {
|
|
1186
|
+
return event.Assignee ? event.Assignee.displayName : 'Unassigned';
|
|
1187
|
+
}
|
|
1188
|
+
if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
|
|
1189
|
+
return event.createdUnix;
|
|
1190
|
+
});
|
|
1191
|
+
if (this.state.sortDesc) source.reverse();
|
|
1192
|
+
}
|
|
1127
1193
|
return source;
|
|
1128
1194
|
});
|
|
1129
1195
|
_defineProperty__default["default"](this, "getCustomFieldValue", field => {
|
|
@@ -1705,7 +1771,29 @@ class JobList extends React.Component {
|
|
|
1705
1771
|
hasMore,
|
|
1706
1772
|
loadingMore
|
|
1707
1773
|
} = this.state;
|
|
1708
|
-
if (!hasMore) return null;
|
|
1774
|
+
if (!hasMore && !loadingMore) return null;
|
|
1775
|
+
|
|
1776
|
+
// During background auto-fill, show a subtle spinner without a button
|
|
1777
|
+
if (loadingMore) {
|
|
1778
|
+
return /*#__PURE__*/React__default["default"].createElement("div", {
|
|
1779
|
+
className: "flex flex-center-row",
|
|
1780
|
+
style: {
|
|
1781
|
+
padding: '16px 0'
|
|
1782
|
+
}
|
|
1783
|
+
}, /*#__PURE__*/React__default["default"].createElement(FontAwesome__default["default"], {
|
|
1784
|
+
name: "spinner fa-pulse fa-fw",
|
|
1785
|
+
style: {
|
|
1786
|
+
fontSize: 16,
|
|
1787
|
+
color: PlussCore.Colours.TEXT_MID,
|
|
1788
|
+
marginRight: 8
|
|
1789
|
+
}
|
|
1790
|
+
}), /*#__PURE__*/React__default["default"].createElement("span", {
|
|
1791
|
+
className: "fontRegular fontSize-13",
|
|
1792
|
+
style: {
|
|
1793
|
+
color: PlussCore.Colours.TEXT_MID
|
|
1794
|
+
}
|
|
1795
|
+
}, "Loading more results\u2026"));
|
|
1796
|
+
}
|
|
1709
1797
|
return /*#__PURE__*/React__default["default"].createElement("div", {
|
|
1710
1798
|
className: "flex flex-center-row",
|
|
1711
1799
|
style: {
|
|
@@ -1715,21 +1803,18 @@ class JobList extends React.Component {
|
|
|
1715
1803
|
inline: true,
|
|
1716
1804
|
buttonType: "tertiary",
|
|
1717
1805
|
onClick: this.loadMore,
|
|
1718
|
-
isActive:
|
|
1719
|
-
},
|
|
1720
|
-
name: "spinner fa-pulse fa-fw",
|
|
1721
|
-
style: {
|
|
1722
|
-
marginRight: 8
|
|
1723
|
-
}
|
|
1724
|
-
}), "Loading more\u2026") : 'Load More'));
|
|
1806
|
+
isActive: true
|
|
1807
|
+
}, "Load More"));
|
|
1725
1808
|
}
|
|
1726
1809
|
renderContent() {
|
|
1727
1810
|
const {
|
|
1728
1811
|
loading,
|
|
1812
|
+
loadingMore,
|
|
1729
1813
|
jobs
|
|
1730
1814
|
} = this.state;
|
|
1731
1815
|
if (loading) return this.renderLoading();
|
|
1732
|
-
if (___default["default"].isEmpty(jobs)) return this.renderEmpty();
|
|
1816
|
+
if (___default["default"].isEmpty(jobs) && !loadingMore) return this.renderEmpty();
|
|
1817
|
+
if (___default["default"].isEmpty(jobs)) return this.renderLoading();
|
|
1733
1818
|
return /*#__PURE__*/React__default["default"].createElement("div", null, /*#__PURE__*/React__default["default"].createElement(reactBootstrap.Table, {
|
|
1734
1819
|
className: "plussTable",
|
|
1735
1820
|
striped: true,
|
package/dist/index.esm.js
CHANGED
|
@@ -780,12 +780,26 @@ class JobList extends Component {
|
|
|
780
780
|
return params;
|
|
781
781
|
});
|
|
782
782
|
/**
|
|
783
|
-
* Minimum number of items to
|
|
784
|
-
* DynamoDB
|
|
785
|
-
*
|
|
786
|
-
*
|
|
783
|
+
* Minimum number of items to auto-fill before stopping background fetch.
|
|
784
|
+
* Because DynamoDB pages are unfiltered and the backend filters after
|
|
785
|
+
* query, a single page may yield very few matching results. We keep
|
|
786
|
+
* fetching in the background until we have this many items to display.
|
|
787
787
|
*/
|
|
788
|
-
_defineProperty(this, "MIN_PAGE_SIZE",
|
|
788
|
+
_defineProperty(this, "MIN_PAGE_SIZE", 20);
|
|
789
|
+
/**
|
|
790
|
+
* Monotonically increasing ID used to ignore stale fetch results.
|
|
791
|
+
* When a filter changes, fetchJobs() increments this counter. Any
|
|
792
|
+
* in-flight fetch or autoFillPages loop checks the counter before
|
|
793
|
+
* applying results — if it doesn't match, the results are discarded.
|
|
794
|
+
*
|
|
795
|
+
* Alternative: AbortController would actually cancel the HTTP request
|
|
796
|
+
* (axios supports it via the `signal` config option). That would save
|
|
797
|
+
* bandwidth and server load but requires threading `signal` through
|
|
798
|
+
* authedFunction → getJobs2 → fetchPage (3 layers). The fetchId
|
|
799
|
+
* approach is simpler and sufficient — stale requests still complete
|
|
800
|
+
* in the background but their results are ignored.
|
|
801
|
+
*/
|
|
802
|
+
_defineProperty(this, "_fetchId", 0);
|
|
789
803
|
/**
|
|
790
804
|
* Fetch a single page from the server using the given lastKey.
|
|
791
805
|
*/
|
|
@@ -798,39 +812,38 @@ class JobList extends Component {
|
|
|
798
812
|
return maintenanceActions$1.getJobs2(auth.site, filters.status, filters.type, filters.priority, filters.assignee, filters.startTime, filters.endTime, filters.search, lastKey);
|
|
799
813
|
});
|
|
800
814
|
/**
|
|
801
|
-
* Fetch the first page
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
815
|
+
* Fetch the first page and render immediately.
|
|
816
|
+
* If fewer than MIN_PAGE_SIZE items returned and there's more data,
|
|
817
|
+
* kicks off a background loop that keeps fetching and appending
|
|
818
|
+
* so results appear progressively.
|
|
805
819
|
*/
|
|
806
820
|
_defineProperty(this, "fetchJobs", async () => {
|
|
821
|
+
const fetchId = ++this._fetchId;
|
|
807
822
|
this.setState({
|
|
808
|
-
loading: true
|
|
823
|
+
loading: true,
|
|
824
|
+
loadingMore: false
|
|
809
825
|
}, async () => {
|
|
810
826
|
try {
|
|
811
|
-
|
|
812
|
-
|
|
827
|
+
const res = await this.fetchPage(null);
|
|
828
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
813
829
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const res = await this.fetchPage(lastKey);
|
|
817
|
-
const items = res.data.Items || [];
|
|
818
|
-
lastKey = res.data.LastKey || null;
|
|
819
|
-
allJobs = [...allJobs, ...items];
|
|
820
|
-
} while (lastKey && allJobs.length < this.MIN_PAGE_SIZE);
|
|
830
|
+
const items = res.data.Items || [];
|
|
831
|
+
const lastKey = res.data.LastKey || null;
|
|
821
832
|
this.setState({
|
|
822
|
-
jobs:
|
|
833
|
+
jobs: items,
|
|
823
834
|
lastKey,
|
|
824
835
|
hasMore: !!lastKey,
|
|
825
836
|
loading: false
|
|
826
837
|
});
|
|
838
|
+
this.props.jobsLoaded(items);
|
|
839
|
+
this.setRequesters(items);
|
|
827
840
|
|
|
828
|
-
//
|
|
829
|
-
this.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
this.setRequesters(allJobs);
|
|
841
|
+
// Auto-fill in background if first page was sparse
|
|
842
|
+
if (lastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
843
|
+
this.autoFillPages(items, lastKey, fetchId);
|
|
844
|
+
}
|
|
833
845
|
} catch (error) {
|
|
846
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
834
847
|
console.error('fetchJobs', error);
|
|
835
848
|
this.setState({
|
|
836
849
|
loading: false
|
|
@@ -839,40 +852,87 @@ class JobList extends Component {
|
|
|
839
852
|
});
|
|
840
853
|
});
|
|
841
854
|
/**
|
|
842
|
-
*
|
|
843
|
-
*
|
|
855
|
+
* Background loop: keep fetching pages and appending results
|
|
856
|
+
* until we have MIN_PAGE_SIZE items or run out of data.
|
|
857
|
+
* Each page is rendered as it arrives so the user sees
|
|
858
|
+
* results appearing progressively.
|
|
859
|
+
*/
|
|
860
|
+
_defineProperty(this, "autoFillPages", async (existingJobs, startKey, fetchId) => {
|
|
861
|
+
this.setState({
|
|
862
|
+
loadingMore: true
|
|
863
|
+
});
|
|
864
|
+
let currentJobs = existingJobs;
|
|
865
|
+
let currentLastKey = startKey;
|
|
866
|
+
try {
|
|
867
|
+
while (currentLastKey && currentJobs.length < this.MIN_PAGE_SIZE) {
|
|
868
|
+
const res = await this.fetchPage(currentLastKey);
|
|
869
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
870
|
+
|
|
871
|
+
const items = res.data.Items || [];
|
|
872
|
+
currentLastKey = res.data.LastKey || null;
|
|
873
|
+
currentJobs = [...currentJobs, ...items];
|
|
874
|
+
|
|
875
|
+
// Append to UI immediately after each page arrives
|
|
876
|
+
this.setState({
|
|
877
|
+
jobs: currentJobs,
|
|
878
|
+
lastKey: currentLastKey,
|
|
879
|
+
hasMore: !!currentLastKey
|
|
880
|
+
});
|
|
881
|
+
if (items.length > 0) {
|
|
882
|
+
this.props.jobsLoaded(items);
|
|
883
|
+
this.setRequesters(currentJobs);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
888
|
+
console.error('autoFillPages', error);
|
|
889
|
+
} finally {
|
|
890
|
+
if (this._fetchId === fetchId) {
|
|
891
|
+
this.setState({
|
|
892
|
+
loadingMore: false
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
/**
|
|
898
|
+
* Load the next batch of jobs when the user clicks "Load More".
|
|
899
|
+
* Fetches one page and also auto-fills in background if sparse.
|
|
844
900
|
*/
|
|
845
901
|
_defineProperty(this, "loadMore", async () => {
|
|
902
|
+
const fetchId = ++this._fetchId;
|
|
846
903
|
const {
|
|
847
|
-
lastKey
|
|
904
|
+
lastKey,
|
|
905
|
+
jobs
|
|
848
906
|
} = this.state;
|
|
849
907
|
this.setState({
|
|
850
908
|
loadingMore: true
|
|
851
909
|
}, async () => {
|
|
852
910
|
try {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const existingCount = this.state.jobs.length;
|
|
911
|
+
const res = await this.fetchPage(lastKey);
|
|
912
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
856
913
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
loadingMore: false
|
|
869
|
-
}));
|
|
914
|
+
const items = res.data.Items || [];
|
|
915
|
+
const newLastKey = res.data.LastKey || null;
|
|
916
|
+
const updatedJobs = [...jobs, ...items];
|
|
917
|
+
this.setState({
|
|
918
|
+
jobs: updatedJobs,
|
|
919
|
+
lastKey: newLastKey,
|
|
920
|
+
hasMore: !!newLastKey
|
|
921
|
+
});
|
|
922
|
+
if (items.length > 0) {
|
|
923
|
+
this.props.jobsLoaded(items);
|
|
924
|
+
}
|
|
870
925
|
|
|
871
|
-
//
|
|
872
|
-
if (
|
|
873
|
-
this.
|
|
926
|
+
// Auto-fill in background if this page was sparse
|
|
927
|
+
if (newLastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
928
|
+
this.autoFillPages(updatedJobs, newLastKey, fetchId);
|
|
929
|
+
} else {
|
|
930
|
+
this.setState({
|
|
931
|
+
loadingMore: false
|
|
932
|
+
});
|
|
874
933
|
}
|
|
875
934
|
} catch (error) {
|
|
935
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
876
936
|
console.error('loadMore', error);
|
|
877
937
|
this.setState({
|
|
878
938
|
loadingMore: false
|
|
@@ -1085,14 +1145,20 @@ class JobList extends Component {
|
|
|
1085
1145
|
return r.userID === this.state.selectedRequesterFilter;
|
|
1086
1146
|
});
|
|
1087
1147
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1148
|
+
|
|
1149
|
+
// Skip sorting while auto-fill is appending results to avoid jumpiness.
|
|
1150
|
+
// Sort is re-applied when the user clicks a column header or when
|
|
1151
|
+
// auto-fill completes.
|
|
1152
|
+
if (!this.state.loadingMore) {
|
|
1153
|
+
source = _.sortBy(source, event => {
|
|
1154
|
+
if (this.state.sortColumn === 'assigned') {
|
|
1155
|
+
return event.Assignee ? event.Assignee.displayName : 'Unassigned';
|
|
1156
|
+
}
|
|
1157
|
+
if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
|
|
1158
|
+
return event.createdUnix;
|
|
1159
|
+
});
|
|
1160
|
+
if (this.state.sortDesc) source.reverse();
|
|
1161
|
+
}
|
|
1096
1162
|
return source;
|
|
1097
1163
|
});
|
|
1098
1164
|
_defineProperty(this, "getCustomFieldValue", field => {
|
|
@@ -1674,7 +1740,29 @@ class JobList extends Component {
|
|
|
1674
1740
|
hasMore,
|
|
1675
1741
|
loadingMore
|
|
1676
1742
|
} = this.state;
|
|
1677
|
-
if (!hasMore) return null;
|
|
1743
|
+
if (!hasMore && !loadingMore) return null;
|
|
1744
|
+
|
|
1745
|
+
// During background auto-fill, show a subtle spinner without a button
|
|
1746
|
+
if (loadingMore) {
|
|
1747
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1748
|
+
className: "flex flex-center-row",
|
|
1749
|
+
style: {
|
|
1750
|
+
padding: '16px 0'
|
|
1751
|
+
}
|
|
1752
|
+
}, /*#__PURE__*/React.createElement(FontAwesome, {
|
|
1753
|
+
name: "spinner fa-pulse fa-fw",
|
|
1754
|
+
style: {
|
|
1755
|
+
fontSize: 16,
|
|
1756
|
+
color: Colours$3.TEXT_MID,
|
|
1757
|
+
marginRight: 8
|
|
1758
|
+
}
|
|
1759
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
1760
|
+
className: "fontRegular fontSize-13",
|
|
1761
|
+
style: {
|
|
1762
|
+
color: Colours$3.TEXT_MID
|
|
1763
|
+
}
|
|
1764
|
+
}, "Loading more results\u2026"));
|
|
1765
|
+
}
|
|
1678
1766
|
return /*#__PURE__*/React.createElement("div", {
|
|
1679
1767
|
className: "flex flex-center-row",
|
|
1680
1768
|
style: {
|
|
@@ -1684,21 +1772,18 @@ class JobList extends Component {
|
|
|
1684
1772
|
inline: true,
|
|
1685
1773
|
buttonType: "tertiary",
|
|
1686
1774
|
onClick: this.loadMore,
|
|
1687
|
-
isActive:
|
|
1688
|
-
},
|
|
1689
|
-
name: "spinner fa-pulse fa-fw",
|
|
1690
|
-
style: {
|
|
1691
|
-
marginRight: 8
|
|
1692
|
-
}
|
|
1693
|
-
}), "Loading more\u2026") : 'Load More'));
|
|
1775
|
+
isActive: true
|
|
1776
|
+
}, "Load More"));
|
|
1694
1777
|
}
|
|
1695
1778
|
renderContent() {
|
|
1696
1779
|
const {
|
|
1697
1780
|
loading,
|
|
1781
|
+
loadingMore,
|
|
1698
1782
|
jobs
|
|
1699
1783
|
} = this.state;
|
|
1700
1784
|
if (loading) return this.renderLoading();
|
|
1701
|
-
if (_.isEmpty(jobs)) return this.renderEmpty();
|
|
1785
|
+
if (_.isEmpty(jobs) && !loadingMore) return this.renderEmpty();
|
|
1786
|
+
if (_.isEmpty(jobs)) return this.renderLoading();
|
|
1702
1787
|
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Table, {
|
|
1703
1788
|
className: "plussTable",
|
|
1704
1789
|
striped: true,
|
package/dist/index.umd.js
CHANGED
|
@@ -800,12 +800,26 @@
|
|
|
800
800
|
return params;
|
|
801
801
|
});
|
|
802
802
|
/**
|
|
803
|
-
* Minimum number of items to
|
|
804
|
-
* DynamoDB
|
|
805
|
-
*
|
|
806
|
-
*
|
|
803
|
+
* Minimum number of items to auto-fill before stopping background fetch.
|
|
804
|
+
* Because DynamoDB pages are unfiltered and the backend filters after
|
|
805
|
+
* query, a single page may yield very few matching results. We keep
|
|
806
|
+
* fetching in the background until we have this many items to display.
|
|
807
807
|
*/
|
|
808
|
-
_defineProperty__default["default"](this, "MIN_PAGE_SIZE",
|
|
808
|
+
_defineProperty__default["default"](this, "MIN_PAGE_SIZE", 20);
|
|
809
|
+
/**
|
|
810
|
+
* Monotonically increasing ID used to ignore stale fetch results.
|
|
811
|
+
* When a filter changes, fetchJobs() increments this counter. Any
|
|
812
|
+
* in-flight fetch or autoFillPages loop checks the counter before
|
|
813
|
+
* applying results — if it doesn't match, the results are discarded.
|
|
814
|
+
*
|
|
815
|
+
* Alternative: AbortController would actually cancel the HTTP request
|
|
816
|
+
* (axios supports it via the `signal` config option). That would save
|
|
817
|
+
* bandwidth and server load but requires threading `signal` through
|
|
818
|
+
* authedFunction → getJobs2 → fetchPage (3 layers). The fetchId
|
|
819
|
+
* approach is simpler and sufficient — stale requests still complete
|
|
820
|
+
* in the background but their results are ignored.
|
|
821
|
+
*/
|
|
822
|
+
_defineProperty__default["default"](this, "_fetchId", 0);
|
|
809
823
|
/**
|
|
810
824
|
* Fetch a single page from the server using the given lastKey.
|
|
811
825
|
*/
|
|
@@ -818,39 +832,38 @@
|
|
|
818
832
|
return maintenanceActions$1.getJobs2(auth.site, filters.status, filters.type, filters.priority, filters.assignee, filters.startTime, filters.endTime, filters.search, lastKey);
|
|
819
833
|
});
|
|
820
834
|
/**
|
|
821
|
-
* Fetch the first page
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
835
|
+
* Fetch the first page and render immediately.
|
|
836
|
+
* If fewer than MIN_PAGE_SIZE items returned and there's more data,
|
|
837
|
+
* kicks off a background loop that keeps fetching and appending
|
|
838
|
+
* so results appear progressively.
|
|
825
839
|
*/
|
|
826
840
|
_defineProperty__default["default"](this, "fetchJobs", async () => {
|
|
841
|
+
const fetchId = ++this._fetchId;
|
|
827
842
|
this.setState({
|
|
828
|
-
loading: true
|
|
843
|
+
loading: true,
|
|
844
|
+
loadingMore: false
|
|
829
845
|
}, async () => {
|
|
830
846
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
847
|
+
const res = await this.fetchPage(null);
|
|
848
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
833
849
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const res = await this.fetchPage(lastKey);
|
|
837
|
-
const items = res.data.Items || [];
|
|
838
|
-
lastKey = res.data.LastKey || null;
|
|
839
|
-
allJobs = [...allJobs, ...items];
|
|
840
|
-
} while (lastKey && allJobs.length < this.MIN_PAGE_SIZE);
|
|
850
|
+
const items = res.data.Items || [];
|
|
851
|
+
const lastKey = res.data.LastKey || null;
|
|
841
852
|
this.setState({
|
|
842
|
-
jobs:
|
|
853
|
+
jobs: items,
|
|
843
854
|
lastKey,
|
|
844
855
|
hasMore: !!lastKey,
|
|
845
856
|
loading: false
|
|
846
857
|
});
|
|
858
|
+
this.props.jobsLoaded(items);
|
|
859
|
+
this.setRequesters(items);
|
|
847
860
|
|
|
848
|
-
//
|
|
849
|
-
this.
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
this.setRequesters(allJobs);
|
|
861
|
+
// Auto-fill in background if first page was sparse
|
|
862
|
+
if (lastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
863
|
+
this.autoFillPages(items, lastKey, fetchId);
|
|
864
|
+
}
|
|
853
865
|
} catch (error) {
|
|
866
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
854
867
|
console.error('fetchJobs', error);
|
|
855
868
|
this.setState({
|
|
856
869
|
loading: false
|
|
@@ -859,40 +872,87 @@
|
|
|
859
872
|
});
|
|
860
873
|
});
|
|
861
874
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
875
|
+
* Background loop: keep fetching pages and appending results
|
|
876
|
+
* until we have MIN_PAGE_SIZE items or run out of data.
|
|
877
|
+
* Each page is rendered as it arrives so the user sees
|
|
878
|
+
* results appearing progressively.
|
|
879
|
+
*/
|
|
880
|
+
_defineProperty__default["default"](this, "autoFillPages", async (existingJobs, startKey, fetchId) => {
|
|
881
|
+
this.setState({
|
|
882
|
+
loadingMore: true
|
|
883
|
+
});
|
|
884
|
+
let currentJobs = existingJobs;
|
|
885
|
+
let currentLastKey = startKey;
|
|
886
|
+
try {
|
|
887
|
+
while (currentLastKey && currentJobs.length < this.MIN_PAGE_SIZE) {
|
|
888
|
+
const res = await this.fetchPage(currentLastKey);
|
|
889
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
890
|
+
|
|
891
|
+
const items = res.data.Items || [];
|
|
892
|
+
currentLastKey = res.data.LastKey || null;
|
|
893
|
+
currentJobs = [...currentJobs, ...items];
|
|
894
|
+
|
|
895
|
+
// Append to UI immediately after each page arrives
|
|
896
|
+
this.setState({
|
|
897
|
+
jobs: currentJobs,
|
|
898
|
+
lastKey: currentLastKey,
|
|
899
|
+
hasMore: !!currentLastKey
|
|
900
|
+
});
|
|
901
|
+
if (items.length > 0) {
|
|
902
|
+
this.props.jobsLoaded(items);
|
|
903
|
+
this.setRequesters(currentJobs);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
908
|
+
console.error('autoFillPages', error);
|
|
909
|
+
} finally {
|
|
910
|
+
if (this._fetchId === fetchId) {
|
|
911
|
+
this.setState({
|
|
912
|
+
loadingMore: false
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
/**
|
|
918
|
+
* Load the next batch of jobs when the user clicks "Load More".
|
|
919
|
+
* Fetches one page and also auto-fills in background if sparse.
|
|
864
920
|
*/
|
|
865
921
|
_defineProperty__default["default"](this, "loadMore", async () => {
|
|
922
|
+
const fetchId = ++this._fetchId;
|
|
866
923
|
const {
|
|
867
|
-
lastKey
|
|
924
|
+
lastKey,
|
|
925
|
+
jobs
|
|
868
926
|
} = this.state;
|
|
869
927
|
this.setState({
|
|
870
928
|
loadingMore: true
|
|
871
929
|
}, async () => {
|
|
872
930
|
try {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const existingCount = this.state.jobs.length;
|
|
931
|
+
const res = await this.fetchPage(lastKey);
|
|
932
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
876
933
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
loadingMore: false
|
|
889
|
-
}));
|
|
934
|
+
const items = res.data.Items || [];
|
|
935
|
+
const newLastKey = res.data.LastKey || null;
|
|
936
|
+
const updatedJobs = [...jobs, ...items];
|
|
937
|
+
this.setState({
|
|
938
|
+
jobs: updatedJobs,
|
|
939
|
+
lastKey: newLastKey,
|
|
940
|
+
hasMore: !!newLastKey
|
|
941
|
+
});
|
|
942
|
+
if (items.length > 0) {
|
|
943
|
+
this.props.jobsLoaded(items);
|
|
944
|
+
}
|
|
890
945
|
|
|
891
|
-
//
|
|
892
|
-
if (
|
|
893
|
-
this.
|
|
946
|
+
// Auto-fill in background if this page was sparse
|
|
947
|
+
if (newLastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
948
|
+
this.autoFillPages(updatedJobs, newLastKey, fetchId);
|
|
949
|
+
} else {
|
|
950
|
+
this.setState({
|
|
951
|
+
loadingMore: false
|
|
952
|
+
});
|
|
894
953
|
}
|
|
895
954
|
} catch (error) {
|
|
955
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
896
956
|
console.error('loadMore', error);
|
|
897
957
|
this.setState({
|
|
898
958
|
loadingMore: false
|
|
@@ -1105,14 +1165,20 @@
|
|
|
1105
1165
|
return r.userID === this.state.selectedRequesterFilter;
|
|
1106
1166
|
});
|
|
1107
1167
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1168
|
+
|
|
1169
|
+
// Skip sorting while auto-fill is appending results to avoid jumpiness.
|
|
1170
|
+
// Sort is re-applied when the user clicks a column header or when
|
|
1171
|
+
// auto-fill completes.
|
|
1172
|
+
if (!this.state.loadingMore) {
|
|
1173
|
+
source = ___default["default"].sortBy(source, event => {
|
|
1174
|
+
if (this.state.sortColumn === 'assigned') {
|
|
1175
|
+
return event.Assignee ? event.Assignee.displayName : 'Unassigned';
|
|
1176
|
+
}
|
|
1177
|
+
if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
|
|
1178
|
+
return event.createdUnix;
|
|
1179
|
+
});
|
|
1180
|
+
if (this.state.sortDesc) source.reverse();
|
|
1181
|
+
}
|
|
1116
1182
|
return source;
|
|
1117
1183
|
});
|
|
1118
1184
|
_defineProperty__default["default"](this, "getCustomFieldValue", field => {
|
|
@@ -1694,7 +1760,29 @@
|
|
|
1694
1760
|
hasMore,
|
|
1695
1761
|
loadingMore
|
|
1696
1762
|
} = this.state;
|
|
1697
|
-
if (!hasMore) return null;
|
|
1763
|
+
if (!hasMore && !loadingMore) return null;
|
|
1764
|
+
|
|
1765
|
+
// During background auto-fill, show a subtle spinner without a button
|
|
1766
|
+
if (loadingMore) {
|
|
1767
|
+
return /*#__PURE__*/React__default["default"].createElement("div", {
|
|
1768
|
+
className: "flex flex-center-row",
|
|
1769
|
+
style: {
|
|
1770
|
+
padding: '16px 0'
|
|
1771
|
+
}
|
|
1772
|
+
}, /*#__PURE__*/React__default["default"].createElement(FontAwesome__default["default"], {
|
|
1773
|
+
name: "spinner fa-pulse fa-fw",
|
|
1774
|
+
style: {
|
|
1775
|
+
fontSize: 16,
|
|
1776
|
+
color: PlussCore.Colours.TEXT_MID,
|
|
1777
|
+
marginRight: 8
|
|
1778
|
+
}
|
|
1779
|
+
}), /*#__PURE__*/React__default["default"].createElement("span", {
|
|
1780
|
+
className: "fontRegular fontSize-13",
|
|
1781
|
+
style: {
|
|
1782
|
+
color: PlussCore.Colours.TEXT_MID
|
|
1783
|
+
}
|
|
1784
|
+
}, "Loading more results\u2026"));
|
|
1785
|
+
}
|
|
1698
1786
|
return /*#__PURE__*/React__default["default"].createElement("div", {
|
|
1699
1787
|
className: "flex flex-center-row",
|
|
1700
1788
|
style: {
|
|
@@ -1704,21 +1792,18 @@
|
|
|
1704
1792
|
inline: true,
|
|
1705
1793
|
buttonType: "tertiary",
|
|
1706
1794
|
onClick: this.loadMore,
|
|
1707
|
-
isActive:
|
|
1708
|
-
},
|
|
1709
|
-
name: "spinner fa-pulse fa-fw",
|
|
1710
|
-
style: {
|
|
1711
|
-
marginRight: 8
|
|
1712
|
-
}
|
|
1713
|
-
}), "Loading more\u2026") : 'Load More'));
|
|
1795
|
+
isActive: true
|
|
1796
|
+
}, "Load More"));
|
|
1714
1797
|
}
|
|
1715
1798
|
renderContent() {
|
|
1716
1799
|
const {
|
|
1717
1800
|
loading,
|
|
1801
|
+
loadingMore,
|
|
1718
1802
|
jobs
|
|
1719
1803
|
} = this.state;
|
|
1720
1804
|
if (loading) return this.renderLoading();
|
|
1721
|
-
if (___default["default"].isEmpty(jobs)) return this.renderEmpty();
|
|
1805
|
+
if (___default["default"].isEmpty(jobs) && !loadingMore) return this.renderEmpty();
|
|
1806
|
+
if (___default["default"].isEmpty(jobs)) return this.renderLoading();
|
|
1722
1807
|
return /*#__PURE__*/React__default["default"].createElement("div", null, /*#__PURE__*/React__default["default"].createElement(reactBootstrap.Table, {
|
|
1723
1808
|
className: "plussTable",
|
|
1724
1809
|
striped: true,
|
package/package.json
CHANGED
|
@@ -154,12 +154,27 @@ class JobList extends Component {
|
|
|
154
154
|
};
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* Minimum number of items to
|
|
158
|
-
* DynamoDB
|
|
159
|
-
*
|
|
160
|
-
*
|
|
157
|
+
* Minimum number of items to auto-fill before stopping background fetch.
|
|
158
|
+
* Because DynamoDB pages are unfiltered and the backend filters after
|
|
159
|
+
* query, a single page may yield very few matching results. We keep
|
|
160
|
+
* fetching in the background until we have this many items to display.
|
|
161
161
|
*/
|
|
162
|
-
MIN_PAGE_SIZE =
|
|
162
|
+
MIN_PAGE_SIZE = 20;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Monotonically increasing ID used to ignore stale fetch results.
|
|
166
|
+
* When a filter changes, fetchJobs() increments this counter. Any
|
|
167
|
+
* in-flight fetch or autoFillPages loop checks the counter before
|
|
168
|
+
* applying results — if it doesn't match, the results are discarded.
|
|
169
|
+
*
|
|
170
|
+
* Alternative: AbortController would actually cancel the HTTP request
|
|
171
|
+
* (axios supports it via the `signal` config option). That would save
|
|
172
|
+
* bandwidth and server load but requires threading `signal` through
|
|
173
|
+
* authedFunction → getJobs2 → fetchPage (3 layers). The fetchId
|
|
174
|
+
* approach is simpler and sufficient — stale requests still complete
|
|
175
|
+
* in the background but their results are ignored.
|
|
176
|
+
*/
|
|
177
|
+
_fetchId = 0;
|
|
163
178
|
|
|
164
179
|
/**
|
|
165
180
|
* Fetch a single page from the server using the given lastKey.
|
|
@@ -181,38 +196,37 @@ class JobList extends Component {
|
|
|
181
196
|
};
|
|
182
197
|
|
|
183
198
|
/**
|
|
184
|
-
* Fetch the first page
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
199
|
+
* Fetch the first page and render immediately.
|
|
200
|
+
* If fewer than MIN_PAGE_SIZE items returned and there's more data,
|
|
201
|
+
* kicks off a background loop that keeps fetching and appending
|
|
202
|
+
* so results appear progressively.
|
|
188
203
|
*/
|
|
189
204
|
fetchJobs = async () => {
|
|
190
|
-
|
|
205
|
+
const fetchId = ++this._fetchId;
|
|
206
|
+
this.setState({ loading: true, loadingMore: false }, async () => {
|
|
191
207
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
208
|
+
const res = await this.fetchPage(null);
|
|
209
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
194
210
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const res = await this.fetchPage(lastKey);
|
|
198
|
-
const items = res.data.Items || [];
|
|
199
|
-
lastKey = res.data.LastKey || null;
|
|
200
|
-
allJobs = [...allJobs, ...items];
|
|
201
|
-
} while (lastKey && allJobs.length < this.MIN_PAGE_SIZE);
|
|
211
|
+
const items = res.data.Items || [];
|
|
212
|
+
const lastKey = res.data.LastKey || null;
|
|
202
213
|
|
|
203
214
|
this.setState({
|
|
204
|
-
jobs:
|
|
215
|
+
jobs: items,
|
|
205
216
|
lastKey,
|
|
206
217
|
hasMore: !!lastKey,
|
|
207
218
|
loading: false,
|
|
208
219
|
});
|
|
209
220
|
|
|
210
|
-
|
|
211
|
-
this.
|
|
221
|
+
this.props.jobsLoaded(items);
|
|
222
|
+
this.setRequesters(items);
|
|
212
223
|
|
|
213
|
-
//
|
|
214
|
-
this.
|
|
224
|
+
// Auto-fill in background if first page was sparse
|
|
225
|
+
if (lastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
226
|
+
this.autoFillPages(items, lastKey, fetchId);
|
|
227
|
+
}
|
|
215
228
|
} catch (error) {
|
|
229
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
216
230
|
console.error('fetchJobs', error);
|
|
217
231
|
this.setState({ loading: false });
|
|
218
232
|
}
|
|
@@ -220,38 +234,84 @@ class JobList extends Component {
|
|
|
220
234
|
};
|
|
221
235
|
|
|
222
236
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
237
|
+
* Background loop: keep fetching pages and appending results
|
|
238
|
+
* until we have MIN_PAGE_SIZE items or run out of data.
|
|
239
|
+
* Each page is rendered as it arrives so the user sees
|
|
240
|
+
* results appearing progressively.
|
|
241
|
+
*/
|
|
242
|
+
autoFillPages = async (existingJobs, startKey, fetchId) => {
|
|
243
|
+
this.setState({ loadingMore: true });
|
|
244
|
+
|
|
245
|
+
let currentJobs = existingJobs;
|
|
246
|
+
let currentLastKey = startKey;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
while (currentLastKey && currentJobs.length < this.MIN_PAGE_SIZE) {
|
|
250
|
+
const res = await this.fetchPage(currentLastKey);
|
|
251
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
252
|
+
|
|
253
|
+
const items = res.data.Items || [];
|
|
254
|
+
currentLastKey = res.data.LastKey || null;
|
|
255
|
+
currentJobs = [...currentJobs, ...items];
|
|
256
|
+
|
|
257
|
+
// Append to UI immediately after each page arrives
|
|
258
|
+
this.setState({
|
|
259
|
+
jobs: currentJobs,
|
|
260
|
+
lastKey: currentLastKey,
|
|
261
|
+
hasMore: !!currentLastKey,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (items.length > 0) {
|
|
265
|
+
this.props.jobsLoaded(items);
|
|
266
|
+
this.setRequesters(currentJobs);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
271
|
+
console.error('autoFillPages', error);
|
|
272
|
+
} finally {
|
|
273
|
+
if (this._fetchId === fetchId) {
|
|
274
|
+
this.setState({ loadingMore: false });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Load the next batch of jobs when the user clicks "Load More".
|
|
281
|
+
* Fetches one page and also auto-fills in background if sparse.
|
|
225
282
|
*/
|
|
226
283
|
loadMore = async () => {
|
|
227
|
-
const
|
|
284
|
+
const fetchId = ++this._fetchId;
|
|
285
|
+
const { lastKey, jobs } = this.state;
|
|
228
286
|
|
|
229
287
|
this.setState({ loadingMore: true }, async () => {
|
|
230
288
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}));
|
|
289
|
+
const res = await this.fetchPage(lastKey);
|
|
290
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
291
|
+
|
|
292
|
+
const items = res.data.Items || [];
|
|
293
|
+
const newLastKey = res.data.LastKey || null;
|
|
294
|
+
|
|
295
|
+
const updatedJobs = [...jobs, ...items];
|
|
296
|
+
|
|
297
|
+
this.setState({
|
|
298
|
+
jobs: updatedJobs,
|
|
299
|
+
lastKey: newLastKey,
|
|
300
|
+
hasMore: !!newLastKey,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (items.length > 0) {
|
|
304
|
+
this.props.jobsLoaded(items);
|
|
305
|
+
}
|
|
249
306
|
|
|
250
|
-
//
|
|
251
|
-
if (
|
|
252
|
-
this.
|
|
307
|
+
// Auto-fill in background if this page was sparse
|
|
308
|
+
if (newLastKey && items.length < this.MIN_PAGE_SIZE) {
|
|
309
|
+
this.autoFillPages(updatedJobs, newLastKey, fetchId);
|
|
310
|
+
} else {
|
|
311
|
+
this.setState({ loadingMore: false });
|
|
253
312
|
}
|
|
254
313
|
} catch (error) {
|
|
314
|
+
if (this._fetchId !== fetchId) return; // stale fetch
|
|
255
315
|
console.error('loadMore', error);
|
|
256
316
|
this.setState({ loadingMore: false });
|
|
257
317
|
}
|
|
@@ -485,14 +545,19 @@ class JobList extends Component {
|
|
|
485
545
|
});
|
|
486
546
|
}
|
|
487
547
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
548
|
+
// Skip sorting while auto-fill is appending results to avoid jumpiness.
|
|
549
|
+
// Sort is re-applied when the user clicks a column header or when
|
|
550
|
+
// auto-fill completes.
|
|
551
|
+
if (!this.state.loadingMore) {
|
|
552
|
+
source = _.sortBy(source, (event) => {
|
|
553
|
+
if (this.state.sortColumn === 'assigned') {
|
|
554
|
+
return event.Assignee ? event.Assignee.displayName : 'Unassigned';
|
|
555
|
+
}
|
|
556
|
+
if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
|
|
557
|
+
return event.createdUnix;
|
|
558
|
+
});
|
|
559
|
+
if (this.state.sortDesc) source.reverse();
|
|
560
|
+
}
|
|
496
561
|
return source;
|
|
497
562
|
};
|
|
498
563
|
|
|
@@ -1015,7 +1080,17 @@ class JobList extends Component {
|
|
|
1015
1080
|
|
|
1016
1081
|
renderLoadMore() {
|
|
1017
1082
|
const { hasMore, loadingMore } = this.state;
|
|
1018
|
-
if (!hasMore) return null;
|
|
1083
|
+
if (!hasMore && !loadingMore) return null;
|
|
1084
|
+
|
|
1085
|
+
// During background auto-fill, show a subtle spinner without a button
|
|
1086
|
+
if (loadingMore) {
|
|
1087
|
+
return (
|
|
1088
|
+
<div className="flex flex-center-row" style={{ padding: '16px 0' }}>
|
|
1089
|
+
<FontAwesome name="spinner fa-pulse fa-fw" style={{ fontSize: 16, color: Colours.TEXT_MID, marginRight: 8 }} />
|
|
1090
|
+
<span className="fontRegular fontSize-13" style={{ color: Colours.TEXT_MID }}>Loading more results…</span>
|
|
1091
|
+
</div>
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1019
1094
|
|
|
1020
1095
|
return (
|
|
1021
1096
|
<div className="flex flex-center-row" style={{ padding: '16px 0' }}>
|
|
@@ -1023,26 +1098,20 @@ class JobList extends Component {
|
|
|
1023
1098
|
inline
|
|
1024
1099
|
buttonType="tertiary"
|
|
1025
1100
|
onClick={this.loadMore}
|
|
1026
|
-
isActive
|
|
1101
|
+
isActive
|
|
1027
1102
|
>
|
|
1028
|
-
|
|
1029
|
-
<span>
|
|
1030
|
-
<FontAwesome name="spinner fa-pulse fa-fw" style={{ marginRight: 8 }} />
|
|
1031
|
-
Loading more…
|
|
1032
|
-
</span>
|
|
1033
|
-
) : (
|
|
1034
|
-
'Load More'
|
|
1035
|
-
)}
|
|
1103
|
+
Load More
|
|
1036
1104
|
</Components.Button>
|
|
1037
1105
|
</div>
|
|
1038
1106
|
);
|
|
1039
1107
|
}
|
|
1040
1108
|
|
|
1041
1109
|
renderContent() {
|
|
1042
|
-
const { loading, jobs } = this.state;
|
|
1110
|
+
const { loading, loadingMore, jobs } = this.state;
|
|
1043
1111
|
|
|
1044
1112
|
if (loading) return this.renderLoading();
|
|
1045
|
-
if (_.isEmpty(jobs)) return this.renderEmpty();
|
|
1113
|
+
if (_.isEmpty(jobs) && !loadingMore) return this.renderEmpty();
|
|
1114
|
+
if (_.isEmpty(jobs)) return this.renderLoading();
|
|
1046
1115
|
|
|
1047
1116
|
return (
|
|
1048
1117
|
<div>
|