@api-components/api-type-document 4.2.34 → 4.2.38

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-components/api-type-document",
3
3
  "description": "A documentation table for type (resource) properties. Works with AMF data model",
4
- "version": "4.2.34",
4
+ "version": "4.2.38",
5
5
  "license": "Apache-2.0",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
@@ -28,7 +28,7 @@
28
28
  "@advanced-rest-client/arc-marked": "^1.1.0",
29
29
  "@advanced-rest-client/markdown-styles": "^3.1.4",
30
30
  "@anypoint-web-components/anypoint-button": "^1.2.3",
31
- "@api-components/amf-helper-mixin": "^4.5.29",
31
+ "@api-components/amf-helper-mixin": "^4.5.34",
32
32
  "@api-components/api-annotation-document": "^4.1.0",
33
33
  "@api-components/api-resource-example-document": "^4.3.3",
34
34
  "@open-wc/dedupe-mixin": "^1.3.0",
@@ -39,15 +39,15 @@
39
39
  "@anypoint-web-components/anypoint-checkbox": "^1.2.2",
40
40
  "@anypoint-web-components/anypoint-styles": "^1.0.2",
41
41
  "@api-components/api-model-generator": "^0.2.14",
42
- "@commitlint/cli": "^13.2.0",
42
+ "@commitlint/cli": "^13.2.1",
43
43
  "@commitlint/config-conventional": "^13.2.0",
44
44
  "@open-wc/eslint-config": "^4.2.0",
45
- "@open-wc/testing": "^2.5.15",
45
+ "@open-wc/testing": "^2.5.32",
46
46
  "@web/dev-server": "^0.1.24",
47
47
  "@web/test-runner": "^0.13.18",
48
- "@web/test-runner-playwright": "0.8.10",
49
- "eslint": "^7.32.0",
50
- "eslint-config-prettier": "^8.1.0",
48
+ "@web/test-runner-playwright": "^0.11.0",
49
+ "eslint": "^8.0.0",
50
+ "eslint-config-prettier": "^8.3.0",
51
51
  "husky": "^7.0.2",
52
52
  "lint-staged": "^11.1.2",
53
53
  "sinon": "^11.1.2",
@@ -54,6 +54,23 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
54
54
  selectedMediaType: number;
55
55
  // The type after it has been resolved.
56
56
  _resolvedType: Object;
57
+ /**
58
+ * Computed properties for object types.
59
+ * This is a reactive property that is recalculated when type, amf, or renderReadOnly changes.
60
+ */
61
+ _computedProperties: any[] | undefined;
62
+ /**
63
+ * Resolved type for examples with all link-target references resolved
64
+ */
65
+ _resolvedExampleType: Object | undefined;
66
+ /**
67
+ * Whether to show the examples section
68
+ */
69
+ _showExamples: boolean | undefined;
70
+ /**
71
+ * Effective media type for examples
72
+ */
73
+ _exampleMediaType: string | undefined;
57
74
  /**
58
75
  * Should be set if described properties has a parent type.
59
76
  * This is used when recursively iterating over properties.
@@ -242,6 +259,15 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
242
259
  */
243
260
  _computeProperties(item: any): any[]|undefined;
244
261
 
262
+ /**
263
+ * Deeply resolves link-target references in a type and its nested properties.
264
+ * This is essential for rendering complete examples with nested objects.
265
+ *
266
+ * @param type The type to resolve
267
+ * @returns The type with all link-target references resolved
268
+ */
269
+ _deepResolveType(type: any): any|undefined;
270
+
245
271
  /**
246
272
  * Computes list values for `andTypes` property.
247
273
  * @param items List of OAS' "and" properties
@@ -68,6 +68,22 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
68
68
  selectedMediaType: { type: Number },
69
69
  // The type after it has been resolved.
70
70
  _resolvedType: { type: Object },
71
+ /**
72
+ * Computed properties from the resolved type
73
+ */
74
+ _computedProperties: { type: Array },
75
+ /**
76
+ * Resolved type for examples with all link-target references resolved
77
+ */
78
+ _resolvedExampleType: { type: Object },
79
+ /**
80
+ * Whether to show the examples section
81
+ */
82
+ _showExamples: { type: Boolean },
83
+ /**
84
+ * Effective media type for examples
85
+ */
86
+ _exampleMediaType: { type: String },
71
87
  /**
72
88
  * Should be set if described properties has a parent type.
73
89
  * This is used when recursively iterating over properties.
@@ -325,6 +341,10 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
325
341
  */
