@truedat/core 4.58.7 → 4.59.0

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/src/api.js +3 -0
  4. package/src/components/AdminMenu.js +2 -0
  5. package/src/components/__tests__/__snapshots__/AdminMenu.spec.js.snap +13 -0
  6. package/src/hooks/index.js +1 -0
  7. package/src/hooks/useLocales.js +9 -0
  8. package/src/hooks/useMessages.js +15 -4
  9. package/src/i18n/components/EditableCell.js +53 -0
  10. package/src/i18n/components/I18nRoutes.js +24 -0
  11. package/src/i18n/components/MessageForm.js +141 -0
  12. package/src/i18n/components/Messages.js +124 -0
  13. package/src/i18n/components/MessagesTable.js +86 -0
  14. package/src/i18n/components/NewMessage.js +47 -0
  15. package/src/i18n/components/__tests__/EditableCell.spec.js +54 -0
  16. package/src/i18n/components/__tests__/I18nRoutes.spec.js +14 -0
  17. package/src/i18n/components/__tests__/MessageForm.spec.js +28 -0
  18. package/src/i18n/components/__tests__/Messages.spec.js +33 -0
  19. package/src/i18n/components/__tests__/NewMessage.spec.js +21 -0
  20. package/src/i18n/components/__tests__/__snapshots__/EditableCell.spec.js.snap +30 -0
  21. package/src/i18n/components/__tests__/__snapshots__/I18nRoutes.spec.js.snap +3 -0
  22. package/src/i18n/components/__tests__/__snapshots__/MessageForm.spec.js.snap +91 -0
  23. package/src/i18n/components/__tests__/__snapshots__/Messages.spec.js.snap +193 -0
  24. package/src/i18n/components/__tests__/__snapshots__/NewMessage.spec.js.snap +127 -0
  25. package/src/i18n/components/index.js +3 -0
  26. package/src/messages/en.js +18 -0
  27. package/src/messages/es.js +18 -0
  28. package/src/routes.js +6 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.59.0] 2023-01-09
4
+
5
+ ### Added
6
+
7
+ - [TD-1968] I18n messages management view
8
+
3
9
  ## [4.58.4] 2022-12-22
4
10
 
5
11
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "4.58.7",
3
+ "version": "4.59.0",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -117,5 +117,5 @@
117
117
  "react-dom": ">= 16.8.6 < 17",
118
118
  "semantic-ui-react": ">= 2.0.3 < 2.2"
119
119
  },
120
- "gitHead": "4803deea029616582194b9704bf57c7ef9ea4faa"
120
+ "gitHead": "7d2cd52d47210cdd67cf3521697dab38c963a570"
121
121
  }
package/src/api.js CHANGED
@@ -1,2 +1,5 @@
1
1
  export const API_COMMENTS = "/api/business_concepts/comments";
2
2
  export const API_LOCALE_MESSAGES = "/api/locales/:lang/messages";
