lula2 0.3.1 → 0.3.2-nightly.1

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.
Files changed (44) hide show
  1. package/dist/_app/immutable/assets/0.CLKu6Q8_.css +1 -0
  2. package/dist/_app/immutable/chunks/{BtOhWAVU.js → 152nb-LI.js} +2 -2
  3. package/dist/_app/immutable/chunks/{C5zWTfmV.js → 1spjHGNy.js} +1 -1
  4. package/dist/_app/immutable/chunks/{DnJ0bPgj.js → BtuEtkd3.js} +1 -1
  5. package/dist/_app/immutable/chunks/C113Bo4B.js +2 -0
  6. package/dist/_app/immutable/chunks/{CoF2vljD.js → CNOPXlDW.js} +1 -1
  7. package/dist/_app/immutable/chunks/DFKxAz5y.js +3 -0
  8. package/dist/_app/immutable/chunks/DJ-Jk3EP.js +65 -0
  9. package/dist/_app/immutable/chunks/DRm-CuN2.js +1 -0
  10. package/dist/_app/immutable/chunks/{DqsOU3kV.js → DY3-lqhI.js} +1 -1
  11. package/dist/_app/immutable/entry/{app.XLGRlmCF.js → app.BZBPzL5_.js} +2 -2
  12. package/dist/_app/immutable/entry/start.DhLq_cQt.js +1 -0
  13. package/dist/_app/immutable/nodes/{0.B6W16O68.js → 0.D4xnWOOY.js} +1 -1
  14. package/dist/_app/immutable/nodes/{1.wIilWkgu.js → 1.CfErG2gH.js} +1 -1
  15. package/dist/_app/immutable/nodes/{2.CG53uQH9.js → 2.CykQIDMZ.js} +1 -1
  16. package/dist/_app/immutable/nodes/{3.D4uH9LCp.js → 3.MN3LEF69.js} +1 -1
  17. package/dist/_app/immutable/nodes/{4.sYW2-VhJ.js → 4.DIv4kITF.js} +1 -1
  18. package/dist/_app/version.json +1 -1
  19. package/dist/cli/commands/ui.js +6 -6
  20. package/dist/cli/server/index.js +6 -6
  21. package/dist/cli/server/server.js +6 -6
  22. package/dist/cli/server/serverState.js +2 -2
  23. package/dist/cli/server/spreadsheetRoutes.js +4 -4
  24. package/dist/cli/server/websocketServer.js +6 -6
  25. package/dist/index.html +10 -10
  26. package/dist/index.js +6 -6
  27. package/package.json +21 -21
  28. package/src/lib/actions/clickOutside.ts +24 -0
  29. package/src/lib/components/controls/ControlsList.svelte +101 -75
  30. package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +1 -1
  31. package/src/lib/components/controls/tabs/TimelineTab.svelte +1 -1
  32. package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +1 -1
  33. package/src/lib/components/forms/DynamicControlForm.svelte +2 -2
  34. package/src/lib/components/setup/ExistingControlSets.svelte +0 -1
  35. package/src/lib/components/setup/SpreadsheetImport.svelte +10 -10
  36. package/src/lib/components/ui/CustomDropdown.svelte +96 -0
  37. package/src/lib/components/ui/FilterBuilder.svelte +415 -0
  38. package/src/stores/compliance.ts +149 -39
  39. package/dist/_app/immutable/assets/0.Dv98laBw.css +0 -1
  40. package/dist/_app/immutable/chunks/BW28MavF.js +0 -1
  41. package/dist/_app/immutable/chunks/Cby0Z7eP.js +0 -2
  42. package/dist/_app/immutable/chunks/DQAmyY_z.js +0 -66
  43. package/dist/_app/immutable/chunks/Ds14DLx0.js +0 -3
  44. package/dist/_app/immutable/entry/start.68S9ad6U.js +0 -1
@@ -234,7 +234,7 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
234
234
  if (req.body.fieldSchema) {
235
235
  try {
236
236
  frontendFieldSchema = JSON.parse(req.body.fieldSchema);
237
- } catch (e) {
237
+ } catch {
238
238
  }
239
239
  }
240
240
  const fields = {};
