Skip to content

Commit 2d454f5

Browse files
authored
Merge pull request #13 from eviltester/11-export-more-programming-languages-feature
Add Code tab with JavaScript export
2 parents bbedc95 + 5fda6f0 commit 2d454f5

11 files changed

Lines changed: 1222 additions & 74 deletions

File tree

.github/copilot-instructions.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Project Guidelines
2+
3+
## Build and Test
4+
5+
```bash
6+
npm run format:check # Prettier formatting check
7+
npm run lint # ESLint
8+
npm test # Jest unit/integration tests (54 suites)
9+
npm run test:browser # Playwright browser smoke tests (app.html, generator.html)
10+
```
11+
12+
## Definition of Done
13+
14+
After making **any code changes**, you MUST run the following before calling `task_complete`:
15+
16+
1. `npm run format:check` — if it fails, run `npm run format` to fix, then re-check
17+
2. `npm test` — all Jest tests must pass
18+
3. `npm run test:browser` — Playwright smoke tests must pass
19+
20+
Do not call `task_complete` until all three commands exit successfully.

.github/workflows/node.js.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ jobs:
3939
- name: Run tests with coverage
4040
run: npm run testcoverage
4141

42+
- name: Install Playwright browser
43+
run: npx playwright install --with-deps chromium
44+
45+
- name: Run browser smoke tests
46+
run: npm run test:browser
47+
4248
- name: Upload coverage artifact
4349
uses: actions/upload-artifact@v4
4450
with:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ coverage_final.txt
88
coverage_output.txt
99
coverage.txt
1010
test_output.txt
11+
playwright-report
12+
test-results
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
slug: export-improvements
3+
title: Export XML, SQL, JSONL
4+
authors: alan
5+
tags: [release]
6+
date: 2026-04-27T19:00
7+
---
8+
9+
New export features like XML, SQL and JSONL plus improved the speed for exporting large datasets and improved on screen tips.
10+
11+
<!--truncate-->
12+
13+
## Large File Exports
14+
15+
There is no point being able to upload and amend 1,000,000+ record files if they are painful to export.
16+
17+
So now they are not.
18+
19+
You'll see visual progress indicators and the page should not freeze, it should just export data well.
20+
21+
## XML, JSONL and SQL
22+
23+
Export all the L's, now you can export the data as:
24+
25+
- [XML](/docs/data-formats/xml/xml)
26+
- [JSONL](/docs/data-formats/jsonl/jsonl)
27+
- [SQL](/docs/data-formats/sql/sql)
28+
29+
There should be enough options on the XML and SQL to handle most common simple variants.
30+
31+
## Tips a-go-go
32+
33+
We've now added tips on all export option options so you can hover and see what the option does.
34+
35+
They are all documented in the docs, but this way you don't have to check.
36+
37+
And to make Faker a little easier we've added documentation of the faker methods in the tips (once you choose one), and linked off to the Faker documentation so you can read about the option in more detail.
38+
39+
## Bug Fixes for Faker
40+
41+
We hadn't mapped Faker internally properly so some of the options you could choose would render as `[object Object]` and as a sub-object in JSON.
42+
43+
But no longer.
44+
45+
Now you can choose `airline.airline.iataCode` instead of just `airline.airline`.

js/gui_components/tabbed-text-control.js

