SDA-2533 Add clear, erase and done functionality. Further optimizations of logic.

This commit is contained in:
psjostrom 2020-11-19 09:17:39 +01:00
parent 6811683e29
commit 064b9b53b2
9 changed files with 257 additions and 118 deletions

View File

@ -4,7 +4,7 @@ exports[`<AnnotateArea/> should render correctly 1`] = `
<svg <svg
data-testid="annotate-area" data-testid="annotate-area"
height={800} height={800}
id="annotate" id="annotate-area"
onMouseDown={[Function]} onMouseDown={[Function]}
onMouseLeave={[Function]} onMouseLeave={[Function]}
onMouseMove={[Function]} onMouseMove={[Function]}

View File

@ -57,25 +57,28 @@ exports[`Snipping Tool should render correctly 1`] = `
</button> </button>
</div> </div>
</header> </header>
<main> <main
<div style={
className="imageContainer" Object {
> "minHeight": 200,
<AnnotateArea }
chosenTool="PEN" }
highlightColor="rgba(0, 142, 255, 0.64)" >
imageDimensions={ <AnnotateArea
Object { chosenTool="PEN"
"height": 600, data-testid="annotate-component"
"width": 800, highlightColor="rgba(0, 142, 255, 0.64)"
} imageDimensions={
Object {
"height": 600,
"width": 800,
} }
onChange={[Function]} }
paths={Array []} onChange={[Function]}
penColor="rgba(0, 142, 255, 1)" paths={Array []}
screenSnippetPath="Screen-Snippet" penColor="rgba(0, 142, 255, 1)"
/> screenSnippetPath="Screen-Snippet"
</div> />
</main> </main>
<footer> <footer>
<button <button

View File

@ -37,7 +37,7 @@ describe('<AnnotateArea/>', () => {
expect(defaultProps.onChange).toHaveBeenCalledWith([{ expect(defaultProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(38, 196, 58, 1)', color: 'rgba(38, 196, 58, 1)',
key: 'path0', key: 'path0',
points: [{ x: 0, y: 0 }], points: [{ x: 2, y: 1 }],
shouldShow: true, shouldShow: true,
strokeWidth: 5, strokeWidth: 5,
}]); }]);
@ -89,4 +89,32 @@ describe('<AnnotateArea/>', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />); const wrapper = shallow(<AnnotateArea {...defaultProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false); expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false);
}); });
it('should call onChange with hidden path if clicked on path with tool eraser', () => {
const pathProps = {
paths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
chosenTool: Tool.eraser,
screenSnippetPath: 'very-nice-path',
};
const wrapper = shallow(<AnnotateArea {...pathProps} />);
const path = wrapper.find('[data-testid="path0"]');
path.simulate('click');
expect(pathProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: false,
strokeWidth: 5,
}]);
});
}); });

View File

@ -1,32 +0,0 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import SnippingTool from '../src/renderer/components/snipping-tool';
import { ipcRenderer } from './__mocks__/electron';
describe('Snipping Tool', () => {
const snippingToolLabel = 'snipping-tool-data';
const onLabelEvent = 'on';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(SnippingTool));
expect(wrapper).toMatchSnapshot();
});
it('should call `snipping-tool-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(SnippingTool));
expect(spy).toBeCalledWith(snippingToolLabel, expect.any(Function));
});
it('should render pen color picker when clicked on pen', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="pen-button"]').simulate('click');
expect(wrapper.find('[data-testid="pen-colorpicker"]').exists()).toBe(true);
});
it('should render highlight color picker when clicked on highlight', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="highlight-button"]').simulate('click');
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(true);
});
});

View File