326
342
  this.selectedAnyOf = undefined;
327
343
  this.renderReadOnly = false;
344
+ this.noMainExample = false;
345
+ this._hasExamples = false;
346
+ this._renderMainExample = false;
347
+ this._cachedDeepResolvedType = undefined;
328
348
 
329
349
  this._isPropertyReadOnly = this._isPropertyReadOnly.bind(this);
330
350
  this.noMediaSelector = false;
@@ -345,6 +365,37 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
345
365
  return isScalar ? false : !!(!noMainExample && hasExamples);
346
366
  }
347
367
 
368
+ /**
369
+ * Called when properties change
370
+ * @param {Map} changedProperties Changed properties
371
+ */
372
+ updated(changedProperties) {
373
+ super.updated(changedProperties);
374
+
375
+ // If amf changed and we have a type, recalculate properties synchronously
376
+ if (changedProperties.has('amf') && this._resolvedType && this.amf) {
377
+ // Cancel any pending debounced calls and recalculate
378
+ this.__typeChangeDebouncer = false;
379
+ this._typeChanged(this._resolvedType);
380
+ // _typeChanged will update _computedProperties, _isGrpcApi, and _deepResolvedType
381
+ }
382
+
383
+ // If renderReadOnly changed and we have an object, recalculate properties
384
+ // This is needed because _filterReadOnlyProperties depends on this.renderReadOnly
385
+ if (changedProperties.has('renderReadOnly') && this._resolvedType && this.isObject) {
386
+ this._computedProperties = this._computeProperties(this._resolvedType);
387
+ }
388
+
389
+ // If noMainExample changed, recalculate whether to render examples
390
+ if (changedProperties.has('noMainExample') && this._resolvedType) {
391
+ this._showExamples = !this.noMainExample && (
392
+ this.renderMediaSelector ||
393
+ this.isObject ||
394
+ this._renderMainExample
395
+ );
396
+ }
397
+ }
398
+
348
399
  /**
349
400
  * Called when resolved type or amf changed.
350
401
  * Creates a debouncer to compute UI values so it's independent of
@@ -397,9 +448,16 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
397
448
  } else if (
398
449
  this._hasType(type, shapesKey.UnionShape)
399
450
  ) {
400
- isUnion = true;
401
- key = this._getAmfKey(shapesKey.anyOf);
402
- this.unionTypes = this._computeTypes(type, key);
451
+ // Check if this is a nullable union (type | null) which should be rendered as scalar
452
+ const nullableCheck = this._checkNullableUnion(type);
453
+ if (nullableCheck && nullableCheck.isNullable) {
454
+ // Treat nullable types as scalar for cleaner rendering
455
+ isScalar = true;
456
+ } else {
457
+ isUnion = true;
458
+ key = this._getAmfKey(shapesKey.anyOf);
459
+ this.unionTypes = this._computeTypes(type, key);
460
+ }
403
461
  } else if (this._hasProperty(type, this.ns.w3.shacl.xone)) {
404
462
  isOneOf = true;
405
463
  key = this._getAmfKey(this.ns.w3.shacl.xone);
@@ -455,6 +513,32 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
455
513
  this.isAnd = isAnd;
456
514
  this.isOneOf = isOneOf;
457
515
  this.isAnyOf = isAnyOf;
516
+
517
+ // Compute properties for objects - this needs to be reactive
518
+ if (isObject) {
519
+ this._computedProperties = this._computeProperties(type);
520
+ } else {
521
+ this._computedProperties = undefined;
522
+ }
523
+
524
+ // Always deep resolve for examples (resolves link-target references for gRPC and similar)
525
+ // This is cheap if there are no link-targets
526
+ if (type) {
527
+ this._resolvedExampleType = this._deepResolveType(type);
528
+ } else {
529
+ this._resolvedExampleType = type;
530
+ }
531
+
532
+ // Determine if we should show the examples section
533
+ // Priority: noMainExample (hide) > renderMediaSelector (show) > isObject (show) > _renderMainExample
534
+ this._showExamples = !this.noMainExample && (
535
+ this.renderMediaSelector || // Need to show the section for the media type selector
536
+ isObject || // Objects can generate examples automatically
537
+ this._renderMainExample // Has explicit examples
538
+ );
539
+
540
+ // Effective media type - use 'application/json' as default for objects without mediaType
541
+ this._exampleMediaType = this.mediaType || (isObject ? 'application/json' : undefined);
458
542
  }
459
543
 
460
544
  /**
@@ -526,18 +610,11 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
526
610
  if (Array.isArray(item)) {
527
611
  [item] = item;
528
612
  }
613
+ // For array types in unions, return the array itself instead of unwrapping to items
614
+ // This preserves the "array of" indicator in the UI
529
615
  if (this._hasType(item, this.ns.aml.vocabularies.shapes.ArrayShape)) {
530
616
  item = this._resolve(item);
531
- const itemsKey = this._getAmfKey(this.ns.aml.vocabularies.shapes.items);
532
- const items = this._ensureArray(item[itemsKey]);
533
- if (items && items.length === 1) {
534
- let result = items[0];
535
- if (Array.isArray(result)) {
536
- [result] = result;
537
- }
538
- result = this._resolve(result);
539
- return result;
540
- }
617
+ return item;
541
618
  }
542
619
  if (Array.isArray(item)) {
543
620
  [item] = item;
@@ -546,6 +623,91 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
546
623
  return this._resolve(item);
547
624
  }
548
625
 
626
+
627
+ /**
628
+ * Deeply resolves link-target references in a type for example generation.
629
+ * This ensures that nested objects show their full structure in examples.
630
+ * This is needed for APIs like gRPC where nested types use link-target references.
631
+ *
632
+ * @param {Object} type The type to resolve
633
+ * @return {Object} The deeply resolved type
634
+ */
635
+ _deepResolveType(type) {
636
+ if (!type || !this.amf) {
637
+ return type;
638
+ }
639
+
640
+ const resolved = this._resolve(type);
641
+ if (!resolved) {
642
+ return type;
643
+ }
644
+
645
+ // Get properties
646
+ const propertyKey = this._getAmfKey(this.ns.w3.shacl.property);
647
+ const properties = this._ensureArray(resolved[propertyKey]);
648
+
649
+ if (!properties || !properties.length) {
650
+ return resolved;
651
+ }
652
+
653
+ // Create a new type object with deeply resolved properties
654
+ const deepResolved = { ...resolved };
655
+ const linkTargetKey = this._getAmfKey(this.ns.aml.vocabularies.document.linkTarget);
656
+ const rangeKey = this._getAmfKey(this.ns.raml.vocabularies.shapes.range);
657
+
658
+ // Resolve each property's range
659
+ const resolvedProperties = properties.map(prop => {
660
+ const resolvedProp = this._resolve(prop);
661
+ if (!resolvedProp) {
662
+ return prop;
663
+ }
664
+
665
+ const range = this._ensureArray(resolvedProp[rangeKey])[0];
666
+ if (!range) {
667
+ return resolvedProp;
668
+ }
669
+
670
+ // If the range has a link-target, resolve it
671
+ if (range[linkTargetKey]) {
672
+ const linkTargetId = this._ensureArray(range[linkTargetKey])[0];
673
+ if (linkTargetId && linkTargetId['@id']) {
674
+ const targetId = linkTargetId['@id'];
675
+
676
+ // Find the target
677
+ const declares = this._computeDeclares(this.amf);
678
+ let target = declares ? this._findById(declares, targetId) : undefined;
679
+
680
+ if (!target) {
681
+ const references = this._computeReferences(this.amf);
682
+ if (references && references.length) {
683
+ for (let i = 0; i < references.length && !target; i++) {
684
+ const refDeclares = this._computeDeclares(references[i]);
685
+ if (refDeclares) {
686
+ target = this._findById(refDeclares, targetId);
687
+ }
688
+ }
689
+ }
690
+ }
691
+
692
+ // If target found, replace the range with the resolved target
693
+ if (target) {
694
+ const resolvedTarget = this._resolve(target);
695
+ if (resolvedTarget) {
696
+ const newProp = { ...resolvedProp };
697
+ newProp[rangeKey] = [resolvedTarget];
698
+ return newProp;
699
+ }
700
+ }
701
+ }
702
+ }
703
+
704
+ return resolvedProp;
705
+ });
706
+
707
+ deepResolved[propertyKey] = resolvedProperties;
708
+ return deepResolved;
709
+ }
710
+
549
711
  /**
550
712
  * Helper function for the view. Extracts `http://www.w3.org/ns/shacl#property`
551
713
  * from the shape model
@@ -561,16 +723,65 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
561
723
  return item;
562
724
  }
563
725
 
726
+ // For objects with link-target, we need to find the actual target with properties
727
+ const linkTargetKey = this._getAmfKey(this.ns.aml.vocabularies.document.linkTarget);
728
+ let resolvedItem = item;
729
+
730
+ if (item[linkTargetKey] && this.amf) {
731
+ const linkTargetId = this._ensureArray(item[linkTargetKey])[0];
732
+ if (linkTargetId && linkTargetId['@id']) {
733
+ const targetId = linkTargetId['@id'];
734
+ // Try to find the target in declares
735
+ const declares = this._computeDeclares(this.amf);
736
+ let target = declares ? this._findById(declares, targetId) : undefined;
737
+
738
+ // If not found in declares, search in references
739
+ if (!target) {
740
+ const references = this._computeReferences(this.amf);
741
+ if (references && references.length) {
742
+ for (let i = 0; i < references.length && !target; i++) {
743
+ const refDeclares = this._computeDeclares(references[i]);
744
+ if (refDeclares) {
745
+ target = this._findById(refDeclares, targetId);
746
+ }
747
+ }
748
+ }
749
+ }
750
+
751
+ // If we found the target and it has properties, use it
752
+ // Don't use the target directly to avoid caching issues
753
+ const propertyKey = this._getAmfKey(this.ns.w3.shacl.property);
754
+ if (target && target[propertyKey]) {
755
+ // Use the target's properties but keep the original item structure
756
+ // This prevents issues with cached __apicResolved flags
757
+ resolvedItem = this._resolve(target);
758
+ // If resolve returned the same object or failed, fallback to standard resolve
759
+ if (!resolvedItem || !resolvedItem[propertyKey]) {
760
+ resolvedItem = this._resolve(item);
761
+ }
762
+ }
763
+ }
764
+ }
765
+
766
+ // Fallback to standard resolve if no link-target or target not found
767
+ if (resolvedItem === item) {
768
+ resolvedItem = this._resolve(item);
769
+ }
770
+
771
+ if (!resolvedItem) {
772
+ return undefined;
773
+ }
774
+
564
775
  const propertyKey = this._getAmfKey(this.ns.w3.shacl.property);
565
- const itemProperties = this._ensureArray(item[propertyKey]||[])
776
+ const itemProperties = this._ensureArray(resolvedItem[propertyKey]||[])
566
777
  const additionalPropertiesKey = this._getAmfKey(this.ns.w3.shacl.additionalPropertiesSchema);
567
778
 
568
779
  // If the item doesn't have additional properties, filter the read-only properties and return
569
- if (!item[additionalPropertiesKey]) {
780
+ if (!resolvedItem[additionalPropertiesKey]) {
570
781
  return this._filterReadOnlyProperties(itemProperties)
571
782
  }
572
783
 
573
- const additionalPropertiesSchema = this._ensureArray(item[additionalPropertiesKey])
784
+ const additionalPropertiesSchema = this._ensureArray(resolvedItem[additionalPropertiesKey])
574
785
 
575
786
  // If the item does have additional properties, ensure they are in an array
576
787
  const additionalProperties = this._ensureArray(additionalPropertiesSchema[0][propertyKey] || additionalPropertiesSchema[0])
@@ -688,7 +899,7 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
688
899
  * @return {TemplateResult[]|string} Templates for object properties
689
900
  */
