diff --git a/Localize/lang/strings.json b/Localize/lang/strings.json index 294f4078ef4..aead90b591b 100644 --- a/Localize/lang/strings.json +++ b/Localize/lang/strings.json @@ -1,6 +1,7 @@ { "++ZVe/": "Testing", "+0H8Or": "Warning: input node type does not match the schema node's type", + "+0ua83": "Parameters", "+0yxlR": "Function display", "+3rROX": "Protected", "+5Jp42": "By", @@ -12,6 +13,7 @@ "+KXX+O": "Select function from", "+M72+a": "Overview", "+R90eK": "Retry policy interval is invalid, must match ISO 8601 duration format", + "+SHn9P": "Save blank unit test", "+Uvo/p": "Alt text for down chevron", "+ZSBrq": "Context menu for {title} card", "+ijo/2": "Paste last used expression", @@ -277,6 +279,7 @@ "7ZR1xr": "Add an action", "7aJqIH": "Optional. The locale to be used when formatting (defaults to 'en-us').", "7adnmH": "Back to template library", + "7eo4/d": "Create unit test", "7fZkLA": "Disable static result", "7gUE8h": "This will revert your workflow to the state it was in before Copilot's edit. If you made additional edits to the workflow after Copilot's, you will lose them. This action cannot be undone. Do you want to continue?", "7jcTNd": "Enter a valid email.", @@ -644,6 +647,7 @@ "Lub7NN": "Required. The expressions that may be true.", "LvLksz": "Loading outputs", "Lx8HRl": "(UTC+02:00) Damascus", + "LxRzQm": "Assertions", "M/gUE8": "About", "M/m3nG": "Action timeout", "M0bbZk": "Flow Checker", @@ -780,6 +784,7 @@ "QBK72a": "Add custom modules, uncover new scenarios, and find troubleshooting tips", "QGbUXX": "Status code", "QNfUf/": "Full screen", + "QQmbz+": "Save unit test definition", "QVtqAn": "Description", "QZBPUx": "Returns a single value matching the key name from form-data or form-encoded trigger output", "QZrxUk": "String functions", @@ -1051,9 +1056,11 @@ "Zi9gQK": "Add new item", "ZihyUf": "Close", "ZkjTbp": "Learn more about dynamic content.", + "ZvAp7m": "Save", "ZyDq4/": "Show a different suggestion", "_++ZVe/.comment": "Title for testing section", "_+0H8Or.comment": "Warning message for when input node type does not match schema node type", + "_+0ua83.comment": "Button text for parameters", "_+0yxlR.comment": "Label for the function display radio group", "_+3rROX.comment": "Label in the chatbot header stating that the users information is protected in this chatbot", "_+5Jp42.comment": "Title for publisher", @@ -1065,6 +1072,7 @@ "_+KXX+O.comment": "Path to the function to select", "_+M72+a.comment": "Button text for whole overview", "_+R90eK.comment": "error message for invalid retry interval", + "_+SHn9P.comment": "Button test for save blank unit test", "_+Uvo/p.comment": "Alt text for down chevron", "_+ZSBrq.comment": "Accessibility label", "_+ijo/2.comment": "Token picker for 'Paste last used expression'", @@ -1330,6 +1338,7 @@ "_7ZR1xr.comment": "Text on example action node", "_7aJqIH.comment": "Optional locale parameter to apply formatNumber function with", "_7adnmH.comment": "Button to navigate back to the template library", + "_7eo4/d.comment": "Button text for create unit test", "_7fZkLA.comment": "Label for toggle to disable static result", "_7gUE8h.comment": "Warning description of what undoing operation will do to the workflow", "_7jcTNd.comment": "Error validation message for emails", @@ -1697,6 +1706,7 @@ "_Lub7NN.comment": "Required expression parameters to apply or function", "_LvLksz.comment": "Loading outputs text", "_Lx8HRl.comment": "Time zone value ", + "_LxRzQm.comment": "Button text for unit test asssertions", "_M/gUE8.comment": "The tab label for the about tab on the operation panel", "_M/m3nG.comment": "title for action timeout setting", "_M0bbZk.comment": "Header for the errors panel", @@ -1833,6 +1843,7 @@ "_QBK72a.comment": "This is a message give link to user to find out more about this action", "_QGbUXX.comment": "Response status code for test map API", "_QNfUf/.comment": "Full Screen token picker", + "_QQmbz+.comment": "Button text for save unit test definition", "_QVtqAn.comment": "Label for description column.", "_QZBPUx.comment": "Label for description of custom triggerFormDataValue Function", "_QZrxUk.comment": "Label for string functions", @@ -2104,6 +2115,7 @@ "_Zi9gQK.comment": "Label to add item to property editor", "_ZihyUf.comment": "Label for the close button in the chatbot header", "_ZkjTbp.comment": "Text for dynamic content link", + "_ZvAp7m.comment": "Button text for save", "_ZyDq4/.comment": "Text for the show different suggestion flow button", "_a7j3gS.comment": "Required number parameter to divide in mod function", "_aAXnqw.comment": "Required number of occurrences to get nthIndexOf function with", @@ -2525,6 +2537,7 @@ "_oZMhX/.comment": "Text of Tooltip to expand", "_ocW+RF.comment": "Title for the details section in the template overview tab", "_odQ554.comment": "Response body for test map API", + "_ohOaXj.comment": "Button text for errors", "_ohpbkw.comment": "title for retry policy exponential interval setting", "_onXUu0.comment": "Text to tell users to click to add comments", "_oqgNX3.comment": "Accessibility label for workflow name", @@ -2620,6 +2633,7 @@ "_sFbnCs.comment": "Time zone value ", "_sFwHQc.comment": "aria label description for cancel button", "_sKy720.comment": "Error message when the workflow name is empty.", + "_sOnphB.comment": "Button text for resubmit", "_sRpETS.comment": "Warning message for when custom value does not match schema node type", "_sVQe34.comment": "The description for the test tab parameters.", "_sVcvcG.comment": "The tab label for the monitoring name and state tab on the create workflow panel", @@ -3287,6 +3301,7 @@ "oZMhX/": "Expand", "ocW+RF": "Details", "odQ554": "Response body", + "ohOaXj": "Errors", "ohpbkw": "Exponential interval", "onXUu0": "Add a note", "oqgNX3": "Workflow name", @@ -3382,6 +3397,7 @@ "sFbnCs": "(UTC-05:00) Chetumal", "sFwHQc": "Cancel creating a connection", "sKy720": "Must provide value for workflow name.", + "sOnphB": "Resubmit", "sRpETS": "Warning: custom value does not match the schema node's type", "sVQe34": "Provide parameters to test the output.", "sVcvcG": "Basics", diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBar.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBar.tsx index 96941e8c5ab..7bdda476b2c 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBar.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/DesignerCommandBar.tsx @@ -19,6 +19,7 @@ import { useNodesInitialized, serializeUnitTestDefinition, useAssertionsValidationErrors, + getNodeOutputOperations, getCustomCodeFilesWithData, resetDesignerDirtyState, collapsePanel, @@ -126,6 +127,14 @@ export const DesignerCommandBar = ({ alert('Check console for unit test serialization'); }); + const { isLoading: isSavingBlankUnitTest, mutate: saveBlankUnitTestMutate } = useMutation(async () => { + const designerState = DesignerStore.getState(); + const operationContents = await getNodeOutputOperations(designerState); + + console.log(operationContents); + alert('Check console for blank unit test operationContents'); + }); + const { isLoading: isDownloadingDocument, mutate: downloadDocument } = useMutation(async () => { const designerState = DesignerStore.getState(); const workflow = await serializeWorkflow(designerState); @@ -161,6 +170,7 @@ export const DesignerCommandBar = ({ const haveSettingsErrors = Object.keys(allSettingsErrors ?? {}).length > 0; const allConnectionErrors = useAllConnectionErrors(); const haveConnectionErrors = Object.keys(allConnectionErrors ?? {}).length > 0; + const saveBlankUnitTestIsDisabled = !isUnitTest || isSavingBlankUnitTest || haveAssertionErrors; const haveErrors = useMemo( () => allInputErrors.length > 0 || haveWorkflowParameterErrors || haveSettingsErrors || haveConnectionErrors, @@ -187,6 +197,7 @@ export const DesignerCommandBar = ({ ); }, onClick: () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions isDesignerView ? saveWorkflowMutate() : saveWorkflowFromCode(() => dispatch(resetDesignerDirtyState(undefined))); }, }, @@ -205,6 +216,20 @@ export const DesignerCommandBar = ({ saveUnitTestMutate(); }, }, + { + key: 'saveBlankUnitTest', + text: 'Save Blank Unit Test', + onRenderIcon: () => { + return isSavingBlankUnitTest ? ( + + ) : ( + + ); + }, + onClick: () => { + saveBlankUnitTestMutate(); + }, + }, { key: 'discard', disabled: isSaving || !isDesignerView, @@ -323,6 +348,8 @@ export const DesignerCommandBar = ({ isSaving, isDesignerView, showConnectionsPanel, + isSavingBlankUnitTest, + saveBlankUnitTestIsDisabled, haveErrors, isDarkMode, isCopilotReady, diff --git a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts index 01a40707de2..d7177f3616d 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts @@ -38,7 +38,10 @@ import { exec } from 'child_process'; import { writeFileSync, readFileSync } from 'fs'; import * as path from 'path'; import { env, ProgressLocation, Uri, ViewColumn, window, workspace } from 'vscode'; +import * as vscode from 'vscode'; import type { WebviewPanel, ProgressOptions } from 'vscode'; +import type { IAzureConnectorsContext } from '../azureConnectorWizard'; +import { saveBlankUnitTest } from '../unitTest/saveBlankUnitTest'; export default class OpenDesignerForLocalProject extends OpenDesignerBase { private readonly workflowFilePath: string; @@ -197,6 +200,10 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase { await this.validateWorkflow(this.panelMetadata.workflowContent); break; } + case ExtensionCommand.saveBlankUnitTest: { + await saveBlankUnitTest(this.context as IAzureConnectorsContext, vscode.Uri.file(this.workflowFilePath), msg.definition); + break; + } case ExtensionCommand.saveUnitTest: { await saveUnitTestDefinition(this.projectPath, this.workflowName, this.unitTestName, msg.definition); break; diff --git a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts index 83f0973da18..fe369ccc05c 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts @@ -27,6 +27,7 @@ import * as vscode from 'vscode'; import type { WebviewPanel } from 'vscode'; import { Uri, ViewColumn } from 'vscode'; import { getArtifactsInLocalProject } from '../../../utils/codeless/artifacts'; +import { saveBlankUnitTest } from '../unitTest/saveBlankUnitTest'; export default class OpenMonitoringViewForLocal extends OpenMonitoringViewBase { private projectPath: string | undefined; @@ -141,6 +142,10 @@ export default class OpenMonitoringViewForLocal extends OpenMonitoringViewBase { await createUnitTest(this.context as IAzureConnectorsContext, vscode.Uri.file(this.workflowFilePath), message.runId); break; } + case ExtensionCommand.saveBlankUnitTest: { + await saveBlankUnitTest(this.context as IAzureConnectorsContext, vscode.Uri.file(this.workflowFilePath), message.definition); + break; + } default: break; } diff --git a/apps/vs-code-designer/src/app/commands/workflows/openOverview.ts b/apps/vs-code-designer/src/app/commands/workflows/openOverview.ts index 10d3e8ffc21..31b6878de77 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openOverview.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openOverview.ts @@ -24,6 +24,7 @@ import { getWorkflowNode } from '../../utils/workspace'; import type { IAzureConnectorsContext } from './azureConnectorWizard'; import { openMonitoringView } from './openMonitoringView/openMonitoringView'; import { createUnitTest } from './unitTest/createUnitTest'; +import { saveBlankUnitTest } from './unitTest/saveBlankUnitTest'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import type { ICallbackUrlResponse } from '@microsoft/vscode-extension-logic-apps'; import { ExtensionCommand, ProjectName } from '@microsoft/vscode-extension-logic-apps'; @@ -161,6 +162,10 @@ export async function openOverview(context: IAzureConnectorsContext, node: vscod await createUnitTest(context, workflowNode as vscode.Uri, message.runId); break; } + case ExtensionCommand.saveBlankUnitTest: { + await saveBlankUnitTest(this.context as IAzureConnectorsContext, vscode.Uri.file(this.workflowFilePath), message.definition); + break; + } default: break; } @@ -194,6 +199,7 @@ async function getLocalWorkflowCallbackInfo( method: HTTP_METHODS.POST, }); return JSON.parse(response); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return undefined; } diff --git a/apps/vs-code-designer/src/app/utils/unitTests.ts b/apps/vs-code-designer/src/app/utils/unitTests.ts index d22fb53c890..fa8ef666bc1 100644 --- a/apps/vs-code-designer/src/app/utils/unitTests.ts +++ b/apps/vs-code-designer/src/app/utils/unitTests.ts @@ -58,6 +58,53 @@ export const saveUnitTestDefinition = async ( }); }; +/** + * Saves the blank unit test definition for a workflow. + * @param {string} projectPath The path of the project. + * @param {string} workflowName The name of the workflow. + * @param {string} unitTestName The name of the unit test. + * @param {any} unitTestDefinition The unit test definition. + * @returns A Promise that resolves when the unit test definition is saved. + */ +export const saveBlankUnitTestDefinition = async ( + projectPath: string, + workflowName: string, + unitTestName: string, + unitTestDefinition: any +): Promise => { + await callWithTelemetryAndErrorHandling(saveUnitTestEvent, async () => { + const options: vscode.ProgressOptions = { + location: vscode.ProgressLocation.Notification, + title: localize('azureFunctions.savingWorkflow', 'Saving Blank Unit Test Definition...'), + }; + + await vscode.window.withProgress(options, async () => { + const projectName = path.basename(projectPath); + const testsDirectory = getTestsDirectory(projectPath); + const unitTestsPath = getUnitTestsPath(testsDirectory.fsPath, projectName, workflowName, unitTestName); + const workflowTestsPath = getWorkflowTestsPath(testsDirectory.fsPath, projectName, workflowName); + + if (!fs.existsSync(workflowTestsPath)) { + fs.mkdirSync(workflowTestsPath, { recursive: true }); + } + try { + fs.writeFileSync(unitTestsPath, JSON.stringify(unitTestDefinition, null, 4)); + await vscode.workspace.updateWorkspaceFolders( + vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, + null, + { uri: testsDirectory } + ); + } catch (error) { + vscode.window.showErrorMessage( + `${localize('saveFailure', 'Blank Unit Test Definition not saved.')} ${error.message}`, + localize('OK', 'OK') + ); + throw error; + } + }); + }); +}; + /** * Retrieves the name of the unit test from the given file path. * @param {string} filePath - The path of the unit test file. diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index 3adedde0753..ddada29b108 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -173,6 +173,7 @@ export const extensionCommand = { dataMapSaveMapDefinition: 'azureLogicAppsStandard.dataMap.saveMapDefinition', dataMapSaveMapXslt: 'azureLogicAppsStandard.dataMap.saveMapXslt', createUnitTest: 'azureLogicAppsStandard.createUnitTest', + saveBlankUnitTest: 'azureLogicAppsStandard.saveBlankUnitTest', editUnitTest: 'azureLogicAppsStandard.editUnitTest', openUnitTestResults: 'azureLogicAppsStandard.openUnitTestResults', runUnitTest: 'azureLogicAppsStandard.runUnitTest', diff --git a/apps/vs-code-designer/src/package.json b/apps/vs-code-designer/src/package.json index e446329846c..f5dca25a4f5 100644 --- a/apps/vs-code-designer/src/package.json +++ b/apps/vs-code-designer/src/package.json @@ -345,6 +345,11 @@ "category": "Azure Logic Apps", "icon": "$(tasklist)" }, + { + "command": "azureLogicAppsStandard.saveBlankUnitTest", + "title": "Create blank unit test", + "category": "Azure Logic Apps" + }, { "command": "azureLogicAppsStandard.editUnitTest", "title": "Edit unit test", @@ -709,6 +714,11 @@ "when": "false", "group": "navigation@4" }, + { + "command": "azureLogicAppsStandard.saveBlankUnitTest", + "when": "resourceFilename==workflow.json", + "group": "navigation@4" + }, { "command": "azureLogicAppsStandard.openUnitTestResults", "when": "false", diff --git a/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx b/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx index 8cef5308f0a..73f927894ee 100644 --- a/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx +++ b/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx @@ -6,6 +6,7 @@ import { serializeWorkflow as serializeBJSWorkflow, store as DesignerStore, serializeUnitTestDefinition, + getNodeOutputOperations, useIsDesignerDirty, validateParameter, updateParameterValidation, @@ -94,6 +95,16 @@ export const DesignerCommandBar: React.FC = ({ }); }); + const { isLoading: isSavingBlankUnitTest, mutate: saveBlankUnitTestMutate } = useMutation(async () => { + const designerState = DesignerStore.getState(); + const definition = await getNodeOutputOperations(designerState); + + await vscode.postMessage({ + command: ExtensionCommand.saveBlankUnitTest, + definition, + }); + }); + const onResubmit = async () => { vscode.postMessage({ command: ExtensionCommand.resubmitRun, @@ -148,6 +159,11 @@ export const DesignerCommandBar: React.FC = ({ id: 'LxRzQm', description: 'Button text for unit test asssertions', }), + UNIT_TEST_SAVE_BLANK: intl.formatMessage({ + defaultMessage: 'Save blank unit test', + id: '+SHn9P', + description: 'Button test for save blank unit test', + }), }; const iconClass = mergeStyles({ @@ -179,6 +195,7 @@ export const DesignerCommandBar: React.FC = ({ const haveAssertionErrors = Object.keys(allAssertionsErrors ?? {}).length > 0; const isSaveUnitTestDisabled = isSavingUnitTest || haveAssertionErrors; + const isSaveBlankUnitTestDisabled = isSavingBlankUnitTest || haveAssertionErrors; const haveErrors = useMemo( () => haveInputErrors || haveWorkflowParameterErrors || haveSettingsErrors || haveConnectionErrors, [haveInputErrors, haveWorkflowParameterErrors, haveSettingsErrors, haveConnectionErrors] @@ -269,6 +286,26 @@ export const DesignerCommandBar: React.FC = ({ onCreateUnitTest(); }, }, + { + key: 'SaveBlank', + disabled: isDisabled, + text: Resources.UNIT_TEST_SAVE_BLANK, + ariaLabel: Resources.UNIT_TEST_SAVE_BLANK, + onRenderIcon: () => { + return isSavingBlankUnitTest ? ( + + ) : ( + + ); + }, + onClick: () => { + saveBlankUnitTestMutate(); + }, + }, ] : []), ]; @@ -294,6 +331,26 @@ export const DesignerCommandBar: React.FC = ({ saveUnitTestMutate(); }, }, + { + key: 'SaveBlank', + disabled: isSaveBlankUnitTestDisabled, + text: Resources.UNIT_TEST_SAVE_BLANK, + ariaLabel: Resources.UNIT_TEST_SAVE_BLANK, + onRenderIcon: () => { + return isSavingBlankUnitTest ? ( + + ) : ( + + ); + }, + onClick: () => { + saveBlankUnitTestMutate(); + }, + }, { key: 'Assertions', text: Resources.UNIT_TEST_ASSERTIONS, diff --git a/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts b/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts index 9b990241b83..6334066dc86 100644 --- a/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts +++ b/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts @@ -3,7 +3,7 @@ import type { ConnectionReferences, Workflow, WorkflowParameter } from '../../.. import type { WorkflowNode } from '../../parsers/models/workflowNode'; import { getConnectorWithSwagger } from '../../queries/connections'; import { getOperationManifest } from '../../queries/operation'; -import type { NodeInputs, NodeOperation, ParameterGroup } from '../../state/operation/operationMetadataSlice'; +import type { NodeInputs, NodeOperation, NodeOutputs, ParameterGroup } from '../../state/operation/operationMetadataSlice'; import { ErrorLevel } from '../../state/operation/operationMetadataSlice'; import { getOperationInputParameters } from '../../state/operation/operationSelector'; import type { OutputMock } from '../../state/unitTest/unitTestInterfaces'; @@ -1119,6 +1119,19 @@ export const serializeUnitTestDefinition = async (rootState: RootState): Promise }; }; +/** + * Gets the node output operations based on the provided root state. + * @param {RootState} rootState The root state object containing the current designer state. + * @returns A promise that resolves to the serialized unit test definition. + */ +export const getNodeOutputOperations = (state: RootState) => { + const outputOperations: { operationInfo: Record; outputParameters: Record } = { + operationInfo: state.operations.operationInfo, + outputParameters: state.operations.outputParameters, + }; + return outputOperations; +}; + /** * Retrieves an array of Assertion objects based on the provided Assertion definitions. * @param {Record} assertions - The Assertion definitions. diff --git a/libs/designer/src/lib/core/index.ts b/libs/designer/src/lib/core/index.ts index 64ab6db3c48..72d773c5a5d 100644 --- a/libs/designer/src/lib/core/index.ts +++ b/libs/designer/src/lib/core/index.ts @@ -35,7 +35,12 @@ export { export { useIsDesignerDirty, resetDesignerDirtyState } from './state/global'; export { useAllSettingsValidationErrors } from './state/setting/settingSelector'; export { useAllConnectionErrors } from './state/operation/operationSelector'; -export { serializeWorkflow, serializeUnitTestDefinition, parseWorkflowParameterValue } from './actions/bjsworkflow/serializer'; +export { + serializeWorkflow, + serializeUnitTestDefinition, + getNodeOutputOperations, + parseWorkflowParameterValue, +} from './actions/bjsworkflow/serializer'; export { setSelectedNodeId, changePanelNode, diff --git a/libs/vscode-extension/src/lib/models/extensioncommand.ts b/libs/vscode-extension/src/lib/models/extensioncommand.ts index 53c59344229..5c927930077 100644 --- a/libs/vscode-extension/src/lib/models/extensioncommand.ts +++ b/libs/vscode-extension/src/lib/models/extensioncommand.ts @@ -42,6 +42,7 @@ export const ExtensionCommand = { webviewLoaded: 'webviewLoaded', webviewRscLoadError: 'webviewRscLoadError', saveUnitTest: 'saveUnitTest', + saveBlankUnitTest: 'saveBlankUnitTest', createUnitTest: 'createUnitTest', viewWorkflow: 'viewWorkflow', openRelativeLink: 'openRelativeLink',