@ -0,0 +1,67 @@
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import SnippingTool from '../src/renderer/components/snipping-tool';
import { ipcRenderer } from './__mocks__/electron';
const waitForPromisesToResolve = () => new Promise((resolve) => setTimeout(resolve));
describe('Snipping Tool', () => {
it('should render correctly', () => {
const wrapper = shallow(React.createElement(SnippingTool));
expect(wrapper).toMatchSnapshot();
});
it('should set up a "once" listener for snipping-tool-data event on mounting', () => {
const spy = jest.spyOn(ipcRenderer, 'once');
shallow(React.createElement(SnippingTool));
expect(spy).toBeCalledWith('snipping-tool-data', expect.any(Function));
});
it('should render pen color picker when clicked on pen', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="pen-button"]').simulate('click');
expect(wrapper.find('[data-testid="pen-colorpicker"]').exists()).toBe(true);
});
it('should render highlight color picker when clicked on highlight', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="highlight-button"]').simulate('click');
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(true);
});
it('should clear all paths when clicked on clear', () => {
const props = {
existingPaths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
};
const wrapper = mount(<SnippingTool {...props} />);
const annotateComponent = wrapper.find('[data-testid="annotate-component"]');
wrapper.find('[data-testid="clear-button"]').simulate('click');
wrapper.update();
expect(annotateComponent.prop('paths')).toEqual(
[{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: false,
strokeWidth: 5,
}]);
});
it('should send upload-snippet event with correct data when clicked on done', async () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const wrapper = mount(<SnippingTool />);
wrapper.find('[data-testid="done-button"]').simulate('click');
wrapper.update();
await waitForPromisesToResolve();
expect(spy).toBeCalledWith('upload-snippet', {
base64PngData: 'NO CANVAS',
screenSnippetPath: 'Screen-Snippet',
});
});
});

View File

