Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(codeeditor): New Code editor component #3830

Merged
merged 15 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@
"@carbon/pictograms-react": "11.25.0",
"@carbon/telemetry": "^0.1.0",
"@carbon/themes": "10.54.0",
"@monaco-editor/react": "^3.6.2",
"@monaco-editor/react": "4.4.5",
"carbon-components": "10.56.0",
"carbon-components-react": "7.56.0",
"carbon-icons": "^7.0.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,12 @@ const CardCodeEditor = ({

/**
*
* @param {func} _editorValue - a method that returns the current value of the editor
* @param {object} val - instance of the monaco editor
* @param {object} editor - instance of the editor
* @param {object} _monaco - instance of monaco
*/
const handleEditorDidMount = (_editorValue, val) => {
editorValue.current = val;
// eslint-disable-next-line no-unused-vars
const handleEditorDidMount = (editor, _monaco) => {
editorValue.current = editor;
};

const handleOnSubmit = () => {
Expand Down Expand Up @@ -176,7 +177,7 @@ const CardCodeEditor = ({
value={initialValue}
line={2}
language={language}
editorDidMount={handleEditorDidMount}
onMount={handleEditorDidMount}
options={{
minimap: {
enabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('CardCodeEditor loaded editor test', () => {
);
cy.get('head')
.find(
'[src="https://cdn.jsdelivr.net/npm/monaco-editor@0.20.0/min/vs/editor/editor.main.js"]'
'[src="https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs/editor/editor.main.js"]'
)
.should('exist');
});
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('CardCodeEditor loaded editor test', () => {

// This component throws a network error with too many calls to the cdn script it loads so adding snapshot to existing instance
onlyOn('headless', () => {
cy.findByTestId('ComposedModal').compareSnapshot('CardCodeEditor');
cy.findByTestId('ComposedModal').compareSnapshot('CardCodeEditor', 0.2);
herleraja marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ exports[`Storybook Snapshot tests and console checks Storyshots 2 - Watson IoT E
</span>
</button>
<section
className="iot--editor-wrapper"
style={
Object {
"display": "flex",
Expand Down Expand Up @@ -402,7 +401,6 @@ exports[`Storybook Snapshot tests and console checks Storyshots 2 - Watson IoT E
</span>
</button>
<section
className="iot--editor-wrapper"
style={
Object {
"display": "flex",
Expand Down
217 changes: 217 additions & 0 deletions packages/react/src/components/CodeEditor/CodeEditor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import React, { useRef, useMemo, useState, useEffect } from 'react';
import Editor from '@monaco-editor/react';
import { CodeSnippetSkeleton, CopyButton, Button } from 'carbon-components-react';
import PropTypes from 'prop-types';
import { Upload16 } from '@carbon/icons-react';
import classnames from 'classnames';

import { settings } from '../../constants/Settings';

const { prefix: carbonPrefix, iotPrefix } = settings;

export const updateEditorAttribute = (disabled, editorValue) => {
const textarea = document.getElementsByClassName('inputarea monaco-mouse-cursor-text')[0];

if (disabled && !!editorValue.current) {
textarea.setAttribute('disabled', '');
} else if (textarea?.hasAttribute('disabled')) {
textarea.removeAttribute('disabled');
}
};

const propTypes = {
/** Initial value for the editor */
initialValue: PropTypes.string,
/** the language being written in the editor */
language: PropTypes.string,
/** Callback called when editor copy icon is pressed */
onCopy: PropTypes.func,
/** List of types that the upload input accept */
accept: PropTypes.arrayOf(PropTypes.string),
/** Light theme */
light: PropTypes.bool,
/** Boolean to define if upload button should be render or not */
hasUpload: PropTypes.bool,
/** Callback on code editor change */
onCodeEditorChange: PropTypes.func,
/** Boolean to disabled code editor */
disabled: PropTypes.bool,
/** All the labels that need translation */
i18n: PropTypes.shape({
copyBtnDescription: PropTypes.string,
copyBtnFeedBack: PropTypes.string,
uploadBtnDescription: PropTypes.string,
}),
testId: PropTypes.string,
};

const defaultProps = {
initialValue: null,
language: 'css',
onCopy: null,
accept: ['.css'],
light: false,
hasUpload: false,
onCodeEditorChange: null,
disabled: false,
i18n: {
copyBtnDescription: 'Copy content',
copyBtnFeedBack: 'Copied',
uploadBtnDescription: 'Upload your file',
},
testId: 'code-editor',
};
const CodeEditor = ({
initialValue,
language,
onCopy,
accept,
hasUpload,
onCodeEditorChange,
light,
disabled,
i18n,
testId,
}) => {
const mergedI18n = useMemo(() => ({ ...defaultProps.i18n, ...i18n }), [i18n]);

const editorValue = useRef();
const inputNode = useRef(null);

const [codeEditorValue, setCodeEditorValue] = useState(initialValue);

useEffect(() => {
updateEditorAttribute(disabled, editorValue);

const button = document.getElementsByClassName('iot--code-editor-copy')[0];

if (disabled) {
button.setAttribute('disabled', '');
} else if (button?.hasAttribute('disabled')) {
button.removeAttribute('disabled');
}
}, [disabled]);

/**
*
* @param {object} editor - instance of the editor
* @param {object} _monaco - instance of monaco
*/
// eslint-disable-next-line no-unused-vars
const handleEditorDidMount = (editor, _monaco) => {
editorValue.current = editor;
updateEditorAttribute(disabled, editorValue);
};

/**
* Function used in editor onChange
* @param {*} editorVal editor value
*/
const handleEditorChange = (editorVal) => {
setCodeEditorValue(editorVal);
if (onCodeEditorChange) {
onCodeEditorChange(editorVal);
}
};

/**
* Handle copy button click
* @returns current editor value
*/
const handleOnCopy = () => onCopy && onCopy(codeEditorValue);

/**
* takes a array of File javascript objects https://developer.mozilla.org/en-US/docs/Web/API/File
* and creates a FileReader and actually calls the readAsText method to trigger the loading of the file.
*/
const readFileContent = (files) => {
const fileReader = new FileReader();
fileReader.onload = (event) => {
setCodeEditorValue(event.target.result);
if (onCodeEditorChange) {
onCodeEditorChange(event.target.result);
}
};
fileReader.readAsText(files[0]);
};

const handleOnChange = (event) => {
event.stopPropagation();
readFileContent(event.target.files);
};

return (
<div data-testid={testId} className={`${iotPrefix}--code-editor-wrapper`}>
{hasUpload ? (
<>
<Button
className={classnames(`${iotPrefix}--code-editor-upload`, {
[`${iotPrefix}--code-editor-upload--light`]: light,
[`${iotPrefix}--code-editor-upload--disabled`]: disabled,
})}
hasIconOnly
iconDescription={mergedI18n.uploadBtnDescription}
onClick={() => {
if (inputNode.current) {
inputNode.current.value = '';
inputNode.current.click();
}
}}
renderIcon={Upload16}
kind="ghost"
size="field"
data-testid={`${testId}-upload-button`}
disabled={disabled}
/>
<input
className={`${carbonPrefix}--visually-hidden`}
ref={inputNode}
id="upload"
type="file"
tabIndex={-1}
multiple={false}
accept={accept}
name="upload"
onChange={handleOnChange}
disabled={disabled}
/>
</>
) : null}
{onCopy && (
<CopyButton
className={classnames(`${iotPrefix}--code-editor-copy`, {
[`${iotPrefix}--code-editor-copy--light`]: light,
[`${iotPrefix}--code-editor-copy--disabled`]: disabled,
})}
onClick={handleOnCopy}
iconDescription={mergedI18n.copyBtnDescription}
feedback={mergedI18n.copyBtnFeedBack}
data-testid={`${testId}-copy-button`}
/>
)}
<Editor
className={classnames(`${iotPrefix}--code-editor-container`, {
[`${iotPrefix}--code-editor-container--light`]: light,
[`${iotPrefix}--code-editor-container--disabled`]: disabled,
})}
wrapperClassName={`${iotPrefix}--code-editor-wrapper`}
loading={<CodeSnippetSkeleton />}
value={codeEditorValue}
line={2}
language={language}
onMount={handleEditorDidMount}
onChange={handleEditorChange}
options={{
minimap: {
enabled: false,
},
autoIndent: true,
wordWrap: 'off',
}}
/>
</div>
);
};
CodeEditor.propTypes = propTypes;
CodeEditor.defaultProps = defaultProps;
export default CodeEditor;
30 changes: 30 additions & 0 deletions packages/react/src/components/CodeEditor/CodeEditor.story.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { select, boolean, array } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';

import CodeEditor from './CodeEditor';

export default {
title: '1 - Watson IoT/CodeEditor',
parameters: {
component: CodeEditor,
docs: {
inlineStories: false,
},
},
};

export const Default = () => (
<CodeEditor
language={select('Editor language', ['css', 'json', 'javascript'])}
onCopy={action('onCopy')}
initialValue="/* write your code here */"
light={boolean('Light (light)', false)}
hasUpload={boolean('Has upload button (hasUpload)', true)}
accept={array('Accepted file types (accept)', ['.scss', '.css'], ',')}
onCodeEditorChange={action('onCodeEditorChange')}
disabled={boolean('Disabled state (disabled)', false)}
/>
);

Default.storyName = 'default';
Loading
Loading