diff --git a/packages/twenty-docs/sidebars.js b/packages/twenty-docs/sidebars.js index 495f37ad2..bc0c76434 100644 --- a/packages/twenty-docs/sidebars.js +++ b/packages/twenty-docs/sidebars.js @@ -51,13 +51,14 @@ const sidebars = { { type: 'link', label: 'Core API', - href: '/rest-api/', + href: '/rest-api/core', }, { type: 'link', label: 'Metadata API', href: '#', className: 'coming-soon', + //href: '/rest-api/metadata', }, ], }, @@ -73,13 +74,12 @@ const sidebars = { { type: 'link', label: 'Core API', - href: '/graphql/', + href: '/graphql/core', }, { type: 'link', label: 'Metadata API', - href: '#', - className: 'coming-soon', + href: '/graphql/metadata', }, ], }, diff --git a/packages/twenty-docs/src/pages/graphql.tsx b/packages/twenty-docs/src/components/graphql-playground.tsx similarity index 74% rename from packages/twenty-docs/src/pages/graphql.tsx rename to packages/twenty-docs/src/components/graphql-playground.tsx index c7a8f3ccc..21018adb1 100644 --- a/packages/twenty-docs/src/pages/graphql.tsx +++ b/packages/twenty-docs/src/components/graphql-playground.tsx @@ -6,19 +6,27 @@ import { createGraphiQLFetcher } from '@graphiql/toolkit'; import Layout from '@theme/Layout'; import { GraphiQL } from 'graphiql'; -import Playground from '../components/playground'; +import Playground from './playground'; import explorerCss from '!css-loader!@graphiql/plugin-explorer/dist/style.css'; import graphiqlCss from '!css-loader!graphiql/graphiql.css'; +const SubDocToPath = { + core: 'graphql', + metadata: 'metadata', +}; + // Docusaurus does SSR for custom pages, but we need to load GraphiQL in the browser -const GraphQlComponent = ({ token }) => { +const GraphQlComponent = ({ token, baseUrl, path }) => { const explorer = explorerPlugin({ showAttribution: true, }); + if (!baseUrl || !token) { + return <>; + } const fetcher = createGraphiQLFetcher({ - url: 'https://api.twenty.com/graphql', + url: baseUrl + '/' + path, }); // We load graphiql style using useEffect as it breaks remaining docs style @@ -50,8 +58,9 @@ const GraphQlComponent = ({ token }) => { ); }; -const graphQL = () => { +const GraphQlPlayground = ({ subDoc }: { subDoc: 'core' | 'metadata' }) => { const [token, setToken] = useState(); + const [baseUrl, setBaseUrl] = useState(); const { setTheme } = useTheme(); useEffect(() => { @@ -71,7 +80,13 @@ const graphQL = () => { return () => window.removeEventListener('storage', handleThemeChange); }, []); - const children = ; + const children = ( + + ); return ( { description="GraphQL Playground for Twenty" > - {() => } + {() => ( + + )} ); }; -export default graphQL; +export default GraphQlPlayground; diff --git a/packages/twenty-docs/src/components/playground.tsx b/packages/twenty-docs/src/components/playground.tsx index 5f908ef83..687900f82 100644 --- a/packages/twenty-docs/src/components/playground.tsx +++ b/packages/twenty-docs/src/components/playground.tsx @@ -1,27 +1,74 @@ import React, { useState } from 'react'; +import { TbLoader2 } from 'react-icons/tb'; + import TokenForm, { TokenFormProps } from '../components/token-form'; -const Playground = ( - { - children, - setOpenApiJson, - setToken - }: Partial -) => { - const [isTokenValid, setIsTokenValid] = useState(false) +const Playground = ({ + children, + setOpenApiJson, + setToken, + setBaseUrl, + subDoc, +}: Partial & { + subDoc: string; +}) => { + const [isTokenValid, setIsTokenValid] = useState(false); + const [isLoading, setIsLoading] = useState(false); return ( - <> +
- { - isTokenValid && children - } - - ) -} + {!isTokenValid && ( +
+
+ A token is required as APIs are dynamically generated for each + workspace based on their unique metadata.
Generate your token + under{' '} + + Settings > Developers + +
+ {isLoading && ( +
+ +
+ )} +
+ )} + {children} +
+ ); +}; export default Playground; diff --git a/packages/twenty-docs/src/components/token-form.css b/packages/twenty-docs/src/components/token-form.css index 9defb0263..2530a5c9c 100644 --- a/packages/twenty-docs/src/components/token-form.css +++ b/packages/twenty-docs/src/components/token-form.css @@ -1,42 +1,90 @@ -.container { - display: flex; - justify-content: center; - align-items: center; - height: 90vh; +.form-container { + height: 45px; + overflow: hidden; + border-bottom: 1px solid var(--ifm-color-secondary-light); + position: sticky; + top: var(--ifm-navbar-height); + padding: 0px 8px; + background: var(--ifm-color-secondary-contrast-background); + z-index: 2; } .form { - text-align: center; - padding: 50px; + display: flex; + height: 45px; + gap: 10px; + width: 50%; + margin-left: auto; } .link { - color: #16233f; - text-decoration: none; + color: white; + text-decoration: underline; position: relative; font-weight: bold; transition: color 0.3s ease; -} -[data-theme='dark'] .link { - color: #a3c0f8; + &:hover { + color: #ddd; + } } .input { padding: 6px; - margin: 20px 0 5px 0; + margin: 5px 0 5px 0; max-width: 460px; width: 100%; box-sizing: border-box; background-color: #f3f3f3; border: 1px solid #ddd; border-radius: 4px; + padding-left:30px; + height: 32px; } [data-theme='dark'] .input { background-color: #16233f; } +.inputWrapper { + display: flex; + align-items: center; + flex: 1; + position: relative; +} + +.inputIcon { + display: flex; + align-items: center; + position: absolute; + top: 0; + height: 100%; + padding: 5px; + color: #B3B3B3; +} + +[data-theme='dark'] .inputIcon { + color: white; +} + +.select { + padding: 6px; + margin: 5px 0 5px 0; + max-width: 460px; + width: 100%; + box-sizing: border-box; + background-color: #f3f3f3; + border: 1px solid #ddd; + border-radius: 4px; + height: 32px; + flex: 1; +} + +[data-theme='dark'] .select { + background-color: #16233f; +} + + .invalid { border: 1px solid #f83e3e; } @@ -75,3 +123,18 @@ align-items: center; height: 50px; } + + +.backButton { + position: absolute; + display: flex; + left: 8px; + height: 100%; + align-items: center; + cursor: pointer; + color: #999999; + + &:hover { + color: #16233f; + } +} \ No newline at end of file diff --git a/packages/twenty-docs/src/components/token-form.tsx b/packages/twenty-docs/src/components/token-form.tsx index 3e499a2df..b89216af7 100644 --- a/packages/twenty-docs/src/components/token-form.tsx +++ b/packages/twenty-docs/src/components/token-form.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { TbLoader2 } from 'react-icons/tb'; +import { useHistory, useLocation } from '@docusaurus/router'; +import { TbApi, TbChevronLeft, TbLink } from '@theme/icons'; import { parseJson } from 'nx/src/utils/json'; import tokenForm from '!css-loader!./token-form.css'; @@ -7,21 +8,38 @@ import tokenForm from '!css-loader!./token-form.css'; export type TokenFormProps = { setOpenApiJson?: (json: object) => void; setToken?: (token: string) => void; + setBaseUrl?: (baseUrl: string) => void; isTokenValid: boolean; setIsTokenValid: (boolean) => void; + setLoadingState: (boolean) => void; + subDoc?: string; }; const TokenForm = ({ setOpenApiJson, setToken, + setBaseUrl: submitBaseUrl, isTokenValid, setIsTokenValid, + subDoc, + setLoadingState, }: TokenFormProps) => { + const history = useHistory(); + const location = useLocation(); const [isLoading, setIsLoading] = useState(false); + const [baseUrl, setBaseUrl] = useState( + parseJson(localStorage.getItem('baseUrl'))?.baseUrl ?? + 'https://api.twenty.com', + ); const token = parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ?? ''; + const updateLoading = (loading: boolean) => { + setIsLoading(loading); + setLoadingState(loading); + }; + const updateToken = async (event: React.ChangeEvent) => { localStorage.setItem( 'TryIt_securitySchemeValues', @@ -30,22 +48,33 @@ const TokenForm = ({ await submitToken(event.target.value); }; - const validateToken = (openApiJson) => setIsTokenValid(!!openApiJson.tags); + const updateBaseUrl = (baseUrl) => { + setBaseUrl(baseUrl); + submitBaseUrl?.(baseUrl); + localStorage.setItem('baseUrl', JSON.stringify({ baseUrl: baseUrl })); + }; + + const validateToken = (openApiJson) => { + setIsTokenValid(!!openApiJson.tags); + }; const getJson = async (token: string) => { - setIsLoading(true); + updateLoading(true); - return await fetch('https://api.twenty.com/open-api', { + return await fetch(baseUrl + '/open-api/' + (subDoc ?? 'core'), { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => res.json()) .then((result) => { validateToken(result); - setIsLoading(false); + updateLoading(false); return result; }) - .catch(() => setIsLoading(false)); + .catch(() => { + updateLoading(false); + setIsTokenValid(false); + }); }; const submitToken = async (token) => { @@ -60,6 +89,7 @@ const TokenForm = ({ useEffect(() => { (async () => { + updateBaseUrl(baseUrl); await submitToken(token); })(); }, []); @@ -74,46 +104,61 @@ const TokenForm = ({ }, []); return ( - !isTokenValid && ( -
-
-
- -

- - - Token invalid - -

- -
-

-
+
+
+
history.goBack()}> + + Back
-
- ) + +
+
+ +
+ +
+
+
+ +
+ updateBaseUrl(event.target.value)} + onBlur={() => submitToken(token)} + /> +
+ {!location.pathname.includes('rest-api') && ( +
+ +
+ )} + +
); }; diff --git a/packages/twenty-docs/src/css/custom.css b/packages/twenty-docs/src/css/custom.css index cb1dde333..c478da86b 100644 --- a/packages/twenty-docs/src/css/custom.css +++ b/packages/twenty-docs/src/css/custom.css @@ -250,7 +250,7 @@ li.coming-soon a::after { } .fullHeightPlayground { - height: calc(100vh - var(--ifm-navbar-height)); + height: calc(100vh - var(--ifm-navbar-height) - 45px); } .display-none { diff --git a/packages/twenty-docs/src/pages/graphql/core.tsx b/packages/twenty-docs/src/pages/graphql/core.tsx new file mode 100644 index 000000000..365ba3674 --- /dev/null +++ b/packages/twenty-docs/src/pages/graphql/core.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import GraphQlPlayground from '../../components/graphql-playground'; + +const CoreGraphql = () => { + return ; +}; + +export default CoreGraphql; diff --git a/packages/twenty-docs/src/pages/graphql/metadata.tsx b/packages/twenty-docs/src/pages/graphql/metadata.tsx new file mode 100644 index 000000000..a5d760d6d --- /dev/null +++ b/packages/twenty-docs/src/pages/graphql/metadata.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import GraphQlPlayground from '../../components/graphql-playground'; + +const CoreGraphql = () => { + return ; +}; + +export default CoreGraphql; diff --git a/packages/twenty-docs/src/pages/rest-api.tsx b/packages/twenty-docs/src/pages/rest-api/core.tsx similarity index 58% rename from packages/twenty-docs/src/pages/rest-api.tsx rename to packages/twenty-docs/src/pages/rest-api/core.tsx index de1c989d3..abc4be281 100644 --- a/packages/twenty-docs/src/pages/rest-api.tsx +++ b/packages/twenty-docs/src/pages/rest-api/core.tsx @@ -1,13 +1,13 @@ -import Layout from '@theme/Layout'; -import BrowserOnly from '@docusaurus/BrowserOnly'; import React, { useEffect, useState } from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; import { API } from '@stoplight/elements'; -import Playground from '../components/playground'; +import Layout from '@theme/Layout'; + +import Playground from '../../components/playground'; + import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css'; - -const RestApiComponent = ({openApiJson}) => { - +const RestApiComponent = ({ openApiJson }) => { // We load spotlightTheme style using useEffect as it breaks remaining docs style useEffect(() => { const styleElement = document.createElement('style'); @@ -18,31 +18,38 @@ const RestApiComponent = ({openApiJson}) => { }, []); return ( - - ) -} +
+ +
+ ); +}; const restApi = () => { - const [openApiJson, setOpenApiJson] = useState({}) + const [openApiJson, setOpenApiJson] = useState({}); - const children = + const children = ; return ( - { - () => - } + + {() => ( + + )} + - ) + ); }; export default restApi; diff --git a/packages/twenty-docs/src/theme/icons.js b/packages/twenty-docs/src/theme/icons.js index 4059b432d..85e365241 100644 --- a/packages/twenty-docs/src/theme/icons.js +++ b/packages/twenty-docs/src/theme/icons.js @@ -21,6 +21,7 @@ export { TbCheck, TbCheckbox, TbChecklist, + TbChevronLeft, TbCircleCheckFilled, TbCircleDot, TbCloud, diff --git a/packages/twenty-server/src/core/open-api/open-api.controller.ts b/packages/twenty-server/src/core/open-api/open-api.controller.ts index 60b485f4a..2dcaf8a4b 100644 --- a/packages/twenty-server/src/core/open-api/open-api.controller.ts +++ b/packages/twenty-server/src/core/open-api/open-api.controller.ts @@ -8,9 +8,22 @@ import { OpenApiService } from 'src/core/open-api/open-api.service'; export class OpenApiController { constructor(private readonly openApiService: OpenApiService) {} - @Get() - async generateOpenApiSchema(@Req() request: Request, @Res() res: Response) { - const data = await this.openApiService.generateSchema(request); + @Get('core') + async generateOpenApiSchemaCore( + @Req() request: Request, + @Res() res: Response, + ) { + const data = await this.openApiService.generateCoreSchema(request); + + res.send(data); + } + + @Get('metadata') + async generateOpenApiSchemaMetaData( + @Req() request: Request, + @Res() res: Response, + ) { + const data = await this.openApiService.generateMetaDataSchema(); res.send(data); } diff --git a/packages/twenty-server/src/core/open-api/open-api.service.ts b/packages/twenty-server/src/core/open-api/open-api.service.ts index c9426c0ee..6f6051e58 100644 --- a/packages/twenty-server/src/core/open-api/open-api.service.ts +++ b/packages/twenty-server/src/core/open-api/open-api.service.ts @@ -24,7 +24,7 @@ export class OpenApiService { private readonly objectMetadataService: ObjectMetadataService, ) {} - async generateSchema(request: Request): Promise { + async generateCoreSchema(request: Request): Promise { const schema = baseSchema(); let objectMetadataItems; @@ -42,8 +42,8 @@ export class OpenApiService { return schema; } schema.paths = objectMetadataItems.reduce((paths, item) => { - paths[`/rest/${item.namePlural}`] = computeManyResultPath(item); - paths[`/rest/${item.namePlural}/{id}`] = computeSingleResultPath(item); + paths[`/${item.namePlural}`] = computeManyResultPath(item); + paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item); return paths; }, schema.paths as OpenAPIV3.PathsObject); @@ -62,4 +62,13 @@ export class OpenApiService { return schema; } + + async generateMetaDataSchema(): Promise { + //TODO Add once Rest MetaData api is ready + const schema = baseSchema(); + + schema.tags = [{ name: 'placeholder' }]; + + return schema; + } } diff --git a/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts b/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts index 2b07d60ca..e916d4513 100644 --- a/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts +++ b/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts @@ -21,7 +21,7 @@ export const baseSchema = (): OpenAPIV3.Document => { // Testing purposes servers: [ { - url: 'https://api.twenty.com/', + url: 'https://api.twenty.com/rest', description: 'Production Development', }, ],