Skip to content

Commit cad8fb1

Browse files
clintandrewhallclaudekibanamachine
authored
[Content List] Add content editor integration (PR 12) (elastic#262630)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent 3825ed1 commit cad8fb1

38 files changed

Lines changed: 993 additions & 80 deletions

src/platform/packages/shared/content-management/content_editor/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
*/
99

1010
export { ContentEditorProvider, ContentEditorKibanaProvider, useOpenContentEditor } from './src';
11-
export type { OpenContentEditorParams } from './src';
11+
export type {
12+
OpenContentEditorParams,
13+
ContentEditorItem,
14+
ContentEditorCustomValidators,
15+
} from './src';
1216
export type { SavedObjectsReference } from './src/services';

src/platform/packages/shared/content-management/content_editor/src/components/editor_flyout_content.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,15 @@ export const ContentEditorFlyoutContent: FC<Props> = ({
7272
const form = useMetadataForm({ item, customValidators });
7373

7474
const hasNoChanges = () => {
75-
const itemTags = item.tags.map((obj) => obj.id).sort();
75+
const itemTags = item.tags.slice().sort();
7676
const formTags = form.tags.value.slice().sort();
77-
78-
const compareTags = (arr1: string[], arr2: string[]) => {
79-
if (arr1.length !== arr2.length) return false;
80-
return arr1.every((tag: string, index) => tag === arr2[index]);
81-
};
82-
8377
const description = item.description || '';
8478

8579
return (
8680
item.title === form.title.value &&
8781
description === form.description.value &&
88-
compareTags(itemTags, formTags)
82+
itemTags.length === formTags.length &&
83+
itemTags.every((tag, i) => tag === formTags[i])
8984
);
9085
};
9186

@@ -137,7 +132,6 @@ export const ContentEditorFlyoutContent: FC<Props> = ({
137132
defaultMessage: 'To edit these details, contact your administrator for access.',
138133
})
139134
}
140-
tagsReferences={item.tags}
141135
TagList={TagList}
142136
TagSelector={TagSelector}
143137
>

src/platform/packages/shared/content-management/content_editor/src/components/inspector_flyout_content.test.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ describe('<ContentEditorFlyoutContent />', () => {
3232
id: '123',
3333
title: 'Foo',
3434
description: 'Some description',
35-
tags: [
36-
{ id: 'id-1', name: 'tag1', type: 'tag' },
37-
{ id: 'id-2', name: 'tag2', type: 'tag' },
38-
],
35+
tags: ['id-1', 'id-2'],
3936
};
4037

4138
const mockedServices = getMockServices();
@@ -230,6 +227,20 @@ describe('<ContentEditorFlyoutContent />', () => {
230227
);
231228
});
232229

230+
test('should render tags in read-only mode', async () => {
231+
await act(async () => {
232+
testBed = await setup();
233+
});
234+
235+
const { exists, component } = testBed!;
236+
237+
expect(exists('tagList')).toBe(true);
238+
239+
const tagListHtml = component.find('[data-test-subj="tagList"]').text();
240+
expect(tagListHtml).toContain('id-1');
241+
expect(tagListHtml).toContain('id-2');
242+
});
243+
233244
test('should update the tag selection', async () => {
234245
const onSave = jest.fn();
235246

src/platform/packages/shared/content-management/content_editor/src/components/metadata_form.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@ import {
2121

2222
import { ContentEditorFlyoutWarningsCallOut } from './editor_flyout_warnings';
2323
import type { Field, MetadataFormState } from './use_metadata_form';
24-
import type { SavedObjectsReference, Services } from '../services';
24+
import type { Services } from '../services';
2525

2626
interface Props {
2727
form: MetadataFormState & {
2828
isSubmitted: boolean;
2929
};
3030
isReadonly: boolean;
3131
readonlyReason: string;
32-
tagsReferences: SavedObjectsReference[];
3332
TagList?: Services['TagList'];
3433
TagSelector?: Services['TagSelector'];
3534
}
@@ -38,7 +37,6 @@ const isFormFieldValid = (field: Field) => !Boolean(field.errors?.length);
3837

3938
export const MetadataForm: FC<React.PropsWithChildren<Props>> = ({
4039
form,
41-
tagsReferences,
4240
TagList,
4341
TagSelector,
4442
isReadonly,
@@ -114,7 +112,7 @@ export const MetadataForm: FC<React.PropsWithChildren<Props>> = ({
114112
/>
115113
</EuiFormRow>
116114

117-
{TagList && isReadonly && tagsReferences.length > 0 && (
115+
{TagList && isReadonly && tags.value.length > 0 && (
118116
<>
119117
<EuiSpacer />
120118
<EuiFormRow
@@ -124,7 +122,7 @@ export const MetadataForm: FC<React.PropsWithChildren<Props>> = ({
124122
fullWidth
125123
isDisabled={isReadonly}
126124
>
127-
<TagList references={tagsReferences} />
125+
<TagList tagIds={tags.value} />
128126
</EuiFormRow>
129127
</>
130128
)}

src/platform/packages/shared/content-management/content_editor/src/components/use_metadata_form.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ const executeValidation = async <TField extends keyof Fields>(
9696
return results;
9797
};
9898

99+
/** Normalize tags, treating `undefined` as an empty array. */
100+
const normalizeTagIds = (tags: string[] | undefined): string[] => tags ?? [];
101+
99102
export const useMetadataForm = ({
100103
item,
101104
customValidators,
@@ -111,7 +114,7 @@ export const useMetadataForm = ({
111114
isChangingValue: false,
112115
},
113116
tags: {
114-
value: item.tags ? item.tags.map(({ id }) => id) : [],
117+
value: normalizeTagIds(item.tags),
115118
isChangingValue: false,
116119
},
117120
});

src/platform/packages/shared/content-management/content_editor/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@
1010
export { ContentEditorProvider, ContentEditorKibanaProvider } from './services';
1111
export { useOpenContentEditor } from './open_content_editor';
1212
export type { OpenContentEditorParams } from './open_content_editor';
13+
export type { Item as ContentEditorItem } from './types';
14+
export type { CustomValidators as ContentEditorCustomValidators } from './components/use_metadata_form';

src/platform/packages/shared/content-management/content_editor/src/mocks.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import React, { useEffect, useState, useCallback } from 'react';
11-
import type { TagSelectorProps, SavedObjectsReference } from './services';
11+
import type { TagSelectorProps } from './services';
1212

1313
const tagsList = ['id-1', 'id-2', 'id-3', 'id-4', 'id-5'];
1414

@@ -46,19 +46,13 @@ export const TagSelector = ({ initialSelection, onTagsSelected }: TagSelectorPro
4646
};
4747

4848
export interface TagListProps {
49-
references?: SavedObjectsReference[];
49+
tagIds: string[];
5050
}
5151

52-
export const TagList = ({ references }: TagListProps) => {
53-
if (!references) {
54-
return null;
55-
}
56-
57-
return (
58-
<ul data-test-subj="tagList">
59-
{references.map((tag) => (
60-
<li key={tag.name}>{tag.name}</li>
61-
))}
62-
</ul>
63-
);
64-
};
52+
export const TagList = ({ tagIds }: TagListProps) => (
53+
<ul data-test-subj="tagList">
54+
{tagIds.map((id) => (
55+
<li key={id}>{id}</li>
56+
))}
57+
</ul>
58+
);
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import React from 'react';
11+
import { render } from '@testing-library/react';
12+
import { QueryClient, QueryClientProvider } from '@kbn/react-query';
13+
import { UserProfilesProvider } from '@kbn/content-management-user-profiles';
14+
import type { ContentEditorKibanaDependencies, TagSelectorProps } from './services';
15+
import { ContentEditorKibanaProvider, useServices } from './services';
16+
17+
type SavedObjectsTagging = NonNullable<ContentEditorKibanaDependencies['savedObjectsTagging']>;
18+
type PluginTagListProps = React.ComponentProps<SavedObjectsTagging['ui']['components']['TagList']>;
19+
20+
const createCoreMock = (): ContentEditorKibanaDependencies['core'] =>
21+
({
22+
analytics: {
23+
reportEvent: jest.fn(),
24+
},
25+
i18n: {},
26+
theme: {
27+
theme$: {},
28+
},
29+
userProfile: {},
30+
overlays: {
31+
openSystemFlyout: jest.fn(() => ({
32+
onClose: Promise.resolve(),
33+
close: () => Promise.resolve(),
34+
})),
35+
},
36+
notifications: {
37+
toasts: {
38+
addDanger: jest.fn(),
39+
},
40+
},
41+
rendering: {
42+
addContext: (element: React.ReactNode) => <>{element}</>,
43+
},
44+
} as unknown as ContentEditorKibanaDependencies['core']);
45+
46+
const createWrapper = (
47+
savedObjectsTagging: ContentEditorKibanaDependencies['savedObjectsTagging']
48+
) => {
49+
const queryClient = new QueryClient();
50+
51+
return ({ children }: { children: React.ReactNode }) => (
52+
<QueryClientProvider client={queryClient}>
53+
<UserProfilesProvider
54+
bulkGetUserProfiles={jest.fn()}
55+
getUserProfile={jest.fn()}
56+
suggestUserProfiles={jest.fn()}
57+
>
58+
<ContentEditorKibanaProvider
59+
core={createCoreMock()}
60+
savedObjectsTagging={savedObjectsTagging}
61+
>
62+
{children}
63+
</ContentEditorKibanaProvider>
64+
</UserProfilesProvider>
65+
</QueryClientProvider>
66+
);
67+
};
68+
69+
const TagListConsumer = ({ tagIds }: { tagIds: string[] }) => {
70+
const { TagList } = useServices();
71+
72+
if (!TagList) {
73+
return null;
74+
}
75+
76+
return <TagList tagIds={tagIds} />;
77+
};
78+
79+
describe('ContentEditorKibanaProvider', () => {
80+
test('adapts tag IDs to saved object references for the plugin TagList', () => {
81+
const PluginTagList = jest.fn<React.ReactElement | null, [PluginTagListProps]>(() => null);
82+
const SavedObjectSaveModalTagSelector = jest.fn<React.ReactElement | null, [TagSelectorProps]>(
83+
() => null
84+
);
85+
86+
render(<TagListConsumer tagIds={['tag-1', 'tag-2']} />, {
87+
wrapper: createWrapper({
88+
ui: {
89+
components: {
90+
TagList: PluginTagList,
91+
SavedObjectSaveModalTagSelector,
92+
},
93+
},
94+
}),
95+
});
96+
97+
expect(PluginTagList.mock.calls[0][0]).toEqual({
98+
object: {
99+
references: [
100+
{ id: 'tag-1', name: 'tag-tag-1', type: 'tag' },
101+
{ id: 'tag-2', name: 'tag-tag-2', type: 'tag' },
102+
],
103+
},
104+
});
105+
});
106+
});

src/platform/packages/shared/content-management/content_editor/src/services.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface Theme {
4747
export interface Services {
4848
openSystemFlyout(node: ReactNode, options?: OverlaySystemFlyoutOpenOptions): OverlayRef;
4949
notifyError: NotifyFn;
50-
TagList?: FC<{ references: SavedObjectsReference[] }>;
50+
TagList?: FC<{ tagIds: string[] }>;
5151
TagSelector?: React.FC<TagSelectorProps>;
5252
}
5353

@@ -133,11 +133,12 @@ export const ContentEditorKibanaProvider: FC<
133133
const { openSystemFlyout: coreOpenFlyout } = overlays;
134134

135135
const TagList = useMemo(() => {
136-
const Comp: Services['TagList'] = ({ references }) => {
136+
const Comp: Services['TagList'] = ({ tagIds }) => {
137137
if (!savedObjectsTagging?.ui.components.TagList) {
138138
return null;
139139
}
140140
const PluginTagList = savedObjectsTagging.ui.components.TagList;
141+
const references = tagIds.map((id) => ({ type: 'tag', id, name: `tag-${id}` }));
141142
return <PluginTagList object={{ references }} />;
142143
};
143144

src/platform/packages/shared/content-management/content_editor/src/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import type { SavedObjectsReference } from './services';
11-
1210
export interface Item {
1311
id: string;
1412
title: string;
1513
description?: string;
16-
tags: SavedObjectsReference[];
14+
tags: string[];
1715

1816
createdAt?: string;
1917
createdBy?: string;

0 commit comments

Comments
 (0)