@vonaffenfels/contentful-slate-editor 1.1.27 → 1.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vonaffenfels/contentful-slate-editor",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
4
4
  "scripts": {
5
5
  "prepublish": "yarn run build",
6
6
  "dev": "yarn run start",
@@ -56,6 +56,8 @@
56
56
  "@babel/preset-env": "^7.13.15",
57
57
  "@babel/preset-react": "^7.13.13",
58
58
  "@contentful/app-sdk": "^3.33.0",
59
+ "@contentful/f36-components": "^4.56.2",
60
+ "@contentful/f36-multiselect": "^4.21.0",
59
61
  "@contentful/field-editor-single-line": "^0.14.1",
60
62
  "@contentful/field-editor-test-utils": "^0.11.1",
61
63
  "@contentful/forma-36-fcss": "^0.3.1",
@@ -91,10 +93,10 @@
91
93
  "webpack-watch-files-plugin": "^1.2.1"
92
94
  },
93
95
  "dependencies": {
94
- "@vonaffenfels/slate-editor": "^1.1.27",
96
+ "@vonaffenfels/slate-editor": "^1.1.29",
95
97
  "webpack": "5.88.2"
96
98
  },
97
- "gitHead": "78986d6bb0100171800ced4a7c77c3d08de9e002",
99
+ "gitHead": "6f766a57107e33b358372874a69e2199dc14d8f6",
98
100
  "publishConfig": {
99
101
  "access": "public"
100
102
  }
@@ -1,83 +1,112 @@
1
- import React, {Component} from 'react';
1
+ import React, {
2
+ useEffect, useState,
3
+ } from 'react';
2
4
  import {
3
- Heading, Form, TextField, Workbench, Paragraph,
5
+ Heading, Form, TextField, Workbench,
4
6
  } from '@contentful/forma-36-react-components';
7
+ import {FormControl} from '@contentful/f36-components';
8
+ import {Multiselect} from '@contentful/f36-multiselect';
5
9
  import {css} from 'emotion';
6
10
 
