Migrated Developer Docs (#5683)
- Migrated developer docs to Twenty website - Modified User Guide and Docs layout to include sections and subsections **Section Example:** <img width="549" alt="Screenshot 2024-05-30 at 15 44 42" src="https://github.com/twentyhq/twenty/assets/102751374/41bd4037-4b76-48e6-bc79-48d3d6be9ab8"> **Subsection Example:** <img width="557" alt="Screenshot 2024-05-30 at 15 44 55" src="https://github.com/twentyhq/twenty/assets/102751374/f14c65a9-ab0c-4530-b624-5b20fc00511a"> - Created different components (Tabs, Tables, Editors etc.) for the mdx files **Tabs & Editor** <img width="665" alt="Screenshot 2024-05-30 at 15 47 39" src="https://github.com/twentyhq/twenty/assets/102751374/5166b5c7-b6cf-417d-9f29-b1f674c1c531"> **Tables** <img width="698" alt="Screenshot 2024-05-30 at 15 57 39" src="https://github.com/twentyhq/twenty/assets/102751374/2bbfe937-ec19-4004-ab00-f7a56e96db4a"> <img width="661" alt="Screenshot 2024-05-30 at 16 03 32" src="https://github.com/twentyhq/twenty/assets/102751374/ae95b47c-dd92-44f9-b535-ccdc953f71ff"> - Created a crawler for Twenty Developers (now that it will be on the twenty website). Once this PR is merged and the website is re-deployed, we need to start crawling and make sure the index name is ‘twenty-developer’ - Added a dropdown menu in the header to access User Guide and Developers + added Developers to footer https://github.com/twentyhq/twenty/assets/102751374/1bd1fbbd-1e65-4461-b18b-84d4ddbb8ea1 - Made new layout responsive Please fill in the information for each mdx file so that it can appear on its card, as well as in the ‘In this article’ section. Example with ‘Getting Started’ in the User Guide: <img width="786" alt="Screenshot 2024-05-30 at 16 29 39" src="https://github.com/twentyhq/twenty/assets/102751374/2714b01d-a664-4ddc-9291-528632ee12ea"> Example with info and sectionInfo filled in for 'Getting Started': <img width="620" alt="Screenshot 2024-05-30 at 16 33 57" src="https://github.com/twentyhq/twenty/assets/102751374/bc69e880-da6a-4b7e-bace-1effea866c11"> Please keep in mind that the images that are being used for Developers are the same as those found in User Guide and may not match the article. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { explorerPlugin } from '@graphiql/plugin-explorer';
|
||||
import { Theme, useTheme } from '@graphiql/react';
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
|
||||
import { SubDoc } from '@/app/_components/playground/token-form';
|
||||
|
||||
import Playground from './playground';
|
||||
|
||||
const SubDocToPath = {
|
||||
core: 'graphql',
|
||||
metadata: 'metadata',
|
||||
};
|
||||
|
||||
const GraphQlComponent = ({ token, baseUrl, path }: any) => {
|
||||
const explorer = explorerPlugin({
|
||||
showAttribution: true,
|
||||
});
|
||||
|
||||
const fetcher = createGraphiQLFetcher({
|
||||
url: baseUrl + '/' + path,
|
||||
});
|
||||
|
||||
if (!baseUrl || !token) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fullHeightPlayground">
|
||||
<GraphiQL
|
||||
plugins={[explorer]}
|
||||
fetcher={fetcher}
|
||||
defaultHeaders={JSON.stringify({ Authorization: `Bearer ${token}` })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GraphQlPlayground = ({ subDoc }: { subDoc: SubDoc }) => {
|
||||
const [token, setToken] = useState<string>();
|
||||
const [baseUrl, setBaseUrl] = useState<string>();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(
|
||||
'graphiql:theme',
|
||||
window.localStorage.getItem('theme') || 'light',
|
||||
);
|
||||
|
||||
const handleThemeChange = (ev: any) => {
|
||||
if (ev.key === 'theme') {
|
||||
setTheme(ev.newValue as Theme);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleThemeChange);
|
||||
|
||||
return () => window.removeEventListener('storage', handleThemeChange);
|
||||
}, []);
|
||||
|
||||
const children = (
|
||||
<GraphQlComponent
|
||||
token={token}
|
||||
baseUrl={baseUrl}
|
||||
path={SubDocToPath[subDoc]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100vh', width: '100vw' }}>
|
||||
<Playground
|
||||
children={children}
|
||||
setToken={setToken}
|
||||
setBaseUrl={setBaseUrl}
|
||||
subDoc={subDoc}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default GraphQlPlayground;
|
||||
@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { TbLoader2 } from 'react-icons/tb';
|
||||
|
||||
import TokenForm, { TokenFormProps } from './token-form';
|
||||
|
||||
const Playground = ({
|
||||
children,
|
||||
setOpenApiJson,
|
||||
setToken,
|
||||
setBaseUrl,
|
||||
subDoc,
|
||||
}: Partial<React.PropsWithChildren> &
|
||||
Omit<
|
||||
TokenFormProps,
|
||||
'isTokenValid' | 'setIsTokenValid' | 'setLoadingState'
|
||||
>) => {
|
||||
const [isTokenValid, setIsTokenValid] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingTop: 15,
|
||||
}}
|
||||
>
|
||||
<TokenForm
|
||||
setOpenApiJson={setOpenApiJson}
|
||||
setToken={setToken}
|
||||
setBaseUrl={setBaseUrl}
|
||||
isTokenValid={isTokenValid}
|
||||
setIsTokenValid={setIsTokenValid}
|
||||
subDoc={subDoc}
|
||||
setLoadingState={setIsLoading}
|
||||
/>
|
||||
{!isTokenValid && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2,
|
||||
background: 'rgba(23,23,23, 0.2)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '50%',
|
||||
background: 'rgba(23,23,23, 0.8)',
|
||||
color: 'white',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
A token is required as APIs are dynamically generated for each
|
||||
workspace based on their unique metadata. <br /> Generate your token
|
||||
under{' '}
|
||||
<a
|
||||
className="link"
|
||||
href="https://app.twenty.com/settings/developers"
|
||||
>
|
||||
Settings > Developers
|
||||
</a>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="loader-container">
|
||||
<TbLoader2 className="loader" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Playground;
|
||||
@ -0,0 +1,146 @@
|
||||
.form-container {
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--ifm-color-secondary-light);
|
||||
position: sticky;
|
||||
top: var(--ifm-navbar-height) + 10;
|
||||
padding: 0px 8px;
|
||||
background: var(--ifm-color-secondary-contrast-background);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
height: 45px;
|
||||
gap: 10px;
|
||||
width: 50%;
|
||||
margin-left: auto;
|
||||
flex: 0.7;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
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;
|
||||
padding-left:30px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.input[disabled] {
|
||||
color: rgb(153, 153, 153)
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
.token-invalid {
|
||||
color: #f83e3e;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.not-visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.loader {
|
||||
color: #16233f;
|
||||
font-size: 2rem;
|
||||
animation: animate 2s infinite;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .loader {
|
||||
color: #a3c0f8;
|
||||
}
|
||||
|
||||
@keyframes animate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(720deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
.backButton {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
left: 8px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: #999999;
|
||||
|
||||
&:hover {
|
||||
color: #16233f;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TbApi, TbChevronLeft, TbLink } from 'react-icons/tb';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
// @ts-expect-error Migration loader as text not passing warnings
|
||||
import tokenForm from '!css-loader!./token-form.css';
|
||||
|
||||
export type SubDoc = 'core' | 'metadata';
|
||||
export type TokenFormProps = {
|
||||
setOpenApiJson?: (json: object) => void;
|
||||
setToken?: (token: string) => void;
|
||||
setBaseUrl?: (baseUrl: string) => void;
|
||||
isTokenValid?: boolean;
|
||||
setIsTokenValid?: (arg: boolean) => void;
|
||||
setLoadingState?: (arg: boolean) => void;
|
||||
subDoc?: SubDoc;
|
||||
};
|
||||
|
||||
const TokenForm = ({
|
||||
setOpenApiJson,
|
||||
setToken,
|
||||
setBaseUrl: submitBaseUrl,
|
||||
isTokenValid,
|
||||
setIsTokenValid,
|
||||
subDoc,
|
||||
setLoadingState,
|
||||
}: TokenFormProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [locationSetting, setLocationSetting] = useState(
|
||||
(window &&
|
||||
window.localStorage.getItem('baseUrl') &&
|
||||
JSON.parse(window.localStorage.getItem('baseUrl') ?? '')
|
||||
?.locationSetting) ??
|
||||
'production',
|
||||
);
|
||||
const [baseUrl, setBaseUrl] = useState(
|
||||
(window.localStorage.getItem('baseUrl') &&
|
||||
JSON.parse(window.localStorage.getItem('baseUrl') ?? '')?.baseUrl) ??
|
||||
'https://api.twenty.com',
|
||||
);
|
||||
|
||||
const tokenLocal = window?.localStorage?.getItem?.(
|
||||
'TryIt_securitySchemeValues',
|
||||
) as string;
|
||||
|
||||
const token = JSON.parse(tokenLocal)?.bearerAuth ?? '';
|
||||
|
||||
const updateLoading = (loading = false) => {
|
||||
setIsLoading(loading);
|
||||
setLoadingState?.(!!loading);
|
||||
};
|
||||
|
||||
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
window.localStorage.setItem(
|
||||
'TryIt_securitySchemeValues',
|
||||
JSON.stringify({ bearerAuth: event.target.value }),
|
||||
);
|
||||
await submitToken(event.target.value);
|
||||
};
|
||||
|
||||
const updateBaseUrl = (baseUrl: string, locationSetting: string) => {
|
||||
let url: string;
|
||||
if (locationSetting === 'production') {
|
||||
url = 'https://api.twenty.com';
|
||||
} else if (locationSetting === 'demo') {
|
||||
url = 'https://api-demo.twenty.com';
|
||||
} else if (locationSetting === 'localhost') {
|
||||
url = 'http://localhost:3000';
|
||||
} else {
|
||||
url = baseUrl?.endsWith('/')
|
||||
? baseUrl.substring(0, baseUrl.length - 1)
|
||||
: baseUrl;
|
||||
}
|
||||
|
||||
setBaseUrl(url);
|
||||
setLocationSetting(locationSetting);
|
||||
submitBaseUrl?.(url);
|
||||
window.localStorage.setItem(
|
||||
'baseUrl',
|
||||
JSON.stringify({ baseUrl: url, locationSetting }),
|
||||
);
|
||||
};
|
||||
|
||||
const validateToken = (openApiJson: any) => {
|
||||
setIsTokenValid?.(!!openApiJson.tags);
|
||||
};
|
||||
|
||||
const getJson = async (token: string) => {
|
||||
updateLoading(true);
|
||||
|
||||
return await fetch(baseUrl + '/open-api/' + (subDoc ?? 'core'), {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((result) => {
|
||||
validateToken(result);
|
||||
updateLoading(false);
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch(() => {
|
||||
updateLoading(false);
|
||||
setIsTokenValid?.(false);
|
||||
});
|
||||
};
|
||||
|
||||
const submitToken = async (token: any) => {
|
||||
if (isLoading) return;
|
||||
|
||||
const json = await getJson(token);
|
||||
|
||||
setToken && setToken(token);
|
||||
|
||||
setOpenApiJson && setOpenApiJson(json);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
updateBaseUrl(baseUrl, locationSetting);
|
||||
await submitToken(token);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// We load playground style using useEffect as it breaks remaining docs style
|
||||
useEffect(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.innerHTML = tokenForm.toString();
|
||||
document.head.append(styleElement);
|
||||
|
||||
return () => styleElement.remove();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="form-container">
|
||||
<form className="form">
|
||||
<div className="backButton" onClick={() => router.back()}>
|
||||
<TbChevronLeft size={18} />
|
||||
<span>Back</span>
|
||||
</div>
|
||||
<div className="inputWrapper">
|
||||
<select
|
||||
className="select"
|
||||
onChange={(event) => {
|
||||
updateBaseUrl(baseUrl, event.target.value);
|
||||
}}
|
||||
value={locationSetting}
|
||||
>
|
||||
<option value="production">Production API</option>
|
||||
<option value="demo">Demo API</option>
|
||||
<option value="localhost">Localhost</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="inputWrapper">
|
||||
<div className="inputIcon" title="Base URL">
|
||||
<TbLink size={20} />
|
||||
</div>
|
||||
<input
|
||||
className={'input'}
|
||||
type="text"
|
||||
readOnly={isLoading}
|
||||
disabled={locationSetting !== 'other'}
|
||||
placeholder="Base URL"
|
||||
value={baseUrl}
|
||||
onChange={(event) =>
|
||||
updateBaseUrl(event.target.value, locationSetting)
|
||||
}
|
||||
onBlur={() => submitToken(token)}
|
||||
/>
|
||||
</div>
|
||||
<div className="inputWrapper">
|
||||
<div className="inputIcon" title="Api Key">
|
||||
<TbApi size={20} />
|
||||
</div>
|
||||
<input
|
||||
className={!isTokenValid && !isLoading ? 'input invalid' : 'input'}
|
||||
type="text"
|
||||
readOnly={isLoading}
|
||||
placeholder="API Key"
|
||||
defaultValue={token}
|
||||
onChange={updateToken}
|
||||
/>
|
||||
</div>
|
||||
<div className="inputWrapper" style={{ maxWidth: '100px' }}>
|
||||
<select
|
||||
className="select"
|
||||
onChange={(event) =>
|
||||
router.replace(
|
||||
'/' + pathname.split('/').at(-2) + '/' + event.target.value,
|
||||
)
|
||||
}
|
||||
value={pathname.split('/').at(-1)}
|
||||
>
|
||||
<option value="core">Core</option>
|
||||
<option value="metadata">Metadata</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenForm;
|
||||
Reference in New Issue
Block a user