Skip to content

Commit 80d27f1

Browse files
committed
feat: add progress bar
1 parent e36d2b6 commit 80d27f1

File tree

9 files changed

+170
-4
lines changed

9 files changed

+170
-4
lines changed

packages/decap-cms-backend-github/src/implementation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ export default class GitHub implements Implementation {
881881

882882
async traverseCursor(cursor: Cursor, action: string) {
883883
const meta = cursor.meta!;
884-
884+
885885
// Check if this is a GraphQL paginated cursor (has folder/extension) or old-style (has files array)
886886
const hasFiles = cursor.data!.has('files');
887887
const hasFolder = cursor.data!.has('folder');

packages/decap-cms-core/src/actions/entries.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const ENTRY_FAILURE = 'ENTRY_FAILURE';
5050
export const ENTRIES_REQUEST = 'ENTRIES_REQUEST';
5151
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
5252
export const ENTRIES_FAILURE = 'ENTRIES_FAILURE';
53+
export const ENTRIES_PROGRESS = 'ENTRIES_PROGRESS';
5354

5455
export const SORT_ENTRIES_REQUEST = 'SORT_ENTRIES_REQUEST';
5556
export const SORT_ENTRIES_SUCCESS = 'SORT_ENTRIES_SUCCESS';
@@ -160,6 +161,18 @@ export function entriesFailed(collection: Collection, error: Error) {
160161
};
161162
}
162163

164+
export function entriesProgress(collection: Collection, loadedCount: number, totalCount: number) {
165+
return {
166+
type: ENTRIES_PROGRESS,
167+
payload: {
168+
collection: collection.get('name'),
169+
loadedCount,
170+
totalCount,
171+
percentage: totalCount > 0 ? Math.round((loadedCount / totalCount) * 100) : 0,
172+
},
173+
};
174+
}
175+
163176
export async function getAllEntries(state: State, collection: Collection) {
164177
const backend = currentBackend(state.config);
165178
const integration = selectIntegration(state, collection.get('name'), 'listEntries');
@@ -605,6 +618,9 @@ export function loadEntries(collection: Collection, page = 0) {
605618
const usingOld = cursor.meta!.get('usingOldPaginationAPI');
606619
const entries = usingOld ? initial.entries.reverse() : initial.entries;
607620
const hasMore = cursor.actions!.has('next');
621+
const totalCount = cursor.meta?.get('count') || 0;
622+
623+
// Dispatch initial entries
608624
dispatch(
609625
entriesLoaded(
610626
collection,
@@ -615,8 +631,15 @@ export function loadEntries(collection: Collection, page = 0) {
615631
hasMore,
616632
),
617633
);
634+
635+
// Dispatch progress if we know the total
636+
if (totalCount > 0) {
637+
dispatch(entriesProgress(collection, entries.length, totalCount));
638+
}
639+
618640
// Stream subsequent pages
619641
let currentCursor = cursor;
642+
let loadedCount = entries.length;
620643
while (currentCursor.actions!.has('next')) {
621644
const { entries: moreEntries, cursor: newCursor } = await traverseCursor(
622645
provider as unknown as Backend,
@@ -626,6 +649,8 @@ export function loadEntries(collection: Collection, page = 0) {
626649
const usingOldMore = newCursor.meta!.get('usingOldPaginationAPI');
627650
const pageEntries = usingOldMore ? moreEntries.reverse() : moreEntries;
628651
const more = newCursor.actions!.has('next');
652+
loadedCount += pageEntries.length;
653+
629654
dispatch(
630655
entriesLoaded(
631656
collection,
@@ -636,6 +661,12 @@ export function loadEntries(collection: Collection, page = 0) {
636661
more,
637662
),
638663
);
664+
665+
// Dispatch progress update
666+
if (totalCount > 0) {
667+
dispatch(entriesProgress(collection, loadedCount, totalCount));
668+
}
669+
639670
currentCursor = newCursor;
640671
}
641672
} else {

packages/decap-cms-core/src/components/Collection/Entries/Entries.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import styled from '@emotion/styled';
44
import ImmutablePropTypes from 'react-immutable-proptypes';
55
import { translate } from 'react-polyglot';
6-
import { Loader, lengths } from 'decap-cms-ui-default';
6+
import { Loader, lengths, ProgressBar } from 'decap-cms-ui-default';
77

88
import EntryListing from './EntryListing';
99

@@ -30,6 +30,7 @@ function Entries({
3030
getUnpublishedEntries,
3131
filterTerm,
3232
error,
33+
progress,
3334
}) {
3435
const loadingMessages = [
3536
t('collection.entries.loadingEntries'),
@@ -57,7 +58,16 @@ function Entries({
5758
filterTerm={filterTerm}
5859
/>
5960
{isFetching && page !== undefined && entries.size > 0 ? (
60-
<PaginationMessage>{t('collection.entries.loadingEntries')}</PaginationMessage>
61+
progress && progress.totalCount > 0 ? (
62+
<ProgressBar
63+
loadedCount={progress.loadedCount}
64+
totalCount={progress.totalCount}
65+
percentage={progress.percentage}
66+
message={t('collection.entries.loadingEntries')}
67+
/>
68+
) : (
69+
<PaginationMessage>{t('collection.entries.loadingEntries')}</PaginationMessage>
70+
)
6171
) : null}
6272
</>
6373
);
@@ -90,6 +100,11 @@ Entries.propTypes = {
90100
getUnpublishedEntries: PropTypes.func,
91101
filterTerm: PropTypes.string,
92102
error: PropTypes.string,
103+
progress: PropTypes.shape({
104+
loadedCount: PropTypes.number,
105+
totalCount: PropTypes.number,
106+
percentage: PropTypes.number,
107+
}),
93108
};
94109

95110
export default translate()(Entries);

packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export class EntriesCollection extends React.Component {
146146
getUnpublishedEntries,
147147
filterTerm,
148148
error,
149+
progress,
149150
} = this.props;
150151

151152
const EntriesToRender = ({ entries }) => {
@@ -163,6 +164,7 @@ export class EntriesCollection extends React.Component {
163164
getUnpublishedEntries={getUnpublishedEntries}
164165
filterTerm={filterTerm}
165166
error={error}
167+
progress={progress}
166168
/>
167169
);
168170
};
@@ -231,6 +233,7 @@ function mapStateToProps(state, ownProps) {
231233
: true;
232234

233235
const error = state.entries.getIn(['pages', collection.get('name'), 'error']);
236+
const progress = state.entries.getIn(['pages', collection.get('name'), 'progress']);
234237

235238
return {
236239
collection,
@@ -245,6 +248,7 @@ function mapStateToProps(state, ownProps) {
245248
unpublishedEntriesLoaded,
246249
isEditorialWorkflowEnabled,
247250
error,
251+
progress,
248252
getWorkflowStatus: (collectionName, slug) => {
249253
const unpublishedEntry = selectUnpublishedEntry(state, collectionName, slug);
250254
return unpublishedEntry ? unpublishedEntry.get('status') : null;

packages/decap-cms-core/src/reducers/__tests__/entries.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe('entries', () => {
4646
posts: {
4747
page: 0,
4848
ids: ['a', 'b'],
49+
isFetching: false,
4950
},
5051
},
5152
}),

packages/decap-cms-core/src/reducers/entries.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
ENTRIES_REQUEST,
2121
ENTRIES_SUCCESS,
2222
ENTRIES_FAILURE,
23+
ENTRIES_PROGRESS,
2324
ENTRY_DELETE_SUCCESS,
2425
SORT_ENTRIES_REQUEST,
2526
SORT_ENTRIES_SUCCESS,
@@ -222,6 +223,22 @@ function entries(
222223
map.setIn(['pages', action.meta.collection, 'error'], action.payload);
223224
});
224225

226+
case ENTRIES_PROGRESS: {
227+
const payload = action.payload as {
228+
collection: string;
229+
loadedCount: number;
230+
totalCount: number;
231+
percentage: number;
232+
};
233+
return state.withMutations(map => {
234+
map.setIn(['pages', payload.collection, 'progress'], {
235+
loadedCount: payload.loadedCount,
236+
totalCount: payload.totalCount,
237+
percentage: payload.percentage,
238+
});
239+
});
240+
}
241+
225242
case ENTRY_FAILURE: {
226243
const payload = action.payload as EntryFailurePayload;
227244
return state.withMutations(map => {

packages/decap-cms-ui-default/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"scripts": {
1515
"develop": "npm run build:esm -- --watch",
1616
"build": "cross-env NODE_ENV=production webpack",
17-
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
17+
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
1818
},
1919
"dependencies": {
2020
"react-aria-menubutton": "^7.0.0",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from 'react';
2+
import styled from '@emotion/styled';
3+
import { keyframes } from '@emotion/react';
4+
5+
const shimmer = keyframes`
6+
0% {
7+
background-position: -1000px 0;
8+
}
9+
100% {
10+
background-position: 1000px 0;
11+
}
12+
`;
13+
14+
const ProgressContainer = styled.div`
15+
width: ${(props: { width?: string }) => props.width || '100%'};
16+
max-width: 600px;
17+
margin: 16px auto;
18+
padding: 0 20px;
19+
`;
20+
21+
const ProgressBarWrapper = styled.div`
22+
width: 100%;
23+
height: 8px;
24+
background-color: #e8eaed;
25+
border-radius: 4px;
26+
overflow: hidden;
27+
position: relative;
28+
`;
29+
30+
const ProgressBarFill = styled.div<{ percentage: number }>`
31+
height: 100%;
32+
width: ${props => props.percentage}%;
33+
background: linear-gradient(
34+
90deg,
35+
#3b82f6 0%,
36+
#60a5fa 25%,
37+
#3b82f6 50%,
38+
#60a5fa 75%,
39+
#3b82f6 100%
40+
);
41+
background-size: 1000px 100%;
42+
animation: ${shimmer} 2s infinite linear;
43+
border-radius: 4px;
44+
transition: width 0.3s ease-out;
45+
`;
46+
47+
const ProgressText = styled.div`
48+
display: flex;
49+
justify-content: space-between;
50+
align-items: center;
51+
margin-top: 8px;
52+
font-size: 13px;
53+
color: #5c6873;
54+
`;
55+
56+
const ProgressMessage = styled.span`
57+
font-weight: 500;
58+
`;
59+
60+
const ProgressCount = styled.span`
61+
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
62+
color: #3b82f6;
63+
`;
64+
65+
export interface ProgressBarProps {
66+
loadedCount: number;
67+
totalCount: number;
68+
percentage: number;
69+
width?: string;
70+
message?: string;
71+
}
72+
73+
export function ProgressBar({
74+
loadedCount,
75+
totalCount,
76+
percentage,
77+
width,
78+
message = 'Loading entries',
79+
}: ProgressBarProps) {
80+
return (
81+
<ProgressContainer width={width}>
82+
<ProgressBarWrapper>
83+
<ProgressBarFill percentage={percentage} />
84+
</ProgressBarWrapper>
85+
<ProgressText>
86+
<ProgressMessage>{message}...</ProgressMessage>
87+
<ProgressCount>
88+
{loadedCount} / {totalCount} ({percentage}%)
89+
</ProgressCount>
90+
</ProgressText>
91+
</ProgressContainer>
92+
);
93+
}
94+
95+
export default ProgressBar;

packages/decap-cms-ui-default/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import AuthenticationPage, { renderPageLogo } from './AuthenticationPage';
1414
import WidgetPreviewContainer from './WidgetPreviewContainer';
1515
import ObjectWidgetTopBar from './ObjectWidgetTopBar';
1616
import GoBackButton from './GoBackButton';
17+
import ProgressBar from './ProgressBar';
1718
import {
1819
fonts,
1920
colorsRaw,
@@ -49,6 +50,7 @@ export const DecapCmsUiDefault = {
4950
AuthenticationPage,
5051
WidgetPreviewContainer,
5152
ObjectWidgetTopBar,
53+
ProgressBar,
5254
fonts,
5355
colorsRaw,
5456
colors,
@@ -83,6 +85,7 @@ export {
8385
AuthenticationPage,
8486
WidgetPreviewContainer,
8587
ObjectWidgetTopBar,
88+
ProgressBar,
8689
fonts,
8790
colorsRaw,
8891
colors,

0 commit comments

Comments
 (0)