@dragonmastery/zinia-forms-core 0.4.8 → 0.5.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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { reactive, watch, provide, ref, computed, unref, inject, getCurrentInstance, nextTick, Teleport } from 'vue';
1
+ import { reactive, watch, provide, ref, computed, unref, nextTick, onMounted, inject, getCurrentInstance, Teleport } from 'vue';
2
2
  import { z } from 'zod';
3
3
  import { jsxs, jsx, Fragment } from 'vue/jsx-runtime';
4
4
 
@@ -6767,6 +6767,166 @@ function createDaisyUIDeleteModal() {
6767
6767
  return DaisyUIDeleteModal;
6768
6768
  }
6769
6769
 
6770
+ // src/filtering/operators.ts
6771
+ var OPERATORS = {
6772
+ // Comparison operators
6773
+ EQUALS: "eq",
6774
+ NOT_EQUALS: "ne",
6775
+ // String operators
6776
+ CONTAINS: "contains",
6777
+ // case-insensitive contains (default for strings)
6778
+ STARTS_WITH: "sw",
6779
+ ENDS_WITH: "ew",
6780
+ // Number/Date comparison operators
6781
+ GREATER_THAN: "gt",
6782
+ GREATER_THAN_OR_EQUAL: "gte",
6783
+ LESS_THAN: "lt",
6784
+ LESS_THAN_OR_EQUAL: "lte",
6785
+ BETWEEN: "between",
6786
+ // Array operators
6787
+ IS_ONE_OF: "in",
6788
+ IS_NOT_ONE_OF: "notIn",
6789
+ // Null check operators
6790
+ IS_EMPTY: "isEmpty",
6791
+ IS_NOT_EMPTY: "isNotEmpty"
6792
+ };
6793
+ var DATA_TYPES = {
6794
+ STRING: "string",
6795
+ NUMBER: "number",
6796
+ DATE: "date",
6797
+ BOOLEAN: "boolean",
6798
+ MONEY: "money",
6799
+ PERCENTAGE: "percentage"
6800
+ };
6801
+ var NUMERIC_TYPES = [
6802
+ DATA_TYPES.NUMBER,
6803
+ DATA_TYPES.MONEY,
6804
+ DATA_TYPES.PERCENTAGE
6805
+ ];
6806
+ var OPERATORS_BY_TYPE = {
6807
+ string: [
6808
+ OPERATORS.EQUALS,
6809
+ OPERATORS.NOT_EQUALS,
6810
+ OPERATORS.CONTAINS,
6811
+ OPERATORS.STARTS_WITH,
6812
+ OPERATORS.ENDS_WITH,
6813
+ OPERATORS.IS_ONE_OF,
6814
+ OPERATORS.IS_NOT_ONE_OF
6815
+ ],
6816
+ number: [
6817
+ OPERATORS.EQUALS,
6818
+ OPERATORS.NOT_EQUALS,
6819
+ OPERATORS.GREATER_THAN,
6820
+ OPERATORS.GREATER_THAN_OR_EQUAL,
6821
+ OPERATORS.LESS_THAN,
6822
+ OPERATORS.LESS_THAN_OR_EQUAL,
6823
+ OPERATORS.BETWEEN
6824
+ ],
6825
+ date: [
6826
+ OPERATORS.EQUALS,
6827
+ OPERATORS.NOT_EQUALS,
6828
+ OPERATORS.GREATER_THAN,
6829
+ OPERATORS.GREATER_THAN_OR_EQUAL,
6830
+ OPERATORS.LESS_THAN,
6831
+ OPERATORS.LESS_THAN_OR_EQUAL,
6832
+ OPERATORS.BETWEEN,
6833
+ OPERATORS.IS_EMPTY,
6834
+ OPERATORS.IS_NOT_EMPTY
6835
+ ],
6836
+ boolean: [OPERATORS.EQUALS, OPERATORS.NOT_EQUALS],
6837
+ enum: [
6838
+ OPERATORS.EQUALS,
6839
+ OPERATORS.NOT_EQUALS,
6840
+ OPERATORS.IS_ONE_OF,
6841
+ OPERATORS.IS_NOT_ONE_OF
6842
+ ],
6843
+ money: [
6844
+ OPERATORS.EQUALS,
6845
+ OPERATORS.NOT_EQUALS,
6846
+ OPERATORS.GREATER_THAN,
6847
+ OPERATORS.GREATER_THAN_OR_EQUAL,
6848
+ OPERATORS.LESS_THAN,
6849
+ OPERATORS.LESS_THAN_OR_EQUAL,
6850
+ OPERATORS.BETWEEN
6851
+ ],
6852
+ percentage: [
6853
+ OPERATORS.EQUALS,
6854
+ OPERATORS.NOT_EQUALS,
6855
+ OPERATORS.GREATER_THAN,
6856
+ OPERATORS.GREATER_THAN_OR_EQUAL,
6857
+ OPERATORS.LESS_THAN,
6858
+ OPERATORS.LESS_THAN_OR_EQUAL,
6859
+ OPERATORS.BETWEEN
6860
+ ]
6861
+ };
6862
+ var SINGLE_VALUE_OPERATORS = [
6863
+ OPERATORS.EQUALS,
6864
+ OPERATORS.NOT_EQUALS,
6865
+ OPERATORS.CONTAINS,
6866
+ OPERATORS.STARTS_WITH,
6867
+ OPERATORS.ENDS_WITH,
6868
+ OPERATORS.GREATER_THAN,
6869
+ OPERATORS.GREATER_THAN_OR_EQUAL,
6870
+ OPERATORS.LESS_THAN,
6871
+ OPERATORS.LESS_THAN_OR_EQUAL
6872
+ ];
6873
+ var ARRAY_VALUE_OPERATORS = [
6874
+ OPERATORS.IS_ONE_OF,
6875
+ OPERATORS.IS_NOT_ONE_OF,
6876
+ OPERATORS.BETWEEN
6877
+ ];
6878
+ var NULL_CHECK_OPERATORS = [OPERATORS.IS_EMPTY, OPERATORS.IS_NOT_EMPTY];
6879
+ function isValidOperatorForType(operator, type) {
6880
+ const validOperators = OPERATORS_BY_TYPE[type];
6881
+ return validOperators.includes(operator);
6882
+ }
6883
+ function requiresSingleValue(operator) {
6884
+ return SINGLE_VALUE_OPERATORS.includes(operator);
6885
+ }
6886
+ function requiresArrayValues(operator) {
6887
+ return ARRAY_VALUE_OPERATORS.includes(operator);
6888
+ }
6889
+ function isNullCheckOperator(operator) {
6890
+ return NULL_CHECK_OPERATORS.includes(operator);
6891
+ }
6892
+
6893
+ // src/filtering/operator-map.ts
6894
+ var SHORTHAND_TO_OPERATOR = {
6895
+ eq: OPERATORS.EQUALS,
6896
+ ne: OPERATORS.NOT_EQUALS,
6897
+ contains: OPERATORS.CONTAINS,
6898
+ sw: OPERATORS.STARTS_WITH,
6899
+ ew: OPERATORS.ENDS_WITH,
6900
+ gt: OPERATORS.GREATER_THAN,
6901
+ gte: OPERATORS.GREATER_THAN_OR_EQUAL,
6902
+ lt: OPERATORS.LESS_THAN,
6903
+ lte: OPERATORS.LESS_THAN_OR_EQUAL,
6904
+ between: OPERATORS.BETWEEN,
6905
+ in: OPERATORS.IS_ONE_OF,
6906
+ notIn: OPERATORS.IS_NOT_ONE_OF,
6907
+ isEmpty: OPERATORS.IS_EMPTY,
6908
+ isNotEmpty: OPERATORS.IS_NOT_EMPTY,
6909
+ // Legacy aliases for backward compatibility
6910
+ isNull: OPERATORS.IS_EMPTY,
6911
+ isNotNull: OPERATORS.IS_NOT_EMPTY
6912
+ };
6913
+ var DEFAULT_OPERATORS = {
6914
+ string: OPERATORS.CONTAINS,
6915
+ // 'contains' - always case-insensitive
6916
+ number: OPERATORS.EQUALS,
6917
+ // 'eq'
6918
+ date: OPERATORS.EQUALS,
6919
+ // 'eq'
6920
+ boolean: OPERATORS.EQUALS,
6921
+ // 'eq'
6922
+ enum: OPERATORS.IS_ONE_OF,
6923
+ // 'in' - Multi-select is the primary control for enums
6924
+ money: OPERATORS.EQUALS,
6925
+ // 'eq'
6926
+ percentage: OPERATORS.EQUALS
6927
+ // 'eq'
6928
+ };
6929
+
6770
6930
  // src/components/createDataTableComponents.ts
6771
6931
  function createDataTableComponents(schema, renderStyle) {
6772
6932
  const styleToUse = renderStyle || getDefaultStyle();
@@ -6779,6 +6939,570 @@ function createDataTableComponents(schema, renderStyle) {
6779
6939
  ZiniaDataTable
6780
6940
  };
6781
6941
  }
