mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-06 18:48:50 +01:00
🏷️ refactor: EditPresetDialog UI and Remove chatGptLabel from Presets (#7543)
* fix: add necessary dep., remove unnecessary dep from useMentions memoization * fix: Migrate deprecated chatGptLabel to modelLabel in cleanupPreset and simplify getPresetTitle logic * fix: Enhance cleanupPreset to remove empty chatGptLabel and add comprehensive tests for label migration and preset handling * chore: Update endpointType prop in PopoverButtons to allow null values for better flexibility * refactor: Replace Dialog with OGDialog in EditPresetDialog for improved UI consistency and structure * style: Update EditPresetDialog layout and styling for improved responsiveness and consistency
This commit is contained in:
parent
fc8d24fa5b
commit
b45ff8e4ed
7 changed files with 717 additions and 122 deletions
224
client/src/utils/__tests__/cleanupPreset.test.ts
Normal file
224
client/src/utils/__tests__/cleanupPreset.test.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import cleanupPreset from '../cleanupPreset';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
|
||||
// Mock parseConvo since we're focusing on testing the chatGptLabel migration logic
|
||||
jest.mock('librechat-data-provider', () => ({
|
||||
...jest.requireActual('librechat-data-provider'),
|
||||
parseConvo: jest.fn((input) => {
|
||||
// Return a simplified mock that passes through most properties
|
||||
const { conversation } = input;
|
||||
return {
|
||||
...conversation,
|
||||
model: conversation?.model || 'gpt-3.5-turbo',
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('cleanupPreset', () => {
|
||||
const basePreset = {
|
||||
presetId: 'test-preset-id',
|
||||
title: 'Test Preset',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4',
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('chatGptLabel migration', () => {
|
||||
it('should migrate chatGptLabel to modelLabel when only chatGptLabel exists', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: 'Custom ChatGPT Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Custom ChatGPT Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prioritize modelLabel over chatGptLabel when both exist', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: 'Old ChatGPT Label',
|
||||
modelLabel: 'New Model Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('New Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep modelLabel when only modelLabel exists', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Existing Model Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Existing Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle preset without either label', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBeUndefined();
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty chatGptLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: '',
|
||||
modelLabel: 'Valid Model Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Valid Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not migrate empty string chatGptLabel when modelLabel exists', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: '',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBeUndefined();
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('presetOverride handling', () => {
|
||||
it('should apply presetOverride and then handle label migration', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: 'Original Label',
|
||||
presetOverride: {
|
||||
modelLabel: 'Override Model Label',
|
||||
temperature: 0.9,
|
||||
},
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Override Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
expect(result.temperature).toBe(0.9);
|
||||
});
|
||||
|
||||
it('should handle label migration in presetOverride', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
presetOverride: {
|
||||
chatGptLabel: 'Override ChatGPT Label',
|
||||
},
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Override ChatGPT Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle undefined preset', () => {
|
||||
const result = cleanupPreset({ preset: undefined });
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: null,
|
||||
presetId: null,
|
||||
title: 'New Preset',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle preset with null endpoint', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
endpoint: null,
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: null,
|
||||
presetId: 'test-preset-id',
|
||||
title: 'Test Preset',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle preset with empty string endpoint', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
endpoint: '',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: null,
|
||||
presetId: 'test-preset-id',
|
||||
title: 'Test Preset',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normal preset properties', () => {
|
||||
it('should preserve all other preset properties', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
promptPrefix: 'Custom prompt:',
|
||||
temperature: 0.8,
|
||||
top_p: 0.9,
|
||||
modelLabel: 'Custom Model',
|
||||
tools: ['plugin1', 'plugin2'],
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.presetId).toBe('test-preset-id');
|
||||
expect(result.title).toBe('Test Preset');
|
||||
expect(result.endpoint).toBe(EModelEndpoint.openAI);
|
||||
expect(result.modelLabel).toBe('Custom Model');
|
||||
expect(result.promptPrefix).toBe('Custom prompt:');
|
||||
expect(result.temperature).toBe(0.8);
|
||||
expect(result.top_p).toBe(0.9);
|
||||
expect(result.tools).toEqual(['plugin1', 'plugin2']);
|
||||
});
|
||||
|
||||
it('should generate default title when title is missing', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: undefined,
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.title).toBe('New Preset');
|
||||
});
|
||||
|
||||
it('should handle null presetId', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
presetId: null,
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.presetId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
362
client/src/utils/__tests__/presets.test.ts
Normal file
362
client/src/utils/__tests__/presets.test.ts
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import { getPresetTitle, removeUnavailableTools } from '../presets';
|
||||
import type { TPreset, TPlugin } from 'librechat-data-provider';
|
||||
|
||||
describe('presets utils', () => {
|
||||
describe('getPresetTitle', () => {
|
||||
const basePreset: TPreset = {
|
||||
presetId: 'test-id',
|
||||
title: 'Test Preset',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4',
|
||||
};
|
||||
|
||||
describe('with modelLabel', () => {
|
||||
it('should use modelLabel as the label', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model Name',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4 (Custom Model Name)');
|
||||
});
|
||||
|
||||
it('should prioritize modelLabel over deprecated chatGptLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'New Model Label',
|
||||
chatGptLabel: 'Old ChatGPT Label',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4 (New Model Label)');
|
||||
});
|
||||
|
||||
it('should handle title that includes the label', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'Custom Model Name Settings',
|
||||
modelLabel: 'Custom Model Name',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Custom Model Name Settings: gpt-4 (Custom Model Name)');
|
||||
});
|
||||
|
||||
it('should handle case-insensitive title matching', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'custom model name preset',
|
||||
modelLabel: 'Custom Model Name',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('custom model name preset: gpt-4 (Custom Model Name)');
|
||||
});
|
||||
|
||||
it('should use label as title when label includes the title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'GPT',
|
||||
modelLabel: 'Custom GPT Assistant',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Custom GPT Assistant: gpt-4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without modelLabel', () => {
|
||||
it('should work without modelLabel', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4');
|
||||
});
|
||||
|
||||
it('should handle empty modelLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: '',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4');
|
||||
});
|
||||
|
||||
it('should handle null modelLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: null,
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('title variations', () => {
|
||||
it('should handle missing title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: null,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('gpt-4 (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle empty title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: '',
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('gpt-4 (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle "New Chat" title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'New Chat',
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('gpt-4 (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle title with whitespace', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: ' ',
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe(': gpt-4 (Custom Model)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mention mode', () => {
|
||||
it('should return mention format with all components', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model',
|
||||
promptPrefix: 'You are a helpful assistant',
|
||||
tools: ['plugin1', 'plugin2'] as string[],
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe(
|
||||
'gpt-4 | Custom Model | You are a helpful assistant | plugin1, plugin2',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle mention format with object tools', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model',
|
||||
tools: [
|
||||
{ pluginKey: 'plugin1', name: 'Plugin 1' } as TPlugin,
|
||||
{ pluginKey: 'plugin3', name: 'Plugin 3' } as TPlugin,
|
||||
] as TPlugin[],
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4 | Custom Model | plugin1, plugin3');
|
||||
});
|
||||
|
||||
it('should handle mention format with minimal data', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4');
|
||||
});
|
||||
|
||||
it('should handle mention format with only modelLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4 | Custom Model');
|
||||
});
|
||||
|
||||
it('should handle mention format with only promptPrefix', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
promptPrefix: 'Custom prompt',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4 | Custom prompt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing model', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
model: null,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle undefined model', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
model: undefined,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: (Custom Model)');
|
||||
});
|
||||
|
||||
it('should trim the final result', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: '',
|
||||
model: '',
|
||||
modelLabel: '',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUnavailableTools', () => {
|
||||
const basePreset: TPreset = {
|
||||
presetId: 'test-id',
|
||||
title: 'Test Preset',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4',
|
||||
};
|
||||
|
||||
const availableTools: Record<string, TPlugin | undefined> = {
|
||||
plugin1: { pluginKey: 'plugin1', name: 'Plugin 1' } as TPlugin,
|
||||
plugin2: { pluginKey: 'plugin2', name: 'Plugin 2' } as TPlugin,
|
||||
plugin3: { pluginKey: 'plugin3', name: 'Plugin 3' } as TPlugin,
|
||||
};
|
||||
|
||||
it('should remove unavailable tools from string array', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['plugin1', 'unavailable-plugin', 'plugin2'] as string[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.tools).toEqual(['plugin1', 'plugin2']);
|
||||
});
|
||||
|
||||
it('should remove unavailable tools from object array', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: [
|
||||
{ pluginKey: 'plugin1', name: 'Plugin 1' } as TPlugin,
|
||||
{ pluginKey: 'unavailable-plugin', name: 'Unavailable' } as TPlugin,
|
||||
{ pluginKey: 'plugin2', name: 'Plugin 2' } as TPlugin,
|
||||
] as TPlugin[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.tools).toEqual(['plugin1', 'plugin2']);
|
||||
});
|
||||
|
||||
it('should handle preset without tools', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result).toEqual(preset);
|
||||
});
|
||||
|
||||
it('should handle preset with empty tools array', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: [] as string[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove all tools when none are available', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['unavailable1', 'unavailable2'] as string[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, {});
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve all other preset properties', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['plugin1'] as string[],
|
||||
modelLabel: 'Custom Model',
|
||||
temperature: 0.8,
|
||||
promptPrefix: 'Test prompt',
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.presetId).toBe(preset.presetId);
|
||||
expect(result.title).toBe(preset.title);
|
||||
expect(result.endpoint).toBe(preset.endpoint);
|
||||
expect(result.model).toBe(preset.model);
|
||||
expect(result.modelLabel).toBe(preset.modelLabel);
|
||||
expect(result.temperature).toBe(preset.temperature);
|
||||
expect(result.promptPrefix).toBe(preset.promptPrefix);
|
||||
expect(result.tools).toEqual(['plugin1']);
|
||||
});
|
||||
|
||||
it('should not mutate the original preset', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['plugin1', 'unavailable-plugin'] as string[],
|
||||
};
|
||||
const originalTools = [...preset.tools];
|
||||
|
||||
removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(preset.tools).toEqual(originalTools);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -20,6 +20,21 @@ const cleanupPreset = ({ preset: _preset }: TCleanupPreset): TPreset => {
|
|||
const { presetOverride = {}, ...rest } = _preset ?? {};
|
||||
const preset = { ...rest, ...presetOverride };
|
||||
|
||||
// Handle deprecated chatGptLabel field
|
||||
// If both chatGptLabel and modelLabel exist, prioritize modelLabel and remove chatGptLabel
|
||||
// If only chatGptLabel exists, migrate it to modelLabel
|
||||
if (preset.chatGptLabel && preset.modelLabel) {
|
||||
// Both exist: prioritize modelLabel, remove chatGptLabel
|
||||
delete preset.chatGptLabel;
|
||||
} else if (preset.chatGptLabel && !preset.modelLabel) {
|
||||
// Only chatGptLabel exists: migrate to modelLabel
|
||||
preset.modelLabel = preset.chatGptLabel;
|
||||
delete preset.chatGptLabel;
|
||||
} else if ('chatGptLabel' in preset) {
|
||||
// chatGptLabel exists but is empty/falsy: remove it
|
||||
delete preset.chatGptLabel;
|
||||
}
|
||||
|
||||
/* @ts-ignore: endpoint can be a custom defined name */
|
||||
const parsedPreset = parseConvo({ endpoint, endpointType, conversation: preset });
|
||||
|
||||
|
|
|
|||
|
|
@ -17,18 +17,10 @@ export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
|
|||
let title = '';
|
||||
let label = '';
|
||||
|
||||
const usesChatGPTLabel: TEndpoints = [
|
||||
EModelEndpoint.azureOpenAI,
|
||||
EModelEndpoint.openAI,
|
||||
EModelEndpoint.custom,
|
||||
];
|
||||
const usesModelLabel: TEndpoints = [EModelEndpoint.google, EModelEndpoint.anthropic];
|
||||
|
||||
if (endpoint != null && endpoint && usesChatGPTLabel.includes(endpoint)) {
|
||||
label = chatGptLabel ?? '';
|
||||
} else if (endpoint != null && endpoint && usesModelLabel.includes(endpoint)) {
|
||||
label = modelLabel ?? '';
|
||||
if (modelLabel) {
|
||||
label = modelLabel;
|
||||
}
|
||||
|
||||
if (
|
||||
label &&
|
||||
presetTitle != null &&
|
||||
|
|
@ -47,13 +39,13 @@ export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
|
|||
}${
|
||||
tools
|
||||
? ` | ${tools
|
||||
.map((tool: TPlugin | string) => {
|
||||
if (typeof tool === 'string') {
|
||||
return tool;
|
||||
}
|
||||
return tool.pluginKey;
|
||||
})
|
||||
.join(', ')}`
|
||||
.map((tool: TPlugin | string) => {
|
||||
if (typeof tool === 'string') {
|
||||
return tool;
|
||||
}
|
||||
return tool.pluginKey;
|
||||
})
|
||||
.join(', ')}`
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue