Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1890180: Bug 1913969: Fix edge case exception for fieldDependency spec descriptor and add support for non-sibling control fields. #7957

Merged
merged 1 commit into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { THOUSAND, MILLION, BILLION } from './const';

const UNSUPPORTED_SCHEMA_PROPERTIES = ['allOf', 'anyOf', 'oneOf'];

// Transform a path string to a JSON schema path array
export const stringPathToUISchemaPath = (path: string): string[] =>
(_.toPath(path) ?? []).map((subPath) => {
return /^\d+$/.test(subPath) ? 'items' : subPath;
});

export const useSchemaLabel = (schema: JSONSchema6, uiSchema: UiSchema, defaultLabel?: string) => {
const options = getUiOptions(uiSchema ?? {});
const showLabel = options?.label ?? true;
Expand Down Expand Up @@ -88,6 +94,21 @@ const getUISortOrder = (uiSchema: UiSchema, fallback: number): number => {
);
};

// Return an array of dependency control field names that exist within uiSchema at the specified
// path.
const getControlFieldsAtPath = (uiSchema: UiSchema, path: string[]): string[] => {
if (!_.isObject(uiSchema)) {
return [];
}
const { 'ui:dependency': dependency } = uiSchema;
const dependencyMatchesPath =
dependency && _.isEqual(dependency.controlFieldPath.slice(0, -1), path ?? []);
return [
...(dependencyMatchesPath ? [dependency.controlFieldName] : []),
..._.flatMap(uiSchema, (childUISchema) => getControlFieldsAtPath(childUISchema, path)),
];
};

