mirror of
https://github.com/finos/SymphonyElectron.git
synced 2024-12-27 01:11:13 -06:00
SDA-3931: Snipping tools improvement
This commit is contained in:
parent
65505c0ccd
commit
fad3656536
21
spec/__snapshots__/menu-button.spec.tsx.snap
Normal file
21
spec/__snapshots__/menu-button.spec.tsx.snap
Normal file
@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Menu Button should show all elements 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="menu-button-wrapper"
|
||||
>
|
||||
<button
|
||||
className="menu-button "
|
||||
data-testid="undefined_MENU_BUTTON"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<img
|
||||
alt="Open menu"
|
||||
src="../renderer/assets/single-chevron-down.svg"
|
||||
title="Open menu"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
@ -86,12 +86,38 @@ exports[`Snipping Tool should render correctly 1`] = `
|
||||
</main>
|
||||
<footer>
|
||||
<button
|
||||
className="done-button"
|
||||
className="close-button"
|
||||
data-testid="close-button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
className="add-to-chat-button"
|
||||
data-testid="done-button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Done
|
||||
Add to chat
|
||||
</button>
|
||||
<MenuButton
|
||||
id="snipping-tool"
|
||||
listItems={
|
||||
Array [
|
||||
Object {
|
||||
"dataTestId": "COPY_TO_CLIPBOARD",
|
||||
"event": "copy-to-clipboard",
|
||||
"name": "Copy to clipboard",
|
||||
"onClick": [Function],
|
||||
},
|
||||
Object {
|
||||
"dataTestId": "SAVE_AS",
|
||||
"event": "save-as",
|
||||
"name": "Save as",
|
||||
"onClick": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
|
91
spec/menu-button.spec.tsx
Normal file
91
spec/menu-button.spec.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { ScreenShotAnnotation } from '../src/common/ipcEvent';
|
||||
import MenuButton from '../src/renderer/components/menu-button';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Menu Button', () => {
|
||||
//#region Logic
|
||||
const waitForPromisesToResolve = () =>
|
||||
new Promise((resolve) => setTimeout(resolve));
|
||||
//#endregion
|
||||
|
||||
//#region Mock
|
||||
const copyToClipboardFn = jest.fn();
|
||||
const saveAsFn = jest.fn();
|
||||
copyToClipboardFn.mockImplementation((event) => {
|
||||
return event;
|
||||
});
|
||||
saveAsFn.mockImplementation((event) => {
|
||||
return event;
|
||||
});
|
||||
const menuItem = [
|
||||
{
|
||||
name: 'Copy to clipboard',
|
||||
event: ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||
onClick: copyToClipboardFn,
|
||||
dataTestId: 'COPY_TO_CLIPBOARD',
|
||||
},
|
||||
{
|
||||
name: 'Save as',
|
||||
event: ScreenShotAnnotation.SAVE_AS,
|
||||
onClick: saveAsFn,
|
||||
dataTestId: 'SAVE_AS',
|
||||
},
|
||||
];
|
||||
//#endregion
|
||||
|
||||
it('should show all elements', () => {
|
||||
const wrapper = shallow(React.createElement(MenuButton));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call event on click copy to clipboard', async () => {
|
||||
const wrapper = mount(
|
||||
<MenuButton id='snipping-tool' listItems={menuItem} />,
|
||||
);
|
||||
|
||||
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
wrapper
|
||||
.find('[data-testid="snipping-tool_COPY_TO_CLIPBOARD"]')
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
expect(copyToClipboardFn).toBeCalledWith(
|
||||
ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call event on click save as', async () => {
|
||||
const wrapper = mount(
|
||||
<MenuButton id='snipping-tool' listItems={menuItem} />,
|
||||
);
|
||||
|
||||
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
wrapper.find('[data-testid="snipping-tool_SAVE_AS"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
expect(saveAsFn).toBeCalledWith(ScreenShotAnnotation.SAVE_AS);
|
||||
});
|
||||
|
||||
it('should disappear on click to other element', async () => {
|
||||
const wrapper = mount(
|
||||
<MenuButton id='snipping-tool' listItems={menuItem} />,
|
||||
);
|
||||
|
||||
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
expect(wrapper.find('.menu').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
@ -1,5 +1,14 @@
|
||||
jest.mock('save-svg-as-png', function () {
|
||||
return {
|
||||
svgAsPngUri: async function (svg) {
|
||||
return Promise.resolve(svg);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { ScreenShotAnnotation } from '../src/common/ipcEvent';
|
||||
import SnippingTool from '../src/renderer/components/snipping-tool';
|
||||
import { ipcRenderer } from './__mocks__/electron';
|
||||
|
||||
@ -112,4 +121,61 @@ describe('Snipping Tool', () => {
|
||||
screenSnippetPath: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send upload-snippet event with correct data when clicked on copy to clipboard', async () => {
|
||||
const wrapper = mount(<SnippingTool />);
|
||||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
jest.spyOn(document, 'getElementById').mockImplementation((selector) => {
|
||||
switch (selector) {
|
||||
case 'annotate-area':
|
||||
return '123';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
wrapper
|
||||
.find('[data-testid="snipping-tool_COPY_TO_CLIPBOARD"]')
|
||||
.simulate('click');
|
||||
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
expect(spy).toBeCalledWith(ScreenShotAnnotation.COPY_TO_CLIPBOARD, {
|
||||
clipboard: '123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send upload-snippet event with correct data when clicked on save as', async () => {
|
||||
const wrapper = mount(<SnippingTool />);
|
||||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
jest.spyOn(document, 'getElementById').mockImplementation((selector) => {
|
||||
switch (selector) {
|
||||
case 'annotate-area':
|
||||
return '123';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
wrapper.find('[data-testid="snipping-tool_SAVE_AS"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
expect(spy).toBeCalledWith(ScreenShotAnnotation.SAVE_AS, {
|
||||
clipboard: '123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send upload-snippet event with correct data when clicked on close', async () => {
|
||||
const wrapper = mount(<SnippingTool />);
|
||||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
wrapper.find('[data-testid="close-button"]').simulate('click');
|
||||
wrapper.update();
|
||||
await waitForPromisesToResolve();
|
||||
expect(spy).toBeCalledWith(ScreenShotAnnotation.CLOSE);
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
clipboard,
|
||||
dialog,
|
||||
ipcMain,
|
||||
nativeImage,
|
||||
WebContents,
|
||||
@ -20,6 +22,7 @@ import {
|
||||
isWindowsOS,
|
||||
} from '../common/env';
|
||||
import { i18n } from '../common/i18n';
|
||||
import { ScreenShotAnnotation } from '../common/ipcEvent';
|
||||
import { logger } from '../common/logger';
|
||||
import {
|
||||
analytics,
|
||||
@ -27,11 +30,18 @@ import {
|
||||
ScreenSnippetActionTypes,
|
||||
} from './analytics-handler';
|
||||
import { updateAlwaysOnTop } from './window-actions';
|
||||
import { windowHandler } from './window-handler';
|
||||
import { ICustomBrowserWindow, windowHandler } from './window-handler';
|
||||
import { windowExists } from './window-utils';
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
|
||||
export interface IListItem {
|
||||
name: string;
|
||||
event: string;
|
||||
dataTestId: string;
|
||||
onClick: (eventName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
class ScreenSnippet {
|
||||
private readonly tempDir: string;
|
||||
private outputFilePath: string | undefined;
|
||||
@ -161,8 +171,18 @@ class ScreenSnippet {
|
||||
}
|
||||
|
||||
windowHandler.closeSnippingToolWindow();
|
||||
windowHandler.createSnippingToolWindow(this.outputFilePath, dimensions);
|
||||
const windowName = this.focusedWindow
|
||||
? (this.focusedWindow as ICustomBrowserWindow).winName
|
||||
: '';
|
||||
windowHandler.createSnippingToolWindow(
|
||||
this.outputFilePath,
|
||||
dimensions,
|
||||
windowName,
|
||||
);
|
||||
this.uploadSnippet(webContents);
|
||||
this.closeSnippet();
|
||||
this.copyToClipboard();
|
||||
this.saveAs();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
@ -194,7 +214,6 @@ class ScreenSnippet {
|
||||
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
try {
|
||||
await this.execCmd(this.captureUtil, []);
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
} catch (error) {
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
@ -343,6 +362,124 @@ class ScreenSnippet {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current snippet
|
||||
*/
|
||||
private closeSnippet() {
|
||||
ipcMain.once(ScreenShotAnnotation.CLOSE, async (_event) => {
|
||||
try {
|
||||
windowHandler.closeSnippingToolWindow();
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
} catch (error) {
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
logger.error(
|
||||
`screen-snippet-handler: close window failed with error: ${error}!`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a screen capture and closes the snippet window
|
||||
*/
|
||||
private copyToClipboard() {
|
||||
ipcMain.on(
|
||||
ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||
async (
|
||||
_event,
|
||||
copyToClipboardData: {
|
||||
action: string;
|
||||
clipboard: string;
|
||||
},
|
||||
) => {
|
||||
logger.info(`screen-snippet-handler: Copied!`);
|
||||
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
try {
|
||||
const [, data] = copyToClipboardData.clipboard.split(',');
|
||||
|
||||
const buffer = Buffer.from(data, 'base64');
|
||||
const img = nativeImage.createFromBuffer(buffer);
|
||||
|
||||
clipboard.writeImage(img);
|
||||
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
} catch (error) {
|
||||
await this.verifyAndUpdateAlwaysOnTop();
|
||||
logger.error(
|
||||
`screen-snippet-handler: cannot copy, failed with error: ${error}!`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger save modal to save the snippet
|
||||
*/
|
||||
private saveAs() {
|
||||
ipcMain.on(
|
||||
ScreenShotAnnotation.SAVE_AS,
|
||||
async (
|
||||
_event,
|
||||
saveAsData: {
|
||||
clipboard: string;
|
||||
},
|
||||
) => {
|
||||
const filePath = path.join(
|
||||
this.tempDir,
|
||||
'symphonyImage-' + Date.now() + '.png',
|
||||
);
|
||||
const [, data] = saveAsData.clipboard.split(',');
|
||||
const buffer = Buffer.from(data, 'base64');
|
||||
const img = nativeImage.createFromBuffer(buffer);
|
||||
|
||||
const dialogResult = await dialog
|
||||
.showSaveDialog(BrowserWindow.getFocusedWindow() as BrowserWindow, {
|
||||
title: 'Select place to store your file',
|
||||
defaultPath: filePath,
|
||||
// defaultPath: path.join(__dirname, '../assets/'),
|
||||
buttonLabel: 'Save',
|
||||
// Restricting the user to only Text Files.
|
||||
filters: [
|
||||
{
|
||||
name: 'Image file',
|
||||
extensions: ['png'],
|
||||
},
|
||||
],
|
||||
properties: [],
|
||||
})
|
||||
.then((file) => {
|
||||
// Stating whether dialog operation was cancelled or not.
|
||||
if (!file.canceled && file.filePath) {
|
||||
// Creating and Writing to the sample.txt file
|
||||
fs.writeFile(file.filePath.toString(), img.toPNG(), (err) => {
|
||||
if (err) {
|
||||
throw logger.error(
|
||||
`screen-snippet-handler: cannot save file, failed with error: ${err}!`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`screen-snippet-handler: modal save opened!`);
|
||||
});
|
||||
}
|
||||
|
||||
return file;
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(
|
||||
`screen-snippet-handler: cannot save file, failed with error: ${err}!`,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
});
|
||||
if (dialogResult?.filePath) {
|
||||
windowHandler.closeSnippingToolWindow();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const screenSnippet = new ScreenSnippet();
|
||||
|
@ -20,6 +20,7 @@ import { format, parse } from 'url';
|
||||
import { apiName, Themes, WindowTypes } from '../common/api-interface';
|
||||
import { isDevEnv, isLinux, isMac, isWindowsOS } from '../common/env';
|
||||
import { i18n, LocaleType } from '../common/i18n';
|
||||
import { ScreenShotAnnotation } from '../common/ipcEvent';
|
||||
import { logger } from '../common/logger';
|
||||
import {
|
||||
calculatePercentage,
|
||||
@ -1101,6 +1102,7 @@ export class WindowHandler {
|
||||
height: number;
|
||||
width: number;
|
||||
},
|
||||
windowName: string,
|
||||
): void {
|
||||
// Prevents creating multiple instances
|
||||
if (didVerifyAndRestoreWindow(this.snippingToolWindow)) {
|
||||
@ -1184,11 +1186,13 @@ export class WindowHandler {
|
||||
toolWidth = scaledImageDimensions.width;
|
||||
}
|
||||
|
||||
const selectedParentWindow = getWindowByName(windowName);
|
||||
const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts(
|
||||
{
|
||||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
modal: false,
|
||||
parent: selectedParentWindow,
|
||||
modal: true,
|
||||
alwaysOnTop: false,
|
||||
resizable: false,
|
||||
fullscreenable: false,
|
||||
@ -1270,6 +1274,9 @@ export class WindowHandler {
|
||||
logger.info(
|
||||
'window-handler, createSnippingToolWindow: Closing snipping window, attempting to delete temp snip image',
|
||||
);
|
||||
ipcMain.removeAllListeners(ScreenShotAnnotation.COPY_TO_CLIPBOARD);
|
||||
ipcMain.removeAllListeners(ScreenShotAnnotation.SAVE_AS);
|
||||
this.snippingToolWindow?.close();
|
||||
this.deleteFile(snipImage);
|
||||
this.removeWindow(opts.winKey);
|
||||
this.screenPickerWindow = null;
|
||||
|
5
src/common/ipcEvent.ts
Normal file
5
src/common/ipcEvent.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum ScreenShotAnnotation {
|
||||
COPY_TO_CLIPBOARD = 'copy-to-clipboard',
|
||||
SAVE_AS = 'save-as',
|
||||
CLOSE = 'close-snippet',
|
||||
}
|
@ -160,7 +160,11 @@
|
||||
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "Done",
|
||||
"Add to chat": "ADD TO CHAT",
|
||||
"Open menu": "Open menu",
|
||||
"Copy to clipboard": "Copy to clipboard",
|
||||
"Save as": "Save as",
|
||||
"Close": "Close",
|
||||
"Erase": "Erase",
|
||||
"Highlight": "Highlight",
|
||||
"Pen": "Pen",
|
||||
@ -236,4 +240,4 @@
|
||||
"Allow once (risky)": "Allow once (risky)",
|
||||
"Deny": "Deny",
|
||||
"Invalid security certificate": "has an invalid security certificate."
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,11 @@
|
||||
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
|
||||
},
|
||||
"ScreenSnippet": {
|
||||
"Done": "Done",
|
||||
"Add to chat": "Add to chat",
|
||||
"Open menu": "Open menu",
|
||||
"Copy to clipboard": "Copy to clipboard",
|
||||
"Save as": "Save as",
|
||||
"Close": "Close",
|
||||
"Erase": "Erase",
|
||||
"Highlight": "Highlight",
|
||||
"Pen": "Pen",
|
||||
@ -236,4 +240,4 @@
|
||||
"Allow once (risky)": "Allow once (risky)",
|
||||
"Deny": "Deny",
|
||||
"Invalid security certificate": "has an invalid security certificate."
|
||||
}
|
||||
}
|
||||
|
3
src/renderer/assets/single-chevron-down.svg
Normal file
3
src/renderer/assets/single-chevron-down.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.292893 0.792893C0.683417 0.402369 1.31658 0.402369 1.70711 0.792893L5 4.08579L8.29289 0.792893C8.68342 0.402369 9.31658 0.402369 9.70711 0.792893C10.0976 1.18342 10.0976 1.81658 9.70711 2.20711L5.70711 6.20711C5.31658 6.59763 4.68342 6.59763 4.29289 6.20711L0.292893 2.20711C-0.0976311 1.81658 -0.0976311 1.18342 0.292893 0.792893Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 461 B |
120
src/renderer/components/menu-button.tsx
Normal file
120
src/renderer/components/menu-button.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
// import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
import { IListItem } from '../../app/screen-snippet-handler';
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
interface IMenuButtonProps {
|
||||
listItems: IListItem[];
|
||||
id;
|
||||
}
|
||||
|
||||
const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
|
||||
listItems,
|
||||
id,
|
||||
}) => {
|
||||
//#region State
|
||||
const [isDisplay, setDisplay] = useState(false);
|
||||
const listRef = useRef<HTMLUListElement>(document.createElement('ul'));
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(
|
||||
document.createElement('button'),
|
||||
);
|
||||
//#endregion
|
||||
|
||||
//#region Variables
|
||||
const testId = {
|
||||
menu: `${id}_MENU_BUTTON`,
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Handlers
|
||||
const onClickMenuButton = () => {
|
||||
setDisplay(!isDisplay);
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region UseEffect
|
||||
useEffect(() => {
|
||||
const isContainListElement = (ev) =>
|
||||
listRef.current && listRef.current.contains(ev.target as HTMLElement);
|
||||
const isContainMenuBtnElement = (ev) =>
|
||||
menuButtonRef.current &&
|
||||
menuButtonRef.current.contains(ev.target as HTMLElement);
|
||||
window.addEventListener('click', (ev: MouseEvent) => {
|
||||
if (!isContainListElement(ev) && !isContainMenuBtnElement(ev)) {
|
||||
setDisplay(false);
|
||||
}
|
||||
});
|
||||
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setDisplay(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', () => {
|
||||
if (!isContainListElement || !isContainMenuBtnElement) {
|
||||
setDisplay(false);
|
||||
}
|
||||
});
|
||||
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setDisplay(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
//#endregion
|
||||
|
||||
const renderListItems = () => {
|
||||
return listItems.map((listItem) => {
|
||||
const sendClick = async () => {
|
||||
await listItem.onClick(listItem.event);
|
||||
setDisplay(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
data-testid={`${id}_${listItem.dataTestId}`}
|
||||
className='list-item general-font'
|
||||
lang={i18n.getLocale()}
|
||||
onClick={sendClick}
|
||||
key={listItem.event}
|
||||
>
|
||||
{listItem.name}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const focusCls = isDisplay ? 'menu-button-focus' : '';
|
||||
|
||||
//#endregion
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='menu-button-wrapper'>
|
||||
<button
|
||||
className={`menu-button ${focusCls}`}
|
||||
onClick={onClickMenuButton}
|
||||
data-testid={testId.menu}
|
||||
ref={menuButtonRef}
|
||||
>
|
||||
<img
|
||||
src={`../renderer/assets/single-chevron-down.svg`}
|
||||
title={i18n.t('Open menu')()}
|
||||
alt={i18n.t('Open menu')()}
|
||||
/>
|
||||
</button>
|
||||
{isDisplay && (
|
||||
<ul className='menu' ref={listRef}>
|
||||
{renderListItems()}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuButton;
|
@ -2,12 +2,14 @@ import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
import { svgAsPngUri } from 'save-svg-as-png';
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
import { ScreenShotAnnotation } from '../../common/ipcEvent';
|
||||
import {
|
||||
AnalyticsElements,
|
||||
ScreenSnippetActionTypes,
|
||||
} from './../../app/analytics-handler';
|
||||
import AnnotateArea from './annotate-area';
|
||||
import ColorPickerPill, { IColor } from './color-picker-pill';
|
||||
import MenuButton from './menu-button';
|
||||
|
||||
const { useState, useRef, useEffect, useLayoutEffect } = React;
|
||||
|
||||
@ -92,6 +94,42 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
||||
false,
|
||||
);
|
||||
|
||||
const mergeImage = async () => {
|
||||
const svg = document.getElementById('annotate-area');
|
||||
const mergedImageData = svg ? await svgAsPngUri(svg, {}) : 'MERGE_FAIL';
|
||||
|
||||
return mergedImageData;
|
||||
};
|
||||
|
||||
const onCopyToClipboard = async (eventName) => {
|
||||
const img = await mergeImage();
|
||||
ipcRenderer.send(eventName, {
|
||||
clipboard: img,
|
||||
});
|
||||
};
|
||||
|
||||
const onSaveAs = async (eventName) => {
|
||||
const img = await mergeImage();
|
||||
ipcRenderer.send(eventName, {
|
||||
clipboard: img,
|
||||
});
|
||||
};
|
||||
|
||||
const menuItem = [
|
||||
{
|
||||
name: i18n.t('Copy to clipboard', SNIPPING_TOOL_NAMESPACE)(),
|
||||
event: ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||
onClick: onCopyToClipboard,
|
||||
dataTestId: 'COPY_TO_CLIPBOARD',
|
||||
},
|
||||
{
|
||||
name: i18n.t('Save as', SNIPPING_TOOL_NAMESPACE)(),
|
||||
event: ScreenShotAnnotation.SAVE_AS,
|
||||
onClick: onSaveAs,
|
||||
dataTestId: 'SAVE_AS',
|
||||
},
|
||||
];
|
||||
|
||||
const getSnipImageData = (
|
||||
{},
|
||||
{
|
||||
@ -225,7 +263,7 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const done = async () => {
|
||||
const addToChat = async () => {
|
||||
const svg = document.getElementById('annotate-area');
|
||||
const mergedImageData = svg
|
||||
? await svgAsPngUri(document.getElementById('annotate-area'), {})
|
||||
@ -237,6 +275,10 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
||||
ipcRenderer.send('upload-snippet', { screenSnippetPath, mergedImageData });
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
ipcRenderer.send(ScreenShotAnnotation.CLOSE);
|
||||
};
|
||||
|
||||
// Removes focus styling from buttons when mouse is clicked
|
||||
document.body.addEventListener('mousedown', () => {
|
||||
document.body.classList.add('using-mouse');
|
||||
@ -350,12 +392,20 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
||||
</main>
|
||||
<footer>
|
||||
<button
|
||||
data-testid='done-button'
|
||||
className='done-button'
|
||||
onClick={done}
|
||||
data-testid='close-button'
|
||||
className='close-button'
|
||||
onClick={onClose}
|
||||
>
|
||||
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
|
||||
{i18n.t('Close', SNIPPING_TOOL_NAMESPACE)()}
|
||||
</button>
|
||||
<button
|
||||
data-testid='done-button'
|
||||
className='add-to-chat-button'
|
||||
onClick={addToChat}
|
||||
>
|
||||
{i18n.t('Add to chat', SNIPPING_TOOL_NAMESPACE)()}
|
||||
</button>
|
||||
<MenuButton id='snipping-tool' listItems={menuItem}></MenuButton>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
@ -124,18 +124,42 @@ body.using-mouse :focus {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 24px;
|
||||
|
||||
.done-button {
|
||||
.add-to-chat-button {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background-color: #008eff;
|
||||
color: white;
|
||||
background-color: @electricity-ui-50;
|
||||
color: @vanilla-white;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
margin-right: 32px;
|
||||
border-radius: 16px 0px 0px 16px;
|
||||
height: 32px;
|
||||
width: 80px;
|
||||
width: 102px;
|
||||
}
|
||||
|
||||
.add-to-chat-button:hover {
|
||||
background-color: @electricity-ui-60;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
padding: 8px 16px;
|
||||
width: 67px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
font-family: @font-family;
|
||||
color: @graphite-50;
|
||||
text-transform: uppercase;
|
||||
margin-right: 16px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,3 +196,73 @@ body.using-mouse :focus {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 8px 8px 6px;
|
||||
width: 30px;
|
||||
height: 32px;
|
||||
background: @electricity-ui-50;
|
||||
border-radius: 0px 16px 16px 0px;
|
||||
margin-left: 1px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-button:focus {
|
||||
background: @electricity-ui-60;
|
||||
}
|
||||
|
||||
.menu-button-focus {
|
||||
background: @electricity-ui-60;
|
||||
}
|
||||
|
||||
.chevron-down {
|
||||
color: @text-color-primary;
|
||||
}
|
||||
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
top: -85px;
|
||||
left: -113px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
position: absolute;
|
||||
width: 143px;
|
||||
min-height: 80px;
|
||||
background: @vanilla-white;
|
||||
box-shadow: 0px 2px 4px rgba(5, 6, 6, 0.08), 0px 12px 28px rgba(5, 6, 6, 0.16);
|
||||
border-radius: 8px;
|
||||
margin: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-button-wrapper {
|
||||
position: relative;
|
||||
margin-right: 32px;
|
||||
}
|
||||
|
||||
.general-font {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
line-height: 20px;
|
||||
color: @graphite-80;
|
||||
font-family: @font-family;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.general-font:lang(ja-JP) {
|
||||
font-family: @font-family-ja;
|
||||
}
|
||||
|
@ -9,9 +9,14 @@
|
||||
@electricity-ui-05: #e9f2f9;
|
||||
@electricity-ui-50: #0277d6;
|
||||
@electricity-ui-30: #6eb9fd;
|
||||
@electricity-ui-60: #27588e;
|
||||
|
||||
@graphite-20: #cdcfd4;
|
||||
@graphite-05: #f1f1f3;
|
||||
@graphite-80: #27292c;
|
||||
@graphite-30: #b0b3ba;
|
||||
@graphite-40: #8f959e;
|
||||
@graphite-90: #141618;
|
||||
@graphite-50: #717681;
|
||||
|
||||
@vanilla-white: #fff;
|
||||
|
Loading…
Reference in New Issue
Block a user