6942
+
6943
+ // src/filtering/validation.ts
6944
+ function validateFieldRegistry(registry) {
6945
+ const errors = [];
6946
+ if (!registry || typeof registry !== "object") {
6947
+ return {
6948
+ valid: false,
6949
+ errors: [{ message: "Field registry must be an object", code: "INVALID_REGISTRY" }]
6950
+ };
6951
+ }
6952
+ const fieldNames = Object.keys(registry);
6953
+ if (fieldNames.length === 0) {
6954
+ return {
6955
+ valid: false,
6956
+ errors: [{ message: "Field registry must contain at least one field", code: "EMPTY_REGISTRY" }]
6957
+ };
6958
+ }
6959
+ const validTypes = ["string", "number", "date", "boolean", "enum", "money", "percentage"];
6960
+ const seenFields = /* @__PURE__ */ new Set();
6961
+ for (const [fieldName, metadata] of Object.entries(registry)) {
6962
+ if (!fieldName || typeof fieldName !== "string") {
6963
+ errors.push({
6964
+ field: fieldName,
6965
+ message: "Field name must be a non-empty string",
6966
+ code: "INVALID_FIELD_NAME"
6967
+ });
6968
+ continue;
6969
+ }
6970
+ if (seenFields.has(fieldName)) {
6971
+ errors.push({
6972
+ field: fieldName,
6973
+ message: `Duplicate field name: ${fieldName}`,
6974
+ code: "DUPLICATE_FIELD"
6975
+ });
6976
+ continue;
6977
+ }
6978
+ seenFields.add(fieldName);
6979
+ if (!metadata || typeof metadata !== "object") {
6980
+ errors.push({
6981
+ field: fieldName,
6982
+ message: "Field metadata must be an object",
6983
+ code: "INVALID_METADATA"
6984
+ });
6985
+ continue;
6986
+ }
6987
+ if (!metadata.type || !validTypes.includes(metadata.type)) {
6988
+ errors.push({
6989
+ field: fieldName,
6990
+ message: `Invalid type: ${metadata.type}. Must be one of: ${validTypes.join(", ")}`,
6991
+ code: "INVALID_TYPE"
6992
+ });
6993
+ }
6994
+ if (metadata.filterable !== void 0 && typeof metadata.filterable !== "boolean") {
6995
+ errors.push({
6996
+ field: fieldName,
6997
+ message: "filterable must be a boolean",
6998
+ code: "INVALID_FILTERABLE"
6999
+ });
7000
+ }
7001
+ if (metadata.searchable !== void 0 && typeof metadata.searchable !== "boolean") {
7002
+ errors.push({
7003
+ field: fieldName,
7004
+ message: "searchable must be a boolean",
7005
+ code: "INVALID_SEARCHABLE"
7006
+ });
7007
+ }
7008
+ if (metadata.sortable !== void 0 && typeof metadata.sortable !== "boolean") {
7009
+ errors.push({
7010
+ field: fieldName,
7011
+ message: "sortable must be a boolean",
7012
+ code: "INVALID_SORTABLE"
7013
+ });
7014
+ }
7015
+ }
7016
+ return {
7017
+ valid: errors.length === 0,
7018
+ errors
7019
+ };
7020
+ }
7021
+ function validateFilterValueObject(fieldName, filter, registry) {
7022
+ const errors = [];
7023
+ if (!(fieldName in registry)) {
7024
+ return {
7025
+ valid: false,
7026
+ errors: [
7027
+ {
7028
+ field: fieldName,
7029
+ message: `Field "${fieldName}" does not exist in registry`,
7030
+ code: "FIELD_NOT_IN_REGISTRY"
7031
+ }
7032
+ ]
7033
+ };
7034
+ }
7035
+ const fieldMetadata = registry[fieldName];
7036
+ const isFilterable = fieldMetadata.filterable !== false;
7037
+ if (!isFilterable) {
7038
+ return {
7039
+ valid: false,
7040
+ errors: [
7041
+ {
7042
+ field: fieldName,
7043
+ message: `Field "${fieldName}" is not filterable`,
7044
+ code: "FIELD_NOT_FILTERABLE"
7045
+ }
7046
+ ]
7047
+ };
7048
+ }
7049
+ const fieldType = fieldMetadata.type;
7050
+ if (!filter.operator || typeof filter.operator !== "string") {
7051
+ errors.push({
7052
+ field: fieldName,
7053
+ message: "Operator is required and must be a string",
7054
+ code: "MISSING_OPERATOR"
7055
+ });
7056
+ } else if (!isValidOperatorForType(filter.operator, fieldType)) {
7057
+ errors.push({
7058
+ field: fieldName,
7059
+ message: `Operator "${filter.operator}" is not valid for type "${fieldType}"`,
7060
+ code: "INVALID_OPERATOR_FOR_TYPE"
7061
+ });
7062
+ }
7063
+ if (filter.operator) {
7064
+ if (isNullCheckOperator(filter.operator)) {
7065
+ if (filter.value !== void 0) {
7066
+ errors.push({
7067
+ field: fieldName,
7068
+ message: `Operator "${filter.operator}" must not have a value`,
7069
+ code: "NULL_CHECK_WITH_VALUE"
7070
+ });
7071
+ }
7072
+ if (filter.values !== void 0) {
7073
+ errors.push({
7074
+ field: fieldName,
7075
+ message: `Operator "${filter.operator}" must not have values`,
7076
+ code: "NULL_CHECK_WITH_VALUES"
7077
+ });
7078
+ }
7079
+ } else if (requiresSingleValue(filter.operator)) {
7080
+ if (filter.value === void 0) {
7081
+ errors.push({
7082
+ field: fieldName,
7083
+ message: `Operator "${filter.operator}" requires a value`,
7084
+ code: "MISSING_VALUE"
7085
+ });
7086
+ }
7087
+ if (filter.values !== void 0) {
7088
+ errors.push({
7089
+ field: fieldName,
7090
+ message: `Operator "${filter.operator}" must use "value", not "values"`,
7091
+ code: "SINGLE_VALUE_WITH_ARRAY"
7092
+ });
7093
+ }
7094
+ } else if (requiresArrayValues(filter.operator)) {
7095
+ if (!filter.values || !Array.isArray(filter.values)) {
7096
+ errors.push({
7097
+ field: fieldName,
7098
+ message: `Operator "${filter.operator}" requires a values array`,
7099
+ code: "MISSING_VALUES"
7100
+ });
7101
+ } else if (filter.values.length === 0) {
7102
+ errors.push({
7103
+ field: fieldName,
7104
+ message: `Operator "${filter.operator}" requires a non-empty values array`,
7105
+ code: "EMPTY_VALUES_ARRAY"
7106
+ });
7107
+ } else if (filter.operator === OPERATORS.BETWEEN && filter.values.length !== 2) {
7108
+ errors.push({
7109
+ field: fieldName,
7110
+ message: `Operator "${OPERATORS.BETWEEN}" requires exactly 2 values, got ${filter.values.length}`,
7111
+ code: "INVALID_BETWEEN_COUNT"
7112
+ });
7113
+ } else if (filter.operator === OPERATORS.BETWEEN) {
7114
+ const [min, max] = filter.values;
7115
+ if (NUMERIC_TYPES.includes(fieldType)) {
7116
+ if (typeof min !== "number" || typeof max !== "number") {
7117
+ errors.push({
7118
+ field: fieldName,
7119
+ message: "between operator values must be numbers",
7120
+ code: "INVALID_BETWEEN_TYPE"
7121
+ });
7122
+ } else if (min > max) {
7123
+ errors.push({
7124
+ field: fieldName,
7125
+ message: `between operator: min (${min}) must be <= max (${max})`,
7126
+ code: "INVALID_BETWEEN_RANGE"
7127
+ });
7128
+ }
7129
+ } else if (fieldType === DATA_TYPES.DATE) {
7130
+ if (typeof min !== "string" || typeof max !== "string") {
7131
+ errors.push({
7132
+ field: fieldName,
7133
+ message: "between operator values must be ISO 8601 date strings",
7134
+ code: "INVALID_BETWEEN_TYPE"
7135
+ });
7136
+ } else {
7137
+ try {
7138
+ const minDate = new Date(min);
7139
+ const maxDate = new Date(max);
7140
+ if (isNaN(minDate.getTime()) || isNaN(maxDate.getTime())) {
7141
+ errors.push({
7142
+ field: fieldName,
7143
+ message: "between operator values must be valid ISO 8601 dates",
7144
+ code: "INVALID_DATE_FORMAT"
7145
+ });
7146
+ } else if (minDate > maxDate) {
7147
+ errors.push({
7148
+ field: fieldName,
7149
+ message: `between operator: start date must be <= end date`,
7150
+ code: "INVALID_BETWEEN_RANGE"
7151
+ });
7152
+ }
7153
+ } catch (e) {
7154
+ errors.push({
7155
+ field: fieldName,
7156
+ message: "between operator values must be valid ISO 8601 dates",
7157
+ code: "INVALID_DATE_FORMAT"
7158
+ });
7159
+ }
7160
+ }
7161
+ }
7162
+ }
7163
+ if (filter.value !== void 0) {
7164
+ errors.push({
7165
+ field: fieldName,
7166
+ message: `Operator "${filter.operator}" must use "values", not "value"`,
7167
+ code: "ARRAY_VALUE_WITH_SINGLE"
7168
+ });
7169
+ }
7170
+ }
7171
+ }
7172
+ if (filter.caseSensitive !== void 0 && fieldType !== "string" && fieldType !== "enum") {
7173
+ errors.push({
7174
+ field: fieldName,
7175
+ message: "caseSensitive is only applicable to string and enum types",
7176
+ code: "INVALID_CASE_SENSITIVE"
7177
+ });
7178
+ }
7179
+ return {
7180
+ valid: errors.length === 0,
7181
+ errors
7182
+ };
7183
+ }
7184
+ function validateFilterConfiguration(config, registry) {
7185
+ const errors = [];
7186
+ const registryValidation = validateFieldRegistry(registry);
7187
+ if (!registryValidation.valid) {
7188
+ return registryValidation;
7189
+ }
7190
+ if (!config || typeof config !== "object") {
7191
+ return {
7192
+ valid: false,
7193
+ errors: [{ message: "Filter configuration must be an object", code: "INVALID_CONFIG" }]
7194
+ };
7195
+ }
7196
+ for (const [fieldName, filter] of Object.entries(config)) {
7197
+ const filterValidation = validateFilterValueObject(fieldName, filter, registry);
7198
+ if (!filterValidation.valid) {
7199
+ errors.push(...filterValidation.errors);
7200
+ }
7201
+ }
7202
+ return {
7203
+ valid: errors.length === 0,
7204
+ errors
7205
+ };
7206
+ }
7207
+ function mapTypeToFilterDataType(type, inputType) {
7208
+ switch (type) {
7209
+ case "string":
7210
+ if (inputType === "select" || inputType === "combobox" || inputType === "radio") {
7211
+ return "enum";
7212
+ }
7213
+ return "string";
7214
+ case "number":
7215
+ if (inputType === "currency") {
7216
+ return "money";
7217
+ }
7218
+ return "number";
7219
+ case "boolean":
7220
+ return "boolean";
7221
+ case "date":
7222
+ return "date";
7223
+ case "enum":
7224
+ return "enum";
7225
+ default:
7226
+ return "string";
7227
+ }
7228
+ }
7229
+ function buildFieldRegistryFromSchema(schema, fieldsMetadata) {
7230
+ const registry = {};
7231
+ const shape = schema.shape;
7232
+ const fieldNames = Object.keys(shape);
7233
+ for (const fieldName of fieldNames) {
7234
+ const fieldMetadata = fieldsMetadata[fieldName];
7235
+ if (!fieldMetadata) {
7236
+ const fieldSchema = shape[fieldName];
7237
+ const inferredType = inferTypeFromZodSchema(fieldSchema);
7238
+ registry[fieldName] = {
7239
+ type: inferredType,
7240
+ filterable: true,
7241
+ searchable: false,
7242
+ sortable: true
7243
+ };
7244
+ continue;
7245
+ }
7246
+ const filterType = mapTypeToFilterDataType(fieldMetadata.type, fieldMetadata.inputType);
7247
+ const filterable = "filterable" in fieldMetadata ? fieldMetadata.filterable !== false : true;
7248
+ const searchable = "searchable" in fieldMetadata ? fieldMetadata.searchable === true : false;
7249
+ const sortable = "sortable" in fieldMetadata ? fieldMetadata.sortable !== false : true;
7250
+ registry[fieldName] = {
7251
+ type: filterType,
7252
+ filterable,
7253
+ searchable,
7254
+ sortable
7255
+ };
7256
+ }
7257
+ return registry;
7258
+ }
7259
+ function inferTypeFromZodSchema(schema) {
7260
+ let unwrapped = schema;
7261
+ while (unwrapped instanceof z.ZodOptional || unwrapped instanceof z.ZodNullable || unwrapped instanceof z.ZodDefault) {
7262
+ if (unwrapped instanceof z.ZodOptional) {
7263
+ unwrapped = unwrapped._def.innerType;
7264
+ } else if (unwrapped instanceof z.ZodNullable) {
7265
+ unwrapped = unwrapped._def.innerType;
7266
+ } else if (unwrapped instanceof z.ZodDefault) {
7267
+ unwrapped = unwrapped._def.innerType;
7268
+ }
7269
+ }
7270
+ if (unwrapped instanceof z.ZodString) {
7271
+ return "string";
7272
+ } else if (unwrapped instanceof z.ZodNumber) {
7273
+ return "number";
7274
+ } else if (unwrapped instanceof z.ZodBoolean) {
7275
+ return "boolean";
7276
+ } else if (unwrapped instanceof z.ZodDate) {
7277
+ return "date";
7278
+ } else if (unwrapped instanceof z.ZodEnum) {
7279
+ return "enum";
7280
+ } else {
7281
+ return "string";
7282
+ }
7283
+ }
7284
+
7285
+ // src/filtering/serialization.ts
7286
+ function serializeFiltersToQueryString(config) {
7287
+ if (Object.keys(config).length === 0) {
7288
+ return "";
7289
+ }
7290
+ try {
7291
+ const params = new URLSearchParams();
7292
+ for (const [fieldName, filter] of Object.entries(config)) {
7293
+ const prefix = `filters[${fieldName}]`;
7294
+ params.append(`${prefix}[operator]`, filter.operator);
7295
+ if (filter.value !== void 0) {
7296
+ params.append(`${prefix}[value]`, String(filter.value));
7297
+ }
7298
+ if (filter.values !== void 0 && Array.isArray(filter.values)) {
7299
+ params.append(`${prefix}[values]`, JSON.stringify(filter.values));
7300
+ }
7301
+ if (filter.caseSensitive !== void 0) {
7302
+ params.append(`${prefix}[caseSensitive]`, String(filter.caseSensitive));
7303
+ }
7304
+ }
7305
+ return params.toString();
7306
+ } catch (error) {
7307
+ console.error("Failed to serialize filters to query string:", error);
7308
+ return "";
7309
+ }
7310
+ }
7311
+ function deserializeFiltersFromQueryString(queryString, registry) {
7312
+ if (!queryString || queryString.trim() === "") {
7313
+ return {};
7314
+ }
7315
+ try {
7316
+ try {
7317
+ const params = new URLSearchParams(queryString);
7318
+ const config = {};
7319
+ const fieldMap = /* @__PURE__ */ new Map();
7320
+ for (const [key, value] of params.entries()) {
7321
+ const match = key.match(/^filters\[([^\]]+)\]\[([^\]]+)\]$/);
7322
+ if (!match) {
7323
+ throw new Error("Not URLSearchParams format, trying base64");
7324
+ }
7325
+ const [, fieldName, property] = match;
7326
+ if (!fieldMap.has(fieldName)) {
7327
+ fieldMap.set(fieldName, {});
7328
+ }
7329
+ const filter = fieldMap.get(fieldName);
7330
+ switch (property) {
7331
+ case "operator":
7332
+ filter.operator = value;
7333
+ break;
7334
+ case "value":
7335
+ filter.value = value;
7336
+ break;
7337
+ case "values":
7338
+ try {
7339
+ filter.values = JSON.parse(value);
7340
+ } catch {
7341
+ }
7342
+ break;
7343
+ case "caseSensitive":
7344
+ filter.caseSensitive = value === "true";
7345
+ break;
7346
+ }
7347
+ }
7348
+ for (const [fieldName, filter] of fieldMap.entries()) {
7349
+ if (filter.operator) {
7350
+ config[fieldName] = filter;
7351
+ }
7352
+ }
7353
+ const validation = validateFilterConfiguration(config, registry);
7354
+ if (!validation.valid) {
7355
+ console.warn("Deserialized filters failed validation:", validation.errors);
7356
+ return {};
7357
+ }
7358
+ return config;
7359
+ } catch {
7360
+ let jsonString;
7361
+ if (typeof atob !== "undefined") {
7362
+ jsonString = atob(queryString);
7363
+ } else if (typeof Buffer !== "undefined") {
7364
+ jsonString = Buffer.from(queryString, "base64").toString("utf-8");
7365
+ } else {
7366
+ jsonString = decodeURIComponent(queryString);
7367
+ }
7368
+ const config = JSON.parse(jsonString);
7369
+ const validation = validateFilterConfiguration(config, registry);
7370
+ if (!validation.valid) {
7371
+ console.warn("Deserialized filters failed validation:", validation.errors);
7372
+ return {};
7373
+ }
7374
+ return config;
7375
+ }
7376
+ } catch (error) {
7377
+ console.error("Failed to deserialize filters from query string:", error);
7378
+ return {};
7379
+ }
7380
+ }
7381
+ function serializeFiltersToQueryParams(config, registry) {
7382
+ const params = {};
7383
+ for (const [fieldName, filter] of Object.entries(config)) {
7384
+ const fieldMeta = registry[fieldName];
7385
+ if (!fieldMeta) continue;
7386
+ const operator = filter.operator;
7387
+ if (!SHORTHAND_TO_OPERATOR[operator]) {
7388
+ console.warn(`Invalid operator: ${operator}`);
7389
+ continue;
7390
+ }
7391
+ if (isNullCheckOperator(operator)) {
7392
+ params[`flt[${fieldName}][${operator}]`] = "";
7393
+ } else if (requiresArrayValues(operator)) {
7394
+ if (filter.values && Array.isArray(filter.values)) {
7395
+ params[`flt[${fieldName}][${operator}]`] = JSON.stringify(filter.values);
7396
+ }
7397
+ } else {
7398
+ params[`flt[${fieldName}][${operator}]`] = String(filter.value);
7399
+ }
7400
+ }
7401
+ return params;
7402
+ }
7403
+ function deserializeFiltersFromQueryParams(params, registry) {
7404
+ const config = {};
7405
+ const fieldMap = /* @__PURE__ */ new Map();
7406
+ for (const [key, value] of Object.entries(params)) {
7407
+ const matchDefault = key.match(/^flt\[([^\]]+)\]$/);
7408
+ const matchWithOp = key.match(/^flt\[([^\]]+)\]\[([^\]]+)\]$/);
7409
+ if (!matchDefault && !matchWithOp) continue;
7410
+ const fieldName = matchDefault ? matchDefault[1] : matchWithOp[1];
7411
+ const operatorShorthand = matchWithOp ? matchWithOp[2] : null;
7412
+ const stringValue = Array.isArray(value) ? value[0] : value;
7413
+ const fieldMeta = registry[fieldName];
7414
+ if (!fieldMeta) continue;
7415
+ const fieldType = fieldMeta.type;
7416
+ const defaultOp = DEFAULT_OPERATORS[fieldType] || "eq";
7417
+ let operator;
7418
+ if (operatorShorthand) {
7419
+ if (!SHORTHAND_TO_OPERATOR[operatorShorthand]) {
7420
+ continue;
7421
+ }
7422
+ operator = operatorShorthand;
7423
+ } else {
7424
+ operator = defaultOp;
7425
+ }
7426
+ if (!fieldMap.has(fieldName)) {
7427
+ fieldMap.set(fieldName, { operator });
7428
+ }
7429
+ const filter = fieldMap.get(fieldName);
7430
+ filter.operator = operator;
7431
+ if (isNullCheckOperator(operator)) ; else if (requiresArrayValues(operator)) {
7432
+ if (!stringValue || stringValue.trim() === "") {
7433
+ fieldMap.delete(fieldName);
7434
+ continue;
7435
+ }
7436
+ try {
7437
+ const parsed = JSON.parse(stringValue);
7438
+ if (Array.isArray(parsed) && parsed.length > 0) {
7439
+ if (NUMERIC_TYPES.includes(fieldType)) {
7440
+ filter.values = parsed.map((v) => Number(v));
7441
+ } else {
7442
+ filter.values = parsed;
7443
+ }
7444
+ } else {
7445
+ fieldMap.delete(fieldName);
7446
+ continue;
7447
+ }
7448
+ } catch (e) {
7449
+ const values = stringValue.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
7450
+ if (values.length === 0) {
7451
+ fieldMap.delete(fieldName);
7452
+ continue;
7453
+ }
7454
+ if (operator === OPERATORS.BETWEEN && values.length === 2) {
7455
+ if (NUMERIC_TYPES.includes(fieldType)) {
7456
+ filter.values = [Number(values[0]), Number(values[1])];
7457
+ } else if (fieldType === DATA_TYPES.DATE) {
7458
+ filter.values = [values[0], values[1]];
7459
+ } else {
7460
+ filter.values = values;
7461
+ }
7462
+ } else if (operator === OPERATORS.BETWEEN) {
7463
+ fieldMap.delete(fieldName);
7464
+ continue;
7465
+ } else {
7466
+ if (NUMERIC_TYPES.includes(fieldType)) {
7467
+ filter.values = values.map((v) => Number(v));
7468
+ } else {
7469
+ filter.values = values;
7470
+ }
7471
+ }
7472
+ }
7473
+ } else {
7474
+ if ((operator === OPERATORS.IS_ONE_OF || operator === OPERATORS.IS_NOT_ONE_OF) && stringValue) {
7475
+ if (NUMERIC_TYPES.includes(fieldType)) {
7476
+ filter.values = [Number(stringValue)];
7477
+ } else {
7478
+ filter.values = [stringValue];
7479
+ }
7480
+ } else {
7481
+ if (NUMERIC_TYPES.includes(fieldType)) {
7482
+ filter.value = Number(stringValue);
7483
+ } else if (fieldType === DATA_TYPES.BOOLEAN) {
7484
+ filter.value = stringValue === "true" || stringValue === "1";
7485
+ } else {
7486
+ filter.value = stringValue;
7487
+ }
7488
+ if (fieldType === DATA_TYPES.STRING && operator === OPERATORS.CONTAINS) {
7489
+ filter.caseSensitive = false;
7490
+ }
7491
+ }
7492
+ }
7493
+ }
7494
+ for (const [fieldName, filter] of fieldMap.entries()) {
7495
+ if (filter.operator) {
7496
+ config[fieldName] = filter;
7497
+ }
7498
+ }
7499
+ const validation = validateFilterConfiguration(config, registry);
7500
+ if (!validation.valid) {
7501
+ console.warn("Deserialized filters failed validation:", validation.errors);
7502
+ return {};
7503
+ }
7504
+ return config;
7505
+ }
6782
7506
  function useDataTableState(initialData = [], options = {}) {
6783
7507
  const data = ref(initialData);
6784
7508
  const isLoading = ref(false);
@@ -6796,6 +7520,7 @@ function useDataTableState(initialData = [], options = {}) {
6796
7520
  field: null,
6797
7521
  direction: "asc"
6798
7522
  });
7523
+ const fieldRegistry = ref(null);
6799
7524
  const filters = ref({});
