hale-commenting-system 2.2.97 → 3.0.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 +9 -8
- package/scripts/integrate.js +64 -430
- package/src/app/commenting-system/components/CommentOverlay.tsx +4 -3
- package/src/app/commenting-system/components/CommentPanel.tsx +11 -64
- package/src/app/commenting-system/components/FloatingWidget.tsx +105 -39
- package/src/app/commenting-system/contexts/CommentContext.tsx +11 -4
- package/src/app/commenting-system/types/index.ts +1 -0
- package/src/app/routes.tsx +0 -4
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hale-commenting-system",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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
|
"repository": "https://github.com/patternfly/patternfly-react-seed.git",
|
|
6
6
|
"homepage": "https://www.npmjs.com/package/hale-commenting-system",
|
|
@@ -71,19 +71,20 @@
|
|
|
71
71
|
"webpack-dev-server": "^5.2.1",
|
|
72
72
|
"webpack-merge": "^6.0.1"
|
|
73
73
|
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@patternfly/react-core": "^6.0.0",
|
|
76
|
+
"@patternfly/react-icons": "^6.0.0",
|
|
77
|
+
"react": "^18.0.0",
|
|
78
|
+
"react-dom": "^18.0.0",
|
|
79
|
+
"react-router-dom": "^7.0.0"
|
|
80
|
+
},
|
|
74
81
|
"dependencies": {
|
|
75
82
|
"@babel/generator": "^7.23.0",
|
|
76
83
|
"@babel/parser": "^7.23.0",
|
|
77
84
|
"@babel/traverse": "^7.23.0",
|
|
78
85
|
"@babel/types": "^7.23.0",
|
|
79
|
-
"@patternfly/react-core": "^6.4.0",
|
|
80
|
-
"@patternfly/react-icons": "^6.4.0",
|
|
81
|
-
"@patternfly/react-styles": "^6.4.0",
|
|
82
86
|
"inquirer": "^8.2.6",
|
|
83
|
-
"node-fetch": "^2.7.0"
|
|
84
|
-
"react": "^18",
|
|
85
|
-
"react-dom": "^18",
|
|
86
|
-
"sirv-cli": "^3.0.0"
|
|
87
|
+
"node-fetch": "^2.7.0"
|
|
87
88
|
},
|
|
88
89
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
89
90
|
}
|
package/scripts/integrate.js
CHANGED
|
@@ -512,41 +512,6 @@ JIRA_API_TOKEN=
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
function createCommentsComponent() {
|
|
516
|
-
const cwd = process.cwd();
|
|
517
|
-
const commentsDir = path.join(cwd, 'src', 'app', 'Comments');
|
|
518
|
-
const commentsFile = path.join(commentsDir, 'Comments.tsx');
|
|
519
|
-
|
|
520
|
-
// Check if already exists
|
|
521
|
-
if (fs.existsSync(commentsFile)) {
|
|
522
|
-
return; // Already exists, skip
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Create directory if it doesn't exist
|
|
526
|
-
if (!fs.existsSync(commentsDir)) {
|
|
527
|
-
fs.mkdirSync(commentsDir, { recursive: true });
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Read the Comments component from the package and modify the import
|
|
531
|
-
// The file is in the package at src/app/Comments/Comments.tsx
|
|
532
|
-
const scriptDir = __dirname || path.dirname(require.resolve('./integrate.js'));
|
|
533
|
-
const packageCommentsFile = path.join(scriptDir, '..', 'src', 'app', 'Comments', 'Comments.tsx');
|
|
534
|
-
|
|
535
|
-
let commentsComponentContent;
|
|
536
|
-
if (fs.existsSync(packageCommentsFile)) {
|
|
537
|
-
// Read from package and replace import path
|
|
538
|
-
commentsComponentContent = fs.readFileSync(packageCommentsFile, 'utf8')
|
|
539
|
-
.replace(/from ['"]@app\/commenting-system['"]/g, "from 'hale-commenting-system'");
|
|
540
|
-
} else {
|
|
541
|
-
// Fallback: create a minimal version (shouldn't happen if package is properly built)
|
|
542
|
-
console.log(' ⚠️ Comments component not found in package, skipping creation');
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
fs.writeFileSync(commentsFile, commentsComponentContent);
|
|
547
|
-
console.log(' ✅ Created Comments component');
|
|
548
|
-
}
|
|
549
|
-
|
|
550
515
|
function integrateWebpackMiddleware() {
|
|
551
516
|
const cwd = process.cwd();
|
|
552
517
|
const webpackDevPath = path.join(cwd, 'webpack.dev.js');
|
|
@@ -1035,19 +1000,13 @@ function modifyIndexTsx(filePath) {
|
|
|
1035
1000
|
}
|
|
1036
1001
|
}
|
|
1037
1002
|
|
|
1038
|
-
function
|
|
1003
|
+
function modifyAppLayoutTsx(filePath) {
|
|
1039
1004
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
1040
|
-
|
|
1041
|
-
// Check if
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if (content.includes("path: '/comments'") || content.includes('path: "/comments"')) {
|
|
1046
|
-
console.log(' ⚠️ Already integrated (Comments route found)');
|
|
1047
|
-
return false;
|
|
1048
|
-
}
|
|
1049
|
-
// If Comments group exists but has no routes, continue to add them
|
|
1050
|
-
console.log(' ℹ️ Comments group exists but has no routes, adding route...');
|
|
1005
|
+
|
|
1006
|
+
// Check if already integrated
|
|
1007
|
+
if (content.includes('CommentPanel') && content.includes('CommentOverlay')) {
|
|
1008
|
+
console.log(' ⚠️ Already integrated (CommentPanel and CommentOverlay found)');
|
|
1009
|
+
return false;
|
|
1051
1010
|
}
|
|
1052
1011
|
|
|
1053
1012
|
try {
|
|
@@ -1056,392 +1015,87 @@ function modifyRoutesTsx(filePath) {
|
|
|
1056
1015
|
plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties']
|
|
1057
1016
|
});
|
|
1058
1017
|
|
|
1059
|
-
let
|
|
1018
|
+
let hasCommentingImport = false;
|
|
1060
1019
|
|
|
1020
|
+
// Check if commenting system imports exist
|
|
1061
1021
|
traverse(ast, {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1022
|
+
ImportDeclaration(path) {
|
|
1023
|
+
const source = path.node.source.value;
|
|
1024
|
+
if (source.includes('hale-commenting-system')) {
|
|
1025
|
+
hasCommentingImport = true;
|
|
1065
1026
|
}
|
|
1066
1027
|
}
|
|
1067
1028
|
});
|
|
1068
1029
|
|
|
1069
|
-
if
|
|
1070
|
-
|
|
1071
|
-
let
|
|
1072
|
-
let
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
const source = path.node.source.value;
|
|
1077
|
-
if (source.includes('Comments') || source.includes('@app/Comments')) {
|
|
1078
|
-
hasCommentsImport = true;
|
|
1079
|
-
// Get the imported name
|
|
1080
|
-
path.node.specifiers.forEach(spec => {
|
|
1081
|
-
if (spec.type === 'ImportSpecifier' && spec.imported.name === 'Comments') {
|
|
1082
|
-
commentsImportName = spec.local.name;
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
// Add Comments import if missing
|
|
1090
|
-
if (!hasCommentsImport) {
|
|
1091
|
-
let lastImportIndex = -1;
|
|
1092
|
-
for (let i = ast.program.body.length - 1; i >= 0; i--) {
|
|
1093
|
-
if (ast.program.body[i].type === 'ImportDeclaration') {
|
|
1094
|
-
lastImportIndex = i;
|
|
1095
|
-
break;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
const importIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0;
|
|
1099
|
-
|
|
1100
|
-
const commentsImport = types.importDeclaration(
|
|
1101
|
-
[types.importSpecifier(types.identifier('Comments'), types.identifier('Comments'))],
|
|
1102
|
-
types.stringLiteral('@app/Comments/Comments')
|
|
1103
|
-
);
|
|
1104
|
-
|
|
1105
|
-
ast.program.body.splice(importIndex, 0, commentsImport);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// Check if Comments route group already exists
|
|
1109
|
-
let existingCommentsGroup = null;
|
|
1110
|
-
let existingCommentsRoutes = null;
|
|
1111
|
-
|
|
1112
|
-
for (const element of routesArray.elements) {
|
|
1113
|
-
if (element.type === 'ObjectExpression') {
|
|
1114
|
-
const labelProp = element.properties.find(
|
|
1115
|
-
prop => prop.key && prop.key.name === 'label' &&
|
|
1116
|
-
prop.value && prop.value.value === 'Comments'
|
|
1117
|
-
);
|
|
1118
|
-
if (labelProp) {
|
|
1119
|
-
existingCommentsGroup = element;
|
|
1120
|
-
const routesProp = element.properties.find(
|
|
1121
|
-
prop => prop.key && prop.key.name === 'routes'
|
|
1122
|
-
);
|
|
1123
|
-
if (routesProp && routesProp.value.type === 'ArrayExpression') {
|
|
1124
|
-
existingCommentsRoutes = routesProp.value;
|
|
1125
|
-
}
|
|
1126
|
-
break;
|
|
1127
|
-
}
|
|
1030
|
+
// Add imports if missing
|
|
1031
|
+
if (!hasCommentingImport) {
|
|
1032
|
+
let lastImportIndex = -1;
|
|
1033
|
+
for (let i = ast.program.body.length - 1; i >= 0; i--) {
|
|
1034
|
+
if (ast.program.body[i].type === 'ImportDeclaration') {
|
|
1035
|
+
lastImportIndex = i;
|
|
1036
|
+
break;
|
|
1128
1037
|
}
|
|
1129
1038
|
}
|
|
1039
|
+
const importIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0;
|
|
1130
1040
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1041
|
+
const commentingImport = types.importDeclaration(
|
|
1042
|
+
[
|
|
1043
|
+
types.importSpecifier(types.identifier('CommentPanel'), types.identifier('CommentPanel')),
|
|
1044
|
+
types.importSpecifier(types.identifier('CommentOverlay'), types.identifier('CommentOverlay'))
|
|
1045
|
+
],
|
|
1046
|
+
types.stringLiteral('hale-commenting-system')
|
|
1136
1047
|
);
|
|
1137
1048
|
|
|
1138
|
-
|
|
1139
|
-
types.objectProperty(types.identifier('element'), commentsRouteElement),
|
|
1140
|
-
types.objectProperty(types.identifier('exact'), types.booleanLiteral(true)),
|
|
1141
|
-
types.objectProperty(types.identifier('label'), types.stringLiteral('View all')),
|
|
1142
|
-
types.objectProperty(types.identifier('path'), types.stringLiteral('/comments')),
|
|
1143
|
-
types.objectProperty(types.identifier('title'), types.stringLiteral('Hale Commenting System | Comments'))
|
|
1144
|
-
]);
|
|
1145
|
-
|
|
1146
|
-
if (existingCommentsGroup && existingCommentsRoutes) {
|
|
1147
|
-
// Add route to existing Comments group
|
|
1148
|
-
existingCommentsRoutes.elements.push(commentsRouteItem);
|
|
1149
|
-
} else {
|
|
1150
|
-
// Create new Comments route group
|
|
1151
|
-
const commentsRoute = types.objectExpression([
|
|
1152
|
-
types.objectProperty(types.identifier('label'), types.stringLiteral('Comments')),
|
|
1153
|
-
types.objectProperty(types.identifier('routes'), types.arrayExpression([commentsRouteItem]))
|
|
1154
|
-
]);
|
|
1155
|
-
routesArray.elements.push(commentsRoute);
|
|
1156
|
-
}
|
|
1049
|
+
ast.program.body.splice(importIndex, 0, commentingImport);
|
|
1157
1050
|
}
|
|
1158
1051
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
// Check if already integrated - look for the comprehensive integration
|
|
1176
|
-
if (content.includes('useComments') && content.includes('useGitHubAuth') &&
|
|
1177
|
-
content.includes('setCommentsEnabled') && content.includes('setFloatingWidgetMode')) {
|
|
1178
|
-
console.log(' ⚠️ Already integrated (full commenting system controls found)');
|
|
1179
|
-
return false;
|
|
1180
|
-
}
|
|
1052
|
+
// Find Page component and wrap its children
|
|
1053
|
+
let pageComponentFound = false;
|
|
1054
|
+
traverse(ast, {
|
|
1055
|
+
JSXElement(path) {
|
|
1056
|
+
const openingElement = path.node.openingElement;
|
|
1057
|
+
if (openingElement.name && openingElement.name.name === 'Page') {
|
|
1058
|
+
pageComponentFound = true;
|
|
1059
|
+
const children = path.node.children;
|
|
1060
|
+
|
|
1061
|
+
// Check if already wrapped
|
|
1062
|
+
if (children.length > 0 &&
|
|
1063
|
+
children.some(child =>
|
|
1064
|
+
child.type === 'JSXElement' &&
|
|
1065
|
+
child.openingElement.name.name === 'CommentPanel')) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1181
1068
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const patternflyMatch = content.match(patternflyImportRegex);
|
|
1188
|
-
|
|
1189
|
-
if (patternflyMatch) {
|
|
1190
|
-
// Find the import statement
|
|
1191
|
-
const importMatch = content.match(/import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-core['"]/);
|
|
1192
|
-
if (importMatch) {
|
|
1193
|
-
const imports = importMatch[1];
|
|
1194
|
-
// Add Switch if not present
|
|
1195
|
-
if (!imports.includes('Switch')) {
|
|
1196
|
-
const newImports = imports.trim() + ',\n Switch';
|
|
1197
|
-
content = content.replace(
|
|
1198
|
-
/import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-core['"]/,
|
|
1199
|
-
`import {${newImports}\n} from '@patternfly/react-core'`
|
|
1069
|
+
// Create CommentOverlay element
|
|
1070
|
+
const commentOverlay = types.jsxElement(
|
|
1071
|
+
types.jsxOpeningElement(types.jsxIdentifier('CommentOverlay'), [], true),
|
|
1072
|
+
null,
|
|
1073
|
+
[]
|
|
1200
1074
|
);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
1075
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const importMatch = content.match(/import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-icons['"]/);
|
|
1211
|
-
if (importMatch) {
|
|
1212
|
-
const imports = importMatch[1];
|
|
1213
|
-
// Add icons if not present
|
|
1214
|
-
let newImports = imports.trim();
|
|
1215
|
-
if (!imports.includes('ExternalLinkAltIcon')) {
|
|
1216
|
-
newImports += ', ExternalLinkAltIcon';
|
|
1217
|
-
}
|
|
1218
|
-
if (!imports.includes('GithubIcon')) {
|
|
1219
|
-
newImports += ', GithubIcon';
|
|
1220
|
-
}
|
|
1221
|
-
if (newImports !== imports.trim()) {
|
|
1222
|
-
content = content.replace(
|
|
1223
|
-
/import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-icons['"]/,
|
|
1224
|
-
`import { ${newImports} } from '@patternfly/react-icons'`
|
|
1076
|
+
// Create CommentPanel wrapping existing children
|
|
1077
|
+
const commentPanel = types.jsxElement(
|
|
1078
|
+
types.jsxOpeningElement(types.jsxIdentifier('CommentPanel'), []),
|
|
1079
|
+
types.jsxClosingElement(types.jsxIdentifier('CommentPanel')),
|
|
1080
|
+
[commentOverlay, ...children]
|
|
1225
1081
|
);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
1082
|
|
|
1230
|
-
|
|
1231
|
-
if (!content.includes('hale-commenting-system')) {
|
|
1232
|
-
// Find where to insert (after other imports)
|
|
1233
|
-
const lastImportMatch = content.match(/import[^;]*;(?=\s*(?:interface|const|export|function))/g);
|
|
1234
|
-
if (lastImportMatch) {
|
|
1235
|
-
const lastImport = lastImportMatch[lastImportMatch.length - 1];
|
|
1236
|
-
const insertPos = content.indexOf(lastImport) + lastImport.length;
|
|
1237
|
-
const commentingImport = `\nimport { CommentOverlay, CommentPanel, useComments, useGitHubAuth } from "hale-commenting-system";`;
|
|
1238
|
-
content = content.slice(0, insertPos) + commentingImport + content.slice(insertPos);
|
|
1239
|
-
}
|
|
1240
|
-
} else {
|
|
1241
|
-
// Update existing import to include all needed items
|
|
1242
|
-
const commentingImportMatch = content.match(/import\s+\{([^}]+)\}\s+from\s+["']hale-commenting-system["']/);
|
|
1243
|
-
if (commentingImportMatch) {
|
|
1244
|
-
const imports = commentingImportMatch[1];
|
|
1245
|
-
let newImports = imports.split(',').map(i => i.trim());
|
|
1246
|
-
|
|
1247
|
-
const needed = ['CommentOverlay', 'CommentPanel', 'useComments', 'useGitHubAuth'];
|
|
1248
|
-
needed.forEach(item => {
|
|
1249
|
-
if (!newImports.includes(item)) {
|
|
1250
|
-
newImports.push(item);
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
content = content.replace(
|
|
1255
|
-
/import\s+\{[^}]+\}\s+from\s+["']hale-commenting-system["']/,
|
|
1256
|
-
`import { ${newImports.join(', ')} } from "hale-commenting-system"`
|
|
1257
|
-
);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// Step 2: Add hooks to the component
|
|
1262
|
-
// Find the AppLayout function/component
|
|
1263
|
-
const componentMatch = content.match(/(const\s+AppLayout[^=]+=\s*\([^)]*\)\s*=>\s*\{)/);
|
|
1264
|
-
if (componentMatch) {
|
|
1265
|
-
const componentStart = content.indexOf(componentMatch[0]);
|
|
1266
|
-
const afterComponentStart = componentStart + componentMatch[0].length;
|
|
1267
|
-
|
|
1268
|
-
// Check if hooks are already added
|
|
1269
|
-
if (!content.includes('const { commentsEnabled, setCommentsEnabled')) {
|
|
1270
|
-
// Find where to insert hooks (after existing useState declarations)
|
|
1271
|
-
const stateMatch = content.slice(afterComponentStart).match(/const\s+\[[^\]]+\]\s*=\s*React\.useState/);
|
|
1272
|
-
let hookInsertPos;
|
|
1273
|
-
|
|
1274
|
-
if (stateMatch) {
|
|
1275
|
-
const statePos = content.indexOf(stateMatch[0], afterComponentStart);
|
|
1276
|
-
const semicolonPos = content.indexOf(';', statePos);
|
|
1277
|
-
hookInsertPos = semicolonPos + 1;
|
|
1278
|
-
} else {
|
|
1279
|
-
hookInsertPos = afterComponentStart;
|
|
1083
|
+
path.node.children = [commentPanel];
|
|
1280
1084
|
}
|
|
1281
|
-
|
|
1282
|
-
const hooks = `
|
|
1283
|
-
const { commentsEnabled, setCommentsEnabled, drawerPinnedOpen, setDrawerPinnedOpen, floatingWidgetMode, setFloatingWidgetMode } = useComments();
|
|
1284
|
-
const { isAuthenticated, user, login, logout } = useGitHubAuth();
|
|
1285
|
-
`;
|
|
1286
|
-
content = content.slice(0, hookInsertPos) + hooks + content.slice(hookInsertPos);
|
|
1287
1085
|
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// Step 3: Add the special renderNavGroup logic
|
|
1291
|
-
// Replace the simple arrow function with a block function that has special Comments handling
|
|
1292
|
-
if (!content.includes("group.label === 'Comments'")) {
|
|
1293
|
-
// Find the renderNavGroup function - handle both arrow expression () => (...) and block () => {...}
|
|
1294
|
-
const arrowFuncMatch = content.match(/const\s+renderNavGroup\s*=\s*\(([^)]+)\)\s*=>\s*\(/);
|
|
1295
|
-
|
|
1296
|
-
if (arrowFuncMatch) {
|
|
1297
|
-
const params = arrowFuncMatch[1];
|
|
1298
|
-
|
|
1299
|
-
// Find the entire function including the closing parenthesis and semicolon
|
|
1300
|
-
const funcStart = content.indexOf(arrowFuncMatch[0]);
|
|
1301
|
-
const afterArrow = funcStart + arrowFuncMatch[0].length;
|
|
1302
|
-
|
|
1303
|
-
// Find matching closing paren and semicolon
|
|
1304
|
-
let depth = 1;
|
|
1305
|
-
let endPos = afterArrow;
|
|
1306
|
-
for (let i = afterArrow; i < content.length; i++) {
|
|
1307
|
-
if (content.charAt(i) === '(') depth++;
|
|
1308
|
-
if (content.charAt(i) === ')') {
|
|
1309
|
-
depth--;
|
|
1310
|
-
if (depth === 0) {
|
|
1311
|
-
// Found the closing paren, now find the semicolon
|
|
1312
|
-
endPos = i + 1;
|
|
1313
|
-
while (endPos < content.length && content.charAt(endPos).trim() === '') endPos++;
|
|
1314
|
-
if (content.charAt(endPos) === ';') endPos++;
|
|
1315
|
-
break;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// Extract the original NavExpandable JSX (we'll use it as the default case)
|
|
1321
|
-
const originalBody = content.slice(funcStart + arrowFuncMatch[0].length - 1, endPos - 1); // Remove opening ( and closing );
|
|
1322
|
-
|
|
1323
|
-
// Create the new block function
|
|
1324
|
-
const newFunction = `const renderNavGroup = (${params}) => {
|
|
1325
|
-
// Special handling for Comments group
|
|
1326
|
-
if (group.label === 'Comments') {
|
|
1327
|
-
return (
|
|
1328
|
-
<NavExpandable
|
|
1329
|
-
key={\`\${group.label}-\${groupIndex}\`}
|
|
1330
|
-
id={\`\${group.label}-\${groupIndex}\`}
|
|
1331
|
-
title="Hale Commenting System"
|
|
1332
|
-
isActive={group.routes.some((route) => route.path === location.pathname)}
|
|
1333
|
-
>
|
|
1334
|
-
<NavItem
|
|
1335
|
-
onClick={(e) => {
|
|
1336
|
-
e.stopPropagation();
|
|
1337
|
-
setFloatingWidgetMode(!floatingWidgetMode);
|
|
1338
|
-
if (!floatingWidgetMode) {
|
|
1339
|
-
setDrawerPinnedOpen(false);
|
|
1340
|
-
}
|
|
1341
|
-
}}
|
|
1342
|
-
style={{ cursor: 'pointer' }}
|
|
1343
|
-
>
|
|
1344
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
1345
|
-
<ExternalLinkAltIcon />
|
|
1346
|
-
<span>{floatingWidgetMode ? 'Close widget' : 'Pop out'}</span>
|
|
1347
|
-
</div>
|
|
1348
|
-
</NavItem>
|
|
1349
|
-
<NavItem>
|
|
1350
|
-
<div
|
|
1351
|
-
data-comment-controls
|
|
1352
|
-
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
|
|
1353
|
-
>
|
|
1354
|
-
<span>Enable Comments</span>
|
|
1355
|
-
<Switch
|
|
1356
|
-
id="comments-enabled-switch"
|
|
1357
|
-
isChecked={commentsEnabled}
|
|
1358
|
-
onChange={(_event, checked) => {
|
|
1359
|
-
setCommentsEnabled(checked);
|
|
1360
|
-
if (checked) {
|
|
1361
|
-
setDrawerPinnedOpen(true);
|
|
1362
|
-
}
|
|
1363
|
-
}}
|
|
1364
|
-
aria-label="Enable or disable comments"
|
|
1365
|
-
/>
|
|
1366
|
-
</div>
|
|
1367
|
-
</NavItem>
|
|
1368
|
-
<NavItem>
|
|
1369
|
-
<div
|
|
1370
|
-
data-comment-controls
|
|
1371
|
-
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
|
|
1372
|
-
>
|
|
1373
|
-
<span>Page info drawer</span>
|
|
1374
|
-
<Switch
|
|
1375
|
-
id="page-info-drawer-switch"
|
|
1376
|
-
isChecked={drawerPinnedOpen}
|
|
1377
|
-
onChange={(_event, checked) => setDrawerPinnedOpen(checked)}
|
|
1378
|
-
aria-label="Pin page info drawer open"
|
|
1379
|
-
/>
|
|
1380
|
-
</div>
|
|
1381
|
-
</NavItem>
|
|
1382
|
-
<NavItem>
|
|
1383
|
-
<div
|
|
1384
|
-
data-comment-controls
|
|
1385
|
-
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
|
|
1386
|
-
>
|
|
1387
|
-
{isAuthenticated ? (
|
|
1388
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1389
|
-
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
|
|
1390
|
-
<GithubIcon />
|
|
1391
|
-
{user?.login ? \`@\${user.login}\` : 'Signed in'}
|
|
1392
|
-
</span>
|
|
1393
|
-
<Button variant="link" isInline onClick={logout}>
|
|
1394
|
-
Sign out
|
|
1395
|
-
</Button>
|
|
1396
|
-
</div>
|
|
1397
|
-
) : (
|
|
1398
|
-
<Button variant="link" isInline icon={<GithubIcon />} onClick={login}>
|
|
1399
|
-
Sign in with GitHub
|
|
1400
|
-
</Button>
|
|
1401
|
-
)}
|
|
1402
|
-
</div>
|
|
1403
|
-
</NavItem>
|
|
1404
|
-
{group.routes.map((route, idx) => route.label && renderNavItem(route, idx))}
|
|
1405
|
-
</NavExpandable>
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// Default handling for other groups
|
|
1410
|
-
return ${originalBody};
|
|
1411
|
-
};`;
|
|
1086
|
+
});
|
|
1412
1087
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1088
|
+
if (!pageComponentFound) {
|
|
1089
|
+
console.error(' ❌ Could not find Page component in AppLayout.tsx');
|
|
1090
|
+
return false;
|
|
1416
1091
|
}
|
|
1417
1092
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
if (pageMatch) {
|
|
1423
|
-
const pageStart = content.indexOf(pageMatch[0]);
|
|
1424
|
-
const pageEnd = content.indexOf('</Page>', pageStart);
|
|
1425
|
-
|
|
1426
|
-
if (pageEnd > pageStart) {
|
|
1427
|
-
// Extract children between Page tags
|
|
1428
|
-
const pageOpenTagEnd = content.indexOf('>', pageStart) + 1;
|
|
1429
|
-
const children = content.slice(pageOpenTagEnd, pageEnd);
|
|
1430
|
-
|
|
1431
|
-
// Wrap with CommentPanel and add CommentOverlay
|
|
1432
|
-
const wrappedChildren = `
|
|
1433
|
-
<CommentPanel>
|
|
1434
|
-
<CommentOverlay />
|
|
1435
|
-
${children.trim()}
|
|
1436
|
-
</CommentPanel>
|
|
1437
|
-
`;
|
|
1438
|
-
|
|
1439
|
-
content = content.slice(0, pageOpenTagEnd) + wrappedChildren + content.slice(pageEnd);
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1093
|
+
const output = generate(ast, {
|
|
1094
|
+
retainLines: false,
|
|
1095
|
+
compact: false
|
|
1096
|
+
}, content);
|
|
1443
1097
|
|
|
1444
|
-
fs.writeFileSync(filePath,
|
|
1098
|
+
fs.writeFileSync(filePath, output.code);
|
|
1445
1099
|
return true;
|
|
1446
1100
|
} catch (error) {
|
|
1447
1101
|
console.error(` ❌ Error modifying ${filePath}:`, error.message);
|
|
@@ -1821,15 +1475,13 @@ async function main() {
|
|
|
1821
1475
|
|
|
1822
1476
|
// Step 5: Integrate into project
|
|
1823
1477
|
console.log('\n🔧 Step 5: Integrating into PatternFly Seed project...\n');
|
|
1824
|
-
|
|
1478
|
+
|
|
1825
1479
|
console.log('This will modify the following files:');
|
|
1826
1480
|
console.log(' • src/app/index.tsx');
|
|
1827
|
-
console.log(' • src/app/routes.tsx');
|
|
1828
1481
|
console.log(' • src/app/AppLayout/AppLayout.tsx');
|
|
1829
1482
|
console.log(' • webpack.dev.js\n');
|
|
1830
1483
|
|
|
1831
1484
|
const indexPath = findFile('index.tsx');
|
|
1832
|
-
const routesPath = findFile('routes.tsx');
|
|
1833
1485
|
const appLayoutPath = findFile('AppLayout/AppLayout.tsx') || findFile('AppLayout.tsx');
|
|
1834
1486
|
|
|
1835
1487
|
if (!indexPath) {
|
|
@@ -1837,11 +1489,6 @@ async function main() {
|
|
|
1837
1489
|
rl.close();
|
|
1838
1490
|
process.exit(1);
|
|
1839
1491
|
}
|
|
1840
|
-
if (!routesPath) {
|
|
1841
|
-
console.error('❌ Could not find src/app/routes.tsx');
|
|
1842
|
-
rl.close();
|
|
1843
|
-
process.exit(1);
|
|
1844
|
-
}
|
|
1845
1492
|
if (!appLayoutPath) {
|
|
1846
1493
|
console.error('❌ Could not find src/app/AppLayout/AppLayout.tsx');
|
|
1847
1494
|
rl.close();
|
|
@@ -1862,19 +1509,6 @@ async function main() {
|
|
|
1862
1509
|
skippedCount++;
|
|
1863
1510
|
}
|
|
1864
1511
|
|
|
1865
|
-
// Create Comments component first (needed for routes)
|
|
1866
|
-
console.log('\n📝 Creating Comments component...');
|
|
1867
|
-
createCommentsComponent();
|
|
1868
|
-
|
|
1869
|
-
// Modify routes.tsx
|
|
1870
|
-
console.log(`\n📝 ${routesPath}`);
|
|
1871
|
-
if (modifyRoutesTsx(routesPath)) {
|
|
1872
|
-
console.log(' ✅ Added Comments route');
|
|
1873
|
-
successCount++;
|
|
1874
|
-
} else {
|
|
1875
|
-
skippedCount++;
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
1512
|
// Modify AppLayout.tsx
|
|
1879
1513
|
console.log(`\n📝 ${appLayoutPath}`);
|
|
1880
1514
|
if (modifyAppLayoutTsx(appLayoutPath)) {
|
|
@@ -16,7 +16,7 @@ export const CommentOverlay: React.FunctionComponent = () => {
|
|
|
16
16
|
const handlePageClick = (e: MouseEvent) => {
|
|
17
17
|
if (!commentsEnabled) return;
|
|
18
18
|
|
|
19
|
-
// Check if clicking on a pin or any interactive element
|
|
19
|
+
// Check if clicking on a pin or any interactive element (including FloatingWidget)
|
|
20
20
|
const target = e.target as HTMLElement;
|
|
21
21
|
if (
|
|
22
22
|
target.closest('button') ||
|
|
@@ -26,9 +26,10 @@ export const CommentOverlay: React.FunctionComponent = () => {
|
|
|
26
26
|
target.closest('textarea') ||
|
|
27
27
|
target.closest('[role="button"]') ||
|
|
28
28
|
target.closest('[data-comment-controls]') ||
|
|
29
|
-
target.closest('[data-comment-pin]')
|
|
29
|
+
target.closest('[data-comment-pin]') ||
|
|
30
|
+
target.closest('[data-floating-widget]')
|
|
30
31
|
) {
|
|
31
|
-
return; // Don't create pin if clicking interactive elements
|
|
32
|
+
return; // Don't create pin if clicking interactive elements or floating widget
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// Get the overlay container dimensions (accounts for drawer being open)
|
|
@@ -6,14 +6,6 @@ import {
|
|
|
6
6
|
Button,
|
|
7
7
|
Card,
|
|
8
8
|
CardBody,
|
|
9
|
-
Drawer,
|
|
10
|
-
DrawerActions,
|
|
11
|
-
DrawerCloseButton,
|
|
12
|
-
DrawerContent,
|
|
13
|
-
DrawerContentBody,
|
|
14
|
-
DrawerHead,
|
|
15
|
-
DrawerPanelBody,
|
|
16
|
-
DrawerPanelContent,
|
|
17
9
|
EmptyState,
|
|
18
10
|
EmptyStateBody,
|
|
19
11
|
Label,
|
|
@@ -40,10 +32,6 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
40
32
|
getThreadsForRoute,
|
|
41
33
|
selectedThreadId,
|
|
42
34
|
setSelectedThreadId,
|
|
43
|
-
drawerPinnedOpen,
|
|
44
|
-
setDrawerPinnedOpen,
|
|
45
|
-
floatingWidgetMode,
|
|
46
|
-
setFloatingWidgetMode,
|
|
47
35
|
addReply,
|
|
48
36
|
updateComment,
|
|
49
37
|
deleteComment,
|
|
@@ -59,28 +47,18 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
59
47
|
const [replyTextByCommentId, setReplyTextByCommentId] = React.useState<Record<string, string>>({});
|
|
60
48
|
const [editingCommentId, setEditingCommentId] = React.useState<string | null>(null);
|
|
61
49
|
const [editText, setEditText] = React.useState('');
|
|
62
|
-
const drawerRef = React.useRef<HTMLSpanElement>(null);
|
|
63
50
|
const [activeTabKey, setActiveTabKey] = React.useState<string | number>('comments');
|
|
64
51
|
|
|
65
52
|
const currentThreads = getThreadsForRoute(location.pathname, detectedVersion);
|
|
66
53
|
const selectedThread = currentThreads.find((t) => t.id === selectedThreadId);
|
|
67
|
-
const isExpanded = !!selectedThreadId || drawerPinnedOpen || floatingWidgetMode;
|
|
68
|
-
|
|
69
|
-
const onExpand = () => {
|
|
70
|
-
drawerRef.current && drawerRef.current.focus();
|
|
71
|
-
};
|
|
72
54
|
|
|
73
55
|
React.useEffect(() => {
|
|
74
56
|
if (selectedThreadId) {
|
|
75
57
|
setActiveTabKey('comments');
|
|
76
|
-
}
|
|
77
|
-
}, [selectedThreadId]);
|
|
78
|
-
|
|
79
|
-
React.useEffect(() => {
|
|
80
|
-
if (drawerPinnedOpen && !selectedThreadId) {
|
|
58
|
+
} else {
|
|
81
59
|
setActiveTabKey('details');
|
|
82
60
|
}
|
|
83
|
-
}, [
|
|
61
|
+
}, [selectedThreadId]);
|
|
84
62
|
|
|
85
63
|
const handleAddComment = () => {
|
|
86
64
|
if (newCommentText.trim() && selectedThread) {
|
|
@@ -150,11 +128,6 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
150
128
|
|
|
151
129
|
const handleClose = () => {
|
|
152
130
|
setSelectedThreadId(null);
|
|
153
|
-
if (floatingWidgetMode) {
|
|
154
|
-
setFloatingWidgetMode(false);
|
|
155
|
-
} else {
|
|
156
|
-
setDrawerPinnedOpen(false);
|
|
157
|
-
}
|
|
158
131
|
setEditingCommentId(null);
|
|
159
132
|
setEditText('');
|
|
160
133
|
setNewCommentText('');
|
|
@@ -221,17 +194,17 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
221
194
|
aria-label="Hale Commenting System drawer tabs"
|
|
222
195
|
>
|
|
223
196
|
<Tab eventKey="details" title={<TabTitleText>Details</TabTitleText>}>
|
|
224
|
-
<div style={{
|
|
197
|
+
<div style={{ padding: '1rem' }}>
|
|
225
198
|
<DetailsTab />
|
|
226
199
|
</div>
|
|
227
200
|
</Tab>
|
|
228
201
|
<Tab eventKey="jira" title={<TabTitleText>Jira</TabTitleText>}>
|
|
229
|
-
<div style={{
|
|
202
|
+
<div style={{ padding: '1rem' }}>
|
|
230
203
|
<JiraTab />
|
|
231
204
|
</div>
|
|
232
205
|
</Tab>
|
|
233
206
|
<Tab eventKey="comments" title={<TabTitleText>Comments</TabTitleText>}>
|
|
234
|
-
<div style={{
|
|
207
|
+
<div style={{ padding: '1rem' }}>
|
|
235
208
|
{!selectedThread ? (
|
|
236
209
|
<EmptyState icon={InfoCircleIcon} titleText="No pin selected" headingLevel="h3">
|
|
237
210
|
<EmptyStateBody>Select or create a comment pin to start a thread.</EmptyStateBody>
|
|
@@ -497,38 +470,12 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
|
|
|
497
470
|
</>
|
|
498
471
|
);
|
|
499
472
|
|
|
500
|
-
if (floatingWidgetMode && isExpanded) {
|
|
501
|
-
return (
|
|
502
|
-
<>
|
|
503
|
-
<FloatingWidget onClose={handleClose} title="Hale Commenting System">
|
|
504
|
-
<div style={{ padding: '1rem' }}>{panelContent}</div>
|
|
505
|
-
</FloatingWidget>
|
|
506
|
-
<div style={{ position: 'relative' }}>{children}</div>
|
|
507
|
-
</>
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const drawerPanelContent = isExpanded ? (
|
|
512
|
-
<DrawerPanelContent isResizable defaultSize={'500px'} minSize={'300px'}>
|
|
513
|
-
<DrawerHead>
|
|
514
|
-
<span tabIndex={isExpanded ? 0 : -1} ref={drawerRef}>
|
|
515
|
-
<Title headingLevel="h2" size="lg">
|
|
516
|
-
Hale Commenting System
|
|
517
|
-
</Title>
|
|
518
|
-
</span>
|
|
519
|
-
<DrawerActions>
|
|
520
|
-
<DrawerCloseButton onClick={handleClose} />
|
|
521
|
-
</DrawerActions>
|
|
522
|
-
</DrawerHead>
|
|
523
|
-
<DrawerPanelBody>{panelContent}</DrawerPanelBody>
|
|
524
|
-
</DrawerPanelContent>
|
|
525
|
-
) : null;
|
|
526
|
-
|
|
527
473
|
return (
|
|
528
|
-
|
|
529
|
-
<
|
|
530
|
-
|
|
531
|
-
</
|
|
532
|
-
|
|
474
|
+
<>
|
|
475
|
+
<FloatingWidget title="Hale Commenting System">
|
|
476
|
+
{panelContent}
|
|
477
|
+
</FloatingWidget>
|
|
478
|
+
<div style={{ position: 'relative' }}>{children}</div>
|
|
479
|
+
</>
|
|
533
480
|
);
|
|
534
481
|
};
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { Button, Switch, Title } from '@patternfly/react-core';
|
|
4
|
+
import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon } from '@patternfly/react-icons';
|
|
5
|
+
import { useComments } from '../contexts/CommentContext';
|
|
6
|
+
import { useGitHubAuth } from '../contexts/GitHubAuthContext';
|
|
4
7
|
|
|
5
8
|
interface FloatingWidgetProps {
|
|
6
9
|
children: React.ReactNode;
|
|
7
|
-
onClose: () => void;
|
|
8
10
|
title?: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ children,
|
|
12
|
-
const [position, setPosition] = React.useState({ x: window.innerWidth - 520, y:
|
|
13
|
+
export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ children, title = 'Hale Commenting System' }) => {
|
|
14
|
+
const [position, setPosition] = React.useState({ x: window.innerWidth - 520, y: 20 });
|
|
13
15
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
14
16
|
const [dragOffset, setDragOffset] = React.useState({ x: 0, y: 0 });
|
|
15
17
|
const [isMinimized, setIsMinimized] = React.useState(false);
|
|
18
|
+
const [viewportHeight, setViewportHeight] = React.useState(window.innerHeight);
|
|
16
19
|
const widgetRef = React.useRef<HTMLDivElement>(null);
|
|
17
20
|
|
|
21
|
+
const { commentsEnabled, setCommentsEnabled } = useComments();
|
|
22
|
+
const { isAuthenticated, user, login, logout } = useGitHubAuth();
|
|
23
|
+
|
|
18
24
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
19
25
|
if (!widgetRef.current) return;
|
|
20
26
|
const rect = widgetRef.current.getBoundingClientRect();
|
|
@@ -48,29 +54,43 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
|
|
|
48
54
|
};
|
|
49
55
|
}, [isDragging, dragOffset]);
|
|
50
56
|
|
|
51
|
-
//
|
|
57
|
+
// Update viewport height on resize to recalculate constraints
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
const handleResize = () => {
|
|
60
|
+
setViewportHeight(window.innerHeight);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
window.addEventListener('resize', handleResize);
|
|
64
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Constrain to viewport but allow moving into topbar area (just keep drag handle accessible)
|
|
52
68
|
const constrainedPosition = React.useMemo(() => {
|
|
53
69
|
const widgetWidth = 500;
|
|
54
|
-
|
|
70
|
+
// Calculate actual widget height: 80vh when expanded, or estimate ~120px when minimized
|
|
71
|
+
const widgetHeight = isMinimized ? 120 : viewportHeight * 0.8;
|
|
55
72
|
const maxX = window.innerWidth - 50; // Allow 50px of widget to be visible for dragging
|
|
56
|
-
const maxY =
|
|
73
|
+
const maxY = viewportHeight - 50;
|
|
74
|
+
// Allow widget to move into topbar, but keep at least 60px of drag handle visible
|
|
75
|
+
const minY = -widgetHeight + 60;
|
|
57
76
|
return {
|
|
58
77
|
x: Math.max(-widgetWidth + 50, Math.min(position.x, maxX)),
|
|
59
|
-
y: Math.max(
|
|
78
|
+
y: Math.max(minY, Math.min(position.y, maxY)),
|
|
60
79
|
};
|
|
61
|
-
}, [position, isMinimized]);
|
|
80
|
+
}, [position, isMinimized, viewportHeight]);
|
|
62
81
|
|
|
63
|
-
|
|
82
|
+
const widgetContent = (
|
|
64
83
|
<div
|
|
65
84
|
ref={widgetRef}
|
|
85
|
+
data-floating-widget
|
|
66
86
|
style={{
|
|
67
87
|
position: 'fixed',
|
|
68
88
|
left: `${constrainedPosition.x}px`,
|
|
69
89
|
top: `${constrainedPosition.y}px`,
|
|
70
90
|
width: '500px',
|
|
71
|
-
height: isMinimized ? '
|
|
91
|
+
height: isMinimized ? 'fit-content' : '80vh',
|
|
72
92
|
maxHeight: '80vh',
|
|
73
|
-
zIndex:
|
|
93
|
+
zIndex: 99999,
|
|
74
94
|
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)',
|
|
75
95
|
borderRadius: 'var(--pf-t--global--border--radius--medium)',
|
|
76
96
|
backgroundColor: '#ffffff',
|
|
@@ -80,43 +100,87 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
|
|
|
80
100
|
}}
|
|
81
101
|
>
|
|
82
102
|
<div
|
|
83
|
-
onMouseDown={handleMouseDown}
|
|
84
103
|
style={{
|
|
85
|
-
padding: '1rem',
|
|
86
104
|
borderBottom: isMinimized ? 'none' : '1px solid var(--pf-t--global--border--color--default)',
|
|
87
|
-
display: 'flex',
|
|
88
|
-
alignItems: 'center',
|
|
89
|
-
justifyContent: 'space-between',
|
|
90
|
-
cursor: 'grab',
|
|
91
|
-
userSelect: 'none',
|
|
92
105
|
backgroundColor: '#ffffff',
|
|
93
106
|
borderRadius: isMinimized ? 'var(--pf-t--global--border--radius--medium)' : 'var(--pf-t--global--border--radius--medium) var(--pf-t--global--border--radius--medium) 0 0',
|
|
94
107
|
}}
|
|
95
108
|
>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
{/* Title bar with drag handle */}
|
|
110
|
+
<div
|
|
111
|
+
onMouseDown={handleMouseDown}
|
|
112
|
+
style={{
|
|
113
|
+
padding: '1rem',
|
|
114
|
+
display: 'flex',
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
justifyContent: 'space-between',
|
|
117
|
+
cursor: 'grab',
|
|
118
|
+
userSelect: 'none',
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1 }}>
|
|
122
|
+
<GripVerticalIcon style={{ color: 'var(--pf-t--global--text--color--subtle)' }} />
|
|
123
|
+
<Title headingLevel="h2" size="lg">
|
|
124
|
+
{title}
|
|
125
|
+
</Title>
|
|
126
|
+
</div>
|
|
127
|
+
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
128
|
+
<Button
|
|
129
|
+
variant="plain"
|
|
130
|
+
icon={<WindowMinimizeIcon />}
|
|
131
|
+
onClick={(e) => {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
setIsMinimized(!isMinimized);
|
|
134
|
+
}}
|
|
135
|
+
aria-label={isMinimized ? 'Maximize widget' : 'Minimize widget'}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
101
138
|
</div>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
|
|
140
|
+
{/* Controls row */}
|
|
141
|
+
{!isMinimized && (
|
|
142
|
+
<div
|
|
143
|
+
style={{
|
|
144
|
+
padding: '0 1rem 0.75rem 1rem',
|
|
145
|
+
display: 'flex',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
gap: '1rem',
|
|
148
|
+
borderBottom: '1px solid var(--pf-t--global--border--color--default)',
|
|
109
149
|
}}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
150
|
+
>
|
|
151
|
+
<Switch
|
|
152
|
+
id="floating-comments-enabled-switch"
|
|
153
|
+
label="Enable Comments"
|
|
154
|
+
isChecked={commentsEnabled}
|
|
155
|
+
onChange={(_event, checked) => setCommentsEnabled(checked)}
|
|
156
|
+
aria-label="Enable or disable comments"
|
|
157
|
+
/>
|
|
158
|
+
<div style={{ flex: 1 }} />
|
|
159
|
+
{isAuthenticated ? (
|
|
160
|
+
<>
|
|
161
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', fontSize: 'var(--pf-t--global--font--size--sm)' }}>
|
|
162
|
+
<GithubIcon />
|
|
163
|
+
{user?.login ? `@${user.login}` : 'Signed in'}
|
|
164
|
+
</span>
|
|
165
|
+
<Button variant="link" isInline onClick={logout} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
|
|
166
|
+
Sign out
|
|
167
|
+
</Button>
|
|
168
|
+
</>
|
|
169
|
+
) : (
|
|
170
|
+
<Button variant="link" isInline icon={<GithubIcon />} onClick={login} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
|
|
171
|
+
Sign in with GitHub
|
|
172
|
+
</Button>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
114
176
|
</div>
|
|
115
177
|
{!isMinimized && (
|
|
116
178
|
<div
|
|
117
179
|
style={{
|
|
118
|
-
|
|
119
|
-
|
|
180
|
+
overflowY: 'auto',
|
|
181
|
+
overflowX: 'hidden',
|
|
182
|
+
flex: '1 1 0',
|
|
183
|
+
minHeight: 0,
|
|
120
184
|
backgroundColor: '#ffffff',
|
|
121
185
|
borderRadius: '0 0 var(--pf-t--global--border--radius--medium) var(--pf-t--global--border--radius--medium)',
|
|
122
186
|
}}
|
|
@@ -126,5 +190,7 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
|
|
|
126
190
|
)}
|
|
127
191
|
</div>
|
|
128
192
|
);
|
|
129
|
-
};
|
|
130
193
|
|
|
194
|
+
// Render widget in a portal to document.body so it floats above ALL page elements
|
|
195
|
+
return typeof document !== 'undefined' ? createPortal(widgetContent, document.body) : null;
|
|
196
|
+
};
|
|
@@ -204,7 +204,9 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
204
204
|
React.useEffect(() => {
|
|
205
205
|
if (typeof window === 'undefined') return;
|
|
206
206
|
try {
|
|
207
|
-
|
|
207
|
+
// Filter out temporary threads before saving to localStorage
|
|
208
|
+
const persistedThreads = threads.filter(t => !t.isTemporary);
|
|
209
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(persistedThreads));
|
|
208
210
|
} catch {
|
|
209
211
|
// ignore quota/serialization errors
|
|
210
212
|
}
|
|
@@ -255,14 +257,17 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
255
257
|
version,
|
|
256
258
|
comments: [],
|
|
257
259
|
provider: 'github',
|
|
258
|
-
syncStatus:
|
|
260
|
+
syncStatus: 'local',
|
|
259
261
|
status: 'open',
|
|
262
|
+
isTemporary: true, // Mark as temporary until first comment is added
|
|
260
263
|
};
|
|
261
264
|
setThreads((prev) => [...prev, newThread]);
|
|
262
265
|
|
|
263
|
-
console.log(`📌 Thread created
|
|
266
|
+
console.log(`📌 Thread created as temporary (will persist when first comment is added)`);
|
|
264
267
|
|
|
265
|
-
//
|
|
268
|
+
// Don't sync to GitHub yet - wait for first comment
|
|
269
|
+
// This prevents creating empty GitHub issues
|
|
270
|
+
/* Commented out - only sync when comment is added
|
|
266
271
|
if (isConfigured) {
|
|
267
272
|
console.log(`🔵 Creating GitHub issue for thread ${threadId}...`);
|
|
268
273
|
|
|
@@ -319,6 +324,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
319
324
|
console.log(`📌 Thread ${threadId} syncStatus updated to: error (exception caught)`);
|
|
320
325
|
});
|
|
321
326
|
}
|
|
327
|
+
*/
|
|
322
328
|
|
|
323
329
|
return threadId;
|
|
324
330
|
};
|
|
@@ -521,6 +527,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
|
|
|
521
527
|
return {
|
|
522
528
|
...thread,
|
|
523
529
|
comments: [...thread.comments, newComment],
|
|
530
|
+
isTemporary: undefined, // Remove temporary flag - thread now persists
|
|
524
531
|
};
|
|
525
532
|
}),
|
|
526
533
|
);
|
package/src/app/routes.tsx
CHANGED
package/tsconfig.json
CHANGED