79f3a5243a
Adds a `callAppRoute` method to `RestApiClient` in `twenty-client-sdk/rest`. It calls one of the app's own HTTP routes using the injected `TWENTY_FUNCTIONS_URL`, resolved internally the same way the client already resolves `TWENTY_API_URL`, so app code no longer reads env vars or knows how function routes are hosted. Both app runtimes already go through `RestApiClient` for route calls (logic functions and front components), so both get this in one place; front components keep the existing 401 token-refresh flow. Pairs with #22825, which makes the injected `TWENTY_FUNCTIONS_URL` callable in every topology (app custom domain -> workspace isolated functions domain -> `SERVER_URL/s`). Once this ships in an SDK release, Call Recorder's own-route plumbing (logic-function and front-component utils) drops its URL resolution and calls `client.callAppRoute(path, body)`. --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { type CSSProperties, useEffect, useState } from 'react';
|
|
import { CoreApiClient } from 'twenty-client-sdk/core';
|
|
import { RestApiClient } from 'twenty-client-sdk/rest';
|
|
import { defineFrontComponent } from 'twenty-sdk/define';
|
|
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
|
|
|
|
import { DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
|
import { Markdown } from 'src/utils/markdown-to-react';
|
|
|
|
const useCurrentRecordId = (): string | null =>
|
|
useFrontComponentExecutionContext((context) =>
|
|
context.recordId ??
|
|
(context.selectedRecordIds.length === 1
|
|
? context.selectedRecordIds[0]
|
|
: null),
|
|
);
|
|
|
|
const styles: Record<string, CSSProperties> = {
|
|
scroll: { height: '100%', overflow: 'auto', background: '#eef1f6', padding: '24px' },
|
|
paper: {
|
|
maxWidth: '720px',
|
|
margin: '0 auto',
|
|
background: '#ffffff',
|
|
borderRadius: '10px',
|
|
boxShadow: '0 10px 40px rgba(24, 39, 75, 0.08)',
|
|
overflow: 'hidden',
|
|
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif",
|
|
},
|
|
body: { padding: '48px 56px 56px' },
|
|
empty: { padding: '40px', textAlign: 'center', color: '#6b7280', fontFamily: 'sans-serif' },
|
|
actions: {
|
|
maxWidth: '720px',
|
|
margin: '0 auto 16px',
|
|
display: 'flex',
|
|
gap: '10px',
|
|
justifyContent: 'flex-end',
|
|
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif",
|
|
},
|
|
actionLink: {
|
|
fontSize: '13px',
|
|
fontWeight: 500,
|
|
color: '#1961ed',
|
|
background: '#ffffff',
|
|
border: '1px solid #d7deee',
|
|
borderRadius: '6px',
|
|
padding: '6px 12px',
|
|
textDecoration: 'none',
|
|
},
|
|
};
|
|
|
|
type LoadedDocument = { name: string; content: string; pdfUrl?: string };
|
|
|
|
const DocumentViewer = () => {
|
|
const recordId = useCurrentRecordId();
|
|
const [document, setDocument] = useState<LoadedDocument | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!recordId) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
// Reset while the newly-selected document loads, so the previous one isn't
|
|
// shown against the new record.
|
|
setLoading(true);
|
|
setDocument(null);
|
|
let cancelled = false;
|
|
const load = async () => {
|
|
const { documents } = await new CoreApiClient().query({
|
|
documents: {
|
|
__args: { filter: { id: { eq: recordId } }, first: 1 },
|
|
edges: {
|
|
node: { id: true, name: true, content: true, file: { url: true } },
|
|
},
|
|
},
|
|
});
|
|
if (cancelled) return;
|
|
const node = documents?.edges?.[0]?.node;
|
|
setDocument({
|
|
name: node?.name ?? 'Document',
|
|
content: node?.content ?? '',
|
|
pdfUrl: node?.file?.[0]?.url ?? undefined,
|
|
});
|
|
};
|
|
load().finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [recordId]);
|
|
|
|
if (loading || !document) {
|
|
return <div style={styles.empty}>{loading ? 'Loading…' : 'Open a document to preview it here.'}</div>;
|
|
}
|
|
|
|
const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
|
|
query: { id: recordId },
|
|
});
|
|
|
|
return (
|
|
<div style={styles.scroll}>
|
|
<div style={styles.actions}>
|
|
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
|
|
Open web page
|
|
</a>
|
|
{document.pdfUrl ? (
|
|
<a style={styles.actionLink} href={document.pdfUrl} target="_blank" rel="noopener noreferrer">
|
|
Download PDF
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
<div style={styles.paper}>
|
|
<div style={styles.body}>
|
|
<Markdown content={document.content} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
|
name: 'document-viewer',
|
|
description: 'Renders a generated document as a styled, printable preview.',
|
|
component: DocumentViewer,
|
|
});
|