@truedat/ai 6.11.1 → 6.12.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 (33) hide show
  1. package/package.json +3 -3
  2. package/src/api.js +8 -0
  3. package/src/components/AiRoutes.js +26 -0
  4. package/src/components/actions/Action.js +90 -0
  5. package/src/components/actions/ActionActions.js +134 -0
  6. package/src/components/actions/ActionBreadcrumbs.js +22 -0
  7. package/src/components/actions/ActionDetail.js +41 -0
  8. package/src/components/actions/ActionEdit.js +70 -0
  9. package/src/components/actions/ActionForm.js +168 -0
  10. package/src/components/actions/ActionNew.js +50 -0
  11. package/src/components/actions/Actions.js +61 -0
  12. package/src/components/actions/ActionsContext.js +52 -0
  13. package/src/components/actions/ActionsTable.js +124 -0
  14. package/src/components/actions/__tests__/Action.spec.js +59 -0
  15. package/src/components/actions/__tests__/ActionActions.spec.js +47 -0
  16. package/src/components/actions/__tests__/ActionBreadcrumbs.spec.js +21 -0
  17. package/src/components/actions/__tests__/ActionDetail.spec.js +106 -0
  18. package/src/components/actions/__tests__/ActionEdit.spec.js +133 -0
  19. package/src/components/actions/__tests__/ActionForm.spec.js +146 -0
  20. package/src/components/actions/__tests__/ActionNew.spec.js +113 -0
  21. package/src/components/actions/__tests__/Actions.spec.js +21 -0
  22. package/src/components/actions/__tests__/ActionsTable.spec.js +72 -0
  23. package/src/components/actions/__tests__/__snapshots__/Action.spec.js.snap +159 -0
  24. package/src/components/actions/__tests__/__snapshots__/ActionActions.spec.js.snap +57 -0
  25. package/src/components/actions/__tests__/__snapshots__/ActionBreadcrumbs.spec.js.snap +25 -0
  26. package/src/components/actions/__tests__/__snapshots__/ActionDetail.spec.js.snap +46 -0
  27. package/src/components/actions/__tests__/__snapshots__/ActionEdit.spec.js.snap +316 -0
  28. package/src/components/actions/__tests__/__snapshots__/ActionForm.spec.js.snap +326 -0
  29. package/src/components/actions/__tests__/__snapshots__/ActionNew.spec.js.snap +518 -0
  30. package/src/components/actions/__tests__/__snapshots__/Actions.spec.js.snap +63 -0
  31. package/src/components/actions/__tests__/__snapshots__/ActionsTable.spec.js.snap +121 -0
  32. package/src/hooks/useActions.js +63 -0
  33. package/src/styles/aiActionEdit.less +19 -0