/**
* Give a property name a sort wieght based on whether it has ui schema, is required, or is a
* control field for a property with a field dependency. A lower weight means higher sort order.
Expand All @@ -110,23 +131,23 @@ const getJSONSchemaPropertySortWeight = (
property: string,
jsonSchema: JSONSchema6,
uiSchema: UiSchema,
currentPath?: string[],
): number => {
const isRequired = (jsonSchema?.required ?? []).includes(property);
const propertyUISchema = uiSchema?.[property];

// All control fields that exist within uiSchema and match this path
const controlFields = getControlFieldsAtPath(propertyUISchema, currentPath);

// Any sibling has a dependency with this as the control field.
const isControlField = _.some(
uiSchema,
({ 'ui:dependency': dependency }) => dependency?.controlFieldName === property,
const isControlField = _.some(uiSchema, ({ 'ui:dependency': siblingDependency }) =>
_.isEqual(siblingDependency?.controlFieldPath, [...(currentPath ?? []), property]),
);

// Minimum'ui:sortOrder' for this property and it's children. Use propertyNames.length as a fallback,
// which ensures that properties without a "ui:sortOrder" have highest weight.
const uiSortOrder = getUISortOrder(propertyUISchema, _.keys(jsonSchema?.properties).length);

// This property's control field name, if it exists
const controlFieldName = propertyUISchema?.['ui:dependency']?.controlFieldName;

// A small offset that is added to the base weight so that control fields get sorted
// below other fields in the same 'tier', and allows for depenendt fields to be sorted
// directly after their control field.
Expand All @@ -135,9 +156,16 @@ const getJSONSchemaPropertySortWeight = (
// Total offset to be added to base tier
const offset = controlFieldOffset + uiSortOrder;

// If this property is a dependent, it's weight is based on it's control field
if (controlFieldName) {
return getJSONSchemaPropertySortWeight(controlFieldName, jsonSchema, uiSchema) + offset;
// If this property or it's children have a control field at the current path, it's weight is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If this property or it's children have a control field at the current path, it's weight is
// If this property or its children have a control field at the current path, it's weight is

// based on the highest weight control field.
if (controlFields?.length) {
return (
Math.max(
...controlFields.map((controlField) =>
getJSONSchemaPropertySortWeight(controlField, jsonSchema, uiSchema, currentPath),
),
) + offset
);
}

// Tier 1 = -1001000000 (negative one billion one million) + offset
Expand All @@ -161,10 +189,17 @@ const getJSONSchemaPropertySortWeight = (
// - optional fields with an associated ui schema next,
// - field dependency properties (control then dependent)
// - all other properties
export const getJSONSchemaOrder = (jsonSchema, uiSchema) => {
export const getJSONSchemaOrder = (
jsonSchema: JSONSchema6,
uiSchema: UiSchema,
currentPath?: string[],
) => {
const type = getSchemaType(jsonSchema ?? {});
const handleArray = () => {
const descendantOrder = getJSONSchemaOrder(jsonSchema?.items as JSONSchema6, uiSchema?.items);
const descendantOrder = getJSONSchemaOrder(jsonSchema?.items as JSONSchema6, uiSchema?.items, [
...(currentPath ?? []),
'items',
]);
return !_.isEmpty(descendantOrder) ? { items: descendantOrder } : {};
};

Expand All @@ -175,15 +210,21 @@ export const getJSONSchemaOrder = (jsonSchema, uiSchema) => {
}

const uiOrder = Immutable.Set(propertyNames)
.sortBy((propertyName) => getJSONSchemaPropertySortWeight(propertyName, jsonSchema, uiSchema))
.sortBy((property) =>
getJSONSchemaPropertySortWeight(property, jsonSchema, uiSchema, currentPath ?? []),
)
.toJS();

return {
...(uiOrder.length > 1 && { 'ui:order': uiOrder }),
..._.reduce(
jsonSchema?.properties ?? {},
(orderAccumulator, propertySchema, propertyName) => {
const descendantOrder = getJSONSchemaOrder(propertySchema, uiSchema?.[propertyName]);
const descendantOrder = getJSONSchemaOrder(
propertySchema as JSONSchema6,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you simply declare the type above in the function definition like

(orderAccumulator, propertySchema: JSONSchema6, propertyName) => {

This is more type safe than using as

uiSchema?.[propertyName],
[...(currentPath ?? []), propertyName],
);
if (_.isEmpty(descendantOrder)) {
return orderAccumulator;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ describe('capabilitiesToUISchema', () => {
});
it('Handles SpecCapablitiy.select', () => {
const uiSchema = capabilitiesToUISchema([
`${SpecCapability.select}DEBUG`,
`${SpecCapability.select}INFO`,
`${SpecCapability.select}WARN`,
`${SpecCapability.select}ERROR`,
`${SpecCapability.select}FATAL`,
] as SpecCapability[]);
`${SpecCapability.select}DEBUG` as SpecCapability,
`${SpecCapability.select}INFO` as SpecCapability,
`${SpecCapability.select}WARN` as SpecCapability,
`${SpecCapability.select}ERROR` as SpecCapability,
`${SpecCapability.select}FATAL` as SpecCapability,
]);
expect(uiSchema['ui:items']).toEqual({
DEBUG: 'DEBUG',
INFO: 'INFO',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { JSONSchema6 } from 'json-schema';
import { UiSchema } from 'react-jsonschema-form';
import { getSchemaType } from 'react-jsonschema-form/lib/utils';
import { modelFor } from '@console/internal/module/k8s';
import { getJSONSchemaOrder } from '@console/shared/src/components/dynamic-form/utils';
import {
getJSONSchemaOrder,
stringPathToUISchemaPath,
} from '@console/shared/src/components/dynamic-form/utils';
import { SpecCapability, Descriptor } from '../descriptors/types';
import { capabilityFieldMap, capabilityWidgetMap } from '../descriptors/spec/spec-descriptor-input';
import { HIDDEN_UI_SCHEMA } from './const';
Expand Down Expand Up @@ -32,12 +35,6 @@ const getCompatibleCapabilities = (jsonSchemaType: JSONSchemaType) => {
}
};

// Transform a path string from a descriptor to a JSON schema path array
export const descriptorPathToUISchemaPath = (path: string): string[] =>
(_.toPath(path) ?? []).map((subPath) => {
return /^\d+$/.test(subPath) ? 'items' : subPath;
});

// Applies a hidden widget and label configuration to every property of the given schema.
// This is useful for whitelisting only a few schema properties when all properties are not known.
export const hideAllExistingProperties = (schema: JSONSchema6) => {
Expand Down Expand Up @@ -66,7 +63,7 @@ const k8sResourceCapabilityToUISchema = (capability: SpecCapability): UiSchema =

const fieldDependencyCapabilityToUISchema = (capability: SpecCapability): UiSchema => {
const [, path, controlFieldValue] = capability.match(REGEXP_FIELD_DEPENDENCY_PATH_VALUE) ?? [];
const controlFieldPath = descriptorPathToUISchemaPath(path);
const controlFieldPath = stringPathToUISchemaPath(path);
const controlFieldName = _.last(controlFieldPath);
return {
...(path &&
Expand Down Expand Up @@ -206,15 +203,15 @@ export const descriptorsToUISchema = (
return uiSchemaAccumulator;
}
const capabilities = getValidCapabilities(descriptor, schemaForDescriptor);
const uiSchemaPath = descriptorPathToUISchemaPath(descriptor.path);
const uiSchemaPath = stringPathToUISchemaPath(descriptor.path);
const isAdvanced = capabilities.includes(SpecCapability.advanced);
const dependency = capabilities.find((capability) =>
capability.startsWith(SpecCapability.fieldDependency),
);
return uiSchemaAccumulator.withMutations((mutable) => {
if (isAdvanced) {
const advancedPropertyName = _.last(uiSchemaPath);
const pathToAdvanced = [...uiSchemaPath.slice(0, uiSchemaPath.length - 1), 'ui:advanced'];
const pathToAdvanced = [...uiSchemaPath.slice(0, -1), 'ui:advanced'];
const currentAdvanced = mutable.getIn(pathToAdvanced) ?? Immutable.List();
mutable.setIn(pathToAdvanced, currentAdvanced.push(advancedPropertyName));
}
Expand Down