Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Explore how to use MetaConfigurator with real-world examples:

* **[External References](./documentation_user/examples/external_references)** – Learn how to handle external references in MetaConfigurator.

* **[RDF Panel](./documentation_user/examples/rdf)** – Learn how to use MetaConfigurator to explore Semantic data.
* **[RDF Panel](./documentation_user/examples/rdf)** – Learn how to use MetaConfigurator to create, explore and edit Semantic RDF data.

📚 Read the full [User Documentation](./documentation_user).

Expand Down
70 changes: 70 additions & 0 deletions meta_configurator/e2e/csvImport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {test, expect} from '@playwright/test';
import {openApp} from './utils';
import {tpGetData} from './utilsTestPanel';
import {
expandImportOptions,
openCsvImportDialog,
setColumnPath,
setCsvTablePath,
submitCsvImport,
uploadCsvFile,
uploadCsvFileAndCheckProgress,
} from './utilsCsvImport';
import {SessionMode} from '../src/store/sessionMode';

test('Import CSV as standalone table with default paths', async ({page}) => {
await openApp(page, 'settings_testpanel.json');

await openCsvImportDialog(page);
await uploadCsvFile(page, 'data_people.csv');

// submit without changing any options — default table path comes from the filename
await submitCsvImport(page);

// filename "data_people.csv" → stringToIdentifier strips underscores → key is "datapeople"
const data = await tpGetData(page, SessionMode.DataEditor);
expect(data).toHaveProperty('datapeople');
expect(data.datapeople).toHaveLength(2);
expect(data.datapeople[0]).toMatchObject({name: 'Alice', city: 'Berlin', role: 'Engineer'});
expect(data.datapeople[1]).toMatchObject({name: 'Bob', city: 'Munich', role: 'Designer'});
});

test('Import CSV with custom table path and renamed column', async ({page}) => {
await openApp(page, 'settings_testpanel.json');

await openCsvImportDialog(page);
await uploadCsvFile(page, 'data_people.csv');

await expandImportOptions(page);
await setCsvTablePath(page, 'people');
await setColumnPath(page, 'city', 'location');

await submitCsvImport(page);

const data = await tpGetData(page, SessionMode.DataEditor);
expect(data).toEqual({
people: [
{name: 'Alice', location: 'Berlin', role: 'Engineer'},
{name: 'Bob', location: 'Munich', role: 'Designer'},
],
});
});

test('CSV file upload progresses reliably for 15 consecutive attempts in the same session without reload', async ({page}) => {
test.setTimeout(60000);
await openApp(page, 'settings_testpanel.json');

for (let attempt = 1; attempt <= 15; attempt++) {
await test.step(`csv upload attempt ${attempt}`, async () => {
await openCsvImportDialog(page);
try {
await uploadCsvFileAndCheckProgress(page, 'data_people.csv', 4000);
} catch (error) {
throw new Error(`CSV upload attempt ${attempt} got stuck before parsing completed`, {
cause: error instanceof Error ? error : undefined,
});
}
await submitCsvImport(page);
});
}
});
5 changes: 4 additions & 1 deletion meta_configurator/e2e/panelGuiEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ test('Edit the feature testing example schema using the GUI Editor, testing basi

// Set the heightInMeter property to the value 10
await editNumberOrIntProperty(page, ['heightInMeter'], 10)
await expect
.poll(async () => (await tpGetData(page, SessionMode.DataEditor)).heightInMeter)
.toBe(10)

// Expect a Schema Violation Symbol because the height value is invalid
await checkPropertySchemaViolation(page, ['heightInMeter'], true)
Expand Down Expand Up @@ -142,4 +145,4 @@ test('Change the GUI editor content and check if the internal data is updated pr
// Validate that the internal data is updated correctly
const dataAfterNameEnter = await tpGetData(page, SessionMode.DataEditor);
expect(dataAfterNameEnter).toEqual({ name: 'Alex', address: { city: 'Berlin' } });
});
});
3 changes: 3 additions & 0 deletions meta_configurator/e2e/test-fixtures/data_people.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name,city,role
Alice,Berlin,Engineer
Bob,Munich,Designer
53 changes: 53 additions & 0 deletions meta_configurator/e2e/utilsCsvImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {Page} from 'playwright';
import {expect} from '@playwright/test';
import path from 'node:path';

