Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/fix-codeblock-react-node-children.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clickhouse/click-ui': minor
---

Trying to fix the code widget so that it can handle dummy variables that people use, like <project-id> without them being interpreted as React components
20 changes: 12 additions & 8 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ else
exit 1
fi

if yarn circular-dependency:check; then
echo "✅ Circular dependency check is good!"
else
echo "⚠️ WARNING: Circular dependency checkup found issues, fix them before committing, please!"
echo "💡 Use the following command to get further reports in your resolution yarn circular-dependency:check"
exit 1
fi

if yarn changeset:verify; then
echo "✅ Changeset file's included!"
else
Expand All @@ -47,5 +39,17 @@ else
exit 1
fi

if [[ -n "$RUN_DEPS_CHECK" ]]; then
if yarn circular-dependency:check; then
echo "✅ Circular dependency check is good!"
else
echo "⚠️ WARNING: Circular dependency checkup found issues, fix them before committing, please!"
echo "💡 Use the following command to get further reports in your resolution yarn circular-dependency:check"
exit 1
fi
else
echo "🦖 Skipped circular dependency check! To enable set RUN_DEPS_CHECK=1"
fi

echo "👍 Health check completed."
echo
5 changes: 4 additions & 1 deletion .scripts/bash/circular-dependency-check
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ TEMP_OUTPUT=$(mktemp)
trap 'rm -f "$TEMP_OUTPUT"' EXIT

if command -v yarn &> /dev/null; then
# NOTE: Redirect stdin to try prevent command hanging
# as a user has reported hanging in macOS (unable replicate
# but if issue persists for user remove redirection)
yarn exec skott "$ENTRY_POINT" \
--displayMode=file-tree \
--showCircularDependencies \
--ignorePattern="$IGNORE_PATTERNS" \
--exitCodeOnCircularDependencies=1 > "$TEMP_OUTPUT" 2>&1 || true
--exitCodeOnCircularDependencies=1 < /dev/null > "$TEMP_OUTPUT" 2>&1 || true
else
echo "👹 Oops! The package skott is missing, have you installed the project dependencies?"

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ Check for circular dependencies that can cause build and runtime issues:
yarn circular-dependency:check
```

> [!TIP]
> Set RUN_DEPS_CHECK=1 to run circular dependency checks automatically on commit.

If circular dependencies are found it'll exit with a report showing the affeced files which require your attention.

### Generating design tokens
Expand Down
54 changes: 54 additions & 0 deletions src/components/CodeBlock/CodeBlock.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { renderCUI } from '@/utils/test-utils';
import userEvent from '@testing-library/user-event';
import { CodeBlock } from '@/components/CodeBlock';

describe('CodeBlock', () => {
const renderCodeBlock = (children: React.ReactNode, language = 'bash') =>
renderCUI(<CodeBlock language={language}>{children}</CodeBlock>);

it('renders a plain string', () => {
const { container } = renderCodeBlock('echo hello');
expect(container.textContent).toContain('echo hello');
});

it('renders $VARIABLE syntax as literal text', () => {
const { container } = renderCodeBlock('export FOO="$BAR"');
expect(container.textContent).toContain('$BAR');
});

it('renders angle-bracket placeholders as literal text', () => {
const { container } = renderCodeBlock('export PROJECT_ID="<project_id>"');
expect(container.textContent).toContain('<project_id>');
expect(container.textContent).not.toContain('[object Object]');
});

it('copies the plain-text string to clipboard', async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText } });

const code = 'echo hello';
const { getByRole } = renderCodeBlock(code);

await userEvent.click(getByRole('button'));
expect(writeText).toHaveBeenCalledWith(code);
});

it('calls onCopy with the plain-text string', async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText } });
const onCopy = vi.fn();

const code = 'echo hello';
const { getByRole } = renderCUI(
<CodeBlock
language="bash"
onCopy={onCopy}
>
{code}
</CodeBlock>
);

await userEvent.click(getByRole('button'));
expect(onCopy).toHaveBeenCalledWith(code);
});
});
36 changes: 30 additions & 6 deletions src/components/CodeBlock/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { HTMLAttributes, useState } from 'react';
import React, { HTMLAttributes, ReactNode, useState } from 'react';
import { Light as SyntaxHighlighter, createElement } from 'react-syntax-highlighter';

import { EmptyButton } from '@/components/EmptyButton';
import { IconButton } from '@/components/IconButton';

import { styled } from 'styled-components';
import useColorStyle from './useColorStyle';
import { CodeBlockProps, CodeThemeType } from './CodeBlock.types';
Expand All @@ -21,6 +19,31 @@ import tsx from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript.js'
import plaintext from 'react-syntax-highlighter/dist/cjs/languages/hljs/plaintext.js';
/* eslint-enable import/extensions */

const nodeToString = (node: ReactNode): string => {
if (node === null || node === undefined || typeof node === 'boolean') {
return '';
}
if (typeof node === 'string') {
return node;
}
if (typeof node === 'number') {
return String(node);
}
if (Array.isArray(node)) {
return node.map(nodeToString).join('');
}
if (React.isValidElement(node)) {
const element = node as React.ReactElement<{ children?: ReactNode }>;
const tagName = typeof element.type === 'string' ? element.type : '';
const children = element.props.children;
if (children !== undefined && children !== null) {
return `<${tagName}>${nodeToString(children)}</${tagName}>`;
}
return `<${tagName}>`;
}
return String(node);
};

SyntaxHighlighter.registerLanguage('sql', sql.default || sql);
SyntaxHighlighter.registerLanguage('bash', bash.default || bash);
SyntaxHighlighter.registerLanguage('json', json.default || json);
Expand Down Expand Up @@ -109,12 +132,13 @@ export const CodeBlock = ({
const [errorCopy, setErrorCopy] = useState(false);
const [wrap, setWrap] = useState(wrapLines);
const customStyle = useColorStyle(theme);
const codeString = nodeToString(children);

const copyCodeToClipboard = async () => {
try {
await navigator.clipboard.writeText(children);
await navigator.clipboard.writeText(codeString);
if (typeof onCopy == 'function') {
onCopy(children);
onCopy(codeString);
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
Expand Down Expand Up @@ -197,7 +221,7 @@ export const CodeBlock = ({
wrapLines={wrap || wrapLines}
wrapLongLines={wrap || wrapLines}
>
{children}
{codeString}
</Highlighter>
</CodeBlockContainer>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/CodeBlock/CodeBlock.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import { HTMLAttributes, ReactNode } from 'react';

export type CodeThemeType = 'light' | 'dark';

Expand All @@ -7,7 +7,7 @@ export interface CodeBlockProps extends Omit<
'children' | 'onCopy'
> {
language?: string;
children: string;
children: ReactNode;
theme?: CodeThemeType;
showLineNumbers?: boolean;
showWrapButton?: boolean;
Expand Down
Loading