Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to declare "embed" content to be displayed as iframe #556

Merged
merged 2 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,22 @@
"name": "Name",
"compose": "Compose",
"upload": "Upload"
},
"embed": {
"tab": "Embed",
"no-embed": "To add an embed, click on",
"allow": "Enable",
"allow-display-data-embed": "Disable",
"all-fields": "Embeds",
"icon": "Icon",
"title": "Embed title",
"title-help": "",
"src": "Address",
"src-help": "Embed address (iframe)",
"preview": "Preview",
"fullscreen": "Fullscreen",
"width": "Width (px)",
"height": "Height (px)"
}
}
},
Expand Down
16 changes: 16 additions & 0 deletions public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,22 @@
"name": "Nom",
"compose": "Composer",
"upload": "Envoyer"
},
"embed": {
"tab": "Inclusions",
"no-embed": "Pour activer les inclusions, cliquer sur",
"allow": "Activer",
"allow-display-data-embed": "Désactiver",
"all-fields": "Inclusions",
"icon": "Icône",
"title": "Titre de l'inclusion",
"title-help": "",
"src": "Adresse",
"src-help": "Adresse de la page à inclure (iframe)",
"preview": "Prévisualiser",
"fullscreen": "Plein écran",
"width": "Largeur (px)",
"height": "Hauteur (px)"
}
}
},
Expand Down
71 changes: 71 additions & 0 deletions src/components/BPIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { Icon } from '@blueprintjs/core';

export const graphIcons = [
'chart',
'curved-range-chart',
'database',
'diagram-tree',
'doughnut-chart',
'flow-branch',
'flow-end',
'flow-linear',
'flow-review',
'flow-review-branch',
'flows',
'form',
'full-stacked-chart',
'gantt-chart',
'graph',
'grid',
'grouped-bar-chart',
'heat-grid',
'heatmap',
'horizontal-bar-chart',
'horizontal-bar-chart-asc',
'horizontal-bar-chart-desc',
'layout',
'layout-auto',
'layout-balloon',
'layout-circle',
'layout-grid',
'layout-group-by',
'layout-hierarchy',
'layout-linear',
'layout-skew-grid',
'layout-sorted-clusters',
'many-to-many',
'many-to-one',
'one-to-many',
'one-to-one',
'pie-chart',
'polygon-filter',
'regression-chart',
'scatter-plot',
'series-add',
'series-configuration',
'series-derived',
'series-filtered',
'series-search',
'stacked-chart',
'step-chart',
'timeline-area-chart',
'timeline-bar-chart',
'timeline-line-chart',
'trending-down',
'trending-up',
'vertical-bar-chart-asc',
'vertical-bar-chart-desc',
'waterfall-chart',
].sort();

const BPIcon = ({ icon, displayIconName, style = {}, ...rest }) =>
(displayIconName ? (
<>
<Icon icon={icon} style={{ marginRight: '1em', ...style }} {...rest} /> {icon}
</>
) : (
<Icon icon={icon} style={style} {...rest} />
));

export default BPIcon;
16 changes: 16 additions & 0 deletions src/modules/RA/DataLayer/components/DataLayerForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import MinisheetTab from './tabs/MinisheetTab';
import FilterTab from './tabs/FilterTab';
import TableTab from './tabs/TableTab';
import StyleImageField from './StyleImageField';
import EmbedTab from './tabs/EmbedTab';

