feat: enable Rich Text as a creatable field type (#18634)
## Summary - Removes `RICH_TEXT` from the excluded/hidden field types in the settings UI so users can create rich text fields on any object (not just Note/Task) - Creates a generic `RichTextFieldEditor` component that uses standard `useUpdateOneRecord` for persistence, decoupled from the Note/Task-specific `ActivityRichTextEditor` - Updates the inline `RichTextFieldInput` and side panel to route to the appropriate editor based on object type (activity editor for Note/Task, generic editor for everything else) ## Details ### Tier 1 — Settings UI unlock - Removed `RICH_TEXT` from `excludedFieldTypes` in `SettingsObjectNewFieldSelect.tsx` - Removed `RICH_TEXT` from `SettingsExcludedFieldType` type union - Added `RICH_TEXT` to `previewableTypes` in `SettingsDataModelFieldSettingsFormCard` ### Tier 2 — Generic inline editing - New `RichTextFieldEditor` — a generic BlockNote editor that works for any object using `useUpdateOneRecord` (no activity-specific coupling) - `RichTextFieldInput` now branches: `ActivityRichTextEditor` for Note/Task, `RichTextFieldEditor` for all other objects - Generalized side panel state (`viewableRichTextComponentState`) from `activityId`/`activityObjectNameSingular` to `recordId`/`objectNameSingular`/`fieldName` - `useOpenRichTextInSidePanel` now accepts an optional `fieldName` parameter ### Tier 3 — Verification - Search: only `markdown` subfield is indexed (correct behavior) - Filters: `RichTextFilter` GraphQL input type already exists - Import/export: `markdown` subfield is already marked `isImportable: true`
This commit is contained in:
+15
@@ -74,4 +74,19 @@ describe('getFirstNonEmptyLineOfRichText', () => {
|
||||
const result = getFirstNonEmptyLineOfRichText(input);
|
||||
expect(result).toBe('First non-empty line');
|
||||
});
|
||||
|
||||
it('should handle non-array string content (e.g. heading blocks)', () => {
|
||||
const input = [{ content: 'Hello heading' }] as unknown as PartialBlock[];
|
||||
const result = getFirstNonEmptyLineOfRichText(input);
|
||||
expect(result).toBe('Hello heading');
|
||||
});
|
||||
|
||||
it('should skip empty non-array string content', () => {
|
||||
const input = [
|
||||
{ content: ' ' },
|
||||
{ content: [{ text: 'Fallback text', type: 'text', styles: {} }] },
|
||||
] as unknown as PartialBlock[];
|
||||
const result = getFirstNonEmptyLineOfRichText(input);
|
||||
expect(result).toBe('Fallback text');
|
||||
});
|
||||
});
|
||||
|
||||
+18
-12
@@ -9,19 +9,25 @@ export const getFirstNonEmptyLineOfRichText = (
|
||||
}
|
||||
for (const block of blocks) {
|
||||
if (!isUndefinedOrNull(block.content)) {
|
||||
const contentArray = block.content as Array<
|
||||
{ text: string } | { link: string }
|
||||
>;
|
||||
if (contentArray.length > 0) {
|
||||
for (const content of contentArray) {
|
||||
if ('link' in content) {
|
||||
return content.link;
|
||||
const contentArray = Array.isArray(block.content)
|
||||
? (block.content as Array<{ text: string } | { link: string }>)
|
||||
: [block.content as { text: string } | { link: string } | string];
|
||||
|
||||
for (const content of contentArray) {
|
||||
if (typeof content === 'string') {
|
||||
const value = content.trim();
|
||||
if (value !== '') {
|
||||
return value;
|
||||
}
|
||||
if ('text' in content) {
|
||||
const value = content.text.trim();
|
||||
if (value !== '') {
|
||||
return value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ('link' in content) {
|
||||
return content.link;
|
||||
}
|
||||
if ('text' in content) {
|
||||
const value = content.text.trim();
|
||||
if (value !== '') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user