decap-cms-backend-github 3.6.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 +8 -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 +451 -16
- package/dist/esm/AuthenticationPage.js +23 -14
- package/dist/esm/implementation.js +176 -7
- package/package.json +2 -2
- package/src/API.ts +524 -12
- package/src/__tests__/implementation.spec.js +487 -0
- package/src/implementation.tsx +203 -3
- package/src/polling.ts +472 -0
package/src/implementation.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
1
|
import semaphore from 'semaphore';
|
|
3
2
|
import trimStart from 'lodash/trimStart';
|
|
4
3
|
import { stripIndent } from 'common-tags';
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
|
|
25
24
|
import AuthenticationPage from './AuthenticationPage';
|
|
26
25
|
import API, { API_NAME } from './API';
|
|
26
|
+
import { ETagPollingManager } from './polling';
|
|
27
27
|
import GraphQLAPI from './GraphQLAPI';
|
|
28
28
|
|
|
29
29
|
import type { Endpoints } from '@octokit/types';
|
|
@@ -39,6 +39,8 @@ import type {
|
|
|
39
39
|
ImplementationFile,
|
|
40
40
|
UnpublishedEntryMediaFile,
|
|
41
41
|
Entry,
|
|
42
|
+
Note,
|
|
43
|
+
IssueChange,
|
|
42
44
|
} from 'decap-cms-lib-util';
|
|
43
45
|
import type { Semaphore } from 'semaphore';
|
|
44
46
|
|
|
@@ -90,6 +92,8 @@ export default class GitHub implements Implementation {
|
|
|
90
92
|
[key: string]: Promise<boolean>;
|
|
91
93
|
};
|
|
92
94
|
_mediaDisplayURLSem?: Semaphore;
|
|
95
|
+
pollingManager?: ETagPollingManager;
|
|
96
|
+
unwatchFunctions: Map<string, () => void> = new Map();
|
|
93
97
|
|
|
94
98
|
constructor(config: Config, options = {}) {
|
|
95
99
|
this.options = {
|
|
@@ -381,12 +385,22 @@ export default class GitHub implements Implementation {
|
|
|
381
385
|
// }
|
|
382
386
|
// }
|
|
383
387
|
|
|
388
|
+
if (this.api && !this.pollingManager) {
|
|
389
|
+
this.pollingManager = new ETagPollingManager(this.api, 15000);
|
|
390
|
+
}
|
|
384
391
|
// Authorized user
|
|
385
392
|
return { ...user, token: state.token as string, useOpenAuthoring: this.useOpenAuthoring };
|
|
386
393
|
}
|
|
387
394
|
|
|
388
395
|
logout() {
|
|
389
396
|
this.token = null;
|
|
397
|
+
// Clean up polling
|
|
398
|
+
if (this.pollingManager) {
|
|
399
|
+
this.pollingManager.destroy();
|
|
400
|
+
this.pollingManager = undefined;
|
|
401
|
+
}
|
|
402
|
+
this.unwatchFunctions.clear();
|
|
403
|
+
|
|
390
404
|
if (this.api && this.api.reset && typeof this.api.reset === 'function') {
|
|
391
405
|
return this.api.reset();
|
|
392
406
|
}
|
|
@@ -710,7 +724,15 @@ export default class GitHub implements Implementation {
|
|
|
710
724
|
// deleteUnpublishedEntry is a transactional operation
|
|
711
725
|
return runWithLock(
|
|
712
726
|
this.lock,
|
|
713
|
-
() =>
|
|
727
|
+
async () => {
|
|
728
|
+
await this.api!.deleteUnpublishedEntry(collection, slug);
|
|
729
|
+
// Clean up associated notes issue
|
|
730
|
+
try {
|
|
731
|
+
await this.api!.closeEntryNotesIssue(collection, slug);
|
|
732
|
+
} catch (error) {
|
|
733
|
+
console.warn('Failed to close notes issue during entry deletion:', error);
|
|
734
|
+
}
|
|
735
|
+
},
|
|
714
736
|
'Failed to acquire delete entry lock',
|
|
715
737
|
);
|
|
716
738
|
}
|
|
@@ -719,8 +741,186 @@ export default class GitHub implements Implementation {
|
|
|
719
741
|
// publishUnpublishedEntry is a transactional operation
|
|
720
742
|
return runWithLock(
|
|
721
743
|
this.lock,
|
|
722
|
-
() =>
|
|
744
|
+
async () => {
|
|
745
|
+
await this.api!.publishUnpublishedEntry(collection, slug);
|
|
746
|
+
await this.api!.closeIssueOnPublish(collection, slug);
|
|
747
|
+
},
|
|
723
748
|
'Failed to acquire publish entry lock',
|
|
724
749
|
);
|
|
725
750
|
}
|
|
751
|
+
|
|
752
|
+
// Notes implementation, which is an abstraction to Github's PR issue comments.
|
|
753
|
+
|
|
754
|
+
// Notes implementation using GitHub Issues
|
|
755
|
+
async getNotes(collection: string, slug: string): Promise<Note[]> {
|
|
756
|
+
try {
|
|
757
|
+
const notes = await this.api!.getEntryNotes(collection, slug);
|
|
758
|
+
return notes.map(note => ({ ...note, entrySlug: slug }));
|
|
759
|
+
} catch (error) {
|
|
760
|
+
console.error('Failed to get notes:', error);
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async addNote(collection: string, slug: string, noteData: Omit<Note, 'id'>): Promise<Note> {
|
|
766
|
+
const currentUser = await this.currentUser({ token: this.token! });
|
|
767
|
+
|
|
768
|
+
const note: Note = {
|
|
769
|
+
...noteData,
|
|
770
|
+
id: 'temp-' + Date.now(),
|
|
771
|
+
author: currentUser.login || currentUser.name || '',
|
|
772
|
+
avatarUrl: currentUser.avatar_url,
|
|
773
|
+
entrySlug: slug,
|
|
774
|
+
timestamp: noteData.timestamp || new Date().toISOString(),
|
|
775
|
+
resolved: noteData.resolved || false,
|
|
776
|
+
issueUrl: undefined,
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// Get entry title for better issue naming
|
|
780
|
+
let entryTitle: string | undefined;
|
|
781
|
+
try {
|
|
782
|
+
const entryData = await this.getEntry(`${collection}/${slug}.md`);
|
|
783
|
+
const titleMatch = entryData.data.match(/^title:\s*["']?([^"'\n]+)["']?/m);
|
|
784
|
+
entryTitle = titleMatch ? titleMatch[1] : undefined;
|
|
785
|
+
} catch (error) {
|
|
786
|
+
// Entry not found or error reading, use undefined title
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const { commentId, issueUrl } = await this.api!.addNoteToEntry(
|
|
790
|
+
collection,
|
|
791
|
+
slug,
|
|
792
|
+
note,
|
|
793
|
+
entryTitle,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
...note,
|
|
798
|
+
id: commentId,
|
|
799
|
+
issueUrl,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async updateNote(
|
|
804
|
+
collection: string,
|
|
805
|
+
slug: string,
|
|
806
|
+
noteId: string,
|
|
807
|
+
updates: Partial<Note>,
|
|
808
|
+
): Promise<Note> {
|
|
809
|
+
const currentNotes = await this.getNotes(collection, slug);
|
|
810
|
+
const existingNote = currentNotes.find(note => note.id === noteId);
|
|
811
|
+
if (!existingNote) {
|
|
812
|
+
throw new Error(`Note with ID ${noteId} not found`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const updatedNote: Note = {
|
|
816
|
+
...existingNote,
|
|
817
|
+
...updates,
|
|
818
|
+
id: noteId,
|
|
819
|
+
entrySlug: slug,
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
await this.api!.updateEntryNote(noteId, updatedNote);
|
|
823
|
+
return updatedNote;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async deleteNote(collection: string, slug: string, noteId: string): Promise<void> {
|
|
827
|
+
const currentNotes = await this.getNotes(collection, slug);
|
|
828
|
+
const noteExists = currentNotes.some(note => note.id === noteId);
|
|
829
|
+
if (!noteExists) {
|
|
830
|
+
throw new Error(`Note with ID ${noteId} not found`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
await this.api!.deleteEntryNote(noteId);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async toggleNoteResolution(collection: string, slug: string, noteId: string): Promise<Note> {
|
|
837
|
+
const currentNotes = await this.getNotes(collection, slug);
|
|
838
|
+
const note = currentNotes.find(n => n.id === noteId);
|
|
839
|
+
if (!note) {
|
|
840
|
+
throw new Error(`Note with ID ${noteId} not found`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return this.updateNote(collection, slug, noteId, {
|
|
844
|
+
resolved: !note.resolved,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async reopenIssueForUnpublishedEntry(collection: string, slug: string) {
|
|
849
|
+
await this.api!.reopenIssueOnUnpublish(collection, slug);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Start watching notes for changes
|
|
853
|
+
* Called from Redux action
|
|
854
|
+
*/
|
|
855
|
+
async startNotesPolling(
|
|
856
|
+
collection: string,
|
|
857
|
+
slug: string,
|
|
858
|
+
callbacks: {
|
|
859
|
+
onUpdate: (notes: Note[], changes: IssueChange[]) => void;
|
|
860
|
+
onChange?: (change: IssueChange) => void;
|
|
861
|
+
},
|
|
862
|
+
): Promise<void> {
|
|
863
|
+
if (!this.pollingManager) {
|
|
864
|
+
console.warn('[DecapNotes Polling] Polling manager not initialized');
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const issueKey = `${collection}/${slug}`;
|
|
869
|
+
|
|
870
|
+
// Check if already watching this exact entry - if so, skip
|
|
871
|
+
if (this.pollingManager.getStatus().currentWatch === issueKey) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// First, ensure any previous polling for this entry is completely stopped
|
|
876
|
+
const existingUnwatch = this.unwatchFunctions.get(issueKey);
|
|
877
|
+
if (existingUnwatch) {
|
|
878
|
+
existingUnwatch();
|
|
879
|
+
this.unwatchFunctions.delete(issueKey);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
try {
|
|
883
|
+
const unwatchFn = await this.pollingManager.watchIssueWithRetry(
|
|
884
|
+
collection,
|
|
885
|
+
slug,
|
|
886
|
+
callbacks,
|
|
887
|
+
5, // maxRetries - will try up to 5 times
|
|
888
|
+
2000, // retryDelay - 2 seconds between attempts
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// Store the new unwatch function
|
|
892
|
+
this.unwatchFunctions.set(issueKey, unwatchFn);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
console.error('[DecapNotes Polling] Failed to start polling after retries:', error);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Stop watching notes for changes
|
|
900
|
+
* Called from Redux action: dispatch(stopNotesPolling(collection, slug))
|
|
901
|
+
*
|
|
902
|
+
* Ensures complete cleanup of polling for this entry
|
|
903
|
+
*/
|
|
904
|
+
async stopNotesPolling(collection: string, slug: string): Promise<void> {
|
|
905
|
+
const issueKey = `${collection}/${slug}`;
|
|
906
|
+
const unwatchFn = this.unwatchFunctions.get(issueKey);
|
|
907
|
+
|
|
908
|
+
if (unwatchFn) {
|
|
909
|
+
unwatchFn();
|
|
910
|
+
this.unwatchFunctions.delete(issueKey);
|
|
911
|
+
} else {
|
|
912
|
+
console.log(`[DecapNotes Polling] No active polling found for ${issueKey}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Manually refresh notes (force check now)
|
|
918
|
+
* Called from Redux action: dispatch(refreshNotesNow(collection, slug))
|
|
919
|
+
*/
|
|
920
|
+
async refreshNotesNow(collection: string, slug: string): Promise<void> {
|
|
921
|
+
if (!this.pollingManager) {
|
|
922
|
+
throw new Error('Polling manager not initialized');
|
|
923
|
+
}
|
|
924
|
+
await this.pollingManager.checkIssueNow(collection, slug);
|
|
925
|
+
}
|
|
726
926
|
}
|