hale-commenting-system 3.6.0 → 3.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hale-commenting-system",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "A commenting system for PatternFly React applications that allows designers and developers to add comments directly on design pages, sync with GitHub Issues, and link Jira tickets.",
5
5
  "homepage": "https://www.npmjs.com/package/hale-commenting-system",
6
6
  "license": "MIT",
@@ -47,8 +47,17 @@ interface LegacyJiraRecord {
47
47
 
48
48
  type JiraStore = Record<string, JiraRecord>;
49
49
 
50
+ // Jira Issue Cache
51
+ type CachedJiraIssue = {
52
+ ticket: JiraTicket;
53
+ fetchedAt: number; // timestamp
54
+ };
55
+ type JiraIssueCache = Record<string, CachedJiraIssue>;
56
+
50
57
  const STORAGE_KEY = 'hale_commenting_jira_v1';
58
+ const CACHE_STORAGE_KEY = 'hale_commenting_jira_cache_v1';
51
59
  const GH_JIRA_PATH = '.hale/jira.json';
60
+ const CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
52
61
 
53
62
  function migrateRecord(record: LegacyJiraRecord): JiraRecord {
54
63
  // If it's already in the new format, return as-is
@@ -100,6 +109,50 @@ function setStore(next: JiraStore) {
100
109
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
101
110
  }
102
111
 
112
+ function getIssueCache(): JiraIssueCache {
113
+ if (typeof window === 'undefined') return {};
114
+ try {
115
+ const raw = window.localStorage.getItem(CACHE_STORAGE_KEY);
116
+ if (!raw) return {};
117
+ return JSON.parse(raw) as JiraIssueCache;
118
+ } catch {
119
+ return {};
120
+ }
121
+ }
122
+
123
+ function setIssueCache(cache: JiraIssueCache) {
124
+ if (typeof window === 'undefined') return;
125
+ window.localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(cache));
126
+ }
127
+
128
+ function getCachedIssue(key: string): JiraTicket | null {
129
+ const cache = getIssueCache();
130
+ const cached = cache[key];
131
+ if (!cached) return null;
132
+
133
+ const now = Date.now();
134
+ const age = now - cached.fetchedAt;
135
+
136
+ // Return cached data if it's still fresh
137
+ if (age < CACHE_TTL_MS) {
138
+ return cached.ticket;
139
+ }
140
+
141
+ // Expired - remove it
142
+ delete cache[key];
143
+ setIssueCache(cache);
144
+ return null;
145
+ }
146
+
147
+ function setCachedIssue(key: string, ticket: JiraTicket) {
148
+ const cache = getIssueCache();
149
+ cache[key] = {
150
+ ticket,
151
+ fetchedAt: Date.now(),
152
+ };
153
+ setIssueCache(cache);
154
+ }
155
+
103
156
  function normalizePathname(pathname: string): string {
104
157
  if (!pathname) return '/';
105
158
  const cleaned = pathname.split('?')[0].split('#')[0];
@@ -339,7 +392,7 @@ export const JiraTab: React.FunctionComponent = () => {
339
392
  // eslint-disable-next-line react-hooks/exhaustive-deps
340
393
  }, []);
341
394
 
342
- // Fetch Jira issue details for all keys.
395
+ // Fetch Jira issue details for all keys (with caching).
343
396
  React.useEffect(() => {
344
397
  const keys = record?.jiraKeys || [];
345
398
  if (keys.length === 0) {
@@ -354,24 +407,50 @@ export const JiraTab: React.FunctionComponent = () => {
354
407
  const results: Record<string, JiraTicket> = {};
355
408
  const errors: Record<string, string> = {};
356
409
 
357
- await Promise.all(
358
- keys.map(async (key) => {
359
- try {
360
- const resp = await fetch(`/api/jira-issue?key=${encodeURIComponent(key)}`);
361
- const payload = await resp.json().catch(() => ({}));
362
- if (!resp.ok) {
363
- const raw = String(payload?.message || `Failed to fetch Jira issue (${resp.status})`);
364
- const sanitized = raw.trim().startsWith('<') ? 'Unauthorized or non-JSON response from Jira.' : raw;
365
- const hint = payload?.hint ? ` ${String(payload.hint)}` : '';
366
- errors[key] = `${sanitized}${hint}`;
367
- return;
410
+ // Check cache first
411
+ const keysToFetch: string[] = [];
412
+ for (const key of keys) {
413
+ const cached = getCachedIssue(key);
414
+ if (cached) {
415
+ results[key] = cached;
416
+ } else {
417
+ keysToFetch.push(key);
418
+ }
419
+ }
420
+
421
+ // Fetch uncached issues
422
+ if (keysToFetch.length > 0) {
423
+ await Promise.all(
424
+ keysToFetch.map(async (key) => {
425
+ try {
426
+ const resp = await fetch(`/api/jira-issue?key=${encodeURIComponent(key)}`);
427
+ const payload = await resp.json().catch(() => ({}));
428
+
429
+ if (!resp.ok) {
430
+ // Handle rate limiting specially
431
+ if (resp.status === 429) {
432
+ errors[key] = 'Rate limit exceeded. Please wait a few minutes and refresh the page.';
433
+ return;
434
+ }
435
+
436
+ const raw = String(payload?.message || `Failed to fetch Jira issue (${resp.status})`);
437
+ const sanitized = raw.trim().startsWith('<') ? 'Unauthorized or non-JSON response from Jira.' : raw;
438
+ const hint = payload?.hint ? ` ${String(payload.hint)}` : '';
439
+ errors[key] = `${sanitized}${hint}`;
440
+ return;
441
+ }
442
+
443
+ const ticket = payload as JiraTicket;
444
+ results[key] = ticket;
445
+
446
+ // Cache the successful result
447
+ setCachedIssue(key, ticket);
448
+ } catch (e: any) {
449
+ errors[key] = e?.message || 'Failed to fetch Jira issue';
368
450
  }
369
- results[key] = payload as JiraTicket;
370
- } catch (e: any) {
371
- errors[key] = e?.message || 'Failed to fetch Jira issue';
372
- }
373
- })
374
- );
451
+ })
452
+ );
453
+ }
375
454
 
376
455
  setIssues(results);
377
456
  setIssueErrors(errors);
@@ -735,7 +814,14 @@ export const JiraTab: React.FunctionComponent = () => {
735
814
 
736
815
  {/* Error state */}
737
816
  {error && !isFetchingIssues && (
738
- <div style={{ fontSize: '0.875rem', color: 'var(--pf-t--global--danger--color--100)' }}>{error}</div>
817
+ <div style={{ display: 'grid', gap: '0.5rem' }}>
818
+ <div style={{ fontSize: '0.875rem', color: 'var(--pf-t--global--danger--color--100)' }}>{error}</div>
819
+ {error.includes('Rate limit') && (
820
+ <div style={{ fontSize: '0.75rem', color: 'var(--pf-t--global--text--color--subtle)' }}>
821
+ Tip: Jira data is cached for 15 minutes to reduce API calls. Refreshing the page will retry.
822
+ </div>
823
+ )}
824
+ </div>
739
825
  )}
740
826
 
741
827
  {/* Issue details */}