@ -230,20 +230,28 @@ class ScreenSnippet {
} }
// remove tmp file (async) // remove tmp file (async)
if (this.outputFileName) { if (this.outputFileName) {
fs.unlink(this.outputFileName, (removeErr) => { this.deleteFile(this.outputFileName);
logger.info(
`screen-snippet-handler: cleaning up temp snippet file: ${this.outputFileName}!`,
);
if (removeErr) {
logger.error(
`screen-snippet-handler: error removing temp snippet file: ${this.outputFileName}, err: ${removeErr}`,
);
}
});
} }
} }
} }
/**
* Deletes a locally stored file
* @param filePath Path for the file to delete
*/
private deleteFile(filePath: string) {
fs.unlink(filePath, (removeErr) => {
logger.info(
`screen-snippet-handler: cleaning up temp snippet file: ${filePath}!`,
);
if (removeErr) {
logger.error(
`screen-snippet-handler: error removing temp snippet file: ${filePath}, err: ${removeErr}`,
);
}
});
}
/** /**
* Verify and updates always on top * Verify and updates always on top
*/ */
@ -279,20 +287,19 @@ class ScreenSnippet {
* @param webContents A browser window's web contents object * @param webContents A browser window's web contents object
*/ */
private uploadSnippet(webContents: Electron.webContents) { private uploadSnippet(webContents: Electron.webContents) {
ipcMain.once('upload-snippet', async (_event, snipImage: string) => { ipcMain.once('upload-snippet', async (_event, snippetData: { screenSnippetPath: string, base64PngData: string }) => {
windowHandler.closeSnippingToolWindow(); windowHandler.closeSnippingToolWindow();
if (snipImage) { const [type, data] = snippetData.base64PngData.split(',');
this.outputFileName = snipImage; const payload = {
} message: 'SUCCESS',
const {
message,
data, data,
type, type,
}: IScreenSnippet = await this.convertFileToData(); };
this.deleteFile(snippetData.screenSnippetPath);
logger.info( logger.info(
`screen-snippet-handler: Snippet captured! Sending data to SFE`, `screen-snippet-handler: Snippet captured! Sending data to SFE`,
); );
webContents.send('screen-snippet-data', { message, data, type }); webContents.send('screen-snippet-data', payload);
await this.verifyAndUpdateAlwaysOnTop(); await this.verifyAndUpdateAlwaysOnTop();
}); });
} }

View File

@ -33,12 +33,28 @@ const MIN_ANNOTATE_AREA_WIDTH = 312;
const PEN_WIDTH = 5; const PEN_WIDTH = 5;
const HIGHLIGHT_WIDTH = 28; const HIGHLIGHT_WIDTH = 28;
const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, highlightColor, penColor, onChange, imageDimensions, chosenTool, screenSnippetPath }) => { const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
paths,
highlightColor,
penColor,
onChange,
imageDimensions,
chosenTool,
screenSnippetPath,
}) => {
const [isDrawing, setIsDrawing] = useState(false); const [isDrawing, setIsDrawing] = useState(false);
const maybeErasePath = (key: string) => { const maybeErasePath = (key: string) => {
// erase logic here if (chosenTool === Tool.eraser) {
return key; const updPaths = [...paths];
updPaths.map((p) => {
if (p && p.key === key) {
p.shouldShow = false;
}
return p;
});
onChange(updPaths);
}
}; };
const stopDrawing = () => { const stopDrawing = () => {
@ -59,10 +75,12 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
// Render and preparing render functions // Render and preparing render functions
const addHighlightPoint = (paths: IPath[], point: IPoint) => { const addHighlightPoint = (paths: IPath[], point: IPoint) => {
const activePath = paths[paths.length - 1]; if (isDrawing) {
const shouldShow = true; const activePath = paths[paths.length - 1];
const key = 'path' + paths.length; activePath.points.push(point);
if (!isDrawing) { } else {
const shouldShow = true;
const key = 'path' + paths.length;
paths.push({ paths.push({
points: [point], points: [point],
color: highlightColor, color: highlightColor,
@ -70,17 +88,17 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
shouldShow, shouldShow,
key, key,
}); });
} else {
activePath.points.push(point);
} }
return paths; return paths;
}; };
const addPenPoint = (paths: IPath[], point: IPoint) => { const addPenPoint = (paths: IPath[], point: IPoint) => {
const activePath = paths[paths.length - 1]; if (isDrawing) {
const shouldShow = true; const activePath = paths[paths.length - 1];
const key = 'path' + paths.length; activePath.points.push(point);
if (!isDrawing) { } else {
const shouldShow = true;
const key = 'path' + paths.length;
paths.push({ paths.push({
points: [point], points: [point],
color: penColor, color: penColor,
@ -88,21 +106,19 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
shouldShow, shouldShow,
key, key,
}); });
} else {
activePath.points.push(point);
} }
return paths; return paths;
}; };
const addPathPoint = (e: React.MouseEvent) => { const addPathPoint = (e: React.MouseEvent) => {
const p = [...paths]; const p = [...paths];
const mousePos = getMousePosition(e); const mousePos: IPoint = getMousePosition(e);
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
if (chosenTool === Tool.highlight) { if (chosenTool === Tool.highlight) {
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
onChange(addHighlightPoint(p, point)); onChange(addHighlightPoint(p, point));
} else { } else {
onChange(addPenPoint(p, point)); onChange(addPenPoint(p, mousePos));
} }
if (!isDrawing) { if (!isDrawing) {
setIsDrawing(true); setIsDrawing(true);
@ -204,7 +220,7 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
<svg <svg
data-testid='annotate-area' data-testid='annotate-area'
style={{ cursor: 'crosshair' }} style={{ cursor: 'crosshair' }}
id='annotate' id='annotate-area'
width={imageDimensions.width} width={imageDimensions.width}
height={imageDimensions.height} height={imageDimensions.height}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}

View File

@ -2,7 +2,7 @@ import * as React from 'react';
export interface IColorPickerPillProps { export interface IColorPickerPillProps {
availableColors: IColor[]; availableColors: IColor[];
onChange: (color: string) => void; onChange: (color: IColor) => void;
} }
export interface IColor { export interface IColor {
@ -32,7 +32,7 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
const widthAndHeight = getWidthAndHeight(); const widthAndHeight = getWidthAndHeight();
const chooseColor = () => { const chooseColor = () => {
props.onChange(color.rgbaColor); props.onChange(color);
}; };
return ( return (

View File

@ -38,12 +38,16 @@ export interface IImageDimensions {
height: number; height: number;
} }
export interface ISnippingToolProps {
existingPaths?: IPath[];
}
const availablePenColors: IColor[] = [ const availablePenColors: IColor[] = [
{ rgbaColor: 'rgba(0, 0, 40, 1)' }, { rgbaColor: 'rgba(0, 0, 40, 1)' },
{ rgbaColor: 'rgba(0, 142, 255, 1)' }, { rgbaColor: 'rgba(0, 142, 255, 1)' },
{ rgbaColor: 'rgba(38, 196, 58, 1)' }, { rgbaColor: 'rgba(38, 196, 58, 1)' },
{ rgbaColor: 'rgba(246, 178, 2, 1)' }, { rgbaColor: 'rgba(246, 178, 2, 1)' },
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' }, { rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(207, 208, 210, 1)' },
]; ];
const availableHighlightColors: IColor[] = [ const availableHighlightColors: IColor[] = [
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' }, { rgbaColor: 'rgba(0, 142, 255, 0.64)' },
@ -53,7 +57,7 @@ const availableHighlightColors: IColor[] = [
]; ];
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet'; const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
const SnippingTool = () => { const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPaths }) => {
// State preparation functions // State preparation functions
const [screenSnippetPath, setScreenSnippetPath] = useState('Screen-Snippet'); const [screenSnippetPath, setScreenSnippetPath] = useState('Screen-Snippet');
@ -61,11 +65,11 @@ const SnippingTool = () => {
height: 600, height: 600,
width: 800, width: 800,
}); });
const [paths, setPaths] = useState<IPath[]>([]); const [paths, setPaths] = useState<IPath[]>(existingPaths || []);
const [chosenTool, setChosenTool] = useState(Tool.pen); const [chosenTool, setChosenTool] = useState(Tool.pen);
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)'); const [penColor, setPenColor] = useState<IColor>({ rgbaColor: 'rgba(0, 142, 255, 1)' });
const [highlightColor, setHighlightColor] = useState( const [highlightColor, setHighlightColor] = useState<IColor>(
'rgba(0, 142, 255, 0.64)', { rgbaColor: 'rgba(0, 142, 255, 0.64)' },
); );
const [ const [
shouldRenderHighlightColorPicker, shouldRenderHighlightColorPicker,
@ -80,15 +84,9 @@ const SnippingTool = () => {
setImageDimensions({ height, width }); setImageDimensions({ height, width });
}; };
ipcRenderer.on('snipping-tool-data', getSnipImageData); ipcRenderer.once('snipping-tool-data', getSnipImageData);
useEffect(() => { // Hook that alerts clicks outside of the passed refs
return () => {
ipcRenderer.removeListener('snipping-tool-data', getSnipImageData);
};
}, []);
// Hook that alerts clicks outside of the passed ref
const useClickOutsideExaminer = ( const useClickOutsideExaminer = (
colorPickerRf: React.RefObject<HTMLDivElement>, colorPickerRf: React.RefObject<HTMLDivElement>,
penRf: React.RefObject<HTMLButtonElement>, penRf: React.RefObject<HTMLButtonElement>,
@ -120,12 +118,12 @@ const SnippingTool = () => {
// State mutating functions // State mutating functions
const penColorChosen = (color: string) => { const penColorChosen = (color: IColor) => {
setPenColor(color); setPenColor(color);
setShouldRenderPenColorPicker(false); setShouldRenderPenColorPicker(false);
}; };
const highlightColorChosen = (color: string) => { const highlightColorChosen = (color: IColor) => {
setHighlightColor(color); setHighlightColor(color);
setShouldRenderHighlightColorPicker(false); setShouldRenderHighlightColorPicker(false);
}; };
@ -147,11 +145,58 @@ const SnippingTool = () => {
}; };
const clear = () => { const clear = () => {
// Clear logic here const updPaths = [...paths];
updPaths.map((p) => {
p.shouldShow = false;
return p;
});
setPaths(updPaths);
}; };
// Utility functions // Utility functions
const getBase64PngData = () => {
const canvas = document.createElement('canvas');
canvas.width = imageDimensions.width;
canvas.height = imageDimensions.height;
// Creates an in memory canvas for mounting img data without adding it to the DOM
const ctx = canvas?.getContext('2d') as CanvasRenderingContext2D;
if (!ctx) {
// Will only be the case in headless browsers, such as with unit tests
return 'NO CANVAS';
}
// Creates an in memory img without adding it to the DOM
const img = document.createElement('img');
const svg = document.getElementById('annotate-area') as HTMLImageElement;
// Parses SVG image to XML data
const svgData = new XMLSerializer().serializeToString(svg);
// Adds the extracted XML data to the in memory img
img.setAttribute(
'src',
'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))),
);
const screenSnippet = document.getElementById('screenSnippet') as HTMLImageElement;
return new Promise((resolve, reject) => {
// Listens to when the img is loaded in memory and adds the data from the SVG paths + screenSnippet to the canvas
img.onload = () => {
ctx.drawImage(screenSnippet, 0, 0);
ctx.drawImage(img, 0, 0);
try {
// Extracts base 64 png img data from the canvas
const data = canvas.toDataURL('image/png');
resolve(data);
} catch (e) {
reject(e);
}
};
});
};
const markChosenColor = (colors: IColor[], chosenColor: string) => { const markChosenColor = (colors: IColor[], chosenColor: string) => {
return colors.map((color) => { return colors.map((color) => {
if (color.rgbaColor === chosenColor) { if (color.rgbaColor === chosenColor) {
@ -167,17 +212,21 @@ const SnippingTool = () => {
return undefined; return undefined;
} }
if (chosenTool === Tool.pen) { if (chosenTool === Tool.pen) {
return { border: '2px solid ' + penColor }; const color = penColor.outline ? penColor.outline : penColor.rgbaColor;
return { border: '2px solid ' + color };
} else if (chosenTool === Tool.highlight) { } else if (chosenTool === Tool.highlight) {
return { border: '2px solid ' + highlightColor }; const color = highlightColor.outline ? highlightColor.outline : highlightColor.rgbaColor;
return { border: '2px solid ' + color };
} else if (chosenTool === Tool.eraser) { } else if (chosenTool === Tool.eraser) {
return { border: '2px solid #008EFF' }; return { border: '2px solid #008EFF' };
} }
return undefined; return undefined;
}; };
const done = () => { const done = async () => {
ipcRenderer.send('upload-snippet', screenSnippetPath); const base64PngData = await getBase64PngData();
// const base64PngData = 'await getBase64PngData();';
ipcRenderer.send('upload-snippet', { screenSnippetPath, base64PngData });
}; };
return ( return (
@ -229,7 +278,7 @@ const SnippingTool = () => {
<div style={{ position: 'relative', left: '-50%' }}> <div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill <ColorPickerPill
data-testid='pen-colorpicker' data-testid='pen-colorpicker'
availableColors={markChosenColor(availablePenColors, penColor)} availableColors={markChosenColor(availablePenColors, penColor.rgbaColor)}
onChange={penColorChosen} onChange={penColorChosen}
/> />
</div> </div>
@ -244,7 +293,7 @@ const SnippingTool = () => {
data-testid='highlight-colorpicker' data-testid='highlight-colorpicker'
availableColors={markChosenColor( availableColors={markChosenColor(
availableHighlightColors, availableHighlightColors,
highlightColor, highlightColor.rgbaColor,
)} )}
onChange={highlightColorChosen} onChange={highlightColorChosen}
/> />
@ -256,9 +305,10 @@ const SnippingTool = () => {
<main> <main>
<div className='imageContainer'> <div className='imageContainer'>
<AnnotateArea <AnnotateArea
paths={paths} data-testid='annotate-component'
highlightColor={highlightColor} paths={paths}
penColor={penColor} highlightColor={highlightColor.rgbaColor}
penColor={penColor.rgbaColor}
onChange={setPaths} onChange={setPaths}
imageDimensions={imageDimensions} imageDimensions={imageDimensions}
screenSnippetPath={screenSnippetPath} screenSnippetPath={screenSnippetPath}