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.
- package/dist/_app/immutable/assets/0.CLKu6Q8_.css +1 -0
- package/dist/_app/immutable/chunks/{BtOhWAVU.js → 152nb-LI.js} +2 -2
- package/dist/_app/immutable/chunks/{C5zWTfmV.js → 1spjHGNy.js} +1 -1
- package/dist/_app/immutable/chunks/{DnJ0bPgj.js → BtuEtkd3.js} +1 -1
- package/dist/_app/immutable/chunks/C113Bo4B.js +2 -0
- package/dist/_app/immutable/chunks/{CoF2vljD.js → CNOPXlDW.js} +1 -1
- package/dist/_app/immutable/chunks/DFKxAz5y.js +3 -0
- package/dist/_app/immutable/chunks/DJ-Jk3EP.js +65 -0
- package/dist/_app/immutable/chunks/DRm-CuN2.js +1 -0
- package/dist/_app/immutable/chunks/{DqsOU3kV.js → DY3-lqhI.js} +1 -1
- package/dist/_app/immutable/entry/{app.XLGRlmCF.js → app.BZBPzL5_.js} +2 -2
- package/dist/_app/immutable/entry/start.DhLq_cQt.js +1 -0
- package/dist/_app/immutable/nodes/{0.B6W16O68.js → 0.D4xnWOOY.js} +1 -1
- package/dist/_app/immutable/nodes/{1.wIilWkgu.js → 1.CfErG2gH.js} +1 -1
- package/dist/_app/immutable/nodes/{2.CG53uQH9.js → 2.CykQIDMZ.js} +1 -1
- package/dist/_app/immutable/nodes/{3.D4uH9LCp.js → 3.MN3LEF69.js} +1 -1
- package/dist/_app/immutable/nodes/{4.sYW2-VhJ.js → 4.DIv4kITF.js} +1 -1
- package/dist/_app/version.json +1 -1
- package/dist/cli/commands/ui.js +6 -6
- package/dist/cli/server/index.js +6 -6
- package/dist/cli/server/server.js +6 -6
- package/dist/cli/server/serverState.js +2 -2
- package/dist/cli/server/spreadsheetRoutes.js +4 -4
- package/dist/cli/server/websocketServer.js +6 -6
- package/dist/index.html +10 -10
- package/dist/index.js +6 -6
- package/package.json +21 -21
- package/src/lib/actions/clickOutside.ts +24 -0
- package/src/lib/components/controls/ControlsList.svelte +101 -75
- package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +1 -1
- package/src/lib/components/controls/tabs/TimelineTab.svelte +1 -1
- package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +1 -1
- package/src/lib/components/forms/DynamicControlForm.svelte +2 -2
- package/src/lib/components/setup/ExistingControlSets.svelte +0 -1
- package/src/lib/components/setup/SpreadsheetImport.svelte +10 -10
- package/src/lib/components/ui/CustomDropdown.svelte +96 -0
- package/src/lib/components/ui/FilterBuilder.svelte +415 -0
- package/src/stores/compliance.ts +149 -39
- package/dist/_app/immutable/assets/0.Dv98laBw.css +0 -1
- package/dist/_app/immutable/chunks/BW28MavF.js +0 -1
- package/dist/_app/immutable/chunks/Cby0Z7eP.js +0 -2
- package/dist/_app/immutable/chunks/DQAmyY_z.js +0 -66
- package/dist/_app/immutable/chunks/Ds14DLx0.js +0 -3
- package/dist/_app/immutable/entry/start.68S9ad6U.js +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts" generics="T">
|
|
5
|
+
import { clickOutside } from '$lib/actions/clickOutside';
|
|
6
|
+
import { fade } from 'svelte/transition';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
8
|
+
import { ChevronDown, ChevronUp } from 'carbon-icons-svelte';
|
|
9
|
+
|
|
10
|
+
// Props
|
|
11
|
+
export let value: T = undefined as unknown as T;
|
|
12
|
+
export let options: Array<{ value: T; label: string }> = [];
|
|
13
|
+
export let placeholder: string = 'Select an option';
|
|
14
|
+
export let label: string | undefined = undefined;
|
|
15
|
+
export let labelId: string | undefined = undefined;
|
|
16
|
+
export let getDisplayValue: (value: T) => string = (val) => {
|
|
17
|
+
const option = options.find((opt) => opt.value === val);
|
|
18
|
+
return option ? option.label : String(val);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// State
|
|
22
|
+
let isOpen = false;
|
|
23
|
+
|
|
24
|
+
// Handle selection
|
|
25
|
+
function selectOption(optionValue: T) {
|
|
26
|
+
value = optionValue;
|
|
27
|
+
isOpen = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Handle toggle
|
|
31
|
+
function toggle() {
|
|
32
|
+
isOpen = !isOpen;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle close
|
|
36
|
+
function close() {
|
|
37
|
+
isOpen = false;
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div>
|
|
42
|
+
{#if label}
|
|
43
|
+
<label for={labelId} class="block text-xs text-gray-600 dark:text-gray-400 mb-1">{label}</label>
|
|
44
|
+
{/if}
|
|
45
|
+
<div class="relative" use:clickOutside={close}>
|
|
46
|
+
<!-- Dropdown Trigger -->
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
id={labelId}
|
|
50
|
+
on:click={toggle}
|
|
51
|
+
class={twMerge(
|
|
52
|
+
'w-full flex items-center justify-between px-3 py-2 text-sm rounded-md border transition-colors',
|
|
53
|
+
isOpen
|
|
54
|
+
? 'border-blue-500 dark:border-blue-400 ring-2 ring-blue-200 dark:ring-blue-900'
|
|
55
|
+
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500',
|
|
56
|
+
'bg-white dark:bg-gray-700 text-gray-900 dark:text-white'
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<span class="truncate">
|
|
60
|
+
{value ? getDisplayValue(value) : placeholder}
|
|
61
|
+
</span>
|
|
62
|
+
{#if isOpen}
|
|
63
|
+
<ChevronUp class="h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
64
|
+
{:else}
|
|
65
|
+
<ChevronDown class="h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
66
|
+
{/if}
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<!-- Dropdown Menu -->
|
|
70
|
+
{#if isOpen}
|
|
71
|
+
<div
|
|
72
|
+
class="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg max-h-60 overflow-y-auto"
|
|
73
|
+
transition:fade={{ duration: 100 }}
|
|
74
|
+
>
|
|
75
|
+
<slot name="header" />
|
|
76
|
+
|
|
77
|
+
{#each options as option}
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class={twMerge(
|
|
81
|
+
'w-full text-left px-3 py-2 text-sm',
|
|
82
|
+
value === option.value
|
|
83
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
84
|
+
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
|
85
|
+
)}
|
|
86
|
+
on:click={() => selectOption(option.value)}
|
|
87
|
+
>
|
|
88
|
+
{option.label}
|
|
89
|
+
</button>
|
|
90
|
+
{/each}
|
|
91
|
+
|
|
92
|
+
<slot name="footer" />
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import {
|
|
6
|
+
complianceStore,
|
|
7
|
+
type FilterOperator,
|
|
8
|
+
type FilterCondition,
|
|
9
|
+
type FilterValue,
|
|
10
|
+
activeFilters,
|
|
11
|
+
FILTER_OPERATORS,
|
|
12
|
+
getOperatorLabel
|
|
13
|
+
} from '$stores/compliance';
|
|
14
|
+
import { appState } from '$lib/websocket';
|
|
15
|
+
import { Filter, Add, TrashCan, ChevronDown, ChevronUp } from 'carbon-icons-svelte';
|
|
16
|
+
import { clickOutside } from '$lib/actions/clickOutside';
|
|
17
|
+
import { slide, fade } from 'svelte/transition';
|
|
18
|
+
import { derived } from 'svelte/store';
|
|
19
|
+
import { twMerge } from 'tailwind-merge';
|
|
20
|
+
import CustomDropdown from './CustomDropdown.svelte';
|
|
21
|
+
|
|
22
|
+
// Local state
|
|
23
|
+
let showFilterPanel = false;
|
|
24
|
+
let showFieldDropdown = false;
|
|
25
|
+
let newFilterField = '';
|
|
26
|
+
let newFilterOperator: FilterOperator = 'equals';
|
|
27
|
+
let newFilterValue = '';
|
|
28
|
+
|
|
29
|
+
// Get field schema and families from app state
|
|
30
|
+
const fieldSchema = derived(appState, ($state) => $state.fieldSchema?.fields || {});
|
|
31
|
+
|
|
32
|
+
// Get available fields from the store
|
|
33
|
+
$: availableFields = complianceStore.getAvailableFields();
|
|
34
|
+
|
|
35
|
+
// Get the active filters from the store
|
|
36
|
+
$: activeFiltersList = $activeFilters;
|
|
37
|
+
|
|
38
|
+
// Get field type for the selected field
|
|
39
|
+
$: selectedFieldSchema = $fieldSchema[newFilterField] || null;
|
|
40
|
+
$: selectedFieldType = selectedFieldSchema?.type || 'string';
|
|
41
|
+
$: selectedFieldUiType = selectedFieldSchema?.ui_type || 'short_text';
|
|
42
|
+
$: isSelectField = selectedFieldUiType === 'select';
|
|
43
|
+
$: fieldOptions = isSelectField ? selectedFieldSchema?.options || [] : [];
|
|
44
|
+
|
|
45
|
+
// Force equals operator for select fields
|
|
46
|
+
$: if (isSelectField) {
|
|
47
|
+
newFilterOperator = 'equals';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Group fields by tab
|
|
51
|
+
$: fieldsByTab = {
|
|
52
|
+
overview: [] as string[],
|
|
53
|
+
implementation: [] as string[],
|
|
54
|
+
custom: [] as string[]
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
$: {
|
|
58
|
+
// Reset arrays before populating
|
|
59
|
+
fieldsByTab.overview = [];
|
|
60
|
+
fieldsByTab.implementation = [];
|
|
61
|
+
fieldsByTab.custom = [];
|
|
62
|
+
|
|
63
|
+
// Group fields by tab
|
|
64
|
+
availableFields.forEach((field) => {
|
|
65
|
+
const schema = $fieldSchema[field];
|
|
66
|
+
if (schema) {
|
|
67
|
+
const tab = schema.tab || getDefaultTabForCategory(schema.category);
|
|
68
|
+
if (tab === 'overview') fieldsByTab.overview.push(field);
|
|
69
|
+
else if (tab === 'implementation') fieldsByTab.implementation.push(field);
|
|
70
|
+
else fieldsByTab.custom.push(field);
|
|
71
|
+
} else {
|
|
72
|
+
// If no schema, default to custom
|
|
73
|
+
fieldsByTab.custom.push(field);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Determine if value input should be shown based on operator
|
|
79
|
+
$: showValueInput = newFilterOperator !== 'exists' && newFilterOperator !== 'not_exists';
|
|
80
|
+
|
|
81
|
+
// Create a new array from the readonly constant to make it mutable for Svelte
|
|
82
|
+
const operatorOptions = FILTER_OPERATORS.map((op) => ({ value: op.value, label: op.label }));
|
|
83
|
+
|
|
84
|
+
// Add a new filter
|
|
85
|
+
function addFilter() {
|
|
86
|
+
if (!newFilterField) return;
|
|
87
|
+
|
|
88
|
+
let value = newFilterValue;
|
|
89
|
+
|
|
90
|
+
// Convert value based on field type
|
|
91
|
+
let processedValue: FilterValue = value;
|
|
92
|
+
if (selectedFieldType === 'boolean' && typeof value === 'string') {
|
|
93
|
+
processedValue = value.toLowerCase() === 'true';
|
|
94
|
+
} else if (selectedFieldType === 'number' && typeof value === 'string') {
|
|
95
|
+
const parsedValue = parseFloat(value);
|
|
96
|
+
processedValue = isNaN(parsedValue) ? value : parsedValue; // Fallback to string if parsing fails
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add the filter
|
|
100
|
+
complianceStore.addFilter(newFilterField, newFilterOperator, processedValue);
|
|
101
|
+
|
|
102
|
+
// Reset the form
|
|
103
|
+
newFilterField = '';
|
|
104
|
+
newFilterOperator = 'equals';
|
|
105
|
+
newFilterValue = '';
|
|
106
|
+
showFilterPanel = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Toggle filter panel
|
|
110
|
+
function toggleFilterPanel() {
|
|
111
|
+
showFilterPanel = !showFilterPanel;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Helper function to map category to tab (same logic as ControlDetailsPanel)
|
|
115
|
+
function getDefaultTabForCategory(category: string): 'overview' | 'implementation' | 'custom' {
|
|
116
|
+
switch (category) {
|
|
117
|
+
case 'core':
|
|
118
|
+
case 'metadata':
|
|
119
|
+
return 'overview';
|
|
120
|
+
case 'compliance':
|
|
121
|
+
case 'content':
|
|
122
|
+
return 'implementation';
|
|
123
|
+
default:
|
|
124
|
+
return 'custom';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get display name for a field
|
|
129
|
+
function getFieldDisplayName(fieldName: string): string {
|
|
130
|
+
const schema = $fieldSchema[fieldName];
|
|
131
|
+
|
|
132
|
+
// Use schema names if available
|
|
133
|
+
if (schema?.original_name || schema?.display_name) {
|
|
134
|
+
return schema.original_name || schema.display_name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Otherwise use the field name with hyphens replaced by spaces and first letter capitalized
|
|
138
|
+
let displayName = fieldName.replace(/-/g, ' ');
|
|
139
|
+
return displayName.charAt(0).toUpperCase() + displayName.slice(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get display value for a filter
|
|
143
|
+
function getFilterDisplayValue(filter: FilterCondition): string {
|
|
144
|
+
// Special cases for exists/not_exists that don't need to show a value
|
|
145
|
+
if (filter.operator === 'exists') return 'exists';
|
|
146
|
+
if (filter.operator === 'not_exists') return 'does not exist';
|
|
147
|
+
|
|
148
|
+
// Use the shared getOperatorLabel function
|
|
149
|
+
const operatorText = getOperatorLabel(filter.operator).toLowerCase();
|
|
150
|
+
return `${operatorText} "${filter.value}"`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle click outside to close the panel
|
|
154
|
+
function handleClickOutside() {
|
|
155
|
+
showFilterPanel = false;
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<div class="relative" use:clickOutside={handleClickOutside}>
|
|
160
|
+
<!-- Filter Button -->
|
|
161
|
+
<button
|
|
162
|
+
onclick={toggleFilterPanel}
|
|
163
|
+
class="flex items-center px-3 py-2 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
164
|
+
aria-expanded={showFilterPanel}
|
|
165
|
+
aria-controls="filter-panel"
|
|
166
|
+
>
|
|
167
|
+
<Filter class="w-4 h-4 mr-2" />
|
|
168
|
+
<span>Filters</span>
|
|
169
|
+
{#if activeFiltersList.length > 0}
|
|
170
|
+
<span
|
|
171
|
+
class="ml-2 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300 text-xs font-medium px-2 py-0.5 rounded-full"
|
|
172
|
+
>
|
|
173
|
+
{activeFiltersList.length}
|
|
174
|
+
</span>
|
|
175
|
+
{/if}
|
|
176
|
+
<ChevronDown class="w-4 h-4 ml-2" />
|
|
177
|
+
</button>
|
|
178
|
+
|
|
179
|
+
<!-- Filter Panel -->
|
|
180
|
+
{#if showFilterPanel}
|
|
181
|
+
<div
|
|
182
|
+
id="filter-panel"
|
|
183
|
+
class="absolute z-10 mt-2 w-96 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
|
184
|
+
transition:slide={{ duration: 200 }}
|
|
185
|
+
>
|
|
186
|
+
<!-- Active Filters -->
|
|
187
|
+
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
|
188
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Active Filters</h3>
|
|
189
|
+
|
|
190
|
+
{#if $activeFilters.length === 0}
|
|
191
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">No filters</p>
|
|
192
|
+
{:else}
|
|
193
|
+
<div class="space-y-2">
|
|
194
|
+
{#each $activeFilters as filter, index}
|
|
195
|
+
<div
|
|
196
|
+
class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 p-2 rounded-md"
|
|
197
|
+
>
|
|
198
|
+
<div class="flex items-center">
|
|
199
|
+
<span class="text-sm text-gray-700 dark:text-gray-300">
|
|
200
|
+
<span class="font-medium">{getFieldDisplayName(filter.fieldName)}</span>
|
|
201
|
+
<span class="mx-1 text-gray-500 dark:text-gray-400"
|
|
202
|
+
>{getFilterDisplayValue(filter)}</span
|
|
203
|
+
>
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
<button
|
|
207
|
+
onclick={() => complianceStore.removeFilter(index)}
|
|
208
|
+
class="text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400"
|
|
209
|
+
aria-label="Remove filter"
|
|
210
|
+
>
|
|
211
|
+
<TrashCan class="w-4 h-4" />
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
{/each}
|
|
215
|
+
</div>
|
|
216
|
+
{/if}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Add New Filter -->
|
|
220
|
+
<div class="p-4">
|
|
221
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Add Filter</h3>
|
|
222
|
+
|
|
223
|
+
<div class="space-y-3">
|
|
224
|
+
<!-- Field Selection with Custom Dropdown -->
|
|
225
|
+
<div>
|
|
226
|
+
<label for="filter-field" class="block text-xs text-gray-600 dark:text-gray-400 mb-1"
|
|
227
|
+
>Field</label
|
|
228
|
+
>
|
|
229
|
+
|
|
230
|
+
<!-- Custom Field Dropdown -->
|
|
231
|
+
<div class="relative" use:clickOutside={() => (showFieldDropdown = false)}>
|
|
232
|
+
<!-- Dropdown Trigger -->
|
|
233
|
+
<button
|
|
234
|
+
onclick={() => (showFieldDropdown = !showFieldDropdown)}
|
|
235
|
+
class={twMerge(
|
|
236
|
+
'w-full flex items-center justify-between px-3 py-2 text-sm rounded-md border transition-colors',
|
|
237
|
+
showFieldDropdown
|
|
238
|
+
? 'border-blue-500 dark:border-blue-400 ring-2 ring-blue-200 dark:ring-blue-900'
|
|
239
|
+
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500',
|
|
240
|
+
'bg-white dark:bg-gray-700 text-gray-900 dark:text-white'
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
<span class="truncate">
|
|
244
|
+
{newFilterField ? getFieldDisplayName(newFilterField) : 'Select a field'}
|
|
245
|
+
</span>
|
|
246
|
+
{#if showFieldDropdown}
|
|
247
|
+
<ChevronUp class="h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
248
|
+
{:else}
|
|
249
|
+
<ChevronDown class="h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
250
|
+
{/if}
|
|
251
|
+
</button>
|
|
252
|
+
|
|
253
|
+
<!-- Dropdown Menu -->
|
|
254
|
+
{#if showFieldDropdown}
|
|
255
|
+
<div
|
|
256
|
+
class="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg max-h-60 overflow-y-auto"
|
|
257
|
+
transition:fade={{ duration: 100 }}
|
|
258
|
+
>
|
|
259
|
+
<!-- Overview fields -->
|
|
260
|
+
<div
|
|
261
|
+
class="px-3 py-1 text-xs font-semibold text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/80"
|
|
262
|
+
>
|
|
263
|
+
Overview Fields
|
|
264
|
+
</div>
|
|
265
|
+
{#each fieldsByTab.overview as field}
|
|
266
|
+
<button
|
|
267
|
+
class={twMerge(
|
|
268
|
+
'w-full text-left px-3 py-2 text-sm',
|
|
269
|
+
newFilterField === field
|
|
270
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
271
|
+
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
|
272
|
+
)}
|
|
273
|
+
onclick={() => {
|
|
274
|
+
newFilterField = field;
|
|
275
|
+
showFieldDropdown = false;
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
{getFieldDisplayName(field)}
|
|
279
|
+
</button>
|
|
280
|
+
{/each}
|
|
281
|
+
|
|
282
|
+
<!-- Implementation fields -->
|
|
283
|
+
{#if fieldsByTab.implementation.length > 0}
|
|
284
|
+
<!-- Divider -->
|
|
285
|
+
<div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
|
|
286
|
+
<div
|
|
287
|
+
class="px-3 py-1 text-xs font-semibold text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/80"
|
|
288
|
+
>
|
|
289
|
+
Implementation Fields
|
|
290
|
+
</div>
|
|
291
|
+
{#each fieldsByTab.implementation as field}
|
|
292
|
+
<button
|
|
293
|
+
class={twMerge(
|
|
294
|
+
'w-full text-left px-3 py-2 text-sm',
|
|
295
|
+
newFilterField === field
|
|
296
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
297
|
+
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
|
298
|
+
)}
|
|
299
|
+
onclick={() => {
|
|
300
|
+
newFilterField = field;
|
|
301
|
+
showFieldDropdown = false;
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{getFieldDisplayName(field)}
|
|
305
|
+
</button>
|
|
306
|
+
{/each}
|
|
307
|
+
{/if}
|
|
308
|
+
|
|
309
|
+
<!-- Custom fields -->
|
|
310
|
+
{#if fieldsByTab.custom.length > 0}
|
|
311
|
+
<!-- Divider -->
|
|
312
|
+
<div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
|
|
313
|
+
<div
|
|
314
|
+
class="px-3 py-1 text-xs font-semibold text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/80"
|
|
315
|
+
>
|
|
316
|
+
Custom Fields
|
|
317
|
+
</div>
|
|
318
|
+
{#each fieldsByTab.custom as field}
|
|
319
|
+
<button
|
|
320
|
+
class={twMerge(
|
|
321
|
+
'w-full text-left px-3 py-2 text-sm',
|
|
322
|
+
newFilterField === field
|
|
323
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
324
|
+
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
|
325
|
+
)}
|
|
326
|
+
onclick={() => {
|
|
327
|
+
newFilterField = field;
|
|
328
|
+
showFieldDropdown = false;
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
{getFieldDisplayName(field)}
|
|
332
|
+
</button>
|
|
333
|
+
{/each}
|
|
334
|
+
{/if}
|
|
335
|
+
</div>
|
|
336
|
+
{/if}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<!-- Operator Selection -->
|
|
341
|
+
<div>
|
|
342
|
+
<label for="filter-operator" class="block text-xs text-gray-600 dark:text-gray-400 mb-1"
|
|
343
|
+
>Operator</label
|
|
344
|
+
>
|
|
345
|
+
|
|
346
|
+
{#if isSelectField}
|
|
347
|
+
<!-- Disabled dropdown for select fields (always equals) -->
|
|
348
|
+
<div
|
|
349
|
+
class="px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
|
|
350
|
+
>
|
|
351
|
+
Equals
|
|
352
|
+
</div>
|
|
353
|
+
{:else}
|
|
354
|
+
<!-- Custom Operator Dropdown -->
|
|
355
|
+
<CustomDropdown
|
|
356
|
+
bind:value={newFilterOperator}
|
|
357
|
+
options={operatorOptions}
|
|
358
|
+
getDisplayValue={getOperatorLabel}
|
|
359
|
+
labelId="filter-operator"
|
|
360
|
+
/>
|
|
361
|
+
{/if}
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<!-- Value Input (conditional) -->
|
|
365
|
+
{#if showValueInput}
|
|
366
|
+
<div>
|
|
367
|
+
<label for="filter-value" class="block text-xs text-gray-600 dark:text-gray-400 mb-1"
|
|
368
|
+
>Value</label
|
|
369
|
+
>
|
|
370
|
+
{#if isSelectField}
|
|
371
|
+
<!-- Custom dropdown for select fields -->
|
|
372
|
+
<CustomDropdown
|
|
373
|
+
bind:value={newFilterValue}
|
|
374
|
+
options={fieldOptions.map((option: string) => ({ value: option, label: option }))}
|
|
375
|
+
placeholder="Select a value"
|
|
376
|
+
labelId="filter-value"
|
|
377
|
+
/>
|
|
378
|
+
{:else if selectedFieldType === 'boolean'}
|
|
379
|
+
<!-- Boolean field with CustomDropdown -->
|
|
380
|
+
<CustomDropdown
|
|
381
|
+
bind:value={newFilterValue}
|
|
382
|
+
options={[
|
|
383
|
+
{ value: 'true', label: 'Yes' },
|
|
384
|
+
{ value: 'false', label: 'No' }
|
|
385
|
+
]}
|
|
386
|
+
placeholder="Select yes/no"
|
|
387
|
+
labelId="filter-value"
|
|
388
|
+
/>
|
|
389
|
+
{:else}
|
|
390
|
+
<!-- Text input for other fields -->
|
|
391
|
+
<input
|
|
392
|
+
id="filter-value"
|
|
393
|
+
type={selectedFieldType === 'number' ? 'number' : 'text'}
|
|
394
|
+
bind:value={newFilterValue}
|
|
395
|
+
placeholder="Enter value"
|
|
396
|
+
class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
|
|
397
|
+
/>
|
|
398
|
+
{/if}
|
|
399
|
+
</div>
|
|
400
|
+
{/if}
|
|
401
|
+
|
|
402
|
+
<!-- Add Button -->
|
|
403
|
+
<button
|
|
404
|
+
onclick={addFilter}
|
|
405
|
+
disabled={!newFilterField}
|
|
406
|
+
class="flex items-center px-3 py-2 text-sm font-medium rounded-lg bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed"
|
|
407
|
+
>
|
|
408
|
+
<Add class="w-4 h-4 mr-2" />
|
|
409
|
+
<span>Add Filter</span>
|
|
410
|
+
</button>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
{/if}
|
|
415
|
+
</div>
|