@bugspotter/sdk 0.1.0-alpha.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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/bugspotter.min.js +2 -0
- package/dist/bugspotter.min.js.LICENSE.txt +14 -0
- package/dist/capture/base-capture.d.ts +34 -0
- package/dist/capture/base-capture.js +23 -0
- package/dist/capture/capture-lifecycle.d.ts +24 -0
- package/dist/capture/capture-lifecycle.js +2 -0
- package/dist/capture/console.d.ts +29 -0
- package/dist/capture/console.js +107 -0
- package/dist/capture/metadata.d.ts +21 -0
- package/dist/capture/metadata.js +76 -0
- package/dist/capture/network.d.ts +32 -0
- package/dist/capture/network.js +135 -0
- package/dist/capture/screenshot.d.ts +19 -0
- package/dist/capture/screenshot.js +52 -0
- package/dist/collectors/dom.d.ts +67 -0
- package/dist/collectors/dom.js +164 -0
- package/dist/collectors/index.d.ts +2 -0
- package/dist/collectors/index.js +5 -0
- package/dist/core/buffer.d.ts +50 -0
- package/dist/core/buffer.js +88 -0
- package/dist/core/circular-buffer.d.ts +42 -0
- package/dist/core/circular-buffer.js +77 -0
- package/dist/core/compress.d.ts +49 -0
- package/dist/core/compress.js +245 -0
- package/dist/core/offline-queue.d.ts +76 -0
- package/dist/core/offline-queue.js +301 -0
- package/dist/core/transport.d.ts +73 -0
- package/dist/core/transport.js +352 -0
- package/dist/core/upload-helpers.d.ts +32 -0
- package/dist/core/upload-helpers.js +79 -0
- package/dist/core/uploader.d.ts +70 -0
- package/dist/core/uploader.js +185 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.esm.js +205 -0
- package/dist/index.js +244 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/sanitize-patterns.d.ts +103 -0
- package/dist/utils/sanitize-patterns.js +282 -0
- package/dist/utils/sanitize.d.ts +73 -0
- package/dist/utils/sanitize.js +254 -0
- package/dist/widget/button.d.ts +33 -0
- package/dist/widget/button.js +143 -0
- package/dist/widget/components/dom-element-cache.d.ts +62 -0
- package/dist/widget/components/dom-element-cache.js +105 -0
- package/dist/widget/components/form-validator.d.ts +66 -0
- package/dist/widget/components/form-validator.js +115 -0
- package/dist/widget/components/pii-detection-display.d.ts +64 -0
- package/dist/widget/components/pii-detection-display.js +142 -0
- package/dist/widget/components/redaction-canvas.d.ts +95 -0
- package/dist/widget/components/redaction-canvas.js +230 -0
- package/dist/widget/components/screenshot-processor.d.ts +44 -0
- package/dist/widget/components/screenshot-processor.js +191 -0
- package/dist/widget/components/style-manager.d.ts +37 -0
- package/dist/widget/components/style-manager.js +296 -0
- package/dist/widget/components/template-manager.d.ts +66 -0
- package/dist/widget/components/template-manager.js +198 -0
- package/dist/widget/modal.d.ts +62 -0
- package/dist/widget/modal.js +299 -0
- package/docs/CDN.md +213 -0
- package/docs/FRAMEWORK_INTEGRATION.md +1104 -0
- package/docs/PUBLISHING.md +550 -0
- package/docs/SESSION_REPLAY.md +381 -0
- package/package.json +90 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FloatingButton = void 0;
|
|
4
|
+
const DEFAULT_BUTTON_OPTIONS = {
|
|
5
|
+
position: 'bottom-right',
|
|
6
|
+
icon: '🐛',
|
|
7
|
+
backgroundColor: '#ef4444',
|
|
8
|
+
size: 60,
|
|
9
|
+
offset: { x: 20, y: 20 },
|
|
10
|
+
zIndex: 999999,
|
|
11
|
+
};
|
|
12
|
+
const BUTTON_STYLES = {
|
|
13
|
+
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
14
|
+
boxShadow: {
|
|
15
|
+
default: '0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)',
|
|
16
|
+
hover: '0 10px 15px rgba(0, 0, 0, 0.2), 0 4px 6px rgba(0, 0, 0, 0.1)',
|
|
17
|
+
},
|
|
18
|
+
transform: {
|
|
19
|
+
default: 'scale(1)',
|
|
20
|
+
hover: 'scale(1.1)',
|
|
21
|
+
active: 'scale(0.95)',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
class FloatingButton {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
var _a, _b, _c, _d, _e, _f;
|
|
27
|
+
this.eventHandlers = new Map();
|
|
28
|
+
this.handleMouseEnter = () => {
|
|
29
|
+
this.button.style.transform = BUTTON_STYLES.transform.hover;
|
|
30
|
+
this.button.style.boxShadow = BUTTON_STYLES.boxShadow.hover;
|
|
31
|
+
};
|
|
32
|
+
this.handleMouseLeave = () => {
|
|
33
|
+
this.button.style.transform = BUTTON_STYLES.transform.default;
|
|
34
|
+
this.button.style.boxShadow = BUTTON_STYLES.boxShadow.default;
|
|
35
|
+
};
|
|
36
|
+
this.handleMouseDown = () => {
|
|
37
|
+
this.button.style.transform = BUTTON_STYLES.transform.active;
|
|
38
|
+
};
|
|
39
|
+
this.handleMouseUp = () => {
|
|
40
|
+
this.button.style.transform = BUTTON_STYLES.transform.hover;
|
|
41
|
+
};
|
|
42
|
+
this.options = {
|
|
43
|
+
position: (_a = options.position) !== null && _a !== void 0 ? _a : DEFAULT_BUTTON_OPTIONS.position,
|
|
44
|
+
icon: (_b = options.icon) !== null && _b !== void 0 ? _b : DEFAULT_BUTTON_OPTIONS.icon,
|
|
45
|
+
backgroundColor: (_c = options.backgroundColor) !== null && _c !== void 0 ? _c : DEFAULT_BUTTON_OPTIONS.backgroundColor,
|
|
46
|
+
size: (_d = options.size) !== null && _d !== void 0 ? _d : DEFAULT_BUTTON_OPTIONS.size,
|
|
47
|
+
offset: (_e = options.offset) !== null && _e !== void 0 ? _e : DEFAULT_BUTTON_OPTIONS.offset,
|
|
48
|
+
zIndex: (_f = options.zIndex) !== null && _f !== void 0 ? _f : DEFAULT_BUTTON_OPTIONS.zIndex,
|
|
49
|
+
};
|
|
50
|
+
this.button = this.createButton();
|
|
51
|
+
// Ensure DOM is ready before appending
|
|
52
|
+
if (document.body) {
|
|
53
|
+
document.body.appendChild(this.button);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
57
|
+
document.body.appendChild(this.button);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
createButton() {
|
|
62
|
+
const btn = document.createElement('button');
|
|
63
|
+
btn.textContent = this.options.icon;
|
|
64
|
+
btn.setAttribute('aria-label', 'Report Bug');
|
|
65
|
+
btn.setAttribute('data-bugspotter-exclude', 'true');
|
|
66
|
+
btn.style.cssText = this.getButtonStyles();
|
|
67
|
+
this.addHoverEffects(btn);
|
|
68
|
+
return btn;
|
|
69
|
+
}
|
|
70
|
+
getButtonStyles() {
|
|
71
|
+
const { position, size, offset, backgroundColor, zIndex } = this.options;
|
|
72
|
+
const positionStyles = this.getPositionStyles(position, offset);
|
|
73
|
+
return `
|
|
74
|
+
position: fixed;
|
|
75
|
+
${positionStyles}
|
|
76
|
+
width: ${size}px;
|
|
77
|
+
height: ${size}px;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
background: ${backgroundColor};
|
|
80
|
+
color: white;
|
|
81
|
+
border: none;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
font-size: ${size * 0.5}px;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
box-shadow: ${BUTTON_STYLES.boxShadow.default};
|
|
88
|
+
transition: ${BUTTON_STYLES.transition};
|
|
89
|
+
z-index: ${zIndex};
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
getPositionStyles(position, offset) {
|
|
93
|
+
switch (position) {
|
|
94
|
+
case 'bottom-right':
|
|
95
|
+
return `bottom: ${offset.y}px; right: ${offset.x}px;`;
|
|
96
|
+
case 'bottom-left':
|
|
97
|
+
return `bottom: ${offset.y}px; left: ${offset.x}px;`;
|
|
98
|
+
case 'top-right':
|
|
99
|
+
return `top: ${offset.y}px; right: ${offset.x}px;`;
|
|
100
|
+
case 'top-left':
|
|
101
|
+
return `top: ${offset.y}px; left: ${offset.x}px;`;
|
|
102
|
+
default:
|
|
103
|
+
return `bottom: ${offset.y}px; right: ${offset.x}px;`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
addHoverEffects(btn) {
|
|
107
|
+
const handlers = {
|
|
108
|
+
mouseenter: this.handleMouseEnter,
|
|
109
|
+
mouseleave: this.handleMouseLeave,
|
|
110
|
+
mousedown: this.handleMouseDown,
|
|
111
|
+
mouseup: this.handleMouseUp,
|
|
112
|
+
};
|
|
113
|
+
Object.entries(handlers).forEach(([event, handler]) => {
|
|
114
|
+
btn.addEventListener(event, handler);
|
|
115
|
+
this.eventHandlers.set(event, handler);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
onClick(handler) {
|
|
119
|
+
this.button.addEventListener('click', handler);
|
|
120
|
+
}
|
|
121
|
+
show() {
|
|
122
|
+
this.button.style.display = 'flex';
|
|
123
|
+
}
|
|
124
|
+
hide() {
|
|
125
|
+
this.button.style.display = 'none';
|
|
126
|
+
}
|
|
127
|
+
setIcon(icon) {
|
|
128
|
+
this.button.textContent = icon;
|
|
129
|
+
}
|
|
130
|
+
setBackgroundColor(color) {
|
|
131
|
+
this.button.style.backgroundColor = color;
|
|
132
|
+
}
|
|
133
|
+
destroy() {
|
|
134
|
+
// Remove all event listeners
|
|
135
|
+
this.eventHandlers.forEach((handler, event) => {
|
|
136
|
+
this.button.removeEventListener(event, handler);
|
|
137
|
+
});
|
|
138
|
+
this.eventHandlers.clear();
|
|
139
|
+
// Remove button from DOM
|
|
140
|
+
this.button.remove();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.FloatingButton = FloatingButton;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOMElementCache
|
|
3
|
+
*
|
|
4
|
+
* Responsibility: Cache DOM element references to avoid repeated querySelector calls
|
|
5
|
+
* Follows SRP: Only handles element reference management
|
|
6
|
+
* Improves Performance: Single querySelector per element
|
|
7
|
+
*/
|
|
8
|
+
export interface ModalElements {
|
|
9
|
+
overlay: HTMLElement;
|
|
10
|
+
modal: HTMLElement;
|
|
11
|
+
closeButton: HTMLElement;
|
|
12
|
+
form: HTMLFormElement;
|
|
13
|
+
titleInput: HTMLInputElement;
|
|
14
|
+
titleError: HTMLElement;
|
|
15
|
+
descriptionTextarea: HTMLTextAreaElement;
|
|
16
|
+
descriptionError: HTMLElement;
|
|
17
|
+
screenshotImg?: HTMLImageElement;
|
|
18
|
+
redactionCanvas?: HTMLCanvasElement;
|
|
19
|
+
redactButton?: HTMLButtonElement;
|
|
20
|
+
clearButton?: HTMLButtonElement;
|
|
21
|
+
piiSection: HTMLElement;
|
|
22
|
+
piiContent: HTMLElement;
|
|
23
|
+
piiConfirmCheckbox: HTMLInputElement;
|
|
24
|
+
cancelButton: HTMLButtonElement;
|
|
25
|
+
submitButton: HTMLButtonElement;
|
|
26
|
+
}
|
|
27
|
+
export declare class DOMElementCache {
|
|
28
|
+
private elements;
|
|
29
|
+
private container;
|
|
30
|
+
/**
|
|
31
|
+
* Initialize cache from a container element or shadow root
|
|
32
|
+
*/
|
|
33
|
+
initialize(container: HTMLElement | ShadowRoot): ModalElements;
|
|
34
|
+
/**
|
|
35
|
+
* Get cached elements (throws if not initialized)
|
|
36
|
+
*/
|
|
37
|
+
get(): ModalElements;
|
|
38
|
+
/**
|
|
39
|
+
* Check if cache is initialized
|
|
40
|
+
*/
|
|
41
|
+
isInitialized(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Clear the cache
|
|
44
|
+
*/
|
|
45
|
+
clear(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Get a required element (throws if not found)
|
|
48
|
+
*/
|
|
49
|
+
private getRequiredElement;
|
|
50
|
+
/**
|
|
51
|
+
* Get an optional element (returns undefined if not found)
|
|
52
|
+
*/
|
|
53
|
+
private getOptionalElement;
|
|
54
|
+
/**
|
|
55
|
+
* Refresh a specific element in the cache
|
|
56
|
+
*/
|
|
57
|
+
refreshElement(key: keyof ModalElements, selector: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Get container element
|
|
60
|
+
*/
|
|
61
|
+
getContainer(): HTMLElement | ShadowRoot | null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DOMElementCache
|
|
4
|
+
*
|
|
5
|
+
* Responsibility: Cache DOM element references to avoid repeated querySelector calls
|
|
6
|
+
* Follows SRP: Only handles element reference management
|
|
7
|
+
* Improves Performance: Single querySelector per element
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DOMElementCache = void 0;
|
|
11
|
+
class DOMElementCache {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.elements = null;
|
|
14
|
+
this.container = null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Initialize cache from a container element or shadow root
|
|
18
|
+
*/
|
|
19
|
+
initialize(container) {
|
|
20
|
+
this.container = container;
|
|
21
|
+
const overlay = this.getRequiredElement('.overlay', container);
|
|
22
|
+
const modal = this.getRequiredElement('.modal', overlay);
|
|
23
|
+
const form = this.getRequiredElement('.form', modal);
|
|
24
|
+
this.elements = {
|
|
25
|
+
overlay,
|
|
26
|
+
modal,
|
|
27
|
+
closeButton: this.getRequiredElement('.close', modal),
|
|
28
|
+
form,
|
|
29
|
+
titleInput: this.getRequiredElement('#title', form),
|
|
30
|
+
titleError: this.getRequiredElement('#title-error', form),
|
|
31
|
+
descriptionTextarea: this.getRequiredElement('#description', form),
|
|
32
|
+
descriptionError: this.getRequiredElement('#description-error', form),
|
|
33
|
+
screenshotImg: this.getOptionalElement('#screenshot', modal),
|
|
34
|
+
redactionCanvas: this.getOptionalElement('#redaction-canvas', modal),
|
|
35
|
+
redactButton: this.getOptionalElement('#btn-redact', modal),
|
|
36
|
+
clearButton: this.getOptionalElement('#btn-clear', modal),
|
|
37
|
+
piiSection: this.getRequiredElement('#pii-section', modal),
|
|
38
|
+
piiContent: this.getRequiredElement('#pii-content', modal),
|
|
39
|
+
piiConfirmCheckbox: this.getRequiredElement('#pii-confirm', modal),
|
|
40
|
+
cancelButton: this.getRequiredElement('#btn-cancel', modal),
|
|
41
|
+
submitButton: this.getRequiredElement('#btn-submit', modal),
|
|
42
|
+
};
|
|
43
|
+
return this.elements;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get cached elements (throws if not initialized)
|
|
47
|
+
*/
|
|
48
|
+
get() {
|
|
49
|
+
if (!this.elements) {
|
|
50
|
+
throw new Error('DOMElementCache not initialized. Call initialize() first.');
|
|
51
|
+
}
|
|
52
|
+
return this.elements;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if cache is initialized
|
|
56
|
+
*/
|
|
57
|
+
isInitialized() {
|
|
58
|
+
return this.elements !== null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clear the cache
|
|
62
|
+
*/
|
|
63
|
+
clear() {
|
|
64
|
+
this.elements = null;
|
|
65
|
+
this.container = null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a required element (throws if not found)
|
|
69
|
+
*/
|
|
70
|
+
getRequiredElement(selector, parent) {
|
|
71
|
+
const searchParent = parent || this.container || document;
|
|
72
|
+
const element = searchParent.querySelector(selector);
|
|
73
|
+
if (!element) {
|
|
74
|
+
throw new Error(`Required element not found: ${selector}`);
|
|
75
|
+
}
|
|
76
|
+
return element;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get an optional element (returns undefined if not found)
|
|
80
|
+
*/
|
|
81
|
+
getOptionalElement(selector, parent) {
|
|
82
|
+
const searchParent = parent || this.container || document;
|
|
83
|
+
const element = searchParent.querySelector(selector);
|
|
84
|
+
return element || undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refresh a specific element in the cache
|
|
88
|
+
*/
|
|
89
|
+
refreshElement(key, selector) {
|
|
90
|
+
if (!this.elements || !this.container) {
|
|
91
|
+
throw new Error('DOMElementCache not initialized');
|
|
92
|
+
}
|
|
93
|
+
const element = this.container.querySelector(selector);
|
|
94
|
+
if (element) {
|
|
95
|
+
this.elements[key] = element;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get container element
|
|
100
|
+
*/
|
|
101
|
+
getContainer() {
|
|
102
|
+
return this.container;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
exports.DOMElementCache = DOMElementCache;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormValidator
|
|
3
|
+
*
|
|
4
|
+
* Responsibility: Validate form input and manage error states
|
|
5
|
+
* Follows SRP: Only handles validation logic (pure functions)
|
|
6
|
+
*/
|
|
7
|
+
export interface ValidationResult {
|
|
8
|
+
isValid: boolean;
|
|
9
|
+
errors: ValidationErrors;
|
|
10
|
+
}
|
|
11
|
+
export interface ValidationErrors {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
piiConfirmation?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FormData {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
piiDetected: boolean;
|
|
20
|
+
piiConfirmed: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class FormValidator {
|
|
23
|
+
private minTitleLength;
|
|
24
|
+
private maxTitleLength;
|
|
25
|
+
private minDescriptionLength;
|
|
26
|
+
private maxDescriptionLength;
|
|
27
|
+
constructor(config?: {
|
|
28
|
+
minTitleLength?: number;
|
|
29
|
+
maxTitleLength?: number;
|
|
30
|
+
minDescriptionLength?: number;
|
|
31
|
+
maxDescriptionLength?: number;
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Validate complete form data
|
|
35
|
+
*/
|
|
36
|
+
validate(data: FormData): ValidationResult;
|
|
37
|
+
/**
|
|
38
|
+
* Validate title field
|
|
39
|
+
*/
|
|
40
|
+
validateTitle(title: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Validate description field
|
|
43
|
+
*/
|
|
44
|
+
validateDescription(description: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Validate single field by name
|
|
47
|
+
*/
|
|
48
|
+
validateField(fieldName: keyof FormData, value: unknown, formData?: Partial<FormData>): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Check if a string is empty or whitespace only
|
|
51
|
+
*/
|
|
52
|
+
isEmpty(value: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Sanitize input (trim whitespace)
|
|
55
|
+
*/
|
|
56
|
+
sanitizeInput(value: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Get validation configuration
|
|
59
|
+
*/
|
|
60
|
+
getConfig(): {
|
|
61
|
+
minTitleLength: number;
|
|
62
|
+
maxTitleLength: number;
|
|
63
|
+
minDescriptionLength: number;
|
|
64
|
+
maxDescriptionLength: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FormValidator
|
|
4
|
+
*
|
|
5
|
+
* Responsibility: Validate form input and manage error states
|
|
6
|
+
* Follows SRP: Only handles validation logic (pure functions)
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.FormValidator = void 0;
|
|
10
|
+
class FormValidator {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.minTitleLength = config.minTitleLength || 3;
|
|
13
|
+
this.maxTitleLength = config.maxTitleLength || 200;
|
|
14
|
+
this.minDescriptionLength = config.minDescriptionLength || 10;
|
|
15
|
+
this.maxDescriptionLength = config.maxDescriptionLength || 5000;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validate complete form data
|
|
19
|
+
*/
|
|
20
|
+
validate(data) {
|
|
21
|
+
const errors = {};
|
|
22
|
+
// Validate title
|
|
23
|
+
const titleError = this.validateTitle(data.title);
|
|
24
|
+
if (titleError) {
|
|
25
|
+
errors.title = titleError;
|
|
26
|
+
}
|
|
27
|
+
// Validate description
|
|
28
|
+
const descriptionError = this.validateDescription(data.description);
|
|
29
|
+
if (descriptionError) {
|
|
30
|
+
errors.description = descriptionError;
|
|
31
|
+
}
|
|
32
|
+
// Validate PII confirmation if PII detected
|
|
33
|
+
if (data.piiDetected && !data.piiConfirmed) {
|
|
34
|
+
errors.piiConfirmation = 'Please confirm you have reviewed sensitive information';
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
isValid: Object.keys(errors).length === 0,
|
|
38
|
+
errors,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate title field
|
|
43
|
+
*/
|
|
44
|
+
validateTitle(title) {
|
|
45
|
+
const trimmed = title.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return 'Title is required';
|
|
48
|
+
}
|
|
49
|
+
if (trimmed.length < this.minTitleLength) {
|
|
50
|
+
return `Title must be at least ${this.minTitleLength} characters`;
|
|
51
|
+
}
|
|
52
|
+
if (trimmed.length > this.maxTitleLength) {
|
|
53
|
+
return `Title must not exceed ${this.maxTitleLength} characters`;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validate description field
|
|
59
|
+
*/
|
|
60
|
+
validateDescription(description) {
|
|
61
|
+
const trimmed = description.trim();
|
|
62
|
+
if (!trimmed) {
|
|
63
|
+
return 'Description is required';
|
|
64
|
+
}
|
|
65
|
+
if (trimmed.length < this.minDescriptionLength) {
|
|
66
|
+
return `Description must be at least ${this.minDescriptionLength} characters`;
|
|
67
|
+
}
|
|
68
|
+
if (trimmed.length > this.maxDescriptionLength) {
|
|
69
|
+
return `Description must not exceed ${this.maxDescriptionLength} characters`;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validate single field by name
|
|
75
|
+
*/
|
|
76
|
+
validateField(fieldName, value, formData) {
|
|
77
|
+
switch (fieldName) {
|
|
78
|
+
case 'title':
|
|
79
|
+
return this.validateTitle(value);
|
|
80
|
+
case 'description':
|
|
81
|
+
return this.validateDescription(value);
|
|
82
|
+
case 'piiConfirmed':
|
|
83
|
+
if ((formData === null || formData === void 0 ? void 0 : formData.piiDetected) && !value) {
|
|
84
|
+
return 'Please confirm you have reviewed sensitive information';
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if a string is empty or whitespace only
|
|
93
|
+
*/
|
|
94
|
+
isEmpty(value) {
|
|
95
|
+
return !value || value.trim().length === 0;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Sanitize input (trim whitespace)
|
|
99
|
+
*/
|
|
100
|
+
sanitizeInput(value) {
|
|
101
|
+
return value.trim();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get validation configuration
|
|
105
|
+
*/
|
|
106
|
+
getConfig() {
|
|
107
|
+
return {
|
|
108
|
+
minTitleLength: this.minTitleLength,
|
|
109
|
+
maxTitleLength: this.maxTitleLength,
|
|
110
|
+
minDescriptionLength: this.minDescriptionLength,
|
|
111
|
+
maxDescriptionLength: this.maxDescriptionLength,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.FormValidator = FormValidator;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PIIDetectionDisplay
|
|
3
|
+
*
|
|
4
|
+
* Responsibility: Render PII detection UI and manage PII-related display logic
|
|
5
|
+
* Follows SRP: Only handles PII visualization
|
|
6
|
+
*/
|
|
7
|
+
import type { PIIDetection } from '../modal';
|
|
8
|
+
export interface PIIDisplayConfig {
|
|
9
|
+
showExamples?: boolean;
|
|
10
|
+
groupByType?: boolean;
|
|
11
|
+
maxExamplesPerType?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class PIIDetectionDisplay {
|
|
14
|
+
private config;
|
|
15
|
+
constructor(config?: PIIDisplayConfig);
|
|
16
|
+
/**
|
|
17
|
+
* Render PII detection information into a container
|
|
18
|
+
*/
|
|
19
|
+
render(piiDetections: PIIDetection[], container: HTMLElement): void;
|
|
20
|
+
/**
|
|
21
|
+
* Render PII grouped by type with badges
|
|
22
|
+
*/
|
|
23
|
+
private renderGroupedPII;
|
|
24
|
+
/**
|
|
25
|
+
* Render PII as a simple list
|
|
26
|
+
*/
|
|
27
|
+
private renderListPII;
|
|
28
|
+
/**
|
|
29
|
+
* Create a PII type badge
|
|
30
|
+
*/
|
|
31
|
+
private createBadge;
|
|
32
|
+
/**
|
|
33
|
+
* Group PII detections by type
|
|
34
|
+
*/
|
|
35
|
+
private groupByType;
|
|
36
|
+
/**
|
|
37
|
+
* Get summary statistics about PII detections
|
|
38
|
+
*/
|
|
39
|
+
getSummary(piiDetections: PIIDetection[]): {
|
|
40
|
+
totalCount: number;
|
|
41
|
+
typeCount: number;
|
|
42
|
+
types: string[];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Check if PII detections contain a specific type
|
|
46
|
+
*/
|
|
47
|
+
hasType(piiDetections: PIIDetection[], type: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Filter PII detections by type
|
|
50
|
+
*/
|
|
51
|
+
filterByType(piiDetections: PIIDetection[], type: string): PIIDetection[];
|
|
52
|
+
/**
|
|
53
|
+
* Escape HTML to prevent XSS
|
|
54
|
+
*/
|
|
55
|
+
private escapeHtml;
|
|
56
|
+
/**
|
|
57
|
+
* Update configuration
|
|
58
|
+
*/
|
|
59
|
+
updateConfig(config: Partial<PIIDisplayConfig>): void;
|
|
60
|
+
/**
|
|
61
|
+
* Get current configuration
|
|
62
|
+
*/
|
|
63
|
+
getConfig(): Required<PIIDisplayConfig>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PIIDetectionDisplay
|
|
4
|
+
*
|
|
5
|
+
* Responsibility: Render PII detection UI and manage PII-related display logic
|
|
6
|
+
* Follows SRP: Only handles PII visualization
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.PIIDetectionDisplay = void 0;
|
|
10
|
+
class PIIDetectionDisplay {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = {
|
|
13
|
+
showExamples: config.showExamples !== false,
|
|
14
|
+
groupByType: config.groupByType !== false,
|
|
15
|
+
maxExamplesPerType: config.maxExamplesPerType || 3,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Render PII detection information into a container
|
|
20
|
+
*/
|
|
21
|
+
render(piiDetections, container) {
|
|
22
|
+
if (!piiDetections || piiDetections.length === 0) {
|
|
23
|
+
container.innerHTML = '';
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (this.config.groupByType) {
|
|
27
|
+
container.innerHTML = this.renderGroupedPII(piiDetections);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
container.innerHTML = this.renderListPII(piiDetections);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render PII grouped by type with badges
|
|
35
|
+
*/
|
|
36
|
+
renderGroupedPII(piiDetections) {
|
|
37
|
+
const grouped = this.groupByType(piiDetections);
|
|
38
|
+
const badges = [];
|
|
39
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
40
|
+
const totalCount = items.reduce((sum, item) => {
|
|
41
|
+
return sum + item.count;
|
|
42
|
+
}, 0);
|
|
43
|
+
badges.push(this.createBadge(type, totalCount));
|
|
44
|
+
}
|
|
45
|
+
let html = `<div class="bugspotter-pii-badges">${badges.join('')}</div>`;
|
|
46
|
+
if (this.config.showExamples) {
|
|
47
|
+
html += '<div class="bugspotter-pii-details">';
|
|
48
|
+
html +=
|
|
49
|
+
'<p style="margin: 10px 0 5px 0; font-size: 13px; color: #856404;">Detected types:</p>';
|
|
50
|
+
html += '<ul class="bugspotter-pii-list">';
|
|
51
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
52
|
+
const totalCount = items.reduce((sum, item) => {
|
|
53
|
+
return sum + item.count;
|
|
54
|
+
}, 0);
|
|
55
|
+
html += `<li><strong>${this.escapeHtml(type)}:</strong> ${totalCount} occurrence${totalCount !== 1 ? 's' : ''}</li>`;
|
|
56
|
+
}
|
|
57
|
+
html += '</ul></div>';
|
|
58
|
+
}
|
|
59
|
+
return html;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Render PII as a simple list
|
|
63
|
+
*/
|
|
64
|
+
renderListPII(piiDetections) {
|
|
65
|
+
let html = '<ul class="bugspotter-pii-list">';
|
|
66
|
+
for (const detection of piiDetections) {
|
|
67
|
+
html += `<li>${this.escapeHtml(detection.type)}: ${detection.count} occurrence${detection.count !== 1 ? 's' : ''}</li>`;
|
|
68
|
+
}
|
|
69
|
+
html += '</ul>';
|
|
70
|
+
return html;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a PII type badge
|
|
74
|
+
*/
|
|
75
|
+
createBadge(type, count) {
|
|
76
|
+
return `<span class="bugspotter-pii-badge">${this.escapeHtml(type)}: ${count}</span>`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Group PII detections by type
|
|
80
|
+
*/
|
|
81
|
+
groupByType(piiDetections) {
|
|
82
|
+
const grouped = {};
|
|
83
|
+
for (const detection of piiDetections) {
|
|
84
|
+
if (!grouped[detection.type]) {
|
|
85
|
+
grouped[detection.type] = [];
|
|
86
|
+
}
|
|
87
|
+
grouped[detection.type].push(detection);
|
|
88
|
+
}
|
|
89
|
+
return grouped;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get summary statistics about PII detections
|
|
93
|
+
*/
|
|
94
|
+
getSummary(piiDetections) {
|
|
95
|
+
const types = new Set();
|
|
96
|
+
for (const detection of piiDetections) {
|
|
97
|
+
types.add(detection.type);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
totalCount: piiDetections.length,
|
|
101
|
+
typeCount: types.size,
|
|
102
|
+
types: Array.from(types),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if PII detections contain a specific type
|
|
107
|
+
*/
|
|
108
|
+
hasType(piiDetections, type) {
|
|
109
|
+
return piiDetections.some((d) => {
|
|
110
|
+
return d.type === type;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Filter PII detections by type
|
|
115
|
+
*/
|
|
116
|
+
filterByType(piiDetections, type) {
|
|
117
|
+
return piiDetections.filter((d) => {
|
|
118
|
+
return d.type === type;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Escape HTML to prevent XSS
|
|
123
|
+
*/
|
|
124
|
+
escapeHtml(text) {
|
|
125
|
+
const div = document.createElement('div');
|
|
126
|
+
div.textContent = text;
|
|
127
|
+
return div.innerHTML;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Update configuration
|
|
131
|
+
*/
|
|
132
|
+
updateConfig(config) {
|
|
133
|
+
this.config = Object.assign(Object.assign({}, this.config), config);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get current configuration
|
|
137
|
+
*/
|
|
138
|
+
getConfig() {
|
|
139
|
+
return Object.assign({}, this.config);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.PIIDetectionDisplay = PIIDetectionDisplay;
|