diff --git a/package.json b/package.json index 090192c..8d99715 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,9 @@ "format": "prettier \"src/**/*.{ts, tsx}\" --check" }, "dependencies": { - "@elyra/application": "^1.0.0-beta.1", - "@elyra/ui-components": "^1.0.0-beta.1", "@jupyterlab/application": "^2.1.2", + "@jupyterlab/apputils": "^2.2.4", + "@jupyterlab/cells": "^2.2.4", "@jupyterlab/coreutils": "^4.1.0", "@jupyterlab/docmanager": "^2.1.2", "@jupyterlab/docregistry": "^2.1.2", @@ -67,7 +67,7 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.20.4", "husky": "^4.2.5", - "lint-staged": "^10.2.11", + "lint-staged": "^10.2.13", "mkdirp": "^1.0.3", "prettier": "^1.19.1", "rimraf": "^2.6.1", @@ -87,6 +87,7 @@ "lint-staged": { "*.{js,jsx,ts,tsx}": [ "eslint --cache --fix" - ] + ], + "*.js": "eslint --cache --fix" } } diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index fafe375..e762a0d 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -1,50 +1,63 @@ -import { FilterTools } from './FilterTools'; -import moreSVGstr from '../style/icon/jupyter_moreicon.svg'; -import { showPreview } from './PreviewSnippet'; -import { showMoreOptions } from './MoreOptions'; -import { - ICodeSnippet, - CodeSnippetContentsService -} from './CodeSnippetContentsService'; -import { find } from '@lumino/algorithm'; - import { Clipboard, Dialog, showDialog } from '@jupyterlab/apputils'; -import { CodeCell, MarkdownCell } from '@jupyterlab/cells'; -import { CodeEditor } from '@jupyterlab/codeeditor'; import { PathExt } from '@jupyterlab/coreutils'; import { DocumentWidget } from '@jupyterlab/docregistry'; import { FileEditor } from '@jupyterlab/fileeditor'; import { Notebook, NotebookPanel } from '@jupyterlab/notebook'; -import { /**copyIcon,*/ LabIcon, addIcon } from '@jupyterlab/ui-components'; -import { IEditorServices } from '@jupyterlab/codeeditor'; +import { LabIcon, addIcon } from '@jupyterlab/ui-components'; +import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor'; +import * as nbformat from '@jupyterlab/nbformat'; +import { JupyterFrontEnd } from '@jupyterlab/application'; +import { + Cell, + CodeCellModel, + ICodeCellModel, + MarkdownCell, + CodeCell +} from '@jupyterlab/cells'; import { Widget } from '@lumino/widgets'; - -import React from 'react'; +import { find } from '@lumino/algorithm'; import { Drag } from '@lumino/dragdrop'; -import { Cell, CodeCellModel, ICodeCellModel } from '@jupyterlab/cells'; import { MimeData } from '@lumino/coreutils'; -import * as nbformat from '@jupyterlab/nbformat'; +import React from 'react'; + import { CodeSnippetWidgetModel } from './CodeSnippetWidgetModel'; -import { JupyterFrontEnd } from '@jupyterlab/application'; +import { FilterTools } from './FilterTools'; +import moreSVGstr from '../style/icon/jupyter_moreicon.svg'; +import { showPreview } from './PreviewSnippet'; +import { showMoreOptions } from './MoreOptions'; +import { + ICodeSnippet, + CodeSnippetContentsService +} from './CodeSnippetContentsService'; -/** - * The class added to snippet cells - */ -// const CODE_SNIPPET_CELL_CLASS = 'jp-CodeSnippet-cell'; /** * The CSS class added to code snippet widget. */ -const CODE_SNIPPETS_HEADER_CLASS = 'codeSnippetsHeader'; -const CODE_SNIPPETS_CONTAINER = 'codeSnippetsContainer'; - -const DISPLAY_NAME_CLASS = 'expandableContainer-name'; -const ELYRA_BUTTON_CLASS = 'jp-button'; -const BUTTON_CLASS = 'expandableContainer-button'; -const TITLE_CLASS = 'expandableContainer-title'; -const ACTION_BUTTONS_WRAPPER_CLASS = 'expandableContainer-action-buttons'; -const ACTION_BUTTON_CLASS = 'expandableContainer-actionButton'; +const CODE_SNIPPETS_HEADER_CLASS = 'jp-codeSnippetsHeader'; +const CODE_SNIPPET_TITLE = 'jp-codeSnippet-title'; +const CODE_SNIPPETS_CONTAINER = 'jp-codeSnippetsContainer'; +const DISPLAY_NAME_CLASS = 'jp-codeSnippetsContainer-name'; +const BUTTON_CLASS = 'jp-codeSnippetsContainer-button'; +const TITLE_CLASS = 'jp-codeSnippetsContainer-title'; +const ACTION_BUTTONS_WRAPPER_CLASS = 'jp-codeSnippetsContainer-action-buttons'; +const ACTION_BUTTON_CLASS = 'jp-codeSnippetsContainer-actionButton'; +const SEARCH_BOLD = 'jp-codeSnippet-search-bolding'; +const SNIPPET_DRAG_IMAGE = 'jp-codeSnippet-drag-image'; +const CODE_SNIPPET_DRAG_HOVER = 'jp-codeSnippet-drag-hover'; +const CODE_SNIPPET_DRAG_HOVER_CLICKED = 'jp-codeSnippet-drag-hover-clicked'; +const CODE_SNIPPET_DRAG_HOVER_SELECTED = 'jp-codeSnippet-drag-hover-selected'; +const CODE_SNIPPET_METADATA = 'jp-codeSnippet-metadata'; +const CODE_SNIPPET_DESC = 'jp-codeSnippet-description'; +const CODE_SNIPPET_EDITOR = 'jp-codeSnippet-editor'; +const CODE_SNIPPET_MORE_OPTIONS = 'jp-codeSnippet-options'; +const CODE_SNIPPET_MORE_OTPIONS_CONTENT = 'jp-codeSnippet-more-options-content'; +const CODE_SNIPPET_MORE_OTPIONS_COPY = 'jp-codeSnippet-more-options-copy'; +const CODE_SNIPPET_MORE_OTPIONS_INSERT = 'jp-codeSnippet-more-options-insert'; +const CODE_SNIPPET_MORE_OTPIONS_EDIT = 'jp-codeSnippet-more-options-edit'; +const CODE_SNIPPET_MORE_OTPIONS_DELETE = 'jp-codeSnippet-more-options-delete'; +const CODE_SNIPPET_CREATE_NEW_BTN = 'jp-createSnippetBtn'; /** * The threshold in pixels to start a drag event. @@ -54,26 +67,14 @@ const DRAG_THRESHOLD = 5; /** * A class used to indicate a snippet item. */ -const CODE_SNIPPET_ITEM = 'codeSnippet-item'; +const CODE_SNIPPET_ITEM = 'jp-codeSnippet-item'; +const CODE_SNIPPET_ITEM_CLICKED = 'jp-codeSnippet-item-clicked'; /** * The mimetype used for Jupyter cell data. */ const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells'; -/** - * Icons used for snippet - */ -// const insertIcon = new LabIcon({ -// name: 'custom-ui-compnents:insert', -// svgstr: insertSVGstr -// }); - -// const launchEditorIcon = new LabIcon({ -// name: 'custom-ui-compnents:launchEditor', -// svgstr: launchEditorSVGstr -// }); - /** * Icon for more options */ @@ -131,7 +132,6 @@ export class CodeSnippetDisplay extends React.Component< const widget: Widget = this.props.getCurrentWidget(); const snippetStr: string = snippet.code.join('\n'); - // if the widget is document widget and it's a file?? in the file editor if ( widget instanceof DocumentWidget && (widget as DocumentWidget).content instanceof FileEditor @@ -179,14 +179,6 @@ export class CodeSnippetDisplay extends React.Component< } }; - // // Handle deleting code snippet - // private deleteCodeSnippet = async (snippet: ICodeSnippet): Promise => { - // const name = snippet.name; - // // const url = 'elyra/metadata/code-snippets/' + name; - - // this.props.openCodeSnippetEditor({ namespace: name, codeSnippet: snippet }); - // }; - // Handle language compatibility between code snippet and editor private verifyLanguageAndInsert = async ( snippet: ICodeSnippet, @@ -239,17 +231,17 @@ export class CodeSnippetDisplay extends React.Component< const _id: number = parseInt(id, 10); document - .getElementsByClassName('drag-hover') - [_id].classList.add('drag-hover-selected'); + .getElementsByClassName(CODE_SNIPPET_DRAG_HOVER) + [_id].classList.add(CODE_SNIPPET_DRAG_HOVER_SELECTED); }; // Remove 6 dots off hover private dragHoverStyleRemove = (id: string): void => { const _id: number = parseInt(id, 10); - if (document.getElementsByClassName('drag-hover')) { + if (document.getElementsByClassName(CODE_SNIPPET_DRAG_HOVER)) { document - .getElementsByClassName('drag-hover') - [_id].classList.remove('drag-hover-selected'); + .getElementsByClassName(CODE_SNIPPET_DRAG_HOVER) + [_id].classList.remove(CODE_SNIPPET_DRAG_HOVER_SELECTED); } }; @@ -259,16 +251,16 @@ export class CodeSnippetDisplay extends React.Component< if ( document - .getElementsByClassName('drag-hover') - [_id].classList.contains('drag-hover-clicked') + .getElementsByClassName(CODE_SNIPPET_DRAG_HOVER) + [_id].classList.contains(CODE_SNIPPET_DRAG_HOVER_CLICKED) ) { document - .getElementsByClassName('drag-hover') - [_id].classList.remove('drag-hover-clicked'); + .getElementsByClassName(CODE_SNIPPET_DRAG_HOVER) + [_id].classList.remove(CODE_SNIPPET_DRAG_HOVER_CLICKED); } else { document - .getElementsByClassName('drag-hover') - [_id].classList.add('drag-hover-clicked'); + .getElementsByClassName(CODE_SNIPPET_DRAG_HOVER) + [_id].classList.add(CODE_SNIPPET_DRAG_HOVER_CLICKED); } }; @@ -292,7 +284,7 @@ export class CodeSnippetDisplay extends React.Component< return ( {start} - {bolded} + {bolded} {end} ); @@ -311,7 +303,6 @@ export class CodeSnippetDisplay extends React.Component< } const target = event.target as HTMLElement; - console.log(target); this._dragData = { pressX: event.clientX, @@ -327,12 +318,11 @@ export class CodeSnippetDisplay extends React.Component< .children[0] as HTMLElement).style.color = dragImageTextColor; // add CSS style - this._dragData.dragImage.classList.add('jp-codesnippet-drag-image'); + this._dragData.dragImage.classList.add(SNIPPET_DRAG_IMAGE); target.addEventListener('mouseup', this._evtMouseUp, true); target.addEventListener('mousemove', this.handleDragMove, true); event.preventDefault(); - // event.stopPropagation(); } private _evtMouseUp(event: MouseEvent): void { @@ -347,8 +337,6 @@ export class CodeSnippetDisplay extends React.Component< } private handleDragMove(event: MouseEvent): void { - // event.preventDefault(); - // event.stopPropagation(); const data = this._dragData; if ( @@ -427,22 +415,22 @@ export class CodeSnippetDisplay extends React.Component< private _evtMouseLeave(): void { //get rid of preview by clicking anything - const preview = document.querySelector('.jp-preview'); + const preview = document.querySelector('.jp-codeSnippet-preview'); if (preview) { // if target is not the code snippet name area, then add inactive // if target area is the code snippet name area, previewSnippet widget will handle preview. if (!preview.classList.contains('inactive')) { preview.classList.add('inactive'); - for (const elem of document.getElementsByClassName('drag-hover')) { - if (elem.classList.contains('drag-hover-clicked')) { - elem.classList.remove('drag-hover-clicked'); + for (const elem of document.getElementsByClassName( + CODE_SNIPPET_DRAG_HOVER + )) { + if (elem.classList.contains(CODE_SNIPPET_DRAG_HOVER_CLICKED)) { + elem.classList.remove(CODE_SNIPPET_DRAG_HOVER_CLICKED); } } - for (const item of document.getElementsByClassName( - 'codeSnippet-item' - )) { - if (item.classList.contains('codeSnippet-item-clicked')) { - item.classList.remove('codeSnippet-item-clicked'); + for (const item of document.getElementsByClassName(CODE_SNIPPET_ITEM)) { + if (item.classList.contains(CODE_SNIPPET_ITEM_CLICKED)) { + item.classList.remove(CODE_SNIPPET_ITEM_CLICKED); } } } @@ -452,9 +440,7 @@ export class CodeSnippetDisplay extends React.Component< //Set the position of the preview to be next to the snippet title. private _setPreviewPosition(id: string): void { const intID = parseInt(id, 10); - const realTarget = document.getElementsByClassName( - 'expandableContainer-title' - )[intID]; + const realTarget = document.getElementsByClassName(TITLE_CLASS)[intID]; // distDown is the number of pixels to shift the preview down let distDown: number = realTarget.getBoundingClientRect().top - 40; if (realTarget.getBoundingClientRect().top > window.screen.height / 2) { @@ -497,25 +483,10 @@ export class CodeSnippetDisplay extends React.Component< codeSnippet: ICodeSnippet, id: string ): JSX.Element => { - const buttonClasses = [ELYRA_BUTTON_CLASS, BUTTON_CLASS].join(' '); + const buttonClasses = BUTTON_CLASS; const displayName = '[' + codeSnippet.language + '] ' + codeSnippet.name; - // const tags = codeSnippet.tags; const actionButtons = [ - // { - // title: 'Copy', - // icon: copyIcon, - // onClick: (): void => { - // Clipboard.copyToSystem(codeSnippet.code.join('\n')); - // } - // }, - // { - // title: 'Insert', - // icon: insertIcon, - // onClick: (): void => { - // this.insertCodeSnippet(codeSnippet); - // } - // }, { title: 'Insert, copy, edit, and delete', icon: moreOptionsIcon, @@ -526,22 +497,6 @@ export class CodeSnippetDisplay extends React.Component< this._setOptionsPosition(event); } } - // { - // title: 'Launch Editor', - // icon: launchEditorIcon, - // onClick: (): void => { - // // showPreview( - // // { - // // id: parseInt(id, 10), - // // title: displayName, - // // body: new PreviewHandler(codeSnippet), - // // codeSnippet: codeSnippet - // // } - // // ); - // this.props.openCodeSnippetEditor(codeSnippet); - // // this.snippetClicked(id); - // } - // } ]; /** TODO: if the type is a cell then display cell */ // type of code snippet: plain code or cell @@ -558,7 +513,7 @@ export class CodeSnippetDisplay extends React.Component< }} >
{ @@ -566,7 +521,7 @@ export class CodeSnippetDisplay extends React.Component< }} >
{ showPreview( { @@ -618,7 +573,7 @@ export class CodeSnippetDisplay extends React.Component< })}
-
+