3
+ export const API_LOCALES = "/api/locales";
4
+ export const API_MESSAGE = "/api/messages/:id";
5
+ export const API_MESSAGES = "/api/messages";
@@ -3,6 +3,7 @@ import { useAuthorized } from "../hooks";
3
3
  import {
4
4
  CONFIGURATIONS,
5
5
  JOBS,
6
+ I18N_MESSAGES,
6
7
  RELATION_TAGS,
7
8
  SOURCES,
8
9
  SUBSCRIPTIONS,
@@ -17,6 +18,7 @@ const items = [
17
18
  { name: "sources", routes: [SOURCES] },
18
19
  { name: "jobs", routes: [JOBS] },
19
20
  { name: "configurations", routes: [CONFIGURATIONS] },
21
+ { name: "i18nMessages", routes: [I18N_MESSAGES] },
20
22
  ];
21
23
 
22
24
  export const AdminMenu = () => {
@@ -108,6 +108,19 @@ exports[`<AdminMenu /> matches the latest snapshot 1`] = `
108
108
  Configuration
109
109
  </span>
110
110
  </a>
111
+ <a
112
+ aria-checked="false"
113
+ class="item"
114
+ href="/i18n/messages"
115
+ name="i18nMessages"
116
+ role="option"
117
+ >
118
+ <span
119
+ class="text"
120
+ >
121
+ Translations
122
+ </span>
123
+ </a>
111
124
  </div>
112
125
  </div>
113
126
  </div>
@@ -1,6 +1,7 @@
1
1
  export * from "./useActiveRoute";
2
2
  export * from "./useActiveRoutes";
3
3
  export * from "./useAuthorized";
4
+ export * from "./useLocales";
4
5
  export * from "./useMessages";
5
6
  export * from "./usePath";
6
7
  export * from "./useOnScreen";
@@ -0,0 +1,9 @@
1
+ import useSWR from "swr";
2
+ import { API_LOCALES } from "../api";
3
+ import { apiJson } from "../services/api";
4
+
5
+ export const useLocales = () => {
6
+ const { data, error, mutate } = useSWR(API_LOCALES, apiJson);
7
+ const locales = data?.data?.data;
8
+ return { locales, error, loading: !error && !data, mutate };
9
+ };
@@ -1,13 +1,24 @@
1
1
  import { compile } from "path-to-regexp";
2
2
  import useSWRImmutable from "swr/immutable";
3
- import { API_LOCALE_MESSAGES } from "../api";
4
- import { apiJson } from "../services/api";
3
+ import useSWRMutations from "swr/mutation";
4
+ import { API_LOCALE_MESSAGES, API_MESSAGE, API_MESSAGES } from "../api";
5
+ import { apiJson, apiJsonPatch, apiJsonPost } from "../services/api";
5
6
 
6
- const toApiPath = compile(API_LOCALE_MESSAGES);
7
+ const toApiLocaleMessagesPath = compile(API_LOCALE_MESSAGES);
8
+ const toApiMessagePath = compile(API_MESSAGE);
7
9
 
8
10
  export const useMessages = (lang) => {
9
- const url = toApiPath({ lang });
11
+ const url = toApiLocaleMessagesPath({ lang });
10
12
  const { data, error } = useSWRImmutable(url, apiJson);
11
13
  const messages = data?.data;
12
14
  return { messages, error, loading: !error && !data };
13
15
  };
16
+
17
+ export const useMessagePatch = (id) => {
18
+ const url = toApiMessagePath({ id });
19
+ return useSWRMutations(url, (url, { arg }) => apiJsonPatch(url, arg));
20
+ };
21
+
22
+ export const useMessagePost = () => {
23
+ return useSWRMutations(API_MESSAGES, (url, { arg }) => apiJsonPost(url, arg));
24
+ };
@@ -0,0 +1,53 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import React, { useState } from "react";
4
+ import { Table, Input } from "semantic-ui-react";
5
+
6
+ const MAX_LENGTH = 255;
7
+
8
+ export default function EditableCell({
9
+ value: propValue,
10
+ placeholder,
11
+ onChange,
12
+ }) {
13
+ const [editionMode, setEditMode] = useState(false);
14
+ const [value, setValue] = useState(propValue || "");
15
+
16
+ const onBlur = () => {
17
+ if (_.isEmpty(value)) setValue(propValue);
18
+ else if (value != propValue) onChange(value);
19
+ setEditMode(false);
20
+ };
21
+
22
+ return editionMode ? (
23
+ <Table.Cell
24
+ className="no-padding"
25
+ width={5}
26
+ content={
27
+ <Input
28
+ fluid
29
+ maxLength={MAX_LENGTH}
30
+ onBlur={onBlur}
31
+ placeholder={placeholder}
32
+ onChange={(e) => setValue(e.target.value)}
33
+ value={value}
34
+ autoFocus
35
+ />
36
+ }
37
+ />
38
+ ) : (
39
+ <Table.Cell
40
+ className="cursor-pointer"
41
+ width={5}
42
+ content={value}
43
+ onClick={() => setEditMode(true)}
44
+ onFocus={() => setEditMode(true)}
45
+ />
46
+ );
47
+ }
48
+
49
+ EditableCell.propTypes = {
50
+ value: PropTypes.string,
51
+ placeholder: PropTypes.string,
52
+ onChange: PropTypes.func,
53
+ };
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ import { Route, Switch } from "react-router-dom";
3
+ import { Unauthorized } from "@truedat/core/components";
4
+ import { useAuthorized } from "@truedat/core/hooks";
5
+ import { I18N, I18N_MESSAGES, I18N_MESSAGES_NEW } from "@truedat/core/routes";
6
+ import Messages from "./Messages";
7
+ import NewMessage from "./NewMessage";
8
+
9
+ export const AuthorizedI18nRoutes = () => (
10
+ <Switch>
11
+ <Route exact path={I18N_MESSAGES} render={() => <Messages />} />
12
+ <Route exact path={I18N_MESSAGES_NEW} render={() => <NewMessage />} />
13
+ </Switch>
14
+ );
15
+
16
+ export default function I18nRoutes() {
17
+ const authorized = useAuthorized();
18
+ return (
19
+ <Route
20
+ path={I18N}
21
+ render={() => (authorized ? <AuthorizedI18nRoutes /> : <Unauthorized />)}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,141 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import React from "react";
4
+ import { useForm, Controller } from "react-hook-form";
5
+ import { useIntl } from "react-intl";
6
+ import { Button, Form, Segment, Header, Loader } from "semantic-ui-react";
7
+ import { HistoryBackButton } from "@truedat/core/components";
8
+ import { useLocales } from "@truedat/core/hooks";
9
+
10
+ export const LangForm = ({
11
+ control,
12
+ lang: { lang, id },
13
+ errors,
14
+ formatMessage,
15
+ }) => {
16
+ return (
17
+ <Segment>
18
+ <Header
19
+ as="h4"
20
+ content={formatMessage({ id: `i18n.messages.locale.${lang}` })}
21
+ />
22
+ <Controller
23
+ control={control}
24
+ name={`langs.${id}.definition`}
25
+ rules={{
26
+ required: formatMessage(
27
+ { id: "form.validation.required" },
28
+ { prop: formatMessage({ id: "i18n.message.props.definition" }) }
29
+ ),
30
+ }}
31
+ render={({ field: { onBlur, onChange, value } }) => (
32
+ <Form.Input
33
+ autoComplete="off"
34
+ error={_.path(["langs", id, "definition", "message"])(errors)}
35
+ label={formatMessage({ id: "i18n.message.props.definition" })}
36
+ onBlur={onBlur}
37
+ onChange={(_e, { value }) => onChange(value)}
38
+ placeholder={formatMessage({
39
+ id: "i18n.message.form.definition.placeholder",
40
+ })}
41
+ value={value || ""}
42
+ required
43
+ />
44
+ )}
45
+ />
46
+ <Controller
47
+ control={control}
48
+ name={`langs.${id}.description`}
49
+ render={({ field: { onBlur, onChange, value } }) => (
50
+ <Form.Input
51
+ autoComplete="off"
52
+ label={formatMessage({ id: "i18n.message.props.description" })}
53
+ onBlur={onBlur}
54
+ onChange={(_e, { value }) => onChange(value)}
55
+ placeholder={formatMessage({
56
+ id: "i18n.message.form.description.placeholder",
57
+ })}
58
+ value={value || ""}
59
+ />
60
+ )}
61
+ />
62
+ </Segment>
63
+ );
64
+ };
65
+
66
+ export const MessageForm = ({ onSubmit, isSubmitting }) => {
67
+ const { locales, loading } = useLocales();
68
+ const { formatMessage } = useIntl();
69
+ const { handleSubmit, control, formState } = useForm({
70
+ mode: "all",
71
+ defaultValues: {
72
+ message_id: "",
73
+ langs: {},
74
+ },
75
+ });
76
+ const { errors, isDirty, isValid } = formState;
77
+
78
+ return loading ? (
79
+ <Loader />
80
+ ) : (
81
+ <Form onSubmit={handleSubmit(onSubmit)}>
82
+ <Controller
83
+ control={control}
84
+ name="message_id"
85
+ rules={{
86
+ required: formatMessage(
87
+ { id: "form.validation.required" },
88
+ { prop: formatMessage({ id: "i18n.message.props.messageId" }) }
89
+ ),
90
+ }}
91
+ render={({ field: { onBlur, onChange, value } }) => (
92
+ <Form.Input
93
+ autoComplete="off"
94
+ error={_.path(["message_id", "message"])(errors)}
95
+ label={formatMessage({ id: "i18n.message.props.messageId" })}
96
+ onBlur={onBlur}
97
+ onChange={(_e, { value }) => onChange(value)}
98
+ placeholder={formatMessage({
99
+ id: "i18n.message.form.messageId.placeholder",
100
+ })}
101
+ value={value || ""}
102
+ required
103
+ />
104
+ )}
105
+ />
106
+
107
+ {locales.map((lang, i) => (
108
+ <LangForm
109
+ key={i}
110
+ control={control}
111
+ lang={lang}
112
+ errors={errors}
113
+ formatMessage={formatMessage}
114
+ />
115
+ ))}
116
+
117
+ <div className="actions">
118
+ <Button
119
+ floated="right"
120
+ type="submit"
121
+ primary
122
+ loading={isSubmitting}
123
+ disabled={isSubmitting || !isDirty || !isValid}
124
+ content={formatMessage({ id: "actions.save" })}
125
+ />
126
+ <HistoryBackButton
127
+ content={formatMessage({ id: "actions.cancel" })}
128
+ disabled={isSubmitting}
129
+ />
130
+ </div>
131
+ </Form>
132
+ );
133
+ };
134
+
135
+ MessageForm.propTypes = {
136
+ message: PropTypes.object,
137
+ onSubmit: PropTypes.func,
138
+ isSubmitting: PropTypes.bool,
139
+ };
140
+
141
+ export default MessageForm;
@@ -0,0 +1,124 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { useIntl, FormattedMessage } from "react-intl";
4
+ import {
5
+ Button,
6
+ Header,
7
+ Divider,
8
+ Icon,
9
+ Segment,
10
+ Dimmer,
11
+ Loader,
12
+ Menu,
13
+ } from "semantic-ui-react";
14
+ import { Link } from "react-router-dom";
15
+ import { SearchInput } from "@truedat/core/components";
16
+ import { lowerDeburrTrim } from "@truedat/core/services/sort";
17
+ import { useLocales } from "@truedat/core/hooks";
18
+ import { I18N_MESSAGES_NEW } from "@truedat/core/routes";
19
+ import { Pagination } from "@truedat/core/components";
20
+ import MessagesTable from "./MessagesTable";
21
+
22
+ const ITEMS_PER_PAGE = 30;
23
+
24
+ export function MessagesContent({ locales, loading }) {
25
+ const langs = _.map("lang")(locales);
26
+ const [selectedLang, setSelectedLang] = useState(_.head(langs));
27
+ const [page, setPage] = useState(1);
28
+ const [filter, setFilter] = useState("");
29
+ const { formatMessage } = useIntl();
30
+
31
+ const handleSearch = (_e, data) => {
32
+ _.flow(_.propOr("", "value"), setFilter)(data);
33
+ setPage(1);
34
+ };
35
+
36
+ const messages = _.flow(
37
+ _.find({ lang: selectedLang }),
38
+ _.prop("messages"),
39
+ _.filter(({ message_id, definition, description }) => {
40
+ const deburrFilter = lowerDeburrTrim(filter);
41
+ return (
42
+ deburrFilter == "" ||
43
+ lowerDeburrTrim(message_id).includes(deburrFilter) ||
44
+ lowerDeburrTrim(definition).includes(deburrFilter) ||
45
+ lowerDeburrTrim(description).includes(deburrFilter)
46
+ );
47
+ })
48
+ )(locales);
49
+
50
+ const totalPages = Math.ceil(messages.length / ITEMS_PER_PAGE);
51
+ const paginatedMessages = _.flow(
52
+ _.drop((page - 1) * ITEMS_PER_PAGE),
53
+ _.take(ITEMS_PER_PAGE)
54
+ )(messages);
55
+
56
+ return (
57
+ <>
58
+ <Menu attached="top" secondary pointing tabular>
59
+ {langs.map((lang, i) => (
60
+ <Menu.Item
61
+ key={i}
62
+ active={lang === selectedLang}
63
+ onClick={() => setSelectedLang(lang)}
64
+ >
65
+ <FormattedMessage id={`i18n.messages.locale.${lang}`} />
66
+ </Menu.Item>
67
+ ))}
68
+ </Menu>
69
+ <Divider hidden />
70
+ <SearchInput
71
+ onChange={handleSearch}
72
+ placeholder={formatMessage({ id: "i18n.messages.search.placeholder" })}
73
+ value={filter}
74
+ />
75
+ <MessagesTable
76
+ messages={paginatedMessages}
77
+ locales={locales}
78
+ loading={loading}
79
+ />
80
+ <Pagination
81
+ totalPages={totalPages}
82
+ activePage={page}
83
+ selectPage={({ activePage }) => setPage(activePage)}
84
+ />
85
+ </>
86
+ );
87
+ }
88
+
89
+ export default function Messages() {
90
+ const { formatMessage } = useIntl();
91
+ const { locales, loading } = useLocales();
92
+
93
+ return (
94
+ <Segment>
95
+ <Header as="h2">
96
+ <Icon circular name="language" />
97
+ <Header.Content>
98
+ <FormattedMessage id={"i18n.messages.header"} />
99
+ <Header.Subheader>
100
+ <FormattedMessage id={"i18n.messages.subheader"} />
101
+ </Header.Subheader>
102
+ </Header.Content>
103
+ </Header>
104
+ <Segment attached="bottom">
105
+ <Dimmer.Dimmable dimmed={loading}>
106
+ <Dimmer active={loading} inverted>
107
+ <Loader />
108
+ </Dimmer>
109
+ </Dimmer.Dimmable>
110
+ <Button
111
+ floated="right"
112
+ primary
113
+ content={formatMessage({ id: "i18n.actions.createMessage" })}
114
+ icon="add circle"
115
+ as={Link}
116
+ to={I18N_MESSAGES_NEW}
117
+ />
118
+ {!loading ? (
119
+ <MessagesContent locales={locales} loading={loading} />
120
+ ) : null}
121
+ </Segment>
122
+ </Segment>
123
+ );
124
+ }
@@ -0,0 +1,86 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { useIntl } from "react-intl";
5
+ import { Table, Header, Icon } from "semantic-ui-react";
6
+ import { useMessagePatch } from "@truedat/core/hooks";
7
+ import EditableCell from "./EditableCell";
8
+
9
+ export const MessageRow = ({ message, formatMessage }) => {
10
+ const { trigger } = useMessagePatch(message.id);
11
+
12
+ const doPatch = (field) => (value) =>
13
+ trigger({ message: { [field]: value } });
14
+
15
+ return (
16
+ <Table.Row>
17
+ <Table.Cell width={6} content={message?.message_id} />
18
+ <EditableCell
19
+ value={message?.definition}
20
+ onChange={doPatch("definition")}
21
+ placeholder={formatMessage({
22
+ id: "i18n.message.form.definition.placeholder",
23
+ })}
24
+ />
25
+ <EditableCell
26
+ value={message?.description}
27
+ onChange={doPatch("description")}
28
+ placeholder={formatMessage({
29
+ id: "i18n.message.form.description.placeholder",
30
+ })}
31
+ />
32
+ </Table.Row>
33
+ );
34
+ };
35
+
36
+ export const MessagesTable = ({ messages, loading }) => {
37
+ const { formatMessage } = useIntl();
38
+
39
+ return (
40
+ <>
41
+ {!_.isEmpty(messages) && (
42
+ <Table>
43
+ <Table.Header>
44
+ <Table.Row>
45
+ <Table.HeaderCell
46
+ content={formatMessage({ id: "i18n.message.props.messageId" })}
47
+ />
48
+ <Table.HeaderCell
49
+ content={formatMessage({ id: "i18n.message.props.definition" })}
50
+ />
51
+ <Table.HeaderCell
52
+ content={formatMessage({
53
+ id: "i18n.message.props.description",
54
+ })}
55
+ />
56
+ </Table.Row>
57
+ </Table.Header>
58
+ <Table.Body>
59
+ {messages.map((message) => (
60
+ <MessageRow
61
+ key={message.id}
62
+ message={message}
63
+ formatMessage={formatMessage}
64
+ />
65
+ ))}
66
+ </Table.Body>
67
+ </Table>
68
+ )}
69
+ {_.isEmpty(messages) && !loading && (
70
+ <Header as="h4">
71
+ <Icon name="search" />
72
+ <Header.Content>
73
+ {formatMessage({ id: "i18n.messages.empty" })}
74
+ </Header.Content>
75
+ </Header>
76
+ )}
77
+ </>
78
+ );
79
+ };
80
+
81
+ MessagesTable.propTypes = {
82
+ messages: PropTypes.array,
83
+ loading: PropTypes.bool,
84
+ };
85
+
86
+ export default MessagesTable;
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { Breadcrumb, Header, Container, Segment } from "semantic-ui-react";
4
+ import { useIntl, FormattedMessage } from "react-intl";
5
+ import { useHistory, Link } from "react-router-dom";
6
+ import { useMessagePost } from "@truedat/core/hooks";
7
+ import { I18N_MESSAGES } from "@truedat/core/routes";
8
+ import MessageForm from "./MessageForm";
9
+
10
+ const NewMessage = () => {
11
+ const { formatMessage } = useIntl();
12
+ const { trigger } = useMessagePost();
13
+ const history = useHistory();
14
+
15
+ const onSubmit = (message) => {
16
+ trigger({ message });
17
+ history.push(I18N_MESSAGES);
18
+ };
19
+
20
+ return (
21
+ <>
22
+ <Breadcrumb>
23
+ <Breadcrumb.Section as={Link} to={I18N_MESSAGES} active={false}>
24
+ <FormattedMessage id="i18n.messages.header" />
25
+ </Breadcrumb.Section>
26
+ <Breadcrumb.Divider icon="right angle" />
27
+ <Breadcrumb.Section active>
28
+ <FormattedMessage id="i18n.actions.createMessage" />
29
+ </Breadcrumb.Section>
30
+ </Breadcrumb>
31
+ <Container text as={Segment}>
32
+ <Header
33
+ as="h2"
34
+ icon="language"
35
+ content={formatMessage({ id: "i18n.actions.createMessage" })}
36
+ />
37
+ <MessageForm onSubmit={onSubmit} />
38
+ </Container>
39
+ </>
40
+ );
41
+ };
42
+
43
+ NewMessage.propTypes = {
44
+ createUser: PropTypes.func,
45
+ };
46
+
47
+ export default NewMessage;
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { render } from "@truedat/test/render";
4
+ import messages from "@truedat/core/messages";
5
+ import EditableCell from "../EditableCell";
6
+
7
+ const renderOpts = { messages };
8
+
9
+ describe("<EditableCell />", () => {
10
+ const onChange = jest.fn();
11
+ const value = "value";
12
+ const placeholder = "placeholder";
13
+
14
+ it("matches the latest snapshot", () => {
15
+ const props = {
16
+ onChange,
17
+ value,
18
+ placeholder,
19
+ };
20
+ const { container } = render(<EditableCell {...props} />, renderOpts);
21
+ expect(container).toMatchSnapshot();
22
+ });
23
+
24
+ it("clicking the cell will enter edition mode and match snapshot", async () => {
25
+ const props = {
26
+ onChange,
27
+ value,
28
+ placeholder,
29
+ };
30
+ const { container, findByText } = render(
31
+ <EditableCell {...props} />,
32
+ renderOpts
33
+ );
34
+ userEvent.click(await findByText(value));
35
+ expect(container).toMatchSnapshot();
36
+ });
37
+
38
+ it("on edition mode edition mode will call onChange on blur", async () => {
39
+ const props = {
40
+ onChange,
41
+ value,
42
+ placeholder,
43
+ };
44
+ const { getByRole, findByText } = render(
45
+ <EditableCell {...props} />,
46
+ renderOpts
47
+ );
48
+ userEvent.click(await findByText(value));
49
+
50
+ userEvent.type(getByRole("textbox", { type: /text/i }), "foo");
51
+ userEvent.tab(getByRole("textbox", { type: /text/i }));
52
+ expect(onChange).toHaveBeenCalledWith("valuefoo");
53
+ });
54
+ });
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import I18nRoutes from "../I18nRoutes";
4
+
5
+ jest.mock("@truedat/core/hooks", () => ({
6
+ useAuthorized: jest.fn(() => true),
7
+ }));
8
+
9
+ describe("<I18nRoutes />", () => {
10
+ it("matches the latest snapshot", () => {
11
+ const { container } = render(<I18nRoutes />);
12
+ expect(container).toMatchSnapshot();
13
+ });
14
+ });
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { render } from "@truedat/test/render";
4
+ import messages from "@truedat/core/messages";
5
+ import MessageForm from "../MessageForm";
6
+
7
+ const renderOpts = { messages };
8
+
9
+ jest.mock("@truedat/core/hooks", () => ({
10
+ useLocales: jest.fn(() => ({
11
+ loading: false,
12
+ locales: [{ lang: "es", id: 1 }],
13
+ })),
14
+ }));
15
+
16
+ describe("<MessageForm />", () => {
17
+ const onSubmit = jest.fn();
18
+ const isSubmitting = false;
19
+
20
+ it("matches the latest snapshot", () => {
21
+ const props = {
22
+ onSubmit,
23
+ isSubmitting,
24
+ };
25
+ const { container } = render(<MessageForm {...props} />, renderOpts);
26
+ expect(container).toMatchSnapshot();
27
+ });
28
+ });
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import messages from "@truedat/core/messages";
4
+ import Messages from "../Messages";
5
+
6
+ const renderOpts = { messages };
7
+
8
+ jest.mock("@truedat/core/hooks", () => ({
9
+ useLocales: jest.fn(() => ({
10
+ loading: false,
11
+ locales: [
12
+ {
13
+ lang: "es",
14
+ id: 1,
15
+ messages: [
16
+ {
17
+ message_id: "message_id",
18
+ definition: "definition",
19
+ description: "description",
20
+ },
21
+ ],
22
+ },
23
+ ],
24
+ })),
25
+ useMessagePatch: jest.fn(() => ({ trigger: jest.fn() })),
26
+ }));
27
+
28
+ describe("<Messages />", () => {
29
+ it("matches the latest snapshot", () => {
30
+ const { container } = render(<Messages />, renderOpts);
31
+ expect(container).toMatchSnapshot();
32
+ });
33
+ });
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import messages from "@truedat/core/messages";
4
+ import NewMessage from "../NewMessage";
5
+
6
+ const renderOpts = { messages };
7
+
8
+ jest.mock("@truedat/core/hooks", () => ({
9
+ useLocales: jest.fn(() => ({
10
+ loading: false,
11
+ locales: [{ lang: "es", id: 1 }],
12
+ })),
13
+ useMessagePost: jest.fn(() => ({ trigger: jest.fn() })),
14
+ }));
15
+
16
+ describe("<NewMessage />", () => {
17
+ it("matches the latest snapshot", () => {
18
+ const { container } = render(<NewMessage />, renderOpts);
19
+ expect(container).toMatchSnapshot();
20
+ });
21
+ });
@@ -0,0 +1,30 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<EditableCell /> clicking the cell will enter edition mode and match snapshot 1`] = `
4
+ <div>
5
+ <td
6
+ class="five wide no-padding"
7
+ >
8
+ <div
9
+ class="ui fluid input"
10
+ >
11
+ <input
12
+ maxlength="255"
13
+ placeholder="placeholder"
14
+ type="text"
15
+ value="value"
16
+ />
17
+ </div>
18
+ </td>
19
+ </div>
20
+ `;
21
+
22
+ exports[`<EditableCell /> matches the latest snapshot 1`] = `
23
+ <div>
24
+ <td
25
+ class="five wide cursor-pointer"
26
+ >
27
+ value
28
+ </td>
29
+ </div>
30
+ `;
@@ -0,0 +1,3 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<I18nRoutes /> matches the latest snapshot 1`] = `<div />`;
@@ -0,0 +1,91 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<MessageForm /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <form
6
+ class="ui form"
7
+ >
8
+ <div
9
+ class="required field"
10
+ >
11
+ <label>
12
+ Message ID
13
+ </label>
14
+ <div
15
+ class="ui input"
16
+ >
17
+ <input
18
+ autocomplete="off"
19
+ placeholder="Message ID"
20
+ required=""
21
+ type="text"
22
+ value=""
23
+ />
24
+ </div>
25
+ </div>
26
+ <div
27
+ class="ui segment"
28
+ >
29
+ <h4
30
+ class="ui header"
31
+ >
32
+ Spanish
33
+ </h4>
34
+ <div
35
+ class="required field"
36
+ >
37
+ <label>
38
+ Definition
39
+ </label>
40
+ <div
41
+ class="ui input"
42
+ >
43
+ <input
44
+ autocomplete="off"
45
+ placeholder="Definition"
46
+ required=""
47
+ type="text"
48
+ value=""
49
+ />
50
+ </div>
51
+ </div>
52
+ <div
53
+ class="field"
54
+ >
55
+ <label>
56
+ Description
57
+ </label>
58
+ <div
59
+ class="ui input"
60
+ >
61
+ <input
62
+ autocomplete="off"
63
+ placeholder="Description"
64
+ type="text"
65
+ value=""
66
+ />
67
+ </div>
68
+ </div>
69
+ </div>
70
+ <div
71
+ class="actions"
72
+ >
73
+ <button
74
+ class="ui primary disabled right floated button"
75
+ disabled=""
76
+ tabindex="-1"
77
+ type="submit"
78
+ >
79
+ Save
80
+ </button>
81
+ <a
82
+ class="ui secondary button"
83
+ href="/"
84
+ role="button"
85
+ >
86
+ Cancel
87
+ </a>
88
+ </div>
89
+ </form>
90
+ </div>
91
+ `;
@@ -0,0 +1,193 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<Messages /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui segment"
7
+ >
8
+ <h2
9
+ class="ui header"
10
+ >
11
+ <i
12
+ aria-hidden="true"
13
+ class="language circular icon"
14
+ />
15
+ <div
16
+ class="content"
17
+ >
18
+ Translations
19
+ <div
20
+ class="sub header"
21
+ >
22
+ Messages used in Truedat
23
+ </div>
24
+ </div>
25
+ </h2>
26
+ <div
27
+ class="ui bottom attached segment"
28
+ >
29
+ <div
30
+ class="dimmable"
31
+ >
32
+ <div
33
+ class="ui inverted dimmer"
34
+ >
35
+ <div
36
+ class="content"
37
+ >
38
+ <div
39
+ class="ui loader"
40
+ />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <a
45
+ class="ui primary right floated button"
46
+ href="/i18n/messages/new"
47
+ role="button"
48
+ >
49
+ <i
50
+ aria-hidden="true"
51
+ class="add circle icon"
52
+ />
53
+ Create message
54
+ </a>
55
+ <div
56
+ class="ui pointing secondary top attached tabular menu"
57
+ >
58
+ <a
59
+ class="active item"
60
+ >
61
+ Spanish
62
+ </a>
63
+ </div>
64
+ <div
65
+ class="ui hidden divider"
66
+ />
67
+ <div
68
+ class="ui icon input"
69
+ >
70
+ <input
71
+ placeholder="Search messages"
72
+ type="text"
73
+ value=""
74
+ />
75
+ <i
76
+ aria-hidden="true"
77
+ class="search link icon"
78
+ />
79
+ </div>
80
+ <table
81
+ class="ui table"
82
+ >
83
+ <thead
84
+ class=""
85
+ >
86
+ <tr
87
+ class=""
88
+ >
89
+ <th
90
+ class=""
91
+ >
92
+ Message ID
93
+ </th>
94
+ <th
95
+ class=""
96
+ >
97
+ Definition
98
+ </th>
99
+ <th
100
+ class=""
101
+ >
102
+ Description
103
+ </th>
104
+ </tr>
105
+ </thead>
106
+ <tbody
107
+ class=""
108
+ >
109
+ <tr
110
+ class=""
111
+ >
112
+ <td
113
+ class="six wide"
114
+ >
115
+ message_id
116
+ </td>
117
+ <td
118
+ class="five wide cursor-pointer"
119
+ >
120
+ definition
121
+ </td>
122
+ <td
123
+ class="five wide cursor-pointer"
124
+ >
125
+ description
126
+ </td>
127
+ </tr>
128
+ </tbody>
129
+ </table>
130
+ <div
131
+ aria-label="Pagination Navigation"
132
+ class="ui pagination menu"
133
+ role="navigation"
134
+ >
135
+ <a
136
+ aria-current="false"
137
+ aria-disabled="true"
138
+ aria-label="First item"
139
+ class="disabled item"
140
+ tabindex="-1"
141
+ type="firstItem"
142
+ value="1"
143
+ >
144
+ «
145
+ </a>
146
+ <a
147
+ aria-current="false"
148
+ aria-disabled="true"
149
+ aria-label="Previous item"
150
+ class="disabled item"
151
+ tabindex="-1"
152
+ type="prevItem"
153
+ value="1"
154
+ >
155
+
156
+ </a>
157
+ <a
158
+ aria-current="true"
159
+ aria-disabled="true"
160
+ class="active disabled item"
161
+ tabindex="-1"
162
+ type="pageItem"
163
+ value="1"
164
+ >
165
+ 1
166
+ </a>
167
+ <a
168
+ aria-current="false"
169
+ aria-disabled="true"
170
+ aria-label="Next item"
171
+ class="disabled item"
172
+ tabindex="-1"
173
+ type="nextItem"
174
+ value="1"
175
+ >
176
+
177
+ </a>
178
+ <a
179
+ aria-current="false"
180
+ aria-disabled="true"
181
+ aria-label="Last item"
182
+ class="disabled item"
183
+ tabindex="-1"
184
+ type="lastItem"
185
+ value="1"
186
+ >
187
+ »
188
+ </a>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ `;
@@ -0,0 +1,127 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<NewMessage /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui breadcrumb"
7
+ >
8
+ <a
9
+ class="section"
10
+ href="/i18n/messages"
11
+ >
12
+ Translations
13
+ </a>
14
+ <i
15
+ aria-hidden="true"
16
+ class="right angle icon divider"
17
+ />
18
+ <div
19
+ class="active section"
20
+ >
21
+ Create message
22
+ </div>
23
+ </div>
24
+ <div
25
+ class="ui segment ui text container"
26
+ >
27
+ <h2
28
+ class="ui header"
29
+ >
30
+ <i
31
+ aria-hidden="true"
32
+ class="language icon"
33
+ />
34
+ <div
35
+ class="content"
36
+ >
37
+ Create message
38
+ </div>
39
+ </h2>
40
+ <form
41
+ class="ui form"
42
+ >
43
+ <div
44
+ class="required field"
45
+ >
46
+ <label>
47
+ Message ID
48
+ </label>
49
+ <div
50
+ class="ui input"
51
+ >
52
+ <input
53
+ autocomplete="off"
54
+ placeholder="Message ID"
55
+ required=""
56
+ type="text"
57
+ value=""
58
+ />
59
+ </div>
60
+ </div>
61
+ <div
62
+ class="ui segment"
63
+ >
64
+ <h4
65
+ class="ui header"
66
+ >
67
+ Spanish
68
+ </h4>
69
+ <div
70
+ class="required field"
71
+ >
72
+ <label>
73
+ Definition
74
+ </label>
75
+ <div
76
+ class="ui input"
77
+ >
78
+ <input
79
+ autocomplete="off"
80
+ placeholder="Definition"
81
+ required=""
82
+ type="text"
83
+ value=""
84
+ />
85
+ </div>
86
+ </div>
87
+ <div
88
+ class="field"
89
+ >
90
+ <label>
91
+ Description
92
+ </label>
93
+ <div
94
+ class="ui input"
95
+ >
96
+ <input
97
+ autocomplete="off"
98
+ placeholder="Description"
99
+ type="text"
100
+ value=""
101
+ />
102
+ </div>
103
+ </div>
104
+ </div>
105
+ <div
106
+ class="actions"
107
+ >
108
+ <button
109
+ class="ui primary disabled right floated button"
110
+ disabled=""
111
+ tabindex="-1"
112
+ type="submit"
113
+ >
114
+ Save
115
+ </button>
116
+ <a
117
+ class="ui secondary button"
118
+ href="/"
119
+ role="button"
120
+ >
121
+ Cancel
122
+ </a>
123
+ </div>
124
+ </form>
125
+ </div>
126
+ </div>
127
+ `;
@@ -0,0 +1,3 @@
1
+ import I18nRoutes from "./I18nRoutes";
2
+
3
+ export { I18nRoutes };
@@ -56,6 +56,23 @@ export default {
56
56
  "form.validation.required": "{prop} is required",
57
57
  "form.validation.minLength": "{prop} must have at least {value} characters",
58
58
  "form.validation.email.invalid": "Invalid email address",
59
+
60
+ "i18n.actions.createMessage": "Create message",
61
+
62
+ "i18n.message.form.messageId.placeholder": "Message ID",
63
+ "i18n.message.form.definition.placeholder": "Definition",
64
+ "i18n.message.form.description.placeholder": "Description",
65
+ "i18n.message.props.messageId": "Message ID",
66
+ "i18n.message.props.definition": "Definition",
67
+ "i18n.message.props.description": "Description",
68
+
69
+ "i18n.messages.header": "Translations",
70
+ "i18n.messages.subheader": "Messages used in Truedat",
71
+ "i18n.messages.empty": "No messages",
72
+ "i18n.messages.locale.es": "Spanish",
73
+ "i18n.messages.locale.en": "English",
74
+ "i18n.messages.search.placeholder": "Search messages",
75
+
59
76
  "navigation.dashboard": "Dashboard",
60
77
  "navigation.menu": "Menu",
61
78
  "search.applied_filters": "Filters:",
@@ -91,6 +108,7 @@ export default {
91
108
  "sidemenu.grant_requests": "Grant Requests",
92
109
  "sidemenu.grants": "Grants",
93
110
  "sidemenu.hide": "Collapse sidebar",
111
+ "sidemenu.i18nMessages": "Translations",
94
112
  "sidemenu.implementations": "Implementations",
95
113
  "sidemenu.implementations_management": "Drafts",
96
114
  "sidemenu.implementations_deprecated": "Deprecated",
@@ -56,6 +56,23 @@ export default {
56
56
  "form.validation.required": "{prop} es un campo requerido",
57
57
  "form.validation.minLength": "{prop} debe tener al menos {value} elementos",
58
58
  "form.validation.email.invalid": "Dirección de email inválida",
59
+
60
+ "i18n.actions.createMessage": "Crear mensaje",
61
+
62
+ "i18n.message.form.messageId.placeholder": "Id del mensaje",
63
+ "i18n.message.form.definition.placeholder": "Definición",
64
+ "i18n.message.form.description.placeholder": "Descripción",
65
+ "i18n.message.props.messageId": "Id del mensaje",
66
+ "i18n.message.props.definition": "Definición",
67
+ "i18n.message.props.description": "Descripción",
68
+
69
+ "i18n.messages.header": "Traducciones",
70
+ "i18n.messages.subheader": "Mensajes utilizados en Truedat",
71
+ "i18n.messages.empty": "No hay mensajes",
72
+ "i18n.messages.locale.es": "Español",
73
+ "i18n.messages.locale.en": "Inglés",
74
+ "i18n.messages.search.placeholder": "Buscar mensajes",
75
+
59
76
  loading: "cargando...",
60
77
  "navigation.dashboard": "Dashboard",
61
78
  "navigation.menu": "Menú",
@@ -94,6 +111,7 @@ export default {
94
111
  "sidemenu.grant_requests": "Peticiones de Accesos",
95
112
  "sidemenu.grants": "Accesos",
96
113
  "sidemenu.hide": "Ocultar",
114
+ "sidemenu.i18nMessages": "Traducciones",
97
115
  "sidemenu.implementations": "Implementaciones",
98
116
  "sidemenu.implementations_management": "Borradores",
99
117
  "sidemenu.implementations_deprecated": "Archivadas",
package/src/routes.js CHANGED
@@ -62,6 +62,9 @@ export const GROUP = "/groups/:id";
62
62
  export const GROUPS = "/groups";
63
63
  export const GROUP_CREATE = "/groups/new";
64
64
  export const GROUP_EDIT = "/groups/:id/edit";
65
+ export const I18N = "/i18n";
66
+ export const I18N_MESSAGES = "/i18n/messages";
67
+ export const I18N_MESSAGES_NEW = "/i18n/messages/new";
65
68
  export const IMPLEMENTATION = "/implementations/:implementation_id(\\d+)";
66
69
  export const IMPLEMENTATIONS = "/implementations";
67
70
  export const IMPLEMENTATIONS_DEPRECATED = "/deprecatedImplementations";
@@ -258,6 +261,9 @@ const routes = {
258
261
  GROUPS,
259
262
  GROUP_CREATE,
260
263
  GROUP_EDIT,
264
+ I18N,
265
+ I18N_MESSAGES,
266
+ I18N_MESSAGES_NEW,
261
267
  IMPLEMENTATION,
262
268
  IMPLEMENTATIONS,
263
269
  IMPLEMENTATIONS_DEPRECATED,