decap-cms-backend-github 3.7.0 → 3.8.0
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/CHANGELOG.md +4 -0
- package/dist/decap-cms-backend-github.js +73 -73
- package/dist/decap-cms-backend-github.js.LICENSE.txt +18 -8
- package/dist/decap-cms-backend-github.js.map +1 -1
- package/dist/esm/API.js +414 -0
- package/dist/esm/AuthenticationPage.js +23 -14
- package/dist/esm/implementation.js +176 -7
- package/package.json +2 -2
- package/src/API.ts +485 -0
- package/src/__tests__/implementation.spec.js +487 -0
- package/src/implementation.tsx +203 -3
- package/src/polling.ts +472 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
-
import * as React from 'react';
|
|
3
1
|
import semaphore from 'semaphore';
|
|
4
2
|
import trimStart from 'lodash/trimStart';
|
|
5
3
|
import { stripIndent } from 'common-tags';
|
|
6
4
|
import { CURSOR_COMPATIBILITY_SYMBOL, Cursor, asyncLock, basename, getBlobSHA, entriesByFolder, entriesByFiles, unpublishedEntries, getMediaDisplayURL, getMediaAsBlob, filterByExtension, getPreviewStatus, runWithLock, blobToFileObj, contentKeyFromBranch, unsentRequest, branchFromContentKey } from 'decap-cms-lib-util';
|
|
7
5
|
import AuthenticationPage from './AuthenticationPage';
|
|
8
6
|
import API, { API_NAME } from './API';
|
|
7
|
+
import { ETagPollingManager } from './polling';
|
|
9
8
|
import GraphQLAPI from './GraphQLAPI';
|
|
10
|
-
import { jsx as
|
|
9
|
+
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
11
10
|
const MAX_CONCURRENT_DOWNLOADS = 10;
|
|
12
11
|
const {
|
|
13
12
|
fetchWithTimeout: fetch
|
|
@@ -17,6 +16,7 @@ const GITHUB_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
|
|
|
17
16
|
const GITHUB_OPERATIONAL_UNITS = ['API Requests', 'Issues, Pull Requests, Projects'];
|
|
18
17
|
export default class GitHub {
|
|
19
18
|
bypassWriteAccessCheckForAppTokens = false;
|
|
19
|
+
unwatchFunctions = new Map();
|
|
20
20
|
constructor(config, options = {}) {
|
|
21
21
|
this.options = {
|
|
22
22
|
proxied: false,
|
|
@@ -82,9 +82,10 @@ export default class GitHub {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
authComponent() {
|
|
85
|
-
const wrappedAuthenticationPage = props =>
|
|
85
|
+
const wrappedAuthenticationPage = props => _jsx(AuthenticationPage, {
|
|
86
|
+
...props,
|
|
86
87
|
backend: this
|
|
87
|
-
})
|
|
88
|
+
});
|
|
88
89
|
wrappedAuthenticationPage.displayName = 'AuthenticationPage';
|
|
89
90
|
return wrappedAuthenticationPage;
|
|
90
91
|
}
|
|
@@ -290,6 +291,9 @@ export default class GitHub {
|
|
|
290
291
|
// }
|
|
291
292
|
// }
|
|
292
293
|
|
|
294
|
+
if (this.api && !this.pollingManager) {
|
|
295
|
+
this.pollingManager = new ETagPollingManager(this.api, 15000);
|
|
296
|
+
}
|
|
293
297
|
// Authorized user
|
|
294
298
|
return {
|
|
295
299
|
...user,
|
|
@@ -299,6 +303,12 @@ export default class GitHub {
|
|
|
299
303
|
}
|
|
300
304
|
logout() {
|
|
301
305
|
this.token = null;
|
|
306
|
+
// Clean up polling
|
|
307
|
+
if (this.pollingManager) {
|
|
308
|
+
this.pollingManager.destroy();
|
|
309
|
+
this.pollingManager = undefined;
|
|
310
|
+
}
|
|
311
|
+
this.unwatchFunctions.clear();
|
|
302
312
|
if (this.api && this.api.reset && typeof this.api.reset === 'function') {
|
|
303
313
|
return this.api.reset();
|
|
304
314
|
}
|
|
@@ -594,10 +604,169 @@ export default class GitHub {
|
|
|
594
604
|
}
|
|
595
605
|
deleteUnpublishedEntry(collection, slug) {
|
|
596
606
|
// deleteUnpublishedEntry is a transactional operation
|
|
597
|
-
return runWithLock(this.lock, () =>
|
|
607
|
+
return runWithLock(this.lock, async () => {
|
|
608
|
+
await this.api.deleteUnpublishedEntry(collection, slug);
|
|
609
|
+
// Clean up associated notes issue
|
|
610
|
+
try {
|
|
611
|
+
await this.api.closeEntryNotesIssue(collection, slug);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
console.warn('Failed to close notes issue during entry deletion:', error);
|
|
614
|
+
}
|
|
615
|
+
}, 'Failed to acquire delete entry lock');
|
|
598
616
|
}
|
|
599
617
|
publishUnpublishedEntry(collection, slug) {
|
|
600
618
|
// publishUnpublishedEntry is a transactional operation
|
|
601
|
-
return runWithLock(this.lock, () =>
|
|
619
|
+
return runWithLock(this.lock, async () => {
|
|
620
|
+
await this.api.publishUnpublishedEntry(collection, slug);
|
|
621
|
+
await this.api.closeIssueOnPublish(collection, slug);
|
|
622
|
+
}, 'Failed to acquire publish entry lock');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Notes implementation, which is an abstraction to Github's PR issue comments.
|
|
626
|
+
|
|
627
|
+
// Notes implementation using GitHub Issues
|
|
628
|
+
async getNotes(collection, slug) {
|
|
629
|
+
try {
|
|
630
|
+
const notes = await this.api.getEntryNotes(collection, slug);
|
|
631
|
+
return notes.map(note => ({
|
|
632
|
+
...note,
|
|
633
|
+
entrySlug: slug
|
|
634
|
+
}));
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error('Failed to get notes:', error);
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async addNote(collection, slug, noteData) {
|
|
641
|
+
const currentUser = await this.currentUser({
|
|
642
|
+
token: this.token
|
|
643
|
+
});
|
|
644
|
+
const note = {
|
|
645
|
+
...noteData,
|
|
646
|
+
id: 'temp-' + Date.now(),
|
|
647
|
+
author: currentUser.login || currentUser.name || '',
|
|
648
|
+
avatarUrl: currentUser.avatar_url,
|
|
649
|
+
entrySlug: slug,
|
|
650
|
+
timestamp: noteData.timestamp || new Date().toISOString(),
|
|
651
|
+
resolved: noteData.resolved || false,
|
|
652
|
+
issueUrl: undefined
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Get entry title for better issue naming
|
|
656
|
+
let entryTitle;
|
|
657
|
+
try {
|
|
658
|
+
const entryData = await this.getEntry(`${collection}/${slug}.md`);
|
|
659
|
+
const titleMatch = entryData.data.match(/^title:\s*["']?([^"'\n]+)["']?/m);
|
|
660
|
+
entryTitle = titleMatch ? titleMatch[1] : undefined;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
// Entry not found or error reading, use undefined title
|
|
663
|
+
}
|
|
664
|
+
const {
|
|
665
|
+
commentId,
|
|
666
|
+
issueUrl
|
|
667
|
+
} = await this.api.addNoteToEntry(collection, slug, note, entryTitle);
|
|
668
|
+
return {
|
|
669
|
+
...note,
|
|
670
|
+
id: commentId,
|
|
671
|
+
issueUrl
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async updateNote(collection, slug, noteId, updates) {
|
|
675
|
+
const currentNotes = await this.getNotes(collection, slug);
|
|
676
|
+
const existingNote = currentNotes.find(note => note.id === noteId);
|
|
677
|
+
if (!existingNote) {
|
|
678
|
+
throw new Error(`Note with ID ${noteId} not found`);
|
|
679
|
+
}
|
|
680
|
+
const updatedNote = {
|
|
681
|
+
...existingNote,
|
|
682
|
+
...updates,
|
|
683
|
+
id: noteId,
|
|
684
|
+
entrySlug: slug
|
|
685
|
+
};
|
|
686
|
+
await this.api.updateEntryNote(noteId, updatedNote);
|
|
687
|
+
return updatedNote;
|
|
688
|
+
}
|
|
689
|
+
async deleteNote(collection, slug, noteId) {
|
|
690
|
+
const currentNotes = await this.getNotes(collection, slug);
|
|
691
|
+
const noteExists = currentNotes.some(note => note.id === noteId);
|
|
692
|
+
if (!noteExists) {
|
|
693
|
+
throw new Error(`Note with ID ${noteId} not found`);
|
|
694
|
+
}
|
|
695
|
+
await this.api.deleteEntryNote(noteId);
|
|
696
|
+
}
|
|
697
|
+
async toggleNoteResolution(collection, slug, noteId) {
|
|
698
|
+
const currentNotes = await this.getNotes(collection, slug);
|
|
699
|
+
const note = currentNotes.find(n => n.id === noteId);
|
|
700
|
+
if (!note) {
|
|
701
|
+
throw new Error(`Note with ID ${noteId} not found`);
|
|
702
|
+
}
|
|
703
|
+
return this.updateNote(collection, slug, noteId, {
|
|
704
|
+
resolved: !note.resolved
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async reopenIssueForUnpublishedEntry(collection, slug) {
|
|
708
|
+
await this.api.reopenIssueOnUnpublish(collection, slug);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Start watching notes for changes
|
|
712
|
+
* Called from Redux action
|
|
713
|
+
*/
|
|
714
|
+
async startNotesPolling(collection, slug, callbacks) {
|
|
715
|
+
if (!this.pollingManager) {
|
|
716
|
+
console.warn('[DecapNotes Polling] Polling manager not initialized');
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const issueKey = `${collection}/${slug}`;
|
|
720
|
+
|
|
721
|
+
// Check if already watching this exact entry - if so, skip
|
|
722
|
+
if (this.pollingManager.getStatus().currentWatch === issueKey) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// First, ensure any previous polling for this entry is completely stopped
|
|
727
|
+
const existingUnwatch = this.unwatchFunctions.get(issueKey);
|
|
728
|
+
if (existingUnwatch) {
|
|
729
|
+
existingUnwatch();
|
|
730
|
+
this.unwatchFunctions.delete(issueKey);
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const unwatchFn = await this.pollingManager.watchIssueWithRetry(collection, slug, callbacks, 5,
|
|
734
|
+
// maxRetries - will try up to 5 times
|
|
735
|
+
2000 // retryDelay - 2 seconds between attempts
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
// Store the new unwatch function
|
|
739
|
+
this.unwatchFunctions.set(issueKey, unwatchFn);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.error('[DecapNotes Polling] Failed to start polling after retries:', error);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Stop watching notes for changes
|
|
747
|
+
* Called from Redux action: dispatch(stopNotesPolling(collection, slug))
|
|
748
|
+
*
|
|
749
|
+
* Ensures complete cleanup of polling for this entry
|
|
750
|
+
*/
|
|
751
|
+
async stopNotesPolling(collection, slug) {
|
|
752
|
+
const issueKey = `${collection}/${slug}`;
|
|
753
|
+
const unwatchFn = this.unwatchFunctions.get(issueKey);
|
|
754
|
+
if (unwatchFn) {
|
|
755
|
+
unwatchFn();
|
|
756
|
+
this.unwatchFunctions.delete(issueKey);
|
|
757
|
+
} else {
|
|
758
|
+
console.log(`[DecapNotes Polling] No active polling found for ${issueKey}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Manually refresh notes (force check now)
|
|
764
|
+
* Called from Redux action: dispatch(refreshNotesNow(collection, slug))
|
|
765
|
+
*/
|
|
766
|
+
async refreshNotesNow(collection, slug) {
|
|
767
|
+
if (!this.pollingManager) {
|
|
768
|
+
throw new Error('Polling manager not initialized');
|
|
769
|
+
}
|
|
770
|
+
await this.pollingManager.checkIssueNow(collection, slug);
|
|
602
771
|
}
|
|
603
772
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decap-cms-backend-github",
|
|
3
3
|
"description": "GitHub backend for Decap CMS",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.8.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github",
|
|
7
7
|
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"browser": {
|
|
45
45
|
"path": "path-browserify"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "ffe5ab7e61f4e7cb374a413bf9fe55f088d20ba2"
|
|
48
48
|
}
|