const fixturesDir = path.resolve(process.cwd(), 'e2e/test-fixtures');

export async function openCsvImportDialog(page: Page) {
await page.locator('#import-data').click();
await page.getByRole('menuitem', {name: 'Import CSV Data'}).click();
await expect(page.getByRole('dialog', {name: 'Import CSV'})).toBeVisible();
}

export async function uploadCsvFile(page: Page, filename: string) {
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByTestId('csv-select-file').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(fixturesDir, filename));
// wait for the Import button to appear, confirming the CSV was parsed
await expect(page.getByTestId('csv-submit-import')).toBeVisible();
}

export async function uploadCsvFileAndCheckProgress(page: Page, filename: string, timeoutMs: number = 5000) {
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser', {timeout: timeoutMs}),
page.getByTestId('csv-select-file').click({timeout: timeoutMs}),
]);
await fileChooser.setFiles(path.join(fixturesDir, filename));
await expect(
page.getByTestId('csv-submit-import'),
'CSV import dialog did not progress past file selection'
).toBeVisible({timeout: timeoutMs});
}

export async function expandImportOptions(page: Page) {
await page.getByTestId('csv-import-options-toggle').click();
}

export async function setCsvTablePath(page: Page, tablePath: string) {
const input = page.getByTestId('csv-table-path-input');
await input.clear();
await input.fill(tablePath);
}

export async function setColumnPath(page: Page, columnName: string, newPath: string) {
const input = page.getByTestId(`csv-column-path-${columnName}`);
await input.clear();
await input.fill(newPath);
}

export async function submitCsvImport(page: Page) {
await page.getByTestId('csv-submit-import').click();
await expect(page.getByRole('dialog', {name: 'Import CSV'})).not.toBeVisible();
}
32 changes: 18 additions & 14 deletions meta_configurator/e2e/utilsGuiEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {Page} from "playwright";
import {expect} from "@playwright/test";
import {Path, PathElement} from "../src/utility/path";
import {pathToString} from "../src/utility/pathUtils";
import {selectAll} from "./utils";


export async function checkPropertyExistence(page: Page, propertyPath: Path, shouldBeVisible: boolean) {
Expand Down Expand Up @@ -40,21 +39,24 @@ export async function editNumberOrIntProperty(page: Page, propertyPath: Path, va
const pathAsString = pathToString(propertyPath);
const spinButton = page.getByTestId(`property-data-${pathAsString}`).getByRole('spinbutton')
await spinButton.click();
await selectAll(page);
await spinButton.press('Backspace');

// Simulate real typing
for (const char of value.toString()) {
await page.keyboard.press(char);
}

await spinButton.press('Enter');
await spinButton.evaluate((input, newValue) => {
const nativeValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
)?.set;
nativeValueSetter?.call(input, String(newValue));
input.dispatchEvent(new Event('input', {bubbles: true}));
input.setAttribute('aria-valuenow', String(newValue));
}, value);
await spinButton.blur();
}

export async function checkNumberOrIntProperty(page: Page, propertyPath: Path, value: number) {
const pathAsString = pathToString(propertyPath);
const textField = page.getByTestId(`property-data-${pathAsString}`).getByRole('spinbutton')
await expect(textField).toHaveValue(value.toString());
await expect
.poll(async () => await textField.getAttribute('aria-valuenow'))
.toBe(value.toString());
}

