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}