6800
7525
  const search = reactive({
6801
7526
  query: "",
@@ -6857,6 +7582,9 @@ function useDataTableState(initialData = [], options = {}) {
6857
7582
  const setFilters = (newFilters) => {
6858
7583
  filters.value = newFilters;
6859
7584
  };
7585
+ const setFieldRegistry = (registry) => {
7586
+ fieldRegistry.value = registry;
7587
+ };
6860
7588
  const setSearch = (query, searchableFields) => {
6861
7589
  search.query = query;
6862
7590
  if (searchableFields) {
@@ -6914,6 +7642,7 @@ function useDataTableState(initialData = [], options = {}) {
6914
7642
  pagination,
6915
7643
  sorting,
6916
7644
  filters,
7645
+ fieldRegistry,
6917
7646
  search,
6918
7647
  selection,
6919
7648
  // UI state
@@ -6937,6 +7666,7 @@ function useDataTableState(initialData = [], options = {}) {
6937
7666
  setPagination,
6938
7667
  setSorting,
6939
7668
  setFilters,
7669
+ setFieldRegistry,
6940
7670
  setSearch,
6941
7671
  // Selection methods
6942
7672
  selectRow,
@@ -6954,6 +7684,7 @@ var ZINIA_DATA_TABLE_ACTIONS_KEY = "ziniaDataTableActions";
6954
7684
  var ZINIA_DATA_TABLE_SEARCH_INPUT_KEY = "ziniaDataTableSearchInput";
6955
7685
  var ZINIA_DATA_TABLE_FILTER_INPUTS_KEY = "ziniaDataTableFilterInputs";
6956
7686
  var ZINIA_DATA_TABLE_FILTER_OPERATORS_KEY = "ziniaDataTableFilterOperators";
7687
+ var ZINIA_DATA_TABLE_FILTER_CASE_SENSITIVE_KEY = "ziniaDataTableFilterCaseSensitive";
6957
7688
  var ZINIA_DATA_TABLE_FILTER_OPTIONS_STATE_KEY = "ziniaDataTableFilterOptionsState";
6958
7689
  var ZINIA_DATA_TABLE_FILTER_OPTIONS_LOADING_KEY = "ziniaDataTableFilterOptionsLoading";
6959
7690
  var ZINIA_DATA_TABLE_NAME_KEY = "ziniaDataTableName";
@@ -6962,12 +7693,15 @@ function useDataTable(schema, options) {
6962
7693
  schemaId: options.schemaId,
6963
7694
  storeName: "dataTable"
6964
7695
  });
7696
+ const fieldRegistry = buildFieldRegistryFromSchema(schema, fieldsMetadata);
6965
7697
  const tableState = useDataTableState([], {
6966
7698
  debug: options.debug
6967
7699
  });
7700
+ tableState.setFieldRegistry(fieldRegistry);
6968
7701
  const searchInputValue = ref("");
6969
7702
  const filterInputValues = ref({});
6970
7703
  const filterOperators = ref({});
7704
+ const filterCaseSensitive = ref({});
6971
7705
  const filterOptionsState = ref({});
6972
7706
  const filterOptionsLoading = ref({});
6973
7707
  if (options.pagination?.pageSize) {
@@ -6984,15 +7718,20 @@ function useDataTable(schema, options) {
6984
7718
  Object.entries(options.initialFilters).forEach(([field, filter]) => {
6985
7719
  if (filter && filter.value !== "" && filter.value !== null && filter.value !== void 0) {
6986
7720
  filterInputValues.value[field] = String(filter.value);
6987
- filterOperators.value[field] = filter.operator || "eq";
7721
+ const fieldMetadata = fieldRegistry[field];
7722
+ const defaultOperator = fieldMetadata ? DEFAULT_OPERATORS[fieldMetadata.type] || OPERATORS.EQUALS : OPERATORS.EQUALS;
7723
+ filterOperators.value[field] = filter.operator || defaultOperator;
6988
7724
  }
6989
7725
  });
6990
7726
  }
6991
7727
  const extractActiveFilters = (filterState) => {
6992
7728
  const activeFilters = {};
6993
- Object.entries(filterState).forEach(([key, filter]) => {
6994
- if (filter && filter.value !== "" && filter.value !== null && filter.value !== void 0) {
6995
- activeFilters[key] = filter;
7729
+ Object.entries(filterState).forEach(([fieldName, filter]) => {
7730
+ const hasValue = filter.value !== void 0 && filter.value !== "" && filter.value !== null;
7731
+ const hasValues = filter.values !== void 0 && Array.isArray(filter.values) && filter.values.length > 0;
7732
+ const isNullCheck = filter.operator === OPERATORS.IS_EMPTY || filter.operator === OPERATORS.IS_NOT_EMPTY;
7733
+ if (hasValue || hasValues || isNullCheck) {
7734
+ activeFilters[fieldName] = filter;
6996
7735
  }
6997
7736
  });
6998
7737
  return activeFilters;
@@ -7022,6 +7761,16 @@ function useDataTable(schema, options) {
7022
7761
  try {
7023
7762
  tableState.setLoading(true);
7024
7763
  tableState.setError(null);
7764
+ const activeFilters = extractActiveFilters(tableState.state.filters.value);
7765
+ const validation = validateFilterConfiguration(activeFilters, fieldRegistry);
7766
+ if (!validation.valid) {
7767
+ const errorMessage = `Filter validation failed: ${validation.errors.map((e) => e.message).join(", ")}`;
7768
+ tableState.setError(errorMessage);
7769
+ if (options.debug) {
7770
+ console.error("\u274C Filter validation errors:", validation.errors);
7771
+ }
7772
+ throw new Error(errorMessage);
7773
+ }
7025
7774
  if (options.debug) {
7026
7775
  console.log("\u{1F504} Fetching table data with params:", {
7027
7776
  page: tableState.state.pagination.currentPage,
@@ -7030,7 +7779,7 @@ function useDataTable(schema, options) {
7030
7779
  field: tableState.state.sorting.field,
7031
7780
  direction: tableState.state.sorting.direction
7032
7781
  } : null,
7033
- filters: extractActiveFilters(tableState.state.filters.value),
7782
+ filters: activeFilters,
7034
7783
  search: {
7035
7784
  query: tableState.state.search.query,
7036
7785
  searchableFields: tableState.state.search.searchableFields
@@ -7044,7 +7793,7 @@ function useDataTable(schema, options) {
7044
7793
  field: tableState.state.sorting.field,
7045
7794
  direction: tableState.state.sorting.direction
7046
7795
  } : null,
7047
- filters: extractActiveFilters(tableState.state.filters.value),
7796
+ filters: activeFilters,
7048
7797
  search: {
7049
7798
  query: tableState.state.search.query,
7050
7799
  searchableFields: tableState.state.search.searchableFields
@@ -7079,13 +7828,30 @@ function useDataTable(schema, options) {
7079
7828
  tableState.setPagination(0);
7080
7829
  await fetchData();
7081
7830
  };
7082
- const setFilter = async (field, value, operator = "eq") => {
7831
+ const setFilter = async (field, filter) => {
7083
7832
  const currentFilters = { ...tableState.state.filters.value };
7084
- if (value === "" || value === null || value === void 0) {
7085
- delete currentFilters[field];
7086
- } else {
7087
- currentFilters[field] = { value, operator };
7833
+ const fieldName = field;
7834
+ const hasValue = filter.value !== void 0 && filter.value !== "" && filter.value !== null;
7835
+ const hasValues = filter.values !== void 0 && Array.isArray(filter.values) && filter.values.length > 0;
7836
+ const isNullCheck = filter.operator === OPERATORS.IS_EMPTY || filter.operator === OPERATORS.IS_NOT_EMPTY;
7837
+ const isEmpty = !hasValue && !hasValues && !isNullCheck;
7838
+ if (isEmpty) {
7839
+ delete currentFilters[fieldName];
7840
+ tableState.setFilters(currentFilters);
7841
+ tableState.setPagination(0);
7842
+ await fetchData();
7843
+ return;
7844
+ }
7845
+ const validation = validateFilterValueObject(fieldName, filter, fieldRegistry);
7846
+ if (!validation.valid) {
7847
+ const errorMessage = `Invalid filter for field "${fieldName}": ${validation.errors.map((e) => e.message).join(", ")}`;
7848
+ tableState.setError(errorMessage);
7849
+ if (options.debug) {
7850
+ console.error("\u274C Filter validation errors:", validation.errors);
7851
+ }
7852
+ throw new Error(errorMessage);
7088
7853
  }
7854
+ currentFilters[fieldName] = filter;
7089
7855
  tableState.setFilters(currentFilters);
7090
7856
  tableState.setPagination(0);
7091
7857
  await fetchData();
@@ -7095,13 +7861,39 @@ function useDataTable(schema, options) {
7095
7861
  delete currentFilters[field];
7096
7862
  tableState.setFilters(currentFilters);
7097
7863
  tableState.setPagination(0);
7864
+ delete filterInputValues.value[field];
7865
+ delete filterInputValues.value[`${field}_start`];
7866
+ delete filterInputValues.value[`${field}_end`];
7867
+ delete filterOperators.value[field];
7868
+ delete filterCaseSensitive.value[field];
7098
7869
  await fetchData();
7099
7870
  };
7100
7871
  const clearAllFilters = async () => {
7101
7872
  tableState.setFilters({});
7102
7873
  tableState.setPagination(0);
7874
+ filterInputValues.value = {};
7875
+ filterOperators.value = {};
7876
+ filterCaseSensitive.value = {};
7877
+ await fetchData();
7878
+ };
7879
+ const setFiltersFromQueryParams = async (params) => {
7880
+ const filters = deserializeFiltersFromQueryParams(params, fieldRegistry);
7881
+ tableState.setFilters(filters);
7882
+ tableState.setPagination(0);
7103
7883
  await fetchData();
7104
7884
  };
7885
+ const setFiltersFromQueryString = async (queryString) => {
7886
+ if (queryString && queryString.trim() !== "") {
7887
+ const params = new URLSearchParams(queryString);
7888
+ const paramsObj = {};
7889
+ params.forEach((value, key) => {
7890
+ paramsObj[key] = value;
7891
+ });
7892
+ await setFiltersFromQueryParams(paramsObj);
7893
+ } else {
7894
+ await setFiltersFromQueryParams({});
7895
+ }
7896
+ };
7105
7897
  const setSearch = async (query) => {
7106
7898
  tableState.setSearch(query);
7107
7899
  tableState.setPagination(0);
@@ -7236,8 +8028,20 @@ function useDataTable(schema, options) {
7236
8028
  },
7237
8029
  get count() {
7238
8030
  return Object.keys(extractActiveFilters(tableState.state.filters.value)).length;
8031
+ },
8032
+ get queryParams() {
8033
+ const activeFilters = extractActiveFilters(tableState.state.filters.value);
8034
+ return serializeFiltersToQueryParams(activeFilters, fieldRegistry);
8035
+ },
8036
+ // Legacy: queryString for backward compatibility (returns empty string, use queryParams instead)
8037
+ get queryString() {
8038
+ return "";
7239
8039
  }
7240
8040
  },
8041
+ // Field registry
8042
+ get fieldRegistry() {
8043
+ return fieldRegistry;
8044
+ },
7241
8045
  // Search
7242
8046
  search: {
7243
8047
  get query() {
@@ -7270,6 +8074,8 @@ function useDataTable(schema, options) {
7270
8074
  setFilter,
7271
8075
  clearFilter,
7272
8076
  clearAllFilters,
8077
+ setFiltersFromQueryParams,
8078
+ setFiltersFromQueryString,
7273
8079
  setSearch,
7274
8080
  clearSearch,
7275
8081
  goToPage,
@@ -7303,6 +8109,7 @@ function useDataTable(schema, options) {
7303
8109
  provide(ZINIA_DATA_TABLE_SEARCH_INPUT_KEY, searchInputValue);
7304
8110
  provide(ZINIA_DATA_TABLE_FILTER_INPUTS_KEY, filterInputValues);
7305
8111
  provide(ZINIA_DATA_TABLE_FILTER_OPERATORS_KEY, filterOperators);
8112
+ provide(ZINIA_DATA_TABLE_FILTER_CASE_SENSITIVE_KEY, filterCaseSensitive);
7306
8113
  provide(ZINIA_DATA_TABLE_FILTER_OPTIONS_STATE_KEY, filterOptionsState);
7307
8114
  provide(ZINIA_DATA_TABLE_FILTER_OPTIONS_LOADING_KEY, filterOptionsLoading);
7308
8115
  provide(ZINIA_DATA_TABLE_NAME_KEY, options.name || "datatable");
@@ -7312,11 +8119,13 @@ function useDataTable(schema, options) {
7312
8119
  console.error("Failed to load filter options:", err);
7313
8120
  }
7314
8121
  });
7315
- fetchData().catch((err) => {
7316
- if (options.debug) {
7317
- console.error("Failed to auto-load table data:", err);
7318
- }
7319
- });
8122
+ if (options.autoLoad !== false) {
8123
+ fetchData().catch((err) => {
8124
+ if (options.debug) {
8125
+ console.error("Failed to auto-load table data:", err);
8126
+ }
8127
+ });
8128
+ }
7320
8129
  return {
7321
8130
  table,
7322
8131
  // Components
@@ -7329,6 +8138,7 @@ function useDataTable(schema, options) {
7329
8138
  setFilter,
7330
8139
  clearFilter,
7331
8140
  clearAllFilters,
8141
+ setFiltersFromQueryString,
7332
8142
  setSearch,
7333
8143
  clearSearch,
7334
8144
  goToPage,
@@ -7358,6 +8168,10 @@ function getFilterType(field, column, fieldsMetadata) {
7358
8168
  return "boolean";
7359
8169
  case "combobox":
7360
8170
  return "combobox";
8171
+ case "date":
8172
+ case "datetime-local":
8173
+ case "time":
8174
+ return "date";
7361
8175
  case "select":
7362
8176
  case "radio":
7363
8177
  return column.filterOptions || fieldMetadata?.options ? "select" : "text";
@@ -7709,170 +8523,787 @@ var DesktopTable = (props) => {
7709
8523
  ] })
7710
8524
  ] }) });
7711
8525
  };
7712
- var SelectFilter = (props) => {
7713
- return /* @__PURE__ */ jsxs(
7714
- "select",
7715
- {
7716
- value: props.value || "",
7717
- onChange: (e) => {
7718
- const value = e.target.value;
7719
- props.onFilterChange(props.field, value, "eq");
7720
- },
7721
- class: "select select-bordered select-sm w-full",
7722
- disabled: props.isLoading || props.isOptionsLoading,
7723
- "data-testid": `datatable-filter-${props.field}-input`,
7724
- children: [
7725
- props.showAllOption !== false && /* @__PURE__ */ jsx("option", { value: "", children: props.isOptionsLoading ? "Loading options..." : props.allOptionText !== void 0 ? props.allOptionText : `All ${props.label}` }),
7726
- props.options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
7727
- ]
7728
- }
7729
- );
7730
- };
7731
-
7732
- // src/fields/daisy_ui/datatable/helpers/debounce.ts
7733
- var timeoutStore = /* @__PURE__ */ new Map();
7734
- function debounce(key, callback, delay = 300) {
7735
- const existing = timeoutStore.get(key);
7736
- if (existing) {
7737
- clearTimeout(existing);
8526
+ var dropdownState = reactive({});
8527
+ var DROPDOWN_MAX_HEIGHT = 240;
8528
+ var MIN_SPACE_BELOW = 100;
8529
+ var EnumMultiSelectFilter = (props) => {
8530
+ if (!dropdownState[props.field]) {
8531
+ dropdownState[props.field] = {
8532
+ isOpen: false,
8533
+ searchQuery: "",
8534
+ selectedIndex: -1,
8535
+ position: { top: 0, left: 0, width: 0, openUpward: false },
8536
+ cleanupFns: []
8537
+ };
7738
8538
  }
7739
- const timeoutId = setTimeout(() => {
7740
- callback();
7741
- timeoutStore.delete(key);
7742
- }, delay);
7743
- timeoutStore.set(key, timeoutId);
7744
- }
7745
- var TextFilter = (props) => {
7746
- const currentOperator = props.filterOperators.value[props.field] || "contains";
7747
- return /* @__PURE__ */ jsxs("div", { class: "flex gap-1.5 md:gap-2 w-full items-center", children: [
7748
- /* @__PURE__ */ jsxs(
8539
+ const state = dropdownState[props.field];
8540
+ const selectedValues = state.isOpen && state.pendingValues !== void 0 ? state.pendingValues : Array.isArray(props.value) ? props.value : props.value ? [props.value] : [];
8541
+ const valuesEqual = (a, b) => {
8542
+ if (typeof a === typeof b) {
8543
+ return a === b;
8544
+ }
8545
+ return String(a) === String(b);
8546
+ };
8547
+ const selectedOptions = computed(() => {
8548
+ return selectedValues.map((val) => {
8549
+ const option = props.options.find((opt) => valuesEqual(opt.value, val));
8550
+ return option || { label: String(val), value: val };
8551
+ }).filter((opt) => Boolean(opt));
8552
+ });
8553
+ const filteredOptions = computed(() => {
8554
+ const query = state.searchQuery.trim().toLowerCase();
8555
+ if (!query) {
8556
+ return props.options;
8557
+ }
8558
+ return props.options.filter((opt) => opt.label.toLowerCase().includes(query));
8559
+ });
8560
+ const updateDropdownPosition = () => {
8561
+ const container = document.querySelector(`[data-enum-multiselect-container="${props.field}"]`);
8562
+ if (!container) return;
8563
+ const rect = container.getBoundingClientRect();
8564
+ const spaceBelow = window.innerHeight - rect.bottom;
8565
+ const spaceAbove = rect.top;
8566
+ const openUpward = spaceBelow < MIN_SPACE_BELOW && spaceAbove > spaceBelow;
8567
+ state.position = {
8568
+ top: openUpward ? rect.top - 4 : rect.bottom + 4,
8569
+ left: rect.left,
8570
+ width: rect.width,
8571
+ openUpward
8572
+ };
8573
+ };
8574
+ const applyPendingFilter = () => {
8575
+ if (state.pendingValues === void 0) return;
8576
+ if (state.pendingValues.length === 0) {
8577
+ if (props.onClearFilter) {
8578
+ props.onClearFilter(props.field);
8579
+ }
8580
+ state.pendingValues = void 0;
8581
+ return;
8582
+ }
8583
+ const filter = {
8584
+ operator: props.operator,
8585
+ values: state.pendingValues
8586
+ };
8587
+ props.onFilterChange(props.field, filter);
8588
+ state.pendingValues = void 0;
8589
+ };
8590
+ const toggleOption = (option) => {
8591
+ const currentValues = [...selectedValues];
8592
+ const index = currentValues.findIndex((v) => valuesEqual(v, option.value));
8593
+ if (index >= 0) {
8594
+ currentValues.splice(index, 1);
8595
+ } else {
8596
+ currentValues.push(option.value);
8597
+ }
8598
+ state.pendingValues = currentValues;
8599
+ nextTick(() => {
8600
+ updateDropdownPosition();
8601
+ });
8602
+ };
8603
+ const removeSelected = (value) => {
8604
+ const currentValues = (Array.isArray(props.value) ? props.value : props.value ? [props.value] : []).filter(
8605
+ (v) => !valuesEqual(v, value)
8606
+ );
8607
+ state.isOpen = false;
8608
+ state.searchQuery = "";
8609
+ state.selectedIndex = -1;
8610
+ state.pendingValues = void 0;
8611
+ cleanupEventListeners();
8612
+ if (currentValues.length === 0) {
8613
+ if (props.onClearFilter) {
8614
+ props.onClearFilter(props.field);
8615
+ }
8616
+ return;
8617
+ }
8618
+ const filter = {
8619
+ operator: props.operator,
8620
+ values: currentValues
8621
+ };
8622
+ props.onFilterChange(props.field, filter);
8623
+ };
8624
+ const handleScrollOrResize = () => {
8625
+ if (state.isOpen) {
8626
+ updateDropdownPosition();
8627
+ }
8628
+ };
8629
+ const handleClickOutside = (e) => {
8630
+ const container = document.querySelector(`[data-enum-multiselect-container="${props.field}"]`);
8631
+ const dropdown = document.querySelector(`[data-enum-multiselect-dropdown="${props.field}"]`);
8632
+ const target = e.target;
8633
+ if (state.isOpen && container && !container.contains(target) && dropdown && !dropdown.contains(target)) {
8634
+ applyPendingFilter();
8635
+ state.isOpen = false;
8636
+ state.searchQuery = "";
8637
+ state.selectedIndex = -1;
8638
+ cleanupEventListeners();
8639
+ }
8640
+ };
8641
+ if (!state.cleanupFns) {
8642
+ state.cleanupFns = [];
8643
+ }
8644
+ const setupEventListeners = () => {
8645
+ if (!state.cleanupFns) {
8646
+ state.cleanupFns = [];
8647
+ }
8648
+ if (state.cleanupFns.length > 0) return;
8649
+ window.addEventListener("scroll", handleScrollOrResize, true);
8650
+ window.addEventListener("resize", handleScrollOrResize);
8651
+ document.addEventListener("click", handleClickOutside, true);
8652
+ state.cleanupFns.push(() => {
8653
+ window.removeEventListener("scroll", handleScrollOrResize, true);
8654
+ window.removeEventListener("resize", handleScrollOrResize);
8655
+ document.removeEventListener("click", handleClickOutside, true);
8656
+ });
8657
+ };
8658
+ const cleanupEventListeners = () => {
8659
+ if (state.cleanupFns && state.cleanupFns.length > 0) {
8660
+ state.cleanupFns.forEach((fn) => fn());
8661
+ state.cleanupFns = [];
8662
+ }
8663
+ };
8664
+ const handleFocus = () => {
8665
+ if (state.pendingValues === void 0) {
8666
+ state.pendingValues = Array.isArray(props.value) ? [...props.value] : props.value ? [props.value] : [];
8667
+ }
8668
+ state.isOpen = true;
8669
+ updateDropdownPosition();
8670
+ setupEventListeners();
8671
+ };
8672
+ const handleBlur = (e) => {
8673
+ setTimeout(() => {
8674
+ const relatedTarget = e.relatedTarget || document.activeElement;
8675
+ const container = document.querySelector(
8676
+ `[data-enum-multiselect-container="${props.field}"]`
8677
+ );
8678
+ const dropdown = document.querySelector(`[data-enum-multiselect-dropdown="${props.field}"]`);
8679
+ if (container && container.contains(relatedTarget) || dropdown && dropdown.contains(relatedTarget)) {
8680
+ return;
8681
+ }
8682
+ if (state.isOpen) {
8683
+ applyPendingFilter();
8684
+ state.isOpen = false;
8685
+ state.searchQuery = "";
8686
+ state.selectedIndex = -1;
8687
+ cleanupEventListeners();
8688
+ }
8689
+ }, 100);
8690
+ };
8691
+ const handleKeyDown = (e) => {
8692
+ if (e.key === "Escape") {
8693
+ e.preventDefault();
8694
+ e.stopPropagation();
8695
+ state.pendingValues = void 0;
8696
+ state.isOpen = false;
8697
+ state.searchQuery = "";
8698
+ state.selectedIndex = -1;
8699
+ cleanupEventListeners();
8700
+ return;
8701
+ }
8702
+ if (e.key === "Enter" || e.key === " ") {
8703
+ e.preventDefault();
8704
+ e.stopPropagation();
8705
+ if (state.selectedIndex >= 0 && state.selectedIndex < filteredOptions.value.length) {
8706
+ toggleOption(filteredOptions.value[state.selectedIndex]);
8707
+ } else if (e.key === "Enter" && filteredOptions.value.length === 1) {
8708
+ toggleOption(filteredOptions.value[0]);
8709
+ }
8710
+ return;
8711
+ }
8712
+ if (e.key === "ArrowDown") {
8713
+ e.preventDefault();
8714
+ e.stopPropagation();
8715
+ if (!state.isOpen) {
8716
+ state.isOpen = true;
8717
+ updateDropdownPosition();
8718
+ setupEventListeners();
8719
+ }
8720
+ const totalOptions = filteredOptions.value.length;
8721
+ if (totalOptions > 0) {
8722
+ state.selectedIndex = state.selectedIndex < 0 ? 0 : (state.selectedIndex + 1) % totalOptions;
8723
+ nextTick(() => scrollToSelected());
8724
+ }
8725
+ return;
8726
+ }
8727
+ if (e.key === "ArrowUp") {
8728
+ e.preventDefault();
8729
+ e.stopPropagation();
8730
+ if (!state.isOpen) {
8731
+ state.isOpen = true;
8732
+ updateDropdownPosition();
8733
+ setupEventListeners();
8734
+ }
8735
+ const totalOptions = filteredOptions.value.length;
8736
+ if (totalOptions > 0) {
8737
+ state.selectedIndex = state.selectedIndex < 0 ? totalOptions - 1 : (state.selectedIndex - 1 + totalOptions) % totalOptions;
8738
+ nextTick(() => scrollToSelected());
8739
+ }
8740
+ return;
8741
+ }
8742
+ if (e.key === "Tab") {
8743
+ applyPendingFilter();
8744
+ state.isOpen = false;
8745
+ state.searchQuery = "";
8746
+ state.selectedIndex = -1;
8747
+ cleanupEventListeners();
8748
+ }
8749
+ };
8750
+ const scrollToSelected = () => {
8751
+ const dropdown = document.querySelector(`[data-enum-multiselect-dropdown="${props.field}"]`);
8752
+ if (!dropdown || state.selectedIndex < 0) return;
8753
+ const option = dropdown.querySelector(`[data-option-index="${state.selectedIndex}"]`);
8754
+ if (!option) return;
8755
+ const elementTop = option.offsetTop;
8756
+ const elementHeight = option.offsetHeight;
8757
+ const containerHeight = dropdown.clientHeight;
8758
+ const containerScrollTop = dropdown.scrollTop;
8759
+ if (elementTop < containerScrollTop) {
8760
+ dropdown.scrollTop = elementTop;
8761
+ } else if (elementTop + elementHeight > containerScrollTop + containerHeight) {
8762
+ dropdown.scrollTop = elementTop + elementHeight - containerHeight;
8763
+ }
8764
+ };
8765
+ const dropdownStyle = computed(() => {
8766
+ const pos = state.position;
8767
+ return {
8768
+ position: "fixed",
8769
+ top: pos.openUpward ? "auto" : `${pos.top}px`,
8770
+ bottom: pos.openUpward ? `${window.innerHeight - pos.top}px` : "auto",
8771
+ left: `${pos.left}px`,
8772
+ width: `${pos.width}px`,
8773
+ maxHeight: `${DROPDOWN_MAX_HEIGHT}px`,
8774
+ zIndex: 9999
8775
+ };
8776
+ });
8777
+ return /* @__PURE__ */ jsxs("div", { class: "flex flex-col gap-2 w-full", children: [
8778
+ selectedOptions.value.length > 0 && /* @__PURE__ */ jsx("div", { class: "flex flex-wrap gap-1.5 max-h-24 overflow-y-auto", children: selectedOptions.value.map((option) => /* @__PURE__ */ jsxs(
8779
+ "div",
8780
+ {
8781
+ class: "badge badge-primary badge-sm gap-1.5 pr-1 max-w-full",
8782
+ "data-testid": `datatable-filter-${props.field}-chip-${option.value}`,
8783
+ children: [
8784
+ /* @__PURE__ */ jsx("span", { class: "truncate max-w-[120px] sm:max-w-[200px]", title: option.label, children: option.label }),
8785
+ /* @__PURE__ */ jsx(
8786
+ "button",
8787
+ {
8788
+ type: "button",
8789
+ onClick: (e) => {
8790
+ e.preventDefault();
8791
+ e.stopPropagation();
8792
+ removeSelected(option.value);
8793
+ },
8794
+ onMousedown: (e) => {
8795
+ e.preventDefault();
8796
+ },
8797
+ class: "btn btn-ghost btn-xs p-0 h-4 w-4 min-h-0 rounded-full hover:bg-primary-focus flex-shrink-0",
8798
+ "aria-label": `Remove ${option.label}`,
8799
+ tabindex: -1,
8800
+ children: /* @__PURE__ */ jsx(
8801
+ "svg",
8802
+ {
8803
+ xmlns: "http://www.w3.org/2000/svg",
8804
+ class: "h-3 w-3",
8805
+ fill: "none",
8806
+ viewBox: "0 0 24 24",
8807
+ stroke: "currentColor",
8808
+ "stroke-width": "2",
8809
+ children: /* @__PURE__ */ jsx("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M6 18L18 6M6 6l12 12" })
8810
+ }
8811
+ )
8812
+ }
8813
+ )
8814
+ ]
8815
+ },
8816
+ String(option.value)
8817
+ )) }),
8818
+ /* @__PURE__ */ jsxs("div", { class: "relative", "data-enum-multiselect-container": props.field, children: [
8819
+ /* @__PURE__ */ jsx(
8820
+ "input",
8821
+ {
8822
+ type: "text",
8823
+ value: state.searchQuery,
8824
+ onInput: (e) => {
8825
+ const value = e.target.value;
8826
+ state.searchQuery = value;
8827
+ state.isOpen = true;
8828
+ state.selectedIndex = -1;
8829
+ updateDropdownPosition();
8830
+ if (!state.cleanupFns || state.cleanupFns.length === 0) {
8831
+ setupEventListeners();
8832
+ }
8833
+ },
8834
+ onFocus: handleFocus,
8835
+ onBlur: handleBlur,
8836
+ onKeydown: handleKeyDown,
8837
+ placeholder: props.isOptionsLoading ? "Loading options..." : selectedOptions.value.length > 0 ? `Search ${props.label}...` : `Select ${props.label}...`,
8838
+ class: "input input-bordered input-sm w-full pr-8",
8839
+ disabled: props.isLoading || props.isOptionsLoading,
8840
+ "data-testid": `datatable-filter-${props.field}-input`
8841
+ }
8842
+ ),
8843
+ /* @__PURE__ */ jsx("div", { class: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none", children: /* @__PURE__ */ jsx(
8844
+ "svg",
8845
+ {
8846
+ xmlns: "http://www.w3.org/2000/svg",
8847
+ class: `h-4 w-4 text-base-content/50 transition-transform ${state.isOpen ? "rotate-180" : ""}`,
8848
+ fill: "none",
8849
+ viewBox: "0 0 24 24",
8850
+ stroke: "currentColor",
8851
+ "stroke-width": "2",
8852
+ children: /* @__PURE__ */ jsx("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M19 9l-7 7-7-7" })
8853
+ }
8854
+ ) }),
8855
+ state.isOpen && /* @__PURE__ */ jsx(Teleport, { to: "body", children: /* @__PURE__ */ jsx(
8856
+ "div",
8857
+ {
8858
+ "data-enum-multiselect-dropdown": props.field,
8859
+ class: "bg-base-100 border border-base-300 rounded-box shadow-lg overflow-auto",
8860
+ style: dropdownStyle.value,
8861
+ onMousedown: (e) => {
8862
+ e.preventDefault();
8863
+ },
8864
+ children: props.isOptionsLoading ? /* @__PURE__ */ jsx("div", { class: "p-2", children: /* @__PURE__ */ jsx("span", { class: "text-sm text-base-content/70", children: "Loading options..." }) }) : props.options.length === 0 ? /* @__PURE__ */ jsx("div", { class: "p-2", children: /* @__PURE__ */ jsx("span", { class: "text-sm text-base-content/70", children: "No options available" }) }) : filteredOptions.value.length === 0 ? /* @__PURE__ */ jsx("div", { class: "p-2", children: /* @__PURE__ */ jsxs("span", { class: "text-sm text-base-content/70", children: [
8865
+ 'No options found matching "',
8866
+ state.searchQuery,
8867
+ '"'
8868
+ ] }) }) : /* @__PURE__ */ jsx("ul", { class: "menu menu-sm w-full p-2", role: "listbox", "aria-label": `${props.label} options`, children: filteredOptions.value.map((option, index) => {
8869
+ const isSelected = selectedValues.some((v) => valuesEqual(v, option.value));
8870
+ const isHighlighted = index === state.selectedIndex;
8871
+ return /* @__PURE__ */ jsx(
8872
+ "li",
8873
+ {
8874
+ "data-option-index": index,
8875
+ role: "option",
8876
+ "aria-selected": isSelected,
8877
+ class: isHighlighted ? "bg-base-200 rounded" : "",
8878
+ children: /* @__PURE__ */ jsxs(
8879
+ "label",
8880
+ {
8881
+ class: "label cursor-pointer gap-2 p-2 rounded hover:bg-base-200",
8882
+ onMousedown: (e) => {
8883
+ e.preventDefault();
8884
+ },
8885
+ children: [
8886
+ /* @__PURE__ */ jsx(
8887
+ "input",
8888
+ {
8889
+ type: "checkbox",
8890
+ checked: isSelected,
8891
+ onChange: () => toggleOption(option),
8892
+ class: "checkbox checkbox-sm checkbox-primary",
8893
+ "data-testid": `datatable-filter-${props.field}-option-${option.value}`,
8894
+ tabindex: -1
8895
+ }
8896
+ ),
8897
+ /* @__PURE__ */ jsx("span", { class: "label-text flex-1 truncate", title: option.label, children: option.label })
8898
+ ]
8899
+ }
8900
+ )
8901
+ },
8902
+ String(option.value)
8903
+ );
8904
+ }) })
8905
+ }
8906
+ ) })
8907
+ ] }),
8908
+ /* @__PURE__ */ jsx("div", { class: "text-xs text-base-content/60", children: props.operator === "isOneOf" ? "Select one or more values" : "Exclude one or more values" })
8909
+ ] });
8910
+ };
8911
+ var SelectFilter = (props) => {
8912
+ const currentOperator = props.operator || OPERATORS.IS_ONE_OF;
8913
+ const isArrayOperator = currentOperator === OPERATORS.IS_ONE_OF || currentOperator === OPERATORS.IS_NOT_ONE_OF;
8914
+ const isDefaultOperator = currentOperator === OPERATORS.IS_ONE_OF;
8915
+ const showOperatorSelect = !isDefaultOperator;
8916
+ const selectedValues = Array.isArray(props.value) ? props.value : props.value ? [props.value] : [];
8917
+ const handleValueChange = (value) => {
8918
+ if (value === "" || value === null || value === void 0) {
8919
+ props.onFilterChange(props.field, { operator: currentOperator, value: "" });
8920
+ return;
8921
+ }
8922
+ const filter = {
8923
+ operator: currentOperator
8924
+ };
8925
+ if (isArrayOperator) {
8926
+ filter.values = Array.isArray(value) ? value : [value];
8927
+ } else {
8928
+ filter.value = value;
8929
+ }
8930
+ props.onFilterChange(props.field, filter);
8931
+ };
8932
+ if (isArrayOperator) {
8933
+ return /* @__PURE__ */ jsxs("div", { class: "flex flex-col gap-1.5 md:gap-2 w-full", children: [
8934
+ showOperatorSelect && /* @__PURE__ */ jsxs(
8935
+ "select",
8936
+ {
8937
+ value: currentOperator,
8938
+ onChange: (e) => {
8939
+ const newOperator = e.target.value;
8940
+ const currentValue = props.value;
8941
+ if (currentValue !== "" && currentValue !== null && currentValue !== void 0) {
8942
+ const filter = {
8943
+ operator: newOperator
8944
+ };
8945
+ if (newOperator === OPERATORS.IS_ONE_OF || newOperator === OPERATORS.IS_NOT_ONE_OF) {
8946
+ filter.values = Array.isArray(currentValue) ? currentValue : [currentValue];
8947
+ } else {
8948
+ filter.value = Array.isArray(currentValue) ? currentValue[0] : currentValue;
8949
+ }
8950
+ props.onFilterChange(props.field, filter);
8951
+ }
8952
+ },
8953
+ class: "select select-bordered select-sm w-full text-xs font-medium",
8954
+ "data-testid": `datatable-filter-${props.field}-operator`,
8955
+ children: [
8956
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.IS_ONE_OF, children: "in" }),
8957
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.IS_NOT_ONE_OF, children: "notIn" })
8958
+ ]
8959
+ }
8960
+ ),
8961
+ /* @__PURE__ */ jsx(
8962
+ EnumMultiSelectFilter,
8963
+ {
8964
+ field: props.field,
8965
+ value: selectedValues,
8966
+ options: props.options,
8967
+ label: props.label,
8968
+ isLoading: props.isLoading,
8969
+ isOptionsLoading: props.isOptionsLoading,
8970
+ operator: currentOperator,
8971
+ onFilterChange: props.onFilterChange,
8972
+ onClearFilter: props.onClearFilter
8973
+ }
8974
+ )
8975
+ ] });
8976
+ }
8977
+ return /* @__PURE__ */ jsx("div", { class: "flex flex-col gap-1.5 md:gap-2 w-full", children: /* @__PURE__ */ jsxs("div", { class: "flex gap-1.5 md:gap-2 w-full items-center", children: [
8978
+ showOperatorSelect && /* @__PURE__ */ jsxs(
8979
+ "select",
8980
+ {
8981
+ value: currentOperator,
8982
+ onChange: (e) => {
8983
+ const newOperator = e.target.value;
8984
+ const currentValue = props.value;
8985
+ if (currentValue !== "" && currentValue !== null && currentValue !== void 0) {
8986
+ const filter = {
8987
+ operator: newOperator,
8988
+ value: Array.isArray(currentValue) ? currentValue[0] : currentValue
8989
+ };
8990
+ props.onFilterChange(props.field, filter);
8991
+ }
8992
+ },
8993
+ class: "select select-bordered select-sm w-20 md:w-24 flex-shrink-0 text-xs font-medium",
8994
+ "data-testid": `datatable-filter-${props.field}-operator`,
8995
+ children: [
8996
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.EQUALS, children: "eq" }),
8997
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.NOT_EQUALS, children: "ne" })
8998
+ ]
8999
+ }
9000
+ ),
9001
+ /* @__PURE__ */ jsxs(
9002
+ "select",
9003
+ {
9004
+ value: props.value || "",
9005
+ onChange: (e) => {
9006
+ const value = e.target.value;
9007
+ handleValueChange(value);
9008
+ },
9009
+ class: "select select-bordered select-sm flex-1 min-w-0",
9010
+ disabled: props.isLoading || props.isOptionsLoading,
9011
+ "data-testid": `datatable-filter-${props.field}-input`,
9012
+ children: [
9013
+ props.showAllOption !== false && /* @__PURE__ */ jsx("option", { value: "", children: props.isOptionsLoading ? "Loading options..." : props.allOptionText !== void 0 ? props.allOptionText : `All ${props.label}` }),
9014
+ props.options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
9015
+ ]
9016
+ }
9017
+ )
9018
+ ] }) });
9019
+ };
9020
+
9021
+ // src/fields/daisy_ui/datatable/helpers/debounce.ts
9022
+ var timeoutStore = /* @__PURE__ */ new Map();
9023
+ function debounce(key, callback, delay = 300) {
9024
+ const existing = timeoutStore.get(key);
9025
+ if (existing) {
9026
+ clearTimeout(existing);
9027
+ }
9028
+ const timeoutId = setTimeout(() => {
9029
+ callback();
9030
+ timeoutStore.delete(key);
9031
+ }, delay);
9032
+ timeoutStore.set(key, timeoutId);
9033
+ }
9034
+ var TextFilter = (props) => {
9035
+ const currentOperator = props.filterOperators.value[props.field] || OPERATORS.CONTAINS;
9036
+ const isArrayOperator = currentOperator === OPERATORS.IS_ONE_OF || currentOperator === OPERATORS.IS_NOT_ONE_OF;
9037
+ const isDefaultOperator = currentOperator === OPERATORS.CONTAINS;
9038
+ const showOperatorSelect = !isDefaultOperator;
9039
+ const filterInputValue = props.filterInputValues.value[props.field];
9040
+ const displayValue = filterInputValue !== void 0 && filterInputValue !== "" ? filterInputValue : props.value || "";
9041
+ const handleFilterChange = (value, operator = currentOperator) => {
9042
+ const filter = {
9043
+ operator,
9044
+ // Strings are always case-insensitive (contains is default)
9045
+ caseSensitive: false
9046
+ };
9047
+ if (isArrayOperator) {
9048
+ if (typeof value === "string" && value.trim()) {
9049
+ filter.values = value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
9050
+ } else if (Array.isArray(value)) {
9051
+ filter.values = value;
9052
+ }
9053
+ } else {
9054
+ filter.value = value;
9055
+ }
9056
+ props.onFilterChange(props.field, filter);
9057
+ };
9058
+ return /* @__PURE__ */ jsxs("div", { class: "flex flex-col gap-1.5 md:gap-2 w-full", children: [
9059
+ /* @__PURE__ */ jsxs("div", { class: "flex gap-1.5 md:gap-2 w-full items-center", children: [
9060
+ showOperatorSelect && /* @__PURE__ */ jsxs(
9061
+ "select",
9062
+ {
9063
+ value: currentOperator,
9064
+ onChange: (e) => {
9065
+ const newOperator = e.target.value;
9066
+ props.filterOperators.value[props.field] = newOperator;
9067
+ const currentValue = props.filterInputValues.value[props.field];
9068
+ if (currentValue) {
9069
+ handleFilterChange(currentValue, newOperator);
9070
+ }
9071
+ },
9072
+ class: "select select-bordered select-sm w-20 md:w-24 flex-shrink-0 text-xs",
9073
+ "data-testid": `datatable-filter-${props.field}-operator`,
9074
+ children: [
9075
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.CONTAINS, children: "contains" }),
9076
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.EQUALS, children: "eq" }),
9077
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.NOT_EQUALS, children: "ne" }),
9078
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.STARTS_WITH, children: "sw" }),
9079
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.ENDS_WITH, children: "ew" }),
9080
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.IS_ONE_OF, children: "in" }),
9081
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.IS_NOT_ONE_OF, children: "notIn" })
9082
+ ]
9083
+ }
9084
+ ),
9085
+ /* @__PURE__ */ jsx(
9086
+ "input",
9087
+ {
9088
+ type: "text",
9089
+ value: displayValue,
9090
+ onInput: (e) => {
9091
+ const value = e.target.value;
9092
+ props.filterInputValues.value[props.field] = value;
9093
+ debounce(
9094
+ `filter_${props.field}`,
9095
+ () => {
9096
+ handleFilterChange(value);
9097
+ },
9098
+ 300
9099
+ );
9100
+ },
9101
+ onKeydown: (e) => {
9102
+ if (e.key === "Escape" && props.filterInputValues.value[props.field]) {
9103
+ e.preventDefault();
9104
+ props.filterInputValues.value[props.field] = "";
9105
+ handleFilterChange("");
9106
+ }
9107
+ },
9108
+ placeholder: isArrayOperator ? `Filter by ${props.label} (comma-separated)` : `Filter by ${props.label}`,
9109
+ class: "input input-bordered input-sm flex-1 min-w-0",
9110
+ "data-testid": `datatable-filter-${props.field}-input`
9111
+ }
9112
+ )
9113
+ ] }),
9114
+ isArrayOperator && /* @__PURE__ */ jsx("div", { class: "text-xs text-base-content/60", children: 'Enter multiple values separated by commas (e.g., "value1, value2, value3")' })
9115
+ ] });
9116
+ };
9117
+ var NumberFilter = (props) => {
9118
+ if (!props.filterOperators.value[props.field]) {
9119
+ props.filterOperators.value[props.field] = OPERATORS.EQUALS;
9120
+ }
9121
+ const currentOperator = props.filterOperators.value[props.field] || OPERATORS.EQUALS;
9122
+ const isBetween = currentOperator === OPERATORS.BETWEEN;
9123
+ const filterInputValue = props.filterInputValues.value[props.field];
9124
+ const displayValue = filterInputValue !== void 0 && filterInputValue !== "" ? filterInputValue : props.value !== void 0 && props.value !== null && props.value !== "" ? String(props.value) : "";
9125
+ const handleFilterChange = (value, operator = currentOperator) => {
9126
+ const filter = {
9127
+ operator
9128
+ };
9129
+ if (isBetween) {
9130
+ if (typeof value === "string" && value.includes(",")) {
9131
+ const parts = value.split(",").map((v) => parseFloat(v.trim())).filter((v) => !isNaN(v));
9132
+ if (parts.length === 2) {
9133
+ filter.values = [Math.min(parts[0], parts[1]), Math.max(parts[0], parts[1])];
9134
+ }
9135
+ } else if (Array.isArray(value) && value.length === 2) {
9136
+ filter.values = [Math.min(value[0], value[1]), Math.max(value[0], value[1])];
9137
+ }
9138
+ } else {
9139
+ const numValue = typeof value === "number" ? value : parseFloat(value);
9140
+ if (!isNaN(numValue)) {
9141
+ filter.value = numValue;
9142
+ }
9143
+ }
9144
+ props.onFilterChange(props.field, filter);
9145
+ };
9146
+ return /* @__PURE__ */ jsxs("div", { class: "flex flex-col gap-1.5 md:gap-2 w-full", children: [
9147
+ /* @__PURE__ */ jsxs("div", { class: "flex gap-1.5 md:gap-2 w-full items-center", children: [
9148
+ /* @__PURE__ */ jsxs(
9149
+ "select",
9150
+ {
9151
+ value: currentOperator,
9152
+ onChange: (e) => {
9153
+ const newOperator = e.target.value;
9154
+ props.filterOperators.value[props.field] = newOperator;
9155
+ const currentValue = props.filterInputValues.value[props.field];
9156
+ if (currentValue) {
9157
+ handleFilterChange(currentValue, newOperator);
9158
+ }
9159
+ },
9160
+ class: "select select-bordered select-sm w-20 md:w-24 flex-shrink-0 text-xs font-medium",
9161
+ "data-testid": `datatable-filter-${props.field}-operator`,
9162
+ children: [
9163
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.EQUALS, children: "eq" }),
9164
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.NOT_EQUALS, children: "ne" }),
9165
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.GREATER_THAN, children: "gt" }),
9166
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.GREATER_THAN_OR_EQUAL, children: "gte" }),
9167
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.LESS_THAN, children: "lt" }),
9168
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.LESS_THAN_OR_EQUAL, children: "lte" }),
9169
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.BETWEEN, children: "between" })
9170
+ ]
9171
+ }
9172
+ ),
9173
+ !isBetween && /* @__PURE__ */ jsx(
9174
+ "input",
9175
+ {
9176
+ type: "number",
9177
+ value: displayValue,
9178
+ onInput: (e) => {
9179
+ const value = e.target.value;
9180
+ props.filterInputValues.value[props.field] = value;
9181
+ debounce(
9182
+ `filter_${props.field}`,
9183
+ () => {
9184
+ handleFilterChange(value);
9185
+ },
9186
+ 300
9187
+ );
9188
+ },
9189
+ onKeydown: (e) => {
9190
+ if (e.key === "Escape" && props.filterInputValues.value[props.field]) {
9191
+ e.preventDefault();
9192
+ props.filterInputValues.value[props.field] = "";
9193
+ handleFilterChange("");
9194
+ }
9195
+ },
9196
+ placeholder: `Filter by ${props.label}`,
9197
+ class: "input input-bordered input-sm flex-1 min-w-0",
9198
+ "data-testid": `datatable-filter-${props.field}-input`
9199
+ }
9200
+ )
9201
+ ] }),
9202
+ isBetween && /* @__PURE__ */ jsxs("div", { class: "flex gap-2 items-center w-full", children: [
9203
+ /* @__PURE__ */ jsx(
9204
+ "input",
9205
+ {
9206
+ type: "number",
9207
+ placeholder: "Min",
9208
+ class: "input input-bordered input-sm flex-1 min-w-0",
9209
+ "data-testid": `datatable-filter-${props.field}-input-min`,
9210
+ onInput: (e) => {
9211
+ const minValue = e.target.value;
9212
+ const maxValue = props.filterInputValues.value[`${props.field}_max`] || "";
9213
+ if (minValue && maxValue) {
9214
+ props.filterInputValues.value[props.field] = `${minValue},${maxValue}`;
9215
+ handleFilterChange(`${minValue},${maxValue}`);
9216
+ } else {
9217
+ props.filterInputValues.value[`${props.field}_min`] = minValue;
9218
+ }
9219
+ }
9220
+ }
9221
+ ),
9222
+ /* @__PURE__ */ jsx("span", { class: "text-xs text-base-content/60", children: "to" }),
9223
+ /* @__PURE__ */ jsx(
9224
+ "input",
9225
+ {
9226
+ type: "number",
9227
+ placeholder: "Max",
9228
+ class: "input input-bordered input-sm flex-1 min-w-0",
9229
+ "data-testid": `datatable-filter-${props.field}-input-max`,
9230
+ onInput: (e) => {
9231
+ const maxValue = e.target.value;
9232
+ const minValue = props.filterInputValues.value[`${props.field}_min`] || props.filterInputValues.value[props.field]?.split(",")[0] || "";
9233
+ if (minValue && maxValue) {
9234
+ props.filterInputValues.value[props.field] = `${minValue},${maxValue}`;
9235
+ handleFilterChange(`${minValue},${maxValue}`);
9236
+ } else {
9237
+ props.filterInputValues.value[`${props.field}_max`] = maxValue;
9238
+ }
9239
+ }
9240
+ }
9241
+ )
9242
+ ] })
9243
+ ] });
9244
+ };
9245
+ var BooleanFilter = (props) => {
9246
+ const currentOperator = props.operator || OPERATORS.EQUALS;
9247
+ const isDefaultOperator = currentOperator === OPERATORS.EQUALS;
9248
+ const showOperatorSelect = !isDefaultOperator;
9249
+ const trueOption = props.options?.find((opt) => opt.value === true) || { label: "Yes"};
9250
+ const falseOption = props.options?.find((opt) => opt.value === false) || { label: "No"};
9251
+ return /* @__PURE__ */ jsx("div", { class: "flex flex-col gap-1.5 md:gap-2 w-full", children: /* @__PURE__ */ jsxs("div", { class: "flex gap-1.5 md:gap-2 w-full items-center", children: [
9252
+ showOperatorSelect && /* @__PURE__ */ jsxs(
7749
9253
  "select",
7750
9254
  {
7751
9255
  value: currentOperator,
7752
9256
  onChange: (e) => {
7753
9257
  const newOperator = e.target.value;
7754
- props.filterOperators.value[props.field] = newOperator;
7755
- const currentValue = props.filterInputValues.value[props.field];
7756
- if (currentValue) {
7757
- props.onFilterChange(props.field, currentValue, newOperator);
9258
+ const currentValue = props.value;
9259
+ if (currentValue !== "" && currentValue !== null && currentValue !== void 0) {
9260
+ const filter = {
9261
+ operator: newOperator,
9262
+ value: currentValue === true || currentValue === "true"
9263
+ };
9264
+ props.onFilterChange(props.field, filter);
7758
9265
  }
7759
9266
  },
7760
- class: "select select-bordered select-sm w-24 md:w-32 flex-shrink-0 text-xs",
9267
+ class: "select select-bordered select-sm w-20 md:w-24 flex-shrink-0 text-xs font-medium",
7761
9268
  "data-testid": `datatable-filter-${props.field}-operator`,
7762
9269
  children: [
7763
- /* @__PURE__ */ jsx("option", { value: "contains", children: "Contains" }),
7764
- /* @__PURE__ */ jsx("option", { value: "eq", children: "Equals" })
9270
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.EQUALS, children: "eq" }),
9271
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.NOT_EQUALS, children: "ne" })
7765
9272
  ]
7766
9273
  }
7767
9274
  ),
7768
- /* @__PURE__ */ jsx(
7769
- "input",
7770
- {
7771
- type: "text",
7772
- value: props.filterInputValues.value[props.field] || "",
7773
- onInput: (e) => {
7774
- const value = e.target.value;
7775
- props.filterInputValues.value[props.field] = value;
7776
- debounce(
7777
- `filter_${props.field}`,
7778
- () => {
7779
- props.onFilterChange(props.field, value, currentOperator);
7780
- },
7781
- 300
7782
- );
7783
- },
7784
- onKeydown: (e) => {
7785
- if (e.key === "Escape" && props.filterInputValues.value[props.field]) {
7786
- e.preventDefault();
7787
- props.filterInputValues.value[props.field] = "";
7788
- props.onFilterChange(props.field, "", currentOperator);
7789
- }
7790
- },
7791
- placeholder: `Filter by ${props.label}`,
7792
- class: "input input-bordered input-sm flex-1 min-w-0",
7793
- "data-testid": `datatable-filter-${props.field}-input`
7794
- }
7795
- )
7796
- ] });
7797
- };
7798
- var NumberFilter = (props) => {
7799
- const currentOperator = props.filterOperators.value[props.field] || "eq";
7800
- return /* @__PURE__ */ jsxs("div", { class: "flex gap-1.5 md:gap-2 w-full items-center", children: [
7801
9275
  /* @__PURE__ */ jsxs(
7802
9276
  "select",
7803
9277
  {
7804
- value: currentOperator,
9278
+ value: props.value === true || props.value === "true" ? "true" : props.value === false || props.value === "false" ? "false" : "",
7805
9279
  onChange: (e) => {
7806
- const newOperator = e.target.value;
7807
- props.filterOperators.value[props.field] = newOperator;
7808
- const currentValue = props.filterInputValues.value[props.field];
7809
- if (currentValue) {
7810
- props.onFilterChange(props.field, currentValue, newOperator);
9280
+ const stringValue = e.target.value;
9281
+ if (stringValue === "") {
9282
+ props.onFilterChange(props.field, { operator: currentOperator, value: "" });
9283
+ } else {
9284
+ const boolValue = stringValue === "true";
9285
+ const filter = {
9286
+ operator: currentOperator,
9287
+ value: boolValue
9288
+ };
9289
+ props.onFilterChange(props.field, filter);
7811
9290
  }
7812
9291
  },
7813
- class: "select select-bordered select-sm w-16 md:w-20 flex-shrink-0 text-xs font-medium",
7814
- "data-testid": `datatable-filter-${props.field}-operator`,
9292
+ class: "select select-bordered select-sm flex-1 min-w-0",
9293
+ disabled: props.isLoading,
9294
+ "data-testid": `datatable-filter-${props.field}-input`,
7815
9295
  children: [
7816
- /* @__PURE__ */ jsx("option", { value: "eq", children: "=" }),
7817
- /* @__PURE__ */ jsx("option", { value: "gt", children: ">" }),
7818
- /* @__PURE__ */ jsx("option", { value: "lt", children: "<" })
9296
+ props.showAllOption !== false && /* @__PURE__ */ jsx("option", { value: "", children: props.allOptionText ?? "All" }),
9297
+ /* @__PURE__ */ jsx("option", { value: "true", children: trueOption.label }),
9298
+ /* @__PURE__ */ jsx("option", { value: "false", children: falseOption.label })
7819
9299
  ]
7820
9300
  }
7821
- ),
7822
- /* @__PURE__ */ jsx(
7823
- "input",
7824
- {
7825
- type: "number",
7826
- value: props.filterInputValues.value[props.field] || "",
7827
- onInput: (e) => {
7828
- const value = e.target.value;
7829
- props.filterInputValues.value[props.field] = value;
7830
- debounce(
7831
- `filter_${props.field}`,
7832
- () => {
7833
- props.onFilterChange(props.field, value, currentOperator);
7834
- },
7835
- 300
7836
- );
7837
- },
7838
- onKeydown: (e) => {
7839
- if (e.key === "Escape" && props.filterInputValues.value[props.field]) {
7840
- e.preventDefault();
7841
- props.filterInputValues.value[props.field] = "";
7842
- props.onFilterChange(props.field, "", currentOperator);
7843
- }
7844
- },
7845
- placeholder: `Filter by ${props.label}`,
7846
- class: "input input-bordered input-sm flex-1 min-w-0",
7847
- "data-testid": `datatable-filter-${props.field}-input`
7848
- }
7849
9301
  )
7850
- ] });
7851
- };
7852
- var BooleanFilter = (props) => {
7853
- return /* @__PURE__ */ jsxs(
7854
- "select",
7855
- {
7856
- value: props.value || "",
7857
- onChange: (e) => {
7858
- const stringValue = e.target.value;
7859
- const value = stringValue === "true" ? true : stringValue === "false" ? false : stringValue;
7860
- props.onFilterChange(props.field, value, "eq");
7861
- },
7862
- class: "select select-bordered select-sm w-full",
7863
- disabled: props.isLoading,
7864
- "data-testid": `datatable-filter-${props.field}-input`,
7865
- children: [
7866
- props.showAllOption !== false && /* @__PURE__ */ jsx("option", { value: "", children: props.allOptionText ?? "All" }),
7867
- /* @__PURE__ */ jsx("option", { value: "true", children: "Yes" }),
7868
- /* @__PURE__ */ jsx("option", { value: "false", children: "No" })
7869
- ]
7870
- }
7871
- );
9302
+ ] }) });
7872
9303
  };
