lhcb-ntuple-wizard-test 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AddTupleToolButton.d.ts +1 -0
- package/dist/components/AddTupleToolButton.js +16 -0
- package/dist/components/BookkeepingPathDropdown.d.ts +1 -1
- package/dist/components/BookkeepingPathDropdown.js +1 -4
- package/dist/components/DatasetEventTypeBadge.js +1 -1
- package/dist/components/DatasetInfo.js +3 -3
- package/dist/components/DecayCard.d.ts +1 -1
- package/dist/components/DecayCard.js +12 -40
- package/dist/components/DecayLatex.d.ts +1 -1
- package/dist/components/DecayLatex.js +4 -87
- package/dist/components/DecayListItem.js +2 -2
- package/dist/components/DecayTagBadge.d.ts +1 -1
- package/dist/components/DecayTagBadge.js +0 -3
- package/dist/components/DecayTreeCard.d.ts +1 -7
- package/dist/components/DecayTreeCard.js +13 -18
- package/dist/components/DecayTreeCardHeader.d.ts +2 -3
- package/dist/components/DecayTreeCardHeader.js +2 -2
- package/dist/components/DecayTreeGraph.d.ts +1 -4
- package/dist/components/DecayTreeGraph.js +66 -41
- package/dist/components/DeleteButton.d.ts +2 -1
- package/dist/components/DeleteButton.js +2 -2
- package/dist/components/DttNameInput.d.ts +9 -0
- package/dist/components/DttNameInput.js +43 -0
- package/dist/components/GuideLinkButton.d.ts +15 -0
- package/dist/components/GuideLinkButton.js +16 -0
- package/dist/components/NtupleWizard.js +1 -7
- package/dist/components/ParticleDropdown.d.ts +11 -1
- package/dist/components/ParticleDropdown.js +3 -6
- package/dist/components/ParticleLatex.d.ts +6 -0
- package/dist/components/ParticleLatex.js +13 -0
- package/dist/components/ParticleTagBadge.d.ts +2 -1
- package/dist/components/ParticleTagBadge.js +5 -7
- package/dist/components/ParticleTagFilters.d.ts +1 -6
- package/dist/components/ParticleTagFilters.js +16 -17
- package/dist/components/RequestButtonGroup.d.ts +1 -1
- package/dist/components/RequestButtonGroup.js +0 -3
- package/dist/components/RequestRow.d.ts +1 -1
- package/dist/components/RequestRow.js +4 -9
- package/dist/components/StrippingLineDropdown.js +14 -16
- package/dist/components/StrippingLineInfo.d.ts +5 -5
- package/dist/components/StrippingLineInfo.js +14 -4
- package/dist/components/StrippingLineInfoButton.d.ts +6 -0
- package/dist/components/StrippingLineInfoButton.js +7 -0
- package/dist/components/StrippingLineVersionBadge.d.ts +7 -0
- package/dist/components/{StrippingLineBadge.js → StrippingLineVersionBadge.js} +5 -5
- package/dist/components/StrippingLinesCountBadge.d.ts +8 -0
- package/dist/components/StrippingLinesCountBadge.js +11 -0
- package/dist/components/TagDropdown.d.ts +3 -3
- package/dist/components/TagDropdown.js +2 -2
- package/dist/components/TupleToolClassDropdown.js +11 -14
- package/dist/components/TupleToolDocsAccordion.d.ts +1 -1
- package/dist/components/TupleToolDocsAccordion.js +4 -8
- package/dist/components/TupleToolGroupsAccordion.d.ts +5 -0
- package/dist/components/TupleToolGroupsAccordion.js +31 -0
- package/dist/components/TupleToolLabel.d.ts +1 -1
- package/dist/components/TupleToolLabel.js +2 -5
- package/dist/components/TupleToolsAccordion.d.ts +1 -0
- package/dist/components/TupleToolsAccordion.js +22 -0
- package/dist/components/modals/AddTupleToolModal.js +3 -2
- package/dist/components/modals/ConfigureTupleToolModal.js +1 -1
- package/dist/components/modals/UploadDttConfigModal.d.ts +1 -1
- package/dist/components/modals/UploadDttConfigModal.js +1 -4
- package/dist/config.d.ts +1 -0
- package/dist/config.js +3 -2
- package/dist/models/bkPath.js +1 -1
- package/dist/models/dtt.d.ts +5 -2
- package/dist/models/dtt.js +37 -18
- package/dist/models/rowData.d.ts +1 -1
- package/dist/models/yamlFile.js +1 -1
- package/dist/pages/DecaySearchPage.js +4 -9
- package/dist/pages/DecayTreeConfigPage.js +4 -38
- package/dist/pages/RequestPage.js +2 -4
- package/dist/providers/DttProvider.d.ts +3 -3
- package/dist/providers/DttProvider.js +30 -55
- package/dist/providers/MetadataProvider.d.ts +1 -1
- package/dist/providers/MetadataProvider.js +11 -5
- package/dist/providers/RequestProvider.js +2 -2
- package/dist/providers/RowProvider.d.ts +2 -2
- package/dist/providers/RowProvider.js +10 -9
- package/dist/providers/RowsProvider.js +0 -3
- package/dist/tests/components/BookkeepingPathDropdown.test.d.ts +1 -0
- package/dist/tests/components/BookkeepingPathDropdown.test.js +118 -0
- package/dist/tests/components/DatasetInfo.test.d.ts +1 -0
- package/dist/tests/components/DatasetInfo.test.js +38 -0
- package/dist/tests/components/DecayCard.test.d.ts +1 -0
- package/dist/tests/components/DecayCard.test.js +115 -0
- package/dist/tests/components/DecayLatex.test.d.ts +1 -0
- package/dist/tests/components/DecayLatex.test.js +31 -0
- package/dist/tests/components/DecayList.test.d.ts +1 -0
- package/dist/tests/components/DecayList.test.js +76 -0
- package/dist/tests/components/DecayListItem.test.d.ts +1 -0
- package/dist/tests/components/DecayListItem.test.js +51 -0
- package/dist/tests/components/DecayTreeCard.test.d.ts +1 -0
- package/dist/tests/components/DecayTreeCard.test.js +119 -0
- package/dist/tests/components/DecayTreeGraph.test.d.ts +1 -0
- package/dist/tests/components/DecayTreeGraph.test.js +125 -0
- package/dist/tests/components/DeleteButton.test.d.ts +1 -0
- package/dist/tests/components/DeleteButton.test.js +45 -0
- package/dist/tests/components/DttNameInput.test.d.ts +1 -0
- package/dist/tests/components/DttNameInput.test.js +75 -0
- package/dist/tests/components/NtupleWizard.test.d.ts +1 -0
- package/dist/tests/components/NtupleWizard.test.js +57 -0
- package/dist/tests/components/ParticleDropdown.test.d.ts +1 -0
- package/dist/tests/components/ParticleDropdown.test.js +23 -0
- package/dist/tests/components/ParticleTagFilters.test.d.ts +1 -0
- package/dist/tests/components/ParticleTagFilters.test.js +87 -0
- package/dist/tests/components/RequestButtonGroup.test.d.ts +1 -0
- package/dist/tests/components/RequestButtonGroup.test.js +132 -0
- package/dist/tests/components/RequestRow.test.d.ts +1 -0
- package/dist/tests/components/RequestRow.test.js +58 -0
- package/dist/tests/components/StrippingLineDropdown.test.d.ts +1 -0
- package/dist/tests/components/StrippingLineDropdown.test.js +88 -0
- package/dist/tests/components/badges.test.d.ts +1 -0
- package/dist/tests/components/badges.test.js +120 -0
- package/dist/tests/components/dropdowns.test.d.ts +1 -0
- package/dist/tests/components/dropdowns.test.js +110 -0
- package/dist/tests/components/dttComponents.test.d.ts +1 -0
- package/dist/tests/components/dttComponents.test.js +287 -0
- package/dist/tests/components/formInputs.test.d.ts +1 -0
- package/dist/tests/components/formInputs.test.js +96 -0
- package/dist/tests/components/metadataComponents.test.d.ts +1 -0
- package/dist/tests/components/metadataComponents.test.js +137 -0
- package/dist/tests/components/miscComponents.test.d.ts +1 -0
- package/dist/tests/components/miscComponents.test.js +134 -0
- package/dist/tests/components/modals.test.d.ts +1 -0
- package/dist/tests/components/modals.test.js +554 -0
- package/dist/tests/components/tupleToolParams.test.d.ts +1 -0
- package/dist/tests/components/tupleToolParams.test.js +213 -0
- package/dist/tests/config.test.d.ts +1 -0
- package/dist/tests/config.test.js +31 -0
- package/dist/tests/mockSetup.d.ts +1 -0
- package/dist/tests/mockSetup.js +30 -0
- package/dist/tests/models/BkPath.test.d.ts +1 -0
- package/dist/tests/models/BkPath.test.js +87 -0
- package/dist/tests/models/Dtt.test.d.ts +1 -0
- package/dist/tests/models/Dtt.test.js +376 -0
- package/dist/tests/models/TupleTool.test.d.ts +1 -0
- package/dist/tests/models/TupleTool.test.js +80 -0
- package/dist/tests/models/YamlFile.test.d.ts +1 -0
- package/dist/tests/models/YamlFile.test.js +123 -0
- package/dist/tests/pages/DecaySearchPage.test.d.ts +1 -0
- package/dist/tests/pages/DecaySearchPage.test.js +228 -0
- package/dist/tests/pages/DecayTreeConfigPage.test.d.ts +1 -0
- package/dist/tests/pages/DecayTreeConfigPage.test.js +105 -0
- package/dist/tests/pages/RequestPage.test.d.ts +1 -0
- package/dist/tests/pages/RequestPage.test.js +439 -0
- package/dist/tests/providers/DttProvider.test.d.ts +1 -0
- package/dist/tests/providers/DttProvider.test.js +105 -0
- package/dist/tests/providers/MetadataProvider.test.d.ts +1 -0
- package/dist/tests/providers/MetadataProvider.test.js +129 -0
- package/dist/tests/providers/RequestProvider.test.d.ts +1 -0
- package/dist/tests/providers/RequestProvider.test.js +306 -0
- package/dist/tests/providers/RowProvider.test.d.ts +1 -0
- package/dist/tests/providers/RowProvider.test.js +110 -0
- package/dist/tests/providers/RowsProvider.test.d.ts +1 -0
- package/dist/tests/providers/RowsProvider.test.js +84 -0
- package/dist/tests/providers/WizardConfigProvider.test.d.ts +1 -0
- package/dist/tests/providers/WizardConfigProvider.test.js +36 -0
- package/dist/tests/setupTests.d.ts +1 -0
- package/dist/tests/setupTests.js +15 -0
- package/dist/tests/testUtils.d.ts +33 -0
- package/dist/tests/testUtils.js +196 -0
- package/dist/tests/utils/latexUtils.test.d.ts +1 -0
- package/dist/tests/utils/latexUtils.test.js +62 -0
- package/dist/tests/utils/utils.test.d.ts +1 -0
- package/dist/tests/utils/utils.test.js +394 -0
- package/dist/utils/latexUtils.d.ts +13 -0
- package/dist/utils/latexUtils.js +86 -0
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.js +4 -1
- package/package.json +16 -7
- package/dist/components/NumStrippingLinesBadge.d.ts +0 -8
- package/dist/components/NumStrippingLinesBadge.js +0 -10
- package/dist/components/StrippingLineBadge.d.ts +0 -7
- package/dist/components/TupleToolGroup.d.ts +0 -5
- package/dist/components/TupleToolGroup.js +0 -22
- package/dist/components/TupleToolList.d.ts +0 -6
- package/dist/components/TupleToolList.js +0 -42
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
// Mock typia before any imports that use it
|
|
3
|
+
vi.mock("typia", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
validate: vi.fn((value) => ({ success: true, data: value })),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
import { sanitizeFilename, processProductionFiles, downloadText, downloadZip, getControlKey, parseProductionFiles, } from "../../utils/utils";
|
|
9
|
+
import { YamlFile } from "../../models/yamlFile";
|
|
10
|
+
import { mockDecay, mockMetadata, mockToolMetadata } from "../testUtils";
|
|
11
|
+
// A minimal saved DTT config that matches mockDecay's descriptorTemplate
|
|
12
|
+
const mockSavedConfig = {
|
|
13
|
+
name: "DecayTreeTuple/MyTree",
|
|
14
|
+
descriptorTemplate: "[${head}B+ -> ${k}K+ ${mup}mu+ ${mum}mu-]CC",
|
|
15
|
+
inputs: ["/Event/Leptonic/Phys/BuToKJpsiee2Line/Particles"],
|
|
16
|
+
tools: [{ TupleToolKinematic: { Verbose: false } }],
|
|
17
|
+
branches: {
|
|
18
|
+
head: { particle: "B+", tools: [] },
|
|
19
|
+
k: { particle: "K+", tools: [] },
|
|
20
|
+
mup: { particle: "mu+", tools: [] },
|
|
21
|
+
mum: { particle: "mu-", tools: [] },
|
|
22
|
+
},
|
|
23
|
+
groups: {},
|
|
24
|
+
};
|
|
25
|
+
// The metadata's decay key must match the template
|
|
26
|
+
const metadataWithDecay = {
|
|
27
|
+
...mockMetadata,
|
|
28
|
+
decays: {
|
|
29
|
+
"some-key": {
|
|
30
|
+
...mockDecay,
|
|
31
|
+
descriptors: {
|
|
32
|
+
...mockDecay.descriptors,
|
|
33
|
+
// Must match mockSavedConfig.descriptorTemplate
|
|
34
|
+
template: "[${head}B+ -> ${k}K+ ${mup}mu+ ${mum}mu-]CC",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
tupleTools: {
|
|
39
|
+
applicationInfo: { Analysis: "v25r1", DaVinci: "v45r7", Phys: "v26r2" },
|
|
40
|
+
tupleTools: mockToolMetadata,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
describe("sanitizeFilename()", () => {
|
|
44
|
+
it("returns the filename unchanged when safe", () => {
|
|
45
|
+
expect(sanitizeFilename("my-file_01.yaml")).toBe("my-file_01.yaml");
|
|
46
|
+
});
|
|
47
|
+
it("strips directory separators (forward slash)", () => {
|
|
48
|
+
expect(sanitizeFilename("path/to/file.yaml")).toBe("file.yaml");
|
|
49
|
+
});
|
|
50
|
+
it("strips directory separators (backslash)", () => {
|
|
51
|
+
expect(sanitizeFilename("path\\to\\file.yaml")).toBe("file.yaml");
|
|
52
|
+
});
|
|
53
|
+
it("removes path traversal by stripping all directory components (keeps only basename)", () => {
|
|
54
|
+
// sanitizeFilename takes the last path segment, so traversal is neutralised
|
|
55
|
+
expect(sanitizeFilename("../../etc/passwd")).toBe("passwd");
|
|
56
|
+
});
|
|
57
|
+
it("removes leading dots", () => {
|
|
58
|
+
expect(sanitizeFilename(".hidden")).toBe("hidden");
|
|
59
|
+
});
|
|
60
|
+
it("removes unsafe characters", () => {
|
|
61
|
+
expect(sanitizeFilename("file name!@#.yaml")).toBe("filename.yaml");
|
|
62
|
+
});
|
|
63
|
+
it("returns the fallback when sanitization produces an empty string", () => {
|
|
64
|
+
expect(sanitizeFilename("!!!")).toBe("file");
|
|
65
|
+
});
|
|
66
|
+
it("returns a custom fallback when provided", () => {
|
|
67
|
+
expect(sanitizeFilename("!!!", "default.yaml")).toBe("default.yaml");
|
|
68
|
+
});
|
|
69
|
+
it("allows dots, hyphens, underscores in filenames", () => {
|
|
70
|
+
expect(sanitizeFilename("my-file_name.tar.gz")).toBe("my-file_name.tar.gz");
|
|
71
|
+
});
|
|
72
|
+
it("handles empty string input with fallback", () => {
|
|
73
|
+
expect(sanitizeFilename("")).toBe("file");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("processProductionFiles()", () => {
|
|
77
|
+
it("returns rows for each saved config", () => {
|
|
78
|
+
const result = processProductionFiles(metadataWithDecay, [mockSavedConfig], null);
|
|
79
|
+
expect(result).not.toBeTypeOf("string");
|
|
80
|
+
if (typeof result === "string")
|
|
81
|
+
return;
|
|
82
|
+
expect(result.rows).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
it("returns an error string when decay template is not found in metadata", () => {
|
|
85
|
+
const badConfig = {
|
|
86
|
+
...mockSavedConfig,
|
|
87
|
+
descriptorTemplate: "UnknownDecay",
|
|
88
|
+
};
|
|
89
|
+
const result = processProductionFiles(metadataWithDecay, [badConfig], null);
|
|
90
|
+
expect(result).toBeTypeOf("string");
|
|
91
|
+
expect(result).toContain("Unknown decay");
|
|
92
|
+
});
|
|
93
|
+
it("returns an error string when config has no inputs", () => {
|
|
94
|
+
const badConfig = { ...mockSavedConfig, inputs: undefined };
|
|
95
|
+
const result = processProductionFiles(metadataWithDecay, [badConfig], null);
|
|
96
|
+
expect(result).toBeTypeOf("string");
|
|
97
|
+
expect(result).toContain("No inputs defined");
|
|
98
|
+
});
|
|
99
|
+
it("parses the stripping line from the input path", () => {
|
|
100
|
+
const result = processProductionFiles(metadataWithDecay, [mockSavedConfig], null);
|
|
101
|
+
if (typeof result === "string")
|
|
102
|
+
throw new Error(result);
|
|
103
|
+
const row = result.rows[0];
|
|
104
|
+
expect(row.line?.line).toBe("StrippingBuToKJpsiee2Line");
|
|
105
|
+
expect(row.line?.stream).toBe("Leptonic");
|
|
106
|
+
});
|
|
107
|
+
it("adds BK paths from info.yaml to rows", () => {
|
|
108
|
+
const VALID_PATH = "/LHCb/Collision16/Beam6500GeV-VeloClosed-MagDown/Real Data/Reco16/Stripping28r2/90000000/DIMUON.DST";
|
|
109
|
+
const infoYaml = {
|
|
110
|
+
defaults: {
|
|
111
|
+
application: "DaVinci/v45r7",
|
|
112
|
+
wg: "OpenData",
|
|
113
|
+
inform: [],
|
|
114
|
+
automatically_configure: true,
|
|
115
|
+
output: "DVNtuple.root",
|
|
116
|
+
},
|
|
117
|
+
job0: {
|
|
118
|
+
input: { bk_query: VALID_PATH },
|
|
119
|
+
options: ["MyTree"],
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const result = processProductionFiles(metadataWithDecay, [mockSavedConfig], infoYaml);
|
|
123
|
+
if (typeof result === "string")
|
|
124
|
+
throw new Error(result);
|
|
125
|
+
expect(result.rows[0].paths).toContain(VALID_PATH);
|
|
126
|
+
});
|
|
127
|
+
it("extracts email from info.yaml defaults.inform", () => {
|
|
128
|
+
const infoYaml = {
|
|
129
|
+
defaults: {
|
|
130
|
+
application: "DaVinci/v45r7",
|
|
131
|
+
wg: "OpenData",
|
|
132
|
+
inform: ["user@cern.ch"],
|
|
133
|
+
automatically_configure: true,
|
|
134
|
+
output: "DVNtuple.root",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const result = processProductionFiles(metadataWithDecay, [mockSavedConfig], infoYaml);
|
|
138
|
+
if (typeof result === "string")
|
|
139
|
+
throw new Error(result);
|
|
140
|
+
expect(result.emails).toContain("user@cern.ch");
|
|
141
|
+
});
|
|
142
|
+
it("returns empty emails list when inform is empty", () => {
|
|
143
|
+
const result = processProductionFiles(metadataWithDecay, [mockSavedConfig], null);
|
|
144
|
+
if (typeof result === "string")
|
|
145
|
+
throw new Error(result);
|
|
146
|
+
expect(result.emails).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
it("sets line to null when inputs array is empty", () => {
|
|
149
|
+
const configWithEmptyInputs = { ...mockSavedConfig, inputs: [] };
|
|
150
|
+
const result = processProductionFiles(metadataWithDecay, [configWithEmptyInputs], null);
|
|
151
|
+
if (typeof result === "string")
|
|
152
|
+
throw new Error(result);
|
|
153
|
+
expect(result.rows[0].line).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
it("orders configs according to info.yaml job order", () => {
|
|
156
|
+
const configA = {
|
|
157
|
+
...mockSavedConfig,
|
|
158
|
+
name: "DecayTreeTuple/TreeA",
|
|
159
|
+
inputs: ["/Event/Leptonic/Phys/BuToKJpsiee2Line/Particles"],
|
|
160
|
+
};
|
|
161
|
+
const configB = {
|
|
162
|
+
...mockSavedConfig,
|
|
163
|
+
name: "DecayTreeTuple/TreeB",
|
|
164
|
+
inputs: ["/Event/Leptonic/Phys/BuToKJpsiee2Line/Particles"],
|
|
165
|
+
};
|
|
166
|
+
const infoYaml = {
|
|
167
|
+
defaults: {
|
|
168
|
+
application: "DaVinci/v45r7",
|
|
169
|
+
wg: "OpenData",
|
|
170
|
+
inform: [],
|
|
171
|
+
automatically_configure: true,
|
|
172
|
+
output: "DVNtuple.root",
|
|
173
|
+
},
|
|
174
|
+
job0: { input: { bk_query: "/q/DST" }, options: ["TreeB"] },
|
|
175
|
+
job1: { input: { bk_query: "/q2/DST" }, options: ["TreeA"] },
|
|
176
|
+
};
|
|
177
|
+
// Pass them in reverse order; info.yaml should sort them
|
|
178
|
+
const result = processProductionFiles(metadataWithDecay, [configA, configB], infoYaml);
|
|
179
|
+
if (typeof result === "string")
|
|
180
|
+
throw new Error(result);
|
|
181
|
+
expect(result.rows[0].dtt?.getName()).toBe("TreeB");
|
|
182
|
+
expect(result.rows[1].dtt?.getName()).toBe("TreeA");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// downloadText / downloadZip
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// jsdom doesn't provide URL.createObjectURL; stub it globally
|
|
189
|
+
const mockCreateObjectURL = vi.fn(() => "blob:mock");
|
|
190
|
+
vi.stubGlobal("URL", {
|
|
191
|
+
...URL,
|
|
192
|
+
createObjectURL: mockCreateObjectURL,
|
|
193
|
+
revokeObjectURL: vi.fn(),
|
|
194
|
+
});
|
|
195
|
+
// jsdom rejects MouseEvent with `view: window`; replace with a passthrough stub
|
|
196
|
+
vi.stubGlobal("MouseEvent", class MockMouseEvent {
|
|
197
|
+
});
|
|
198
|
+
describe("downloadText()", () => {
|
|
199
|
+
let mockLink;
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
201
|
+
let appendChildSpy;
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
|
+
let removeChildSpy;
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
mockCreateObjectURL.mockReturnValue("blob:mock");
|
|
206
|
+
mockLink = { href: "", download: "", dispatchEvent: vi.fn() };
|
|
207
|
+
vi.spyOn(document, "createElement").mockReturnValue(mockLink);
|
|
208
|
+
appendChildSpy = vi.spyOn(document.body, "appendChild").mockImplementation((n) => n);
|
|
209
|
+
removeChildSpy = vi.spyOn(document.body, "removeChild").mockImplementation((n) => n);
|
|
210
|
+
});
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
vi.restoreAllMocks();
|
|
213
|
+
});
|
|
214
|
+
it("calls URL.createObjectURL with a Blob", () => {
|
|
215
|
+
const yamlFile = new YamlFile("test.yaml", "content: true");
|
|
216
|
+
downloadText(yamlFile);
|
|
217
|
+
expect(mockCreateObjectURL).toHaveBeenCalledWith(expect.any(Blob));
|
|
218
|
+
});
|
|
219
|
+
it("appends and removes a link element to trigger download", () => {
|
|
220
|
+
const yamlFile = new YamlFile("test.yaml", "content: true");
|
|
221
|
+
downloadText(yamlFile);
|
|
222
|
+
expect(appendChildSpy).toHaveBeenCalled();
|
|
223
|
+
expect(removeChildSpy).toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
it("sanitizes the filename before setting the download attribute", () => {
|
|
226
|
+
const yamlFile = new YamlFile("../../evil.yaml", "x: 1");
|
|
227
|
+
downloadText(yamlFile);
|
|
228
|
+
expect(mockLink.download).toBe("evil.yaml");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe("downloadZip()", () => {
|
|
232
|
+
let mockLink;
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
234
|
+
let appendChildSpy;
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
236
|
+
let removeChildSpy;
|
|
237
|
+
beforeEach(() => {
|
|
238
|
+
mockCreateObjectURL.mockReturnValue("blob:mock");
|
|
239
|
+
mockLink = { href: "", download: "", dispatchEvent: vi.fn() };
|
|
240
|
+
vi.spyOn(document, "createElement").mockReturnValue(mockLink);
|
|
241
|
+
appendChildSpy = vi.spyOn(document.body, "appendChild").mockImplementation((n) => n);
|
|
242
|
+
removeChildSpy = vi.spyOn(document.body, "removeChild").mockImplementation((n) => n);
|
|
243
|
+
});
|
|
244
|
+
afterEach(() => {
|
|
245
|
+
vi.restoreAllMocks();
|
|
246
|
+
});
|
|
247
|
+
it("creates a ZIP and triggers a download", async () => {
|
|
248
|
+
const files = [new YamlFile("a.yaml", "a: 1"), new YamlFile("b.yaml", "b: 2")];
|
|
249
|
+
await downloadZip(files, "my-archive.zip");
|
|
250
|
+
expect(mockCreateObjectURL).toHaveBeenCalledWith(expect.any(Blob));
|
|
251
|
+
expect(appendChildSpy).toHaveBeenCalled();
|
|
252
|
+
expect(removeChildSpy).toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
it("uses default archive name when none provided", async () => {
|
|
255
|
+
const files = [new YamlFile("a.yaml", "a: 1")];
|
|
256
|
+
await downloadZip(files);
|
|
257
|
+
expect(mockLink.download).toBe("archive.zip");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// getControlKey
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
describe("getControlKey()", () => {
|
|
264
|
+
afterEach(() => {
|
|
265
|
+
vi.restoreAllMocks();
|
|
266
|
+
});
|
|
267
|
+
it("returns 'cmd' on Mac", () => {
|
|
268
|
+
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36");
|
|
269
|
+
expect(getControlKey()).toBe("cmd");
|
|
270
|
+
});
|
|
271
|
+
it("returns 'ctrl' on non-Mac", () => {
|
|
272
|
+
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
|
273
|
+
expect(getControlKey()).toBe("ctrl");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// parseProductionFiles (async)
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
function makeFile(name, content) {
|
|
280
|
+
return new File([content], name, { type: "text/plain" });
|
|
281
|
+
}
|
|
282
|
+
const validDttYaml = `
|
|
283
|
+
name: DecayTreeTuple/MyTree
|
|
284
|
+
descriptorTemplate: "[\${head}B+ -> \${k}K+ \${mup}mu+ \${mum}mu-]CC"
|
|
285
|
+
inputs:
|
|
286
|
+
- /Event/Leptonic/Phys/BuToKJpsiee2Line/Particles
|
|
287
|
+
tools:
|
|
288
|
+
- TupleToolKinematic:
|
|
289
|
+
Verbose: false
|
|
290
|
+
branches:
|
|
291
|
+
head:
|
|
292
|
+
particle: "B+"
|
|
293
|
+
tools: []
|
|
294
|
+
k:
|
|
295
|
+
particle: "K+"
|
|
296
|
+
tools: []
|
|
297
|
+
mup:
|
|
298
|
+
particle: "mu+"
|
|
299
|
+
tools: []
|
|
300
|
+
mum:
|
|
301
|
+
particle: "mu-"
|
|
302
|
+
tools: []
|
|
303
|
+
groups: {}
|
|
304
|
+
`.trim();
|
|
305
|
+
const validInfoYaml = `
|
|
306
|
+
defaults:
|
|
307
|
+
application: DaVinci/v45r7
|
|
308
|
+
wg: OpenData
|
|
309
|
+
inform:
|
|
310
|
+
- user@cern.ch
|
|
311
|
+
automatically_configure: true
|
|
312
|
+
output: DVNtuple.root
|
|
313
|
+
`.trim();
|
|
314
|
+
describe("parseProductionFiles()", () => {
|
|
315
|
+
it("parses a valid DTT config file and returns rows", async () => {
|
|
316
|
+
const dttFile = makeFile("my_tree.yaml", validDttYaml);
|
|
317
|
+
const result = await parseProductionFiles([dttFile], metadataWithDecay);
|
|
318
|
+
expect(result).not.toBeTypeOf("string");
|
|
319
|
+
if (typeof result === "string")
|
|
320
|
+
return;
|
|
321
|
+
expect(result.rows).toHaveLength(1);
|
|
322
|
+
});
|
|
323
|
+
it("returns an error string for an invalid YAML file", async () => {
|
|
324
|
+
const badFile = makeFile("my_tree.yaml", ": bad: yaml: {{{{");
|
|
325
|
+
const result = await parseProductionFiles([badFile], metadataWithDecay);
|
|
326
|
+
expect(result).toBeTypeOf("string");
|
|
327
|
+
expect(result).toContain("is not a valid yaml file");
|
|
328
|
+
});
|
|
329
|
+
it("returns an error string when info.yaml is invalid YAML", async () => {
|
|
330
|
+
const infoFile = makeFile("info.yaml", ": bad: yaml: {{{{");
|
|
331
|
+
const result = await parseProductionFiles([infoFile], metadataWithDecay);
|
|
332
|
+
expect(result).toBeTypeOf("string");
|
|
333
|
+
expect(result).toContain("info.yaml");
|
|
334
|
+
});
|
|
335
|
+
it("parses with valid info.yaml and extracts emails", async () => {
|
|
336
|
+
const dttFile = makeFile("my_tree.yaml", validDttYaml);
|
|
337
|
+
const infoFile = makeFile("info.yaml", validInfoYaml);
|
|
338
|
+
const result = await parseProductionFiles([dttFile, infoFile], metadataWithDecay);
|
|
339
|
+
expect(result).not.toBeTypeOf("string");
|
|
340
|
+
if (typeof result === "string")
|
|
341
|
+
return;
|
|
342
|
+
expect(result.emails).toContain("user@cern.ch");
|
|
343
|
+
});
|
|
344
|
+
it("returns an error when typia validation fails for DTT config", async () => {
|
|
345
|
+
// Override typia mock to fail for this test
|
|
346
|
+
const typia = await import("typia");
|
|
347
|
+
vi.mocked(typia.default.validate).mockReturnValueOnce({ success: false, errors: [], data: null });
|
|
348
|
+
vi.mocked(typia.default.validate).mockReturnValueOnce({ success: false, errors: [], data: null });
|
|
349
|
+
const dttFile = makeFile("my_tree.yaml", "name: test");
|
|
350
|
+
const result = await parseProductionFiles([dttFile], metadataWithDecay);
|
|
351
|
+
expect(result).toBeTypeOf("string");
|
|
352
|
+
// Reset mock
|
|
353
|
+
vi.mocked(typia.default.validate).mockImplementation((v) => ({ success: true, data: v }));
|
|
354
|
+
});
|
|
355
|
+
it("returns an error string when info.yaml fails typia validation", async () => {
|
|
356
|
+
const typia = await import("typia");
|
|
357
|
+
// First call is for info.yaml validation
|
|
358
|
+
vi.mocked(typia.default.validate).mockReturnValueOnce({ success: false, errors: [], data: null });
|
|
359
|
+
const infoFile = makeFile("info.yaml", "defaults: {}");
|
|
360
|
+
const result = await parseProductionFiles([infoFile], metadataWithDecay);
|
|
361
|
+
expect(result).toBeTypeOf("string");
|
|
362
|
+
expect(result).toContain("info.yaml");
|
|
363
|
+
vi.mocked(typia.default.validate).mockImplementation((v) => ({ success: true, data: v }));
|
|
364
|
+
});
|
|
365
|
+
it("skips addBkPaths for rows where the DTT has no name", () => {
|
|
366
|
+
// Config with no name: dtt.config.name will be undefined → line 63 skipped
|
|
367
|
+
const noNameConfig = { ...mockSavedConfig, name: undefined };
|
|
368
|
+
const infoYaml = {
|
|
369
|
+
defaults: {
|
|
370
|
+
application: "DaVinci/v45r7",
|
|
371
|
+
wg: "OpenData",
|
|
372
|
+
inform: [],
|
|
373
|
+
automatically_configure: true,
|
|
374
|
+
output: "DVNtuple.root",
|
|
375
|
+
},
|
|
376
|
+
job0: { input: { bk_query: "/some/path/DST" }, options: ["MyTree"] },
|
|
377
|
+
};
|
|
378
|
+
const result = processProductionFiles(metadataWithDecay, [noNameConfig], infoYaml);
|
|
379
|
+
if (typeof result === "string")
|
|
380
|
+
throw new Error(result);
|
|
381
|
+
// Row should have no paths because addBkPathsToRows skipped the nameless DTT row
|
|
382
|
+
expect(result.rows[0].paths).toHaveLength(0);
|
|
383
|
+
});
|
|
384
|
+
it("sorts configs alphabetically when order is equal (both not in info.yaml)", () => {
|
|
385
|
+
const configB = { ...mockSavedConfig, name: "DecayTreeTuple/BTree" };
|
|
386
|
+
const configA = { ...mockSavedConfig, name: "DecayTreeTuple/ATree" };
|
|
387
|
+
// No info.yaml → both get MAX_SAFE_INTEGER order → localeCompare used
|
|
388
|
+
const result = processProductionFiles(metadataWithDecay, [configB, configA], null);
|
|
389
|
+
if (typeof result === "string")
|
|
390
|
+
throw new Error(result);
|
|
391
|
+
expect(result.rows[0].dtt?.getName()).toBe("ATree");
|
|
392
|
+
expect(result.rows[1].dtt?.getName()).toBe("BTree");
|
|
393
|
+
});
|
|
394
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DecayData } from "../models/decayData";
|
|
2
|
+
import { MetadataContext } from "../providers/MetadataProvider";
|
|
3
|
+
export declare const LATEX_SELECTED_COLOR = "#0d6efd";
|
|
4
|
+
/**
|
|
5
|
+
* Converts a LoKi decay selector string to LaTeX representation
|
|
6
|
+
*/
|
|
7
|
+
export declare const selectionDescriptorToLatex: (metadata: MetadataContext, decay: DecayData, selection: string[]) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Converts a single particle token to LaTeX representation.
|
|
10
|
+
* Particle tokens may include a LoKi selection prefix ('^') and/or sub-decay parentheses.
|
|
11
|
+
* The `highlighted` param can be used instead of a '^' prefix when calling directly.
|
|
12
|
+
*/
|
|
13
|
+
export declare const convertParticleToLatex: (metadata: MetadataContext, particle: string, highlighted?: boolean) => string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const LATEX_SELECTED_COLOR = "#0d6efd";
|
|
2
|
+
const LOKI_MARKERS = {
|
|
3
|
+
SELECTION_PREFIX: "^",
|
|
4
|
+
ARROW: "->",
|
|
5
|
+
};
|
|
6
|
+
const LATEX_COMMANDS = {
|
|
7
|
+
ARROW: "\\to",
|
|
8
|
+
TEXT: (content) => `\\text{${content}}`,
|
|
9
|
+
COLOR: (color, content) => `\\textcolor{${color}}{${content}}`,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Removes leading and trailing square brackets from LoKi selector string
|
|
13
|
+
*/
|
|
14
|
+
const cleanLoKiSelector = (selector) => {
|
|
15
|
+
return selector.replace(/^\[/, "").replace(/^\^\[/, "^").replace(/\]CC$/, "");
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Counts occurrences of a pattern in a string
|
|
19
|
+
*/
|
|
20
|
+
const countMatches = (text, pattern) => {
|
|
21
|
+
return (text.match(pattern) || []).length;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Extracts and processes parentheses from particle names for sub-decay handling
|
|
25
|
+
*/
|
|
26
|
+
const extractParentheses = (name) => {
|
|
27
|
+
const openCount = countMatches(name, /\(/g);
|
|
28
|
+
const closeCount = countMatches(name, /\)/g);
|
|
29
|
+
if (name.startsWith("(")) {
|
|
30
|
+
return {
|
|
31
|
+
cleanName: name.replace(/^\(/, ""),
|
|
32
|
+
openBrace: "(",
|
|
33
|
+
closeBrace: "",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (name.endsWith(")") && closeCount > openCount) {
|
|
37
|
+
const extraClosingBraces = closeCount - openCount;
|
|
38
|
+
return {
|
|
39
|
+
cleanName: name.replace(/\)+$/, ""),
|
|
40
|
+
openBrace: "",
|
|
41
|
+
closeBrace: ")".repeat(extraClosingBraces),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { cleanName: name, openBrace: "", closeBrace: "" };
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Converts a LoKi decay selector string to LaTeX representation
|
|
48
|
+
*/
|
|
49
|
+
export const selectionDescriptorToLatex = (metadata, decay, selection) => {
|
|
50
|
+
// Generate LoKi selector by marking selected particles with '^'
|
|
51
|
+
const decaySelector = decay.descriptors.template.replace(/\${(\w+)}/g, (_, variableName) => {
|
|
52
|
+
return selection.includes(variableName) ? LOKI_MARKERS.SELECTION_PREFIX : "";
|
|
53
|
+
});
|
|
54
|
+
const cleanedSelector = cleanLoKiSelector(decaySelector);
|
|
55
|
+
const particles = cleanedSelector.split(" ");
|
|
56
|
+
const latexTokens = particles.map((particle) => convertParticleToLatex(metadata, particle));
|
|
57
|
+
return latexTokens.join(" ");
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Converts a single particle token to LaTeX representation.
|
|
61
|
+
* Particle tokens may include a LoKi selection prefix ('^') and/or sub-decay parentheses.
|
|
62
|
+
* The `highlighted` param can be used instead of a '^' prefix when calling directly.
|
|
63
|
+
*/
|
|
64
|
+
export const convertParticleToLatex = (metadata, particle, highlighted = false) => {
|
|
65
|
+
// Handle arrow tokens
|
|
66
|
+
if (particle === LOKI_MARKERS.ARROW) {
|
|
67
|
+
return LATEX_COMMANDS.ARROW;
|
|
68
|
+
}
|
|
69
|
+
const hasPrefix = particle.startsWith(LOKI_MARKERS.SELECTION_PREFIX);
|
|
70
|
+
const isMarked = highlighted || hasPrefix;
|
|
71
|
+
const particleName = hasPrefix ? particle.slice(LOKI_MARKERS.SELECTION_PREFIX.length) : particle;
|
|
72
|
+
// Handle sub-decay parentheses
|
|
73
|
+
const { cleanName, openBrace, closeBrace } = extractParentheses(particleName);
|
|
74
|
+
// Validate particle exists in metadata
|
|
75
|
+
if (!metadata.particleProperties[cleanName]) {
|
|
76
|
+
console.error(`${cleanName} not found in particle property metadata!`);
|
|
77
|
+
return LATEX_COMMANDS.TEXT(cleanName);
|
|
78
|
+
}
|
|
79
|
+
// Get base LaTeX representation
|
|
80
|
+
let latex = metadata.particleProperties[cleanName].latex;
|
|
81
|
+
// Color highlighted particles
|
|
82
|
+
if (isMarked) {
|
|
83
|
+
latex = LATEX_COMMANDS.COLOR(LATEX_SELECTED_COLOR, latex);
|
|
84
|
+
}
|
|
85
|
+
return `${openBrace}${latex}${closeBrace}`;
|
|
86
|
+
};
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -40,3 +40,4 @@ export declare function downloadText(yamlFile: YamlFile, type?: string): void;
|
|
|
40
40
|
* @param name - The desired archive filename
|
|
41
41
|
*/
|
|
42
42
|
export declare function downloadZip(files: YamlFile[], name?: string): Promise<void>;
|
|
43
|
+
export declare function getControlKey(): "cmd" | "ctrl";
|
package/dist/utils/utils.js
CHANGED
|
@@ -560,7 +560,7 @@ export function processProductionFiles(metadata, configs, infoYaml) {
|
|
|
560
560
|
rows.push({
|
|
561
561
|
id: rows.length,
|
|
562
562
|
decay,
|
|
563
|
-
|
|
563
|
+
line: config.inputs.length > 0 ? parseInput(decay, config.inputs[0]) : null,
|
|
564
564
|
paths: [],
|
|
565
565
|
pathOptions: [],
|
|
566
566
|
dtt: Dtt.fromSavedConfig(config, metadata.tupleTools.tupleTools),
|
|
@@ -644,3 +644,6 @@ export async function downloadZip(files, name = "archive.zip") {
|
|
|
644
644
|
const blob = await zip.generateAsync({ type: "blob" });
|
|
645
645
|
downloadBlob(blob, safeZipName);
|
|
646
646
|
}
|
|
647
|
+
export function getControlKey() {
|
|
648
|
+
return navigator.userAgent.includes("Mac") ? "cmd" : "ctrl";
|
|
649
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lhcb-ntuple-wizard-test",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "An application to access large-scale open data from LHCb",
|
|
5
5
|
"url": "https://gitlab.cern.ch/lhcb-dpa/wp6-analysis-preservation-and-open-data/lhcb-ntuple-wizard-frontend/issues",
|
|
6
6
|
"private": false,
|
|
@@ -17,22 +17,21 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@mathjax/src": "4.1.0",
|
|
20
|
-
"better-react-mathjax": "2.3.0",
|
|
21
20
|
"bootstrap": "5.3.8",
|
|
22
21
|
"cytoscape": "3.33.1",
|
|
23
22
|
"cytoscape-dagre": "^2.5.0",
|
|
24
|
-
"dompurify": "^3.3.1",
|
|
25
23
|
"email-validator": "2.0.4",
|
|
26
24
|
"js-yaml": "4.1.1",
|
|
27
25
|
"jszip": "3.10.1",
|
|
26
|
+
"katex": "^0.16.33",
|
|
28
27
|
"lodash": "^4.17.23",
|
|
29
28
|
"lodash.memoize": "4.1.2",
|
|
30
|
-
"mathjax-full": "3.2.2",
|
|
31
29
|
"pako": "2.1.0",
|
|
32
30
|
"react-bootstrap": "2.10.10",
|
|
33
31
|
"react-bootstrap-icons": "1.11.6",
|
|
34
32
|
"react-cytoscapejs": "^2.0.0",
|
|
35
33
|
"react-infinite-scroll-component": "^6.1.1",
|
|
34
|
+
"react-katex": "^3.1.0",
|
|
36
35
|
"react-select": "5.10.2",
|
|
37
36
|
"react-toastify": "^11.0.5",
|
|
38
37
|
"typia": "^11.0.3"
|
|
@@ -43,11 +42,13 @@
|
|
|
43
42
|
"react-router-dom": ">=7.12.0"
|
|
44
43
|
},
|
|
45
44
|
"overrides": {
|
|
46
|
-
"autoprefixer": "10.4.5"
|
|
45
|
+
"autoprefixer": "10.4.5",
|
|
46
|
+
"@xmldom/xmldom": "0.9.9"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"start": "vite --open",
|
|
50
50
|
"build": "tsc",
|
|
51
|
+
"test": "vitest run --coverage",
|
|
51
52
|
"prepublishOnly": "npm run build",
|
|
52
53
|
"prepare": "ts-patch install"
|
|
53
54
|
},
|
|
@@ -72,6 +73,10 @@
|
|
|
72
73
|
"@babel/preset-react": "7.27.1",
|
|
73
74
|
"@eslint/js": "^9.17.0",
|
|
74
75
|
"@kennethwkz/unplugin-typia": "^2.6.7",
|
|
76
|
+
"@testing-library/dom": "^10.4.1",
|
|
77
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
78
|
+
"@testing-library/react": "^16.3.2",
|
|
79
|
+
"@testing-library/user-event": "^14.6.1",
|
|
75
80
|
"@types/cytoscape-dagre": "^2.3.4",
|
|
76
81
|
"@types/jest": "^30.0.0",
|
|
77
82
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -81,20 +86,24 @@
|
|
|
81
86
|
"@types/react": "^19.2.7",
|
|
82
87
|
"@types/react-cytoscapejs": "^1.2.6",
|
|
83
88
|
"@types/react-dom": "^19.2.3",
|
|
89
|
+
"@types/react-katex": "^3.0.4",
|
|
84
90
|
"@types/react-router-dom": "^5.3.3",
|
|
85
91
|
"@types/vis": "^4.21.27",
|
|
86
92
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
87
93
|
"@typescript-eslint/parser": "^8.56.0",
|
|
88
94
|
"@vitejs/plugin-react": "5.1.2",
|
|
89
|
-
"
|
|
95
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
96
|
+
"eslint": "^9.39.4",
|
|
90
97
|
"eslint-plugin-react": "^7.37.5",
|
|
91
98
|
"globals": "^16.0.0",
|
|
99
|
+
"jsdom": "^29.0.1",
|
|
92
100
|
"react": "^19.2.3",
|
|
93
101
|
"react-dom": "^19.2.3",
|
|
94
102
|
"react-router-dom": "^7.12.0",
|
|
95
103
|
"ts-patch": "^3.3.0",
|
|
96
104
|
"typescript": "~5.9.3",
|
|
97
105
|
"vite": "7.3.0",
|
|
98
|
-
"vite-tsconfig-paths": "^6.0.3"
|
|
106
|
+
"vite-tsconfig-paths": "^6.0.3",
|
|
107
|
+
"vitest": "^4.1.2"
|
|
99
108
|
}
|
|
100
109
|
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Badge, OverlayTrigger, Popover } from "react-bootstrap";
|
|
3
|
-
import { StrippingLineInfo } from "./StrippingLineInfo.js";
|
|
4
|
-
export function NumStrippingLinesBadge({ strippingLines, selected }) {
|
|
5
|
-
const numStrippingLines = Object.keys(strippingLines).length;
|
|
6
|
-
return (_jsx(OverlayTrigger, { placement: "right", overlay: _jsxs(Popover, { children: [_jsx(Popover.Header, { children: "Stripping lines" }), _jsx(Popover.Body, { children: Object.keys(strippingLines).map((line) => {
|
|
7
|
-
const [stream, name] = line.split("/");
|
|
8
|
-
return (_jsx(StrippingLineInfo, { line: name, stream: stream, versions: strippingLines[line] }, line));
|
|
9
|
-
}) })] }), children: _jsxs(Badge, { pill: true, bg: selected ? "primary" : "secondary", children: [numStrippingLines, " stripping line", numStrippingLines === 1 ? "" : "s"] }) }));
|
|
10
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Button, Card, ListGroup, OverlayTrigger, Popover } from "react-bootstrap";
|
|
3
|
-
import { PencilSquare } from "react-bootstrap-icons";
|
|
4
|
-
import { DecayLatex } from "./DecayLatex";
|
|
5
|
-
import { useDtt } from "../providers/DttProvider";
|
|
6
|
-
export function TupleToolGroup({ onGroupSelected }) {
|
|
7
|
-
const { dtt, decay, selectedBranch } = useDtt();
|
|
8
|
-
let groups = {};
|
|
9
|
-
if (selectedBranch.length === 0) {
|
|
10
|
-
groups = dtt.config.groups;
|
|
11
|
-
}
|
|
12
|
-
else if (selectedBranch.length === 1) {
|
|
13
|
-
const filteredGroups = Object.entries(dtt.config.groups).filter(([key]) => key.includes(selectedBranch[0]));
|
|
14
|
-
groups = Object.fromEntries(filteredGroups);
|
|
15
|
-
}
|
|
16
|
-
const groupKeys = Object.keys(groups);
|
|
17
|
-
function ToolCount({ group }) {
|
|
18
|
-
const tools = dtt.listTools(group);
|
|
19
|
-
return (_jsx(OverlayTrigger, { overlay: _jsx(Popover, { children: _jsx(ListGroup, { children: tools.map((tool) => (_jsx(ListGroup.Item, { children: tool.toString() }, tool.toString()))) }) }), children: _jsxs("div", { children: [tools.length, " TupleTool", tools.length === 1 ? "" : "s"] }) }));
|
|
20
|
-
}
|
|
21
|
-
return (_jsxs(Card, { className: "mt-2", children: [_jsxs(Card.Header, { children: [groupKeys.length, " group", groupKeys.length === 1 ? "" : "s"] }), _jsx(ListGroup, { variant: "flush", children: groupKeys.map((selection) => (_jsxs(ListGroup.Item, { className: "d-flex justify-content-between align-items-start", children: [_jsx(DecayLatex, { decay: decay, selection: selection.split(",") }), _jsx(ToolCount, { group: selection.split(",") }), _jsx(Button, { variant: "outline-secondary", size: "sm", onClick: () => onGroupSelected(selection), children: _jsx(PencilSquare, {}) })] }, selection))) })] }));
|
|
22
|
-
}
|