@@ -0,0 +1,61 @@
1
+ import React from "react";
2
+ import {
3
+ Header,
4
+ Icon,
5
+ Segment,
6
+ Grid,
7
+ Container,
8
+ Button,
9
+ } from "semantic-ui-react";
10
+ import { FormattedMessage } from "react-intl";
11
+ import { Link } from "react-router-dom";
12
+ import { ACTION_NEW } from "@truedat/core/routes";
13
+ import { useActionsSearch } from "../../hooks/useActions";
14
+ import { ActionsTable } from "./ActionsTable";
15
+ import { ActionsContextProvider } from "./ActionsContext";
16
+
17
+ export function ActionsContent() {
18
+ return (
19
+ <Segment>
20
+ <Header as="h2">
21
+ <Icon name="caret square right" circular />
22
+ <Header.Content>
23
+ <FormattedMessage id="ai.actions.header" />
24
+ <Header.Subheader>
25
+ <FormattedMessage id="ai.actions.subheader" />
26
+ </Header.Subheader>
27
+ </Header.Content>
28
+ </Header>
29
+ <Grid>
30
+ <Grid.Column width={8}>{/* filters */}</Grid.Column>
31
+ <Grid.Column width={8}>
32
+ <Container textAlign="right">
33
+ <Button
34
+ primary
35
+ as={Link}
36
+ to={ACTION_NEW}
37
+ content={<FormattedMessage id="ai.actions.actions.create" />}
38
+ />
39
+ </Container>
40
+ </Grid.Column>
41
+ </Grid>
42
+ <ActionsTable />
43
+ </Segment>
44
+ );
45
+ }
46
+
47
+ export const Actions = () => {
48
+ const searchProps = {
49
+ initialSortColumn: "updated_at",
50
+ initialSortDirection: "descending",
51
+ defaultFilters: { deleted: false },
52
+ useSearch: useActionsSearch,
53
+ };
54
+ return (
55
+ <ActionsContextProvider {...searchProps}>
56
+ <ActionsContent />
57
+ </ActionsContextProvider>
58
+ );
59
+ };
60
+
61
+ export default Actions;
@@ -0,0 +1,52 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState, useEffect, useContext, createContext } from "react";
3
+
4
+ export const ActionsContext = createContext();
5
+ export const useActionsContext = () => useContext(ActionsContext);
6
+
7
+ export const ActionsContextProvider = (props) => {
8
+ const children = _.prop("children")(props);
9
+ const initialDefaultFilters = _.prop("defaultFilters")(props);
10
+ const initialSortColumn = _.prop("initialSortColumn")(props);
11
+ const initialSortDirection = _.prop("initialSortDirection")(props);
12
+ const useSearch = _.prop("useSearch")(props);
13
+ const [defaultFilters, setDefaultFilters] = useState(initialDefaultFilters);
14
+ const [rawData, setRawData] = useState([]);
15
+ const [sortColumn, setSortColumn] = useState(initialSortColumn);
16
+ const [sortDirection, setSortDirection] = useState(initialSortDirection);
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ if (initialDefaultFilters !== defaultFilters) {
21
+ setDefaultFilters(initialDefaultFilters);
22
+ }
23
+ }, [initialDefaultFilters]);
24
+
25
+ const { trigger: triggerSearch } = useSearch();
26
+
27
+ useEffect(() => {
28
+ triggerSearch(defaultFilters).then(({ data }) => {
29
+ setRawData(data?.data);
30
+ setLoading(false);
31
+ });
32
+ }, [defaultFilters]);
33
+
34
+ const searchData = _.flow(
35
+ _.orderBy([sortColumn], [sortDirection == "ascending" ? "asc" : "desc"])
36
+ )(rawData);
37
+
38
+ const context = {
39
+ loading,
40
+ searchData,
41
+ sortColumn,
42
+ sortDirection,
43
+ setSortColumn,
44
+ setSortDirection,
45
+ };
46
+
47
+ return (
48
+ <ActionsContext.Provider value={context}>
49
+ {children}
50
+ </ActionsContext.Provider>
51
+ );
52
+ };
@@ -0,0 +1,124 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { FormattedMessage } from "react-intl";
4
+ import { Link } from "react-router-dom";
5
+ import { Table, Header, Icon } from "semantic-ui-react";
6
+ import { linkTo } from "@truedat/core/routes";
7
+ import { Loading } from "@truedat/core/components";
8
+ import { columnDecorator } from "@truedat/core/services";
9
+ import { sortColumn as sortHandler } from "@truedat/core/services/sort";
10
+ import { DateDecorator } from "@truedat/core/services/columnDecorators";
11
+ import { useActionsContext } from "./ActionsContext";
12
+
13
+ const translateDecorator = (text) => (
14
+ <FormattedMessage id={text} defaultMessage={text} />
15
+ );
16
+
17
+ export const ActionsTable = () => {
18
+ const {
19
+ searchData: actions,
20
+ sortColumn,
21
+ sortDirection,
22
+ setSortColumn,
23
+ setSortDirection,
24
+ loading,
25
+ } = useActionsContext();
26
+
27
+ const columns = [
28
+ {
29
+ name: "name",
30
+ sort: { name: "name" },
31
+ fieldSelector: _.pick(["id", "name"]),
32
+ fieldDecorator: ({ id, name }) => (
33
+ <Link to={linkTo.ACTION({ id })}>{name}</Link>
34
+ ),
35
+ },
36
+ {
37
+ name: "user.full_name",
38
+ sort: { name: "full_name" },
39
+ },
40
+ {
41
+ name: "type",
42
+ sort: { name: "type" },
43
+ fieldDecorator: (field) => field,
44
+ },
45
+ {
46
+ name: "is_enabled",
47
+ sort: { name: "is_enabled" },
48
+ fieldDecorator: (field) =>
49
+ translateDecorator(`ai.actions.is_enabled.${field}`),
50
+ },
51
+ {
52
+ name: "updated_at",
53
+ sort: { name: "updated_at" },
54
+ fieldSelector: ({ updated_at }) => ({
55
+ date: updated_at,
56
+ }),
57
+ width: 2,
58
+ fieldDecorator: DateDecorator,
59
+ textAlign: "center",
60
+ },
61
+ ];
62
+
63
+ return _.isEmpty(actions) ? (
64
+ <Header as="h4">
65
+ <Icon name="search" />
66
+ <Header.Content>
67
+ <FormattedMessage id="ai.actions.search.results.empty" />
68
+ </Header.Content>
69
+ </Header>
70
+ ) : loading ? (
71
+ <Loading />
72
+ ) : (
73
+ <Table sortable>
74
+ <Table.Header>
75
+ <Table.Row>
76
+ {columns.map((column, key) => (
77
+ <Table.HeaderCell
78
+ key={key}
79
+ width={column.width}
80
+ content={
81
+ <FormattedMessage
82
+ id={`ai.actions.form.${column.header || column.name}`}
83
+ defaultMessage={column.name}
84
+ />
85
+ }
86
+ sorted={
87
+ _.path("sort.name")(column) === sortColumn
88
+ ? sortDirection
89
+ : null
90
+ }
91
+ className={_.path("sort.name")(column) ? "" : "disabled"}
92
+ onClick={() =>
93
+ sortHandler(
94
+ column,
95
+ () => {},
96
+ setSortDirection,
97
+ setSortColumn,
98
+ sortDirection,
99
+ sortColumn
100
+ )
101
+ }
102
+ />
103
+ ))}
104
+ </Table.Row>
105
+ </Table.Header>
106
+ <Table.Body>
107
+ {actions.map((t, i) => (
108
+ <Table.Row key={i}>
109
+ {columns.map((column, i) => (
110
+ <Table.Cell
111
+ key={i}
112
+ warning={!t.is_enabled}
113
+ textAlign={column.textAlign}
114
+ content={columnDecorator(column)(t)}
115
+ />
116
+ ))}
117
+ </Table.Row>
118
+ ))}
119
+ </Table.Body>
120
+ </Table>
121
+ );
122
+ };
123
+
124
+ export default ActionsTable;
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import Action from "../Action";
4
+
5
+ const action = {
6
+ id: 3,
7
+ name: "foo",
8
+ type: "baz",
9
+ user: {
10
+ id: 123,
11
+ full_name: "agent",
12
+ },
13
+ updated_at: "2024-09-04T14:35:16.854749Z",
14
+ user_id: 123,
15
+ deleted_at: null,
16
+ is_enabled: true,
17
+ dynamic_content: {},
18
+ };
19
+ jest.mock("@truedat/ai/hooks/useActions", () => {
20
+ const originalModule = jest.requireActual("@truedat/ai/hooks/useActions");
21
+
22
+ return {
23
+ __esModule: true,
24
+ ...originalModule,
25
+ useAction: () => ({
26
+ action,
27
+ loading: false,
28
+ mutate: jest.fn(),
29
+ }),
30
+ };
31
+ });
32
+
33
+ const renderOpts = {
34
+ messages: {
35
+ en: {
36
+ "ai.actions.actions.delete.confirmation.content":
37
+ "Action will be deleted. Are you sure?",
38
+ "ai.actions.actions.delete.confirmation.header": "Delete action",
39
+ "ai.actions.actions.delete": "Delete",
40
+ "ai.actions.actions.disable.confirmation.content":
41
+ " Action will be disabled. Are you sure?",
42
+ "ai.actions.actions.disable.confirmation.header": "Disable action",
43
+ "ai.actions.actions.disable": "Disable",
44
+ "ai.actions.actions.edit": "Edit",
45
+ "ai.actions.actions.form.user": "Agent",
46
+ "ai.actions.is_enabled.true": "Active",
47
+ "navigation.admin.actions": "Actions",
48
+ "ai.actions.form.type": "Type",
49
+ },
50
+ },
51
+ fallback: "lazy",
52
+ };
53
+
54
+ describe("<Action />", () => {
55
+ it("matches the latest snapshot", () => {
56
+ const { container } = render(<Action />, renderOpts);
57
+ expect(container).toMatchSnapshot();
58
+ });
59
+ });
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import ActionActions from "../ActionActions";
4
+
5
+ const action = {
6
+ id: 3,
7
+ name: "foo",
8
+ type: "baz",
9
+ user: {
10
+ id: 123,
11
+ full_name: "agent",
12
+ },
13
+ updated_at: "2024-09-04T14:35:16.854749Z",
14
+ user_id: 123,
15
+ deleted_at: null,
16
+ is_enabled: true,
17
+ dynamic_content: {},
18
+ };
19
+
20
+ const renderOpts = {
21
+ messages: {
22
+ en: {
23
+ "ai.actions.actions.delete.confirmation.content":
24
+ "Action will be deleted. Are you sure?",
25
+ "ai.actions.actions.delete.confirmation.header": "Delete action",
26
+ "ai.actions.actions.delete": "Delete",
27
+ "ai.actions.actions.disable.confirmation.content":
28
+ " Action will be disabled. Are you sure?",
29
+ "ai.actions.actions.disable.confirmation.header": "Disable action",
30
+ "ai.actions.actions.disable": "Disable",
31
+ "ai.actions.actions.edit": "Edit",
32
+ "navigation.admin.actions": "Actions",
33
+ },
34
+ },
35
+ };
36
+
37
+ describe("<ActionActions />", () => {
38
+ const props = {
39
+ action,
40
+ mutate: jest.fn(),
41
+ };
42
+
43
+ it("matches the latest snapshot", () => {
44
+ const { container } = render(<ActionActions {...props} />, renderOpts);
45
+ expect(container).toMatchSnapshot();
46
+ });
47
+ });
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import ActionBreadcrumbs from "../ActionBreadcrumbs";
4
+
5
+ const renderOpts = {
6
+ messages: {
7
+ en: {
8
+ "navigation.admin.actions": "Actions",
9
+ },
10
+ },
11
+ };
12
+
13
+ describe("<ActionBreadcrumbs />", () => {
14
+ const text = "edit";
15
+ const props = { text };
16
+
17
+ it("matches the latest snapshot", () => {
18
+ const { container } = render(<ActionBreadcrumbs {...props} />, renderOpts);
19
+ expect(container).toMatchSnapshot();
20
+ });
21
+ });
@@ -0,0 +1,106 @@
1
+ import React, { Suspense } from "react";
2
+ import { waitFor } from "@testing-library/react";
3
+ import { render } from "@truedat/test/render";
4
+ import { TEMPLATES_QUERY } from "@truedat/core/api/queries";
5
+ import ActionDetail from "../ActionDetail";
6
+
7
+ const template = {
8
+ __typename: "Template",
9
+ content: [
10
+ {
11
+ fields: [
12
+ {
13
+ cardinality: "1",
14
+ default: {
15
+ origin: "default",
16
+ value: "",
17
+ },
18
+ label: "Field Label",
19
+ name: "field_name",
20
+ type: "string",
21
+ values: {
22
+ fixed_tuple: [
23
+ {
24
+ text: "Value One",
25
+ value: "value_one",
26
+ },
27
+ ],
28
+ },
29
+ widget: "dropdown",
30
+ },
31
+ ],
32
+ name: "",
33
+ },
34
+ ],
35
+ id: "1",
36
+ label: "template_label",
37
+ name: "Template Name",
38
+ scope: "actions",
39
+ };
40
+
41
+ jest.mock("@truedat/core/hooks", () => ({
42
+ useTemplate: jest.fn(() => ({
43
+ data: { template },
44
+ })),
45
+ }));
46
+
47
+ const templatesMock = {
48
+ request: {
49
+ query: TEMPLATES_QUERY,
50
+ variables: { scope: "actions", domainIds: null },
51
+ },
52
+ result: { data: { templates: [template] } },
53
+ };
54
+
55
+ const action = {
56
+ id: 3,
57
+ name: "foo",
58
+ type: "baz",
59
+ user: {
60
+ id: 123,
61
+ full_name: "agent",
62
+ },
63
+ updated_at: "2024-09-04T14:35:16.854749Z",
64
+ user_id: 123,
65
+ deleted_at: null,
66
+ is_enabled: true,
67
+ dynamic_content: {
68
+ field_name: {
69
+ origin: "user",
70
+ value: "value_one",
71
+ },
72
+ },
73
+ };
74
+
75
+ const renderOpts = {
76
+ messages: {
77
+ en: {
78
+ "ai.actions.search.results.empty": "Empty",
79
+ "ai.actions.actions.create": "Create",
80
+ "ai.actions.subheader": "AI actions",
81
+ "ai.actions.header": "AI actions",
82
+ "ai.actions.actions.form.user": "Agent",
83
+ "ai.actions.form.type": "Type",
84
+ },
85
+ },
86
+ mocks: [templatesMock],
87
+ };
88
+
89
+ describe("<ActionDetail />", () => {
90
+ const props = {
91
+ action,
92
+ };
93
+
94
+ it("matches the latest snapshot", async () => {
95
+ const { queryByText, container } = render(
96
+ <Suspense fallback={null}>
97
+ <ActionDetail {...props} />
98
+ </Suspense>,
99
+ renderOpts
100
+ );
101
+ await waitFor(() =>
102
+ expect(queryByText(/Value One/i)).not.toBeInTheDocument()
103
+ );
104
+ expect(container).toMatchSnapshot();
105
+ });
106
+ });
@@ -0,0 +1,133 @@
1
+ import React from "react";
2
+ import { waitFor } from "@testing-library/react";
3
+ import { render } from "@truedat/test/render";
4
+ import { TEMPLATES_QUERY } from "@truedat/core/api/queries";
5
+ import ActionEdit from "../ActionEdit";
6
+
7
+ jest.mock("@truedat/auth/hooks/useUsers", () => ({
8
+ useAgents: jest.fn(() => ({
9
+ agents: [
10
+ { id: 1, full_name: "Agent 1" },
11
+ { id: 2, full_name: "Agent 2" },
12
+ ],
13
+ })),
14
+ }));
15
+
16
+ jest.mock("react-router-dom", () => ({
17
+ ...jest.requireActual("react-router-dom"),
18
+ useParams: () => ({ id: "123" }),
19
+ }));
20
+
21
+ const template = {
22
+ __typename: "Template",
23
+ content: [
24
+ {
25
+ fields: [
26
+ {
27
+ cardinality: "1",
28
+ default: {
29
+ origin: "default",
30
+ value: "",
31
+ },
32
+ label: "Field Label",
33
+ name: "field_name",
34
+ type: "string",
35
+ values: {
36
+ fixed_tuple: [
37
+ {
38
+ text: "Value One",
39
+ value: "value_one",
40
+ },
41
+ ],
42
+ },
43
+ widget: "dropdown",
44
+ },
45
+ ],
46
+ name: "",
47
+ },
48
+ ],
49
+ id: "1",
50
+ label: "template_label",
51
+ name: "Template Name",
52
+ scope: "actions",
53
+ };
54
+
55
+ jest.mock("@truedat/core/hooks", () => ({
56
+ useTemplate: jest.fn(() => ({
57
+ data: { template },
58
+ })),
59
+ }));
60
+
61
+ const templatesMock = {
62
+ request: {
63
+ query: TEMPLATES_QUERY,
64
+ variables: { scope: "actions", domainIds: null },
65
+ },
66
+ result: { data: { templates: [template] } },
67
+ };
68
+
69
+ const action = {
70
+ deleted_at: null,
71
+ dynamic_content: {
72
+ field_name: {
73
+ origin: "user",
74
+ value: "value_one",
75
+ },
76
+ },
77
+ id: 123,
78
+ is_enabled: true,
79
+ name: "Action Name",
80
+ type: "Template Name",
81
+ updated_at: "2024-10-01T00:00:00.000000Z",
82
+ user: {
83
+ full_name: "Agent 1",
84
+ id: 1,
85
+ },
86
+ user_id: 1,
87
+ };
88
+
89
+ jest.mock("@truedat/ai/hooks/useActions", () => ({
90
+ useAction: jest.fn(() => ({
91
+ action,
92
+ })),
93
+ useActionUpdate: jest.fn(() => ({
94
+ trigger: jest.fn(() => new Promise(() => {})),
95
+ })),
96
+ }));
97
+
98
+ const renderOpts = {
99
+ messages: {
100
+ en: {
101
+ "actions.cancel": "Cancel",
102
+ "actions.update": "Update",
103
+ "ai.actions.actions.edit": "Edit Action",
104
+ "ai.actions.actions.form.active": "Active",
105
+ "ai.actions.actions.form.name": "Name",
106
+ "ai.actions.actions.form.user": "Agent",
107
+ "fields.dropdown.placeholder": "Select One...",
108
+ loading: "Loading",
109
+ "navigation.admin.actions": "Actions",
110
+ "selector.no.selection": "No selection",
111
+ "template.form.validation.empty_required": "Can't be empty",
112
+ "template.selector.label": "Template",
113
+ },
114
+ },
115
+ mocks: [templatesMock],
116
+ fallback: "lazy",
117
+ };
118
+
119
+ describe("<ActionEdit />", () => {
120
+ const setActionResource = jest.fn();
121
+ const props = {
122
+ setActionResource,
123
+ };
124
+ it("matches the latest snapshot", async () => {
125
+ const { container, queryByText } = render(
126
+ <ActionEdit {...props} />,
127
+ renderOpts
128
+ );
129
+
130
+ await waitFor(() => expect(queryByText(/lazy/i)).not.toBeInTheDocument());
131
+ expect(container).toMatchSnapshot();
132
+ });
133
+ });