const initialErrorState = {
definition: false,
Expand All @@ -22,6 +23,7 @@ const initialErrorState = {
minisheet: false,
filter: false,
table: false,
embed: false,
};

const inErrorReducer = (state, { type, payload }) => {
Expand Down Expand Up @@ -91,6 +93,10 @@ const DataLayerForm = React.memo(props => {
dispatch({ type: 'minisheet', payload: inError });
}, []);

const onEmbedErrorChange = React.useCallback(() => {
dispatch({ type: 'embed', paylaod: false });
}, []);

const onTableErrorChange = React.useCallback(({
values: { fields = [], table_enable: tableEnable },
}) => {
Expand Down Expand Up @@ -205,6 +211,16 @@ const DataLayerForm = React.memo(props => {
<FormTab disabled={external} label="datalayer.form.widget.tab" path="other">
<JSONInput source="settings.widgets" label="resources.datalayer.fields.settings-widgets" fullWidth />
</FormTab>

<CustomFormTab
disabled={external}
label="datalayer.form.embed.tab"
path="embed"
inError={errorState.embed}
onChange={onEmbedErrorChange}
>
<EmbedTab />
</CustomFormTab>
</TabbedForm>
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import {
useTranslate,
useInput,
ArrayInput,
SimpleFormIterator,
} from 'react-admin';
import Typography from '@material-ui/core/Typography';

import EmbedItemInput from './EmbedItemInput';

const EmbedConfigField = ({ label, ...rest }) => {
const translate = useTranslate();
const {
meta: { error },
} = useInput(rest);

return (
<>
<Typography variant="h5" component="h2">
{translate(label)}
</Typography>

{error?.length > 0 &&
error.flatMap(
err =>
err &&
Object.entries(err).map(([key, value]) => (
<Typography color="error">
{key}: {translate(value)}
</Typography>
)),
)}
<div style={{ maxWidth: '60em' }}>
<ArrayInput source="settings.embed" label="">
<SimpleFormIterator>
<EmbedItemInput />
</SimpleFormIterator>
</ArrayInput>
</div>
</>
);
};

export default EmbedConfigField;
111 changes: 111 additions & 0 deletions src/modules/RA/DataLayer/components/tabs/EmbedTab/EmbedItemInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useEffect } from 'react';
import { BooleanInput, NumberInput, SelectInput, TextInput, useTranslate } from 'react-admin';

import {
Accordion,
AccordionDetails,
AccordionSummary,
Typography,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { useField } from 'react-final-form';
import Condition from '../../../../../../components/react-admin/Condition';
import BPIcon, { graphIcons } from '../../../../../../components/BPIcon';

const EmbedItemInput = ({ source }) => {
const [previewExpanded, setPreviewExpanded] = React.useState(false);
const translate = useTranslate();

const iconChoices = React.useMemo(
() =>
graphIcons.map(bpIcon => ({
id: bpIcon,
name: <BPIcon icon={bpIcon} displayIconName />,
})),
[],
);

const {
input: { value: url },
} = useField(`${source}.src`);

useEffect(() => {
setPreviewExpanded(false);
}, [url]);

return (
<div>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '1em' }}>
<SelectInput
source={`${source}.icon`}
label="datalayer.form.embed.icon"
choices={iconChoices}
translateChoice={false}
helperText={false}
SelectProps={{
renderValue: value => <BPIcon icon={value} />,
}}
style={{ width: '5em', minWidth: '5em' }}
/>

<TextInput
label="datalayer.form.embed.title"
source={`${source}.title`}
type="text"
style={{ width: '34em' }}
helperText="datalayer.form.embed.title-help"
/>
</div>
<TextInput
label="datalayer.form.embed.src"
source={`${source}.src`}
type="url"
style={{ width: '40em' }}
placeholder="https://myiframe.page"
helperText="datalayer.form.embed.src-help"
/>
<Condition
when={`${source}.src`}
is={val => val.match(/(^(https?:)?\/\/.)/)}
>
<Accordion
expanded={previewExpanded}
onChange={(_, val) => setPreviewExpanded(val)}
style={{ width: '40em', marginBottom: '2em' }}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{translate('datalayer.form.embed.preview')}</Typography>
</AccordionSummary>
<AccordionDetails>
{previewExpanded && (
<iframe width="100%" height="300px" title="preview" src={url} />
)}
</AccordionDetails>
</Accordion>
</Condition>
<BooleanInput
label="datalayer.form.embed.fullscreen"
source={`${source}.fullScreen`}
defaultValue
/>
<Condition when={`${source}.fullScreen`} is={false}>
<div style={{ display: 'flex', gap: '2em' }}>
<NumberInput
source={`${source}.size.width`}
label="datalayer.form.embed.width"
min={0}
step={1}
/>
<NumberInput
source={`${source}.size.height`}
label="datalayer.form.embed.height"
min={0}
step={1}
/>
</div>
</Condition>
</div>
);
};

export default EmbedItemInput;
70 changes: 70 additions & 0 deletions src/modules/RA/DataLayer/components/tabs/EmbedTab/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';

import { BooleanInput, useTranslate } from 'react-admin';
import { useField } from 'react-final-form';

import Typography from '@material-ui/core/Typography';
import Placeholder from '../../../../../../components/Placeholder';

import EmbedConfigField from './EmbedConfigField';

const validateEmbedFields = data => {
const valid = !data.some(({ label }) => label && !label.length);
if (!valid) {
return 'datalayer.form.embed.row-in-error';
}
return undefined;
};

const EmbedConfigTabContent = props => {
const { input: { value: embedEnable, onChange: onEmbedEnableChange } } = useField('settings.embed_enable');
const { input: { value: source } } = useField('source');
const { input: { value: embedExportEnable } } = useField('embed_export_enable');
const translate = useTranslate();

if (!source) {
return (
<Placeholder>
<Typography variant="h5" component="h2">
{translate('datalayer.form.embed.no-source')}
</Typography>
</Placeholder>
);
}

// No embed yet
if (!embedEnable) {
return (
<Placeholder>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1em', alignItems: 'center' }}>
<Typography variant="h5" component="h2">{translate('datalayer.form.embed.no-embed')}</Typography>
<BooleanInput
source="settings.embed_enable"
label="datalayer.form.embed.allow"
onChange={onEmbedEnableChange}
/>
</div>
</Placeholder>
);
}

return (
<>
<BooleanInput
source="settings.embed_enable"
label="datalayer.form.embed.allow-display-data-embed"
onChange={onEmbedEnableChange}
/>
<EmbedConfigField
source="fields"
label="datalayer.form.embed.all-fields"
exportEnabled={embedExportEnable}
validate={validateEmbedFields}
{...props}
/>
</>
);
};


export default EmbedConfigTabContent;