690
901
  _objectTemplate() {
691
- const items = this._computeProperties(this._resolvedType);
902
+ const items = this._computedProperties;
692
903
  if (!items || !items.length) {
693
904
  return '';
694
905
  }
@@ -924,8 +1135,16 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
924
1135
  parts +=
925
1136
  'code-content-action-button-active, code-wrapper, example-code-wrapper, markdown-html';
926
1137
  const mediaTypes = (this.mediaTypes || []);
1138
+ // Use cached values if available, otherwise fallback to computed values
1139
+ const shouldRenderExamples = this._showExamples !== undefined
1140
+ ? this._showExamples
1141
+ : this._renderMainExample;
1142
+ const exampleMediaType = this._exampleMediaType !== undefined
1143
+ ? this._exampleMediaType
1144
+ : (this.mediaType || (this.isObject ? 'application/json' : undefined));
1145
+
927
1146
  return html`<style>${this.styles}</style>
928
- <section class="examples" ?hidden="${!this._renderMainExample}">
1147
+ ${shouldRenderExamples ? html`<section class="examples">
929
1148
  ${this.shouldRenderMediaSelector
930
1149
  ? html`<div class="media-type-selector">
931
1150
  <span>Media type:</span>
@@ -950,18 +1169,18 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
950
1169
  <api-resource-example-document
951
1170
  .amf="${this.amf}"
952
1171
  .payloadId="${this.selectedBodyId}"
953
- .examples="${this._resolvedType}"
954
- .mediaType="${this.mediaType}"
1172
+ .examples="${this._resolvedExampleType || this._resolvedType}"
1173
+ .mediaType="${exampleMediaType}"
955
1174
  .typeName="${this.parentTypeName}"
956
1175
  @has-examples-changed="${this._hasExamplesHandler}"
957
1176
  ?noauto="${!!this.isScalar}"
958
1177
  ?noactions="${this.noExamplesActions}"
959
- ?rawOnly="${!this.mediaType}"
1178
+ ?rawOnly="${!exampleMediaType}"
960
1179
  ?compatibility="${this.compatibility}"
961
1180
  exportParts="${parts}"
962
1181
  ?renderReadOnly="${this.renderReadOnly}"
963
1182
  ></api-resource-example-document>
964
- </section>
1183
+ </section>` : ''}
965
1184
 
966
1185
  ${this.isObject ? this._objectTemplate() : ''}
967
1186
  ${this.isArray ? this._arrayTemplate() : ''}
@@ -49,6 +49,15 @@ interface PropertyDocumentMixin extends AmfHelperMixin {
49
49
  */
50
50
  graph: boolean;
51
51
 
52
+ /**
53
+ * Checks if a union shape represents a nullable type (union with null).
54
+ * A nullable type is a union of exactly 2 members where one is NilShape.
55
+ *
56
+ * @param range AMF range object (should be UnionShape)
57
+ * @returns Returns {baseType, isNullable: true} if nullable, undefined otherwise
58
+ */
59
+ _checkNullableUnion(range: any): { baseType: any; isNullable: boolean } | undefined;
60
+
52
61
  /**
53
62
  * Computes type from a `http://raml.org/vocabularies/shapes#range` object
54
63
  *
@@ -107,6 +107,65 @@ const mxFunction = (base) => {
107
107
  this._hasMediaType = false;
108
108
  }
109
109
 
110
+ /**
111
+ * Checks if a union shape represents a nullable type (union with null).
112
+ * A nullable type is a union of exactly 2 members where one is NilShape
113
+ * and the other is any type (scalar, array, object, etc.).
114
+ *
115
+ * This is specifically for OpenAPI 3.0 nullable: true which AMF converts
116
+ * to union of type + null. This simplifies the rendering to "Type or null"
117
+ * instead of showing a full union selector.
118
+ *
119
+ * @param {any} range AMF range object (should be UnionShape)
120
+ * @return {Object|undefined} Returns {baseType, isNullable: true} if nullable, undefined otherwise
121
+ */
122
+ _checkNullableUnion(range) {
123
+ if (!range || !this._hasType(range, this.ns.aml.vocabularies.shapes.UnionShape)) {
124
+ return undefined;
125
+ }
126
+
127
+ const key = this._getAmfKey(this.ns.aml.vocabularies.shapes.anyOf);
128
+ const unionMembers = this._ensureArray(range[key]);
129
+
130
+ if (!unionMembers || unionMembers.length !== 2) {
131
+ return undefined;
132
+ }
133
+
134
+ // Check if one member is NilShape
135
+ let nilIndex = -1;
136
+ let baseTypeIndex = -1;
137
+
138
+ for (let i = 0; i < unionMembers.length; i++) {
139
+ let member = unionMembers[i];
140
+ if (Array.isArray(member)) {
141
+ [member] = member;
142
+ }
143
+ member = this._resolve(member);
144
+
145
+ if (this._hasType(member, this.ns.aml.vocabularies.shapes.NilShape)) {
146
+ nilIndex = i;
147
+ } else {
148
+ baseTypeIndex = i;
149
+ }
150
+ }
151
+
152
+ // If we found exactly one nil and one non-nil type, it's a nullable
153
+ if (nilIndex !== -1 && baseTypeIndex !== -1) {
154
+ let baseType = unionMembers[baseTypeIndex];
155
+ if (Array.isArray(baseType)) {
156
+ [baseType] = baseType;
157
+ }
158
+ baseType = this._resolve(baseType);
159
+
160
+ return {
161
+ baseType,
162
+ isNullable: true
163
+ };
164
+ }
165
+
166
+ return undefined;
167
+ }
168
+
110
169
  /**
111
170
  * Computes type from a `http://raml.org/vocabularies/shapes#range` object
112
171
  *
@@ -122,6 +181,12 @@ const mxFunction = (base) => {
122
181
  return this._computeScalarDataType(range);
123
182
  }
124
183
  if (this._hasType(range, rs.UnionShape)) {
184
+ // Check if this is a nullable union (type | null)
185
+ const nullableCheck = this._checkNullableUnion(range);
186
+ if (nullableCheck && nullableCheck.isNullable) {
187
+ const baseTypeName = this._computeRangeDataType(nullableCheck.baseType);
188
+ return `${baseTypeName} or null`;
189
+ }
125
190
  return 'Union';
126
191
  }
127
192
  if (this._hasType(range, rs.ArrayShape)) {
@@ -145,9 +210,6 @@ const mxFunction = (base) => {
145
210
  if (this._hasType(range, rs.TupleShape)) {
146
211
  return 'Tuple';
147
212
  }
148
- if (this._hasType(range, rs.UnionShape)) {
149
- return 'Union';
150
- }
151
213
  if (this._hasType(range, rs.RecursiveShape)) {
152
214
  return 'Recursive';
153
215
  }
@@ -313,7 +375,12 @@ const mxFunction = (base) => {
313
375
  * @return {Boolean}
314
376
  */
315
377
  _computeIsUnion(range) {
316
- return this._hasType(range, this.ns.aml.vocabularies.shapes.UnionShape);
378
+ if (!this._hasType(range, this.ns.aml.vocabularies.shapes.UnionShape)) {
379
+ return false;
380
+ }
381
+ // Check if it's a nullable union (which we don't treat as union for UI)
382
+ const nullableCheck = this._checkNullableUnion(range);
383
+ return !nullableCheck; // Only true if NOT nullable
317
384
  }
318
385
 
319
386
  /**
@@ -325,7 +392,15 @@ const mxFunction = (base) => {
325
392
  * @return {Boolean}
326
393
  */
327
394
  _computeIsObject(range) {
328
- return this._hasType(range, this.ns.w3.shacl.NodeShape);
395
+ if (this._hasType(range, this.ns.w3.shacl.NodeShape)) {
396
+ return true;
397
+ }
398
+ // Check if it's a nullable object
399
+ const nullableCheck = this._checkNullableUnion(range);
400
+ if (nullableCheck) {
401
+ return this._hasType(nullableCheck.baseType, this.ns.w3.shacl.NodeShape);
402
+ }
403
+ return false;
329
404
  }
330
405
 
331
406
  /**
@@ -337,7 +412,15 @@ const mxFunction = (base) => {
337
412
  * @return {Boolean}
338
413
  */
339
414
  _computeIsArray(range) {
340
- return this._hasType(range, this.ns.aml.vocabularies.shapes.ArrayShape);
415
+ if (this._hasType(range, this.ns.aml.vocabularies.shapes.ArrayShape)) {
416
+ return true;
417
+ }
418
+ // Check if it's a nullable array
419
+ const nullableCheck = this._checkNullableUnion(range);
420
+ if (nullableCheck) {
421
+ return this._hasType(nullableCheck.baseType, this.ns.aml.vocabularies.shapes.ArrayShape);
422
+ }
423
+ return false;
341
424
  }
342
425
 
343
426
  /**
@@ -735,7 +735,14 @@ export class PropertyShapeDocument extends PropertyDocumentMixin(LitElement) {
735
735
  if (!this.isComplex || !this.opened || this.isScalarArray) {
736
736
  return '';
737
737
  }
738
- const range = this._resolve(this.range);
738
+ let range = this._resolve(this.range);
739
+
740
+ // If this is a nullable complex type, extract the base type
741
+ const nullableCheck = this._checkNullableUnion(range);
742
+ if (nullableCheck) {
743
+ range = nullableCheck.baseType;
744
+ }
745
+
739
746
  const parentTypeName = this._getParentTypeName();
740
747
  return html`<api-type-document
741
748
  class="children complex"