Baptiste Devessier
2026-04-02 13:20:08 +02:00
committed by GitHub
parent 268543a08e
commit 885ab8b444
20 changed files with 1087 additions and 275 deletions
@@ -0,0 +1,120 @@
import {
defineFrontComponent,
useFrontComponentExecutionContext,
useUserId,
} from 'twenty-sdk';
import { useState } from 'react';
const HelloWorld = () => {
const [renderCount, setRenderCount] = useState(0);
const userId = useUserId();
const fullContext = useFrontComponentExecutionContext((context) => context);
return (
<div
style={{
padding: 24,
display: 'flex',
flexDirection: 'column',
gap: 16,
fontFamily: 'system-ui, sans-serif',
background: '#f0f9ff',
borderRadius: 12,
border: '2px solid #38bdf8',
maxWidth: 400,
}}
>
<h2
style={{
color: '#0369a1',
fontWeight: 700,
fontSize: 18,
margin: 0,
}}
>
Hello World
</h2>
<div>
<p
style={{
fontSize: 12,
fontWeight: 600,
color: '#64748b',
textTransform: 'uppercase',
letterSpacing: 1,
}}
>
User ID
</p>
<p
style={{
fontSize: 16,
fontWeight: 700,
color: '#0c4a6e',
wordBreak: 'break-all',
}}
>
{userId ?? 'null'}
</p>
</div>
<div>
<p
style={{
fontSize: 12,
fontWeight: 600,
color: '#64748b',
textTransform: 'uppercase',
letterSpacing: 1,
}}
>
Execution Context
</p>
<pre
style={{
fontSize: 13,
fontWeight: 700,
color: '#0c4a6e',
background: '#e0f2fe',
padding: 12,
borderRadius: 8,
overflow: 'auto',
wordBreak: 'break-all',
}}
>
{JSON.stringify(fullContext, null, 2) ?? 'undefined'}
</pre>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<button
onClick={() => setRenderCount((c) => c + 1)}
style={{
padding: '8px 16px',
backgroundColor: '#0284c7',
color: 'white',
border: 'none',
borderRadius: 6,
fontWeight: 600,
cursor: 'pointer',
}}
>
Re-render
</button>
<span style={{ fontSize: 14, color: '#64748b' }}>
Render count: {renderCount}
</span>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'seed-front-component-hello-world',
name: 'Hello World',
description:
'A sample visual front component that displays execution context',
component: HelloWorld,
});
@@ -0,0 +1,32 @@
import {
defineFrontComponent,
enqueueSnackbar,
unmountFrontComponent,
} from 'twenty-sdk';
import { useEffect } from 'react';
const ShowNotification = () => {
useEffect(() => {
const run = async () => {
await enqueueSnackbar({
message: 'Hello from a headless front component!',
variant: 'success',
duration: 5000,
});
await unmountFrontComponent();
};
run();
}, []);
return null;
};
export default defineFrontComponent({
universalIdentifier: 'seed-front-component-show-notification',
name: 'Show Notification',
description: 'A sample headless front component that displays a notification',
isHeadless: true,
component: ShowNotification,
});
@@ -1,6 +1,12 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -16,6 +22,11 @@ export class CreateFrontComponentInput {
@HideField()
universalIdentifier?: string;
@IsBoolean()
@IsOptional()
@HideField()
isHeadless?: boolean;
@IsString()
@IsOptional()
@Field()
@@ -0,0 +1,46 @@
import fs from 'fs/promises';
import path from 'path';
import { ASSET_PATH } from 'src/constants/assets-path';
export type FrontComponentSeedProjectFile = {
name: string;
path: string;
content: Buffer;
};
const getAllFiles = async (
rootDir: string,
dir: string = rootDir,
files: FrontComponentSeedProjectFile[] = [],
): Promise<FrontComponentSeedProjectFile[]> => {
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of dirEntries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await getAllFiles(rootDir, fullPath, files);
} else {
files.push({
path: path.relative(rootDir, dir),
name: entry.name,
content: await fs.readFile(fullPath),
});
}
}
return files;
};
export const getFrontComponentSeedProjectFiles = async (
subdirectory: string,
): Promise<FrontComponentSeedProjectFile[]> => {
const seedProjectPath = path.join(
ASSET_PATH,
'engine/metadata-modules/front-component/constants/seed-project',
subdirectory,
);
return getAllFiles(seedProjectPath);
};