diff --git a/e2e-tests/tests/workspace.test.ts b/e2e-tests/tests/workspace.test.ts index bd3dc33039..6867cd25eb 100644 --- a/e2e-tests/tests/workspace.test.ts +++ b/e2e-tests/tests/workspace.test.ts @@ -590,7 +590,7 @@ test.describe.serial('Workspace', () => { test('Edit and save user metadata', async () => { // Create a file - const { sequenceName } = await workspace.createSequence(undefined, `${generateRandomName()}.seq`); + const { sequenceName } = await workspace.createSequence(undefined, `${generateRandomName()}.seqN.txt`); await workspace.searchForFileAndWait(sequenceName); await workspace.clickFile(sequenceName); @@ -631,7 +631,7 @@ test.describe.serial('Workspace', () => { test('Cancel discards user metadata changes', async () => { // Create a file - const { sequenceName } = await workspace.createSequence(undefined, `${generateRandomName()}.seq`); + const { sequenceName } = await workspace.createSequence(undefined, `${generateRandomName()}.seqN.txt`); await workspace.searchForFileAndWait(sequenceName); await workspace.clickFile(sequenceName); @@ -657,7 +657,7 @@ test.describe.serial('Workspace', () => { test('Invalid JSON disables user metadata save button', async () => { // Create a file - const { sequenceName } = await workspace.createSequence(undefined, `${generateRandomName()}.seq`); + const { sequenceName } = await workspace.createSequence(undefined, `${generateRandomName()}.seqN.txt`); await workspace.searchForFileAndWait(sequenceName); await workspace.clickFile(sequenceName); @@ -688,8 +688,8 @@ test.describe.serial('Workspace', () => { test('Switching files discards unsaved user metadata edits', async () => { // Create two files - const { sequenceName: file1 } = await workspace.createSequence(undefined, `${generateRandomName()}.seq`); - const { sequenceName: file2 } = await workspace.createSequence(undefined, `${generateRandomName()}.seq`); + const { sequenceName: file1 } = await workspace.createSequence(undefined, `${generateRandomName()}.seqN.txt`); + const { sequenceName: file2 } = await workspace.createSequence(undefined, `${generateRandomName()}.seqN.txt`); // Open first file and start editing metadata await workspace.searchForFileAndWait(file1); diff --git a/src/components/sequencing/EditorToolbar.svelte b/src/components/sequencing/EditorToolbar.svelte index 4481012a19..7aae1d7aec 100644 --- a/src/components/sequencing/EditorToolbar.svelte +++ b/src/components/sequencing/EditorToolbar.svelte @@ -1,8 +1,8 @@ + +
+ {#if outputLanguages.length > 0} + + + {/if} +
+ + + + + + + + + + +
+
diff --git a/src/components/sequencing/SequenceEditor.svelte b/src/components/sequencing/SequenceEditor.svelte index 7c358cfabe..81f7a653b4 100644 --- a/src/components/sequencing/SequenceEditor.svelte +++ b/src/components/sequencing/SequenceEditor.svelte @@ -13,10 +13,9 @@ PhoenixAdaptation, PhoenixContext, } from '@nasa-jpl/aerie-sequence-languages'; - import { Button, Label } from '@nasa-jpl/stellar-svelte'; import { basicSetup, EditorView } from 'codemirror'; import { debounce } from 'lodash-es'; - import { FileBracesCorner, PanelBottomClose, PanelBottomOpen } from 'lucide-svelte'; + import { FileBracesCorner } from 'lucide-svelte'; import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { clearWorkspaceAdaptationMessages } from '../../stores/workspaceErrors'; import type { ActionDefinition } from '../../types/actions'; @@ -26,22 +25,23 @@ import { blockTheme } from '../../utilities/codemirror/themes/block'; import { phoenixResources } from '../../utilities/sequence-editor/adaptation-resources'; import { showFailureToast, showSuccessToast } from '../../utilities/toast'; - import { replaceFileExtension } from '../../utilities/workspaces'; + import { doesFilenameMatchExtension, replaceFileExtension } from '../../utilities/workspaces'; import CssGrid from '../ui/CssGrid.svelte'; import CssGridGutter from '../ui/CssGridGutter.svelte'; import Panel from '../ui/Panel.svelte'; import SectionTitle from '../ui/SectionTitle.svelte'; - import Tooltip from '../ui/Tooltip.svelte'; import FileMetadataBanner from '../workspace/FileMetadataBanner.svelte'; import CommandPanel from './CommandPanel/CommandPanel.svelte'; import EditorToolbar from './EditorToolbar.svelte'; + import OutputToolbar from './OutputToolbar.svelte'; export let availableActions: { action: ActionDefinition; parameter: string }[] = []; export let fileMetadata: WorkspaceFileMetadata | null = null; - export let phoenixContext: PhoenixContext; + export let phoenixContext: PhoenixContext | undefined; export let includeActions: boolean = false; export let preserveAdaptationLog: boolean = false; export let isLoading: boolean = false; + export let isInputFile: boolean = false; export let previewOnly: boolean = false; export let readOnly: boolean = false; export let sequenceAdaptation: PhoenixAdaptation; @@ -58,7 +58,7 @@ const dispatch = createEventDispatcher<{ adaptationError: { error: Error; filePath: string }; downloadInput: { filePath: string }; - downloadOutput: { content: string; filePath: string; filename: string; outputLanguage: OutputLanguage }; + downloadOutput: { content: string; filePath: string; filename: string }; editorViewChange: EditorView | null; lintChange: { diagnostics: LintDiagnostic[]; filePath: string }; runAction: { action: ActionDefinition; parameter: string }; @@ -67,6 +67,7 @@ sequenceOutputUpdate: { filePath: string; output?: string }; }>(); + let commandFormBuilderGrid: string; let compartmentAdaptation: Compartment; let compartmentOutputAdaptation: Compartment; let compartmentReadonly: Compartment; @@ -93,12 +94,23 @@ $: commandInfoMapper = sequenceAdaptation.input.commandInfoMapper; - $: if (phoenixContext && sequenceAdaptation.input.getEditorExtension) { + // Only use input extensions if the sequence file matches the input file extensions + $: if (phoenixContext && isInputFile && sequenceAdaptation.input.getEditorExtension) { inputEditorExtension = sequenceAdaptation.input.getEditorExtension(phoenixContext, phoenixResources); + } else if (phoenixContext && sequenceAdaptation.outputs.length > 0) { + const matchingOutputLanguage = sequenceAdaptation.outputs.find(output => + doesFilenameMatchExtension(output.fileExtension, sequenceName), + ); + inputEditorExtension = matchingOutputLanguage?.getEditorExtension?.(phoenixContext, phoenixResources) ?? []; + } else { + inputEditorExtension = []; } - $: if (sequenceAdaptation.outputs.length > 0) { + // Only use output extensions if the sequence file is an input file + $: if (isInputFile && sequenceAdaptation.outputs.length > 0) { selectedOutputFormat = sequenceAdaptation.outputs[0]; + } else { + selectedOutputFormat = undefined; } $: if (phoenixContext && selectedOutputFormat?.getEditorExtension) { @@ -152,7 +164,7 @@ $: { previousShowOutputs = showOutputs; - showOutputs = sequenceAdaptation.outputs.length > 0; + showOutputs = isInputFile && sequenceAdaptation.outputs.length > 0; } $: if (showOutputs) { editorHeights = toggleSeqJsonPreview ? '1fr 3px 1fr' : '1.88fr 3px 80px'; @@ -217,25 +229,28 @@ } function updateOutputFormat(sequence: string): void { - let output: string | undefined; + if (phoenixContext) { + let output: string | undefined; - if (!preserveAdaptationLog) { - clearWorkspaceAdaptationMessages(); - } - try { - output = selectedOutputFormat?.toOutputFormat?.(sequence, phoenixContext, sequenceName); - } catch (e) { - console.error('Adaptation toOutputFormat error:', e); - if (sequenceFilePath) { - dispatch('adaptationError', { error: e as Error, filePath: sequenceFilePath }); + if (!preserveAdaptationLog) { + clearWorkspaceAdaptationMessages(); } - output = `// Error in adaptation toOutputFormat:\n// ${(e as Error).message}`; - } - editorOutputView.dispatch({ changes: { from: 0, insert: output ?? '', to: editorOutputView.state.doc.length } }); + try { + output = selectedOutputFormat?.toOutputFormat?.(sequence, phoenixContext, sequenceName); + } catch (e) { + console.error('Adaptation toOutputFormat error:', e); + if (sequenceFilePath) { + dispatch('adaptationError', { error: e as Error, filePath: sequenceFilePath }); + } + output = `// Error in adaptation toOutputFormat:\n// ${(e as Error).message}`; + } - if (output !== undefined) { - dispatch('sequenceOutputUpdate', { filePath: sequenceFilePath, output }); + editorOutputView.dispatch({ changes: { from: 0, insert: output ?? '', to: editorOutputView.state.doc.length } }); + + if (output !== undefined) { + dispatch('sequenceOutputUpdate', { filePath: sequenceFilePath, output }); + } } } @@ -261,16 +276,22 @@ } }, 300); - function downloadOutputFormat(outputLanguage: OutputLanguage): void { + function downloadOutputFormat(): void { const content = editorOutputView.state.doc.toString(); - const filename = replaceFileExtension( - sequenceName, - sequenceAdaptation.input.fileExtension, - outputLanguage.fileExtension, - ); + if (selectedOutputFormat) { + const filename = replaceFileExtension( + sequenceName, + sequenceAdaptation.input.fileExtension, + selectedOutputFormat.fileExtension, + ); - dispatch('downloadOutput', { content, filePath: sequenceFilePath, filename, outputLanguage }); + dispatch('downloadOutput', { + content, + filePath: sequenceFilePath, + filename, + }); + } } function downloadInputFormat(): void { @@ -305,7 +326,7 @@ function formatDocument() { let format = sequenceAdaptation.input.format; - if (format !== undefined) { + if (format !== undefined && phoenixContext) { format(editorSequenceView, phoenixContext); } } @@ -428,10 +449,6 @@ downloadDisabled={disableCopyAndExport} downloadTooltip="Download sequence contents" onDownload={downloadInputFormat} - outputFormats={showOutputs ? sequenceAdaptation.outputs : []} - outputDisabled={disableCopyAndExport} - onCopyOutput={copyOutputFormatToClipboard} - onDownloadOutput={downloadOutputFormat} showSaveButton={!(readOnly || previewOnly || isLoading)} saveDisabled={!isSequenceDefinitionUpdated} saveHighlighted={isSequenceDefinitionUpdated} @@ -455,30 +472,15 @@ {selectedOutputFormat?.name} (Read-only)
-
- {#if sequenceAdaptation.outputs.length > 0} - - - {/if} - - - - -
+
@@ -491,7 +493,7 @@ {#if showCommandFormBuilder} - {#if phoenixContext.commandDictionary !== null} + {#if phoenixContext && phoenixContext.commandDictionary !== null} {:else} diff --git a/src/routes/workspaces/[workspaceId]/+page.svelte b/src/routes/workspaces/[workspaceId]/+page.svelte index ba3fabaf53..1cf97b702b 100644 --- a/src/routes/workspaces/[workspaceId]/+page.svelte +++ b/src/routes/workspaces/[workspaceId]/+page.svelte @@ -8,8 +8,8 @@ import { env } from '$env/dynamic/public'; import type { ChannelDictionary, CommandDictionary, ParameterDictionary } from '@nasa-jpl/aerie-ampcs'; import type { + CommandInfoMapper, LibrarySequenceSignature, - OutputLanguage, PhoenixContext, UserSequence, } from '@nasa-jpl/aerie-sequence-languages'; @@ -97,6 +97,7 @@ WorkspaceNodesEvent, } from '../../../types/workspace'; import type { + WorkspaceFileMetadata, WorkspaceTreeMap, WorkspaceTreeNode, WorkspaceTreeNodeWithFullPath, @@ -114,6 +115,7 @@ import { showFailureToast, showSuccessToast } from '../../../utilities/toast'; import { computeMovedFilePath, + doesFilenameMatchExtension, downloadWorkspaceNodesAsZip, findNodeAffectingPath, flattenWorkspaceTreeWithPaths, @@ -144,11 +146,14 @@ let activeFileIsSequence: boolean = false; let actionDetailIsDirty: boolean = false; + let activeFileMetadata: WorkspaceFileMetadata | null = null; + let activeFileIsInputSequence: boolean = false; let availableActionsForActiveFile: ActionParameterPair[] = []; let panelsReady: boolean = false; let allActionsForWorkspace: ActionDefinition[] = []; let channelDictionary: ChannelDictionary | null = null; let commandDictionary: CommandDictionary | null = null; + let commandInfoMapper: CommandInfoMapper | null = null; let consolePaneApi: PaneAPI; let leftPaneApi: PaneAPI; let leftPanelActiveTab: string = @@ -161,6 +166,7 @@ let hasEditWorkspaceCollaboratorsPermission: boolean = false; let hasRunActionPermission: boolean = false; let isConsoleExpanded: boolean = false; + let isFileReadOnly: boolean = false; let parameterDictionaries: ParameterDictionary[] = []; let phoenixContext: PhoenixContext; let isWorkspaceLoading: boolean = false; @@ -176,6 +182,7 @@ let showLoadingSpinner: boolean = false; let librarySequences: LibrarySequenceSignature[] = []; let loadingSpinnerTimeout: ReturnType | null = null; + let logLevelLabel: string = 'Default levels'; let logLevels: LogLevel[] = defaultLogLevels; let preserveAdaptationLog: boolean = false; let workspaceSequences: UserSequence[] = []; @@ -280,16 +287,23 @@ $: activeFileIsSequence = $activeDocumentPath === null || ($activeDocument.type !== null && $activeDocument.type === WorkspaceContentType.Sequence); + $: { + activeFileIsInputSequence = + activeFileIsSequence && + (!$activeDocument.fileName || + (!!$activeDocument.fileName && + doesFilenameMatchExtension($sequenceAdaptation.input.fileExtension, $activeDocument.fileName))); + } $: commandInfoMapper = $sequenceAdaptation.input.commandInfoMapper; $: isFileReadOnly = activeFileMetadata?.readOnly ?? false; // Auto-switch the right-panel tab only when the editor crosses between sequence mode and // non-sequence mode. Switching between two sequence files (or between blank and a sequence // file) preserves whatever tab the user last chose. - let previousActiveFileIsSequence: boolean = activeFileIsSequence; - $: if (activeFileIsSequence !== previousActiveFileIsSequence) { - previousActiveFileIsSequence = activeFileIsSequence; - if (!activeFileIsSequence) { + let previousActiveFileIsInputSequence: boolean = activeFileIsInputSequence; + $: if (activeFileIsInputSequence !== previousActiveFileIsInputSequence) { + previousActiveFileIsInputSequence = activeFileIsInputSequence; + if (!activeFileIsInputSequence) { rightPanelActiveTab = 'metadata'; } else { rightPanelActiveTab = 'command'; @@ -1215,9 +1229,7 @@ onDownloadFile(filePath); } - async function onDownloadOutput( - event: CustomEvent<{ content: string; filePath: string; filename: string; outputLanguage: OutputLanguage }>, - ) { + async function onDownloadOutput(event: CustomEvent<{ content: string; filePath: string; filename: string }>) { const { content, filePath, filename } = event.detail; // Check if downloading output for the active file with unsaved changes @@ -1429,6 +1441,7 @@ fileMetadata={activeFileMetadata} includeActions={hasRunActionPermission} isLoading={$activeDocumentIsLoading} + isInputFile={activeFileIsInputSequence} onReadOnlyChange={readOnly => onReadOnlyChange(readOnly)} {preserveAdaptationLog} previewOnly={!hasEditFilePermission} @@ -1542,7 +1555,7 @@ filePath={$activeDocumentPath} fileMetadata={activeFileMetadata} hasEditPermission={hasEditFilePermission} - isSequenceFile={activeFileIsSequence} + isSequenceFile={activeFileIsInputSequence} {phoenixContext} {commandInfoMapper} on:updateUserMetadata={onUpdateUserMetadata} @@ -1557,7 +1570,7 @@ bind:activeTab={rightPanelActiveTab} bind:panelOpen={rightPanelOpen} commandNodeName={rightPanelCommandNodeName} - isSequenceFile={activeFileIsSequence} + isSequenceFile={activeFileIsInputSequence} /> {/if}