7873
9304
  var comboboxState = reactive({});
7874
- var DROPDOWN_MAX_HEIGHT = 240;
7875
- var MIN_SPACE_BELOW = 100;
9305
+ var DROPDOWN_MAX_HEIGHT2 = 240;
9306
+ var MIN_SPACE_BELOW2 = 100;
7876
9307
  var ComboboxFilter = (props) => {
7877
9308
  if (!comboboxState[props.field]) {
7878
9309
  comboboxState[props.field] = {
@@ -7888,7 +9319,7 @@ var ComboboxFilter = (props) => {
7888
9319
  const rect = input.getBoundingClientRect();
7889
9320
  const spaceBelow = window.innerHeight - rect.bottom;
7890
9321
  const spaceAbove = rect.top;
7891
- const openUpward = spaceBelow < MIN_SPACE_BELOW && spaceAbove > spaceBelow;
9322
+ const openUpward = spaceBelow < MIN_SPACE_BELOW2 && spaceAbove > spaceBelow;
7892
9323
  state.position = {
7893
9324
  top: openUpward ? rect.top - 4 : rect.bottom + 4,
7894
9325
  // 4px gap
@@ -7953,14 +9384,26 @@ var ComboboxFilter = (props) => {
7953
9384
  };
7954
9385
  const handleSelectOption = (option) => {
7955
9386
  props.filterInputValues.value[props.field] = option.label;
7956
- props.onFilterChange(props.field, option.value, "eq");
9387
+ const operator = props.filterOperators?.value[props.field] || (props.fieldType === "string" ? DEFAULT_OPERATORS.string : OPERATORS.EQUALS);
9388
+ const filter = {
9389
+ operator,
9390
+ value: option.value,
9391
+ caseSensitive: props.caseSensitive?.value[props.field] || false
9392
+ };
9393
+ props.onFilterChange(props.field, filter);
7957
9394
  state.isOpen = false;
7958
9395
  state.selectedIndex = -1;
7959
9396
  };
7960
9397
  const handleSelectNewValue = () => {
7961
9398
  const newValue = searchQuery.value.trim();
7962
9399
  if (newValue) {
7963
- props.onFilterChange(props.field, newValue, "eq");
9400
+ const operator = props.filterOperators?.value[props.field] || (props.fieldType === "string" ? DEFAULT_OPERATORS.string : OPERATORS.EQUALS);
9401
+ const filter = {
9402
+ operator,
9403
+ value: newValue,
9404
+ caseSensitive: props.caseSensitive?.value[props.field] || false
9405
+ };
9406
+ props.onFilterChange(props.field, filter);
7964
9407
  state.isOpen = false;
7965
9408
  state.selectedIndex = -1;
7966
9409
  }
@@ -7993,7 +9436,8 @@ var ComboboxFilter = (props) => {
7993
9436
  state.isOpen = false;
7994
9437
  state.selectedIndex = -1;
7995
9438
  if (props.value) {
7996
- props.onFilterChange(props.field, "", "eq");
9439
+ const operator = props.filterOperators?.value[props.field] || (props.fieldType === "string" ? DEFAULT_OPERATORS.string : OPERATORS.EQUALS);
9440
+ props.onFilterChange(props.field, { operator, value: "" });
7997
9441
  props.filterInputValues.value[props.field] = "";
7998
9442
  }
7999
9443
  return;
@@ -8103,7 +9547,8 @@ var ComboboxFilter = (props) => {
8103
9547
  const currentLabel = selectedOption.value;
8104
9548
  if (currentQuery === "") {
8105
9549
  if (currentValue) {
8106
- props.onFilterChange(props.field, "", "eq");
9550
+ const operator = props.filterOperators?.value[props.field] || (props.fieldType === "string" ? DEFAULT_OPERATORS.string : OPERATORS.EQUALS);
9551
+ props.onFilterChange(props.field, { operator, value: "" });
8107
9552
  props.filterInputValues.value[props.field] = "";
8108
9553
  }
8109
9554
  } else {
@@ -8119,9 +9564,23 @@ var ComboboxFilter = (props) => {
8119
9564
  if (!queryMatchesCurrentValue) {
8120
9565
  const matchingOption = props.options.find((opt) => opt.label.toLowerCase() === currentQuery.toLowerCase());
8121
9566
  if (matchingOption) {
8122
- props.onFilterChange(props.field, matchingOption.value, "eq");
9567
+ const operator = props.filterOperators?.value[props.field] || (props.fieldType === "string" ? DEFAULT_OPERATORS.string : OPERATORS.EQUALS);
9568
+ const filter = {
9569
+ operator,
9570
+ value: matchingOption.value,
9571
+ // Strings are always case-insensitive (contains is default)
9572
+ caseSensitive: false
9573
+ };
9574
+ props.onFilterChange(props.field, filter);
8123
9575
  } else if (props.allowCreate) {
8124
- props.onFilterChange(props.field, currentQuery, "eq");
9576
+ const operator = props.filterOperators?.value[props.field] || (props.fieldType === "string" ? DEFAULT_OPERATORS.string : OPERATORS.EQUALS);
9577
+ const filter = {
9578
+ operator,
9579
+ value: currentQuery,
9580
+ // Strings are always case-insensitive (contains is default)
9581
+ caseSensitive: false
9582
+ };
9583
+ props.onFilterChange(props.field, filter);
8125
9584
  } else {
8126
9585
  if (currentLabel) {
8127
9586
  props.filterInputValues.value[props.field] = currentLabel;
@@ -8142,7 +9601,7 @@ var ComboboxFilter = (props) => {
8142
9601
  bottom: pos.openUpward ? `${window.innerHeight - pos.top}px` : "auto",
8143
9602
  left: `${pos.left}px`,
8144
9603
  width: `${pos.width}px`,
8145
- maxHeight: `${DROPDOWN_MAX_HEIGHT}px`,
9604
+ maxHeight: `${DROPDOWN_MAX_HEIGHT2}px`,
8146
9605
  zIndex: 9999
8147
9606
  };
8148
9607
  });
@@ -8290,6 +9749,296 @@ var ComboboxFilter = (props) => {
8290
9749
  ) })
8291
9750
  ] });
8292
9751
  };
9752
+ function formatDateForInput(dateValue) {
9753
+ if (!dateValue) return "";
9754
+ try {
9755
+ const date = typeof dateValue === "string" ? new Date(dateValue) : dateValue;
9756
+ if (isNaN(date.getTime())) return "";
9757
+ const year = date.getFullYear();
9758
+ const month = String(date.getMonth() + 1).padStart(2, "0");
9759
+ const day = String(date.getDate()).padStart(2, "0");
9760
+ return `${year}-${month}-${day}`;
9761
+ } catch {
9762
+ return "";
9763
+ }
9764
+ }
9765
+ function parseDateValue(value) {
9766
+ if (!value) return "";
9767
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
9768
+ return formatDateForInput(value);
9769
+ }
9770
+ var DateFilter = (props) => {
9771
+ if (!props.filterOperators.value[props.field]) {
9772
+ props.filterOperators.value[props.field] = OPERATORS.EQUALS;
9773
+ }
9774
+ const currentOperator = props.filterOperators.value[props.field] || OPERATORS.EQUALS;
9775
+ const isBetween = currentOperator === OPERATORS.BETWEEN;
9776
+ const isNullCheck = currentOperator === OPERATORS.IS_EMPTY || currentOperator === OPERATORS.IS_NOT_EMPTY;
9777
+ const currentFilterValue = props.value?.value;
9778
+ const currentFilterValues = props.value?.values;
9779
+ if (props.value?.operator && props.value.operator !== currentOperator) {
9780
+ props.filterOperators.value[props.field] = props.value.operator;
9781
+ }
9782
+ const singleDateValue = parseDateValue(
9783
+ props.filterInputValues.value[props.field] || (currentFilterValue ? String(currentFilterValue) : "")
9784
+ );
9785
+ let startDateValue = "";
9786
+ let endDateValue = "";
9787
+ if (isBetween) {
9788
+ if (currentFilterValues && Array.isArray(currentFilterValues) && currentFilterValues.length === 2) {
9789
+ startDateValue = parseDateValue(String(currentFilterValues[0]));
9790
+ endDateValue = parseDateValue(String(currentFilterValues[1]));
9791
+ if (startDateValue && endDateValue) {
9792
+ props.filterInputValues.value[`${props.field}_start`] = startDateValue;
9793
+ props.filterInputValues.value[`${props.field}_end`] = endDateValue;
9794
+ }
9795
+ } else {
9796
+ const commaValue = props.filterInputValues.value[props.field];
9797
+ if (commaValue && typeof commaValue === "string" && commaValue.includes(",")) {
9798
+ const parts = commaValue.split(",").map((v) => v.trim());
9799
+ if (parts.length === 2) {
9800
+ startDateValue = parseDateValue(parts[0]);
9801
+ endDateValue = parseDateValue(parts[1]);
9802
+ props.filterInputValues.value[`${props.field}_start`] = startDateValue;
9803
+ props.filterInputValues.value[`${props.field}_end`] = endDateValue;
9804
+ }
9805
+ } else {
9806
+ startDateValue = parseDateValue(props.filterInputValues.value[`${props.field}_start`]);
9807
+ endDateValue = parseDateValue(props.filterInputValues.value[`${props.field}_end`]);
9808
+ }
9809
+ }
9810
+ }
9811
+ const handleFilterChange = (value, operator = currentOperator) => {
9812
+ const isNullCheckOp = operator === OPERATORS.IS_EMPTY || operator === OPERATORS.IS_NOT_EMPTY;
9813
+ if (!value && !isNullCheckOp) {
9814
+ if (props.onClearFilter) {
9815
+ props.onClearFilter(props.field);
9816
+ }
9817
+ return;
9818
+ }
9819
+ const filter = {
9820
+ operator
9821
+ };
9822
+ if (isNullCheckOp) ; else if (operator === OPERATORS.BETWEEN) {
9823
+ if (typeof value === "string" && value.includes(",")) {
9824
+ const parts = value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
9825
+ if (parts.length === 2) {
9826
+ const date1 = new Date(parts[0]);
9827
+ const date2 = new Date(parts[1]);
9828
+ if (!isNaN(date1.getTime()) && !isNaN(date2.getTime())) {
9829
+ filter.values = date1 <= date2 ? [parts[0], parts[1]] : [parts[1], parts[0]];
9830
+ } else {
9831
+ if (props.onClearFilter) {
9832
+ props.onClearFilter(props.field);
9833
+ }
9834
+ return;
9835
+ }
9836
+ } else {
9837
+ if (props.onClearFilter) {
9838
+ props.onClearFilter(props.field);
9839
+ }
9840
+ return;
9841
+ }
9842
+ } else if (Array.isArray(value) && value.length === 2) {
9843
+ const date1 = new Date(value[0]);
9844
+ const date2 = new Date(value[1]);
9845
+ if (!isNaN(date1.getTime()) && !isNaN(date2.getTime())) {
9846
+ filter.values = date1 <= date2 ? value : [value[1], value[0]];
9847
+ } else {
9848
+ if (props.onClearFilter) {
9849
+ props.onClearFilter(props.field);
9850
+ }
9851
+ return;
9852
+ }
9853
+ } else {
9854
+ if (props.onClearFilter) {
9855
+ props.onClearFilter(props.field);
9856
+ }
9857
+ return;
9858
+ }
9859
+ } else {
9860
+ if (value) {
9861
+ if (typeof value === "string") {
9862
+ const date = new Date(value);
9863
+ if (!isNaN(date.getTime())) {
9864
+ filter.value = value;
9865
+ } else {
9866
+ if (props.onClearFilter) {
9867
+ props.onClearFilter(props.field);
9868
+ }
9869
+ return;
9870
+ }
9871
+ } else if (value instanceof Date) {
9872
+ filter.value = value.toISOString();
9873
+ } else {
9874
+ if (props.onClearFilter) {
9875
+ props.onClearFilter(props.field);
9876
+ }
9877
+ return;
9878
+ }
9879
+ }
9880
+ }
9881
+ props.onFilterChange(props.field, filter);
9882
+ };
9883
+ const handleNullCheckToggle = (operator) => {
9884
+ props.filterOperators.value[props.field] = operator;
9885
+ handleFilterChange("", operator);
9886
+ };
9887
+ return /* @__PURE__ */ jsxs("div", { class: "flex flex-col gap-1.5 md:gap-2 w-full", children: [
9888
+ /* @__PURE__ */ jsxs("div", { class: "flex gap-1.5 md:gap-2 w-full items-center", children: [
9889
+ /* @__PURE__ */ jsxs(
9890
+ "select",
9891
+ {
9892
+ value: currentOperator,
9893
+ onChange: (e) => {
9894
+ const newOperator = e.target.value;
9895
+ props.filterOperators.value[props.field] = newOperator;
9896
+ const currentValue = props.filterInputValues.value[props.field];
9897
+ const isNewNullCheck = newOperator === OPERATORS.IS_EMPTY || newOperator === OPERATORS.IS_NOT_EMPTY;
9898
+ if (currentValue || isNewNullCheck) {
9899
+ handleFilterChange(currentValue, newOperator);
9900
+ } else if (!currentValue && props.onClearFilter) {
9901
+ props.onClearFilter(props.field);
9902
+ }
9903
+ },
9904
+ class: "select select-bordered select-sm w-20 md:w-24 flex-shrink-0 text-xs font-medium",
9905
+ "data-testid": `datatable-filter-${props.field}-operator`,
9906
+ children: [
9907
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.EQUALS, children: "eq" }),
9908
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.NOT_EQUALS, children: "ne" }),
9909
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.GREATER_THAN, children: "gt" }),
9910
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.GREATER_THAN_OR_EQUAL, children: "gte" }),
9911
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.LESS_THAN, children: "lt" }),
9912
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.LESS_THAN_OR_EQUAL, children: "lte" }),
9913
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.BETWEEN, children: "between" }),
9914
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.IS_EMPTY, children: "isEmpty" }),
9915
+ /* @__PURE__ */ jsx("option", { value: OPERATORS.IS_NOT_EMPTY, children: "isNotEmpty" })
9916
+ ]
9917
+ }
9918
+ ),
9919
+ !isBetween && /* @__PURE__ */ jsx(Fragment, { children: isNullCheck ? /* @__PURE__ */ jsxs("div", { class: "flex gap-2 items-center flex-1", children: [
9920
+ /* @__PURE__ */ jsx(
9921
+ "button",
9922
+ {
9923
+ onClick: () => handleNullCheckToggle(OPERATORS.IS_EMPTY),
9924
+ class: `btn btn-sm ${currentOperator === OPERATORS.IS_EMPTY ? "btn-primary" : "btn-outline"}`,
9925
+ "data-testid": `datatable-filter-${props.field}-empty`,
9926
+ children: "Empty"
9927
+ }
9928
+ ),
9929
+ /* @__PURE__ */ jsx(
9930
+ "button",
9931
+ {
9932
+ onClick: () => handleNullCheckToggle(OPERATORS.IS_NOT_EMPTY),
9933
+ class: `btn btn-sm ${currentOperator === OPERATORS.IS_NOT_EMPTY ? "btn-primary" : "btn-outline"}`,
9934
+ "data-testid": `datatable-filter-${props.field}-not-empty`,
9935
+ children: "Not Empty"
9936
+ }
9937
+ )
9938
+ ] }) : /* @__PURE__ */ jsx(
9939
+ "input",
9940
+ {
9941
+ type: "date",
9942
+ value: singleDateValue,
9943
+ onInput: (e) => {
9944
+ const value = e.target.value;
9945
+ props.filterInputValues.value[props.field] = value;
9946
+ if (!value) {
9947
+ if (props.onClearFilter) {
9948
+ props.onClearFilter(props.field);
9949
+ }
9950
+ return;
9951
+ }
9952
+ debounce(
9953
+ `filter_${props.field}`,
9954
+ () => {
9955
+ handleFilterChange(value);
9956
+ },
9957
+ 300
9958
+ );
9959
+ },
9960
+ onKeydown: (e) => {
9961
+ if (e.key === "Escape" && singleDateValue) {
9962
+ e.preventDefault();
9963
+ props.filterInputValues.value[props.field] = "";
9964
+ if (props.onClearFilter) {
9965
+ props.onClearFilter(props.field);
9966
+ }
9967
+ }
9968
+ },
9969
+ placeholder: `Filter by ${props.label}`,
9970
+ class: "input input-bordered input-sm flex-1 min-w-0",
9971
+ "data-testid": `datatable-filter-${props.field}-input`
9972
+ }
9973
+ ) })
9974
+ ] }),
9975
+ isBetween && /* @__PURE__ */ jsxs("div", { class: "flex gap-2 items-center w-full", children: [
9976
+ /* @__PURE__ */ jsx(
9977
+ "input",
9978
+ {
9979
+ type: "date",
9980
+ value: startDateValue,
9981
+ placeholder: "Start date",
9982
+ class: "input input-bordered input-sm flex-1 min-w-0",
9983
+ "data-testid": `datatable-filter-${props.field}-input-start`,
9984
+ onInput: (e) => {
9985
+ const startValue = e.target.value;
9986
+ props.filterInputValues.value[`${props.field}_start`] = startValue;
9987
+ const endValue = props.filterInputValues.value[`${props.field}_end`] || endDateValue;
9988
+ if (startValue && endValue) {
9989
+ props.filterInputValues.value[props.field] = `${startValue},${endValue}`;
9990
+ handleFilterChange(`${startValue},${endValue}`);
9991
+ } else if (!startValue && !endValue) {
9992
+ if (props.onClearFilter) {
9993
+ props.onClearFilter(props.field);
9994
+ }
9995
+ }
9996
+ }
9997
+ }
9998
+ ),
9999
+ /* @__PURE__ */ jsx("span", { class: "text-xs text-base-content/60", children: "to" }),
10000
+ /* @__PURE__ */ jsx(
10001
+ "input",
10002
+ {
10003
+ type: "date",
10004
+ value: endDateValue,
10005
+ placeholder: "End date",
10006
+ class: "input input-bordered input-sm flex-1 min-w-0",
10007
+ "data-testid": `datatable-filter-${props.field}-input-end`,
10008
+ onInput: (e) => {
10009
+ const endValue = e.target.value;
10010
+ props.filterInputValues.value[`${props.field}_end`] = endValue;
10011
+ const startValue = props.filterInputValues.value[`${props.field}_start`] || startDateValue;
10012
+ if (startValue && endValue) {
10013
+ props.filterInputValues.value[props.field] = `${startValue},${endValue}`;
10014
+ handleFilterChange(`${startValue},${endValue}`);
10015
+ } else if (!startValue && !endValue) {
10016
+ if (props.onClearFilter) {
10017
+ props.onClearFilter(props.field);
10018
+ }
10019
+ }
10020
+ }
10021
+ }
10022
+ )
10023
+ ] })
10024
+ ] });
10025
+ };
10026
+ function convertToDataType(metadataType) {
10027
+ switch (metadataType) {
10028
+ case "string":
10029
+ case "number":
10030
+ case "boolean":
10031
+ case "date":
10032
+ case "enum":
10033
+ return metadataType;
10034
+ case "array":
10035
+ case "object":
10036
+ case "union":
10037
+ return "string";
10038
+ default:
10039
+ return "string";
10040
+ }
10041
+ }
8293
10042
  var FilterDrawer = (props) => {
8294
10043
  const injectedFilterOptionsState = inject(
8295
10044
  ZINIA_DATA_TABLE_FILTER_OPTIONS_STATE_KEY,
@@ -8391,7 +10140,7 @@ var FilterDrawer = (props) => {
8391
10140
  SelectFilter,
8392
10141
  {
8393
10142
  field,
8394
- value: currentValue,
10143
+ value: Array.isArray(currentFilter?.values) ? currentFilter.values : currentValue,
8395
10144
  options: getFilterOptions(
8396
10145
  field,
8397
10146
  column,
@@ -8403,7 +10152,10 @@ var FilterDrawer = (props) => {
8403
10152
  isOptionsLoading: filterOptionsLoading?.value?.[field] ?? false,
8404
10153
  allOptionText: column.filterAllOptionText,
8405
10154
  showAllOption: column.filterShowAllOption,
8406
- onFilterChange: props.onFilterChange
10155
+ operator: currentFilter?.operator,
10156
+ fieldType: convertToDataType(props.fieldsMetadata[field]?.type || "enum"),
10157
+ onFilterChange: props.onFilterChange,
10158
+ onClearFilter: props.onClearFilter
8407
10159
  }
8408
10160
  ),
8409
10161
  filterType === "text" && /* @__PURE__ */ jsx(
@@ -8414,6 +10166,8 @@ var FilterDrawer = (props) => {
8414
10166
  label: column.label,
8415
10167
  filterInputValues: props.filterInputValues,
8416
10168
  filterOperators: props.filterOperators,
10169
+ caseSensitive: props.caseSensitive || { value: {} },
10170
+ fieldType: convertToDataType(props.fieldsMetadata[field]?.type || "string"),
8417
10171
  onFilterChange: props.onFilterChange
8418
10172
  }
8419
10173
  ),
@@ -8425,6 +10179,7 @@ var FilterDrawer = (props) => {
8425
10179
  label: column.label,
8426
10180
  filterInputValues: props.filterInputValues,
8427
10181
  filterOperators: props.filterOperators,
10182
+ fieldType: convertToDataType(props.fieldsMetadata[field]?.type || "number"),
8428
10183
  onFilterChange: props.onFilterChange
8429
10184
  }
8430
10185
  ),
@@ -8437,6 +10192,14 @@ var FilterDrawer = (props) => {
8437
10192
  isLoading: props.isLoading,
8438
10193
  allOptionText: column.filterAllOptionText,
8439
10194
  showAllOption: column.filterShowAllOption,
10195
+ operator: currentFilter?.operator,
10196
+ fieldType: convertToDataType(props.fieldsMetadata[field]?.type || "boolean"),
10197
+ options: getFilterOptions(
10198
+ field,
10199
+ column,
10200
+ props.fieldsMetadata,
10201
+ filterOptionsState?.value || {}
10202
+ ).map((opt) => ({ label: opt.label, value: opt.value })),
8440
10203
  onFilterChange: props.onFilterChange
8441
10204
  }
8442
10205
  ),
@@ -8456,8 +10219,24 @@ var FilterDrawer = (props) => {
8456
10219
  isOptionsLoading: filterOptionsLoading?.value?.[field] ?? false,
8457
10220
  allowCreate: column.filterAllowCreate ?? false,
8458
10221
  filterInputValues: props.filterInputValues,
10222
+ filterOperators: props.filterOperators,
10223
+ caseSensitive: props.caseSensitive || { value: {} },
10224
+ fieldType: convertToDataType(props.fieldsMetadata[field]?.type || "string"),
8459
10225
  onFilterChange: props.onFilterChange
8460
10226
  }
10227
+ ),
10228
+ filterType === "date" && /* @__PURE__ */ jsx(
10229
+ DateFilter,
10230
+ {
10231
+ field,
10232
+ value: currentFilter,
10233
+ label: column.label,
10234
+ filterInputValues: props.filterInputValues,
10235
+ filterOperators: props.filterOperators,
10236
+ fieldType: convertToDataType(props.fieldsMetadata[field]?.type || "date"),
10237
+ onFilterChange: props.onFilterChange,
10238
+ onClearFilter: props.onClearFilter
10239
+ }
8461
10240
  )
8462
10241
  ]
