hale-commenting-system 2.1.1 → 2.2.1
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/.claude/settings.local.json +7 -0
- package/.editorconfig +17 -0
- package/.eslintrc.js +75 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
- package/.github/workflows/ci.yaml +51 -0
- package/.prettierignore +1 -0
- package/.prettierrc +4 -0
- package/GITHUB_OAUTH_ENV_TEMPLATE.md +53 -0
- package/LICENSE +21 -0
- package/README.md +92 -21
- package/package.json +74 -50
- package/scripts/README.md +42 -0
- package/scripts/integrate.js +440 -0
- package/src/app/AppLayout/AppLayout.tsx +248 -0
- package/src/app/Comments/Comments.tsx +273 -0
- package/src/app/Dashboard/Dashboard.tsx +10 -0
- package/src/app/NotFound/NotFound.tsx +35 -0
- package/src/app/Settings/General/GeneralSettings.tsx +16 -0
- package/src/app/Settings/Profile/ProfileSettings.tsx +18 -0
- package/src/app/Support/Support.tsx +50 -0
- package/src/app/__snapshots__/app.test.tsx.snap +524 -0
- package/src/app/app.css +11 -0
- package/src/app/app.test.tsx +55 -0
- package/src/app/bgimages/Patternfly-Logo.svg +28 -0
- package/src/app/commenting-system/components/CommentOverlay.tsx +93 -0
- package/src/app/commenting-system/components/CommentPanel.tsx +534 -0
- package/src/app/commenting-system/components/CommentPin.tsx +60 -0
- package/src/app/commenting-system/components/DetailsTab.tsx +516 -0
- package/src/app/commenting-system/components/FloatingWidget.tsx +130 -0
- package/src/app/commenting-system/components/JiraTab.tsx +696 -0
- package/src/app/commenting-system/contexts/CommentContext.tsx +1033 -0
- package/src/app/commenting-system/contexts/GitHubAuthContext.tsx +84 -0
- package/{dist/index.d.ts → src/app/commenting-system/index.ts} +5 -4
- package/src/app/commenting-system/services/githubAdapter.ts +359 -0
- package/src/app/commenting-system/types/index.ts +27 -0
- package/src/app/commenting-system/utils/version.ts +19 -0
- package/src/app/index.tsx +22 -0
- package/src/app/routes.tsx +81 -0
- package/src/app/utils/useDocumentTitle.ts +13 -0
- package/src/favicon.png +0 -0
- package/src/index.html +18 -0
- package/src/index.tsx +25 -0
- package/src/test/setup.ts +33 -0
- package/src/typings.d.ts +12 -0
- package/stylePaths.js +14 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +19 -0
- package/webpack.common.js +139 -0
- package/webpack.dev.js +318 -0
- package/webpack.prod.js +38 -0
- package/bin/detect.d.ts +0 -10
- package/bin/detect.js +0 -134
- package/bin/generators.d.ts +0 -18
- package/bin/generators.js +0 -193
- package/bin/hale-commenting.js +0 -4
- package/bin/index.d.ts +0 -2
- package/bin/index.js +0 -61
- package/bin/onboarding.d.ts +0 -1
- package/bin/onboarding.js +0 -349
- package/bin/postinstall.d.ts +0 -2
- package/bin/postinstall.js +0 -65
- package/bin/validators.d.ts +0 -2
- package/bin/validators.js +0 -66
- package/dist/cli/detect.d.ts +0 -10
- package/dist/cli/detect.js +0 -134
- package/dist/cli/generators.d.ts +0 -18
- package/dist/cli/generators.js +0 -193
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -61
- package/dist/cli/onboarding.d.ts +0 -1
- package/dist/cli/onboarding.js +0 -349
- package/dist/cli/postinstall.d.ts +0 -2
- package/dist/cli/postinstall.js +0 -65
- package/dist/cli/validators.d.ts +0 -2
- package/dist/cli/validators.js +0 -66
- package/dist/components/CommentOverlay.d.ts +0 -2
- package/dist/components/CommentOverlay.js +0 -101
- package/dist/components/CommentPanel.d.ts +0 -6
- package/dist/components/CommentPanel.js +0 -334
- package/dist/components/CommentPin.d.ts +0 -11
- package/dist/components/CommentPin.js +0 -64
- package/dist/components/DetailsTab.d.ts +0 -2
- package/dist/components/DetailsTab.js +0 -380
- package/dist/components/FloatingWidget.d.ts +0 -8
- package/dist/components/FloatingWidget.js +0 -128
- package/dist/components/JiraTab.d.ts +0 -2
- package/dist/components/JiraTab.js +0 -507
- package/dist/contexts/CommentContext.d.ts +0 -30
- package/dist/contexts/CommentContext.js +0 -891
- package/dist/contexts/GitHubAuthContext.d.ts +0 -13
- package/dist/contexts/GitHubAuthContext.js +0 -96
- package/dist/index.js +0 -27
- package/dist/services/githubAdapter.d.ts +0 -56
- package/dist/services/githubAdapter.js +0 -321
- package/dist/types/index.d.ts +0 -25
- package/dist/types/index.js +0 -2
- package/dist/utils/version.d.ts +0 -1
- package/dist/utils/version.js +0 -23
- package/templates/webpack-middleware.js +0 -226
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { GitHubUser, clearGitHubAuth, getStoredUser, storeGitHubAuth } from '../services/githubAdapter';
|
|
3
|
+
|
|
4
|
+
interface GitHubAuthContextType {
|
|
5
|
+
user: GitHubUser | null;
|
|
6
|
+
isAuthenticated: boolean;
|
|
7
|
+
login: () => void;
|
|
8
|
+
logout: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const GitHubAuthContext = React.createContext<GitHubAuthContextType | undefined>(undefined);
|
|
12
|
+
|
|
13
|
+
export const GitHubAuthProvider: React.FunctionComponent<{ children: React.ReactNode }> = ({ children }) => {
|
|
14
|
+
const [user, setUser] = React.useState<GitHubUser | null>(null);
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
const stored = getStoredUser();
|
|
18
|
+
if (stored) setUser(stored);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
// Handle local dev OAuth callback via hash: /#/auth-callback?token=...&login=...&avatar=...
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
const hash = window.location.hash;
|
|
24
|
+
if (!hash.includes('#/auth-callback')) return;
|
|
25
|
+
|
|
26
|
+
const query = hash.split('?')[1] || '';
|
|
27
|
+
const params = new URLSearchParams(query);
|
|
28
|
+
const token = params.get('token');
|
|
29
|
+
const login = params.get('login');
|
|
30
|
+
const avatar = params.get('avatar');
|
|
31
|
+
|
|
32
|
+
if (token && login && avatar) {
|
|
33
|
+
const decodedUser = { login, avatar: decodeURIComponent(avatar) };
|
|
34
|
+
storeGitHubAuth(token, decodedUser);
|
|
35
|
+
setUser(decodedUser);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// remove hash and return to home
|
|
39
|
+
window.location.hash = '/';
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const login = () => {
|
|
43
|
+
const clientId = process.env.VITE_GITHUB_CLIENT_ID;
|
|
44
|
+
if (!clientId) {
|
|
45
|
+
// eslint-disable-next-line no-alert
|
|
46
|
+
alert('GitHub login is not configured (missing VITE_GITHUB_CLIENT_ID).');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const redirectUri = `${window.location.origin}/api/github-oauth-callback`;
|
|
51
|
+
// Use 'repo' scope for full access (works with both public and private repos)
|
|
52
|
+
// Use 'public_repo' only if you only need public repo access
|
|
53
|
+
const scope = 'repo';
|
|
54
|
+
|
|
55
|
+
const url =
|
|
56
|
+
`https://github.com/login/oauth/authorize?client_id=${encodeURIComponent(clientId)}` +
|
|
57
|
+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
58
|
+
`&scope=${encodeURIComponent(scope)}`;
|
|
59
|
+
|
|
60
|
+
console.log('🔑 GitHub OAuth URL:', url);
|
|
61
|
+
console.log('📋 Scope requested:', scope);
|
|
62
|
+
|
|
63
|
+
window.location.href = url;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const logout = () => {
|
|
67
|
+
clearGitHubAuth();
|
|
68
|
+
setUser(null);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<GitHubAuthContext.Provider value={{ user, isAuthenticated: !!user, login, logout }}>
|
|
73
|
+
{children}
|
|
74
|
+
</GitHubAuthContext.Provider>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const useGitHubAuth = (): GitHubAuthContextType => {
|
|
79
|
+
const ctx = React.useContext(GitHubAuthContext);
|
|
80
|
+
if (!ctx) throw new Error('useGitHubAuth must be used within a GitHubAuthProvider');
|
|
81
|
+
return ctx;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
// Contexts
|
|
1
2
|
export { CommentProvider, useComments } from './contexts/CommentContext';
|
|
2
3
|
export { GitHubAuthProvider, useGitHubAuth } from './contexts/GitHubAuthContext';
|
|
4
|
+
|
|
5
|
+
// Components
|
|
3
6
|
export { CommentOverlay } from './components/CommentOverlay';
|
|
4
7
|
export { CommentPin } from './components/CommentPin';
|
|
5
8
|
export { CommentPanel } from './components/CommentPanel';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export { FloatingWidget } from './components/FloatingWidget';
|
|
9
|
-
export { githubAdapter, isGitHubConfigured } from './services/githubAdapter';
|
|
9
|
+
|
|
10
|
+
// Types
|
|
10
11
|
export type { Comment, Thread, SyncStatus, ThreadStatus } from './types';
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
export interface GitHubUser {
|
|
2
|
+
login: string;
|
|
3
|
+
avatar: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const GITHUB_TOKEN_STORAGE_KEY = 'github_access_token';
|
|
7
|
+
export const GITHUB_USER_STORAGE_KEY = 'github_user';
|
|
8
|
+
|
|
9
|
+
export const storeGitHubAuth = (token: string, user: GitHubUser) => {
|
|
10
|
+
localStorage.setItem(GITHUB_TOKEN_STORAGE_KEY, token);
|
|
11
|
+
localStorage.setItem(GITHUB_USER_STORAGE_KEY, JSON.stringify(user));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const clearGitHubAuth = () => {
|
|
15
|
+
localStorage.removeItem(GITHUB_TOKEN_STORAGE_KEY);
|
|
16
|
+
localStorage.removeItem(GITHUB_USER_STORAGE_KEY);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getStoredToken = (): string | null => {
|
|
20
|
+
return localStorage.getItem(GITHUB_TOKEN_STORAGE_KEY);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const getStoredUser = (): GitHubUser | null => {
|
|
24
|
+
const raw = localStorage.getItem(GITHUB_USER_STORAGE_KEY);
|
|
25
|
+
if (!raw) return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(raw) as GitHubUser;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const isGitHubConfigured = (): boolean => {
|
|
34
|
+
return Boolean(getStoredToken() && process.env.VITE_GITHUB_OWNER && process.env.VITE_GITHUB_REPO);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const diagnoseGitHubSetup = () => {
|
|
38
|
+
const token = getStoredToken();
|
|
39
|
+
const user = getStoredUser();
|
|
40
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
41
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
42
|
+
|
|
43
|
+
console.log('🔍 GitHub Configuration Diagnostic:');
|
|
44
|
+
console.log(' Token:', token ? `Present (${token.substring(0, 10)}...)` : 'Missing ❌');
|
|
45
|
+
console.log(' User:', user ? `${user.login}` : 'Not logged in ❌');
|
|
46
|
+
console.log(' Owner:', owner || 'Missing ❌');
|
|
47
|
+
console.log(' Repo:', repo || 'Missing ❌');
|
|
48
|
+
console.log(' Full repo path:', owner && repo ? `${owner}/${repo}` : 'Incomplete ❌');
|
|
49
|
+
console.log('\n💡 To fix 403 Forbidden error:');
|
|
50
|
+
console.log(' 1. Make sure you have write access to the repository');
|
|
51
|
+
console.log(' 2. Check that issues are enabled on the repository');
|
|
52
|
+
console.log(' 3. Re-authenticate to get a fresh token with correct scopes');
|
|
53
|
+
console.log(' 4. Token needs "repo" scope for private repos or "public_repo" for public repos');
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
hasToken: !!token,
|
|
57
|
+
hasUser: !!user,
|
|
58
|
+
hasOwner: !!owner,
|
|
59
|
+
hasRepo: !!repo,
|
|
60
|
+
isComplete: !!(token && user && owner && repo)
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface GitHubResult<T = any> {
|
|
65
|
+
success: boolean;
|
|
66
|
+
data?: T;
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function githubProxyRequest(method: string, endpoint: string, data?: any): Promise<any> {
|
|
71
|
+
const token = getStoredToken();
|
|
72
|
+
if (!token) {
|
|
73
|
+
throw new Error('Not authenticated with GitHub');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`🔵 GitHub API Request:`, { method, endpoint, hasData: !!data });
|
|
77
|
+
|
|
78
|
+
const resp = await fetch('/api/github-api', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({ token, method, endpoint, data }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const payload = await resp.json();
|
|
85
|
+
|
|
86
|
+
console.log(`🔵 GitHub API Response:`, {
|
|
87
|
+
status: resp.status,
|
|
88
|
+
ok: resp.ok,
|
|
89
|
+
payload
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!resp.ok) {
|
|
93
|
+
const message = (payload && (payload.message || payload.error)) || `GitHub API error (${resp.status})`;
|
|
94
|
+
|
|
95
|
+
if (resp.status === 403) {
|
|
96
|
+
console.error(`❌ 403 Forbidden - Possible causes:
|
|
97
|
+
1. Token doesn't have 'repo' or 'public_repo' scope
|
|
98
|
+
2. You don't have write access to the repository
|
|
99
|
+
3. Issues are disabled on the repository
|
|
100
|
+
4. Token has expired or been revoked
|
|
101
|
+
|
|
102
|
+
Current config:
|
|
103
|
+
- Owner: ${process.env.VITE_GITHUB_OWNER}
|
|
104
|
+
- Repo: ${process.env.VITE_GITHUB_REPO}
|
|
105
|
+
- Endpoint: ${endpoint}
|
|
106
|
+
`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error(message);
|
|
110
|
+
}
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const encodePath = (path: string): string => {
|
|
115
|
+
return path
|
|
116
|
+
.split('/')
|
|
117
|
+
.map((seg) => encodeURIComponent(seg))
|
|
118
|
+
.join('/');
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const base64EncodeUtf8 = (input: string): string => {
|
|
122
|
+
// btoa expects latin1; convert safely for utf-8
|
|
123
|
+
return btoa(unescape(encodeURIComponent(input)));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const base64DecodeUtf8 = (input: string): string => {
|
|
127
|
+
return decodeURIComponent(escape(atob(input)));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const getLabelNames = (issue: any): string[] => {
|
|
131
|
+
const labels = issue?.labels;
|
|
132
|
+
if (!Array.isArray(labels)) return [];
|
|
133
|
+
return labels
|
|
134
|
+
.map((l: any) => (typeof l === 'string' ? l : l?.name))
|
|
135
|
+
.filter((n: any) => typeof n === 'string');
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const issueHasAnyVersion = (issue: any): boolean => {
|
|
139
|
+
const labelNames = getLabelNames(issue);
|
|
140
|
+
if (labelNames.some((n) => n.startsWith('version:'))) return true;
|
|
141
|
+
const body: string = issue?.body || '';
|
|
142
|
+
return body.includes('Version:');
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const githubAdapter = {
|
|
146
|
+
async createIssue(params: {
|
|
147
|
+
title: string;
|
|
148
|
+
body: string;
|
|
149
|
+
route: string;
|
|
150
|
+
xPercent: number;
|
|
151
|
+
yPercent: number;
|
|
152
|
+
version?: string;
|
|
153
|
+
}): Promise<GitHubResult<{ number: number; html_url: string }>> {
|
|
154
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
155
|
+
|
|
156
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
157
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const metadata = [
|
|
161
|
+
`- Route: \`${params.route}\``,
|
|
162
|
+
params.version ? `- Version: \`${params.version}\`` : null,
|
|
163
|
+
`- Coordinates: \`(${params.xPercent.toFixed(1)}%, ${params.yPercent.toFixed(1)}%)\``,
|
|
164
|
+
]
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
.join('\n');
|
|
167
|
+
|
|
168
|
+
const issueBody = {
|
|
169
|
+
title: params.title,
|
|
170
|
+
body: `${params.body}\n\n---\n**Metadata:**\n${metadata}`,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const data = await githubProxyRequest('POST', `/repos/${owner}/${repo}/issues`, issueBody);
|
|
174
|
+
|
|
175
|
+
// Add helpful labels (non-fatal if it fails)
|
|
176
|
+
try {
|
|
177
|
+
const labels: string[] = [
|
|
178
|
+
'hale-comment',
|
|
179
|
+
`route:${params.route}`,
|
|
180
|
+
`coords:${Math.round(params.xPercent)},${Math.round(params.yPercent)}`,
|
|
181
|
+
];
|
|
182
|
+
if (params.version) labels.push(`version:${params.version}`);
|
|
183
|
+
await githubProxyRequest('POST', `/repos/${owner}/${repo}/issues/${data.number}/labels`, { labels });
|
|
184
|
+
} catch {
|
|
185
|
+
// ignore label failures
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { success: true, data };
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
return { success: false, error: e?.message || 'Failed to create issue' };
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async createComment(issueNumber: number, body: string): Promise<GitHubResult> {
|
|
195
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
196
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
197
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const data = await githubProxyRequest('POST', `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { body });
|
|
201
|
+
return { success: true, data };
|
|
202
|
+
} catch (e: any) {
|
|
203
|
+
return { success: false, error: e?.message || 'Failed to create comment' };
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async fetchIssuesForRoute(route: string): Promise<GitHubResult<any[]>> {
|
|
208
|
+
return githubAdapter.fetchIssuesForRouteAndVersion(route);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
async fetchIssuesForRouteAndVersion(route: string, version?: string): Promise<GitHubResult<any[]>> {
|
|
212
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
213
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
214
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
215
|
+
try {
|
|
216
|
+
const data = await githubProxyRequest(
|
|
217
|
+
'GET',
|
|
218
|
+
`/repos/${owner}/${repo}/issues?state=all&per_page=100`,
|
|
219
|
+
);
|
|
220
|
+
// Filter by metadata OR labels (route:${route}), and optionally by version
|
|
221
|
+
const filtered = (Array.isArray(data) ? data : [])
|
|
222
|
+
.filter((issue: any) => {
|
|
223
|
+
const body: string = issue?.body || '';
|
|
224
|
+
const labels = getLabelNames(issue);
|
|
225
|
+
const bodyMatch = body.includes(`Route: \`${route}\``);
|
|
226
|
+
const labelMatch = labels.includes(`route:${route}`);
|
|
227
|
+
return bodyMatch || labelMatch;
|
|
228
|
+
})
|
|
229
|
+
.filter((issue: any) => {
|
|
230
|
+
if (!version) return true;
|
|
231
|
+
|
|
232
|
+
const labels = getLabelNames(issue);
|
|
233
|
+
const body: string = issue?.body || '';
|
|
234
|
+
const versionLabelMatch = labels.includes(`version:${version}`);
|
|
235
|
+
const bodyVersionMatch = body.includes(`Version: \`${version}\``);
|
|
236
|
+
|
|
237
|
+
// Back-compat: if an issue has no version metadata at all, treat it as default "1"
|
|
238
|
+
if (!issueHasAnyVersion(issue) && version === '1') return true;
|
|
239
|
+
|
|
240
|
+
return versionLabelMatch || bodyVersionMatch;
|
|
241
|
+
});
|
|
242
|
+
return { success: true, data: filtered };
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
return { success: false, error: e?.message || 'Failed to fetch issues' };
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
async fetchIssueComments(issueNumber: number): Promise<GitHubResult<any[]>> {
|
|
249
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
250
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
251
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
252
|
+
try {
|
|
253
|
+
const data = await githubProxyRequest(
|
|
254
|
+
'GET',
|
|
255
|
+
`/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100`,
|
|
256
|
+
);
|
|
257
|
+
return { success: true, data };
|
|
258
|
+
} catch (e: any) {
|
|
259
|
+
return { success: false, error: e?.message || 'Failed to fetch issue comments' };
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
async updateComment(commentId: number, body: string): Promise<GitHubResult> {
|
|
264
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
265
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
266
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
267
|
+
try {
|
|
268
|
+
const data = await githubProxyRequest('PATCH', `/repos/${owner}/${repo}/issues/comments/${commentId}`, { body });
|
|
269
|
+
return { success: true, data };
|
|
270
|
+
} catch (e: any) {
|
|
271
|
+
return { success: false, error: e?.message || 'Failed to update comment' };
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
async deleteComment(commentId: number): Promise<GitHubResult> {
|
|
276
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
277
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
278
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
279
|
+
try {
|
|
280
|
+
await githubProxyRequest('DELETE', `/repos/${owner}/${repo}/issues/comments/${commentId}`);
|
|
281
|
+
return { success: true, data: {} };
|
|
282
|
+
} catch (e: any) {
|
|
283
|
+
return { success: false, error: e?.message || 'Failed to delete comment' };
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async closeIssue(issueNumber: number): Promise<GitHubResult> {
|
|
288
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
289
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
290
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
291
|
+
try {
|
|
292
|
+
const data = await githubProxyRequest('PATCH', `/repos/${owner}/${repo}/issues/${issueNumber}`, { state: 'closed' });
|
|
293
|
+
return { success: true, data };
|
|
294
|
+
} catch (e: any) {
|
|
295
|
+
return { success: false, error: e?.message || 'Failed to close issue' };
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async reopenIssue(issueNumber: number): Promise<GitHubResult> {
|
|
300
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
301
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
302
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
303
|
+
try {
|
|
304
|
+
const data = await githubProxyRequest('PATCH', `/repos/${owner}/${repo}/issues/${issueNumber}`, { state: 'open' });
|
|
305
|
+
return { success: true, data };
|
|
306
|
+
} catch (e: any) {
|
|
307
|
+
return { success: false, error: e?.message || 'Failed to reopen issue' };
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
async getRepoFile(path: string): Promise<GitHubResult<{ text: string; sha: string } | null>> {
|
|
312
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
313
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
314
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
315
|
+
try {
|
|
316
|
+
const data = await githubProxyRequest('GET', `/repos/${owner}/${repo}/contents/${encodePath(path)}`);
|
|
317
|
+
const content = typeof data?.content === 'string' ? data.content.replace(/\n/g, '') : '';
|
|
318
|
+
const sha = data?.sha as string | undefined;
|
|
319
|
+
if (!content || !sha) return { success: true, data: null };
|
|
320
|
+
const text = base64DecodeUtf8(content);
|
|
321
|
+
return { success: true, data: { text, sha } };
|
|
322
|
+
} catch (e: any) {
|
|
323
|
+
// If file doesn't exist yet, treat as empty
|
|
324
|
+
if (String(e?.message || '').toLowerCase().includes('not found')) {
|
|
325
|
+
return { success: true, data: null };
|
|
326
|
+
}
|
|
327
|
+
return { success: false, error: e?.message || 'Failed to read repo file' };
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
async putRepoFile(params: {
|
|
332
|
+
path: string;
|
|
333
|
+
text: string;
|
|
334
|
+
message: string;
|
|
335
|
+
sha?: string;
|
|
336
|
+
}): Promise<GitHubResult<{ sha: string }>> {
|
|
337
|
+
if (!isGitHubConfigured()) return { success: false, error: 'Please sign in with GitHub' };
|
|
338
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
339
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
340
|
+
try {
|
|
341
|
+
const payload: any = {
|
|
342
|
+
message: params.message,
|
|
343
|
+
content: base64EncodeUtf8(params.text),
|
|
344
|
+
};
|
|
345
|
+
if (params.sha) payload.sha = params.sha;
|
|
346
|
+
const data = await githubProxyRequest(
|
|
347
|
+
'PUT',
|
|
348
|
+
`/repos/${owner}/${repo}/contents/${encodePath(params.path)}`,
|
|
349
|
+
payload,
|
|
350
|
+
);
|
|
351
|
+
const newSha = data?.content?.sha as string | undefined;
|
|
352
|
+
return { success: true, data: { sha: newSha || params.sha || '' } };
|
|
353
|
+
} catch (e: any) {
|
|
354
|
+
return { success: false, error: e?.message || 'Failed to write repo file' };
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Comment {
|
|
2
|
+
id: string;
|
|
3
|
+
author?: string;
|
|
4
|
+
text: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
githubCommentId?: number;
|
|
7
|
+
parentCommentId?: string; // local id of parent comment
|
|
8
|
+
parentGitHubCommentId?: number; // GitHub comment id of parent (if known)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SyncStatus = 'synced' | 'local' | 'pending' | 'syncing' | 'error';
|
|
12
|
+
export type ThreadStatus = 'open' | 'closed';
|
|
13
|
+
|
|
14
|
+
export interface Thread {
|
|
15
|
+
id: string;
|
|
16
|
+
xPercent: number; // Percentage from left (0-100)
|
|
17
|
+
yPercent: number; // Percentage from top (0-100)
|
|
18
|
+
route: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
comments: Comment[];
|
|
21
|
+
issueNumber?: number;
|
|
22
|
+
issueUrl?: string;
|
|
23
|
+
provider?: 'github';
|
|
24
|
+
syncStatus?: SyncStatus;
|
|
25
|
+
syncError?: string;
|
|
26
|
+
status?: ThreadStatus; // open or closed (mirrors GitHub issue state)
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const getVersionFromPathOrQuery = (pathname: string, search: string): string | undefined => {
|
|
2
|
+
try {
|
|
3
|
+
const params = new URLSearchParams(search || '');
|
|
4
|
+
const fromQuery = params.get('version') || params.get('v');
|
|
5
|
+
if (fromQuery && String(fromQuery).trim()) return String(fromQuery).trim();
|
|
6
|
+
|
|
7
|
+
// Common pattern: /v3/... or /version/3/...
|
|
8
|
+
const m1 = pathname.match(/^\/v(\d+)(?:\/|$)/i);
|
|
9
|
+
if (m1?.[1]) return m1[1];
|
|
10
|
+
|
|
11
|
+
const m2 = pathname.match(/\/version\/(\d+)(?:\/|$)/i);
|
|
12
|
+
if (m2?.[1]) return m2[1];
|
|
13
|
+
} catch {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import '@patternfly/react-core/dist/styles/base.css';
|
|
3
|
+
import { BrowserRouter as Router } from 'react-router-dom';
|
|
4
|
+
import { AppLayout } from '@app/AppLayout/AppLayout';
|
|
5
|
+
import { AppRoutes } from '@app/routes';
|
|
6
|
+
import { CommentProvider } from '@app/commenting-system';
|
|
7
|
+
import { GitHubAuthProvider } from '@app/commenting-system';
|
|
8
|
+
import '@app/app.css';
|
|
9
|
+
|
|
10
|
+
const App: React.FunctionComponent = () => (
|
|
11
|
+
<Router>
|
|
12
|
+
<GitHubAuthProvider>
|
|
13
|
+
<CommentProvider>
|
|
14
|
+
<AppLayout>
|
|
15
|
+
<AppRoutes />
|
|
16
|
+
</AppLayout>
|
|
17
|
+
</CommentProvider>
|
|
18
|
+
</GitHubAuthProvider>
|
|
19
|
+
</Router>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export default App;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Route, Routes } from 'react-router-dom';
|
|
3
|
+
import { Dashboard } from '@app/Dashboard/Dashboard';
|
|
4
|
+
import { Support } from '@app/Support/Support';
|
|
5
|
+
import { GeneralSettings } from '@app/Settings/General/GeneralSettings';
|
|
6
|
+
import { ProfileSettings } from '@app/Settings/Profile/ProfileSettings';
|
|
7
|
+
import { NotFound } from '@app/NotFound/NotFound';
|
|
8
|
+
|
|
9
|
+
export interface IAppRoute {
|
|
10
|
+
label?: string; // Excluding the label will exclude the route from the nav sidebar in AppLayout
|
|
11
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
12
|
+
element: React.ReactElement;
|
|
13
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
14
|
+
exact?: boolean;
|
|
15
|
+
path: string;
|
|
16
|
+
title: string;
|
|
17
|
+
routes?: undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IAppRouteGroup {
|
|
21
|
+
label: string;
|
|
22
|
+
routes: IAppRoute[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type AppRouteConfig = IAppRoute | IAppRouteGroup;
|
|
26
|
+
|
|
27
|
+
const routes: AppRouteConfig[] = [
|
|
28
|
+
{
|
|
29
|
+
element: <Dashboard />,
|
|
30
|
+
exact: true,
|
|
31
|
+
label: 'Dashboard',
|
|
32
|
+
path: '/',
|
|
33
|
+
title: 'Hale Commenting System | Main Dashboard',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
element: <Support />,
|
|
37
|
+
exact: true,
|
|
38
|
+
label: 'Support',
|
|
39
|
+
path: '/support',
|
|
40
|
+
title: 'Hale Commenting System | Support Page',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: 'Settings',
|
|
44
|
+
routes: [
|
|
45
|
+
{
|
|
46
|
+
element: <GeneralSettings />,
|
|
47
|
+
exact: true,
|
|
48
|
+
label: 'General',
|
|
49
|
+
path: '/settings/general',
|
|
50
|
+
title: 'Hale Commenting System | General Settings',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
element: <ProfileSettings />,
|
|
54
|
+
exact: true,
|
|
55
|
+
label: 'Profile',
|
|
56
|
+
path: '/settings/profile',
|
|
57
|
+
title: 'Hale Commenting System | Profile Settings',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'Comments',
|
|
63
|
+
routes: [],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const flattenedRoutes: IAppRoute[] = routes.reduce(
|
|
68
|
+
(flattened, route) => [...flattened, ...(route.routes ? route.routes : [route])],
|
|
69
|
+
[] as IAppRoute[],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const AppRoutes = (): React.ReactElement => (
|
|
73
|
+
<Routes>
|
|
74
|
+
{flattenedRoutes.map(({ path, element }, idx) => (
|
|
75
|
+
<Route path={path} element={element} key={idx} />
|
|
76
|
+
))}
|
|
77
|
+
<Route element={<NotFound />} />
|
|
78
|
+
</Routes>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export { AppRoutes, routes };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
// a custom hook for setting the page title
|
|
4
|
+
export function useDocumentTitle(title: string) {
|
|
5
|
+
React.useEffect(() => {
|
|
6
|
+
const originalTitle = document.title;
|
|
7
|
+
document.title = title;
|
|
8
|
+
|
|
9
|
+
return () => {
|
|
10
|
+
document.title = originalTitle;
|
|
11
|
+
};
|
|
12
|
+
}, [title]);
|
|
13
|
+
}
|
package/src/favicon.png
ADDED
|
Binary file
|
package/src/index.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Hale Commenting System</title>
|
|
7
|
+
<meta id="appName" name="application-name" content="Hale Commenting System">
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="/images/favicon.png">
|
|
10
|
+
<base href="/">
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<noscript>Enabling JavaScript is required to run this app.</noscript>
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
</body>
|
|
17
|
+
|
|
18
|
+
</html>
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import App from '@app/index';
|
|
4
|
+
|
|
5
|
+
if (process.env.NODE_ENV !== "production") {
|
|
6
|
+
const config = {
|
|
7
|
+
rules: [
|
|
8
|
+
{
|
|
9
|
+
id: 'color-contrast',
|
|
10
|
+
enabled: false
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
};
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
const axe = require("react-axe");
|
|
16
|
+
axe(React, ReactDOM, 1000, config);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const root = ReactDOM.createRoot(document.getElementById("root") as Element);
|
|
20
|
+
|
|
21
|
+
root.render(
|
|
22
|
+
<React.StrictMode>
|
|
23
|
+
<App />
|
|
24
|
+
</React.StrictMode>
|
|
25
|
+
)
|