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.
@@ -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 ___EmotionJSX } from "@emotion/react";
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 => ___EmotionJSX(AuthenticationPage, _extends({}, 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, () => this.api.deleteUnpublishedEntry(collection, slug), 'Failed to acquire delete entry 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, () => this.api.publishUnpublishedEntry(collection, slug), 'Failed to acquire publish entry 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.6.0",
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": "45c9f5b9a1a12f74321ce4658b71ec88d6365ec1"
47
+ "gitHead": "ffe5ab7e61f4e7cb374a413bf9fe55f088d20ba2"
48
48
  }