7
- export default class Config extends Component {
11
+ const Config = ({sdk}) => {
12
+ const [params, setParams] = useState({
13
+ translationService: "",
14
+ usersWithTranslation: [],
15
+ });
16
+ const [spaceUsers, setSpaceUsers] = useState([]);
8
17
 
9
- constructor(props) {
10
- super(props);
11
- this.state = {parameters: {}};
18
+ const onConfigure = async () => {
19
+ const currentState = await sdk.app.getCurrentState();
12
20
 
13
- // `onConfigure` allows to configure a callback to be
14
- // invoked when a user attempts to install the app or update
15
- // its configuration.
16
- props.sdk.app.onConfigure(() => this.onConfigure());
17
- }
18
-
19
- async componentDidMount() {
20
- // Get current parameters of the app.
21
- // If the app is not installed yet, `parameters` will be `null`.
22
- const parameters = await this.props.sdk.app.getParameters();
21
+ return {
22
+ parameters: params,
23
+ targetState: currentState,
24
+ };
25
+ };
23
26
 
24
- this.setState(parameters ? {parameters} : this.state, () => {
25
- // Once preparation has finished, call `setReady` to hide
26
- // the loading screen and present the app to a user.
27
- this.props.sdk.app.setReady();
27
+ const onFieldChanged = (value, field) => {
28
+ setParams({
29
+ ...params,
30
+ [field]: value,
28
31
  });
29
- }
32
+ };
30
33
 
31
- async onConfigure() {
32
- // This method will be called when a user clicks on "Install"
33
- // or "Save" in the configuration screen.
34
- // for more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook
34
+ useEffect(() => {
35
+ if (sdk) {
36
+ sdk.app.getParameters().then(params => {
37
+ setParams(params);
38
+ sdk.app.setReady();
39
+ });
40
+ sdk.space.getUsers().then(users => {
41
+ setSpaceUsers(users.items.map(v => v.email));
42
+ });
43
+ }
44
+ }, [sdk]);
35
45
 
36
- // Get current the state of EditorInterface and other entities
37
- // related to this app installation
38
- const currentState = await this.props.sdk.app.getCurrentState();
46
+ useEffect(() => {
47
+ if (sdk) {
48
+ sdk.app.onConfigure(() => onConfigure());
49
+ }
50
+ }, [sdk, params]);
39
51
 
40
- return {
41
- // Parameters to be persisted as the app configuration.
42
- parameters: this.state.parameters,
43
- // In case you don't want to submit any update to app
44
- // locations, you can just pass the currentState as is
45
- targetState: currentState,
46
- };
47
- }
52
+ const handleSelectItem = (event) => {
53
+ const {
54
+ checked,
55
+ value,
56
+ } = event.target;
57
+
58
+ let newValue = params?.usersWithTranslation || [];
59
+ if (checked) {
60
+ newValue = [...newValue, value];
61
+ } else {
62
+ newValue = newValue.filter((email) => email !== value);
63
+ }
48
64
 
49
- onFieldChanged(value, field) {
50
- this.setState({
51
- parameters: {
52
- ...this.state.parameters,
53
- [field]: value,
54
- },
65
+ setParams({
66
+ ...params,
67
+ usersWithTranslation: newValue,
55
68
  });
56
- }
69
+ };
70
+
71
+
72
+ return (
73
+ <Workbench className={css({
74
+ padding: '80px',
75
+ backgroundColor: "#FFF",
76
+ })}>
77
+ <Form>
78
+ <Heading>Configuration</Heading>
57
79
 
58
- render() {
59
- return (
60
- <Workbench className={css({
61
- padding: '80px',
62
- backgroundColor: "#FFF",
63
- })}>
64
- <Form>
65
- <Heading>Configuration</Heading>
66
- <TextField
67
- value={this.state.parameters.cloudinaryApiKey}
68
- onChange={e => this.onFieldChanged(e.target.value, "cloudinaryApiKey")}
69
- labelText="Cloudinary API Key"
70
- required
71
- />
72
- <TextField
73
- value={this.state.parameters.cloudinaryCloudName}
74
- onChange={e => this.onFieldChanged(e.target.value, "cloudinaryCloudName")}
75
- labelText="Cloudinary Cloud Name"
76
- required
77
- />
78
- </Form>
79
- </Workbench>
80
- );
81
- }
80
+ <FormControl>
81
+ <FormControl.Label>Benutzer mit Übersetzung im Editor</FormControl.Label>
82
+ <Multiselect
83
+ currentSelection={params.usersWithTranslation}
84
+ popoverProps={{isFullWidth: true}}
85
+ >
86
+ {spaceUsers.map((userEmail) => {
87
+ const val = userEmail.toLowerCase().replace(/\s/g, '-');
88
+ return (
89
+ <Multiselect.Option
90
+ key={`user-${val}}`}
91
+ itemId={val}
92
+ value={userEmail}
93
+ label={userEmail}
94
+ onSelectItem={handleSelectItem}
95
+ isChecked={params.usersWithTranslation?.includes(userEmail)}
96
+ />
97
+ );
98
+ })}
99
+ </Multiselect>
100
+ </FormControl>
101
+ <TextField
102
+ value={params.translationService}
103
+ onChange={e => onFieldChanged(e.target.value, "translationService")}
104
+ labelText="Translation Service URL"
105
+ required
106
+ />
107
+ </Form>
108
+ </Workbench>
109
+ );
110
+ };
82
111
 
83
- }
112
+ export default Config;
@@ -7,6 +7,7 @@ import componentLoader from "@vonaffenfels/slate-editor/componentLoader";
7
7
  import BlockEditor from "@vonaffenfels/slate-editor/dist/BlockEditor";
8
8
  // eslint-disable-next-line import/no-unresolved
9
9
  import storybookStories from "storybookStories";
10
+ import {DialogTranslation} from "./DialogTranslation";
10
11
 
11
12
  const Dialog = ({
12
13
  sdk,
@@ -30,6 +31,8 @@ const Dialog = ({
30
31
  switch (sdk.parameters.invocation.type) {
31
32
  case "cloudinary":
32
33
  return <DialogCloudinary sdk={sdk} config={sdk.parameters.invocation} activeConfig={activeConfig} />;
34
+ case "translation":
35
+ return <DialogTranslation sdk={sdk} config={sdk.parameters.invocation} activeConfig={activeConfig} />;
33
36
  default:
34
37
  return <div className="relative flex h-full max-h-full justify-between">
35
38
  <BlockEditor
@@ -0,0 +1,119 @@
1
+ import {
2
+ useEffect, useState,
3
+ } from "react";
4
+ import {
5
+ Button, Table,
6
+ } from "@contentful/f36-components";
7
+
8
+ export const DialogTranslation = ({
9
+ sdk,
10
+ activeConfig,
11
+ }) => {
12
+ const {entryId} = sdk.parameters.invocation;
13
+ const [entry, setEntry] = useState(null);
14
+ const [loading, setLoading] = useState({});
15
+ const [contentType, setContentType] = useState(null);
16
+
17
+ useEffect(() => {
18
+ sdk.space.getEntry(entryId).then(entry => {
19
+ sdk.space.getContentType(entry.sys.contentType.sys.id).then(contentType => {
20
+ setEntry(entry);
21
+ setContentType(contentType);
22
+ });
23
+ });
24
+ }, []);
25
+
26
+ if (!contentType) {
27
+ return null;
28
+ }
29
+
30
+ const translateValue = async (content, targetLanguage, translateFromLocale = "de") => {
31
+ if (!sdk?.parameters?.installation?.translationService) {
32
+ console.error(`no translation service configured!`);
33
+ return;
34
+ }
35
+
36
+ try {
37
+ const response = await fetch(sdk?.parameters?.installation?.translationService, {
38
+ method: "POST",
39
+ body: JSON.stringify({
40
+ editorContent: typeof content === "object" ? content : undefined,
41
+ text: typeof content === "string" ? content : undefined,
42
+ targetLanguage,
43
+ sourceLanguage: translateFromLocale,
44
+ }),
45
+ });
46
+
47
+ const translatedValue = await response.json();
48
+ return translatedValue?.translation || "";
49
+ } catch (e) {
50
+ console.error(e);
51
+ }
52
+ };
53
+
54
+ const localizedFields = contentType.fields.filter(field => field.localized);
55
+ const onTranslateClick = async (targetLanguage) => {
56
+ setLoading({[targetLanguage]: true});
57
+ const newEntry = {...entry};
58
+ for (let i = 0; i < localizedFields.length; i++) {
59
+ const localizedField = localizedFields[i];
60
+ const newValue = await translateValue(entry?.fields[localizedField.id]?.[sdk.locales.default], targetLanguage);
61
+
62
+ newEntry.fields[localizedField.id][targetLanguage] = newValue;
63
+ }
64
+
65
+ await sdk.space.updateEntry(newEntry);
66
+ setEntry(await sdk.space.getEntry(entryId));
67
+ setLoading({});
68
+ };
69
+
70
+
71
+ return <div className="flex flex-col gap-2 p-4">
72
+ <Table>
73
+ <Table.Head>
74
+ <Table.Row>
75
+ <Table.Cell />
76
+ {sdk.locales.available.map(locale => {
77
+ return <Table.Cell key={locale}>
78
+ {locale !== "de" && <Button size="small" variant="primary" onClick={e => onTranslateClick(locale)}>Übersetzen</Button>}
79
+ </Table.Cell>;
80
+ })}
81
+ </Table.Row>
82
+ </Table.Head>
83
+ <Table.Body>
84
+ <Table.Row>
85
+ <Table.Cell />
86
+ {sdk.locales.available.map(locale => {
87
+ const name = sdk.locales.names[locale];
88
+
89
+ return <Table.Cell key={locale}>
90
+ {name}
91
+ </Table.Cell>;
92
+ })}
93
+ </Table.Row>
94
+ {localizedFields.map(field => {
95
+ return <Table.Row key={field.id}>
96
+ <Table.Cell>
97
+ {field.name}
98
+ </Table.Cell>
99
+ {sdk.locales.available.map(locale => {
100
+ let preview = entry?.fields[field.id]?.[locale];
101
+
102
+ if (!preview) {
103
+ preview = "";
104
+ }
105
+
106
+ if (typeof preview === "object") {
107
+ preview = "Keine Vorschau...";
108
+ }
109
+
110
+ return <Table.Cell key={locale}>
111
+ {preview}
112
+ </Table.Cell>;
113
+ })}
114
+ </Table.Row>;
115
+ })}
116
+ </Table.Body>
117
+ </Table>
118
+ </div>;
119
+ };
@@ -22,6 +22,7 @@ const EditorField = ({
22
22
  sdk,
23
23
  portal,
24
24
  locale,
25
+ setLocale,
25
26
  onStorybookElementClick,
26
27
  elementPropsMap,
27
28
  activeConfig,
@@ -151,6 +152,8 @@ const EditorField = ({
151
152
  <BlockEditor
152
153
  onChange={onChange}
153
154
  value={validateValue(value)}
155
+ locale={locale}
156
+ setLocale={setLocale}
154
157
  contentfulSdk={sdk}
155
158
  config={JSON.parse("{}")}
156
159
  elementPropsMap={elementPropsMap}
@@ -37,6 +37,7 @@ const Entry = ({
37
37
  fieldSdk={fieldSdk}
38
38
  editorSdk={sdk.editor}
39
39
  locale={locale}
40
+ setLocale={setLocale}
40
41
  elementPropsMap={elementPropsMap}
41
42
  entrySdk={sdk.entry}
42
43
  appSdk={sdk.app}
@@ -0,0 +1,71 @@
1
+ import {Button} from "@contentful/f36-components";
2
+ import React, {
3
+ useEffect, useState,
4
+ } from "react";
5
+
6
+ export const Sidebar = ({
7
+ sdk,
8
+ activeConfig,
9
+ }) => {
10
+ return <div className="flex w-full flex-col gap-2">
11
+ <div className="w-full">
12
+ <OpenProdPageLink sdk={sdk} activeConfig={activeConfig}/>
13
+ </div>
14
+ <div className="w-full">
15
+ <SidebarTranslations sdk={sdk}/>
16
+ </div>
17
+ </div>;
18
+ };
19
+
20
+ const OpenProdPageLink = ({
21
+ activeConfig,
22
+ sdk,
23
+ }) => {
24
+ const [slug, setSlug] = useState(sdk?.entry?.fields?.slug?.getValue());
25
+
26
+ useEffect(() => {
27
+ if (sdk?.entry?.fields?.slug) {
28
+ return sdk.entry.fields.slug.onValueChanged(setSlug);
29
+ }
30
+ }, [sdk]);
31
+
32
+ const disabled = !activeConfig?.domain || !slug;
33
+ const link = `${activeConfig.domain}/${slug}`;
34
+
35
+ const onClick = () => {
36
+ if (disabled) {
37
+ return;
38
+ }
39
+ window.open(link, "_blank");
40
+ };
41
+
42
+ return <Button isDisabled={disabled} isFullWidth={true} variant="secondary" onClick={onClick}>Live-Seite öffnen</Button>;
43
+ };
44
+
45
+ const SidebarTranslations = ({sdk}) => {
46
+ const enabled = sdk?.parameters?.installation?.usersWithTranslation?.includes(sdk?.user?.email) && sdk?.parameters?.installation?.translationService;
47
+
48
+ if (!enabled) {
49
+ return null;
50
+ }
51
+
52
+ const onClick = () => {
53
+ sdk.dialogs.openCurrentApp({
54
+ title: 'Übersetzen',
55
+ width: 'fullWidth',
56
+ position: "center",
57
+ allowHeightOverflow: true,
58
+ shouldCloseOnOverlayClick: false,
59
+ shouldCloseOnEscapePress: false,
60
+ minHeight: 500,
61
+ parameters: {
62
+ type: "translation",
63
+ entryId: sdk.entry.getSys().id,
64
+ },
65
+ });
66
+ };
67
+
68
+ return <>
69
+ <Button isFullWidth={true} onClick={onClick}>Übersetzen</Button>
70
+ </>;
71
+ };
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ import EntryEditor from './components/EntryEditor';
14
14
  import Config from './components/ConfigScreen';
15
15
  import Field from './components/Field';
16
16
  import Dialog from "./components/Dialog";
17
+ import {Sidebar} from "./components/Sidebar";
17
18
 
18
19
  export const BaseContentfulApp = (props) => {
19
20
  init(async (sdk) => {
@@ -69,6 +70,10 @@ export const BaseContentfulApp = (props) => {
69
70
  location: locations.LOCATION_DIALOG,
70
71
  component: <Dialog sdk={sdk} elementPropsMap={elementPropsMap} activeConfig={config}/>,
71
72
  },
73
+ {
74
+ location: locations.LOCATION_ENTRY_SIDEBAR,
75
+ component: <Sidebar sdk={sdk} activeConfig={config}/>,
76
+ },
72
77
  ];
73
78
 
74
79
  ComponentLocationSettings.forEach((componentLocationSetting) => {