🔥(app-impress) remove templates
We will generate the templates internally. We remove the template editor.
This commit is contained in:
@@ -1,174 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createTemplate, keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test.describe('Template Editor', () => {
|
||||
test('checks the template editor interact correctly', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const randomTemplate = await createTemplate(
|
||||
page,
|
||||
'template-editor',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomTemplate[0])).toBeVisible();
|
||||
|
||||
await page.getByTitle('Open Blocks').click();
|
||||
await expect(
|
||||
page.locator('.gjs-editor .gjs-block[title="Text"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the template editor save on changed', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(
|
||||
browserName !== 'chromium',
|
||||
'This test failed with safary because of the dragNdrop',
|
||||
);
|
||||
|
||||
const randomTemplate = await createTemplate(
|
||||
page,
|
||||
'template-editor',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomTemplate[0])).toBeVisible();
|
||||
|
||||
const iframe = page.frameLocator('iFrame.gjs-frame');
|
||||
|
||||
await page.getByTitle('Open Blocks').click();
|
||||
await page
|
||||
.locator('.gjs-editor .gjs-block[title="Text"]')
|
||||
.dragTo(iframe.locator('body.gjs-dashed'));
|
||||
|
||||
await iframe.getByText('Insert your text here').fill('Hello World');
|
||||
await iframe.locator('body.gjs-dashed').click();
|
||||
|
||||
// Come on the page again to check the changes are saved
|
||||
await page.locator('menu').first().getByLabel(`Template button`).click();
|
||||
const panel = page.getByLabel('Templates panel').first();
|
||||
await panel.locator('li').getByText(randomTemplate[0]).click();
|
||||
|
||||
await expect(iframe.getByText('Hello World')).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
test('it saves the html generated by the template', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(
|
||||
browserName !== 'chromium',
|
||||
'This test failed with safary because of the dragNdrop',
|
||||
);
|
||||
|
||||
const randomTemplate = await createTemplate(
|
||||
page,
|
||||
'template-html',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomTemplate[0])).toBeVisible();
|
||||
|
||||
const iframe = page.frameLocator('iFrame.gjs-frame');
|
||||
|
||||
await page.getByTitle('Open Blocks').click();
|
||||
await page
|
||||
.locator('.gjs-editor .gjs-block[title="Text"]')
|
||||
.dragTo(iframe.locator('body.gjs-dashed'));
|
||||
|
||||
await iframe.getByText('Insert your text here').fill('Hello World');
|
||||
await iframe.locator('body.gjs-dashed').click();
|
||||
|
||||
await page.getByText('Save template').click();
|
||||
await expect(page.getByText('Template save successfully')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it shows a warning if body tag not present', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(
|
||||
browserName !== 'chromium',
|
||||
'This test failed with safary because of the dragNdrop',
|
||||
);
|
||||
|
||||
const randomTemplate = await createTemplate(
|
||||
page,
|
||||
'template-html',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomTemplate[0])).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText('The {{body}} tag is necessary to works with the pads.'),
|
||||
).toBeVisible();
|
||||
|
||||
const iframe = page.frameLocator('iFrame.gjs-frame');
|
||||
|
||||
await page.getByTitle('Open Blocks').click();
|
||||
await page
|
||||
.locator('.gjs-editor .gjs-block[title="Text"]')
|
||||
.dragTo(iframe.locator('body.gjs-dashed'));
|
||||
|
||||
await iframe.getByText('Insert your text here').fill('{{body}}');
|
||||
await iframe.locator('body.gjs-dashed').click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The {{body}} tag is necessary to works with the pads.'),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it duplicates the template', async ({ page, browserName }) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(
|
||||
browserName !== 'chromium',
|
||||
'This test failed with safary because of the dragNdrop',
|
||||
);
|
||||
|
||||
const randomTemplate = await createTemplate(
|
||||
page,
|
||||
'template-duplicate',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomTemplate[0])).toBeVisible();
|
||||
|
||||
const iframe = page.frameLocator('iFrame.gjs-frame');
|
||||
|
||||
await page.getByTitle('Open Blocks').click();
|
||||
await page
|
||||
.locator('.gjs-editor .gjs-block[title="Text"]')
|
||||
.dragTo(iframe.locator('body.gjs-dashed'));
|
||||
|
||||
await iframe.getByText('Insert your text here').fill('Hello World');
|
||||
await iframe.locator('body.gjs-dashed').click();
|
||||
|
||||
await page.getByText('Duplicate template').click();
|
||||
|
||||
await expect(
|
||||
page.getByText('Template duplicated successfully'),
|
||||
).toBeVisible();
|
||||
const panel = page.getByLabel('Templates panel').first();
|
||||
await expect(panel.getByText(`${randomTemplate[0]} - Copy`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,6 @@
|
||||
"zustand": "4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grapesjs/react": "1.0.0",
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.32.0",
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
|
||||
@@ -6,7 +6,6 @@ import useCunninghamTheme from '@/cunningham/useCunninghamTheme';
|
||||
|
||||
import MenuItem from './MenuItems';
|
||||
import IconPad from './assets/icon-pad.svg';
|
||||
import IconTemplate from './assets/icon-template.svg';
|
||||
|
||||
export const Menu = () => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
@@ -22,11 +21,6 @@ export const Menu = () => {
|
||||
>
|
||||
<Box className="pt-l" $direction="column" $gap="0.8rem">
|
||||
<MenuItem Icon={IconPad} label={t('Pad')} href="/" alias={['/pads/']} />
|
||||
<MenuItem
|
||||
Icon={IconTemplate}
|
||||
label={t('Template')}
|
||||
href="/templates/"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<svg
|
||||
class="svg-icon"
|
||||
style="vertical-align: middle; overflow: hidden"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M139.636 139.636v744.728h744.728V139.636H139.636z m0-46.545h744.728a46.545 46.545 0 0 1 46.545 46.545v744.728a46.545 46.545 0 0 1-46.545 46.545H139.636a46.545 46.545 0 0 1-46.545-46.545V139.636a46.545 46.545 0 0 1 46.545-46.545z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M395.636 93.09q23.273 0 23.273 23.274v791.272q0 23.273-23.273 23.273-23.272 0-23.272-23.273V116.364q0-23.273 23.272-23.273z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M928.256 395.636a23.273 23.273 0 0 1-23.273 23.273H395.636a23.273 23.273 0 1 1 0-46.545h509.347a23.273 23.273 0 0 1 23.273 23.272z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 782 B |
@@ -1,3 +0,0 @@
|
||||
export * from './template';
|
||||
export * from './template-create';
|
||||
export * from './template-panel';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './useCreateTemplate';
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { KEY_LIST_TEMPLATE, Template } from '@/features/templates';
|
||||
|
||||
type CreateTemplateParam = Partial<Template>;
|
||||
|
||||
export const createTemplate = async (
|
||||
props: CreateTemplateParam,
|
||||
): Promise<Template> => {
|
||||
const response = await fetchAPI(`templates/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(props),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to create the template',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<Template>;
|
||||
};
|
||||
|
||||
interface CreateTemplateProps {
|
||||
onSuccess: (data: Template) => void;
|
||||
}
|
||||
|
||||
export function useCreateTemplate({ onSuccess }: CreateTemplateProps) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Template, APIError, CreateTemplateParam>({
|
||||
mutationFn: createTemplate,
|
||||
onSuccess: (data) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_TEMPLATE],
|
||||
});
|
||||
onSuccess(data);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Button, Switch } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IconGroup from '@/assets/icons/icon-group2.svg';
|
||||
import { Box, Card, StyledLink, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { useCreateTemplate } from '../api/useCreateTemplate';
|
||||
|
||||
import { InputTemplateName } from './InputTemplateName';
|
||||
|
||||
export const CardCreateTemplate = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const {
|
||||
mutate: createTemplate,
|
||||
isError,
|
||||
isPending,
|
||||
error,
|
||||
} = useCreateTemplate({
|
||||
onSuccess: (pad) => {
|
||||
router.push(`/templates/${pad.id}`);
|
||||
},
|
||||
});
|
||||
const [templateName, setTemplateName] = useState('');
|
||||
const [templatePublic, setTemplatePublic] = useState(false);
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="p-b"
|
||||
$height="70%"
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
$maxWidth="24rem"
|
||||
$minWidth="22rem"
|
||||
aria-label={t('Create new template card')}
|
||||
>
|
||||
<Box $gap="1rem">
|
||||
<Box $align="center">
|
||||
<IconGroup
|
||||
width={44}
|
||||
color={colorsTokens()['primary-text']}
|
||||
aria-label={t('icon group')}
|
||||
/>
|
||||
<Text as="h3" $textAlign="center">
|
||||
{t('Name the template')}
|
||||
</Text>
|
||||
</Box>
|
||||
<InputTemplateName
|
||||
label={t('Template name')}
|
||||
{...{ error, isError, isPending, setTemplateName }}
|
||||
/>
|
||||
<Switch
|
||||
label={t('Is it public ?')}
|
||||
labelSide="right"
|
||||
onChange={() => setTemplatePublic(!templatePublic)}
|
||||
/>
|
||||
</Box>
|
||||
<Box $justify="space-between" $direction="row" $align="center">
|
||||
<StyledLink href="/">
|
||||
<Button color="secondary">{t('Cancel')}</Button>
|
||||
</StyledLink>
|
||||
<Button
|
||||
onClick={() =>
|
||||
createTemplate({ title: templateName, is_public: templatePublic })
|
||||
}
|
||||
disabled={!templateName}
|
||||
>
|
||||
{t('Create the template')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Input, Loader } from '@openfun/cunningham-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, TextErrors } from '@/components';
|
||||
|
||||
interface InputTemplateNameProps {
|
||||
error: APIError | null;
|
||||
isError: boolean;
|
||||
isPending: boolean;
|
||||
label: string;
|
||||
setTemplateName: (newTemplateName: string) => void;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export const InputTemplateName = ({
|
||||
defaultValue,
|
||||
error,
|
||||
isError,
|
||||
isPending,
|
||||
label,
|
||||
setTemplateName,
|
||||
}: InputTemplateNameProps) => {
|
||||
const [isInputError, setIsInputError] = useState(isError);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
setIsInputError(true);
|
||||
}
|
||||
}, [isError]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
fullWidth
|
||||
type="text"
|
||||
label={label}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => {
|
||||
setTemplateName(e.target.value);
|
||||
setIsInputError(false);
|
||||
}}
|
||||
rightIcon={<span className="material-icons">edit</span>}
|
||||
state={isInputError ? 'error' : 'default'}
|
||||
/>
|
||||
{isError && error && <TextErrors causes={error.cause} />}
|
||||
{isPending && (
|
||||
<Box $align="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './CardCreateTemplate';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './components';
|
||||
export * from './api';
|
||||
@@ -1,174 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { Panel } from '../components/Panel';
|
||||
import { TemplateList } from '../components/TemplateList';
|
||||
|
||||
window.HTMLElement.prototype.scroll = function () {};
|
||||
|
||||
jest.mock('next/router', () => ({
|
||||
...jest.requireActual('next/router'),
|
||||
useRouter: () => ({
|
||||
query: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PanelTemplates', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it('renders with no template to display', async () => {
|
||||
fetchMock.mock(`/api/templates/?page=1&ordering=-created_at`, {
|
||||
count: 0,
|
||||
results: [],
|
||||
});
|
||||
|
||||
render(<TemplateList />, { wrapper: AppWrapper });
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'Create your first template by clicking on the "Create a new template" button.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an empty template', async () => {
|
||||
fetchMock.mock(`/api/templates/?page=1&ordering=-created_at`, {
|
||||
count: 1,
|
||||
results: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Template 1',
|
||||
accesses: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TemplateList />, { wrapper: AppWrapper });
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByLabelText('Empty templates icon'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a template with only 1 member', async () => {
|
||||
fetchMock.mock(`/api/templates/?page=1&ordering=-created_at`, {
|
||||
count: 1,
|
||||
results: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Template 1',
|
||||
accesses: [
|
||||
{
|
||||
id: '1',
|
||||
role: 'owner',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TemplateList />, { wrapper: AppWrapper });
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByLabelText('Empty templates icon'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a non-empty template', async () => {
|
||||
fetchMock.mock(`/api/templates/?page=1&ordering=-created_at`, {
|
||||
count: 1,
|
||||
results: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Template 1',
|
||||
accesses: [
|
||||
{
|
||||
id: '1',
|
||||
role: 'admin',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'member',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TemplateList />, { wrapper: AppWrapper });
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByLabelText('Templates icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the error', async () => {
|
||||
fetchMock.mock(`/api/templates/?page=1&ordering=-created_at`, {
|
||||
status: 500,
|
||||
});
|
||||
|
||||
render(<TemplateList />, { wrapper: AppWrapper });
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'Something bad happens, please refresh the page.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with template panel open', async () => {
|
||||
fetchMock.mock(`/api/templates/?page=1&ordering=-created_at`, {
|
||||
count: 1,
|
||||
results: [],
|
||||
});
|
||||
|
||||
render(<Panel />, { wrapper: AppWrapper });
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Close the templates panel' }),
|
||||
).toBeVisible();
|
||||
|
||||
expect(await screen.findByText('Recents')).toBeVisible();
|
||||
});
|
||||
|
||||
it('closes and opens the template panel', async () => {
|
||||
fetchMock.mock(`/api/templates/?page=1&ordering=-created_at`, {
|
||||
count: 1,
|
||||
results: [],
|
||||
});
|
||||
|
||||
render(<Panel />, { wrapper: AppWrapper });
|
||||
|
||||
expect(await screen.findByText('Recents')).toBeVisible();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Close the templates panel',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Recents')).not.toBeVisible();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Open the templates panel',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Recents')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from './useTemplates';
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M20.75 0.75H3.25C1.8625 0.75 0.75 1.875 0.75 3.25V20.75C0.75 22.125 1.8625 23.25 3.25 23.25H20.75C22.125 23.25 23.25 22.125 23.25 20.75V3.25C23.25 1.875 22.125 0.75 20.75 0.75ZM17 13.25H13.25V17C13.25 17.6875 12.6875 18.25 12 18.25C11.3125 18.25 10.75 17.6875 10.75 17V13.25H7C6.3125 13.25 5.75 12.6875 5.75 12C5.75 11.3125 6.3125 10.75 7 10.75H10.75V7C10.75 6.3125 11.3125 5.75 12 5.75C12.6875 5.75 13.25 6.3125 13.25 7V10.75H17C17.6875 10.75 18.25 11.3125 18.25 12C18.25 12.6875 17.6875 13.25 17 13.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 630 B |
@@ -1,13 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_508_5524)">
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM4 12C4 7.58 7.58 4 12 4C13.85 4 15.55 4.63 16.9 5.69L5.69 16.9C4.63 15.55 4 13.85 4 12ZM12 20C10.15 20 8.45 19.37 7.1 18.31L18.31 7.1C19.37 8.45 20 10.15 20 12C20 16.42 16.42 20 12 20Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_508_5524">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 578 B |
@@ -1,14 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="11.5"
|
||||
transform="rotate(-180 12 12)"
|
||||
fill="white"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M14.1683 16.232C14.4803 15.92 14.4803 15.416 14.1683 15.104L11.0643 12L14.1683 8.896C14.4803 8.584 14.4803 8.08 14.1683 7.768C13.8563 7.456 13.3523 7.456 13.0403 7.768L9.36834 11.44C9.05634 11.752 9.05634 12.256 9.36834 12.568L13.0403 16.24C13.3443 16.544 13.8563 16.544 14.1683 16.232Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 542 B |
@@ -1,13 +0,0 @@
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_178_17837)">
|
||||
<path
|
||||
d="M11.25 3.75L6.25 8.7375H10V17.5H12.5V8.7375H16.25L11.25 3.75ZM20 21.2625V12.5H17.5V21.2625H13.75L18.75 26.25L23.75 21.2625H20Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_178_17837">
|
||||
<rect width="30" height="30" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 429 B |
@@ -1,83 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import IconOpenClose from '../assets/icon-open-close.svg';
|
||||
|
||||
import { PanelActions } from './PanelActions';
|
||||
import { TemplateList } from './TemplateList';
|
||||
|
||||
export const Panel = () => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const closedOverridingStyles = !isOpen && {
|
||||
$width: '0',
|
||||
$maxWidth: '0',
|
||||
$minWidth: '0',
|
||||
};
|
||||
|
||||
const transition = 'all 0.5s ease-in-out';
|
||||
|
||||
return (
|
||||
<Box
|
||||
$width="100%"
|
||||
$maxWidth="20rem"
|
||||
$minWidth="14rem"
|
||||
$css={`
|
||||
position: relative;
|
||||
border-right: 1px solid ${colorsTokens()['primary-300']};
|
||||
transition: ${transition};
|
||||
`}
|
||||
$height="inherit"
|
||||
aria-label="Templates panel"
|
||||
{...closedOverridingStyles}
|
||||
>
|
||||
<BoxButton
|
||||
aria-label={
|
||||
isOpen
|
||||
? t('Close the templates panel')
|
||||
: t('Open the templates panel')
|
||||
}
|
||||
$color={colorsTokens()['primary-600']}
|
||||
$css={`
|
||||
position: absolute;
|
||||
right: -1.2rem;
|
||||
top: 1.03rem;
|
||||
transform: rotate(${isOpen ? '0' : '180'}deg);
|
||||
transition: ${transition};
|
||||
`}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<IconOpenClose width={24} height={24} />
|
||||
</BoxButton>
|
||||
<Box
|
||||
$css={`
|
||||
overflow: hidden;
|
||||
opacity: ${isOpen ? '1' : '0'};
|
||||
transition: ${transition};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
className="pr-l pl-s pt-s pb-s"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$css={`
|
||||
border-bottom: 1px solid ${colorsTokens()['primary-300']};
|
||||
`}
|
||||
>
|
||||
<Text $weight="bold" $size="1.25rem">
|
||||
{t('Recents')}
|
||||
</Text>
|
||||
<PanelActions />
|
||||
</Box>
|
||||
<TemplateList />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxButton, StyledLink } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { TemplatesOrdering } from '../api';
|
||||
import IconAdd from '../assets/icon-add.svg';
|
||||
import IconSort from '../assets/icon-sort.svg';
|
||||
import { useTemplatePanelStore } from '../store';
|
||||
|
||||
export const PanelActions = () => {
|
||||
const { t } = useTranslation();
|
||||
const { changeOrdering, ordering } = useTemplatePanelStore();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const isSortAsc = ordering === TemplatesOrdering.BY_CREATED_ON;
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="1rem"
|
||||
$css={`
|
||||
& button {
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
padding: 0.1rem;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<BoxButton
|
||||
aria-label={
|
||||
isSortAsc
|
||||
? t('Sort the templates by creation date descendent')
|
||||
: t('Sort the templates by creation date ascendent')
|
||||
}
|
||||
onClick={changeOrdering}
|
||||
$radius="100%"
|
||||
$background={isSortAsc ? colorsTokens()['primary-200'] : 'transparent'}
|
||||
$color={colorsTokens()['primary-600']}
|
||||
>
|
||||
<IconSort
|
||||
width={30}
|
||||
height={30}
|
||||
aria-label={t('Sort templates icon')}
|
||||
/>
|
||||
</BoxButton>
|
||||
<StyledLink href="/templates/create">
|
||||
<BoxButton
|
||||
aria-label={t('Add a template')}
|
||||
$color={colorsTokens()['primary-600']}
|
||||
>
|
||||
<IconAdd width={30} height={30} aria-label={t('Add template icon')} />
|
||||
</BoxButton>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IconGroup from '@/assets/icons/icon-group.svg';
|
||||
import { Box, StyledLink, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Template } from '@/features/templates/template';
|
||||
|
||||
import IconNone from '../assets/icon-none.svg';
|
||||
|
||||
interface TemplateItemProps {
|
||||
template: Template;
|
||||
}
|
||||
|
||||
export const TemplateItem = ({ template }: TemplateItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const {
|
||||
query: { id },
|
||||
} = useRouter();
|
||||
|
||||
// There is at least 1 owner in the team
|
||||
const hasMembers = template.accesses.length > 1;
|
||||
const isActive = template.id === id;
|
||||
|
||||
const commonProps = {
|
||||
className: 'p-t',
|
||||
width: 52,
|
||||
style: {
|
||||
borderRadius: '10px',
|
||||
flexShrink: 0,
|
||||
background: '#fff',
|
||||
},
|
||||
};
|
||||
|
||||
const activeStyle = `
|
||||
border-right: 4px solid ${colorsTokens()['primary-600']};
|
||||
background: ${colorsTokens()['primary-400']};
|
||||
span{
|
||||
color: ${colorsTokens()['primary-text']};
|
||||
}
|
||||
`;
|
||||
|
||||
const hoverStyle = `
|
||||
&:hover{
|
||||
border-right: 4px solid ${colorsTokens()['primary-400']};
|
||||
background: ${colorsTokens()['primary-300']};
|
||||
|
||||
span{
|
||||
color: ${colorsTokens()['primary-text']};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="m-0"
|
||||
as="li"
|
||||
$css={`
|
||||
transition: all 0.2s ease-in;
|
||||
border-right: 4px solid transparent;
|
||||
${isActive ? activeStyle : hoverStyle}
|
||||
`}
|
||||
>
|
||||
<StyledLink className="p-s pt-t pb-t" href={`/templates/${template.id}`}>
|
||||
<Box $align="center" $direction="row" $gap="0.5rem">
|
||||
{hasMembers ? (
|
||||
<IconGroup
|
||||
aria-label={t(`Templates icon`)}
|
||||
color={colorsTokens()['primary-500']}
|
||||
{...commonProps}
|
||||
style={{
|
||||
...commonProps.style,
|
||||
border: `1px solid ${colorsTokens()['primary-300']}`,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IconNone
|
||||
aria-label={t(`Empty templates icon`)}
|
||||
color={colorsTokens()['greyscale-500']}
|
||||
{...commonProps}
|
||||
style={{
|
||||
...commonProps.style,
|
||||
border: `1px solid ${colorsTokens()['greyscale-300']}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
$weight="bold"
|
||||
$color={!hasMembers ? colorsTokens()['greyscale-600'] : undefined}
|
||||
$css={`
|
||||
min-width: 14rem;
|
||||
`}
|
||||
>
|
||||
{template.title}
|
||||
</Text>
|
||||
</Box>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { InfiniteScroll } from '@/components/InfiniteScroll';
|
||||
import { Template } from '@/features/templates/template';
|
||||
|
||||
import { useTemplates } from '../api';
|
||||
import { useTemplatePanelStore } from '../store';
|
||||
|
||||
import { TemplateItem } from './TemplateItem';
|
||||
|
||||
interface PanelTeamsStateProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
templates?: Template[];
|
||||
}
|
||||
|
||||
const TemplateListState = ({
|
||||
isLoading,
|
||||
isError,
|
||||
templates,
|
||||
}: PanelTeamsStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Box $justify="center" className="mb-b">
|
||||
<Text $theme="danger" $align="center" $textAlign="center">
|
||||
{t('Something bad happens, please refresh the page.')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box $align="center" className="m-l">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!templates?.length) {
|
||||
return (
|
||||
<Box $justify="center" className="m-s">
|
||||
<Text as="p" className="mb-0 mt-0" $theme="greyscale" $variation="500">
|
||||
{t('0 group to display.')}
|
||||
</Text>
|
||||
<Text as="p" $theme="greyscale" $variation="500">
|
||||
{t(
|
||||
'Create your first template by clicking on the "Create a new template" button.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return templates.map((template) => (
|
||||
<TemplateItem template={template} key={template.id} />
|
||||
));
|
||||
};
|
||||
|
||||
export const TemplateList = () => {
|
||||
const ordering = useTemplatePanelStore((state) => state.ordering);
|
||||
const {
|
||||
data,
|
||||
isError,
|
||||
isLoading,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useTemplates({
|
||||
ordering,
|
||||
});
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const templates = useMemo(() => {
|
||||
return data?.pages.reduce((acc, page) => {
|
||||
return acc.concat(page.results);
|
||||
}, [] as Template[]);
|
||||
}, [data?.pages]);
|
||||
|
||||
return (
|
||||
<Box $css="overflow-y: auto; overflow-x: hidden;" ref={containerRef}>
|
||||
<InfiniteScroll
|
||||
hasMore={hasNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
next={() => {
|
||||
void fetchNextPage();
|
||||
}}
|
||||
scrollContainer={containerRef.current}
|
||||
as="ul"
|
||||
className="p-0 mt-0"
|
||||
role="listbox"
|
||||
>
|
||||
<TemplateListState
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
templates={templates}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './Panel';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './useTemplatePanelStore';
|
||||
@@ -1,19 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { TemplatesOrdering } from '../api/useTemplates';
|
||||
|
||||
interface TemplatePanelStore {
|
||||
ordering: TemplatesOrdering;
|
||||
changeOrdering: () => void;
|
||||
}
|
||||
|
||||
export const useTemplatePanelStore = create<TemplatePanelStore>((set) => ({
|
||||
ordering: TemplatesOrdering.BY_CREATED_ON_DESC,
|
||||
changeOrdering: () =>
|
||||
set(({ ordering }) => ({
|
||||
ordering:
|
||||
ordering === TemplatesOrdering.BY_CREATED_ON
|
||||
? TemplatesOrdering.BY_CREATED_ON_DESC
|
||||
: TemplatesOrdering.BY_CREATED_ON,
|
||||
})),
|
||||
}));
|
||||
@@ -1 +0,0 @@
|
||||
export * from './useTemplate';
|
||||
@@ -1,37 +0,0 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { Template } from '../types';
|
||||
|
||||
export type TemplateParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const getTemplate = async ({
|
||||
id,
|
||||
}: TemplateParams): Promise<Template> => {
|
||||
const response = await fetchAPI(`templates/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to get the template',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<Template>;
|
||||
};
|
||||
|
||||
export const KEY_TEMPLATE = 'template';
|
||||
|
||||
export function useTemplate(
|
||||
param: TemplateParams,
|
||||
queryConfig?: UseQueryOptions<Template, APIError, Template>,
|
||||
) {
|
||||
return useQuery<Template, APIError, Template>({
|
||||
queryKey: [KEY_TEMPLATE, param],
|
||||
queryFn: () => getTemplate(param),
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { KEY_LIST_TEMPLATE } from '../../template-panel';
|
||||
import { Template } from '../types';
|
||||
|
||||
import { KEY_TEMPLATE } from './useTemplate';
|
||||
|
||||
type UpdateTemplateProps = {
|
||||
id: Template['id'];
|
||||
css?: string;
|
||||
html?: string;
|
||||
title?: Template['title'];
|
||||
};
|
||||
|
||||
export const updateTemplate = async ({
|
||||
id,
|
||||
title,
|
||||
css,
|
||||
html,
|
||||
}: UpdateTemplateProps): Promise<Template> => {
|
||||
const response = await fetchAPI(`templates/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
css,
|
||||
code: html,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to update the template',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<Template>;
|
||||
};
|
||||
|
||||
interface UseUpdateTemplateProps {
|
||||
onSuccess: (data: Template) => void;
|
||||
}
|
||||
|
||||
export function useUpdateTemplate({ onSuccess }: UseUpdateTemplateProps) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Template, APIError, UpdateTemplateProps>({
|
||||
mutationFn: updateTemplate,
|
||||
onSuccess: (data) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_TEMPLATE],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_TEMPLATE],
|
||||
});
|
||||
onSuccess(data);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { Template } from '../types';
|
||||
|
||||
type UpdateTemplateProps = Pick<Template, 'code_editor' | 'id'>;
|
||||
|
||||
export const updateTemplateCodeEditor = async ({
|
||||
code_editor,
|
||||
id,
|
||||
}: UpdateTemplateProps): Promise<Template> => {
|
||||
const response = await fetchAPI(`templates/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
code_editor,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to update the template',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<Template>;
|
||||
};
|
||||
|
||||
export function useUpdateTemplateCodeEditor(
|
||||
onSuccess?: (data: Template) => void,
|
||||
) {
|
||||
return useMutation<Template, APIError, UpdateTemplateProps>({
|
||||
mutationFn: updateTemplateCodeEditor,
|
||||
onSuccess: (data) => {
|
||||
onSuccess?.(data);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import GjsEditor from '@grapesjs/react';
|
||||
import { Alert, VariantType } from '@openfun/cunningham-react';
|
||||
import grapesjs, { Editor, ProjectData } from 'grapesjs';
|
||||
import 'grapesjs/dist/css/grapes.min.css';
|
||||
import pluginBlocksBasic from 'grapesjs-blocks-basic';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
import { useUpdateTemplateCodeEditor } from '../api/useUpdateTemplateCodeEditor';
|
||||
import { Template } from '../types';
|
||||
|
||||
import { TemplateTools } from './TemplateTools';
|
||||
|
||||
interface TemplateEditorProps {
|
||||
template: Template;
|
||||
}
|
||||
|
||||
export const TemplateEditor = ({ template }: TemplateEditorProps) => {
|
||||
const { mutate: updateCodeEditor } = useUpdateTemplateCodeEditor();
|
||||
const [editor, setEditor] = useState<Editor>();
|
||||
const html = editor?.getHtml();
|
||||
|
||||
const [showWarning, setShowWarning] = useState(!!html);
|
||||
|
||||
useEffect(() => {
|
||||
if (!html) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShowWarning(!html.includes('{{body}}'));
|
||||
}, [html]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor?.loadProjectData || !editor?.Storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectData = Object.keys(template.code_editor).length
|
||||
? template.code_editor
|
||||
: editor.getProjectData();
|
||||
|
||||
editor?.loadProjectData(projectData);
|
||||
|
||||
editor.Storage.add('remote', {
|
||||
load() {
|
||||
return Promise.resolve(projectData);
|
||||
},
|
||||
store(data: ProjectData) {
|
||||
updateCodeEditor({
|
||||
code_editor: data,
|
||||
id: template.id,
|
||||
});
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
}, [editor, template.code_editor, template.id, updateCodeEditor]);
|
||||
|
||||
const onEditor = (editor: Editor) => {
|
||||
setEditor(editor);
|
||||
|
||||
editor?.Storage.add('remote', {
|
||||
load() {
|
||||
return Promise.resolve(editor.getProjectData());
|
||||
},
|
||||
store() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplateTools
|
||||
template={template}
|
||||
html={html}
|
||||
cssStyle={editor?.getCss()}
|
||||
/>
|
||||
<Box
|
||||
className="m-b"
|
||||
$css="margin-top:0;"
|
||||
$effect={showWarning ? 'show' : 'hide'}
|
||||
>
|
||||
<Alert
|
||||
type={VariantType.WARNING}
|
||||
>{`The {{body}} tag is necessary to works with the pads.`}</Alert>
|
||||
</Box>
|
||||
|
||||
{!template.abilities.partial_update && (
|
||||
<Box className="m-b" $css="margin-top:0;">
|
||||
<Alert
|
||||
type={VariantType.WARNING}
|
||||
>{`Read only, you don't have the right to update this template.`}</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
className="m-b"
|
||||
$overflow="auto"
|
||||
$css={`
|
||||
margin-top:0;
|
||||
flex:1;
|
||||
& .gjs-pn-panel.gjs-pn-options,
|
||||
& .gjs-pn-panel.gjs-pn-views span[title='Settings'] {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<GjsEditor
|
||||
grapesjs={grapesjs}
|
||||
options={{
|
||||
storageManager: {
|
||||
type: 'remote',
|
||||
},
|
||||
showToolbar: false,
|
||||
showDevices: false,
|
||||
}}
|
||||
plugins={[(editor) => pluginBlocksBasic(editor, {})]}
|
||||
onEditor={onEditor}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCreateTemplate } from '@/features/templates/template-create';
|
||||
|
||||
import { useUpdateTemplate } from '../api/useUpdateTemplate';
|
||||
import { Template } from '../types';
|
||||
|
||||
interface TemplateToolsProps {
|
||||
template: Template;
|
||||
html?: string;
|
||||
cssStyle?: string;
|
||||
}
|
||||
|
||||
export const TemplateTools = ({
|
||||
template,
|
||||
html,
|
||||
cssStyle,
|
||||
}: TemplateToolsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const { mutate: updateTemplate } = useUpdateTemplate({
|
||||
onSuccess: () => {
|
||||
toast(t('Template save successfully'), VariantType.SUCCESS);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: duplicateTemplate } = useCreateTemplate({
|
||||
onSuccess: () => {
|
||||
toast(t('Template duplicated successfully'), VariantType.SUCCESS);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="m-b mb-t mt-t"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
>
|
||||
<Text as="h2" $align="center">
|
||||
{template.title}
|
||||
</Text>
|
||||
<Box $direction="row" $gap="2rem">
|
||||
<Button
|
||||
onClick={() => {
|
||||
duplicateTemplate({
|
||||
title: `${template.title} - ${t('Copy')}`,
|
||||
code_editor: template.code_editor,
|
||||
css: template.css,
|
||||
code: template.code,
|
||||
});
|
||||
}}
|
||||
color="secondary"
|
||||
>
|
||||
{t('Duplicate template')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateTemplate({
|
||||
id: template.id,
|
||||
css: cssStyle,
|
||||
html,
|
||||
});
|
||||
}}
|
||||
disabled={!template.abilities.partial_update}
|
||||
>
|
||||
{t('Save template')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './TemplateEditor';
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
export * from './types';
|
||||
@@ -1,26 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Panel } from '@/features/templates/template-panel';
|
||||
|
||||
import { MainLayout } from './MainLayout';
|
||||
|
||||
export function TemplateLayout({ children }: PropsWithChildren) {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<Box $height="inherit" $direction="row">
|
||||
<Panel />
|
||||
<Box
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$width="100%"
|
||||
$height="inherit"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './MainLayout';
|
||||
export * from './PadLayout';
|
||||
export * from './TemplateLayout';
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter as useNavigate } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { TextErrors } from '@/components/TextErrors';
|
||||
import { TemplateEditor, useTemplate } from '@/features/templates/template';
|
||||
import { TemplateLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const {
|
||||
query: { id },
|
||||
} = useRouter();
|
||||
|
||||
if (typeof id !== 'string') {
|
||||
throw new Error('Invalid template id');
|
||||
}
|
||||
|
||||
return <Template id={id} />;
|
||||
};
|
||||
|
||||
interface TemplateProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const Template = ({ id }: TemplateProps) => {
|
||||
const {
|
||||
data: template,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useTemplate(
|
||||
{ id },
|
||||
{
|
||||
queryKey: ['template', { id }],
|
||||
staleTime: 0,
|
||||
},
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isError && error) {
|
||||
if (error.status === 404) {
|
||||
navigate.replace(`/404`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TextErrors causes={error.cause} />;
|
||||
}
|
||||
|
||||
if (isLoading || !template) {
|
||||
return (
|
||||
<Box $align="center" $justify="center" $height="100%">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <TemplateEditor template={template} />;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <TemplateLayout>{page}</TemplateLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { CardCreateTemplate } from '@/features/templates/';
|
||||
import { TemplateLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return (
|
||||
<Box className="p-l" $justify="center" $align="start" $height="inherit">
|
||||
<CardCreateTemplate />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <TemplateLayout>{page}</TemplateLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Box, StyledLink } from '@/components';
|
||||
import { TemplateLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box $align="center" $justify="center" $height="inherit">
|
||||
<StyledLink href="/templates/create">
|
||||
<StyledButton>{t('Create a new template')}</StyledButton>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <TemplateLayout>{page}</TemplateLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1482,11 +1482,6 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@grapesjs/react@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@grapesjs/react/-/react-1.0.0.tgz#bfa938128db7b69ca1c23439b66decf4eaa0ca9a"
|
||||
integrity sha512-HHttzxwgvhbxQqqsiiQzb+OJtKFl8yWI9Yp3IgPgiKXqwY19lEJWSkCukvfqxugbYRxi0M1uERLAt209AmY5vQ==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.14":
|
||||
version "0.11.14"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
|
||||
|
||||
Reference in New Issue
Block a user