Skip to content

Commit

Permalink
user preference for Create/Edit method (form/yaml)
Browse files Browse the repository at this point in the history
  • Loading branch information
nemesis09 committed Aug 20, 2021
1 parent b138d33 commit d07d518
Show file tree
Hide file tree
Showing 18 changed files with 337 additions and 40 deletions.
23 changes: 23 additions & 0 deletions frontend/packages/console-app/console-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,28 @@
},
"insertBefore": "topology.preferredView"
}
},
{
"type": "console.user-preference/item",
"properties": {
"id": "console.preferredCreateEditMethod",
"label": "%console-app~Create/Edit resource method%",
"groupId": "general",
"description": "%console-app~If both form and YAML are available, the console defaults to your selection.%",
"field": {
"type": "dropdown",
"userSettingsKey": "console.preferredCreateEditMethod",
"defaultValue": "latest",
"options": [
{
"value": "latest",
"label": "%console-app~Last viewed%"
},
{ "value": "form", "label": "%console-app~Form%" },
{ "value": "yaml", "label": "%console-app~YAML%" }
]
},
"insertAfter": "topology.preferredView"
}
}
]
6 changes: 5 additions & 1 deletion frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
"If a perspective is not selected, the console defaults to the last viewed.": "If a perspective is not selected, the console defaults to the last viewed.",
"Project": "Project",
"If a project is not selected, the console defaults to the last viewed.": "If a project is not selected, the console defaults to the last viewed.",
"Create/Edit resource method": "Create/Edit resource method",
"If both form and YAML are available, the console defaults to your selection.": "If both form and YAML are available, the console defaults to your selection.",
"Last viewed": "Last viewed",
"Form": "Form",
"YAML": "YAML",
"Delete {{kind}}": "Delete {{kind}}",
"Edit {{kind}}": "Edit {{kind}}",
"Edit labels": "Edit labels",
Expand Down Expand Up @@ -218,7 +223,6 @@
"Guided tour": "Guided tour",
"Step {{stepNumber, number}}/{{totalSteps, number}}": "Step {{stepNumber, number}}/{{totalSteps, number}}",
"guided tour {{step, number}}": "guided tour {{step, number}}",
"Last viewed": "Last viewed",
"Search project": "Search project",
"Search namespace": "Search namespace",
"No projects found": "No projects found",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './usePreferredCreateEditMethod';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useUserSettings } from '@console/shared';

export const PREFERRED_CREATE_EDIT_METHOD_USER_SETTING_VALUE_LATEST = 'latest';
const PREFERRED_CREATE_EDIT_METHOD_USER_SETTING_KEY = 'console.preferredCreateEditMethod';

