@stndrds/schema 0.1.0-alpha.32 → 0.1.0-alpha.34

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.
@@ -4559,14 +4559,6 @@ var RollupAttributeBuilder = class extends BaseAttributeBuilder {
4559
4559
  this.attr.decimals = value;
4560
4560
  return this;
4561
4561
  }
4562
- /**
4563
- * Display the rollup value using the target attribute's type formatter
4564
- * instead of the default rollup-specific formatting
4565
- */
4566
- showAsOriginal() {
4567
- this.attr.showAsOriginal = true;
4568
- return this;
4569
- }
4570
4562
  /**
4571
4563
  * Set multi-level path for traversing nested relations (Phase 4+)
4572
4564
  * @param path - Dot notation path (e.g., "orders.items")
@@ -4575,6 +4567,24 @@ var RollupAttributeBuilder = class extends BaseAttributeBuilder {
4575
4567
  this.attr.relationPath = relationPath;
4576
4568
  return this;
4577
4569
  }
4570
+ /**
4571
+ * Set the target attribute type for display purposes
4572
+ * Required when using function="original" to render values correctly
4573
+ * @param type - The type of the target attribute (e.g., "select", "status", "text")
4574
+ */
4575
+ targetType(type) {
4576
+ this.attr.targetAttributeType = type;
4577
+ return this;
4578
+ }
4579
+ /**
4580
+ * Set the target attribute options for select-like types
4581
+ * Required when function="original" and target is select/status/multiselect
4582
+ * @param options - The options array from the target attribute
4583
+ */
4584
+ targetOptions(options) {
4585
+ this.attr.targetAttributeOptions = options;
4586
+ return this;
4587
+ }
4578
4588
  // Override to prevent making rollups required
4579
4589
  // biome-ignore lint/suspicious/noExplicitAny: intentional override
4580
4590
  required() {
@@ -5529,7 +5539,7 @@ var ViewBuilder = class {
5529
5539
  return this;
5530
5540
  }
5531
5541
  /**
5532
- * Mark as default view for the object
5542
+ * Mark as default view for the object (within its layout)
5533
5543
  */
5534
5544
  default() {
5535
5545
  this.data.default = true;
@@ -5542,6 +5552,23 @@ var ViewBuilder = class {
5542
5552
  this.data.system = true;
5543
5553
  return this;
5544
5554
  }
5555
+ /**
5556
+ * Set the layout mode for the view
5557
+ * @param value - "page" for full tabs, "modal" for single form
5558
+ * @default "page"
5559
+ */
5560
+ layout(value) {
5561
+ this.data.layout = value;
5562
+ return this;
5563
+ }
5564
+ /**
5565
+ * Shorthand for .layout("modal")
5566
+ * Modal views have a single form tab without tabs UI
5567
+ */
5568
+ modal() {
5569
+ this.data.layout = "modal";
5570
+ return this;
5571
+ }
5545
5572
  /**
5546
5573
  * Set metadata
5547
5574
  */
@@ -5586,6 +5613,14 @@ var ViewBuilder = class {
5586
5613
  }
5587
5614
  tabNames.add(tab.name);
5588
5615
  }
5616
+ if (this.data.layout === "modal") {
5617
+ if (this.data.tabs.length !== 1) {
5618
+ throw new Error("[ViewBuilder] Modal views must have exactly one tab");
5619
+ }
5620
+ if (this.data.tabs[0].type !== "form") {
5621
+ throw new Error("[ViewBuilder] Modal views must have a form tab");
5622
+ }
5623
+ }
5589
5624
  this.validated = true;
5590
5625
  return this.data;
5591
5626
  }
