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

feat: enable passing of documentation link to form properties panel #1201

Merged
merged 1 commit into from
Jun 6, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import { reduce, isArray } from 'min-dash';

import { FormPropertiesPanelContext } from './context';

import { PropertiesPanelHeaderProvider } from './PropertiesPanelHeaderProvider';
import { getPropertiesPanelHeaderProvider } from './PropertiesPanelHeaderProvider';
import { PropertiesPanelPlaceholderProvider } from './PropertiesPanelPlaceholderProvider';

const EMPTY = {};

export function PropertiesPanel(props) {
const { eventBus, getProviders, injector } = props;

const formEditor = injector.get('formEditor');
const modeling = injector.get('modeling');
const selectionModule = injector.get('selection');
const propertiesPanelConfig = injector.get('config.propertiesPanel') || {};
const propertiesPanelConfig = injector.get('config.propertiesPanel') || EMPTY;

const { feelPopupContainer } = propertiesPanelConfig;

Expand Down Expand Up @@ -83,6 +85,17 @@ export function PropertiesPanel(props) {
);
}, [providers, selectedFormField, editField]);

const formFields = getService('formFields');

const PropertiesPanelHeaderProvider = useMemo(
() =>
getPropertiesPanelHeaderProvider({
getDocumentationRef: propertiesPanelConfig.getDocumentationRef,
formFields,
}),
[formFields, propertiesPanelConfig],
);
Comment on lines +90 to +97
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this memo really necessary? If you didn't see any noticeable performance issue without it we should remove to avoid memory leaks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nothing is necessary but not having this means we re-render the whole header every render cycle. Which isn't ideal. It won't lag, yet, but could cause problems down the line.

Basically, this header provider stuff is a config, and we need to keep the reference to the config stable, hence the memo.

Copy link
Contributor

@vsgoulart vsgoulart Jun 6, 2024

Choose a reason for hiding this comment

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

@Skaiir Just rerendering is not an issue, as long as rerendering doesn't take long. Also, not all rerenders are committed to the DOM which is usually the most expensive.

But overusing memoization might cause memory leaks as explained here

It's better to only memoize when it's accompanied by some performance issue, and even then most of the time you can easily just restructure your logic.