8463
10242
  },
@@ -8480,30 +10259,81 @@ var FilterDrawer = (props) => {
8480
10259
  }
8481
10260
  );
8482
10261
  };
8483
- function formatFilterValue(field, value, column, fieldsMetadata, filterOptionsState) {
10262
+ function formatFilterValue(field, filter, column, fieldsMetadata, filterOptionsState) {
10263
+ if (filter.operator === "isEmpty" || filter.operator === "isNotEmpty" || filter.operator === "isNull" || filter.operator === "isNotNull") {
10264
+ return filter.operator === "isEmpty" || filter.operator === "isNull" ? "Empty" : "Not Empty";
10265
+ }
10266
+ if (Array.isArray(filter.values) && filter.values.length > 0) {
10267
+ if (filter.operator === "between" && filter.values.length === 2) {
10268
+ return `${filter.values[0]} - ${filter.values[1]}`;
10269
+ }
10270
+ if (filter.operator === "in" || filter.operator === "notIn") {
10271
+ const options2 = getFilterOptions(field, column, fieldsMetadata, filterOptionsState);
10272
+ if (options2.length > 0) {
10273
+ const labels = filter.values.map((val) => {
10274
+ const option = options2.find((opt) => opt.value === val);
10275
+ return option ? option.label : String(val);
10276
+ });
10277
+ return labels.join(", ");
10278
+ }
10279
+ }
10280
+ return filter.values.join(", ");
10281
+ }
10282
+ const value = filter.value;
8484
10283
  if (value === "" || value === null || value === void 0) return "";
10284
+ const options = getFilterOptions(field, column, fieldsMetadata, filterOptionsState);
8485
10285
  if (typeof value === "boolean") {
10286
+ const option = options.find((opt) => opt.value === value);
10287
+ if (option) return option.label;
8486
10288
  return value ? "Yes" : "No";
8487
10289
  }
8488
- const options = getFilterOptions(field, column, fieldsMetadata, filterOptionsState);
8489
10290
  if (options.length > 0) {
8490
10291
  const option = options.find((opt) => opt.value === value);
8491
10292
  if (option) return option.label;
8492
10293
  }
8493
- if (value instanceof Date) {
8494
- return value.toLocaleDateString();
10294
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
10295
+ try {
10296
+ const date = new Date(value);
10297
+ if (!isNaN(date.getTime())) {
10298
+ return date.toLocaleDateString();
10299
+ }
10300
+ } catch {
10301
+ }
8495
10302
  }
8496
10303
  const strValue = String(value);
8497
10304
  return strValue.length > 20 ? strValue.substring(0, 20) + "..." : strValue;
8498
10305
  }
8499
10306
  function getOperatorLabel(operator) {
8500
10307
  const labels = {
8501
- eq: "=",
10308
+ // Shorthand operators (current standard)
10309
+ eq: "eq",
10310
+ ne: "ne",
8502
10311
  contains: "contains",
8503
- gt: ">",
8504
- lt: "<",
10312
+ sw: "sw",
10313
+ ew: "ew",
10314
+ gt: "gt",
10315
+ gte: "gte",
10316
+ lt: "lt",
10317
+ lte: "lte",
10318
+ between: "between",
8505
10319
  in: "in",
8506
- between: "between"
10320
+ notIn: "notIn",
10321
+ isEmpty: "isEmpty",
10322
+ isNotEmpty: "isNotEmpty",
10323
+ // Legacy aliases (map to new names for backward compatibility)
10324
+ isNull: "isEmpty",
10325
+ isNotNull: "isNotEmpty",
10326
+ // Legacy full names (map to shorthand for consistency)
10327
+ equals: "eq",
10328
+ notEquals: "ne",
10329
+ startsWith: "sw",
10330
+ endsWith: "ew",
10331
+ greaterThan: "gt",
10332
+ greaterThanOrEqual: "gte",
10333
+ lessThan: "lt",
10334
+ lessThanOrEqual: "lte",
10335
+ isOneOf: "in",
10336
+ isNotOneOf: "notIn"
8507
10337
  };
8508
10338
  return labels[operator] || operator;
8509
10339
  }
@@ -8511,12 +10341,18 @@ var FiltersRow = (props) => {
8511
10341
  const filterableColumns = Object.entries(props.columns).filter(([_, column]) => column?.filterable);
8512
10342
  const activeFilterBadges = filterableColumns.map(([field, column]) => {
8513
10343
  const filter = props.activeFilters[field];
8514
- if (!filter || filter.value === "" || filter.value === null || filter.value === void 0) {
10344
+ if (!filter) {
10345
+ return null;
10346
+ }
10347
+ const hasValue = filter.value !== void 0 && filter.value !== "" && filter.value !== null;
10348
+ const hasValues = Array.isArray(filter.values) && filter.values.length > 0;
10349
+ const isNullCheck = filter.operator === "isEmpty" || filter.operator === "isNotEmpty" || filter.operator === "isNull" || filter.operator === "isNotNull";
10350
+ if (!hasValue && !hasValues && !isNullCheck) {
8515
10351
  return null;
8516
10352
  }
8517
10353
  const displayValue = formatFilterValue(
8518
10354
  field,
8519
- filter.value,
10355
+ filter,
8520
10356
  column,
8521
10357
  props.fieldsMetadata,
8522
10358
  props.filterOptionsState?.value
@@ -8537,7 +10373,17 @@ var FiltersRow = (props) => {
8537
10373
  if (props.filterOperators.value[field]) {
8538
10374
  delete props.filterOperators.value[field];
8539
10375
  }
8540
- props.onFilterChange(field, "", "eq");
10376
+ if (props.onClearFilter) {
10377
+ props.onClearFilter(field);
10378
+ } else {
10379
+ const currentFilter = props.activeFilters[field];
10380
+ if (currentFilter) {
10381
+ props.onFilterChange(field, {
10382
+ operator: currentFilter.operator,
10383
+ value: ""
10384
+ });
10385
+ }
10386
+ }
8541
10387
  };
8542
10388
  return /* @__PURE__ */ jsxs("div", { class: "mb-4 space-y-2", children: [
8543
10389
  /* @__PURE__ */ jsxs("div", { class: "flex items-center gap-2", children: [
@@ -8592,7 +10438,7 @@ var FiltersRow = (props) => {
8592
10438
  badge.label,
8593
10439
  ":"
8594
10440
  ] }),
8595
- badge.operator !== "eq" && badge.operator !== "contains" && /* @__PURE__ */ jsx("span", { class: "opacity-70", children: badge.operatorLabel }),
10441
+ /* @__PURE__ */ jsx("span", { class: "opacity-70", children: badge.operatorLabel }),
8596
10442
  /* @__PURE__ */ jsx("span", { children: badge.displayValue }),
8597
10443
  /* @__PURE__ */ jsx(
8598
10444
  "button",
@@ -9056,9 +10902,8 @@ function createDaisyUIDataTable() {
9056
10902
  const actions = inject(ZINIA_DATA_TABLE_ACTIONS_KEY);
9057
10903
  const searchInputValue = inject(ZINIA_DATA_TABLE_SEARCH_INPUT_KEY);
9058
10904
  const filterInputValues = inject(ZINIA_DATA_TABLE_FILTER_INPUTS_KEY);
9059
- const filterOperators = inject(
9060
- ZINIA_DATA_TABLE_FILTER_OPERATORS_KEY
9061
- );
10905
+ const filterOperators = inject(ZINIA_DATA_TABLE_FILTER_OPERATORS_KEY);
10906
+ const filterCaseSensitive = inject(ZINIA_DATA_TABLE_FILTER_CASE_SENSITIVE_KEY, ref({}));
9062
10907
  const filterOptionsState = inject(ZINIA_DATA_TABLE_FILTER_OPTIONS_STATE_KEY);
9063
10908
  const filterOptionsLoading = inject(ZINIA_DATA_TABLE_FILTER_OPTIONS_LOADING_KEY);
9064
10909
  const tableName = inject(ZINIA_DATA_TABLE_NAME_KEY);
@@ -9114,11 +10959,10 @@ function createDaisyUIDataTable() {
9114
10959
  const currentFilter = table.filters.active[field];
9115
10960
  filterInputValues.value[field] = currentFilter?.value || "";
9116
10961
  if (filterOperators) {
9117
- if (filterType === "text") {
9118
- filterOperators.value[field] = currentFilter?.operator || "contains";
9119
- } else if (filterType === "number") {
9120
- filterOperators.value[field] = currentFilter?.operator || "eq";
9121
- }
10962
+ const fieldMetadata = table.fieldsMetadata[field];
10963
+ const fieldType = fieldMetadata?.type;
10964
+ const defaultOperator = fieldType ? DEFAULT_OPERATORS[fieldType] || OPERATORS.EQUALS : filterType === "text" ? OPERATORS.CONTAINS : OPERATORS.EQUALS;
10965
+ filterOperators.value[field] = currentFilter?.operator || defaultOperator;
9122
10966
  }
9123
10967
  }
9124
10968
  }
@@ -9171,7 +11015,8 @@ function createDaisyUIDataTable() {
9171
11015
  isLoading: table.isLoading,
9172
11016
  filterCount: table.filters.count,
9173
11017
  filterDrawerOpen: table.ui.filterDrawerOpen,
9174
- onFilterChange: (field, value, operator) => table.setFilter(field, value, operator),
11018
+ onFilterChange: (field, filter) => table.setFilter(field, filter),
11019
+ onClearFilter: (field) => table.clearFilter(field),
9175
11020
  onClearAllFilters: () => table.clearAllFilters(),
9176
11021
  onRefresh: () => table.refresh(),
9177
11022
  onOpenFilterDrawer: () => table.ui.openFilterDrawer(),
@@ -9291,7 +11136,9 @@ function createDaisyUIDataTable() {
9291
11136
  isLoading: table.isLoading,
9292
11137
  tableName,
9293
11138
  onClose: () => table.ui.closeFilterDrawer(),
9294
- onFilterChange: (field, value, operator) => table.setFilter(field, value, operator),
11139
+ caseSensitive: filterCaseSensitive,
11140
+ onFilterChange: (field, filter) => table.setFilter(field, filter),
11141
+ onClearFilter: (field) => table.clearFilter(field),
9295
11142
  onClearAllFilters: () => table.clearAllFilters()
9296
11143
  }
9297
11144
  )
@@ -9403,6 +11250,7 @@ function useCursorDataTableState(initialData = [], options = {}) {
9403
11250
  field: null,
9404
11251
  direction: "asc"
9405
11252
  });
11253
+ const fieldRegistry = ref(null);
9406
11254
  const filters = ref({});
9407
11255
  const search = reactive({
9408
11256
  query: "",
@@ -9480,6 +11328,9 @@ function useCursorDataTableState(initialData = [], options = {}) {
9480
11328
  filters.value = newFilters;
9481
11329
  pagination.currentPageNumber = 0;
9482
11330
  };
11331
+ const setFieldRegistry = (registry) => {
11332
+ fieldRegistry.value = registry;
11333
+ };
9483
11334
  const setSearch = (query, searchableFields) => {
9484
11335
  search.query = query;
9485
11336
  if (searchableFields) {
@@ -9538,6 +11389,7 @@ function useCursorDataTableState(initialData = [], options = {}) {
9538
11389
  pagination,
9539
11390
  sorting,
9540
11391
  filters,
11392
+ fieldRegistry,
9541
11393
  search,
9542
11394
  selection,
9543
11395
  // UI state
@@ -9562,6 +11414,7 @@ function useCursorDataTableState(initialData = [], options = {}) {
9562
11414
  setPageSize,
9563
11415
  setSorting,
9564
11416
  setFilters,
11417
+ setFieldRegistry,
9565
11418
  setSearch,
9566
11419
  // Selection methods
9567
11420
  selectRow,
@@ -9577,12 +11430,15 @@ function useCursorDataTable(schema, options) {
9577
11430
  schemaId: options.schemaId,
9578
11431
  storeName: "dataTable"
9579
11432
  });
11433
+ const fieldRegistry = buildFieldRegistryFromSchema(schema, fieldsMetadata);
9580
11434
  const tableState = useCursorDataTableState([], {
9581
11435
  debug: options.debug
9582
11436
  });
11437
+ tableState.setFieldRegistry(fieldRegistry);
9583
11438
  const searchInputValue = ref("");
9584
11439
  const filterInputValues = ref({});
9585
11440
  const filterOperators = ref({});
11441
+ const filterCaseSensitive = ref({});
9586
11442
  const filterOptionsState = ref({});
9587
11443
  const filterOptionsLoading = ref({});
9588
11444
  if (options.pagination?.pageSize) {
@@ -9599,15 +11455,20 @@ function useCursorDataTable(schema, options) {
9599
11455
  Object.entries(options.initialFilters).forEach(([field, filter]) => {
9600
11456
  if (filter && filter.value !== "" && filter.value !== null && filter.value !== void 0) {
9601
11457
  filterInputValues.value[field] = String(filter.value);
9602
- filterOperators.value[field] = filter.operator || "eq";
11458
+ const fieldMetadata = fieldRegistry[field];
11459
+ const defaultOperator = fieldMetadata ? DEFAULT_OPERATORS[fieldMetadata.type] || OPERATORS.EQUALS : OPERATORS.EQUALS;
11460
+ filterOperators.value[field] = filter.operator || defaultOperator;
9603
11461
  }
9604
11462
  });
9605
11463
  }
9606
11464
  const extractActiveFilters = (filterState) => {
9607
11465
  const activeFilters = {};
9608
- Object.entries(filterState).forEach(([key, filter]) => {
9609
- if (filter && filter.value !== "" && filter.value !== null && filter.value !== void 0) {
9610
- activeFilters[key] = filter;
11466
+ Object.entries(filterState).forEach(([fieldName, filter]) => {
11467
+ const hasValue = filter.value !== void 0 && filter.value !== "" && filter.value !== null;
11468
+ const hasValues = filter.values !== void 0 && Array.isArray(filter.values) && filter.values.length > 0;
11469
+ const isNullCheck = filter.operator === "isEmpty" || filter.operator === "isNotEmpty";
11470
+ if (hasValue || hasValues || isNullCheck) {
11471
+ activeFilters[fieldName] = filter;
9611
11472
  }
9612
11473
  });
9613
11474
  return activeFilters;
@@ -9637,6 +11498,16 @@ function useCursorDataTable(schema, options) {
9637
11498
  try {
9638
11499
  tableState.setLoading(true);
9639
11500
  tableState.setError(null);
11501
+ const activeFilters = extractActiveFilters(tableState.state.filters.value);
11502
+ const validation = validateFilterConfiguration(activeFilters, fieldRegistry);
11503
+ if (!validation.valid) {
11504
+ const errorMessage = `Filter validation failed: ${validation.errors.map((e) => e.message).join(", ")}`;
11505
+ tableState.setError(errorMessage);
11506
+ if (options.debug) {
11507
+ console.error("\u274C Filter validation errors:", validation.errors);
11508
+ }
11509
+ throw new Error(errorMessage);
11510
+ }
9640
11511
  if (options.debug) {
9641
11512
  console.log("\u{1F504} Fetching cursor table data with params:", {
9642
11513
  cursor: tableState.state.pagination.currentCursor,
@@ -9645,7 +11516,7 @@ function useCursorDataTable(schema, options) {
9645
11516
  field: tableState.state.sorting.field,
9646
11517
  direction: tableState.state.sorting.direction
9647
11518
  } : null,
9648
- filters: extractActiveFilters(tableState.state.filters.value),
11519
+ filters: activeFilters,
9649
11520
  search: {
9650
11521
  query: tableState.state.search.query,
9651
11522
  searchableFields: tableState.state.search.searchableFields
@@ -9659,7 +11530,7 @@ function useCursorDataTable(schema, options) {
9659
11530
  field: tableState.state.sorting.field,
9660
11531
  direction: tableState.state.sorting.direction
9661
11532
  } : null,
9662
- filters: extractActiveFilters(tableState.state.filters.value),
11533
+ filters: activeFilters,
9663
11534
  search: {
9664
11535
  query: tableState.state.search.query,
9665
11536
  searchableFields: tableState.state.search.searchableFields
@@ -9700,13 +11571,29 @@ function useCursorDataTable(schema, options) {
9700
11571
  tableState.setCursorNavigation(void 0);
9701
11572
  await fetchData();
9702
11573
  };
9703
- const setFilter = async (field, value, operator = "eq") => {
11574
+ const setFilter = async (field, filter) => {
9704
11575
  const currentFilters = { ...tableState.state.filters.value };
9705
- if (value === "" || value === null || value === void 0) {
9706
- delete currentFilters[field];
9707
- } else {
9708
- currentFilters[field] = { value, operator };
11576
+ const fieldName = field;
11577
+ const hasValue = filter.value !== void 0 && filter.value !== "" && filter.value !== null;
11578
+ const hasValues = filter.values !== void 0 && Array.isArray(filter.values) && filter.values.length > 0;
11579
+ const isNullCheck = filter.operator === "isEmpty" || filter.operator === "isNotEmpty";
11580
+ const isEmpty = !hasValue && !hasValues && !isNullCheck;
11581
+ if (isEmpty) {
11582
+ delete currentFilters[fieldName];
11583
+ tableState.setFilters(currentFilters);
11584
+ tableState.setCursorNavigation(void 0);
11585
+ return;
11586
+ }
11587
+ const validation = validateFilterValueObject(fieldName, filter, fieldRegistry);
11588
+ if (!validation.valid) {
11589
+ const errorMessage = `Invalid filter for field "${fieldName}": ${validation.errors.map((e) => e.message).join(", ")}`;
11590
+ tableState.setError(errorMessage);
11591
+ if (options.debug) {
11592
+ console.error("\u274C Filter validation errors:", validation.errors);
11593
+ }
11594
+ throw new Error(errorMessage);
9709
11595
  }
11596
+ currentFilters[fieldName] = filter;
9710
11597
  tableState.setFilters(currentFilters);
9711
11598
  tableState.setCursorNavigation(void 0);
9712
11599
  await fetchData();
@@ -9716,13 +11603,39 @@ function useCursorDataTable(schema, options) {
9716
11603
  delete currentFilters[field];
9717
11604
  tableState.setFilters(currentFilters);
9718
11605
  tableState.setCursorNavigation(void 0);
11606
+ delete filterInputValues.value[field];
11607
+ delete filterInputValues.value[`${field}_start`];
11608
+ delete filterInputValues.value[`${field}_end`];
11609
+ delete filterOperators.value[field];
11610
+ delete filterCaseSensitive.value[field];
9719
11611
  await fetchData();
9720
11612
  };
9721
11613
  const clearAllFilters = async () => {
9722
11614
  tableState.setFilters({});
9723
11615
  tableState.setCursorNavigation(void 0);
11616
+ filterInputValues.value = {};
11617
+ filterOperators.value = {};
11618
+ filterCaseSensitive.value = {};
9724
11619
  await fetchData();
9725
11620
  };
11621
+ const setFiltersFromQueryParams = async (params) => {
11622
+ const filters = deserializeFiltersFromQueryParams(params, fieldRegistry);
11623
+ tableState.setFilters(filters);
11624
+ tableState.setCursorNavigation(void 0);
11625
+ await fetchData();
11626
+ };
11627
+ const setFiltersFromQueryString = async (queryString) => {
11628
+ if (queryString && queryString.trim() !== "") {
11629
+ const params = new URLSearchParams(queryString);
11630
+ const paramsObj = {};
11631
+ params.forEach((value, key) => {
11632
+ paramsObj[key] = value;
11633
+ });
11634
+ await setFiltersFromQueryParams(paramsObj);
11635
+ } else {
11636
+ await setFiltersFromQueryParams({});
11637
+ }
11638
+ };
9726
11639
  const setSearch = async (query) => {
9727
11640
  tableState.setSearch(query);
9728
11641
  tableState.setCursorNavigation(void 0);
@@ -9861,8 +11774,20 @@ function useCursorDataTable(schema, options) {
9861
11774
  },
9862
11775
  get count() {
9863
11776
  return Object.keys(extractActiveFilters(tableState.state.filters.value)).length;
11777
+ },
11778
+ get queryParams() {
11779
+ const activeFilters = extractActiveFilters(tableState.state.filters.value);
11780
+ return serializeFiltersToQueryParams(activeFilters, fieldRegistry);
11781
+ },
11782
+ // Legacy: queryString for backward compatibility (returns empty string, use queryParams instead)
11783
+ get queryString() {
11784
+ return "";
9864
11785
  }
9865
11786
  },
11787
+ // Field registry
11788
+ get fieldRegistry() {
11789
+ return fieldRegistry;
11790
+ },
9866
11791
  // Search (same interface)
9867
11792
  search: {
9868
11793
  get query() {
@@ -9895,6 +11820,8 @@ function useCursorDataTable(schema, options) {
9895
11820
  setFilter,
9896
11821
  clearFilter,
9897
11822
  clearAllFilters,
11823
+ setFiltersFromQueryParams,
11824
+ setFiltersFromQueryString,
9898
11825
  setSearch,
9899
11826
  clearSearch,
9900
11827
  nextPage,
@@ -9928,6 +11855,7 @@ function useCursorDataTable(schema, options) {
9928
11855
  provide(ZINIA_DATA_TABLE_SEARCH_INPUT_KEY, searchInputValue);
9929
11856
  provide(ZINIA_DATA_TABLE_FILTER_INPUTS_KEY, filterInputValues);
9930
11857
  provide(ZINIA_DATA_TABLE_FILTER_OPERATORS_KEY, filterOperators);
11858
+ provide(ZINIA_DATA_TABLE_FILTER_CASE_SENSITIVE_KEY, filterCaseSensitive);
9931
11859
  provide(ZINIA_DATA_TABLE_FILTER_OPTIONS_STATE_KEY, filterOptionsState);
9932
11860
  provide(ZINIA_DATA_TABLE_FILTER_OPTIONS_LOADING_KEY, filterOptionsLoading);
9933
11861
  provide(ZINIA_DATA_TABLE_NAME_KEY, options.name || "datatable");
@@ -9937,11 +11865,13 @@ function useCursorDataTable(schema, options) {
9937
11865
  console.error("Failed to load filter options:", err);
9938
11866
  }
9939
11867
  });
9940
- fetchData().catch((err) => {
9941
- if (options.debug) {
9942
- console.error("Failed to auto-load cursor table data:", err);
9943
- }
9944
- });
11868
+ if (options.autoLoad !== false) {
11869
+ fetchData().catch((err) => {
11870
+ if (options.debug) {
11871
+ console.error("Failed to auto-load cursor table data:", err);
11872
+ }
11873
+ });
11874
+ }
9945
11875
  return {
9946
11876
  table,
9947
11877
  // Components
@@ -9954,6 +11884,7 @@ function useCursorDataTable(schema, options) {
9954
11884
  setFilter,
9955
11885
  clearFilter,
9956
11886
  clearAllFilters,
11887
+ setFiltersFromQueryString,
9957
11888
  setSearch,
9958
11889
  clearSearch,
9959
11890
  nextPage,
@@ -9966,7 +11897,79 @@ function useCursorDataTable(schema, options) {
9966
11897
  isRowSelected
9967
11898
  };
9968
11899
  }
11900
+ function useDataTableUrlSync(table, router, options = {}) {
11901
+ const { loadOnMount = true } = options;
11902
+ const isUpdatingFromRoute = ref(false);
11903
+ const originalSetFilter = table.setFilter.bind(table);
11904
+ const originalClearAllFilters = table.clearAllFilters.bind(table);
11905
+ const extractFilterParams = (query) => {
11906
+ const filterParams = {};
11907
+ Object.keys(query).forEach((key) => {
11908
+ if (key.startsWith("flt[")) {
11909
+ filterParams[key] = query[key];
11910
+ }
11911
+ });
11912
+ return filterParams;
11913
+ };
11914
+ const updateUrlFromFilters = () => {
11915
+ if (isUpdatingFromRoute.value) return;
11916
+ const queryParams = table.filters.queryParams || {};
11917
+ const query = { ...router.currentRoute.value.query };
11918
+ Object.keys(query).forEach((key) => {
11919
+ if (key.startsWith("flt[")) delete query[key];
11920
+ });
11921
+ if (Object.keys(queryParams).length > 0) {
11922
+ Object.assign(query, queryParams);
11923
+ }
11924
+ router.push({ query });
11925
+ };
11926
+ table.setFilter = async (field, filter) => {
11927
+ await originalSetFilter(field, filter);
11928
+ updateUrlFromFilters();
11929
+ };
11930
+ table.clearAllFilters = async () => {
11931
+ await originalClearAllFilters();
11932
+ updateUrlFromFilters();
11933
+ };
11934
+ watch(
11935
+ () => router.currentRoute.value.query,
11936
+ async (newQuery) => {
11937
+ if (isUpdatingFromRoute.value) return;
11938
+ const filterParams = extractFilterParams(newQuery);
11939
+ const currentParams = table.filters.queryParams || {};
11940
+ const paramsMatch = Object.keys(filterParams).length === Object.keys(currentParams).length && Object.keys(filterParams).every((key) => filterParams[key] === currentParams[key]);
11941
+ if (paramsMatch) return;
11942
+ isUpdatingFromRoute.value = true;
11943
+ try {
11944
+ if (Object.keys(filterParams).length > 0) {
11945
+ await table.setFiltersFromQueryParams(filterParams);
11946
+ } else if (Object.keys(currentParams).length > 0) {
11947
+ await originalClearAllFilters();
11948
+ }
11949
+ } finally {
11950
+ await nextTick();
11951
+ isUpdatingFromRoute.value = false;
11952
+ }
11953
+ },
11954
+ { deep: true }
11955
+ );
11956
+ if (loadOnMount) {
11957
+ onMounted(async () => {
11958
+ const filterParams = extractFilterParams(router.currentRoute.value.query);
11959
+ if (Object.keys(filterParams).length > 0) {
11960
+ try {
11961
+ await table.setFiltersFromQueryParams(filterParams);
11962
+ } catch (error) {
11963
+ console.error("Failed to load filters from URL:", error);
11964
+ await table.load();
11965
+ }
11966
+ } else {
11967
+ await table.load();
11968
+ }
11969
+ });
11970
+ }
11971
+ }
9969
11972
 
9970
- export { ActionIcons, COMBOBOX_FIELD_PROP_NAMES, DEBUG_CONFIG, ErrorDisplay, LoadingDisplay, NoDataDisplay, SCHEMA_ID_SYMBOL, SELECT_FIELD_PROP_NAMES, ZINIA_DATA_TABLE_ACTIONS_KEY, ZINIA_DATA_TABLE_COLUMNS_KEY, ZINIA_DATA_TABLE_FILTER_INPUTS_KEY, ZINIA_DATA_TABLE_FILTER_OPERATORS_KEY, ZINIA_DATA_TABLE_FILTER_OPTIONS_LOADING_KEY, ZINIA_DATA_TABLE_FILTER_OPTIONS_STATE_KEY, ZINIA_DATA_TABLE_KEY, ZINIA_DATA_TABLE_NAME_KEY, ZINIA_DATA_TABLE_OPTIONS_KEY, ZINIA_DATA_TABLE_SEARCH_INPUT_KEY, ZINIA_DELETE_MODAL_FIELDS_GENERIC_KEY, ZINIA_DELETE_MODAL_FIELDS_KEY, ZINIA_DELETE_MODAL_KEY, ZINIA_DELETE_MODAL_SCHEMA_KEY, ZINIA_DISPLAY_FIELDS_GENERIC_KEY, ZINIA_DISPLAY_FIELDS_KEY, ZINIA_DISPLAY_KEY, ZINIA_DISPLAY_SCHEMA_KEY, ZINIA_FIELDS_GENERIC_KEY, ZINIA_FIELDS_KEY, ZINIA_FORM_KEY, ZINIA_FORM_SCHEMA_KEY, ZiniaPlugin, clearAllMetadata, clearSchemaMetadata, createBaseComponents, createBaseDisplayComponents, createPartialStyle, createStyleTemplate, createTypedArrayField, createTypedComboboxField, createTypedSelectField, daisyUIStyle, extendStyle, generateDisplayComponents, generateFieldComponents, getAllSchemaMetadata, getDefaultStyle, getFieldMetadata, getRegisteredStyle, getRegisteredStyleNames, getSchemaId, hasRegisteredStyle, hasSchemaMetadata, isDebugEnabled, mergeStyles, registerSchemaMetadata, registerStyle, setSchemaMetadata, useCursorDataTable, useDataTable, useDeleteModal, useDisplay, useForm, withMetadata };
11973
+ export { ActionIcons, COMBOBOX_FIELD_PROP_NAMES, DEBUG_CONFIG, ErrorDisplay, LoadingDisplay, NoDataDisplay, SCHEMA_ID_SYMBOL, SELECT_FIELD_PROP_NAMES, ZINIA_DATA_TABLE_ACTIONS_KEY, ZINIA_DATA_TABLE_COLUMNS_KEY, ZINIA_DATA_TABLE_FILTER_CASE_SENSITIVE_KEY, ZINIA_DATA_TABLE_FILTER_INPUTS_KEY, ZINIA_DATA_TABLE_FILTER_OPERATORS_KEY, ZINIA_DATA_TABLE_FILTER_OPTIONS_LOADING_KEY, ZINIA_DATA_TABLE_FILTER_OPTIONS_STATE_KEY, ZINIA_DATA_TABLE_KEY, ZINIA_DATA_TABLE_NAME_KEY, ZINIA_DATA_TABLE_OPTIONS_KEY, ZINIA_DATA_TABLE_SEARCH_INPUT_KEY, ZINIA_DELETE_MODAL_FIELDS_GENERIC_KEY, ZINIA_DELETE_MODAL_FIELDS_KEY, ZINIA_DELETE_MODAL_KEY, ZINIA_DELETE_MODAL_SCHEMA_KEY, ZINIA_DISPLAY_FIELDS_GENERIC_KEY, ZINIA_DISPLAY_FIELDS_KEY, ZINIA_DISPLAY_KEY, ZINIA_DISPLAY_SCHEMA_KEY, ZINIA_FIELDS_GENERIC_KEY, ZINIA_FIELDS_KEY, ZINIA_FORM_KEY, ZINIA_FORM_SCHEMA_KEY, ZiniaPlugin, buildFieldRegistryFromSchema, clearAllMetadata, clearSchemaMetadata, createBaseComponents, createBaseDisplayComponents, createPartialStyle, createStyleTemplate, createTypedArrayField, createTypedComboboxField, createTypedSelectField, daisyUIStyle, deserializeFiltersFromQueryString, extendStyle, generateDisplayComponents, generateFieldComponents, getAllSchemaMetadata, getDefaultStyle, getFieldMetadata, getRegisteredStyle, getRegisteredStyleNames, getSchemaId, hasRegisteredStyle, hasSchemaMetadata, isDebugEnabled, isValidOperatorForType, mergeStyles, registerSchemaMetadata, registerStyle, serializeFiltersToQueryString, setSchemaMetadata, useCursorDataTable, useDataTable, useDataTableUrlSync, useDeleteModal, useDisplay, useForm, validateFieldRegistry, validateFilterConfiguration, withMetadata };
9971
11974
  //# sourceMappingURL=index.js.map
9972
11975
  //# sourceMappingURL=index.js.map