export const usePreferredCreateEditMethod = (): [string, boolean] => {
const [preferredCreateEditMethod, , preferredCreateEditMethodLoaded] = useUserSettings<string>(
PREFERRED_CREATE_EDIT_METHOD_USER_SETTING_KEY,
);
return [preferredCreateEditMethod, preferredCreateEditMethodLoaded];
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import cx from 'classnames';
import { useField, useFormikContext, FormikValues } from 'formik';
import * as _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { LoadingBox } from '@console/internal/components/utils';
import { safeYAMLToJS, safeJSToYAML } from '../../utils/yaml';
import { EditorType } from '../synced-editor/editor-toggle';
import { useEditorType } from '../synced-editor/useEditorType';
import RadioGroupField from './RadioGroupField';

import './SyncedEditorField.scss';
Expand All @@ -29,6 +31,7 @@ type SyncedEditorFieldProps = {
name: string;
formContext: EditorContext<SanitizeToForm>;
yamlContext: EditorContext<SanitizeToYAML>;
lastViewUserSettingKey: string;
noMargin?: boolean;
};

Expand All @@ -37,10 +40,12 @@ const SyncedEditorField: React.FC<SyncedEditorFieldProps> = ({
formContext,
yamlContext,
noMargin = false,
lastViewUserSettingKey,
}) => {
const { t } = useTranslation();
const [field] = useField(name);

const { values, setFieldValue } = useFormikContext<FormikValues>();
const { t } = useTranslation();

const formData = _.get(values, formContext.name);
const yamlData: string = _.get(values, yamlContext.name);
Expand All @@ -49,7 +54,19 @@ const SyncedEditorField: React.FC<SyncedEditorFieldProps> = ({
const [sanitizeToCallback, setSanitizeToCallback] = React.useState<FormErrorCallback>(undefined);
const [disabledFormAlert, setDisabledFormAlert] = React.useState<boolean>(formContext.isDisabled);

const changeEditorType = (newType: EditorType): void => {
const isEditorTypeEnabled = (type: EditorType): boolean =>
!(type === EditorType.Form ? formContext?.isDisabled : yamlContext?.isDisabled);

const [editorType, setEditorType, resourceLoaded] = useEditorType(
lastViewUserSettingKey,
field.value as EditorType,
isEditorTypeEnabled,
);

const loaded = resourceLoaded && field.value === editorType;

const changeEditorType = (newType: EditorType) => {
setEditorType(newType);
setFieldValue(name, newType);
};

Expand Down Expand Up @@ -118,9 +135,12 @@ const SyncedEditorField: React.FC<SyncedEditorFieldProps> = ({

React.useEffect(() => {
setDisabledFormAlert(formContext.isDisabled);
}, [formContext.isDisabled]);
if (field.value !== editorType) {
setFieldValue(name, editorType);
}
}, [editorType, field.value, formContext.isDisabled, name, setFieldValue]);

return (
return loaded ? (
<>
<div
className={cx('ocs-synced-editor-field__editor-toggle', { margin: !noMargin })}
Expand Down Expand Up @@ -172,8 +192,12 @@ const SyncedEditorField: React.FC<SyncedEditorFieldProps> = ({
isInline
/>
)}
{field.value === EditorType.Form ? formContext.editor : yamlContext.editor}
{editorType === EditorType.Form && !disabledFormAlert
? formContext.editor
: yamlContext.editor}
</>
) : (
<LoadingBox />
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as React from 'react';
import { Alert } from '@patternfly/react-core';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import { useField } from 'formik';
import * as _ from 'lodash';
import { LoadingBox } from '@console/internal/components/utils';
import { EditorType } from '../../synced-editor/editor-toggle';
import { useEditorType } from '../../synced-editor/useEditorType';
import DynamicFormField from '../DynamicFormField';
import RadioGroupField from '../RadioGroupField';
import SyncedEditorField from '../SyncedEditorField';
Expand All @@ -24,29 +27,47 @@ jest.mock('formik', () => ({
})),
}));

jest.mock('../../synced-editor/useEditorType', () => ({
useEditorType: jest.fn(),
}));

const mockUseEditorType = useEditorType as jest.Mock;
const mockUseField = useField as jest.Mock;
const i18nNS = 'console-shared';

const mockEditors = {
form: <DynamicFormField name="formData" schema={{}} />,
yaml: <YAMLEditorField name="yamlData" />,
};
describe('SyncedEditorField', () => {
type SyncedEditorFieldProps = React.ComponentProps<typeof SyncedEditorField>;
let wrapper: ShallowWrapper<SyncedEditorFieldProps>;

const mockEditors = {
form: <DynamicFormField name="formData" schema={{}} />,
yaml: <YAMLEditorField name="yamlData" />,
};

const props: SyncedEditorFieldProps = {
name: 'editorType',
formContext: {
name: 'formData',
editor: mockEditors.form,
isDisabled: false,
},
yamlContext: {
name: 'yamlData',
editor: mockEditors.yaml,
isDisabled: false,
},
lastViewUserSettingKey: 'key',
};

afterEach(() => {
mockUseField.mockReset();
mockUseEditorType.mockReset();
});

const props = {
name: 'editorType',
formContext: {
name: 'formData',
editor: mockEditors.form,
isDisabled: false,
},
yamlContext: {
name: 'yamlData',
editor: mockEditors.yaml,
isDisabled: false,
},
};
describe('DropdownField', () => {
it('should render radio group field inline', () => {
const wrapper = shallow(<SyncedEditorField {...props} />);
mockUseField.mockReturnValue([{ value: EditorType.Form }, {}]);
mockUseEditorType.mockReturnValue([EditorType.Form, jest.fn(), true]);
wrapper = shallow(<SyncedEditorField {...props} />);
expect(wrapper.find(RadioGroupField).exists()).toBe(true);
expect(
wrapper
Expand All @@ -56,21 +77,26 @@ describe('DropdownField', () => {
).toBe(true);
});

it('should render dynamic form field if initial editor type is form', () => {
const wrapper = shallow(<SyncedEditorField {...props} />);
it('should render dynamic form field if useEditorType returns form', () => {
mockUseField.mockReturnValue([{ value: EditorType.Form }, {}]);
mockUseEditorType.mockReturnValue([EditorType.Form, jest.fn(), true]);
wrapper = shallow(<SyncedEditorField {...props} />);
expect(wrapper.find(DynamicFormField).exists()).toBe(true);
});

it('should render dynamic form field if initial editor type is form', () => {
(useField as any).mockImplementation(() => [{ value: 'yaml' }, {}]);
const wrapper = shallow(<SyncedEditorField {...props} />);
it('should render dynamic yaml field if useEditorType returns yaml', () => {
mockUseField.mockReturnValue([{ value: EditorType.YAML }, {}]);
mockUseEditorType.mockReturnValue([EditorType.YAML, jest.fn(), true]);
wrapper = shallow(<SyncedEditorField {...props} />);
expect(wrapper.find(YAMLEditorField).exists()).toBe(true);
});

it('should disable corresponding radio button if any editor context is disaled', () => {
it('should disable corresponding radio button if any editor context is disabled', () => {
mockUseField.mockReturnValue([{ value: EditorType.Form }, {}]);
mockUseEditorType.mockReturnValue([EditorType.Form, jest.fn(), true]);
const newProps = _.cloneDeep(props);
newProps.yamlContext.isDisabled = true;
const wrapper = shallow(<SyncedEditorField {...newProps} />);
wrapper = shallow(<SyncedEditorField {...newProps} />);
expect(
wrapper
.find(RadioGroupField)
Expand All @@ -80,9 +106,11 @@ describe('DropdownField', () => {
});

it('should show an alert if form context is disaled', () => {
mockUseField.mockReturnValue([{ value: EditorType.YAML }, {}]);
mockUseEditorType.mockReturnValue([EditorType.YAML, jest.fn(), true]);
const newProps = _.cloneDeep(props);
newProps.formContext.isDisabled = true;
const wrapper = shallow(<SyncedEditorField {...newProps} />);
wrapper = shallow(<SyncedEditorField {...newProps} />);
expect(wrapper.find(Alert).exists()).toBe(true);
expect(
wrapper
Expand All @@ -91,4 +119,18 @@ describe('DropdownField', () => {
.props().title,
).toBe(`${i18nNS}~Form view is disabled for this chart because the schema is not available`);
});

it('should render LoadingBox if useEditorType returns false for loaded', () => {
mockUseField.mockReturnValue([{ value: EditorType.YAML }, {}]);
mockUseEditorType.mockReturnValue([EditorType.YAML, jest.fn(), false]);
wrapper = shallow(<SyncedEditorField {...props} />);
expect(wrapper.find(LoadingBox).exists()).toBe(true);
});

it('should render LoadingBox if formik field value does not match with editorType returned by useEditorType', () => {
mockUseField.mockReturnValue([{ value: EditorType.Form }, {}]);
mockUseEditorType.mockReturnValue([EditorType.YAML, jest.fn(), true]);
wrapper = shallow(<SyncedEditorField {...props} />);
expect(wrapper.find(LoadingBox).exists()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
PREFERRED_CREATE_EDIT_METHOD_USER_SETTING_VALUE_LATEST,
usePreferredCreateEditMethod,
} from '@console/app/src/components/user-preferences/synced-editor';
import { useUserSettings } from '@console/shared';
import { testHook } from '../../../../../../__tests__/utils/hooks-utils';
import { EditorType } from '../editor-toggle';
import { useEditorType } from '../useEditorType';

jest.mock('@console/shared/src/hooks/useUserSettings', () => ({
useUserSettings: jest.fn(),
}));

jest.mock(
'@console/app/src/components/user-preferences/synced-editor/usePreferredCreateEditMethod',
() => ({
usePreferredCreateEditMethod: jest.fn(),
}),
);

const mockUserSettings = useUserSettings as jest.Mock;
const mockUsePreferredCreateEditMethod = usePreferredCreateEditMethod as jest.Mock;

describe('useEditorType', () => {
const lastViewUserSettingKey = 'key';
const defaultValue = EditorType.Form;

afterEach(() => {
jest.resetAllMocks();
});

it('should return editor type corresponding to preferred editor type if it is defined and enabled', () => {
mockUserSettings.mockReturnValue([EditorType.Form, jest.fn(), true]);
mockUsePreferredCreateEditMethod.mockReturnValue([EditorType.YAML, true]);
const { result } = testHook(() => useEditorType(lastViewUserSettingKey, defaultValue));
const [editorType, , loaded] = result.current;
expect(editorType).toEqual(EditorType.YAML);
expect(loaded).toBe(true);
});

it(`should return editor type corresponding to last viewed editor type if it is defined and enabled and preferred editor type is ${PREFERRED_CREATE_EDIT_METHOD_USER_SETTING_VALUE_LATEST}`, () => {
mockUserSettings.mockReturnValue([EditorType.YAML, jest.fn(), true]);
mockUsePreferredCreateEditMethod.mockReturnValue([
PREFERRED_CREATE_EDIT_METHOD_USER_SETTING_VALUE_LATEST,
true,
]);
const { result } = testHook(() => useEditorType(lastViewUserSettingKey, defaultValue));
const [editorType, , loaded] = result.current;
expect(editorType).toEqual(EditorType.YAML);
expect(loaded).toBe(true);
});

it('should return editor type corresponding to last viewed editor type if it is defined and enabled preferred editor type is defined but disabled', () => {
mockUserSettings.mockReturnValue([EditorType.YAML, jest.fn(), true]);
mockUsePreferredCreateEditMethod.mockReturnValue([EditorType.Form, true]);
const { result } = testHook(() =>
useEditorType(
lastViewUserSettingKey,
defaultValue,
(type: string) => !(type === EditorType.Form),
),
);
const [editorType, , loaded] = result.current;
expect(editorType).toEqual(EditorType.YAML);
expect(loaded).toBe(true);
});

it('should return editor type corresponding to last viewed editor type if it is defined and enabled and preferred editor type is not defined', () => {
mockUserSettings.mockReturnValue([EditorType.YAML, jest.fn(), true]);
mockUsePreferredCreateEditMethod.mockReturnValue([undefined, true]);
const { result } = testHook(() => useEditorType(lastViewUserSettingKey, defaultValue));
const [editorType, , loaded] = result.current;
expect(editorType).toEqual(EditorType.YAML);
expect(loaded).toBe(true);
});

it('should return editor type corresponding to default value if both preferred and last viewed editor type are not defined or disabled', () => {
mockUserSettings.mockReturnValue([undefined, jest.fn(), true]);
mockUsePreferredCreateEditMethod.mockReturnValue([undefined, true]);
const { result } = testHook(() => useEditorType(lastViewUserSettingKey, defaultValue));
const [editorType, , loaded] = result.current;
expect(editorType).toEqual(defaultValue);
expect(loaded).toBe(true);
});

it('should return false for loaded if preferred editor type has not loaded', () => {
mockUserSettings.mockReturnValue([EditorType.YAML, jest.fn(), true]);
mockUsePreferredCreateEditMethod.mockReturnValue([undefined, false]);
const { result } = testHook(() => useEditorType(lastViewUserSettingKey, defaultValue));
const [editorType, , loaded] = result.current;
expect(editorType).toEqual(EditorType.YAML);
expect(loaded).toBe(false);
});

it('should return false for loaded if last viewed editor type has not loaded', () => {
mockUserSettings.mockReturnValue([undefined, jest.fn(), false]);
mockUsePreferredCreateEditMethod.mockReturnValue([EditorType.YAML, true]);
const { result } = testHook(() => useEditorType(lastViewUserSettingKey, defaultValue));
const [editorType, , loaded] = result.current;
expect(editorType).toEqual(EditorType.YAML);
expect(loaded).toBe(false);
});
});

0 comments on commit d07d518

Please sign in to comment.