return (
<div
class="fjs-properties-panel"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,50 @@
import { textToLabel } from './Util';

import { iconsByType } from '../../render/components/icons';

import { getPaletteIcon } from '../palette/components/Palette';

import { useService } from './hooks';

const headerlessTypes = ['spacer', 'separator', 'expression', 'html'];

export const PropertiesPanelHeaderProvider = {
getElementLabel: (field) => {
const { type } = field;

if (headerlessTypes.includes(type)) {
return '';
}

if (type === 'text') {
return textToLabel(field.text);
}

if (type === 'image') {
return field.alt;
}

if (type === 'default') {
return field.id;
}

return field.label;
},

getElementIcon: (field) => {
const { type } = field;

// @Note: We know that we are inside the properties panel context,
// so we can savely use the hook here.
// eslint-disable-next-line react-hooks/rules-of-hooks
const fieldDefinition = useService('formFields').get(type).config;

const Icon = fieldDefinition.icon || iconsByType(type);

if (Icon) {
return () => <Icon width="36" height="36" viewBox="0 0 54 54" />;
} else if (fieldDefinition.iconUrl) {
return getPaletteIcon({ iconUrl: fieldDefinition.iconUrl, label: fieldDefinition.label });
}
},

getTypeLabel: (field) => {
const { type } = field;

if (type === 'default') {
return 'Form';
}

// @Note: We know that we are inside the properties panel context,
// so we can savely use the hook here.
// eslint-disable-next-line react-hooks/rules-of-hooks
const fieldDefinition = useService('formFields').get(type).config;

return fieldDefinition.label || type;
},
};
export function getPropertiesPanelHeaderProvider(options = {}) {
const { getDocumentationRef, formFields } = options;

return {
getElementLabel: (field) => {
const { type } = field;
if (headerlessTypes.includes(type)) {
return '';
}
if (type === 'text') {
return textToLabel(field.text);
}
if (type === 'image') {
return field.alt;
}
if (type === 'default') {
return field.id;
}
return field.label;
},

getElementIcon: (field) => {
const { type } = field;
const fieldDefinition = formFields.get(type).config;
const Icon = fieldDefinition.icon || iconsByType(type);
if (Icon) {
return () => <Icon width="36" height="36" viewBox="0 0 54 54" />;
} else if (fieldDefinition.iconUrl) {
return getPaletteIcon({ iconUrl: fieldDefinition.iconUrl, label: fieldDefinition.label });
}
},

getTypeLabel: (field) => {
const { type } = field;
if (type === 'default') {
return 'Form';
}
const fieldDefinition = formFields.get(type).config;
return fieldDefinition.label || type;
},

getDocumentationRef,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cleanup, render } from '@testing-library/preact/pure';

import { FormFields } from '@bpmn-io/form-js-viewer';

import { PropertiesPanelHeaderProvider } from '../../../../src/features/properties-panel/PropertiesPanelHeaderProvider';
import { getPropertiesPanelHeaderProvider } from '../../../../src/features/properties-panel/PropertiesPanelHeaderProvider';

import { MockPropertiesPanelContext, TestPropertiesPanel } from './helper';

Expand Down Expand Up @@ -50,6 +50,22 @@ describe('PropertiesPanelHeaderProvider', function () {
expect(label.innerText).to.eql(field.label);
});

it('should render documentation link', function () {
// given
const field = { type: 'textfield' };

const getDocumentationRef = () => 'https://example.com/';

// when
const { container } = renderHeader({ field, getDocumentationRef });

// then
const documentationLink = container.querySelector('.bio-properties-panel-header-link');

expect(documentationLink).to.exist;
expect(documentationLink.href).to.eql(getDocumentationRef());
});

describe('extension support', function () {
it('should render type label from config', function () {
// given
Expand Down Expand Up @@ -130,7 +146,7 @@ describe('PropertiesPanelHeaderProvider', function () {

// helpers /////////

function renderHeader({ services, ...restOptions }) {
function renderHeader({ services = {}, getDocumentationRef, ...restOptions }) {
const defaultField = { type: 'textfield' };

const options = {
Expand All @@ -140,7 +156,13 @@ function renderHeader({ services, ...restOptions }) {

return render(
<MockPropertiesPanelContext options={options} services={services}>
<TestPropertiesPanel field={options.field} headerProvider={PropertiesPanelHeaderProvider} />
<TestPropertiesPanel
field={options.field}
headerProvider={getPropertiesPanelHeaderProvider({
formFields: services.formFields || new FormFields(),
getDocumentationRef,
})}
/>
</MockPropertiesPanelContext>,
);
}
41 changes: 41 additions & 0 deletions packages/form-js-playground/test/spec/Playground.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,47 @@ describe('playground', function () {
expect(playground).to.exist;
});

it('should render with documentation links', async function () {
// given
const data = {};

const getDocumentationRef = (field) => {
if (field.type === 'default') {
return 'https://example.com/default';
}

return 'https://example.com/other';
};

// when
await act(() => {
playground = new Playground({
container,
schema,
data,
propertiesPanel: {
getDocumentationRef,
},
});
});

// then
expect(playground).to.exist;

const documentationLink = domQuery('.bio-properties-panel-header-link', container);
expect(documentationLink).to.exist;
expect(documentationLink.href).to.eql('https://example.com/default');

// when
const formField = domQuery('.fjs-form-field', container);
await act(() => formField.click());

// then
const documentationLinkSelected = domQuery('.bio-properties-panel-header-link', container);
expect(documentationLinkSelected).to.exist;
expect(documentationLinkSelected.href).to.eql('https://example.com/other');
});

it('should append sample data', async function () {
// given
const attrs = {
Expand Down
Loading