export async function removeOptionalPropertyValue(page: Page, propertyPath: Path) {
Expand All @@ -77,9 +79,11 @@ export async function addArrayItem(page: Page, propertyPath: Path) {

export async function checkPropertySchemaViolation(page: Page, propertyPath: Path, shouldBeVisible: boolean) {
const pathAsString = pathToString(propertyPath);
const validationErrorIcon = page.getByTestId(`property-metadata-${pathAsString}`).getByTestId("validation-error-icon");
const propertyMetadata = page.getByTestId(`property-metadata-${pathAsString}`);
const validationErrorIcon = propertyMetadata.getByTestId("validation-error-icon");
if (shouldBeVisible) {
await expect(validationErrorIcon).toBeVisible();
await expect(propertyMetadata).toBeVisible();
await expect(validationErrorIcon).toBeVisible({timeout: 8000});
} else {
await expect(validationErrorIcon).not.toBeVisible();
}
Expand All @@ -101,4 +105,4 @@ export async function expandOrCollapseProperty(page: Page, propertyPathElement:
// do check if the name starts with the propertyName, but ignore the other part of the name, as it can differ always depending on the children count
const expansionButton = page.getByRole('cell', { name: new RegExp(`^${propertyPathElement} :`) }).getByRole('button');
await expansionButton.click();
}
}
19 changes: 11 additions & 8 deletions meta_configurator/src/components/CombinedEditorComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Main component of the application.
Combines the code editor and the gui editor.
-->
<script lang="ts" setup>
import {computed, onMounted, onUnmounted, type Ref, ref, watch} from 'vue';
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
import 'primeicons/primeicons.css';
import SplitterPanel from 'primevue/splitterpanel';
import Splitter from 'primevue/splitter';
Expand All @@ -15,13 +15,13 @@ import {useConfirm} from 'primevue/useconfirm';
import {confirmationService} from '@/utility/confirmationService';
import {toastService} from '@/utility/toastService';
import {useAppRouter} from '@/router/router';
import {useDropZone, useWindowSize, watchImmediate} from '@vueuse/core/index';
import {useDropZone, useWindowSize, watchImmediate} from '@vueuse/core';
import {readFileContentToDataLink} from '@/utility/readFileContent';
import {getDataForMode} from '@/data/useDataLink';
import {useSettings} from '@/settings/useSettings';
import {modeToRoute, SessionMode} from '@/store/sessionMode';
import {useSessionStore} from '@/store/sessionStore';
import type {SettingsInterfacePanels, SettingsInterfaceRoot} from '@/settings/settingsTypes';
import type {SettingsInterfacePanels} from '@/settings/settingsTypes';
import {SETTINGS_DATA_DEFAULT} from '@/settings/defaultSettingsData';
import {updateSettingsWithDefaults} from '@/settings/settingsUpdater';
import {panelTypeRegistry} from '@/components/panels/panelTypeRegistry';
Expand All @@ -38,15 +38,17 @@ let panelsDefinition: SettingsInterfacePanels = settings.value.panels;
// Ace Editor
watchImmediate(
() => settings,
(settings: Ref<SettingsInterfaceRoot>) => {
let panels = settings.value.panels;
settings => {
const panels = settings.value.panels;
if (JSON.stringify(panels) !== JSON.stringify(panelsDefinition)) {
panelsDefinition = panels;
}
// fix panels if they are not defined
for (let mode of Object.values(SessionMode)) {
if (!panels[mode]) {
panels[mode] = structuredClone(SETTINGS_DATA_DEFAULT.panels[mode]);
panels[mode] = structuredClone(
SETTINGS_DATA_DEFAULT.panels[mode]
) as SettingsInterfacePanels[typeof mode];
}
}
}
Expand All @@ -61,6 +63,7 @@ const panels = computed(() => {
};
});
});
const panelsKey = computed(() => JSON.stringify(panels.value));

let {width} = useWindowSize();

Expand Down Expand Up @@ -164,10 +167,10 @@ onUnmounted(() => {
class="h-full"
style="min-width: 0"
:layout="width < 600 ? 'vertical' : 'horizontal'"
:key="panels">
:key="panelsKey">
<SplitterPanel
v-for="(panel, index) in panels"
:key="index + panel"
:key="`${panel.sessionMode}-${index}`"
:min-size="10"
:size="panel.size"
:resizable="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,10 @@ async function submitPromptExportDocument() {
// if the schema defines export formats, the user prompt is ignored and the selected export format is used
if (documentExportFormatNames.value.length > 0) {
const exportFormatName = selectedExportFormat.value;
const exportFormatDef = documentExportFormats.value![exportFormatName];
const exportFormatDef = documentExportFormats.value?.[exportFormatName];
if (!exportFormatDef) {
throw new Error(`Unknown export format "${exportFormatName}".`);
}
// if export format is just a string, it is the URL
if (typeof exportFormatDef === 'string') {
userPrompt = await fetchExternalContentText(exportFormatDef);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,12 @@ function validationErrorsToAnnotations(editor: Editor, errors: ErrorObject[]): A
}

const index = positions[instancePathKey];
let position;
if (index === undefined) {
continue;
}
let position: {row: number; column: number};
if (index in cachedPositionsForIndices) {
position = cachedPositionsForIndices[index];
position = cachedPositionsForIndices[index]!;
} else {
position = editor.session.doc.indexToPosition(index, 0);
cachedPositionsForIndices[index] = position;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@/components/panels/code-editor/aceUtility';
import type {SessionMode} from '@/store/sessionMode';
import {getDataForMode, getSessionForMode} from '@/data/useDataLink';
import {watchImmediate} from '@vueuse/core/index';
import {watchImmediate} from '@vueuse/core';
import {useSettings} from '@/settings/useSettings';
import {sizeOf} from '@/utility/sizeOf';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,27 +507,26 @@ const schemaInfoOverlay = ref<InstanceType<typeof SchemaInfoOverlay> | undefined
const allowShowOverlay = ref(true);
const overlayShowScheduled = ref(false);

const showInfoOverlayPanelInstantly = (nodeData: ConfigTreeNodeData, event: MouseEvent) => {
const showInfoOverlayPanelInstantly = (nodeData: ConfigTreeNodeData, event: Event) => {
const relevantErrors = getValidationResults(nodeData.absolutePath).errors;
// @ts-ignore
schemaInfoOverlay.value?.showPanel(
nodeData.schema,
nodeData.name,
String(nodeData.name),
nodeData.parentSchema,
relevantErrors,
event
);
};
const showInfoOverlayPanelDebounced = useDebounceFn(
(nodeData: ConfigTreeNodeData, event: MouseEvent) => {
(nodeData: ConfigTreeNodeData, event: Event) => {
if (allowShowOverlay.value && overlayShowScheduled.value) {
showInfoOverlayPanelInstantly(nodeData, event);
}
},
1000
);

function showInfoOverlayPanel(nodeData: ConfigTreeNodeData, event: MouseEvent) {
function showInfoOverlayPanel(nodeData: ConfigTreeNodeData, event: Event) {
overlayShowScheduled.value = true;
showInfoOverlayPanelDebounced(nodeData, event);
}
Expand Down Expand Up @@ -608,7 +607,9 @@ function zoomIntoPath(path: Path) {
@update_tree="updateTree"
@click="() => clickedPropertyData(slotProps.node.data)"
bodyClass="w-full"
@keydown.ctrl.i="event => showInfoOverlayPanelInstantly(slotProps.node.data, event)"
@keydown.ctrl.i="
(event: KeyboardEvent) => showInfoOverlayPanelInstantly(slotProps.node.data, event)
"
:data-testid="'property-data-' + pathToString(slotProps.node.data.absolutePath)" />
</span>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function zoomIntoPath() {
}
}

function updatePropertyName(event) {
function updatePropertyName(event: KeyboardEvent | FocusEvent) {
const target = event.target as HTMLElement;
let text = target.innerText;

Expand All @@ -103,12 +103,12 @@ function updatePropertyName(event) {
if (isPropertyNameEditable(props.type)) {
emit('update_property_name', props.node.data.name as string, text);
} else {
event.target.innerText = props.node.data.name;
target.innerText = String(props.node.data.name);
}

isEditingPropertyName.value = false;
emit('stop_editing_property_name');
event.target.contenteditable = false;
target.contentEditable = 'false';
showPencil.value = true;
}

Expand Down
Loading
Loading