@@ -5706,6 +5741,25 @@ function isSystemAttributeObject(attr) {
5706
5741
 
5707
5742
  // src/validation/validators.ts
5708
5743
  import { z as z4 } from "zod";
5744
+ var DEFAULT_VALIDATION_MESSAGES = {
5745
+ required: (attr) => `${attr.label} is required`,
5746
+ invalidType: (attr, expected) => `${attr.label} must be a ${expected}`,
5747
+ minLength: (attr, min) => `${attr.label} must be at least ${min} characters`,
5748
+ maxLength: (attr, max) => `${attr.label} must be at most ${max} characters`,
5749
+ invalidPattern: (attr) => `${attr.label} format is invalid`,
5750
+ minValue: (attr, min) => `${attr.label} must be at least ${min}`,
5751
+ maxValue: (attr, max) => `${attr.label} must be at most ${max}`,
5752
+ mustBeInteger: (attr) => `${attr.label} must be an integer`,
5753
+ invalidDate: (attr) => `${attr.label} must be a valid date`,
5754
+ invalidOption: (attr, options) => `${attr.label} must be one of: ${options.join(", ")}`,
5755
+ invalidId: (attr) => `${attr.label} must be a valid ID`,
5756
+ minItems: (attr, min) => `${attr.label} must have at least ${min} item${min > 1 ? "s" : ""}`,
5757
+ maxItems: (attr, max) => `${attr.label} must have at most ${max} item${max > 1 ? "s" : ""}`,
5758
+ invalidRichtext: (attr) => `${attr.label} must be valid rich text content`,
5759
+ invalidPhone: (attr) => `${attr.label} must be a valid phone number`,
5760
+ invalidCurrency: (attr) => `${attr.label} must be a valid currency value`,
5761
+ invalidLocation: (attr) => `${attr.label} must be a valid location`
5762
+ };
5709
5763
  var baseConfigSchema = z4.object({
5710
5764
  disabled: z4.boolean().optional(),
5711
5765
  placeholder: z4.string().optional(),
@@ -5830,10 +5884,11 @@ var rollupConfigSchema = baseConfigSchema.extend({
5830
5884
  "countEmpty",
5831
5885
  // Percent (universal)
5832
5886
  "percentEmpty",
5833
- "percentNotEmpty"
5887
+ "percentNotEmpty",
5888
+ // Lookup (universal)
5889
+ "original"
5834
5890
  ]),
5835
5891
  decimals: z4.number().int().min(0).max(10).optional(),
5836
- showAsOriginal: z4.boolean().optional(),
5837
5892
  targetAttributeType: z4.string().optional(),
5838
5893
  targetAttributeOptions: z4.array(
5839
5894
  z4.object({
@@ -5890,158 +5945,152 @@ function safeParseAttributeConfig(type, config) {
5890
5945
  const result = schema.strip().safeParse(config);
5891
5946
  return result.success ? result.data : void 0;
5892
5947
  }
5893
- function createTextValidator(attr) {
5948
+ function createTextValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5894
5949
  let schema = z4.string();
5895
5950
  if (attr.minLength !== void 0) {
5896
- schema = schema.min(
5897
- attr.minLength,
5898
- `${attr.label} must be at least ${attr.minLength} characters`
5899
- );
5951
+ schema = schema.min(attr.minLength, messages.minLength(attr, attr.minLength));
5900
5952
  }
5901
5953
  if (attr.maxLength !== void 0) {
5902
- schema = schema.max(
5903
- attr.maxLength,
5904
- `${attr.label} must be at most ${attr.maxLength} characters`
5905
- );
5954
+ schema = schema.max(attr.maxLength, messages.maxLength(attr, attr.maxLength));
5906
5955
  }
5907
5956
  if (attr.pattern) {
5908
- schema = schema.regex(new RegExp(attr.pattern), `${attr.label} format is invalid`);
5957
+ schema = schema.regex(new RegExp(attr.pattern), messages.invalidPattern(attr));
5909
5958
  }
5910
5959
  return schema;
5911
5960
  }
5912
- function createNumberValidator(attr) {
5961
+ function createNumberValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5913
5962
  let schema = z4.number();
5914
5963
  if (attr.min !== void 0) {
5915
- schema = schema.min(attr.min, `${attr.label} must be at least ${attr.min}`);
5964
+ schema = schema.min(attr.min, messages.minValue(attr, attr.min));
5916
5965
  }
5917
5966
  if (attr.max !== void 0) {
5918
- schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
5967
+ schema = schema.max(attr.max, messages.maxValue(attr, attr.max));
5919
5968
  }
5920
5969
  if (attr.unit === "integer") {
5921
- schema = schema.int(`${attr.label} must be an integer`);
5970
+ schema = schema.int(messages.mustBeInteger(attr));
5922
5971
  }
5923
5972
  return schema;
5924
5973
  }
5925
- function createCheckboxValidator(_attr) {
5974
+ function createCheckboxValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
5926
5975
  return z4.boolean();
5927
5976
  }
5928
- function createDateValidator(attr) {
5929
- return z4.coerce.date({ message: `${attr.label} must be a valid ISO date` });
5977
+ function createDateValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5978
+ return z4.coerce.date({ message: messages.invalidDate(attr) });
5930
5979
  }
5931
- function createPhoneValidator(_attr) {
5932
- return z4.object({
5933
- countryCode: z4.string().length(3),
5934
- phoneNumber: z4.string().min(1)
5935
- });
5980
+ function createPhoneValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5981
+ return z4.object(
5982
+ {
5983
+ countryCode: z4.string().length(3),
5984
+ phoneNumber: z4.string().min(1)
5985
+ },
5986
+ { message: messages.invalidPhone(attr) }
5987
+ );
5936
5988
  }
5937
- function createCurrencyValidator(_attr) {
5938
- return z4.object({
5939
- code: z4.string().length(3),
5940
- value: z4.number().min(0)
5941
- });
5989
+ function createCurrencyValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5990
+ return z4.object(
5991
+ {
5992
+ code: z4.string().length(3),
5993
+ value: z4.number().min(0)
5994
+ },
5995
+ { message: messages.invalidCurrency(attr) }
5996
+ );
5942
5997
  }
5943
- function createStatusValidator(attr) {
5998
+ function createStatusValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5944
5999
  const validValues = attr.options.map((opt) => opt.value);
5945
6000
  return z4.enum(validValues, {
5946
- error: `${attr.label} must be one of: ${validValues.join(", ")}`
6001
+ message: messages.invalidOption(attr, validValues)
5947
6002
  });
5948
6003
  }
5949
- function createSelectValidator(attr) {
6004
+ function createSelectValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5950
6005
  const validValues = attr.options.map((opt) => opt.value);
5951
6006
  return z4.enum(validValues, {
5952
- error: `${attr.label} must be one of: ${validValues.join(", ")}`
6007
+ message: messages.invalidOption(attr, validValues)
5953
6008
  });
5954
6009
  }
5955
- function createMultiselectValidator(attr) {
6010
+ function createMultiselectValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5956
6011
  const validValues = attr.options.map((opt) => opt.value);
5957
6012
  return z4.array(
5958
6013
  z4.enum(validValues, {
5959
- error: `Each value must be one of: ${validValues.join(", ")}`
6014
+ message: messages.invalidOption(attr, validValues)
5960
6015
  })
5961
6016
  );
5962
6017
  }
5963
- function createLocationValidator(_attr) {
5964
- return z4.object({
5965
- address: z4.string().optional(),
5966
- address2: z4.string().optional(),
5967
- city: z4.string().optional(),
5968
- state: z4.string().optional(),
5969
- postalCode: z4.string().optional(),
5970
- country: z4.string().length(3).optional(),
5971
- latitude: z4.number().optional(),
5972
- longitude: z4.number().optional()
5973
- });
6018
+ function createLocationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6019
+ return z4.object(
6020
+ {
6021
+ address: z4.string().optional(),
6022
+ address2: z4.string().optional(),
6023
+ city: z4.string().optional(),
6024
+ state: z4.string().optional(),
6025
+ postalCode: z4.string().optional(),
6026
+ country: z4.string().length(3).optional(),
6027
+ latitude: z4.number().optional(),
6028
+ longitude: z4.number().optional()
6029
+ },
6030
+ { message: messages.invalidLocation(attr) }
6031
+ );
5974
6032
  }
5975
- function createFileValidator(attr) {
6033
+ function createFileValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5976
6034
  const uuidSchema = z4.uuid({
5977
- message: `${attr.label} must be a valid file ID`
6035
+ message: messages.invalidId(attr)
5978
6036
  });
5979
6037
  if (attr.multiple) {
5980
6038
  let arraySchema = z4.array(uuidSchema);
5981
6039
  if (attr.maxFiles) {
5982
- arraySchema = arraySchema.max(
5983
- attr.maxFiles,
5984
- `${attr.label} cannot have more than ${attr.maxFiles} file${attr.maxFiles > 1 ? "s" : ""}`
5985
- );
6040
+ arraySchema = arraySchema.max(attr.maxFiles, messages.maxItems(attr, attr.maxFiles));
5986
6041
  }
5987
6042
  return arraySchema;
5988
6043
  }
5989
6044
  return uuidSchema;
5990
6045
  }
5991
- function createUserValidator(attr) {
6046
+ function createUserValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
5992
6047
  const uuidSchema = z4.uuid({
5993
- message: `${attr.label} must be a valid user ID`
6048
+ message: messages.invalidId(attr)
5994
6049
  });
5995
6050
  if (attr.multiple) {
5996
6051
  return z4.array(uuidSchema);
5997
6052
  }
5998
6053
  return uuidSchema;
5999
6054
  }
6000
- function createSingleRelationValidator(attr) {
6055
+ function createSingleRelationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6001
6056
  const uuidSchema = z4.uuid({
6002
- message: `${attr.label} must be a valid record ID`
6057
+ message: messages.invalidId(attr)
6003
6058
  });
6004
6059
  return z4.union([uuidSchema, z4.null()]);
6005
6060
  }
6006
- function createMultiRelationValidator(attr) {
6061
+ function createMultiRelationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6007
6062
  const uuidSchema = z4.uuid({
6008
- message: `Each ${attr.label} item must be a valid record ID`
6063
+ message: messages.invalidId(attr)
6009
6064
  });
6010
6065
  let arraySchema = z4.array(uuidSchema);
6011
6066
  if (attr.minItems !== void 0) {
6012
- arraySchema = arraySchema.min(
6013
- attr.minItems,
6014
- `${attr.label} must have at least ${attr.minItems} item${attr.minItems > 1 ? "s" : ""}`
6015
- );
6067
+ arraySchema = arraySchema.min(attr.minItems, messages.minItems(attr, attr.minItems));
6016
6068
  }
6017
6069
  if (attr.maxItems !== void 0) {
6018
- arraySchema = arraySchema.max(
6019
- attr.maxItems,
6020
- `${attr.label} must have at most ${attr.maxItems} item${attr.maxItems > 1 ? "s" : ""}`
6021
- );
6070
+ arraySchema = arraySchema.max(attr.maxItems, messages.maxItems(attr, attr.maxItems));
6022
6071
  }
6023
6072
  return arraySchema;
6024
6073
  }
6025
- function createRelationValidator(attr) {
6074
+ function createRelationValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6026
6075
  if (attr.cardinality === "many") {
6027
- return createMultiRelationValidator(attr);
6076
+ return createMultiRelationValidator(attr, messages);
6028
6077
  }
6029
- return createSingleRelationValidator(attr);
6078
+ return createSingleRelationValidator(attr, messages);
6030
6079
  }
6031
- function createRatingValidator(attr) {
6080
+ function createRatingValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6032
6081
  let schema = z4.number().min(0);
6033
6082
  if (attr.max !== void 0) {
6034
- schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
6083
+ schema = schema.max(attr.max, messages.maxValue(attr, attr.max));
6035
6084
  }
6036
6085
  return schema;
6037
6086
  }
6038
- function createFormulaValidator(_attr) {
6087
+ function createFormulaValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
6039
6088
  return z4.unknown();
6040
6089
  }
6041
- function createRollupValidator(_attr) {
6090
+ function createRollupValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
6042
6091
  return z4.unknown();
6043
6092
  }
6044
- function createTextAreaValidator(_attr) {
6093
+ function createTextAreaValidator(_attr, _messages = DEFAULT_VALIDATION_MESSAGES) {
6045
6094
  return z4.string();
6046
6095
  }
6047
6096
  var blockNoteBlockSchema = z4.lazy(
@@ -6053,53 +6102,60 @@ var blockNoteBlockSchema = z4.lazy(
6053
6102
  children: z4.array(z4.any())
6054
6103
  })
6055
6104
  );
6056
- function createRichtextValidator(attr) {
6105
+ function createRichtextValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6057
6106
  return z4.array(blockNoteBlockSchema, {
6058
- message: `${attr.label} must be a valid BlockNote content array`
6107
+ message: messages.invalidRichtext(attr)
6059
6108
  });
6060
6109
  }
6061
- function createAttributeValidator(attr) {
6110
+ function createAttributeValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6062
6111
  switch (attr.type) {
6063
6112
  case "text":
6064
- return createTextValidator(attr);
6113
+ return createTextValidator(attr, messages);
6065
6114
  case "textarea":
6066
- return createTextAreaValidator(attr);
6115
+ return createTextAreaValidator(attr, messages);
6067
6116
  case "richtext":
6068
- return createRichtextValidator(attr);
6117
+ return createRichtextValidator(attr, messages);
6069
6118
  case "number":
6070
- return createNumberValidator(attr);
6119
+ return createNumberValidator(attr, messages);
6071
6120
  case "checkbox":
6072
- return createCheckboxValidator(attr);
6121
+ return createCheckboxValidator(attr, messages);
6073
6122
  case "date":
6074
- return createDateValidator(attr);
6123
+ return createDateValidator(attr, messages);
6075
6124
  case "phone":
6076
- return createPhoneValidator(attr);
6125
+ return createPhoneValidator(attr, messages);
6077
6126
  case "currency":
6078
- return createCurrencyValidator(attr);
6127
+ return createCurrencyValidator(attr, messages);
6079
6128
  case "status":
6080
- return createStatusValidator(attr);
6129
+ return createStatusValidator(attr, messages);
6081
6130
  case "location":
6082
- return createLocationValidator(attr);
6131
+ return createLocationValidator(attr, messages);
6083
6132
  case "select":
6084
- return createSelectValidator(attr);
6133
+ return createSelectValidator(attr, messages);
6085
6134
  case "multiselect":
6086
- return createMultiselectValidator(attr);
6135
+ return createMultiselectValidator(attr, messages);
6087
6136
  case "file":
6088
- return createFileValidator(attr);
6137
+ return createFileValidator(attr, messages);
6089
6138
  case "user":
6090
- return createUserValidator(attr);
6139
+ return createUserValidator(attr, messages);
6091
6140
  case "relation":
6092
- return createRelationValidator(attr);
6141
+ return createRelationValidator(attr, messages);
6093
6142
  case "rating":
6094
- return createRatingValidator(attr);
6143
+ return createRatingValidator(attr, messages);
6095
6144
  case "formula":
6096
- return createFormulaValidator(attr);
6145
+ return createFormulaValidator(attr, messages);
6097
6146
  case "rollup":
6098
- return createRollupValidator(attr);
6147
+ return createRollupValidator(attr, messages);
6099
6148
  default:
6100
6149
  return z4.unknown();
6101
6150
  }
6102
6151
  }
6152
+ function createFormAttributeValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
6153
+ const validator = createAttributeValidator(attr, messages);
6154
+ if (!attr.required) {
6155
+ return validator.nullish();
6156
+ }
6157
+ return validator;
6158
+ }
6103
6159
  function createObjectValidator(objectDef) {
6104
6160
  const shape = {};
6105
6161
  for (const attr of objectDef.attributes) {
@@ -7892,6 +7948,9 @@ var RollupService = class {
7892
7948
  const empty = values.filter((v) => v == null || v === "").length;
7893
7949
  return empty / values.length;
7894
7950
  }
7951
+ // Lookup - returns all values as array
7952
+ case "original":
7953
+ return values;
7895
7954
  }
7896
7955
  }
7897
7956
  /**
@@ -7911,6 +7970,8 @@ var RollupService = class {
7911
7970
  case "earliest":
7912
7971
  case "latest":
7913
7972
  return null;
7973
+ case "original":
7974
+ return [];
7914
7975
  }
7915
7976
  }
7916
7977
  /**
@@ -8268,7 +8329,6 @@ var RecordService = class extends TenantAwareService {
8268
8329
  * Resolve relation IDs to their display labels
8269
8330
  * @internal
8270
8331
  */
8271
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complexity is ok here
8272
8332
  async resolveRelationLabels(relationAttrs, values) {
8273
8333
  const resolvedMap = /* @__PURE__ */ new Map();
8274
8334
  const idsWithAttrId = [];
@@ -8512,6 +8572,12 @@ var RecordService = class extends TenantAwareService {
8512
8572
  if (!options?.skipHooks) {
8513
8573
  await this.hookRegistry.execute("beforeUpdate", schema.name, hookCtx);
8514
8574
  }
8575
+ const hookModifiedValues = {};
8576
+ for (const key of Object.keys(hookCtx.newValues)) {
8577
+ if (!(key in data) && hookCtx.newValues[key] !== existing.values[key]) {
8578
+ hookModifiedValues[key] = hookCtx.newValues[key];
8579
+ }
8580
+ }
8515
8581
  if (options?.validate !== false) {
8516
8582
  if (options?.partial) {
8517
8583
  validateDraftOrThrow(schema, mergedData);
@@ -8519,16 +8585,23 @@ var RecordService = class extends TenantAwareService {
8519
8585
  validateObjectOrThrow(schema, mergedData);
8520
8586
  }
8521
8587
  if (!options?.skipRelationValidation) {
8522
- await this.relationService.validateRelationsOrThrow(schema, data);
8588
+ await this.relationService.validateRelationsOrThrow(schema, {
8589
+ ...data,
8590
+ ...hookModifiedValues
8591
+ });
8523
8592
  }
8524
8593
  if (!options?.skipUserValidation) {
8525
- await this.userService.validateUsersOrThrow(schema, data);
8594
+ await this.userService.validateUsersOrThrow(schema, {
8595
+ ...data,
8596
+ ...hookModifiedValues
8597
+ });
8526
8598
  }
8527
8599
  }
8528
8600
  const completionStatus = computeRecordStatus(schema, mergedData);
8529
8601
  const label = await this.computeLabel(schema, mergedData);
8530
8602
  const updatePayload = {
8531
8603
  ...data,
8604
+ ...hookModifiedValues,
8532
8605
  __completionStatus: completionStatus,
8533
8606
  __label: label,
8534
8607
  __lastUpdatedBy: this.userId
@@ -8550,11 +8623,15 @@ var RecordService = class extends TenantAwareService {
8550
8623
  await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
8551
8624
  }
8552
8625
  await this.recalculateParentRollups(updated, schema);
8553
- if (this.auditService && this.userId && changedAttributes.length > 0) {
8554
- const changes = changedAttributes.map((attr) => ({
8626
+ const allChangedAttributes = [
8627
+ ...changedAttributes,
8628
+ ...Object.keys(hookModifiedValues).filter((k) => !changedAttributes.includes(k))
8629
+ ];
8630
+ if (this.auditService && this.userId && allChangedAttributes.length > 0) {
8631
+ const changes = allChangedAttributes.map((attr) => ({
8555
8632
  field: attr,
8556
8633
  oldValue: hookCtx.oldValues?.[attr],
8557
- newValue: data[attr]
8634
+ newValue: hookCtx.newValues[attr]
8558
8635
  }));
8559
8636
  await this.auditService.logRecordAction({
8560
8637
  action: "record.updated",
@@ -9494,20 +9571,22 @@ var ViewService = class extends TenantAwareService {
9494
9571
  * Get the default view for an object
9495
9572
  *
9496
9573
  * Priority:
9497
- * 1. Custom view marked as default
9498
- * 2. Native view marked as default
9499
- * 3. First available view
9574
+ * 1. Custom view marked as default (for the specified layout)
9575
+ * 2. Native view marked as default (for the specified layout)
9576
+ * 3. First available view (for the specified layout)
9500
9577
  *
9501
9578
  * @param objectName - Object name
9579
+ * @param layout - Optional layout filter ("page" or "modal")
9502
9580
  * @returns Default view or null
9503
9581
  */
9504
- async getDefaultView(objectName) {
9582
+ async getDefaultView(objectName, layout) {
9505
9583
  const views = await this.getViewsForObject(objectName);
9506
- const customDefault = views.find((v) => v.default && !v.system);
9584
+ const filtered = layout ? views.filter((v) => (v.layout ?? "page") === layout) : views;
9585
+ const customDefault = filtered.find((v) => v.default && !v.system);
9507
9586
  if (customDefault) return customDefault;
9508
- const nativeDefault = views.find((v) => v.default && v.system);
9587
+ const nativeDefault = filtered.find((v) => v.default && v.system);
9509
9588
  if (nativeDefault) return nativeDefault;
9510
- return views[0] ?? null;
9589
+ return filtered[0] ?? null;
9511
9590
  }
9512
9591
  /**
9513
9592
  * Create a custom view.
@@ -9518,6 +9597,7 @@ var ViewService = class extends TenantAwareService {
9518
9597
  */
9519
9598
  async createView(input) {
9520
9599
  this.validateViewName(input.name);
9600
+ this.validateModalLayout(input.tabs, input.layout);
9521
9601
  const existing = await this.adapter.views.findByName(input.objectName, input.name);
9522
9602
  if (existing) {
9523
9603
  throw new SchemaError(
@@ -9539,6 +9619,7 @@ var ViewService = class extends TenantAwareService {
9539
9619
  label: input.label,
9540
9620
  description: input.description,
9541
9621
  icon: input.icon,
9622
+ layout: input.layout,
9542
9623
  tabs: input.tabs ?? [],
9543
9624
  default: input.default ?? false,
9544
9625
  system: false,
@@ -9562,10 +9643,14 @@ var ViewService = class extends TenantAwareService {
9562
9643
  if (dbView.system) {
9563
9644
  throw new ProtectedResourceError("view", dbView.name, "modify");
9564
9645
  }
9646
+ const effectiveLayout = input.layout ?? dbView.layout;
9647
+ const effectiveTabs = input.tabs ?? dbView.tabs;
9648
+ this.validateModalLayout(effectiveTabs, effectiveLayout);
9565
9649
  const updated = await this.adapter.views.update(viewId, {
9566
9650
  label: input.label,
9567
9651
  description: input.description,
9568
9652
  icon: input.icon,
9653
+ layout: input.layout,
9569
9654
  tabs: input.tabs,
9570
9655
  default: input.default,
9571
9656
  metadata: input.metadata
@@ -9588,7 +9673,8 @@ var ViewService = class extends TenantAwareService {
9588
9673
  await this.adapter.views.delete(viewId);
9589
9674
  }
9590
9675
  /**
9591
- * Set a view as default for its object.
9676
+ * Set a view as default for its object and layout.
9677
+ * Only unsets other defaults for the same layout.
9592
9678
  * Automatically uses tenant context from AsyncLocalStorage.
9593
9679
  *
9594
9680
  * @param viewId - View ID
@@ -9599,9 +9685,11 @@ var ViewService = class extends TenantAwareService {
9599
9685
  if (!dbView) {
9600
9686
  throw new NotFoundError("View", viewId);
9601
9687
  }
9688
+ const viewLayout = dbView.layout ?? "page";
9602
9689
  const currentViews = await this.adapter.views.findByObjectName(dbView.objectName);
9603
9690
  for (const v of currentViews) {
9604
- if (v.default && v.id !== viewId && !v.system) {
9691
+ const vLayout = v.layout ?? "page";
9692
+ if (v.default && v.id !== viewId && !v.system && vLayout === viewLayout) {
9605
9693
  await this.adapter.views.update(v.id, { default: false });
9606
9694
  }
9607
9695
  }
@@ -9635,6 +9723,28 @@ var ViewService = class extends TenantAwareService {
9635
9723
  ]);
9636
9724
  }
9637
9725
  }
9726
+ /**
9727
+ * Validate modal layout constraints.
9728
+ * Modal views must have exactly one form tab.
9729
+ */
9730
+ validateModalLayout(tabs, layout) {
9731
+ if (layout !== "modal") return;
9732
+ if (!tabs || tabs.length === 0) {
9733
+ throw new ValidationError("Modal views must have exactly one form tab", [
9734
+ { path: ["tabs"], message: "Modal views must have exactly one form tab" }
9735
+ ]);
9736
+ }
9737
+ if (tabs.length > 1) {
9738
+ throw new ValidationError("Modal views can only have one tab", [
9739
+ { path: ["tabs"], message: "Modal views can only have one tab" }
9740
+ ]);
9741
+ }
9742
+ if (tabs[0].type !== "form") {
9743
+ throw new ValidationError("Modal views must have a form tab", [
9744
+ { path: ["tabs"], message: "Modal views must have a form tab, not a " + tabs[0].type }
9745
+ ]);
9746
+ }
9747
+ }
9638
9748
  /**
9639
9749
  * Convert database view to ViewDefinition
9640
9750
  */
@@ -9646,6 +9756,7 @@ var ViewService = class extends TenantAwareService {
9646
9756
  description: dbView.description,
9647
9757
  icon: dbView.icon,
9648
9758
  object: dbView.objectName,
9759
+ layout: dbView.layout,
9649
9760
  tabs: dbView.tabs,
9650
9761
  default: dbView.default,
9651
9762
  system: dbView.system,
@@ -10070,6 +10181,7 @@ export {
10070
10181
  view,
10071
10182
  group,
10072
10183
  registry,
10184
+ DEFAULT_VALIDATION_MESSAGES,
10073
10185
  textConfigSchema,
10074
10186
  textareaConfigSchema,
10075
10187
  richtextConfigSchema,
@@ -10114,6 +10226,7 @@ export {
10114
10226
  createTextAreaValidator,
10115
10227
  createRichtextValidator,
10116
10228
  createAttributeValidator,
10229
+ createFormAttributeValidator,
10117
10230
  createObjectValidator,
10118
10231
  validateAttribute,
10119
10232
  validateObject,