@@ -743,7 +743,7 @@ router.post("/parse-excel", upload.single("file"), async (req, res) => {
743
743
  if (!worksheet) {
744
744
  return res.status(400).json({ error: "No worksheet found in file" });
745
745
  }
746
- worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
746
+ worksheet.eachRow({ includeEmpty: false }, (row, _rowNumber) => {
747
747
  const rowData = [];
748
748
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
749
749
  rowData[colNumber - 1] = cell.value;
@@ -753,7 +753,7 @@ router.post("/parse-excel", upload.single("file"), async (req, res) => {
753
753
  }
754
754
  const headerCandidates = rows.slice(0, 5).map((row, index) => ({
755
755
  row: index + 1,
756
- preview: row.slice(0, 4).filter((v) => v != null).join(", ") + (row.length > 4 ? ", ..." : "")
756
+ preview: row.slice(0, 4).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
757
757
  }));
758
758
  res.json({
759
759
  sheets,
@@ -788,7 +788,7 @@ router.post("/parse-excel-sheet", upload.single("file"), async (req, res) => {
788
788
  if (!worksheet) {
789
789
  return res.status(400).json({ error: `Sheet "${sheetName}" not found` });
790
790
  }
791
- worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
791
+ worksheet.eachRow({ includeEmpty: false }, (row, _rowNumber) => {
792
792
  const rowData = [];
793
793
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
794
794
  rowData[colNumber - 1] = cell.value;
@@ -990,7 +990,7 @@ var init_gitHistory = __esm({
990
990
  changes = diffResult.changes;
991
991
  diff = diffResult.diff;
992
992
  yamlDiff = diffResult.yamlDiff;
993
- } catch (error) {
993
+ } catch {
994
994
  }
995
995
  }
996
996
  gitCommits.push({
@@ -1047,7 +1047,7 @@ var init_gitHistory = __esm({
1047
1047
  diff,
1048
1048
  yamlDiff
1049
1049
  };
1050
- } catch (error) {
1050
+ } catch {
1051
1051
  return { changes: { insertions: 0, deletions: 0, files: 1 } };
1052
1052
  }
1053
1053
  }
@@ -1722,7 +1722,7 @@ var init_spreadsheetRoutes = __esm({
1722
1722
  if (req.body.fieldSchema) {
1723
1723
  try {
1724
1724
  frontendFieldSchema = JSON.parse(req.body.fieldSchema);
1725
- } catch (e) {
1725
+ } catch {
1726
1726
  }
1727
1727
  }
1728
1728
  const fields = {};
@@ -1982,7 +1982,7 @@ var init_spreadsheetRoutes = __esm({
1982
1982
  if (!worksheet) {
1983
1983
  return res.status(400).json({ error: "No worksheet found in file" });
1984
1984
  }
1985
- worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
1985
+ worksheet.eachRow({ includeEmpty: false }, (row, _rowNumber) => {
1986
1986
  const rowData = [];
1987
1987
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
1988
1988
  rowData[colNumber - 1] = cell.value;
@@ -1992,7 +1992,7 @@ var init_spreadsheetRoutes = __esm({
1992
1992
  }
1993
1993
  const headerCandidates = rows.slice(0, 5).map((row, index) => ({
1994
1994
  row: index + 1,
1995
- preview: row.slice(0, 4).filter((v) => v != null).join(", ") + (row.length > 4 ? ", ..." : "")
1995
+ preview: row.slice(0, 4).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
1996
1996
  }));
1997
1997
  res.json({
1998
1998
  sheets,
@@ -2027,7 +2027,7 @@ var init_spreadsheetRoutes = __esm({
2027
2027
  if (!worksheet) {
2028
2028
  return res.status(400).json({ error: `Sheet "${sheetName}" not found` });
2029
2029
  }
2030
- worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
2030
+ worksheet.eachRow({ includeEmpty: false }, (row, _rowNumber) => {
2031
2031
  const rowData = [];
2032
2032
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
2033
2033
  rowData[colNumber - 1] = cell.value;
package/dist/index.html CHANGED
@@ -6,28 +6,28 @@
6
6
  <link rel="icon" href="/lula.png" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
8
 
9
- <link rel="modulepreload" href="/_app/immutable/entry/start.68S9ad6U.js">
10
- <link rel="modulepreload" href="/_app/immutable/chunks/Ds14DLx0.js">
11
- <link rel="modulepreload" href="/_app/immutable/chunks/Cby0Z7eP.js">
12
- <link rel="modulepreload" href="/_app/immutable/entry/app.XLGRlmCF.js">
9
+ <link rel="modulepreload" href="/_app/immutable/entry/start.DhLq_cQt.js">
10
+ <link rel="modulepreload" href="/_app/immutable/chunks/DFKxAz5y.js">
11
+ <link rel="modulepreload" href="/_app/immutable/chunks/C113Bo4B.js">
12
+ <link rel="modulepreload" href="/_app/immutable/entry/app.BZBPzL5_.js">
13
13
  <link rel="modulepreload" href="/_app/immutable/chunks/DsnmJJEf.js">
14
- <link rel="modulepreload" href="/_app/immutable/chunks/DqsOU3kV.js">
15
- <link rel="modulepreload" href="/_app/immutable/chunks/CoF2vljD.js">
16
- <link rel="modulepreload" href="/_app/immutable/chunks/DnJ0bPgj.js">
14
+ <link rel="modulepreload" href="/_app/immutable/chunks/DY3-lqhI.js">
15
+ <link rel="modulepreload" href="/_app/immutable/chunks/CNOPXlDW.js">
16
+ <link rel="modulepreload" href="/_app/immutable/chunks/BtuEtkd3.js">
17
17
  </head>
18
18
  <body data-sveltekit-preload-data="hover">
19
19
  <div style="display: contents">
20
20
  <script>
21
21
  {
22
- __sveltekit_6f9em3 = {
22
+ __sveltekit_11l0s6o = {
23
23
  base: ""
24
24
  };
25
25
 
26
26
  const element = document.currentScript.parentElement;
27
27
 
28
28
  Promise.all([
29
- import("/_app/immutable/entry/start.68S9ad6U.js"),
30
- import("/_app/immutable/entry/app.XLGRlmCF.js")
29
+ import("/_app/immutable/entry/start.DhLq_cQt.js"),
30
+ import("/_app/immutable/entry/app.BZBPzL5_.js")
31
31
  ]).then(([kit, app]) => {
32
32
  kit.start(app, element);
33
33
  });
package/dist/index.js CHANGED
@@ -2562,7 +2562,7 @@ var init_gitHistory = __esm({
2562
2562
  changes = diffResult.changes;
2563
2563
  diff = diffResult.diff;
2564
2564
  yamlDiff = diffResult.yamlDiff;
2565
- } catch (error) {
2565
+ } catch {
2566
2566
  }
2567
2567
  }
2568
2568
  gitCommits.push({
@@ -2619,7 +2619,7 @@ var init_gitHistory = __esm({
2619
2619
  diff,
2620
2620
  yamlDiff
2621
2621
  };
2622
- } catch (error) {
2622
+ } catch {
2623
2623
  return { changes: { insertions: 0, deletions: 0, files: 1 } };
2624
2624
  }
2625
2625
  }
@@ -3294,7 +3294,7 @@ var init_spreadsheetRoutes = __esm({
3294
3294
  if (req.body.fieldSchema) {
3295
3295
  try {
3296
3296
  frontendFieldSchema = JSON.parse(req.body.fieldSchema);
3297
- } catch (e) {
3297
+ } catch {
3298
3298
  }
3299
3299
  }
3300
3300
  const fields = {};
@@ -3554,7 +3554,7 @@ var init_spreadsheetRoutes = __esm({
3554
3554
  if (!worksheet) {
3555
3555
  return res.status(400).json({ error: "No worksheet found in file" });
3556
3556
  }
3557
- worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
3557
+ worksheet.eachRow({ includeEmpty: false }, (row, _rowNumber) => {
3558
3558
  const rowData = [];
3559
3559
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
3560
3560
  rowData[colNumber - 1] = cell.value;
@@ -3564,7 +3564,7 @@ var init_spreadsheetRoutes = __esm({
3564
3564
  }
3565
3565
  const headerCandidates = rows.slice(0, 5).map((row, index) => ({
3566
3566
  row: index + 1,
3567
- preview: row.slice(0, 4).filter((v) => v != null).join(", ") + (row.length > 4 ? ", ..." : "")
3567
+ preview: row.slice(0, 4).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
3568
3568
  }));
3569
3569
  res.json({
3570
3570
  sheets,
@@ -3599,7 +3599,7 @@ var init_spreadsheetRoutes = __esm({
3599
3599
  if (!worksheet) {
3600
3600
  return res.status(400).json({ error: `Sheet "${sheetName}" not found` });
3601
3601
  }
3602
- worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
3602
+ worksheet.eachRow({ includeEmpty: false }, (row, _rowNumber) => {
3603
3603
  const rowData = [];
3604
3604
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
3605
3605
  rowData[colNumber - 1] = cell.value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lula2",
3
- "version": "0.3.1",
3
+ "version": "0.3.2-nightly.1",
4
4
  "description": "A tool for managing compliance as code in your GitHub repositories.",
5
5
  "bin": {
6
6
  "lula2": "./dist/lula2"
@@ -33,25 +33,6 @@
33
33
  "!dist/**/*.test.js*",
34
34
  "!dist/**/*.test.d.ts*"
35
35
  ],
36
- "scripts": {
37
- "dev": "vite dev --port 5173",
38
- "dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
39
- "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
40
- "build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
41
- "build:svelte": "vite build",
42
- "build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
43
- "postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
44
- "preview": "vite preview",
45
- "prepare": "svelte-kit sync || echo ''",
46
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
47
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
48
- "format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
49
- "format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
50
- "lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
51
- "test": "npm run test:unit -- --run --coverage",
52
- "test:integration": "vitest --config integration/vitest.config.integration.ts",
53
- "test:unit": "vitest"
54
- },
55
36
  "dependencies": {
56
37
  "@octokit/rest": "^22.0.0",
57
38
  "@types/ws": "^8.18.1",
@@ -75,6 +56,7 @@
75
56
  "@commitlint/cli": "^19.8.1",
76
57
  "@commitlint/config-conventional": "^19.8.1",
77
58
  "@eslint/compat": "^1.3.2",
59
+ "@eslint/eslintrc": "^3.3.1",
78
60
  "@eslint/js": "^9.35.0",
79
61
  "@playwright/test": "^1.55.0",
80
62
  "@sveltejs/adapter-static": "^3.0.9",
@@ -123,5 +105,23 @@
123
105
  "main",
124
106
  "next"
125
107
  ]
108
+ },
109
+ "scripts": {
110
+ "dev": "vite dev --port 5173",
111
+ "dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
112
+ "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
113
+ "build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
114
+ "build:svelte": "vite build",
115
+ "build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
116
+ "postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
117
+ "preview": "vite preview",
118
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
119
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
120
+ "format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
121
+ "format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
122
+ "lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
123
+ "test": "npm run test:unit -- --run --coverage",
124
+ "test:integration": "vitest --config integration/vitest.config.integration.ts",
125
+ "test:unit": "vitest"
126
126
  }
127
- }
127
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ /**
5
+ * Action that dispatches an event when a click occurs outside of the node it's applied to
6
+ */
7
+ export function clickOutside(node: HTMLElement, callback: () => void) {
8
+ const handleClick = (event: MouseEvent) => {
9
+ if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
10
+ callback();
11
+ }
12
+ };
13
+
14
+ document.addEventListener('click', handleClick, true);
15
+
16
+ return {
17
+ destroy() {
18
+ document.removeEventListener('click', handleClick, true);
19
+ },
20
+ update(newCallback: () => void) {
21
+ callback = newCallback;
22
+ }
23
+ };
24
+ }
@@ -4,16 +4,16 @@
4
4
  <script lang="ts">
5
5
  import { goto } from '$app/navigation';
6
6
  import { page } from '$app/stores';
7
- import { Dropdown, SearchBar, Tooltip } from '$components/ui';
7
+ import { SearchBar, Tooltip } from '$components/ui';
8
+ import FilterBuilder from '$components/ui/FilterBuilder.svelte';
8
9
  import type { Control, FieldSchema } from '$lib/types';
9
10
  import { appState } from '$lib/websocket';
10
- import { complianceStore, searchTerm, selectedFamily } from '$stores/compliance';
11
- import { Filter, Information } from 'carbon-icons-svelte';
11
+ import { complianceStore, searchTerm, activeFilters, getOperatorLabel } from '$stores/compliance';
12
+ import { Information } from 'carbon-icons-svelte';
12
13
  import { derived } from 'svelte/store';
13
14
 
14
- // Derive controls and families from appState
15
+ // Derive controls from appState
15
16
  const controls = derived(appState, ($state) => $state.controls || []);
16
- const families = derived(appState, ($state) => $state.families || []);
17
17
  const loading = derived(appState, ($state) => !$state.isConnected);
18
18
 
19
19
  // Derive controls with mappings
@@ -104,26 +104,64 @@
104
104
 
105
105
  // Create filtered controls with mappings
106
106
  const filteredControlsWithMappings = derived(
107
- [controlsWithMappings, selectedFamily, searchTerm],
108
- ([$controlsWithMappings, $selectedFamily, $searchTerm]) => {
107
+ [controlsWithMappings, searchTerm, activeFilters],
108
+ ([$controlsWithMappings, $searchTerm, $activeFilters]) => {
109
109
  let results = $controlsWithMappings;
110
110
 
111
- if ($selectedFamily) {
112
- results = results.filter((c) => {
113
- const family =
114
- (c as any)?._metadata?.family ||
115
- (c as any)?.family ||
116
- (c as any)?.['control-acronym']?.split('-')[0] ||
117
- '';
118
- return family === $selectedFamily;
119
- });
120
- }
121
-
111
+ // Apply search term
122
112
  if ($searchTerm) {
123
113
  const term = $searchTerm.toLowerCase();
124
114
  results = results.filter((c) => JSON.stringify(c).toLowerCase().includes(term));
125
115
  }
126
116
 
117
+ // Apply advanced filters
118
+ if ($activeFilters.length > 0) {
119
+ results = results.filter((control) => {
120
+ // Control must match all filters
121
+ return $activeFilters.every(filter => {
122
+ const dynamicControl = control as Record<string, unknown>;
123
+ const fieldValue = dynamicControl[filter.fieldName];
124
+
125
+ switch (filter.operator) {
126
+ case 'equals':
127
+ return fieldValue === filter.value;
128
+
129
+ case 'not_equals':
130
+ return fieldValue !== filter.value;
131
+
132
+ case 'exists':
133
+ return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
134
+
135
+ case 'not_exists':
136
+ return fieldValue === undefined || fieldValue === null || fieldValue === '';
137
+
138
+ case 'includes':
139
+ if (typeof fieldValue === 'string') {
140
+ return fieldValue.toLowerCase().includes(String(filter.value).toLowerCase());
141
+ } else if (Array.isArray(fieldValue)) {
142
+ return fieldValue.some(item =>
143
+ String(item).toLowerCase().includes(String(filter.value).toLowerCase())
144
+ );
145
+ }
146
+ return false;
147
+
148
+ case 'not_includes':
149
+ if (typeof fieldValue === 'string') {
150
+ return !fieldValue.toLowerCase().includes(String(filter.value).toLowerCase());
151
+ } else if (Array.isArray(fieldValue)) {
152
+ return !fieldValue.some(item =>
153
+ String(item).toLowerCase().includes(String(filter.value).toLowerCase())
154
+ );
155
+ }
156
+ return true;
157
+
158
+ default:
159
+ return true;
160
+ }
161
+ });
162
+ });
163
+ }
164
+
127
165
  return results;
128
166
  }
129
167
  );
@@ -243,64 +281,53 @@
243
281
  </span>
244
282
  </div>
245
283
 
246
- <!-- Search Bar, Family Filter, and Export -->
284
+ <!-- Search Bar, Filter, and Export -->
247
285
  <div class="flex gap-3">
248
286
  <div class="flex-1">
249
287
  <SearchBar />
250
288
  </div>
289
+
290
+ <!-- Filter Builder -->
251
291
  <div class="flex-shrink-0">
252
- <Dropdown
253
- buttonLabel={$selectedFamily || 'All Families'}
254
- buttonIcon={Filter}
255
- buttonClass="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
256
- dropdownClass="w-64"
257
- >
258
- {#snippet children()}
259
- <div class="space-y-1">
260
- <button
261
- onclick={() => {
262
- complianceStore.setSelectedFamily(null);
263
- }}
264
- class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 flex items-center justify-between {$selectedFamily ===
265
- null
266
- ? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
267
- : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
268
- >
269
- <span>All Families</span>
270
- <span class="text-xs bg-gray-200 dark:bg-gray-600 px-2 py-1 rounded-full">
271
- {$controls.length}
272
- </span>
273
- </button>
274
-
275
- {#each $families as family}
276
- {@const familyCount = $controls.filter((c) => {
277
- const controlFamily =
278
- (c as any)?._metadata?.family ||
279
- (c as any)?.family ||
280
- (c as any)?.['control-acronym']?.split('-')[0] ||
281
- '';
282
- return controlFamily === family;
283
- }).length}
284
- <button
285
- onclick={() => {
286
- complianceStore.setSelectedFamily(family);
287
- }}
288
- class="w-full text-left px-3 py-2 text-sm rounded-md transition-colors duration-200 flex items-center justify-between {$selectedFamily ===
289
- family
290
- ? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
291
- : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
292
- >
293
- <span>{family}</span>
294
- <span class="text-xs bg-gray-200 dark:bg-gray-600 px-2 py-1 rounded-full">
295
- {familyCount}
296
- </span>
297
- </button>
298
- {/each}
299
- </div>
300
- {/snippet}
301
- </Dropdown>
292
+ <FilterBuilder />
302
293
  </div>
303
294
  </div>
295
+
296
+ <!-- Active Filters Summary -->
297
+ {#if $activeFilters.length > 0}
298
+ <div class="mt-2 flex flex-wrap gap-2">
299
+ {#each $activeFilters as filter, index}
300
+ <div class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300">
301
+ <span>{filter.fieldName}: </span>
302
+ {#if filter.operator === 'exists' || filter.operator === 'not_exists'}
303
+ <span>{getOperatorLabel(filter.operator).toLowerCase()}</span>
304
+ {:else if filter.operator === 'equals'}
305
+ <span>= {filter.value}</span>
306
+ {:else if filter.operator === 'not_equals'}
307
+ <span>≠ {filter.value}</span>
308
+ {:else}
309
+ <span>{getOperatorLabel(filter.operator).toLowerCase()} "{filter.value}"</span>
310
+ {/if}
311
+ <button
312
+ onclick={() => complianceStore.removeFilter(index)}
313
+ class="ml-1 text-blue-700 dark:text-blue-300 hover:text-blue-900 dark:hover:text-blue-100"
314
+ aria-label="Remove filter"
315
+ >
316
+ ×
317
+ </button>
318
+ </div>
319
+ {/each}
320
+
321
+ {#if $activeFilters.length > 1}
322
+ <button
323
+ onclick={() => complianceStore.clearFilters()}
324
+ class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
325
+ >
326
+ Clear all
327
+ </button>
328
+ {/if}
329
+ </div>
330
+ {/if}
304
331
  </div>
305
332
 
306
333
  <!-- Controls Table -->
@@ -582,15 +609,14 @@
582
609
  </svg>
583
610
  <h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No controls found</h3>
584
611
  <p class="mt-2 text-sm text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
585
- {#if $searchTerm}
586
- No controls match your search criteria. Try adjusting your search terms or clearing
587
- filters.
588
- {:else if $selectedFamily}
589
- No controls available in this family. Select a different family or check your data.
612
+ {#if $activeFilters.length > 0}
613
+ No controls match your filter criteria. Try adjusting or removing some filters.
614
+ {:else if $searchTerm}
615
+ No controls match your search criteria. Try adjusting your search terms.
590
616
  {:else if $controls.length === 0}
591
617
  No controls have been imported yet.
592
618
  {:else}
593
- No controls available. Select a different family or check your data.
619
+ No controls available. Check your data.
594
620
  {/if}
595
621
  </p>
596
622
  {#if $controls.length === 0}
@@ -127,4 +127,4 @@
127
127
  <p class="text-gray-500 dark:text-gray-400">No custom fields configured</p>
128
128
  </div>
129
129
  {/if}
130
- </div>
130
+ </div>
@@ -11,7 +11,7 @@
11
11
  timeline?: any; // Timeline type from control.timeline
12
12
  }
13
13
 
14
- let { control, timeline }: Props = $props();
14
+ let { timeline }: Props = $props();
15
15
 
16
16
  const commits = $derived(timeline?.commits || []);
17
17
  </script>
@@ -23,7 +23,7 @@
23
23
  <div class="overflow-x-auto">
24
24
  <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
25
25
  <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
26
- {#each section.data.rows as row, i}
26
+ {#each section.data.rows as row, _i}
27
27
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
28
28
  {#each row.columns as column, j}
29
29
  <td class="px-3 py-2 text-sm {j === 0 ? 'font-medium text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-400'}">
@@ -128,7 +128,7 @@
128
128
  {#if readonly}
129
129
  <!-- View Mode: Clean minimal layout -->
130
130
  <div class="space-y-6">
131
- {#each Object.entries(fieldGroups) as [groupName, fields]}
131
+ {#each Object.entries(fieldGroups) as [_groupName, fields]}
132
132
  {#each [fields] as fieldList}
133
133
  {@const importantFields = fieldList.filter((f) =>
134
134
  ['id', 'title', 'priority', 'status'].includes(f.id)
@@ -226,7 +226,7 @@
226
226
  </span>
227
227
  </h3>
228
228
  <div class="space-y-3">
229
- {#each value as item, index}
229
+ {#each value as item, _index}
230
230
  <div
231
231
  class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700"
232
232
  >
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
3
2
  import { createEventDispatcher } from 'svelte';
4
3
  import { appState } from '$lib/websocket';
5
4
 
@@ -784,8 +784,8 @@
784
784
  aria-label="Overview tab drop zone"
785
785
  >
786
786
  {#each Array.from(fieldConfigs.entries())
787
- .filter(([field, config]) => config.tab === 'overview')
788
- .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, config], index (field)}
787
+ .filter(([_field, config]) => config.tab === 'overview')
788
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, _config], _index (field)}
789
789
  <div
790
790
  draggable="true"
791
791
  on:dragstart={(e) => handleFieldDragStart(e, field)}
@@ -805,7 +805,7 @@
805
805
  <span class="truncate">{field}</span>
806
806
  </div>
807
807
  {/each}
808
- {#if Array.from(fieldConfigs.entries()).filter(([field, config]) => config.tab === 'overview').length === 0}
808
+ {#if Array.from(fieldConfigs.entries()).filter(([_field, config]) => config.tab === 'overview').length === 0}
809
809
  <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
810
810
  Drop fields here
811
811
  </p>
@@ -835,8 +835,8 @@
835
835
  aria-label="Implementation tab drop zone"
836
836
  >
837
837
  {#each Array.from(fieldConfigs.entries())
838
- .filter(([field, config]) => config.tab === 'implementation')
839
- .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, config], index (field)}
838
+ .filter(([_field, config]) => config.tab === 'implementation')
839
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, _config], index (field)}
840
840
  <div
841
841
  draggable="true"
842
842
  on:dragstart={(e) => handleFieldDragStart(e, field)}
@@ -856,7 +856,7 @@
856
856
  <span class="truncate">{field}</span>
857
857
  </div>
858
858
  {/each}
859
- {#if Array.from(fieldConfigs.entries()).filter(([field, config]) => config.tab === 'implementation').length === 0}
859
+ {#if Array.from(fieldConfigs.entries()).filter(([_field, config]) => config.tab === 'implementation').length === 0}
860
860
  <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
861
861
  Drop fields here
862
862
  </p>
@@ -889,7 +889,7 @@
889
889
  <div class="space-y-2">
890
890
  {#if justificationFields.length > 0}
891
891
  <!-- Display justification fields -->
892
- {#each justificationFields as field, index}
892
+ {#each justificationFields as field, _index}
893
893
  <div
894
894
  draggable="true"
895
895
  on:dragstart={(e) => handleFieldDragStart(e, field)}
@@ -966,8 +966,8 @@
966
966
  aria-label="Custom fields drop zone"
967
967
  >
968
968
  {#each Array.from(fieldConfigs.entries())
969
- .filter(([field, config]) => config.tab === 'custom')
970
- .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, config], index (field)}
969
+ .filter(([_field, config]) => config.tab === 'custom')
970
+ .sort((a, b) => a[1].displayOrder - b[1].displayOrder) as [field, _config], _index (field)}
971
971
  <div
972
972
  draggable="true"
973
973
  on:dragstart={(e) => handleFieldDragStart(e, field)}
@@ -987,7 +987,7 @@
987
987
  <span class="truncate">{field}</span>
988
988
  </div>
989
989
  {/each}
990
- {#if Array.from(fieldConfigs.entries()).filter(([field, config]) => config.tab === 'custom').length === 0}
990
+ {#if Array.from(fieldConfigs.entries()).filter(([_field, config]) => config.tab === 'custom').length === 0}
991
991
  <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
992
992
  Drop fields here
993
993
  </p>