mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): remove design file mentions with chips (#3204)
This commit is contained in:
parent
7a9dcf38d7
commit
259295419a
2 changed files with 141 additions and 0 deletions
|
|
@ -1234,6 +1234,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
|
|||
function removeStaged(p: string) {
|
||||
setStaged((s) => s.filter((a) => a.path !== p));
|
||||
setStagedVisualComments((current) => current.filter((attachment) => attachment.screenshotPath !== p));
|
||||
setDraft((current) => stripInlineMentionToken(current, p));
|
||||
}
|
||||
|
||||
function removeCommentAttachment(id: string) {
|
||||
|
|
@ -2893,6 +2894,14 @@ function escapeRegExp(value: string): string {
|
|||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function stripInlineMentionToken(text: string, label: string): string {
|
||||
const token = inlineMentionToken(label);
|
||||
return text.replace(
|
||||
new RegExp(`(^|[\\s([{"'])${escapeRegExp(token)}(?=$|\\s|[.,;:!?)}\\]"'])([^\\S\\r\\n])?`, 'g'),
|
||||
'$1',
|
||||
);
|
||||
}
|
||||
|
||||
function loadComposerDraft(key?: string): string | null {
|
||||
if (!key || typeof window === 'undefined') return null;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -328,6 +328,138 @@ describe('ChatComposer context pickers', () => {
|
|||
expect(screen.getByTestId('chat-composer-mention-overlay').textContent).toContain('@My Export');
|
||||
});
|
||||
|
||||
it('removes the inline design file token when its staged chip is removed', async () => {
|
||||
renderComposer({
|
||||
projectFiles: [
|
||||
{
|
||||
path: 'designs/landing.html',
|
||||
name: 'landing.html',
|
||||
kind: 'html',
|
||||
mime: 'text/html',
|
||||
mtime: 1,
|
||||
size: 128,
|
||||
},
|
||||
],
|
||||
});
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'Use @landing', selectionStart: 12 },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
|
||||
fireEvent.click(screen.getByText('designs/landing.html'));
|
||||
|
||||
expect(input.value).toBe('Use @designs/landing.html ');
|
||||
expect(screen.getByTestId('staged-attachments').textContent).toContain('landing.html');
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Remove landing.html'));
|
||||
|
||||
expect(input.value).toBe('Use ');
|
||||
expect(screen.queryByTestId('staged-attachments')).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves surrounding draft formatting when removing a design file token', async () => {
|
||||
renderComposer({
|
||||
projectFiles: [
|
||||
{
|
||||
path: 'designs/landing.html',
|
||||
name: 'landing.html',
|
||||
kind: 'html',
|
||||
mime: 'text/html',
|
||||
mtime: 1,
|
||||
size: 128,
|
||||
},
|
||||
],
|
||||
});
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
const draft = 'Plan:\n\n@landing\n\nKeep spacing';
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: draft, selectionStart: 'Plan:\n\n@landing'.length },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
|
||||
fireEvent.click(screen.getByText('designs/landing.html'));
|
||||
|
||||
expect(input.value).toBe('Plan:\n\n@designs/landing.html \n\nKeep spacing');
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Remove landing.html'));
|
||||
|
||||
expect(input.value).toBe('Plan:\n\n\n\nKeep spacing');
|
||||
expect(screen.queryByTestId('staged-attachments')).toBeNull();
|
||||
});
|
||||
|
||||
it('removes a design file token when punctuation follows it', async () => {
|
||||
renderComposer({
|
||||
projectFiles: [
|
||||
{
|
||||
path: 'designs/landing.html',
|
||||
name: 'landing.html',
|
||||
kind: 'html',
|
||||
mime: 'text/html',
|
||||
mtime: 1,
|
||||
size: 128,
|
||||
},
|
||||
],
|
||||
});
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'Use @landing', selectionStart: 12 },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
|
||||
fireEvent.click(screen.getByText('designs/landing.html'));
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: 'Use @designs/landing.html, please',
|
||||
selectionStart: 'Use @designs/landing.html, please'.length,
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Remove landing.html'));
|
||||
|
||||
expect(input.value).toBe('Use , please');
|
||||
expect(screen.queryByTestId('staged-attachments')).toBeNull();
|
||||
});
|
||||
|
||||
it('removes a quoted design file token when its chip is removed', async () => {
|
||||
renderComposer({
|
||||
projectFiles: [
|
||||
{
|
||||
path: 'designs/landing.html',
|
||||
name: 'landing.html',
|
||||
kind: 'html',
|
||||
mime: 'text/html',
|
||||
mtime: 1,
|
||||
size: 128,
|
||||
},
|
||||
],
|
||||
});
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '@landing', selectionStart: 8 },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
|
||||
fireEvent.click(screen.getByText('designs/landing.html'));
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '"@designs/landing.html"',
|
||||
selectionStart: '"@designs/landing.html"'.length,
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Remove landing.html'));
|
||||
|
||||
expect(input.value).toBe('""');
|
||||
expect(screen.queryByTestId('staged-attachments')).toBeNull();
|
||||
});
|
||||
|
||||
it('lets the tools panel switch between Official and My plugins', async () => {
|
||||
renderComposer();
|
||||
fireEvent.click(screen.getByLabelText('Open CLI and model settings'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue