import fs from "fs"; import bootstrap from "bootstrap"; import React from "react"; import { createRoot } from "react-dom/client"; import { observable, computed, action, autorun, Lambda, toJS, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { humanize, camelize, capitalize, stringCompare } from "eez-studio-shared/string"; import { Loader } from "eez-studio-ui/loader"; import type { IParameter, IParameterType, IEnum } from "instrument/scpi"; import { createObject, objectToJS, ProjectStore } from "project-editor/store"; import { ScpiCommand, ScpiSubsystem } from "project-editor/features/scpi/scpi"; import { ScpiEnum } from "project-editor/features/scpi/enum"; import { ProjectContext } from "project-editor/project/context"; //////////////////////////////////////////////////////////////////////////////// interface Command { name: string; helpLink?: string; parameters: IParameter[]; } interface Subsystem { name: string; commands: Command[]; helpLink?: string; } interface CommandDefinition { command: Command; subsystem: Subsystem; } interface MovedCommandDefinition extends CommandDefinition { toSubsystem: Subsystem; } interface Changes { subsystems: Subsystem[]; added: CommandDefinition[]; deleted: CommandDefinition[]; moved: MovedCommandDefinition[]; updated: CommandDefinition[]; newEnums: IEnum[]; } class FindChanges { constructor( private projectStore: ProjectStore, private existingEnums: ScpiEnum[] ) {} cleanUpScpiCommand(command: string) { command = command.trim(); let i = command.lastIndexOf(" "); if (i !== -1) { command = command.slice(i + 1); } if (command.indexOf("(") != -1 || command.indexOf(")") != -1) { return undefined; } let m = command.match(/[A-Z]{3,}/); if (!m) { // no 3 or more upper case letters return false; } if (command.length < 4) { return undefined; } return command; } getCommandFromSyntax(command: string) { command = command.trim(); let i = command.indexOf(" "); if (i !== -1) { return command.slice(0, i); } return command; } getParameterTypeAndRange(name: string, table: Element) { const elements = table.querySelectorAll("tr>td>p"); for (let i = 0; i < elements.length; ++i) { if ( elements[i].textContent && elements[i].textContent!.trim() === name ) { let type; let range; let sibling = elements[i].parentElement!.nextElementSibling; if (sibling) { type = sibling.textContent; sibling = sibling.nextElementSibling; if (sibling) { range = sibling.textContent; } } return { type, range }; } } return { type: undefined, range: undefined }; } newEnums: IEnum[] = []; enumExists(name: string) { for (let i = 0; i < this.existingEnums.length; ++i) { if (this.existingEnums[i].name === name) { return true; } } for (let i = 0; i < this.newEnums.length; ++i) { if (this.newEnums[i].name === name) { return true; } } return false; } getUniqueEnumName(suggestedEnumName: string) { let name = suggestedEnumName; let suffix = 1; while (this.enumExists(name)) { name = suggestedEnumName + suffix; ++suffix; } return name; } findEnum(suggestedEnumName: string, members: string): string { // find in existing enums for (let i = 0; i < this.existingEnums.length; ++i) { if ( this.existingEnums[i].members .map(member => member.name) .join("|") === members ) { return this.existingEnums[i].name; } } // find in new enums for (let i = 0; i < this.newEnums.length; ++i) { if ( this.newEnums[i].members .map(member => member.name) .join("|") === members ) { return this.newEnums[i].name; } } // create a new enum const newEnum = { name: this.getUniqueEnumName(suggestedEnumName), members: members.split("|").map((member, i) => ({ name: member, value: "" })) }; this.newEnums.push(newEnum); return newEnum.name; } extractEnumFromRange(suggestedEnumName: string, range: string) { // join lines and remove whitespaces range = range.split("\n").join(""); // remove whitespaces between | range = range .split("|") .map(x => x.trim()) .join("|"); // We are handling all kind of funny cases like: // "0 – 9999999|INFinite" => ["INFinite"] // "0 to MAXimum, MIN|DEF|MAX|UP|DOWN" => ["MIN", "DEF", "MAX", "UP", "DOWN"] // "MIN|MID|MAX (see also Section 8.1)" => ["MIN", "MID", "MAX"] // "0.5, 5, MIN|MAX|DEFault" => ["MIN", "MAX", "DEFault"] // etc const matches = range.match(/(([A-Za-z0-9]*)\|?)+/g); if (!matches) { return undefined; } // find longest match let result = matches[0]; for (let i = 1; i < matches.length; ++i) { if (matches[i].length > result.length) { result = matches[i]; } } // trim and remove empty members return this.findEnum( suggestedEnumName, result .split("|") .map(x => x.trim()) .filter(x => !!x) .join("|") ); } getCommandParameters(commandNameAndParams: string, table: Element) { // Find parameters in p.textContent. // Look in surounding table to get parameters type and range. // We are handling all these cases: // {} (this is mandatory param) // [] (this is optional param) // {}, {}, {} // {} [, ] // {} [, [, ]] // {} [, ...] ("..." means repetition of parameter before is possible ) if (!table) { return []; } commandNameAndParams = commandNameAndParams.trim(); let i = commandNameAndParams.trim().indexOf(" "); if (i === -1) { // no params return []; } let nextIsOptional = false; let params: IParameter[] = commandNameAndParams .substr(i) .trim() .split(",") .map(name => { name = name.trim(); let isOptional = nextIsOptional === true; // remove [ and ] at the beginning while (name.startsWith("[") || name.startsWith("]")) { name = name.substr(1, name.length - 1).trim(); isOptional = true; } // remove [ and ] at the end while (name.endsWith("[") || name.endsWith("]")) { name = name.substr(0, name.length - 1).trim(); nextIsOptional = true; } if (name.startsWith("{")) { isOptional = false; if (!name.endsWith("}")) { console.error( "Invalid params spec", commandNameAndParams ); return undefined; } name = name.substr(1, name.length - 2); } let types: IParameterType[] = []; if (name === "...") { return undefined; } if (!name.startsWith("<") || !name.endsWith(">")) { console.error("Invalid params spec", commandNameAndParams); return undefined; } let { type, range } = this.getParameterTypeAndRange( name, table ); name = name.substr(1, name.length - 2); if (type != undefined && range != undefined) { type.trim() .toLowerCase() .split("|") .forEach(type => { if ( type === "nr1" || type === "nr2" || type === "nr3" || type === "boolean" ) { types.push({ type }); } else if (type === "integer") { types.push({ type: "nr1" }); } else if (type === "real") { types.push({ type: "nr3" }); } else if (type === "bool") { types.push({ type: "boolean" }); } else if ( type === "quoted string" || type === "ascii string" ) { types.push({ type: "quoted-string" }); } else if (type === "data block") { types.push({ type: "data-block" }); } else if (type === "channellist") { types.push({ type: "channel-list" }); } else if (type === "channel") { types.push({ type: "discrete", enumeration: "Channel" }); } else if (type === "discrete") { types.push({ type: "discrete", enumeration: this.extractEnumFromRange( capitalize(camelize(name)), range! ) }); } else { console.error( `unknown type "${type}" for parameter "${name}" in "${commandNameAndParams}"` ); } }); } else { console.error( `type or range undefined for parameter "${name}" in "${commandNameAndParams}"` ); } return { name, type: types, isOptional }; }) .filter(param => !!param) as IParameter[]; // some checks, only purpose for now is to emit some errors in console let firstOptional = false; for (let i = 0; i < params.length; ++i) { if (i < params.length - 1 && params[i].name === "...") { console.error( 'Type "..." should be at the end', commandNameAndParams ); } if (firstOptional) { if (!params[i].isOptional) { console.error( "All optional should be at the end", commandNameAndParams ); } } else { if (params[i].isOptional) { firstOptional = true; } } } return params; } detectVersionOfScpiFileDoc(anchorElements: NodeListOf) { for (let i = 0; i < anchorElements.length; i++) { let bookmark = anchorElements[i].getAttribute("name"); if ( bookmark && (bookmark.startsWith("_scpi_subsys_") || bookmark.startsWith("_scpi_")) ) { return 2; } } return 1; } addCommand( commands: { bookmark: string; name: string; parameters: IParameter[]; }[], bookmark: string, textContent: string, table: Element ) { const name = this.getCommandFromSyntax(textContent); const parameters = this.getCommandParameters(textContent, table); const existingCommand = commands.find( existingCommand => existingCommand.name === name ); if (!existingCommand) { commands.push({ bookmark, name, parameters }); } else { if (parameters.length > existingCommand.parameters.length) { existingCommand.parameters = parameters; } } } getSubsystemFromScpiFileVersion1Doc( file: string, anchorElements: NodeListOf ) { let topicElement = anchorElements[0] && anchorElements[0].parentElement; if (topicElement != null) { let topic = topicElement.textContent; if (topic) { let subsystem: Subsystem = { name: topic, commands: [], helpLink: file }; let commands: { bookmark: string; name: string; parameters: IParameter[]; }[] = []; for (let i = 1; i < anchorElements.length; i++) { let bookmark = anchorElements[i].getAttribute("name"); if (bookmark) { let commandElement = anchorElements[i] .parentElement as HTMLElement; if (commandElement) { let command = commandElement.textContent; if (command) { let cleanedUpCommand = this.cleanUpScpiCommand(command); if (cleanedUpCommand) { let table = commandElement.nextElementSibling; while (table) { if (table.tagName == "TABLE") { let tr = table.querySelector( "tr:first-child" ); if (tr) { let td = tr.querySelector( "td:first-child" ); if ( td && td.textContent && td.textContent.indexOf( "Syntax" ) != -1 ) { let pList = tr.querySelectorAll( "td:nth-child(2)>p" ); let p = pList[0]; if (p && p.textContent) { this.addCommand( commands, bookmark, p.textContent, table ); p = pList[1]; if ( p && p.textContent ) { this.addCommand( commands, bookmark, p.textContent, table ); } } } } break; } table = table.nextElementSibling; } } } } } } commands.forEach(({ bookmark, name, parameters }) => { subsystem.commands.push({ name, helpLink: file + "#" + bookmark, parameters }); }); return [subsystem]; } } return []; } getSubsystemFromScpiFileVersion2Doc( file: string, anchorElements: NodeListOf ) { let subsystems: Subsystem[] = []; let subsystem: Subsystem | undefined; let commandBookmark: string | undefined; for (let i = 0; i < anchorElements.length; i++) { let bookmark = anchorElements[i].getAttribute("name"); if (bookmark) { let parentElement = anchorElements[i].parentElement; if (parentElement != null) { let text = parentElement.textContent; if (text) { if (bookmark.startsWith("_scpi_subsys_")) { subsystem = { name: text, commands: [], helpLink: file }; subsystems.push(subsystem); } else if (subsystem && bookmark.startsWith("_scpi_")) { const parameters = this.getCommandParameters( text, $(parentElement).parents("table")[0] ); subsystem.commands.push({ name: this.getCommandFromSyntax(text), helpLink: file + "#" + (commandBookmark || bookmark), parameters }); } else { commandBookmark = bookmark; } } } } } return subsystems; } getSubsystemFromScpiFileDoc( file: string, anchorElements: NodeListOf ) { let version = this.detectVersionOfScpiFileDoc(anchorElements); if (version === 1) { return this.getSubsystemFromScpiFileVersion1Doc( file, anchorElements ); } else if (version === 2) { return this.getSubsystemFromScpiFileVersion2Doc( file, anchorElements ); } else { return []; } } getCommandsFromScpiDoc() { return new Promise((resolve, reject) => { if ( this.projectStore.project.settings.general.commandsDocFolder === undefined ) { reject("SCPI help folder is not defined"); return; } let scpiHelpFolderPath = this.projectStore.getAbsoluteFilePath( this.projectStore.project.settings.general.commandsDocFolder ); fs.exists(scpiHelpFolderPath, (exists: boolean) => { if (!exists) { reject( `SCPI help folder "${scpiHelpFolderPath}" doesn't exists.` ); } else { fs.readdir( scpiHelpFolderPath, (err: any, files: string[]) => { if (err) { reject(err); return; } files = files.filter(file => file.endsWith("html")); let promises: Promise[] = files.map( file => { return new Promise( (resolve, reject) => { fs.readFile( scpiHelpFolderPath + "/" + file, "utf-8", (err: any, data: string) => { if (!err) { let element = document.createElement( "div" ); element.innerHTML = data; let anchorElements = element.querySelectorAll( "A[name]" ); let subsystems = this.getSubsystemFromScpiFileDoc( file, anchorElements ); resolve(subsystems); } else { resolve([]); } } ); } ); } ); Promise.all(promises) .then(results => { let allSubsystems: Subsystem[] = []; results.forEach(subsystems => { if (subsystems) { allSubsystems = allSubsystems.concat( subsystems ); } }); resolve(allSubsystems); }) .catch(err => reject(err)); } ); } }); }); } static compareCommandDefinitions( a: CommandDefinition, b: CommandDefinition ) { let c1 = a.command.name.toUpperCase(); let c2 = b.command.name.toUpperCase(); return c1 < c2 ? -1 : c1 > c2 ? 1 : 0; } findCommandInSubsystems(subsystems: Subsystem[], commandName: string) { for (const subsystem of subsystems) { for (const command of subsystem.commands) { if (command.name === commandName) { return { subsystem: subsystem, command: command }; } } } return undefined; } findMissingCommands(subsystems1: Subsystem[], subsystems2: Subsystem[]) { let missingCommands: CommandDefinition[] = []; subsystems1.forEach(subsystem => { subsystem.commands.forEach(command => { if (!this.findCommandInSubsystems(subsystems2, command.name)) { missingCommands.push({ command: command, subsystem: subsystem }); } }); }); return missingCommands.sort(FindChanges.compareCommandDefinitions); } compareParameterTypes(types1: IParameterType[], types2: IParameterType[]) { const sortedTypes1: IParameterType[] = types1 .slice() .sort((a, b) => stringCompare(a.type, b.type)); const sortedTypes2: IParameterType[] = types2 .slice() .sort((a, b) => stringCompare(a.type, b.type)); for (let i = 0; i < sortedTypes2.length; ++i) { if (sortedTypes2[i].type === "any") { return true; } } if (sortedTypes1.length !== sortedTypes2.length) { return false; } for (let i = 0; i < sortedTypes1.length; ++i) { const type1 = sortedTypes1[i]; const type2 = sortedTypes2[i]; if (type1.type !== type2.type) { return false; } if (type1.type === "discrete") { if ( !!type1.enumeration && type1.enumeration !== type2.enumeration ) { return false; } } } return true; } compareParameters(parameters1: IParameter[], parameters2: IParameter[]) { const sortedParameters1: IParameter[] = parameters1 .slice() .sort((a: IParameter, b: IParameter) => stringCompare(a.name, b.name) ); const sortedParameters2: IParameter[] = parameters2 .slice() .sort((a: IParameter, b: IParameter) => stringCompare(a.name, b.name) ); if (sortedParameters1.length !== sortedParameters2.length) { return false; } for (let i = 0; i < sortedParameters1.length; ++i) { const param1 = sortedParameters1[i]; const param2 = sortedParameters2[i]; if (param1.name !== param2.name) { return false; } if (param1.description !== param2.description) { return false; } if (param1.isOptional !== param2.isOptional) { return false; } if (!this.compareParameterTypes(param1.type, param2.type)) { return false; } } return true; } getChanges() { return new Promise((resolve, reject) => { this.getCommandsFromScpiDoc() .then(subsystems => { let existingSubsystems = objectToJS( this.projectStore.project.scpi.subsystems ); // added let added = this.findMissingCommands( subsystems, existingSubsystems ); // deleted let deleted = this.findMissingCommands( existingSubsystems, subsystems ); // moved let moved: MovedCommandDefinition[] = []; subsystems.forEach(subsystem => { subsystem.commands.forEach(command => { let result = this.findCommandInSubsystems( existingSubsystems, command.name ); if ( result && result.subsystem.name !== subsystem.name ) { moved.push({ command: command, subsystem: result.subsystem, toSubsystem: subsystem }); } }); }); moved = moved.sort(FindChanges.compareCommandDefinitions); // updated let updated: CommandDefinition[] = []; subsystems.forEach(subsystem => { subsystem.commands.forEach(command => { let existingCommand = this.findCommandInSubsystems( existingSubsystems, command.name ); if ( existingCommand && existingCommand.subsystem.name === subsystem.name && (!existingCommand.command.helpLink || !subsystem.helpLink || (existingCommand.command.helpLink && subsystem.helpLink && !existingCommand.command.helpLink.startsWith( subsystem.helpLink )) || !this.compareParameters( command.parameters, existingCommand.command.parameters )) ) { updated.push({ command: command, subsystem }); } }); }); resolve({ subsystems, added, deleted, moved, updated, newEnums: this.newEnums }); }) .catch(reject); }); } } //////////////////////////////////////////////////////////////////////////////// function findCommandInScpiSubsystems( subsystems: ScpiSubsystem[], commandName: string ) { for (const subsystem of subsystems) { for (const command of subsystem.commands) { if (command.name === commandName) { return { subsystem: subsystem, command: command }; } } } return undefined; } //////////////////////////////////////////////////////////////////////////////// type Section = "added" | "deleted" | "moved" | "updated" | "newEnums"; const SECTIONS: Section[] = [ "added", "deleted", "moved", "updated", "newEnums" ]; export const ImportScpiDocDialog = observer( class ImportScpiDocDialog extends React.Component<{ onHidden: () => void; }> { static contextType = ProjectContext; declare context: React.ContextType; constructor(props: any) { super(props); makeObservable(this, { changes: observable, selectedChanges: observable, error: observable, activeTab: observable, hasChanges: computed, hasSelectedChanges: computed, onOk: action, handleTabClick: action, handleSelectCommand: action }); } dialog: HTMLDivElement; modal: bootstrap.Modal; addedSelectAllCheckbox: HTMLInputElement; deletedSelectAllCheckbox: HTMLInputElement; movedSelectAllCheckbox: HTMLInputElement; updatedSelectAllCheckbox: HTMLInputElement; newEnumsSelectAllCheckbox: HTMLInputElement; changes: Changes; selectedChanges: Changes = { subsystems: [], added: [], deleted: [], moved: [], updated: [], newEnums: [] }; error: any; activeTab: Section; selectAllCheckboxDisposers: { added?: Lambda; deleted?: Lambda; moved?: Lambda; updated?: Lambda; newEnums?: Lambda; } = {}; get hasChanges() { return ( this.changes.added.length > 0 || this.changes.deleted.length > 0 || this.changes.moved.length > 0 || this.changes.updated.length > 0 || this.changes.newEnums.length > 0 ); } get hasSelectedChanges() { return ( this.selectedChanges.added.length > 0 || this.selectedChanges.deleted.length > 0 || this.selectedChanges.moved.length > 0 || this.selectedChanges.updated.length > 0 || this.selectedChanges.newEnums.length > 0 ); } componentDidMount() { $(this.dialog).on("hidden.bs.modal", () => { this.props.onHidden(); }); this.modal = new bootstrap.Modal(this.dialog); this.modal.show(); const scpi = this.context.project.scpi; const findChanges = new FindChanges(this.context, scpi.enums); findChanges .getChanges() .then( action((changes: Changes) => { this.changes = changes; if (changes.added.length > 0) { this.activeTab = "added"; } else if (changes.deleted.length > 0) { this.activeTab = "deleted"; } else if (changes.moved.length > 0) { this.activeTab = "moved"; } else if (changes.updated.length > 0) { this.activeTab = "updated"; } else if (changes.newEnums.length > 0) { this.activeTab = "newEnums"; } }) ) .catch( action((err: any) => { this.error = err; }) ); this.handleSelectAllCheckboxes(); } componentDidUpdate() { this.handleSelectAllCheckboxes(); } componentWillUnmount() { SECTIONS.forEach(section => { let disposer = this.selectAllCheckboxDisposers[section]; if (disposer) { disposer(); } }); } onOkCalled = false; onOk = (event: any) => { event.preventDefault(); if (this.onOkCalled) { return; } this.onOkCalled = true; this.context.backgroundCheckEnabled = false; this.modal.hide(); this.context.undoManager.setCombineCommands(true); const scpi = this.context.project.scpi; let existingSubsystems = scpi.subsystems; let getOrAddSubsystem = (subsystem: Subsystem) => { let existingSubsystem = existingSubsystems.find( existingSubsystem => existingSubsystem.name === subsystem.name ); if (existingSubsystem) { return existingSubsystem; } const scpiSubsystem = createObject( this.context, { name: subsystem.name, helpLink: subsystem.helpLink, commands: [] }, ScpiSubsystem ); return this.context.addObject( existingSubsystems, scpiSubsystem ) as ScpiSubsystem; }; this.changes.subsystems.forEach(subsystem => getOrAddSubsystem(subsystem) ); this.selectedChanges.added.forEach(commandDefinition => { let subsystem = getOrAddSubsystem(commandDefinition.subsystem); const command = createObject( this.context, commandDefinition.command as any, ScpiCommand ); this.context.addObject(subsystem.commands, command); }); this.selectedChanges.deleted.forEach(commandDefinition => { let result = findCommandInScpiSubsystems( existingSubsystems, commandDefinition.command.name ); if (result) { this.context.deleteObject(result.command as ScpiCommand); } }); this.selectedChanges.moved.forEach(commandDefinition => { let result = findCommandInScpiSubsystems( existingSubsystems, commandDefinition.command.name ); if (result) { let command = result.command as ScpiCommand; this.context.deleteObject(command); command.helpLink = commandDefinition.command.helpLink; let subsystem = getOrAddSubsystem( commandDefinition.toSubsystem ); this.context.addObject(subsystem.commands, command); } }); this.selectedChanges.newEnums.forEach(newEnum => { this.context.addObject( scpi.enums, createObject( this.context, newEnum as any, ScpiEnum ) ); }); this.selectedChanges.updated.forEach(commandDefinition => { let result = findCommandInScpiSubsystems( existingSubsystems, commandDefinition.command.name ); if (result) { this.context.updateObject(result.command, { helpLink: commandDefinition.command.helpLink, parameters: toJS( commandDefinition.command.parameters ).map(parameter => { return { name: parameter.name, type: parameter.type.map(type => { if (type.type === "discrete") { if ( !scpi.enums.find( scpiEnum => scpiEnum.name === type.enumeration ) ) { return { type: "discrete" }; } } return type; }), isOptional: parameter.isOptional, description: parameter.description }; }) }); } }); this.context.undoManager.setCombineCommands(false); this.context.backgroundCheckEnabled = true; }; onCancel = () => { this.modal.hide(); }; handleTabClick(activeTab: Section, event: any) { event.preventDefault(); this.activeTab = activeTab; } handleSelectAllCheckboxes() { SECTIONS.forEach(section => { if (this.selectAllCheckboxDisposers[section]) { return; } let checkbox: HTMLInputElement; if (section === "added") { checkbox = this.addedSelectAllCheckbox; } else if (section === "deleted") { checkbox = this.deletedSelectAllCheckbox; } else if (section === "moved") { checkbox = this.movedSelectAllCheckbox; } else if (section === "updated") { checkbox = this.updatedSelectAllCheckbox; } else { checkbox = this.newEnumsSelectAllCheckbox; } if (!checkbox) { return; } this.selectAllCheckboxDisposers[section] = autorun(() => { if (this.selectedChanges[section].length == 0) { checkbox.indeterminate = false; checkbox.checked = false; } else if ( this.selectedChanges[section].length == this.changes[section].length ) { checkbox.indeterminate = false; checkbox.checked = true; } else { checkbox.indeterminate = true; checkbox.checked = false; } }); checkbox.addEventListener( "click", action((event: any) => { if (this.selectedChanges[section].length == 0) { (this.selectedChanges as any)[section] = this.changes[section].slice(); } else { this.selectedChanges[section] = []; } }) ); }); } isChangeSelected( section: Section, changeDefinition: CommandDefinition | IEnum ) { let commandDefinitions: any = this.selectedChanges[section]; return commandDefinitions.indexOf(changeDefinition) !== -1; } handleSelectCommand( section: Section, commandDefinition: CommandDefinition, event: any ) { let commandDefinitions: any = this.selectedChanges[section]; if (event.target.checked) { commandDefinitions.push(commandDefinition); } else { let i = commandDefinitions.indexOf(commandDefinition); commandDefinitions.splice(i, 1); } } render() { let content; let buttons; if (this.error) { content =
{this.error}
; buttons = [ ]; } else if (this.changes) { if (this.hasChanges) { let tabs = SECTIONS.filter( section => this.changes[section].length > 0 ).map(section => (
  • {humanize(section).toUpperCase()} {this.changes[section].length}
  • )); let tables = SECTIONS.map(section => { if (this.changes[section].length === 0) { return; } let thead; if (section === "added") { thead = ( (this.addedSelectAllCheckbox = ref!) } type="checkbox" />{" "} Command To ); } else if (section === "deleted") { thead = ( (this.deletedSelectAllCheckbox = ref!) } type="checkbox" />{" "} Command From ); } else if (section === "moved") { thead = ( (this.movedSelectAllCheckbox = ref!) } type="checkbox" />{" "} Command From To ); } else if (section === "updated") { thead = ( (this.updatedSelectAllCheckbox = ref!) } type="checkbox" />{" "} Command In ); } else if (section === "newEnums") { thead = ( (this.newEnumsSelectAllCheckbox = ref!) } type="checkbox" />{" "} Enum Members ); } let tbody = (this.changes[section] as any).map( ( commandOrNewEnumDefinition: | CommandDefinition | IEnum ) => { let checkbox = ( ); if (section === "newEnums") { const newEnum = commandOrNewEnumDefinition as IEnum; return ( {checkbox} {newEnum.name} {newEnum.members .map(member => member.name) .join("|")} ); } else { const commandDefinition = commandOrNewEnumDefinition as CommandDefinition; if ( section === "added" || section === "deleted" || section === "updated" ) { return ( {checkbox}{" "} { commandDefinition .command.name } { commandDefinition .subsystem.name } ); } else { // section === "moved" return ( {checkbox}{" "} { commandDefinition .command.name } { commandDefinition .subsystem.name } { ( commandDefinition as MovedCommandDefinition ).toSubsystem.name } ); } } } ); return ( {thead}{tbody}
    ); }); content = (
      {tabs}
    {tables}
    ); buttons = [ , ]; } else { content = (

    No changes!

    ); buttons = [ ]; } } else { content = ; } let footer; if (buttons && buttons.length > 0) { footer =
    {buttons}
    ; } return (
    (this.dialog = ref!)} className={"modal fade EezStudio_ImportScpiDocDialogDiv"} tabIndex={-1} role="dialog" >
    Detected SCPI Command Changes
    {content}
    {footer}
    ); } } ); //////////////////////////////////////////////////////////////////////////////// export function showImportScpiDocDialog(projectStore: ProjectStore) { let el = document.createElement("div"); document.body.appendChild(el); const root = createRoot(el); root.render( { el.remove(); }} /> ); }