@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 CHANGED
@@ -811,12 +811,26 @@ class JobList extends React.Component {
811
811
  return params;
812
812
  });
813
813
  /**
814
- * Minimum number of items to show before stopping auto-pagination.
815
- * DynamoDB returns unfiltered pages; the backend filters after query.
816
- * A single DynamoDB page may yield very few matching results,
817
- * so we auto-fetch additional pages until we have enough to display.
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", 50);
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 of jobs with current filters.
833
- * Auto-paginates if the server returns fewer items than MIN_PAGE_SIZE
834
- * (because DynamoDB pages are unfiltered a single page may yield
835
- * very few results after server-side filtering).
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
- let allJobs = [];
843
- let lastKey = null;
858
+ const res = await this.fetchPage(null);
859
+ if (this._fetchId !== fetchId) return; // stale fetch
844
860
 
845
- // Keep fetching pages until we have enough items or run out of data
846
- do {
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: allJobs,
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
- // Sync to Redux so other components (e.g. Job detail) can access
860
- this.props.jobsLoaded(allJobs);
861
-
862
- // Update requesters list for the requester filter
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
- * Load the next page of jobs and append to the existing list.
874
- * Also auto-paginates to fill up to MIN_PAGE_SIZE new items.
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
- let newJobs = [];
885
- let currentLastKey = lastKey;
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
- // Keep fetching pages until we have enough new items or run out of data
889
- do {
890
- const res = await this.fetchPage(currentLastKey);
891
- const items = res.data.Items || [];
892
- currentLastKey = res.data.LastKey || null;
893
- newJobs = [...newJobs, ...items];
894
- } while (currentLastKey && existingCount + newJobs.length < existingCount + this.MIN_PAGE_SIZE);
895
- this.setState(prevState => ({
896
- jobs: [...prevState.jobs, ...newJobs],
897
- lastKey: currentLastKey,
898
- hasMore: !!currentLastKey,
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
- // Sync newly loaded jobs to Redux (append)
903
- if (newJobs.length > 0) {
904
- this.props.jobsLoaded(newJobs);
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
- source = ___default["default"].sortBy(source, event => {
1120
- if (this.state.sortColumn === 'assigned') {
1121
- return event.Assignee ? event.Assignee.displayName : 'Unassigned';
1122
- }
1123
- if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
1124
- return event.createdUnix;
1125
- });
1126
- if (this.state.sortDesc) source.reverse();
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: !loadingMore
1719
- }, loadingMore ? /*#__PURE__*/React__default["default"].createElement("span", null, /*#__PURE__*/React__default["default"].createElement(FontAwesome__default["default"], {
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 show before stopping auto-pagination.
784
- * DynamoDB returns unfiltered pages; the backend filters after query.
785
- * A single DynamoDB page may yield very few matching results,
786
- * so we auto-fetch additional pages until we have enough to display.
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", 50);
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 of jobs with current filters.
802
- * Auto-paginates if the server returns fewer items than MIN_PAGE_SIZE
803
- * (because DynamoDB pages are unfiltered a single page may yield
804
- * very few results after server-side filtering).
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
- let allJobs = [];
812
- let lastKey = null;
827
+ const res = await this.fetchPage(null);
828
+ if (this._fetchId !== fetchId) return; // stale fetch
813
829
 
814
- // Keep fetching pages until we have enough items or run out of data
815
- do {
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: allJobs,
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
- // Sync to Redux so other components (e.g. Job detail) can access
829
- this.props.jobsLoaded(allJobs);
830
-
831
- // Update requesters list for the requester filter
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
- * Load the next page of jobs and append to the existing list.
843
- * Also auto-paginates to fill up to MIN_PAGE_SIZE new items.
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
- let newJobs = [];
854
- let currentLastKey = lastKey;
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
- // Keep fetching pages until we have enough new items or run out of data
858
- do {
859
- const res = await this.fetchPage(currentLastKey);
860
- const items = res.data.Items || [];
861
- currentLastKey = res.data.LastKey || null;
862
- newJobs = [...newJobs, ...items];
863
- } while (currentLastKey && existingCount + newJobs.length < existingCount + this.MIN_PAGE_SIZE);
864
- this.setState(prevState => ({
865
- jobs: [...prevState.jobs, ...newJobs],
866
- lastKey: currentLastKey,
867
- hasMore: !!currentLastKey,
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
- // Sync newly loaded jobs to Redux (append)
872
- if (newJobs.length > 0) {
873
- this.props.jobsLoaded(newJobs);
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
- source = _.sortBy(source, event => {
1089
- if (this.state.sortColumn === 'assigned') {
1090
- return event.Assignee ? event.Assignee.displayName : 'Unassigned';
1091
- }
1092
- if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
1093
- return event.createdUnix;
1094
- });
1095
- if (this.state.sortDesc) source.reverse();
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: !loadingMore
1688
- }, loadingMore ? /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement(FontAwesome, {
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 show before stopping auto-pagination.
804
- * DynamoDB returns unfiltered pages; the backend filters after query.
805
- * A single DynamoDB page may yield very few matching results,
806
- * so we auto-fetch additional pages until we have enough to display.
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", 50);
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 of jobs with current filters.
822
- * Auto-paginates if the server returns fewer items than MIN_PAGE_SIZE
823
- * (because DynamoDB pages are unfiltered a single page may yield
824
- * very few results after server-side filtering).
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
- let allJobs = [];
832
- let lastKey = null;
847
+ const res = await this.fetchPage(null);
848
+ if (this._fetchId !== fetchId) return; // stale fetch
833
849
 
834
- // Keep fetching pages until we have enough items or run out of data
835
- do {
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: allJobs,
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
- // Sync to Redux so other components (e.g. Job detail) can access
849
- this.props.jobsLoaded(allJobs);
850
-
851
- // Update requesters list for the requester filter
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
- * Load the next page of jobs and append to the existing list.
863
- * Also auto-paginates to fill up to MIN_PAGE_SIZE new items.
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
- let newJobs = [];
874
- let currentLastKey = lastKey;
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
- // Keep fetching pages until we have enough new items or run out of data
878
- do {
879
- const res = await this.fetchPage(currentLastKey);
880
- const items = res.data.Items || [];
881
- currentLastKey = res.data.LastKey || null;
882
- newJobs = [...newJobs, ...items];
883
- } while (currentLastKey && existingCount + newJobs.length < existingCount + this.MIN_PAGE_SIZE);
884
- this.setState(prevState => ({
885
- jobs: [...prevState.jobs, ...newJobs],
886
- lastKey: currentLastKey,
887
- hasMore: !!currentLastKey,
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
- // Sync newly loaded jobs to Redux (append)
892
- if (newJobs.length > 0) {
893
- this.props.jobsLoaded(newJobs);
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
- source = ___default["default"].sortBy(source, event => {
1109
- if (this.state.sortColumn === 'assigned') {
1110
- return event.Assignee ? event.Assignee.displayName : 'Unassigned';
1111
- }
1112
- if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
1113
- return event.createdUnix;
1114
- });
1115
- if (this.state.sortDesc) source.reverse();
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: !loadingMore
1708
- }, loadingMore ? /*#__PURE__*/React__default["default"].createElement("span", null, /*#__PURE__*/React__default["default"].createElement(FontAwesome__default["default"], {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-maintenance-web-feedback",
3
- "version": "1.1.37-beta.0",
3
+ "version": "1.1.37-beta.1",
4
4
  "description": "Extension package to enable maintenance on Pluss Communities Platform",
5
5
  "main": "dist/index.cjs.js",
6
6
  "scripts": {
@@ -154,12 +154,27 @@ class JobList extends Component {
154
154
  };
155
155
 
156
156
  /**
157
- * Minimum number of items to show before stopping auto-pagination.
158
- * DynamoDB returns unfiltered pages; the backend filters after query.
159
- * A single DynamoDB page may yield very few matching results,
160
- * so we auto-fetch additional pages until we have enough to display.
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 = 50;
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 of jobs with current filters.
185
- * Auto-paginates if the server returns fewer items than MIN_PAGE_SIZE
186
- * (because DynamoDB pages are unfiltered a single page may yield
187
- * very few results after server-side filtering).
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
- this.setState({ loading: true }, async () => {
205
+ const fetchId = ++this._fetchId;
206
+ this.setState({ loading: true, loadingMore: false }, async () => {
191
207
  try {
192
- let allJobs = [];
193
- let lastKey = null;
208
+ const res = await this.fetchPage(null);
209
+ if (this._fetchId !== fetchId) return; // stale fetch
194
210
 
195
- // Keep fetching pages until we have enough items or run out of data
196
- do {
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: allJobs,
215
+ jobs: items,
205
216
  lastKey,
206
217
  hasMore: !!lastKey,
207
218
  loading: false,
208
219
  });
209
220
 
210
- // Sync to Redux so other components (e.g. Job detail) can access
211
- this.props.jobsLoaded(allJobs);
221
+ this.props.jobsLoaded(items);
222
+ this.setRequesters(items);
212
223
 
213
- // Update requesters list for the requester filter
214
- this.setRequesters(allJobs);
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
- * Load the next page of jobs and append to the existing list.
224
- * Also auto-paginates to fill up to MIN_PAGE_SIZE new items.
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 { lastKey } = this.state;
284
+ const fetchId = ++this._fetchId;
285
+ const { lastKey, jobs } = this.state;
228
286
 
229
287
  this.setState({ loadingMore: true }, async () => {
230
288
  try {
231
- let newJobs = [];
232
- let currentLastKey = lastKey;
233
- const existingCount = this.state.jobs.length;
234
-
235
- // Keep fetching pages until we have enough new items or run out of data
236
- do {
237
- const res = await this.fetchPage(currentLastKey);
238
- const items = res.data.Items || [];
239
- currentLastKey = res.data.LastKey || null;
240
- newJobs = [...newJobs, ...items];
241
- } while (currentLastKey && (existingCount + newJobs.length) < existingCount + this.MIN_PAGE_SIZE);
242
-
243
- this.setState((prevState) => ({
244
- jobs: [...prevState.jobs, ...newJobs],
245
- lastKey: currentLastKey,
246
- hasMore: !!currentLastKey,
247
- loadingMore: false,
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
- // Sync newly loaded jobs to Redux (append)
251
- if (newJobs.length > 0) {
252
- this.props.jobsLoaded(newJobs);
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
- source = _.sortBy(source, (event) => {
489
- if (this.state.sortColumn === 'assigned') {
490
- return event.Assignee ? event.Assignee.displayName : 'Unassigned';
491
- }
492
- if (this.state.sortColumn !== 'createdUnix') return event[this.state.sortColumn];
493
- return event.createdUnix;
494
- });
495
- if (this.state.sortDesc) source.reverse();
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={!loadingMore}
1101
+ isActive
1027
1102
  >
1028
- {loadingMore ? (
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>