From 2630a782b27b3d997d205754e1067c8ba4ca8540 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 6 Dec 2024 09:48:07 -0800 Subject: [PATCH] Feature/command dictionary (#1571) * add CommandPanel component and CommandDictionary * pull dictionary logic out from SelectedCommand into SequenceEditor for reuse * add command dictionary argument components --- src/assets/inclusive-range.svg | 5 + src/assets/ruler.svg | 7 + .../sequencing/CommandPanel/CommandArg.svelte | 33 ++ .../CommandPanel/CommandArg.svelte.test.ts | 184 ++++++++++ .../CommandPanel/CommandArgUnit.svelte | 91 +++++ .../CommandArgUnit.svelte.test.ts | 58 +++ .../CommandPanel/CommandBooleanArgDef.svelte | 20 ++ .../CommandBooleanArgDef.svelte.test.ts | 28 ++ .../CommandPanel/CommandDictionary.svelte | 295 ++++++++++++++++ .../CommandPanel/CommandEnumArgDef.svelte | 29 ++ .../CommandEnumArgDef.svelte.test.ts | 26 ++ .../CommandPanel/CommandNumberArgDef.svelte | 60 ++++ .../CommandNumberArgDef.svelte.test.ts | 88 +++++ .../CommandPanel/CommandPanel.svelte | 113 ++++++ .../CommandPanel/CommandRepeatArgDef.svelte | 28 ++ .../CommandRepeatArgDef.svelte.test.ts | 52 +++ .../CommandPanel/CommandStringArgDef.svelte | 20 ++ .../CommandStringArgDef.svelte.test.ts | 24 ++ .../CommandPanel/SelectedCommand.svelte | 238 +++++++++++++ .../CommandPanel/TimeTagEditor.svelte | 31 ++ .../sequencing/SequenceEditor.svelte | 187 +++++++++- .../sequencing/form/ArgEditor.svelte | 2 +- .../sequencing/form/NumEditor.svelte | 3 +- .../sequencing/form/SelectedCommand.svelte | 330 ------------------ src/css/stellar.css | 8 + src/types/app.ts | 1 + src/types/sequencing.ts | 26 ++ .../codemirror/codemirror-utils.test.ts | 232 +++++++++++- src/utilities/codemirror/codemirror-utils.ts | 26 +- src/utilities/time.ts | 1 - 30 files changed, 1884 insertions(+), 362 deletions(-) create mode 100644 src/assets/inclusive-range.svg create mode 100644 src/assets/ruler.svg create mode 100644 src/components/sequencing/CommandPanel/CommandArg.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandArg.svelte.test.ts create mode 100644 src/components/sequencing/CommandPanel/CommandArgUnit.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandArgUnit.svelte.test.ts create mode 100644 src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte.test.ts create mode 100644 src/components/sequencing/CommandPanel/CommandDictionary.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte.test.ts create mode 100644 src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte.test.ts create mode 100644 src/components/sequencing/CommandPanel/CommandPanel.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte.test.ts create mode 100644 src/components/sequencing/CommandPanel/CommandStringArgDef.svelte create mode 100644 src/components/sequencing/CommandPanel/CommandStringArgDef.svelte.test.ts create mode 100644 src/components/sequencing/CommandPanel/SelectedCommand.svelte create mode 100644 src/components/sequencing/CommandPanel/TimeTagEditor.svelte delete mode 100644 src/components/sequencing/form/SelectedCommand.svelte diff --git a/src/assets/inclusive-range.svg b/src/assets/inclusive-range.svg new file mode 100644 index 0000000000..88a53e93a4 --- /dev/null +++ b/src/assets/inclusive-range.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/ruler.svg b/src/assets/ruler.svg new file mode 100644 index 0000000000..4a7451fabc --- /dev/null +++ b/src/assets/ruler.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/src/components/sequencing/CommandPanel/CommandArg.svelte b/src/components/sequencing/CommandPanel/CommandArg.svelte new file mode 100644 index 0000000000..1f8fa9be22 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandArg.svelte @@ -0,0 +1,33 @@ + + + + +
+ {#if isFswCommandArgumentBoolean(commandArgumentDefinition)} + + {:else if isStringArg(commandArgumentDefinition)} + + {:else if isNumberArg(commandArgumentDefinition)} + + {:else if isFswCommandArgumentEnum(commandArgumentDefinition)} + + {:else if isFswCommandArgumentRepeat(commandArgumentDefinition)} + + {/if} +
diff --git a/src/components/sequencing/CommandPanel/CommandArg.svelte.test.ts b/src/components/sequencing/CommandPanel/CommandArg.svelte.test.ts new file mode 100644 index 0000000000..6d1a582d15 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandArg.svelte.test.ts @@ -0,0 +1,184 @@ +import type { + FswCommandArgument, + FswCommandArgumentBoolean, + FswCommandArgumentEnum, + FswCommandArgumentFloat, + FswCommandArgumentInteger, + FswCommandArgumentNumeric, + FswCommandArgumentRepeat, + FswCommandArgumentUnsigned, + FswCommandArgumentVarString, +} from '@nasa-jpl/aerie-ampcs'; +import { cleanup, render } from '@testing-library/svelte'; +import { keyBy } from 'lodash-es'; +import { afterEach, describe, expect, it } from 'vitest'; +import CommandArg from './CommandArg.svelte'; + +describe('CommandArg component', () => { + afterEach(() => { + cleanup(); + }); + + it('Should render a boolean command argument', () => { + const booleanArgument: FswCommandArgumentBoolean = { + arg_type: 'boolean', + bit_length: 1, + default_value: 'Foo true', + description: 'test boolean arg', + format: { + false_str: 'Foo false', + true_str: 'Foo true', + }, + name: 'Foo Boolean', + }; + const { getByText } = render(CommandArg, { commandArgumentDefinition: booleanArgument }); + expect(getByText(/Foo false/)).toBeDefined(); + expect(getByText(/Foo true/)).toBeDefined(); + }); + + it('Should render an enum command argument', () => { + const enumArgument: FswCommandArgumentEnum = { + arg_type: 'enum', + bit_length: 3, + default_value: 'Foo true', + description: 'test enum arg', + enum_name: 'Foo enum name', + name: 'Foo Enum', + range: ['bar', 'baz', 'buzz'], + }; + const { getByDisplayValue } = render(CommandArg, { commandArgumentDefinition: enumArgument }); + expect(getByDisplayValue(/bar/)).toBeDefined(); + expect(getByDisplayValue(/baz/)).toBeDefined(); + expect(getByDisplayValue(/buzz/)).toBeDefined(); + }); + + describe('number command arguments', () => { + it('Should render a float command argument', () => { + const floatArgument: FswCommandArgumentFloat = { + arg_type: 'float', + bit_length: 3, + default_value: 10, + description: 'test float arg', + name: 'Foo Float', + range: { + max: 20, + min: 0, + }, + units: 'foo', + }; + const { getByText } = render(CommandArg, { commandArgumentDefinition: floatArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); + + it('Should render an integer command argument', () => { + const integerArgument: FswCommandArgumentInteger = { + arg_type: 'integer', + bit_length: 3, + default_value: 10, + description: 'test integer arg', + name: 'Foo Integer', + range: { + max: 20, + min: 0, + }, + units: 'foo', + }; + const { getByText } = render(CommandArg, { commandArgumentDefinition: integerArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); + + it('Should render an unsigned command argument', () => { + const unsignedArgument: FswCommandArgumentUnsigned = { + arg_type: 'unsigned', + bit_length: 3, + default_value: 10, + description: 'test unsigned arg', + name: 'Foo Unsigned', + range: { + max: 20, + min: 0, + }, + units: 'foo', + }; + const { getByText } = render(CommandArg, { commandArgumentDefinition: unsignedArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); + + it('Should render an unsigned command argument', () => { + const numericArgument: FswCommandArgumentNumeric = { + arg_type: 'numeric', + bit_length: 3, + default_value: 10, + description: 'test numeric arg', + name: 'Foo Numeric', + range: { + max: 20, + min: 0, + }, + type: 'float', + units: 'foo', + }; + const { getByText } = render(CommandArg, { commandArgumentDefinition: numericArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); + }); + + it('Should render a string command argument', () => { + const stringArgument: FswCommandArgumentVarString = { + arg_type: 'var_string', + default_value: 'Foo string', + description: 'test string arg', + max_bit_length: 8, + name: 'Foo String', + prefix_bit_length: 2, + valid_regex: '', + }; + const { getByText } = render(CommandArg, { commandArgumentDefinition: stringArgument }); + expect(getByText(/test string arg/)).toBeDefined(); + }); + + it('Should render a repeating command argument', () => { + const commandArguments: FswCommandArgument[] = [ + { + arg_type: 'boolean', + bit_length: 1, + default_value: 'Foo true', + description: 'test boolean arg', + format: { + false_str: 'Foo false', + true_str: 'Foo true', + }, + name: 'Boolean', + }, + { + arg_type: 'var_string', + default_value: 'Foo string', + description: 'test string arg', + max_bit_length: 8, + name: 'String', + prefix_bit_length: 2, + valid_regex: '', + }, + ]; + const repeatArgument: FswCommandArgumentRepeat = { + arg_type: 'repeat', + description: 'test repeating arg', + name: 'Foo Repeat', + prefix_bit_length: 2, + repeat: { + argumentMap: keyBy(commandArguments, 'name'), + arguments: commandArguments, + max: 4, + min: 1, + }, + }; + const { getByText } = render(CommandArg, { commandArgumentDefinition: repeatArgument }); + expect(getByText(/test repeating arg/)).toBeDefined(); + expect(getByText(/test boolean arg/)).toBeDefined(); + expect(getByText(/test string arg/)).toBeDefined(); + }); +}); diff --git a/src/components/sequencing/CommandPanel/CommandArgUnit.svelte b/src/components/sequencing/CommandPanel/CommandArgUnit.svelte new file mode 100644 index 0000000000..7ffc6fa362 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandArgUnit.svelte @@ -0,0 +1,91 @@ + + + + +
+ {#if typeDisplay} +
+
+ {typeDisplay} +
+ {/if} + {#if range} + {#if type === 'boolean'} +
+ {range.min}|{range.max} +
+ {:else} +
+
+ {range.min}-{range.max} +
+ {/if} + {/if} + {#if isValidUnit(unit, unitShortName)} +
+
+ {unitShortName || unit} +
+ {/if} +
+ + diff --git a/src/components/sequencing/CommandPanel/CommandArgUnit.svelte.test.ts b/src/components/sequencing/CommandPanel/CommandArgUnit.svelte.test.ts new file mode 100644 index 0000000000..5eeffe0002 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandArgUnit.svelte.test.ts @@ -0,0 +1,58 @@ +import { cleanup, render } from '@testing-library/svelte'; +import { afterEach, describe, expect, it } from 'vitest'; +import CommandArgUnit from './CommandArgUnit.svelte'; + +describe('CommandArgUnit component', () => { + afterEach(() => { + cleanup(); + }); + + it('Should render units for a boolean command argument', () => { + const { getByText } = render(CommandArgUnit, { + range: { max: 'Foo true', min: 'Foo false' }, + type: 'boolean', + typeDisplay: 'Foo boolean', + }); + expect(getByText(/Foo false/)).toBeDefined(); + expect(getByText(/Foo true/)).toBeDefined(); + }); + + it('Should render a range for a command argument', () => { + const { getByText } = render(CommandArgUnit, { + range: { max: '24', min: '2' }, + type: 'number', + typeDisplay: 'Foo number', + }); + expect(getByText(/2/)).toBeDefined(); + expect(getByText(/-/)).toBeDefined(); + expect(getByText(/24/)).toBeDefined(); + }); + + it('Should render units for a string command argument', () => { + const { getByText } = render(CommandArgUnit, { + type: 'string', + typeDisplay: 'Foo string', + }); + expect(getByText(/Foo string/)).toBeDefined(); + }); + + it('Should render short version of unit', () => { + const { getByText, queryByText } = render(CommandArgUnit, { + type: 'string', + typeDisplay: 'Foo string', + unit: 'bar baz', + unitShortName: 'foo', + }); + expect(getByText('foo')).toBeDefined(); + expect(queryByText('bar baz')).toBeNull(); + }); + + it('Should render unit if no short version is provided', () => { + const { getByText } = render(CommandArgUnit, { + type: 'string', + typeDisplay: 'Foo string', + unit: 'bar baz', + }); + expect(getByText('bar baz')).toBeDefined(); + }); +}); diff --git a/src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte b/src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte new file mode 100644 index 0000000000..74dd574fd2 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte @@ -0,0 +1,20 @@ + + + + + +
{argDef.description}
+
+ +
+
diff --git a/src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte.test.ts b/src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte.test.ts new file mode 100644 index 0000000000..0a04355f86 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandBooleanArgDef.svelte.test.ts @@ -0,0 +1,28 @@ +import type { FswCommandArgumentBoolean } from '@nasa-jpl/aerie-ampcs'; +import { cleanup, render } from '@testing-library/svelte'; +import { afterEach, describe, expect, it } from 'vitest'; +import CommandBooleanArgDef from './CommandBooleanArgDef.svelte'; + +describe('CommandBooleanArgDef component', () => { + afterEach(() => { + cleanup(); + }); + + it('Should render units for a boolean command argument', () => { + const booleanArgument: FswCommandArgumentBoolean = { + arg_type: 'boolean', + bit_length: 1, + default_value: 'Foo true', + description: 'test boolean arg', + format: { + false_str: 'Foo false', + true_str: 'Foo true', + }, + name: 'Foo Boolean', + }; + const { getByText } = render(CommandBooleanArgDef, { argDef: booleanArgument }); + expect(getByText(/Foo false/)).toBeDefined(); + expect(getByText(/Foo false/)).toBeDefined(); + expect(getByText('test boolean arg')).toBeDefined(); + }); +}); diff --git a/src/components/sequencing/CommandPanel/CommandDictionary.svelte b/src/components/sequencing/CommandPanel/CommandDictionary.svelte new file mode 100644 index 0000000000..20ef58a0c8 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandDictionary.svelte @@ -0,0 +1,295 @@ + + + + +
+
+ + +
+ + +
+ + diff --git a/src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte b/src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte new file mode 100644 index 0000000000..1c7de2f509 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte @@ -0,0 +1,29 @@ + + + + + +
{argDef.description}
+
+ +
+
+ +
+
+ + diff --git a/src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte.test.ts b/src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte.test.ts new file mode 100644 index 0000000000..8e6bb7bde6 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandEnumArgDef.svelte.test.ts @@ -0,0 +1,26 @@ +import type { FswCommandArgumentEnum } from '@nasa-jpl/aerie-ampcs'; +import { cleanup, render } from '@testing-library/svelte'; +import { afterEach, describe, expect, it } from 'vitest'; +import CommandEnumArgDef from './CommandEnumArgDef.svelte'; + +describe('CommandEnumArgDef component', () => { + afterEach(() => { + cleanup(); + }); + + it('Should render units for an enum command argument', () => { + const enumArgument: FswCommandArgumentEnum = { + arg_type: 'enum', + bit_length: 3, + default_value: 'Foo true', + description: 'test enum arg', + enum_name: 'Foo enum name', + name: 'Foo Enum', + range: ['bar', 'baz', 'buzz'], + }; + const { getByDisplayValue } = render(CommandEnumArgDef, { argDef: enumArgument }); + expect(getByDisplayValue(/bar/)).toBeDefined(); + expect(getByDisplayValue(/baz/)).toBeDefined(); + expect(getByDisplayValue(/buzz/)).toBeDefined(); + }); +}); diff --git a/src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte b/src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte new file mode 100644 index 0000000000..1690cba942 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte @@ -0,0 +1,60 @@ + + + + + +
{argDef.description}
+
+ +
+
diff --git a/src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte.test.ts b/src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte.test.ts new file mode 100644 index 0000000000..5c2105d955 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandNumberArgDef.svelte.test.ts @@ -0,0 +1,88 @@ +import type { + FswCommandArgumentFloat, + FswCommandArgumentInteger, + FswCommandArgumentNumeric, + FswCommandArgumentUnsigned, +} from '@nasa-jpl/aerie-ampcs'; +import { cleanup, render } from '@testing-library/svelte'; +import { afterEach, describe, expect, it } from 'vitest'; +import CommandNumberArgDef from './CommandNumberArgDef.svelte'; + +describe('CommandNumberArgDef component', () => { + afterEach(() => { + cleanup(); + }); + + it('Should render a float command argument', () => { + const floatArgument: FswCommandArgumentFloat = { + arg_type: 'float', + bit_length: 3, + default_value: 10, + description: 'test float arg', + name: 'Foo Float', + range: { + max: 20, + min: 0, + }, + units: 'foo', + }; + const { getByText } = render(CommandNumberArgDef, { argDef: floatArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); + + it('Should render an integer command argument', () => { + const integerArgument: FswCommandArgumentInteger = { + arg_type: 'integer', + bit_length: 3, + default_value: 10, + description: 'test integer arg', + name: 'Foo Integer', + range: { + max: 20, + min: 0, + }, + units: 'foo', + }; + const { getByText } = render(CommandNumberArgDef, { argDef: integerArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); + + it('Should render an unsigned command argument', () => { + const unsignedArgument: FswCommandArgumentUnsigned = { + arg_type: 'unsigned', + bit_length: 3, + default_value: 10, + description: 'test unsigned arg', + name: 'Foo Unsigned', + range: { + max: 20, + min: 0, + }, + units: 'foo', + }; + const { getByText } = render(CommandNumberArgDef, { argDef: unsignedArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); + + it('Should render an unsigned command argument', () => { + const numericArgument: FswCommandArgumentNumeric = { + arg_type: 'numeric', + bit_length: 3, + default_value: 10, + description: 'test numeric arg', + name: 'Foo Numeric', + range: { + max: 20, + min: 0, + }, + type: 'float', + units: 'foo', + }; + const { getByText } = render(CommandNumberArgDef, { argDef: numericArgument }); + expect(getByText(/0/)).toBeDefined(); + expect(getByText(/20/)).toBeDefined(); + }); +}); diff --git a/src/components/sequencing/CommandPanel/CommandPanel.svelte b/src/components/sequencing/CommandPanel/CommandPanel.svelte new file mode 100644 index 0000000000..72b09a93ba --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandPanel.svelte @@ -0,0 +1,113 @@ + + + + +
+ + + + {commandNode ? `Selected ${formatTypeName(commandNode.name)}` : 'Selected Command'} + + Command Dictionary + + + + + + + + +
+ + diff --git a/src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte b/src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte new file mode 100644 index 0000000000..96bf2fce23 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte @@ -0,0 +1,28 @@ + + + + + +
{argDef.description}
+
+ {#each argDef.repeat?.arguments ?? [] as childArgumentDefinition} + {#if childArgumentDefinition} + + {/if} + {/each} +
+
+ +
+
diff --git a/src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte.test.ts b/src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte.test.ts new file mode 100644 index 0000000000..50d2aa8e72 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandRepeatArgDef.svelte.test.ts @@ -0,0 +1,52 @@ +import type { FswCommandArgument, FswCommandArgumentRepeat } from '@nasa-jpl/aerie-ampcs'; +import { cleanup, render } from '@testing-library/svelte'; +import { keyBy } from 'lodash-es'; +import { afterEach, describe, expect, it } from 'vitest'; +import CommandRepeatArgDef from './CommandRepeatArgDef.svelte'; + +describe('CommandRepeatArgDef component', () => { + afterEach(() => { + cleanup(); + }); + + it('Should render a repeating command argument', () => { + const commandArguments: FswCommandArgument[] = [ + { + arg_type: 'boolean', + bit_length: 1, + default_value: 'Foo true', + description: 'test boolean arg', + format: { + false_str: 'Foo false', + true_str: 'Foo true', + }, + name: 'Boolean', + }, + { + arg_type: 'var_string', + default_value: 'Foo string', + description: 'test string arg', + max_bit_length: 8, + name: 'String', + prefix_bit_length: 2, + valid_regex: '', + }, + ]; + const repeatArgument: FswCommandArgumentRepeat = { + arg_type: 'repeat', + description: 'test repeating arg', + name: 'Foo Repeat', + prefix_bit_length: 2, + repeat: { + argumentMap: keyBy(commandArguments, 'name'), + arguments: commandArguments, + max: 4, + min: 1, + }, + }; + const { getByText } = render(CommandRepeatArgDef, { argDef: repeatArgument }); + expect(getByText(/test repeating arg/)).toBeDefined(); + expect(getByText(/test boolean arg/)).toBeDefined(); + expect(getByText(/test string arg/)).toBeDefined(); + }); +}); diff --git a/src/components/sequencing/CommandPanel/CommandStringArgDef.svelte b/src/components/sequencing/CommandPanel/CommandStringArgDef.svelte new file mode 100644 index 0000000000..4f20d54f45 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandStringArgDef.svelte @@ -0,0 +1,20 @@ + + + + + +
{argDef.description}
+
+ +
+
diff --git a/src/components/sequencing/CommandPanel/CommandStringArgDef.svelte.test.ts b/src/components/sequencing/CommandPanel/CommandStringArgDef.svelte.test.ts new file mode 100644 index 0000000000..a74f5d9523 --- /dev/null +++ b/src/components/sequencing/CommandPanel/CommandStringArgDef.svelte.test.ts @@ -0,0 +1,24 @@ +import type { FswCommandArgumentVarString } from '@nasa-jpl/aerie-ampcs'; +import { cleanup, render } from '@testing-library/svelte'; +import { afterEach, describe, expect, it } from 'vitest'; +import CommandStringArgDef from './CommandStringArgDef.svelte'; + +describe('CommandStringArgDef component', () => { + afterEach(() => { + cleanup(); + }); + + it('Should render units for a boolean command argument', () => { + const stringArgument: FswCommandArgumentVarString = { + arg_type: 'var_string', + default_value: 'Foo string', + description: 'test string arg', + max_bit_length: 8, + name: 'Foo String', + prefix_bit_length: 2, + valid_regex: '', + }; + const { getByText } = render(CommandStringArgDef, { argDef: stringArgument }); + expect(getByText(/test string arg/)).toBeDefined(); + }); +}); diff --git a/src/components/sequencing/CommandPanel/SelectedCommand.svelte b/src/components/sequencing/CommandPanel/SelectedCommand.svelte new file mode 100644 index 0000000000..10221e4ea2 --- /dev/null +++ b/src/components/sequencing/CommandPanel/SelectedCommand.svelte @@ -0,0 +1,238 @@ + + + + +
+ {#if commandName != null} +
+ {commandName} + +
+ {/if} + {#if commandDef != null && commandDef.description != null} +
{commandDef.description}
+ {/if} + {#if !!timeTagNode} +
+ + +
+ {/if} + {#if !!commandNode} + {#if commandInfoMapper.nodeTypeHasArguments(commandNode)} + {#if !!commandDef} +
+ {commandDef.description} +
+ + {#each editorArgInfoArray as argInfo} + setInEditor(editorSequenceView, token, val), 250)} + addDefaultArgs={(commandNode, missingArgDefArray) => + addDefaultArgs(commandDictionary, editorSequenceView, commandNode, missingArgDefArray, commandInfoMapper)} + /> + {/each} + + {#if missingArgDefArray.length} +
+ { + if (commandNode) { + addDefaultArgs( + commandDictionary, + editorSequenceView, + commandNode, + missingArgDefArray, + commandInfoMapper, + ); + } + }} + /> +
+ {/if} + {:else} +
+
{commandName ?? ''}
+
+
Command type is not present in dictionary
+ {/if} + {:else} +
+
{`${formatTypeName(commandNode.name)} Name`}
+
+ { + if (commandNameNode) { + setInEditor(editorSequenceView, commandNameNode, val); + } + }} + /> +
+
+ {/if} + {:else} +
+ Select a command or open the +
+ . +
+
+ {/if} +
+ + diff --git a/src/components/sequencing/CommandPanel/TimeTagEditor.svelte b/src/components/sequencing/CommandPanel/TimeTagEditor.svelte new file mode 100644 index 0000000000..7f4208d081 --- /dev/null +++ b/src/components/sequencing/CommandPanel/TimeTagEditor.svelte @@ -0,0 +1,31 @@ + + + + +
Time Tag:
diff --git a/src/components/sequencing/SequenceEditor.svelte b/src/components/sequencing/SequenceEditor.svelte index 08e81a7cf4..4fef546ce4 100644 --- a/src/components/sequencing/SequenceEditor.svelte +++ b/src/components/sequencing/SequenceEditor.svelte @@ -7,7 +7,14 @@ import { Compartment, EditorState } from '@codemirror/state'; import { type ViewUpdate } from '@codemirror/view'; import type { SyntaxNode, Tree } from '@lezer/common'; - import type { ChannelDictionary, CommandDictionary, ParameterDictionary } from '@nasa-jpl/aerie-ampcs'; + import type { + ChannelDictionary, + CommandDictionary, + FswCommand, + FswCommandArgument, + FswCommandArgumentRepeat, + ParameterDictionary, + } from '@nasa-jpl/aerie-ampcs'; import ChevronDownIcon from '@nasa-jpl/stellar/icons/chevron_down.svg?component'; import CollapseIcon from 'bootstrap-icons/icons/arrow-bar-down.svg?component'; import ExpandIcon from 'bootstrap-icons/icons/arrow-bar-up.svg?component'; @@ -16,6 +23,7 @@ import { EditorView, basicSetup } from 'codemirror'; import { debounce } from 'lodash-es'; import { createEventDispatcher, onDestroy, onMount } from 'svelte'; + import { TOKEN_ERROR } from '../../constants/seq-n-grammar-constants'; import { inputFormat, outputFormat, @@ -35,8 +43,16 @@ userSequences, } from '../../stores/sequencing'; import type { User } from '../../types/app'; - import type { IOutputFormat, LibrarySequence, Parcel } from '../../types/sequencing'; + import type { + ArgTextDef, + IOutputFormat, + ISequenceAdaptation, + LibrarySequence, + Parcel, + TimeTagInfo, + } from '../../types/sequencing'; import { SeqLanguage, setupLanguageSupport } from '../../utilities/codemirror'; + import { isFswCommandArgumentRepeat } from '../../utilities/codemirror/codemirror-utils'; import type { CommandInfoMapper } from '../../utilities/codemirror/commandInfoMapper'; import { seqNHighlightBlock, seqqNBlockHighlighter } from '../../utilities/codemirror/seq-n-highlighter'; import { SeqNCommandInfoMapper } from '../../utilities/codemirror/seq-n-tree-utils'; @@ -54,7 +70,7 @@ import { VmlCommandInfoMapper } from '../../utilities/codemirror/vml/vmlTreeUtils'; import effects from '../../utilities/effects'; import { downloadBlob, downloadJSON } from '../../utilities/generic'; - import { inputLinter, outputLinter } from '../../utilities/sequence-editor/extension-points'; + import { getCustomArgDef, inputLinter, outputLinter } from '../../utilities/sequence-editor/extension-points'; import { seqNFormat } from '../../utilities/sequence-editor/sequence-autoindent'; import { sequenceTooltip } from '../../utilities/sequence-editor/sequence-tooltip'; import { parseVariables } from '../../utilities/sequence-editor/to-seq-json'; @@ -66,7 +82,7 @@ import CssGridGutter from '../ui/CssGridGutter.svelte'; import Panel from '../ui/Panel.svelte'; import SectionTitle from '../ui/SectionTitle.svelte'; - import SelectedCommand from './form/SelectedCommand.svelte'; + import CommandPanel from './CommandPanel/CommandPanel.svelte'; export let parcel: Parcel | null; export let showCommandFormBuilder: boolean = false; @@ -114,6 +130,14 @@ let showOutputs: boolean = true; let editorHeights: string = toggleSeqJsonPreview ? '1fr 3px 1fr' : '1.88fr 3px 80px'; + let argInfoArray: ArgTextDef[] = []; + let commandNode: SyntaxNode | null = null; + let commandNameNode: SyntaxNode | null = null; + let commandName: string | null = null; + let commandDef: FswCommand | null = null; + let timeTagNode: TimeTagInfo = null; + let variablesInScope: string[] = []; + $: { loadSequenceAdaptation(parcel?.sequence_adaptation_id); } @@ -246,6 +270,10 @@ }); }); } + } else { + commandDictionary = null; + channelDictionary = null; + parameterDictionaries = []; } } @@ -258,6 +286,27 @@ } } + $: commandNode = commandInfoMapper.getContainingCommand(selectedNode); + $: commandNameNode = commandInfoMapper.getNameNode(commandNode); + $: commandName = commandNameNode && editorSequenceView.state.sliceDoc(commandNameNode.from, commandNameNode.to); + $: commandDef = getCommandDef(commandDictionary, commandName ?? ''); + $: timeTagNode = getTimeTagInfo(editorSequenceView, commandNode); + $: argInfoArray = getArgumentInfo( + commandInfoMapper, + editorSequenceView, + commandInfoMapper.getArgumentNodeContainer(commandNode), + commandDef?.arguments, + undefined, + parameterDictionaries, + ); + $: variablesInScope = getVariablesInScope( + commandInfoMapper, + editorSequenceView, + $sequenceAdaptation, + currentTree, + commandNode?.from, + ); + onMount(() => { compartmentSeqJsonLinter = new Compartment(); compartmentSeqLanguage = new Compartment(); @@ -435,6 +484,113 @@ function inVmlMode(sequenceName: string | undefined): boolean { return sequenceName !== undefined && sequenceName.endsWith('.vml'); } + + function getTimeTagInfo(seqEditorView: EditorView, commandNode: SyntaxNode | null): TimeTagInfo { + const node = commandNode?.getChild('TimeTag'); + + return ( + node && { + node, + text: seqEditorView.state.sliceDoc(node.from, node.to) ?? '', + } + ); + } + + function getCommandDef(commandDictionary: CommandDictionary | null, stemName: string): FswCommand | null { + return commandDictionary?.fswCommandMap[stemName] ?? null; + } + + function getVariablesInScope( + infoMapper: CommandInfoMapper, + seqEditorView: EditorView, + adaptation: ISequenceAdaptation, + tree: Tree | null, + cursorPosition?: number, + ): string[] { + const globalNames = (adaptation.globals ?? []).map(globalVariable => globalVariable.name); + if (tree && cursorPosition !== undefined) { + const docText = seqEditorView.state.doc.toString(); + return [...globalNames, ...infoMapper.getVariables(docText, tree, cursorPosition)]; + } + return globalNames; + } + + function getArgumentInfo( + infoMapper: CommandInfoMapper, + seqEditorView: EditorView, + args: SyntaxNode | null, + argumentDefs: FswCommandArgument[] | undefined, + parentArgDef: FswCommandArgumentRepeat | undefined, + parameterDictionaries: ParameterDictionary[], + ) { + const argArray: ArgTextDef[] = []; + const precedingArgValues: string[] = []; + const parentRepeatLength = parentArgDef?.repeat?.arguments.length; + + if (args) { + for (const node of infoMapper.getArgumentsFromContainer(args)) { + if (node.name === TOKEN_ERROR) { + continue; + } + + let argDef: FswCommandArgument | undefined = undefined; + if (argumentDefs) { + let argDefIndex = argArray.length; + if (parentRepeatLength !== undefined) { + // for repeat args shift index + argDefIndex %= parentRepeatLength; + } + argDef = argumentDefs[argDefIndex]; + } + + if (commandDef && argDef) { + argDef = getCustomArgDef( + commandDef?.stem, + argDef, + precedingArgValues, + parameterDictionaries, + channelDictionary, + ); + } + + let children: ArgTextDef[] | undefined = undefined; + if (!!argDef && isFswCommandArgumentRepeat(argDef)) { + children = getArgumentInfo( + infoMapper, + seqEditorView, + node, + argDef.repeat?.arguments, + argDef, + parameterDictionaries, + ); + } + const argValue = seqEditorView.state.sliceDoc(node.from, node.to); + argArray.push({ + argDef, + children, + node, + parentArgDef, + text: argValue, + }); + precedingArgValues.push(argValue); + } + } + // add entries for defined arguments missing from editor + if (argumentDefs) { + if (!parentArgDef) { + argArray.push(...argumentDefs.slice(argArray.length).map(argDef => ({ argDef }))); + } else { + const repeatArgs = parentArgDef?.repeat?.arguments; + if (repeatArgs) { + if (argArray.length % repeatArgs.length !== 0) { + argArray.push(...argumentDefs.slice(argArray.length % repeatArgs.length).map(argDef => ({ argDef }))); + } + } + } + } + + return argArray; + } @@ -560,20 +716,23 @@ {#if showCommandFormBuilder} - {#if !!commandDictionary && !!selectedNode} - {:else} - + - Selected Command + Selected Command @@ -619,4 +778,8 @@ .output-format label { width: 10rem; } + + .command-title { + padding: 8px; + } diff --git a/src/components/sequencing/form/ArgEditor.svelte b/src/components/sequencing/form/ArgEditor.svelte index 638658541b..17d2e1cafc 100644 --- a/src/components/sequencing/form/ArgEditor.svelte +++ b/src/components/sequencing/form/ArgEditor.svelte @@ -3,6 +3,7 @@ - - - - {commandNode ? `Selected ${formatTypeName(commandNode.name)}` : 'No command selected'} - - -
- {#if !!timeTagNode} -
- - -
- {/if} - {#if !!commandNode} - {#if commandInfoMapper.nodeTypeHasArguments(commandNode)} - {#if !!commandDef} -
- {commandDef.description} -
- - {#each editorArgInfoArray as argInfo} - setInEditor(editorSequenceView, token, val), 250)} - addDefaultArgs={(commandNode, missingArgDefArray) => - addDefaultArgs( - commandDictionary, - editorSequenceView, - commandNode, - missingArgDefArray, - commandInfoMapper, - )} - /> - {/each} - - {#if missingArgDefArray.length} -
- { - if (commandNode) { - addDefaultArgs( - commandDictionary, - editorSequenceView, - commandNode, - missingArgDefArray, - commandInfoMapper, - ); - } - }} - /> -
- {/if} - {:else} -
-
{commandName ?? ''}
-
-
Command type is not present in dictionary
- {/if} - {:else} -
-
{`${formatTypeName(commandNode.name)} Name`}
-
- { - if (commandNameNode) { - setInEditor(editorSequenceView, commandNameNode, val); - } - }} - /> -
-
- {/if} - {:else} -
Select a command to modify its parameters.
- {/if} -
-
-
- - diff --git a/src/css/stellar.css b/src/css/stellar.css index c580a15e1a..2bfea8c2e0 100644 --- a/src/css/stellar.css +++ b/src/css/stellar.css @@ -39,3 +39,11 @@ border-color: var(--st-error-red); color: var(--st-utility-red); } + +.st-button-link { + background: none; + border: none; + color: var(--st-primary-60); + cursor: pointer; + padding: 0; +} diff --git a/src/types/app.ts b/src/types/app.ts index a5c6cea35a..5e6fc7fc72 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -37,3 +37,4 @@ export type Version = { }; export type PartialWith = Partial & Pick; +export type UnionOfValues, K extends keyof T> = T[K] extends infer U ? U : never; diff --git a/src/types/sequencing.ts b/src/types/sequencing.ts index b05840a656..ab0b5fa941 100644 --- a/src/types/sequencing.ts +++ b/src/types/sequencing.ts @@ -6,6 +6,14 @@ import type { ChannelDictionary as AmpcsChannelDictionary, CommandDictionary as AmpcsCommandDictionary, ParameterDictionary as AmpcsParameterDictionary, + FswCommandArgument, + FswCommandArgumentFixedString, + FswCommandArgumentFloat, + FswCommandArgumentInteger, + FswCommandArgumentNumeric, + FswCommandArgumentRepeat, + FswCommandArgumentUnsigned, + FswCommandArgumentVarString, } from '@nasa-jpl/aerie-ampcs'; import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; @@ -159,3 +167,21 @@ export type Workspace = { owner: UserId; updated_at: string; }; + +export type TimeTagInfo = { node: SyntaxNode; text: string } | null | undefined; + +export type StringArg = FswCommandArgumentVarString | FswCommandArgumentFixedString; + +export type NumberArg = + | FswCommandArgumentFloat + | FswCommandArgumentInteger + | FswCommandArgumentNumeric + | FswCommandArgumentUnsigned; + +export type ArgTextDef = { + argDef?: FswCommandArgument; + children?: ArgTextDef[]; + node?: SyntaxNode; + parentArgDef?: FswCommandArgumentRepeat; + text?: string; +}; diff --git a/src/utilities/codemirror/codemirror-utils.test.ts b/src/utilities/codemirror/codemirror-utils.test.ts index c3b399ed67..cc3ecbb7ba 100644 --- a/src/utilities/codemirror/codemirror-utils.test.ts +++ b/src/utilities/codemirror/codemirror-utils.test.ts @@ -1,9 +1,35 @@ +import type { + FswCommand, + FswCommandArgumentBoolean, + FswCommandArgumentEnum, + FswCommandArgumentFixedString, + FswCommandArgumentFloat, + FswCommandArgumentInteger, + FswCommandArgumentNumeric, + FswCommandArgumentRepeat, + FswCommandArgumentUnsigned, + FswCommandArgumentVarString, + HwCommand, +} from '@nasa-jpl/aerie-ampcs'; import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, test } from 'vitest'; import { getDefaultVariableArgs, + isFswCommand, + isFswCommandArgumentBoolean, + isFswCommandArgumentEnum, + isFswCommandArgumentFixedString, + isFswCommandArgumentFloat, + isFswCommandArgumentInteger, + isFswCommandArgumentNumeric, + isFswCommandArgumentRepeat, + isFswCommandArgumentUnsigned, + isFswCommandArgumentVarString, isHexValue, + isHwCommand, + isNumberArg, isQuoted, + isStringArg, parseNumericArg, quoteEscape, removeEscapedQuotes, @@ -119,6 +145,210 @@ describe('isHexValue', () => { expect(isHexValue('0x12xx')).toBe(false); }); }); + +describe('Command and argument typeguards', () => { + test('isFswCommand', () => { + expect( + isFswCommand({ + type: 'fsw_command', + } as FswCommand), + ).toBeTruthy(); + + expect( + isFswCommand({ + type: 'hw_command', + } as HwCommand), + ).toBeFalsy(); + }); + + test('isHwCommand', () => { + expect( + isHwCommand({ + type: 'hw_command', + } as HwCommand), + ).toBeTruthy(); + + expect( + isHwCommand({ + type: 'fsw_command', + } as FswCommand), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentEnum', () => { + expect( + isFswCommandArgumentEnum({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeTruthy(); + + expect( + isFswCommandArgumentEnum({ + arg_type: 'boolean', + } as FswCommandArgumentBoolean), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentInteger', () => { + expect( + isFswCommandArgumentInteger({ + arg_type: 'integer', + } as FswCommandArgumentInteger), + ).toBeTruthy(); + + expect( + isFswCommandArgumentInteger({ + arg_type: 'float', + } as FswCommandArgumentFloat), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentFloat', () => { + expect( + isFswCommandArgumentFloat({ + arg_type: 'float', + } as FswCommandArgumentFloat), + ).toBeTruthy(); + + expect( + isFswCommandArgumentFloat({ + arg_type: 'integer', + } as FswCommandArgumentInteger), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentNumeric', () => { + expect( + isFswCommandArgumentNumeric({ + arg_type: 'numeric', + } as FswCommandArgumentNumeric), + ).toBeTruthy(); + + expect( + isFswCommandArgumentNumeric({ + arg_type: 'integer', + } as FswCommandArgumentInteger), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentUnsigned', () => { + expect( + isFswCommandArgumentUnsigned({ + arg_type: 'unsigned', + } as FswCommandArgumentUnsigned), + ).toBeTruthy(); + + expect( + isFswCommandArgumentUnsigned({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentRepeat', () => { + expect( + isFswCommandArgumentRepeat({ + arg_type: 'repeat', + } as FswCommandArgumentRepeat), + ).toBeTruthy(); + + expect( + isFswCommandArgumentRepeat({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentVarString', () => { + expect( + isFswCommandArgumentVarString({ + arg_type: 'var_string', + } as FswCommandArgumentVarString), + ).toBeTruthy(); + + expect( + isFswCommandArgumentVarString({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentFixedString', () => { + expect( + isFswCommandArgumentFixedString({ + arg_type: 'fixed_string', + } as FswCommandArgumentFixedString), + ).toBeTruthy(); + + expect( + isFswCommandArgumentFixedString({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeFalsy(); + }); + + test('isFswCommandArgumentBoolean', () => { + expect( + isFswCommandArgumentBoolean({ + arg_type: 'boolean', + } as FswCommandArgumentBoolean), + ).toBeTruthy(); + + expect( + isFswCommandArgumentBoolean({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeFalsy(); + }); + + test('isNumberArg', () => { + expect( + isNumberArg({ + arg_type: 'float', + } as FswCommandArgumentFloat), + ).toBeTruthy(); + expect( + isNumberArg({ + arg_type: 'integer', + } as FswCommandArgumentInteger), + ).toBeTruthy(); + expect( + isNumberArg({ + arg_type: 'numeric', + } as FswCommandArgumentNumeric), + ).toBeTruthy(); + expect( + isNumberArg({ + arg_type: 'unsigned', + } as FswCommandArgumentUnsigned), + ).toBeTruthy(); + + expect( + isNumberArg({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeFalsy(); + }); + + test('isStringArg', () => { + expect( + isStringArg({ + arg_type: 'var_string', + } as FswCommandArgumentVarString), + ).toBeTruthy(); + expect( + isStringArg({ + arg_type: 'fixed_string', + } as FswCommandArgumentFixedString), + ).toBeTruthy(); + + expect( + isStringArg({ + arg_type: 'enum', + } as FswCommandArgumentEnum), + ).toBeFalsy(); + }); +}); describe('getDefaultVariableArgs', () => { const mockParameters = [ { name: 'exampleString', type: 'STRING' }, diff --git a/src/utilities/codemirror/codemirror-utils.ts b/src/utilities/codemirror/codemirror-utils.ts index 454aff9a17..5dc316e65f 100644 --- a/src/utilities/codemirror/codemirror-utils.ts +++ b/src/utilities/codemirror/codemirror-utils.ts @@ -1,6 +1,7 @@ import type { SyntaxNode } from '@lezer/common'; import type { CommandDictionary, + FswCommand, FswCommandArgument, FswCommandArgumentBoolean, FswCommandArgumentEnum, @@ -11,12 +12,21 @@ import type { FswCommandArgumentRepeat, FswCommandArgumentUnsigned, FswCommandArgumentVarString, + HwCommand, } from '@nasa-jpl/aerie-ampcs'; import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; +import type { ArgTextDef, NumberArg, StringArg } from '../../types/sequencing'; import { fswCommandArgDefault } from '../sequence-editor/command-dictionary'; import type { CommandInfoMapper } from './commandInfoMapper'; +export function isFswCommand(command: FswCommand | HwCommand): command is FswCommand { + return (command as FswCommand).type === 'fsw_command'; +} +export function isHwCommand(command: FswCommand | HwCommand): command is HwCommand { + return (command as HwCommand).type === 'hw_command'; +} + export function isFswCommandArgumentEnum(arg: FswCommandArgument): arg is FswCommandArgumentEnum { return arg.arg_type === 'enum'; } @@ -66,22 +76,6 @@ export function isStringArg(arg: FswCommandArgument): arg is StringArg { return isFswCommandArgumentVarString(arg) || isFswCommandArgumentFixedString(arg); } -export type StringArg = FswCommandArgumentVarString | FswCommandArgumentFixedString; - -export type NumberArg = - | FswCommandArgumentFloat - | FswCommandArgumentInteger - | FswCommandArgumentNumeric - | FswCommandArgumentUnsigned; - -export type ArgTextDef = { - argDef?: FswCommandArgument; - children?: ArgTextDef[]; - node?: SyntaxNode; - parentArgDef?: FswCommandArgumentRepeat; - text?: string; -}; - export function addDefaultArgs( commandDictionary: CommandDictionary, view: EditorView, diff --git a/src/utilities/time.ts b/src/utilities/time.ts index 508f2c1418..dc4e3ad530 100644 --- a/src/utilities/time.ts +++ b/src/utilities/time.ts @@ -261,7 +261,6 @@ export function parseDurationString( // Normalize microseconds millisecond += Math.floor(microsecond / 1000000); microsecond = microsecond % 1000000; - // eslint-disable-next-line @typescript-eslint/no-unused-vars // Normalize milliseconds and seconds second += Math.floor(millisecond / 1000);