Lines changed: 137 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ class TabbedTextControl {
22
constructor(parentElement, theImportExportControls) {
33
this.parent = parentElement;
44
this.importExportController = theImportExportControls;
5+
this.defaultTabId = 'csv';
6+
this.tabDefinitions = [
7+
{ id: 'markdown', label: 'Markdown', type: 'markdown' },
8+
{ id: 'csv', label: 'CSV', type: 'csv' },
9+
{ id: 'dsv', label: 'Delimited', type: 'dsv' },
10+
{ id: 'json', label: 'JSON', type: 'json' },
11+
{
12+
id: 'code',
13+
label: 'Code',
14+
subtasks: [{ id: 'javascript', label: 'JavaScript', type: 'javascript' }],
15+
},
16+
{ id: 'gherkin', label: 'Gherkin', type: 'gherkin' },
17+
{ id: 'html', label: 'HTML', type: 'html' },
18+
{ id: 'asciitable', label: 'ASCII', type: 'asciitable' },
19+
];
520
}
621

722
// TODO : populate this from the registered importers and exporters rather than hard coding
@@ -10,37 +25,15 @@ class TabbedTextControl {
1025
<div class="conversionTabs">
1126
<div id="conversionTypes" class="conversionTypes">
1227
<ul class="conversionTypesList">
13-
<li id="type-markdown" class="type-select">
14-
<a class="type-select-action" data-type="markdown" href="#">Markdown</a>
15-
</li>
16-
<li id="type-csv" class="type-select active-type">
17-
<a class="type-select-action" data-type="csv" href="#">CSV</a>
18-
</li>
19-
<li id="type-dsv" class="type-select">
20-
<a class="type-select-action" data-type="dsv" href="#">Delimited</a>
21-
</li>
22-
<li id="type-json" class="type-select">
23-
<a class="type-select-action" data-type="json" href="#">JSON</a>
24-
</li>
25-
<li id="type-javascript" class="type-select">
26-
<a class="type-select-action" data-type="javascript" href="#">JavaScript</a>
27-
</li>
28-
<li id="type-gherkin" class="type-select">
29-
<a class="type-select-action" data-type="gherkin" href="#">Gherkin</a>
30-
</li>
31-
<li id="type-html" class="type-select">
32-
<a class="type-select-action" data-type="html" href="#">HTML</a>
33-
</li>
34-
<li id="type-asciitable" class="type-select">
35-
<a class="type-select-action" data-type="asciitable" href="#">ASCII</a>
36-
</li>
28+
${this._renderMainTabMarkup()}
3729
</ul>
3830
</div>
3931
<div class="rightbuttons">
4032
<button title="Toggle Preview/Edit mode" id="previewEditModeButton">Preview</button>
4133
<button title="Copy text to clipboard" id="copyTextButton">Copy</button>
4234
</div>
4335
</div>
36+
<div id="conversionSubtasks" class="conversionSubtasks" style="display: none"></div>
4437
4538
<div class="edit-area">
4639
<div class="options-parent" style="display: none"></div>
@@ -50,18 +43,10 @@ class TabbedTextControl {
5043
</div>
5144
`;
5245

53-
this.parent.querySelectorAll('.type-select-action').forEach((lielem) =>
54-
lielem.addEventListener('click', (e) => {
55-
// set the display buttons
56-
this.parent.querySelectorAll('.type-select').forEach((elem) => elem.classList.remove('active-type'));
57-
58-
e.target.parentElement.classList.add('active-type');
59-
60-
// switched tab so re-render text
61-
this.importExportController.renderTextFromGrid();
62-
this.importExportController.setFileFormatType();
63-
this.importExportController.setOptionsViewForFormatType();
64-
// don't try to navigate
46+
this.parent.querySelectorAll('.type-select-action').forEach((linkElem) =>
47+
linkElem.addEventListener('click', (e) => {
48+
e.preventDefault();
49+
this._handleMainTabSelection(e.currentTarget.dataset.tabId);
6550
return false;
6651
})
6752
);
@@ -80,9 +65,125 @@ class TabbedTextControl {
8065
});
8166
}
8267

68+
this._handleMainTabSelection(this.defaultTabId, { notifyController: false });
8369
this._syncPreviewEditButtonLabel();
8470
}
8571

72+
_renderMainTabMarkup() {
73+
return this.tabDefinitions
74+
.map((definition) => {
75+
const typeAttributes = definition.type ? `data-type="${definition.type}"` : `data-group="${definition.id}"`;
76+
77+
return `
78+
<li id="type-${definition.id}" class="type-select" data-tab-id="${definition.id}">
79+
<a class="type-select-action" data-tab-id="${definition.id}" ${typeAttributes} href="#">${definition.label}</a>
80+
</li>`;
81+
})
82+
.join('');
83+
}
84+
85+
_handleMainTabSelection(tabId, { notifyController = true } = {}) {
86+
const definition = this._getTabDefinition(tabId);
87+
if (!definition) {
88+
return false;
89+
}
90+
91+
if (!definition.subtasks) {
92+
this._activateDirectType(tabId, definition.type, { notifyController });
93+
return false;
94+
}
95+
96+
this._renderSubtasks(definition);
97+
this._activateGroupedType(tabId, definition.subtasks[0].type, { notifyController });
98+
return false;
99+
}
100+
101+
_renderSubtasks(definition) {
102+
const subtaskHost = this.parent.querySelector('#conversionSubtasks');
103+
if (!subtaskHost) {
104+
return;
105+
}
106+
107+
subtaskHost.innerHTML = `
108+
<ul class="conversionSubtasksList">
109+
${definition.subtasks
110+
.map(
111+
(subtask) => `
112+
<li class="subtask-select" data-type="${subtask.type}">
113+
<a class="subtask-select-action" data-type="${subtask.type}" href="#">${subtask.label}</a>
114+
</li>`
115+
)
116+
.join('')}
117+
</ul>`;
118+
subtaskHost.style.display = 'block';
119+
120+
subtaskHost.querySelectorAll('.subtask-select-action').forEach((linkElem) =>
121+
linkElem.addEventListener('click', (e) => {
122+
e.preventDefault();
123+
this._activateGroupedType(definition.id, e.currentTarget.dataset.type, { notifyController: true });
124+
return false;
125+
})
126+
);
127+
128+
subtaskHost.querySelectorAll('li.subtask-select').forEach((listElem) =>
129+
listElem.addEventListener('click', (e) => {
130+
e.target.querySelector('a.subtask-select-action')?.click();
131+
})
132+
);
133+
}
134+
135+
_activateDirectType(tabId, type, { notifyController = true } = {}) {
136+
this._clearActiveSelections();
137+
this.parent.querySelector(`#type-${tabId}`)?.classList.add('active-type');
138+
this._hideSubtasks();
139+
if (notifyController) {
140+
this._notifyTypeChanged();
141+
}
142+
}
143+
144+
_activateGroupedType(tabId, type, { notifyController = true } = {}) {
145+
this._clearActiveSelections();
146+
this.parent.querySelector(`#type-${tabId}`)?.classList.add('active-main-type');
147+
148+
const subtaskHost = this.parent.querySelector('#conversionSubtasks');
149+
const activeSubtask = subtaskHost?.querySelector(`.subtask-select[data-type="${type}"]`);
150+
if (activeSubtask) {
151+
activeSubtask.classList.add('active-type');
152+
}
153+
154+
if (notifyController) {
155+
this._notifyTypeChanged();
156+
}
157+
}
158+
159+
_clearActiveSelections() {
160+
this.parent.querySelectorAll('.type-select').forEach((elem) => {
161+
elem.classList.remove('active-type');
162+
elem.classList.remove('active-main-type');
163+
});
164+
165+
this.parent.querySelectorAll('.subtask-select').forEach((elem) => elem.classList.remove('active-type'));
166+
}
167+
168+
_hideSubtasks() {
169+
const subtaskHost = this.parent.querySelector('#conversionSubtasks');
170+
if (!subtaskHost) {
171+
return;
172+
}
173+
subtaskHost.innerHTML = '';
174+
subtaskHost.style.display = 'none';
175+
}
176+
177+
_notifyTypeChanged() {
178+
this.importExportController.renderTextFromGrid();
179+
this.importExportController.setFileFormatType();
180+
this.importExportController.setOptionsViewForFormatType();
181+
}
182+
183+
_getTabDefinition(tabId) {
184+
return this.tabDefinitions.find((definition) => definition.id === tabId);
185+
}
186+
86187
_syncPreviewEditButtonLabel() {
87188
const modeButton = this.parent.querySelector('#previewEditModeButton');
88189
if (!modeButton) {

0 commit comments

Comments
 (0)