{`${codeSnippet.description}`}

{/*
@@ -721,7 +676,7 @@ export class CodeSnippetDisplay extends React.Component< ] }).then((response: any): void => { if (response.button.accept) { - const widgetId = `jp-codeSnippet-editor-${codeSnippet.id}`; + const widgetId = `${CODE_SNIPPET_EDITOR}-${codeSnippet.id}`; const editor = find( this.props.app.shell.widgets('main'), (widget: Widget, _: number) => { @@ -732,6 +687,7 @@ export class CodeSnippetDisplay extends React.Component< if (editor) { editor.dispose(); } + contentsService.delete('snippets/' + codeSnippet.name + '.json'); this.props._codeSnippetWidgetModel.deleteSnippet(codeSnippet.id); this.props._codeSnippetWidgetModel.updateSnippetContents(); @@ -744,7 +700,7 @@ export class CodeSnippetDisplay extends React.Component< // remove dropdown menu private removeOptionsNode(): void { - const temp = document.getElementsByClassName('jp-options')[0]; + const temp = document.getElementsByClassName(CODE_SNIPPET_MORE_OPTIONS)[0]; if (!temp.classList.contains('inactive')) { temp.classList.add('inactive'); } @@ -755,16 +711,16 @@ export class CodeSnippetDisplay extends React.Component< const body = document.createElement('div'); const optionsContainer = document.createElement('div'); - optionsContainer.className = 'jp-more-options-content'; + optionsContainer.className = CODE_SNIPPET_MORE_OTPIONS_CONTENT; const insertSnip = document.createElement('div'); - insertSnip.className = 'jp-more-options-insert'; + insertSnip.className = CODE_SNIPPET_MORE_OTPIONS_INSERT; insertSnip.textContent = 'Insert snippet'; insertSnip.onclick = (): void => { this.insertCodeSnippet(codeSnippet); this.removeOptionsNode(); }; const copySnip = document.createElement('div'); - copySnip.className = 'jp-more-options-copy'; + copySnip.className = CODE_SNIPPET_MORE_OTPIONS_COPY; copySnip.textContent = 'Copy snippet to clipboard'; copySnip.onclick = (): void => { Clipboard.copyToSystem(codeSnippet.code.join('\n')); @@ -772,7 +728,7 @@ export class CodeSnippetDisplay extends React.Component< this.removeOptionsNode(); }; const editSnip = document.createElement('div'); - editSnip.className = 'jp-more-options-edit'; + editSnip.className = CODE_SNIPPET_MORE_OTPIONS_EDIT; editSnip.textContent = 'Edit snippet'; editSnip.onclick = (): void => { console.log(codeSnippet); @@ -790,7 +746,7 @@ export class CodeSnippetDisplay extends React.Component< this.removeOptionsNode(); }; const deleteSnip = document.createElement('div'); - deleteSnip.className = 'jp-more-options-delete'; + deleteSnip.className = CODE_SNIPPET_MORE_OTPIONS_DELETE; deleteSnip.textContent = 'Delete snippet'; deleteSnip.onclick = (): void => { this.deleteCommand(codeSnippet); @@ -808,9 +764,9 @@ export class CodeSnippetDisplay extends React.Component< return (
- {'Snippets'} + {'Snippets'}
- {/*
*/} { - // object.insertCodeSnippet(codeSnippet); - // }; - // const copySnip = document.createElement('div'); - // copySnip.className = 'jp-more-options-copy'; - // copySnip.textContent = 'Copy snippet to clipboard'; - // copySnip.onclick = (): void => { - // Clipboard.copyToSystem(codeSnippet.code.join('\n')); - // alert('saved to clipboard'); - // }; - // const editSnip = document.createElement('div'); - // editSnip.className = 'jp-more-options-edit'; - // editSnip.textContent = 'Edit snippet'; - // const deleteSnip = document.createElement('div'); - // deleteSnip.className = 'jp-more-options-delete'; - // deleteSnip.textContent = 'Delete snippet'; - // optionsContainer.appendChild(insertSnip); - // optionsContainer.appendChild(copySnip); - // optionsContainer.appendChild(editSnip); - // optionsContainer.appendChild(deleteSnip); - // body.append(optionsContainer); - // return body; - // } } /** diff --git a/src/CodeSnippetEditor.tsx b/src/CodeSnippetEditor.tsx index 249721c..79c7a34 100644 --- a/src/CodeSnippetEditor.tsx +++ b/src/CodeSnippetEditor.tsx @@ -1,32 +1,46 @@ import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor'; -// import { ICodeSnippet } from './CodeSnippetContentsService'; import { ReactWidget, showDialog, Dialog, WidgetTracker } from '@jupyterlab/apputils'; -import React from 'react'; -import { Message } from '@lumino/messaging'; import { Button } from '@jupyterlab/ui-components'; +import { Contents } from '@jupyterlab/services'; + +import { Message } from '@lumino/messaging'; + +import React from 'react'; + import { CodeSnippetContentsService } from './CodeSnippetContentsService'; import { CodeSnippetWidget } from './CodeSnippetWidget'; import { SUPPORTED_LANGUAGES } from './index'; import { CodeSnippetEditorTags } from './CodeSnippetEditorTags'; -import { Contents } from '@jupyterlab/services'; /** * CSS style classes */ const CODE_SNIPPET_EDITOR = 'jp-codeSnippet-editor'; -const CODE_SNIPPET_EDITOR_NAME_LABEL = 'jp-snippet-editor-name-label'; -const CODE_SNIPPET_EDITOR_LABEL_ACTIVE = 'jp-snippet-editor-label-active'; -const CODE_SNIPPET_EDITOR_INPUT_ACTIVE = 'jp-snippet-editor-active'; -const CODE_SNIPPET_EDITOR_NAME_INPUT = 'jp-snippet-editor-name'; -const CODE_SNIPPET_EDITOR_DESC_LABEL = 'jp-snippet-editor-description-label'; -const CODE_SNIPPET_EDITOR_DESC_INPUT = 'jp-snippet-editor-description'; -const CODE_SNIPPET_EDITOR_LANG_INPUT = 'jp-snippet-editor-language'; +const CODE_SNIPPET_EDITOR_TITLE = 'jp-codeSnippet-editor-title'; +const CODE_SNIPPET_EDITOR_METADATA = 'jp-codeSnippet-editor-metadata'; +const CODE_SNIPPET_EDITOR_NAME_LABEL = 'jp-codeSnippet-editor-name-label'; +const CODE_SNIPPET_EDITOR_LABEL_ACTIVE = 'jp-codeSnippet-editor-label-active'; +const CODE_SNIPPET_EDITOR_INPUT_ACTIVE = 'jp-codeSnippet-editor-active'; +const CODE_SNIPPET_EDITOR_NAME_INPUT = 'jp-codeSnippet-editor-name'; +const CODE_SNIPPET_EDITOR_DESC_LABEL = + 'jp-codeSnippet-editor-description-label'; +const CODE_SNIPPET_EDITOR_DESC_INPUT = 'jp-codeSnippet-editor-description'; +const CODE_SNIPPET_EDITOR_LANG_INPUT = 'jp-codeSnippet-editor-language'; const CODE_SNIPPET_EDITOR_MIRROR = 'jp-codeSnippetInput-editor'; +const CODE_SNIPPET_EDITOR_INPUTAREA = 'jp-codeSnippetInputArea'; +const CODE_SNIPPET_EDITOR_INPUTAREA_MIRROR = 'jp-codeSnippetInputArea-editor'; +const CODE_SNIPPET_EDITOR_MIRROR_LABEL = 'jp-codeSnippetInputArea-editorTitle'; + +const CODE_SNIPPET_EDITOR_INPUTNAME_VALIDITY = + 'jp-codeSnippet-inputName-validity'; +const CODE_SNIPPET_EDITOR_INPUTDESC_VALIDITY = + 'jp-codeSnippet-inputDesc-validity'; +const CODE_SNIPPET_EDITOR_TAGS_LABEL = 'jp-codeSnippet-editor-tags-label'; const EDITOR_DIRTY_CLASS = 'jp-mod-dirty'; @@ -469,6 +483,11 @@ export class CodeSnippetEditor extends ReactWidget { // update the display in code snippet explorer this.codeSnippetWidget.updateCodeSnippets(); + + // close editor if it's from scratch + if (this._codeSnippetEditorMetaData.fromScratch) { + this.dispose(); + } } handleChangeOnTag(selectedTags: string[], allTags: string[]): void { @@ -488,7 +507,7 @@ export class CodeSnippetEditor extends ReactWidget { renderCodeInput(): React.ReactElement { return (
): void => { @@ -535,13 +554,13 @@ export class CodeSnippetEditor extends ReactWidget { this.deactivateEditor(event); }} > - + {fromScratch ? 'Add New Code Snippet' : 'Edit Code Snippet'} -
- +
+ -

+

{ - 'Name of the code snippet MUST be alphanumeric or composed of underscore(_)' + 'Name of the code snippet MUST be lowercased, alphanumeric or composed of underscore(_)' }

- + -

+

{ - 'Description of the code snippet MUST be alphanumeric or composed of underscore(_) or space' + 'Description of the code snippet MUST be alphanumeric but can include space or punctuation' }

{this.renderLanguages()} - +
- Code + Code {this.renderCodeInput()}