hale-commenting-system 3.7.2 ā 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/package.json +1 -1
- package/scripts/integrate.js +174 -33
- package/src/app/commenting-system/components/CommentPanel.tsx +7 -4
- package/src/app/commenting-system/components/FloatingWidget.tsx +52 -7
- package/src/app/commenting-system/contexts/CommentContext.tsx +33 -30
- package/src/app/commenting-system/contexts/GitHubAuthContext.tsx +8 -85
- package/src/app/commenting-system/contexts/ProviderAuthContext.tsx +151 -0
- package/src/app/commenting-system/index.ts +2 -1
- package/src/app/commenting-system/services/githubAdapter.ts +56 -40
- package/src/app/commenting-system/services/gitlabAdapter.ts +400 -0
- package/src/app/commenting-system/services/providerFactory.ts +148 -0
- package/src/app/commenting-system/types/index.ts +2 -1
- package/src/app/commenting-system/types/provider.ts +117 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hale-commenting-system",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.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",
|
package/scripts/integrate.js
CHANGED
|
@@ -369,12 +369,34 @@ function generateFiles(config) {
|
|
|
369
369
|
|
|
370
370
|
`;
|
|
371
371
|
|
|
372
|
-
|
|
372
|
+
// Provider configuration (GitHub or GitLab)
|
|
373
|
+
if (config.provider && config.provider.type === 'gitlab') {
|
|
374
|
+
envContent += `# Provider Type
|
|
375
|
+
VITE_PROVIDER_TYPE=gitlab
|
|
376
|
+
|
|
377
|
+
# GitLab OAuth (client-side; safe to expose)
|
|
378
|
+
VITE_GITLAB_CLIENT_ID=${config.provider.clientId}
|
|
379
|
+
VITE_GITLAB_BASE_URL=${config.provider.baseUrl}
|
|
380
|
+
|
|
381
|
+
# Target project for Issues/Comments
|
|
382
|
+
VITE_GITLAB_PROJECT_PATH=${config.provider.projectPath}
|
|
383
|
+
|
|
384
|
+
`;
|
|
385
|
+
} else if (config.provider && config.provider.type === 'github') {
|
|
386
|
+
envContent += `# Provider Type
|
|
387
|
+
VITE_PROVIDER_TYPE=github
|
|
388
|
+
|
|
389
|
+
# GitHub OAuth (client-side; safe to expose)
|
|
390
|
+
VITE_GITHUB_CLIENT_ID=${config.provider.clientId}
|
|
391
|
+
|
|
392
|
+
# Target repo for Issues/Comments
|
|
393
|
+
VITE_GITHUB_OWNER=${config.provider.owner || config.owner}
|
|
394
|
+
VITE_GITHUB_REPO=${config.provider.repo || config.repo}
|
|
395
|
+
|
|
396
|
+
`;
|
|
397
|
+
} else if (config.github && config.github.clientId) {
|
|
398
|
+
// Backward compatibility: old GitHub-only config
|
|
373
399
|
envContent += `# GitHub OAuth (client-side; safe to expose)
|
|
374
|
-
# Get your Client ID from: https://github.com/settings/developers
|
|
375
|
-
# 1. Click "New OAuth App"
|
|
376
|
-
# 2. Fill in the form (Homepage: http://localhost:9000, Callback: http://localhost:9000/api/github-oauth-callback)
|
|
377
|
-
# 3. Copy the Client ID
|
|
378
400
|
VITE_GITHUB_CLIENT_ID=${config.github.clientId}
|
|
379
401
|
|
|
380
402
|
# Target repo for Issues/Comments
|
|
@@ -385,14 +407,11 @@ VITE_GITHUB_REPO=${config.github.repo || config.repo}
|
|
|
385
407
|
} else {
|
|
386
408
|
envContent += `# GitHub OAuth (client-side; safe to expose)
|
|
387
409
|
# Get your Client ID from: https://github.com/settings/developers
|
|
388
|
-
# 1. Click "New OAuth App"
|
|
389
|
-
# 2. Fill in the form (Homepage: http://localhost:9000, Callback: http://localhost:9000/api/github-oauth-callback)
|
|
390
|
-
# 3. Copy the Client ID
|
|
391
410
|
VITE_GITHUB_CLIENT_ID=
|
|
392
411
|
|
|
393
412
|
# Target repo for Issues/Comments
|
|
394
|
-
VITE_GITHUB_OWNER=${config.owner}
|
|
395
|
-
VITE_GITHUB_REPO=${config.repo}
|
|
413
|
+
VITE_GITHUB_OWNER=${config.owner || ''}
|
|
414
|
+
VITE_GITHUB_REPO=${config.repo || ''}
|
|
396
415
|
|
|
397
416
|
`;
|
|
398
417
|
}
|
|
@@ -457,7 +476,19 @@ VITE_JIRA_BASE_URL=
|
|
|
457
476
|
|
|
458
477
|
`;
|
|
459
478
|
|
|
460
|
-
|
|
479
|
+
// Provider secrets (GitHub or GitLab)
|
|
480
|
+
if (config.provider && config.provider.type === 'gitlab' && config.provider.clientSecret) {
|
|
481
|
+
envServerContent += `# GitLab OAuth Application Secret (server-only)
|
|
482
|
+
GITLAB_CLIENT_SECRET=${config.provider.clientSecret}
|
|
483
|
+
|
|
484
|
+
`;
|
|
485
|
+
} else if (config.provider && config.provider.type === 'github' && config.provider.clientSecret) {
|
|
486
|
+
envServerContent += `# GitHub OAuth Client Secret (server-only)
|
|
487
|
+
GITHUB_CLIENT_SECRET=${config.provider.clientSecret}
|
|
488
|
+
|
|
489
|
+
`;
|
|
490
|
+
} else if (config.github && config.github.clientSecret) {
|
|
491
|
+
// Backward compatibility: old GitHub-only config
|
|
461
492
|
envServerContent += `# GitHub OAuth Client Secret (server-only)
|
|
462
493
|
# Get this from your GitHub OAuth App settings: https://github.com/settings/developers
|
|
463
494
|
# Click on your OAuth App, then "Generate a new client secret"
|
|
@@ -1355,24 +1386,35 @@ async function main() {
|
|
|
1355
1386
|
}
|
|
1356
1387
|
}
|
|
1357
1388
|
|
|
1358
|
-
// Step 2:
|
|
1359
|
-
console.log('\nš¦ Step 2:
|
|
1360
|
-
console.log('
|
|
1361
|
-
console.log('
|
|
1362
|
-
|
|
1363
|
-
|
|
1389
|
+
// Step 2: Issue Tracking Integration
|
|
1390
|
+
console.log('\nš¦ Step 2: Issue Tracking Integration\n');
|
|
1391
|
+
console.log('Comments can sync with GitHub or GitLab Issues.');
|
|
1392
|
+
console.log('This allows comments to persist and be managed like regular issues.\n');
|
|
1393
|
+
console.log('Options:');
|
|
1394
|
+
console.log(' ⢠GitHub - Sync with GitHub Issues');
|
|
1395
|
+
console.log(' ⢠GitLab - Sync with GitLab Issues (supports self-hosted)');
|
|
1396
|
+
console.log(' ⢠Skip - Set up later (you can still use local comments)\n');
|
|
1397
|
+
|
|
1398
|
+
const platformChoice = await prompt([
|
|
1364
1399
|
{
|
|
1365
|
-
type: '
|
|
1366
|
-
name: '
|
|
1367
|
-
message: '
|
|
1368
|
-
|
|
1400
|
+
type: 'list',
|
|
1401
|
+
name: 'platform',
|
|
1402
|
+
message: 'Select your issue tracking platform:',
|
|
1403
|
+
choices: [
|
|
1404
|
+
{ name: 'GitHub', value: 'github' },
|
|
1405
|
+
{ name: 'GitLab', value: 'gitlab' },
|
|
1406
|
+
{ name: 'Skip (set up later)', value: 'skip' }
|
|
1407
|
+
],
|
|
1408
|
+
default: 'github'
|
|
1369
1409
|
}
|
|
1370
1410
|
]);
|
|
1371
1411
|
|
|
1372
|
-
|
|
1373
|
-
let githubValid = false;
|
|
1412
|
+
const selectedPlatform = platformChoice.platform; // 'github', 'gitlab', or 'skip'
|
|
1374
1413
|
|
|
1375
|
-
|
|
1414
|
+
let providerConfig = null;
|
|
1415
|
+
let providerValid = false;
|
|
1416
|
+
|
|
1417
|
+
if (selectedPlatform === 'github') {
|
|
1376
1418
|
console.log('\nTo sync comments with GitHub Issues, we need to authenticate with GitHub.');
|
|
1377
1419
|
console.log('This requires creating a GitHub OAuth App.\n');
|
|
1378
1420
|
console.log('Instructions:');
|
|
@@ -1610,14 +1652,112 @@ async function main() {
|
|
|
1610
1652
|
}
|
|
1611
1653
|
console.log('ā
GitHub credentials validated!\n');
|
|
1612
1654
|
|
|
1613
|
-
|
|
1655
|
+
providerConfig = {
|
|
1656
|
+
type: 'github',
|
|
1614
1657
|
clientId: githubAnswers.clientId,
|
|
1615
1658
|
clientSecret: githubAnswers.clientSecret,
|
|
1616
1659
|
owner: targetOwner,
|
|
1617
1660
|
repo: targetRepo
|
|
1618
1661
|
};
|
|
1619
|
-
|
|
1620
|
-
|
|
1662
|
+
providerValid = githubValid;
|
|
1663
|
+
} else if (selectedPlatform === 'gitlab') {
|
|
1664
|
+
// GitLab setup flow
|
|
1665
|
+
console.log('\nTo sync comments with GitLab Issues, we need to authenticate with GitLab.');
|
|
1666
|
+
console.log('This requires creating a GitLab OAuth Application.\n');
|
|
1667
|
+
|
|
1668
|
+
// Prompt for GitLab instance URL
|
|
1669
|
+
console.log('š” This supports both gitlab.com and self-hosted GitLab instances.');
|
|
1670
|
+
console.log(' Examples:');
|
|
1671
|
+
console.log(' ⢠https://gitlab.com (public GitLab)');
|
|
1672
|
+
console.log(' ⢠https://gitlab.cee.redhat.com (Red Hat internal)\n');
|
|
1673
|
+
|
|
1674
|
+
const gitlabInstanceAnswer = await prompt([
|
|
1675
|
+
{
|
|
1676
|
+
type: 'input',
|
|
1677
|
+
name: 'baseUrl',
|
|
1678
|
+
message: 'GitLab instance URL:',
|
|
1679
|
+
default: 'https://gitlab.com',
|
|
1680
|
+
validate: (input) => {
|
|
1681
|
+
if (!input.trim()) return 'Base URL is required';
|
|
1682
|
+
try {
|
|
1683
|
+
new URL(input);
|
|
1684
|
+
return true;
|
|
1685
|
+
} catch {
|
|
1686
|
+
return 'Invalid URL format (must start with https://)';
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
]);
|
|
1691
|
+
|
|
1692
|
+
const baseUrl = gitlabInstanceAnswer.baseUrl.replace(/\/+$/, '');
|
|
1693
|
+
const isSelfHosted = !baseUrl.includes('gitlab.com');
|
|
1694
|
+
|
|
1695
|
+
console.log('\nInstructions:');
|
|
1696
|
+
console.log(`1. Visit: ${baseUrl}/-/user_settings/applications`);
|
|
1697
|
+
console.log('2. Click "Add new application"');
|
|
1698
|
+
console.log('3. Fill in the form:');
|
|
1699
|
+
console.log(' - Name: Your app name (e.g., "My Design Comments")');
|
|
1700
|
+
console.log(' - Redirect URI: http://localhost:9000/api/gitlab-oauth-callback');
|
|
1701
|
+
console.log(' - Confidential: ā (checked)');
|
|
1702
|
+
console.log(' - Scopes: ā api (full API access)');
|
|
1703
|
+
console.log('4. Click "Save application"');
|
|
1704
|
+
console.log('5. Copy the Application ID and Secret\n');
|
|
1705
|
+
|
|
1706
|
+
const gitlabAnswers = await prompt([
|
|
1707
|
+
{
|
|
1708
|
+
type: 'input',
|
|
1709
|
+
name: 'clientId',
|
|
1710
|
+
message: 'GitLab Application ID:',
|
|
1711
|
+
validate: (input) => {
|
|
1712
|
+
if (!input.trim()) return 'Application ID is required';
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
type: 'password',
|
|
1718
|
+
name: 'clientSecret',
|
|
1719
|
+
message: 'GitLab Application Secret:',
|
|
1720
|
+
mask: '*',
|
|
1721
|
+
validate: (input) => {
|
|
1722
|
+
if (!input.trim()) return 'Application Secret is required';
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
]);
|
|
1727
|
+
|
|
1728
|
+
// Prompt for project path
|
|
1729
|
+
console.log('\nWhere do you want to store comments as GitLab Issues?');
|
|
1730
|
+
console.log('This should be a project you have maintainer/owner access to.');
|
|
1731
|
+
console.log('Format: group/project or namespace/group/project\n');
|
|
1732
|
+
|
|
1733
|
+
const projectPathAnswer = await prompt([
|
|
1734
|
+
{
|
|
1735
|
+
type: 'input',
|
|
1736
|
+
name: 'projectPath',
|
|
1737
|
+
message: 'GitLab project path (e.g., mygroup/myproject):',
|
|
1738
|
+
validate: (input) => {
|
|
1739
|
+
if (!input.trim()) return 'Project path is required';
|
|
1740
|
+
if (!input.includes('/')) return 'Project path must include at least one slash (e.g., group/project)';
|
|
1741
|
+
return true;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
]);
|
|
1745
|
+
|
|
1746
|
+
// Note: GitLab credential validation would require more complex setup
|
|
1747
|
+
console.log('\nā ļø Note: GitLab credentials will not be validated automatically.');
|
|
1748
|
+
console.log('Please ensure you have maintainer/owner access to the project and the OAuth app is configured correctly.\n');
|
|
1749
|
+
|
|
1750
|
+
providerConfig = {
|
|
1751
|
+
type: 'gitlab',
|
|
1752
|
+
clientId: gitlabAnswers.clientId,
|
|
1753
|
+
clientSecret: gitlabAnswers.clientSecret,
|
|
1754
|
+
baseUrl: baseUrl,
|
|
1755
|
+
projectPath: projectPathAnswer.projectPath
|
|
1756
|
+
};
|
|
1757
|
+
providerValid = true; // Assume valid since we can't validate GitLab easily
|
|
1758
|
+
} else if (selectedPlatform === 'skip') {
|
|
1759
|
+
console.log('\nāļø Skipping issue tracking setup. Comments will work locally only.');
|
|
1760
|
+
console.log('You can add GitHub or GitLab integration later by editing .env and .env.server files.\n');
|
|
1621
1761
|
}
|
|
1622
1762
|
|
|
1623
1763
|
// Step 3: Jira Setup (Optional)
|
|
@@ -1702,10 +1842,11 @@ async function main() {
|
|
|
1702
1842
|
// Step 4: Generate files
|
|
1703
1843
|
console.log('š Step 4: Generating configuration files...\n');
|
|
1704
1844
|
generateFiles({
|
|
1705
|
-
|
|
1845
|
+
provider: providerConfig,
|
|
1846
|
+
github: providerConfig && providerConfig.type === 'github' ? providerConfig : null, // For backward compat
|
|
1706
1847
|
jira: jiraConfig,
|
|
1707
|
-
owner:
|
|
1708
|
-
repo:
|
|
1848
|
+
owner: providerConfig && providerConfig.type === 'github' ? providerConfig.owner : owner,
|
|
1849
|
+
repo: providerConfig && providerConfig.type === 'github' ? providerConfig.repo : repo
|
|
1709
1850
|
});
|
|
1710
1851
|
|
|
1711
1852
|
// Step 5: Integrate into project
|
|
@@ -1785,10 +1926,10 @@ async function main() {
|
|
|
1785
1926
|
console.log(' 4. Restart your dev server\n');
|
|
1786
1927
|
}
|
|
1787
1928
|
|
|
1788
|
-
if (!
|
|
1929
|
+
if (!providerConfig || !jiraConfig) {
|
|
1789
1930
|
console.log('š To add integrations later:');
|
|
1790
|
-
if (!
|
|
1791
|
-
console.log(' ⢠GitHub: Edit .env and .env.server files (see comments in files for instructions)');
|
|
1931
|
+
if (!providerConfig) {
|
|
1932
|
+
console.log(' ⢠GitHub/GitLab: Edit .env and .env.server files (see comments in files for instructions)');
|
|
1792
1933
|
}
|
|
1793
1934
|
if (!jiraConfig) {
|
|
1794
1935
|
console.log(' ⢠Jira: Edit .env and .env.server files (see comments in files for instructions)');
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
TextArea,
|
|
18
18
|
Title,
|
|
19
19
|
} from '@patternfly/react-core';
|
|
20
|
-
import { ExternalLinkAltIcon, GithubIcon, InfoCircleIcon, TrashIcon } from '@patternfly/react-icons';
|
|
20
|
+
import { ExternalLinkAltIcon, GithubIcon, GitlabIcon, InfoCircleIcon, TrashIcon } from '@patternfly/react-icons';
|
|
21
21
|
import { useComments } from '../contexts/CommentContext';
|
|
22
|
+
import { useProviderAuth } from '../contexts/ProviderAuthContext';
|
|
22
23
|
import { DetailsTab } from './DetailsTab';
|
|
23
24
|
import { JiraTab } from './JiraTab';
|
|
24
25
|
import { FloatingWidget } from './FloatingWidget';
|
|
@@ -41,6 +42,8 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
41
42
|
removePin,
|
|
42
43
|
retrySync,
|
|
43
44
|
} = useComments();
|
|
45
|
+
const { providerType } = useProviderAuth();
|
|
46
|
+
const ProviderIcon = providerType === 'gitlab' ? GitlabIcon : GithubIcon;
|
|
44
47
|
const location = useLocation();
|
|
45
48
|
const detectedVersion = getVersionFromPathOrQuery(location.pathname, location.search);
|
|
46
49
|
const [newCommentText, setNewCommentText] = React.useState('');
|
|
@@ -170,7 +173,7 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
170
173
|
switch (status) {
|
|
171
174
|
case 'synced':
|
|
172
175
|
return (
|
|
173
|
-
<Label color="green" icon={<
|
|
176
|
+
<Label color="green" icon={<ProviderIcon />}>
|
|
174
177
|
Synced
|
|
175
178
|
</Label>
|
|
176
179
|
);
|
|
@@ -293,13 +296,13 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
293
296
|
rel="noopener noreferrer"
|
|
294
297
|
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}
|
|
295
298
|
>
|
|
296
|
-
<
|
|
299
|
+
<ProviderIcon />
|
|
297
300
|
Issue #{selectedThread.issueNumber}
|
|
298
301
|
<ExternalLinkAltIcon style={{ fontSize: '0.75rem' }} />
|
|
299
302
|
</a>
|
|
300
303
|
) : (
|
|
301
304
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', color: 'var(--pf-t--global--text--color--subtle)' }}>
|
|
302
|
-
<
|
|
305
|
+
<ProviderIcon />
|
|
303
306
|
Issue pendingā¦
|
|
304
307
|
</span>
|
|
305
308
|
)}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import { Button, Switch, Title } from '@patternfly/react-core';
|
|
4
|
-
import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon, ArrowsAltVIcon } from '@patternfly/react-icons';
|
|
3
|
+
import { Button, Dropdown, DropdownItem, DropdownList, MenuToggle, Switch, Title } from '@patternfly/react-core';
|
|
4
|
+
import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon, GitlabIcon, ArrowsAltVIcon } from '@patternfly/react-icons';
|
|
5
5
|
import { useComments } from '../contexts/CommentContext';
|
|
6
|
-
import {
|
|
6
|
+
import { useProviderAuth } from '../contexts/ProviderAuthContext';
|
|
7
7
|
|
|
8
8
|
interface FloatingWidgetProps {
|
|
9
9
|
children: React.ReactNode;
|
|
@@ -23,7 +23,10 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
|
|
|
23
23
|
const resizeHandleRef = React.useRef<HTMLDivElement>(null);
|
|
24
24
|
|
|
25
25
|
const { commentsEnabled, setCommentsEnabled, showPinsEnabled, setShowPinsEnabled } = useComments();
|
|
26
|
-
const { isAuthenticated, user, login, logout } =
|
|
26
|
+
const { isAuthenticated, user, login, logout, providerType, providerDisplayName, availableProviders } = useProviderAuth();
|
|
27
|
+
const ProviderIcon = providerType === 'gitlab' ? GitlabIcon : GithubIcon;
|
|
28
|
+
const [isSignInOpen, setIsSignInOpen] = React.useState(false);
|
|
29
|
+
const showProviderMenu = availableProviders.length > 1;
|
|
27
30
|
|
|
28
31
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
29
32
|
if (!widgetRef.current) return;
|
|
@@ -211,16 +214,58 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
|
|
|
211
214
|
{isAuthenticated ? (
|
|
212
215
|
<>
|
|
213
216
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', fontSize: 'var(--pf-t--global--font--size--sm)' }}>
|
|
214
|
-
<
|
|
217
|
+
<ProviderIcon />
|
|
215
218
|
{user?.login ? `@${user.login}` : 'Signed in'}
|
|
216
219
|
</span>
|
|
217
220
|
<Button variant="link" isInline onClick={logout} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
|
|
218
221
|
Sign out
|
|
219
222
|
</Button>
|
|
220
223
|
</>
|
|
224
|
+
) : showProviderMenu ? (
|
|
225
|
+
<Dropdown
|
|
226
|
+
isOpen={isSignInOpen}
|
|
227
|
+
onSelect={() => setIsSignInOpen(false)}
|
|
228
|
+
toggle={(toggleRef) => (
|
|
229
|
+
<MenuToggle
|
|
230
|
+
ref={toggleRef}
|
|
231
|
+
variant="plain"
|
|
232
|
+
isExpanded={isSignInOpen}
|
|
233
|
+
onClick={() => setIsSignInOpen((prev) => !prev)}
|
|
234
|
+
style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}
|
|
235
|
+
aria-label="Sign in menu"
|
|
236
|
+
>
|
|
237
|
+
Sign in
|
|
238
|
+
</MenuToggle>
|
|
239
|
+
)}
|
|
240
|
+
>
|
|
241
|
+
<DropdownList>
|
|
242
|
+
{availableProviders.includes('github') && (
|
|
243
|
+
<DropdownItem
|
|
244
|
+
onClick={() => {
|
|
245
|
+
setIsSignInOpen(false);
|
|
246
|
+
login('github');
|
|
247
|
+
}}
|
|
248
|
+
icon={<GithubIcon />}
|
|
249
|
+
>
|
|
250
|
+
Sign in with GitHub
|
|
251
|
+
</DropdownItem>
|
|
252
|
+
)}
|
|
253
|
+
{availableProviders.includes('gitlab') && (
|
|
254
|
+
<DropdownItem
|
|
255
|
+
onClick={() => {
|
|
256
|
+
setIsSignInOpen(false);
|
|
257
|
+
login('gitlab');
|
|
258
|
+
}}
|
|
259
|
+
icon={<GitlabIcon />}
|
|
260
|
+
>
|
|
261
|
+
Sign in with GitLab
|
|
262
|
+
</DropdownItem>
|
|
263
|
+
)}
|
|
264
|
+
</DropdownList>
|
|
265
|
+
</Dropdown>
|
|
221
266
|
) : (
|
|
222
|
-
<Button variant="link" isInline icon={<
|
|
223
|
-
Sign in with
|
|
267
|
+
<Button variant="link" isInline icon={<ProviderIcon />} onClick={() => login()} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
|
|
268
|
+
Sign in with {providerDisplayName}
|
|
224
269
|
</Button>
|
|
225
270
|
)}
|
|
226
271
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Comment, Thread, ComponentMetadata } from '../types';
|
|
3
|
-
import {
|
|
3
|
+
import { getProviderAdapter } from '../services/providerFactory';
|
|
4
|
+
import { getStoredUser } from '../services/githubAdapter';
|
|
4
5
|
|
|
5
6
|
interface CommentContextType {
|
|
6
7
|
threads: Thread[];
|
|
@@ -31,6 +32,8 @@ interface CommentContextType {
|
|
|
31
32
|
const CommentContext = React.createContext<CommentContextType | undefined>(undefined);
|
|
32
33
|
|
|
33
34
|
export const CommentProvider: React.FunctionComponent<{ children: React.ReactNode }> = ({ children }) => {
|
|
35
|
+
const adapter = getProviderAdapter();
|
|
36
|
+
|
|
34
37
|
const stripHaleReplyMarkers = (body: string): string => {
|
|
35
38
|
// Remove hidden markers we embed for threading reconstruction
|
|
36
39
|
return body
|
|
@@ -164,10 +167,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
164
167
|
}
|
|
165
168
|
// If not set, default to enabled when GitHub is not configured (standalone mode)
|
|
166
169
|
// This allows the commenting system to work without GitHub/Jira integration
|
|
167
|
-
return !
|
|
170
|
+
return !adapter.isConfigured();
|
|
168
171
|
} catch {
|
|
169
172
|
// On error, default to enabled if GitHub is not configured
|
|
170
|
-
return !
|
|
173
|
+
return !adapter.isConfigured();
|
|
171
174
|
}
|
|
172
175
|
});
|
|
173
176
|
const [selectedThreadId, setSelectedThreadId] = React.useState<string | null>(null);
|
|
@@ -180,10 +183,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
180
183
|
}
|
|
181
184
|
// If not set, default to open when GitHub is not configured (standalone mode)
|
|
182
185
|
// This makes the commenting system visible immediately
|
|
183
|
-
return !
|
|
186
|
+
return !adapter.isConfigured();
|
|
184
187
|
} catch {
|
|
185
188
|
// On error, default to open if GitHub is not configured
|
|
186
|
-
return !
|
|
189
|
+
return !adapter.isConfigured();
|
|
187
190
|
}
|
|
188
191
|
});
|
|
189
192
|
const [floatingWidgetMode, setFloatingWidgetMode] = React.useState<boolean>(() => {
|
|
@@ -257,7 +260,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
257
260
|
|
|
258
261
|
const addThread = (cssSelector: string, elementDescription: string, componentMetadata: ComponentMetadata | null, xPercent: number, yPercent: number, route: string, version?: string): string => {
|
|
259
262
|
const threadId = `thread-${Date.now()}`;
|
|
260
|
-
const isConfigured =
|
|
263
|
+
const isConfigured = adapter.isConfigured();
|
|
261
264
|
|
|
262
265
|
console.log('š addThread called:', {
|
|
263
266
|
threadId,
|
|
@@ -295,7 +298,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
295
298
|
if (isConfigured) {
|
|
296
299
|
console.log(`šµ Creating GitHub issue for thread ${threadId}...`);
|
|
297
300
|
|
|
298
|
-
|
|
301
|
+
adapter
|
|
299
302
|
.createIssue({
|
|
300
303
|
title: `Feedback: ${route}`,
|
|
301
304
|
body: `Thread created from pin at (${xPercent.toFixed(1)}%, ${yPercent.toFixed(1)}%).`,
|
|
@@ -318,7 +321,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
318
321
|
const num = result.data.number;
|
|
319
322
|
hiddenIssueNumbersRef.current.add(num);
|
|
320
323
|
writeNumberSet(HIDDEN_ISSUES_KEY, hiddenIssueNumbersRef.current);
|
|
321
|
-
|
|
324
|
+
adapter.closeIssue(num).catch(() => undefined);
|
|
322
325
|
removedThreadIdsRef.current.delete(threadId);
|
|
323
326
|
}
|
|
324
327
|
|
|
@@ -394,7 +397,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
394
397
|
};
|
|
395
398
|
|
|
396
399
|
const syncFromGitHub = async (route: string, version?: string) => {
|
|
397
|
-
if (!
|
|
400
|
+
if (!adapter.isConfigured()) return;
|
|
398
401
|
|
|
399
402
|
const key = `${route}::${version ?? ''}`;
|
|
400
403
|
const existing = syncInFlightByKey.current.get(key);
|
|
@@ -417,7 +420,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
417
420
|
const run = (async () => {
|
|
418
421
|
setSyncInFlightCount((c) => c + 1);
|
|
419
422
|
try {
|
|
420
|
-
const issuesResult = await
|
|
423
|
+
const issuesResult = await adapter.fetchIssuesForRouteAndVersion(route, version);
|
|
421
424
|
if (!issuesResult.success || !issuesResult.data) return;
|
|
422
425
|
|
|
423
426
|
const hidden = hiddenIssueNumbersRef.current;
|
|
@@ -436,7 +439,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
436
439
|
|
|
437
440
|
const metadata = parseMetadataFromIssueBody(issue?.body || '');
|
|
438
441
|
|
|
439
|
-
const commentsResult = await
|
|
442
|
+
const commentsResult = await adapter.fetchIssueComments(issueNumber);
|
|
440
443
|
const ghComments = commentsResult.success && commentsResult.data ? commentsResult.data : [];
|
|
441
444
|
|
|
442
445
|
const mappedComments: Comment[] = (Array.isArray(ghComments) ? ghComments : []).map((c: any) => {
|
|
@@ -578,7 +581,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
578
581
|
const thread = threadsRef.current.find((t) => t.id === threadId);
|
|
579
582
|
const issueNumber = thread?.issueNumber;
|
|
580
583
|
|
|
581
|
-
if (!
|
|
584
|
+
if (!adapter.isConfigured() || !thread) return;
|
|
582
585
|
|
|
583
586
|
// If the thread hasn't finished creating its issue yet, create it now, then backfill any local-only comments.
|
|
584
587
|
const ensureIssueAndBackfill = async () => {
|
|
@@ -591,7 +594,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
591
594
|
let ensuredIssueUrl = thread.issueUrl;
|
|
592
595
|
|
|
593
596
|
if (!ensuredIssueNumber) {
|
|
594
|
-
const created = await
|
|
597
|
+
const created = await adapter.createIssue({
|
|
595
598
|
title: `Feedback: ${thread.route}`,
|
|
596
599
|
body: `Thread created from pin${thread.elementDescription ? ` on ${thread.elementDescription}` : ''}.`,
|
|
597
600
|
route: thread.route,
|
|
@@ -672,7 +675,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
672
675
|
}
|
|
673
676
|
|
|
674
677
|
const body = buildGitHubReplyBody(c.text, resolvedParentGitHubId ? { ...parentForBody, githubCommentId: resolvedParentGitHubId } : parentForBody);
|
|
675
|
-
const res = await
|
|
678
|
+
const res = await adapter.createComment(ensuredIssueNumber!, body);
|
|
676
679
|
if (!res.success || !res.data?.id) {
|
|
677
680
|
throw new Error(res.error || 'Failed to create GitHub comment');
|
|
678
681
|
}
|
|
@@ -711,7 +714,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
711
714
|
}
|
|
712
715
|
|
|
713
716
|
const body = buildGitHubReplyBody(text, parent);
|
|
714
|
-
|
|
717
|
+
adapter
|
|
715
718
|
.createComment(issueNumber, body)
|
|
716
719
|
.then((result) => {
|
|
717
720
|
if (!result.success) throw new Error(result.error || 'Failed to create GitHub comment');
|
|
@@ -758,8 +761,8 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
758
761
|
}),
|
|
759
762
|
);
|
|
760
763
|
|
|
761
|
-
if (
|
|
762
|
-
|
|
764
|
+
if (adapter.isConfigured() && issueNumber && githubCommentId) {
|
|
765
|
+
adapter.updateComment(githubCommentId, text).then((result) => {
|
|
763
766
|
if (result.success) {
|
|
764
767
|
setThreads((prev) =>
|
|
765
768
|
prev.map((t) => (t.id === threadId ? { ...t, syncStatus: 'synced', syncError: undefined } : t)),
|
|
@@ -787,7 +790,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
787
790
|
issueNumber,
|
|
788
791
|
githubCommentId,
|
|
789
792
|
hasExistingComment: !!existingComment,
|
|
790
|
-
isGitHubConfigured:
|
|
793
|
+
isGitHubConfigured: adapter.isConfigured(),
|
|
791
794
|
});
|
|
792
795
|
|
|
793
796
|
// Remove from local state immediately (optimistic delete)
|
|
@@ -804,10 +807,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
804
807
|
);
|
|
805
808
|
|
|
806
809
|
// Attempt GitHub deletion if applicable
|
|
807
|
-
if (
|
|
810
|
+
if (adapter.isConfigured() && issueNumber && githubCommentId) {
|
|
808
811
|
console.log(`šµ Attempting to delete GitHub comment #${githubCommentId} on issue #${issueNumber}`);
|
|
809
812
|
|
|
810
|
-
|
|
813
|
+
adapter.deleteComment(githubCommentId).then((result) => {
|
|
811
814
|
if (result.success) {
|
|
812
815
|
console.log(`ā
Successfully deleted GitHub comment #${githubCommentId}`);
|
|
813
816
|
setThreads((prev) =>
|
|
@@ -853,7 +856,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
853
856
|
});
|
|
854
857
|
} else {
|
|
855
858
|
console.log(`ā¹ļø GitHub deletion skipped:`, {
|
|
856
|
-
reason: !
|
|
859
|
+
reason: !adapter.isConfigured()
|
|
857
860
|
? 'GitHub not configured'
|
|
858
861
|
: !issueNumber
|
|
859
862
|
? 'No issue number'
|
|
@@ -878,10 +881,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
878
881
|
// Keep the thread selected so the UI can switch to a "Reopen" state (GitHub-like)
|
|
879
882
|
|
|
880
883
|
// Sync close to GitHub
|
|
881
|
-
if (
|
|
884
|
+
if (adapter.isConfigured() && issueNumber) {
|
|
882
885
|
console.log(`šµ Closing GitHub issue #${issueNumber}...`);
|
|
883
886
|
|
|
884
|
-
|
|
887
|
+
adapter.closeIssue(issueNumber).then((result) => {
|
|
885
888
|
if (result.success) {
|
|
886
889
|
console.log(`ā
Successfully closed GitHub issue #${issueNumber}`);
|
|
887
890
|
setThreads((prev) =>
|
|
@@ -913,10 +916,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
913
916
|
);
|
|
914
917
|
|
|
915
918
|
// Sync reopen to GitHub
|
|
916
|
-
if (
|
|
919
|
+
if (adapter.isConfigured() && issueNumber) {
|
|
917
920
|
console.log(`šµ Reopening GitHub issue #${issueNumber}...`);
|
|
918
921
|
|
|
919
|
-
|
|
922
|
+
adapter.reopenIssue(issueNumber).then((result) => {
|
|
920
923
|
if (result.success) {
|
|
921
924
|
console.log(`ā
Successfully reopened GitHub issue #${issueNumber}`);
|
|
922
925
|
setThreads((prev) =>
|
|
@@ -944,7 +947,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
944
947
|
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
945
948
|
if (selectedThreadId === threadId) setSelectedThreadId(null);
|
|
946
949
|
|
|
947
|
-
if (!
|
|
950
|
+
if (!adapter.isConfigured()) return;
|
|
948
951
|
|
|
949
952
|
if (!issueNumber) {
|
|
950
953
|
// Issue may still be creating; mark so we can close it once we get the number.
|
|
@@ -959,7 +962,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
959
962
|
pendingCloseIssueNumbersRef.current.add(issueNumber);
|
|
960
963
|
writeNumberSet(PENDING_CLOSE_ISSUES_KEY, pendingCloseIssueNumbersRef.current);
|
|
961
964
|
|
|
962
|
-
|
|
965
|
+
adapter.closeIssue(issueNumber).then((result) => {
|
|
963
966
|
if (result.success) {
|
|
964
967
|
pendingCloseIssueNumbersRef.current.delete(issueNumber);
|
|
965
968
|
writeNumberSet(PENDING_CLOSE_ISSUES_KEY, pendingCloseIssueNumbersRef.current);
|
|
@@ -974,7 +977,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
974
977
|
};
|
|
975
978
|
|
|
976
979
|
const retrySync = async () => {
|
|
977
|
-
if (!
|
|
980
|
+
if (!adapter.isConfigured()) return;
|
|
978
981
|
setSyncInFlightCount((c) => c + 1);
|
|
979
982
|
try {
|
|
980
983
|
const current = threadsRef.current;
|
|
@@ -985,7 +988,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
985
988
|
if (t.syncStatus !== 'error' && t.syncStatus !== 'pending' && t.syncStatus !== 'syncing' && t.syncStatus !== 'local') continue;
|
|
986
989
|
|
|
987
990
|
setThreads((prev) => prev.map((x) => (x.id === t.id ? { ...x, syncStatus: 'syncing', syncError: undefined } : x)));
|
|
988
|
-
const created = await
|
|
991
|
+
const created = await adapter.createIssue({
|
|
989
992
|
title: `Feedback: ${t.route}`,
|
|
990
993
|
body: `Thread created from pin${t.elementDescription ? ` on ${t.elementDescription}` : ''}.`,
|
|
991
994
|
route: t.route,
|
|
@@ -1021,7 +1024,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
1021
1024
|
setThreads((prev) => prev.map((x) => (x.id === t.id ? { ...x, syncStatus: 'syncing', syncError: undefined } : x)));
|
|
1022
1025
|
|
|
1023
1026
|
for (const c of localOnly) {
|
|
1024
|
-
const res = await
|
|
1027
|
+
const res = await adapter.createComment(t.issueNumber, c.text);
|
|
1025
1028
|
if (res.success && res.data?.id) {
|
|
1026
1029
|
const newId = res.data.id as number;
|
|
1027
1030
|
setThreads((prev) =>
|