@pautena/react-design-system 0.4.7 → 0.5.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 (146) hide show
  1. package/dist/cjs/index.js +4 -4
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/alerts/expandable-alert/expandable-alert.d.ts +11 -0
  4. package/dist/cjs/types/components/alerts/expandable-alert/index.d.ts +1 -0
  5. package/dist/cjs/types/components/alerts/index.d.ts +1 -0
  6. package/dist/cjs/types/components/data-display/board/board.d.ts +10 -0
  7. package/dist/cjs/types/components/data-display/board/index.d.ts +1 -0
  8. package/dist/cjs/types/components/data-display/index.d.ts +2 -0
  9. package/dist/cjs/types/components/data-display/markdown/index.d.ts +1 -0
  10. package/dist/cjs/types/components/data-display/markdown/markdown.d.ts +7 -0
  11. package/dist/cjs/types/components/dialogs/bootstrap-dialog/bootstrap-dialog.d.ts +2 -0
  12. package/dist/cjs/types/components/dialogs/bootstrap-dialog/index.d.ts +1 -0
  13. package/dist/cjs/types/components/dialogs/confirm-dialog/confirm-dialog.d.ts +10 -0
  14. package/dist/cjs/types/components/dialogs/confirm-dialog/index.d.ts +1 -0
  15. package/dist/cjs/types/components/dialogs/dialog-hooks/index.d.ts +1 -0
  16. package/dist/cjs/types/components/dialogs/dialog-hooks/use-dialog.d.ts +7 -0
  17. package/dist/cjs/types/components/dialogs/dialog.types.d.ts +26 -0
  18. package/dist/cjs/types/components/dialogs/form-dialog/form-dialog.d.ts +10 -0
  19. package/dist/cjs/types/components/dialogs/form-dialog/index.d.ts +1 -0
  20. package/dist/cjs/types/components/dialogs/index.d.ts +5 -0
  21. package/dist/cjs/types/components/index.d.ts +2 -0
  22. package/dist/cjs/types/components/inputs/enhanced-autocomplete/enhanced-autocomplete.d.ts +10 -0
  23. package/dist/cjs/types/components/inputs/enhanced-autocomplete/index.d.ts +1 -0
  24. package/dist/cjs/types/components/inputs/enhanced-text-field/enhanced-text-field.d.ts +7 -0
  25. package/dist/cjs/types/components/inputs/enhanced-text-field/index.d.ts +1 -0
  26. package/dist/cjs/types/components/inputs/index.d.ts +1 -0
  27. package/dist/cjs/types/components/value-displays/index.d.ts +2 -1
  28. package/dist/cjs/types/components/value-displays/value-base/index.d.ts +2 -0
  29. package/dist/cjs/types/components/value-displays/{value-displays.types.d.ts → value-base/value-displays.types.d.ts} +10 -0
  30. package/dist/cjs/types/components/value-displays/value-base/value-edit.d.ts +19 -0
  31. package/dist/cjs/types/components/value-displays/value-boolean/value-boolean.d.ts +3 -3
  32. package/dist/cjs/types/components/value-displays/value-datetime/value-datetime.d.ts +8 -3
  33. package/dist/cjs/types/components/value-displays/value-image/value-image.d.ts +1 -1
  34. package/dist/cjs/types/components/value-displays/value-rating/value-rating.d.ts +3 -3
  35. package/dist/cjs/types/components/value-displays/value-text/value-text.d.ts +3 -17
  36. package/dist/esm/index.js +4 -4
  37. package/dist/esm/index.js.map +1 -1
  38. package/dist/esm/types/components/alerts/expandable-alert/expandable-alert.d.ts +11 -0
  39. package/dist/esm/types/components/alerts/expandable-alert/index.d.ts +1 -0
  40. package/dist/esm/types/components/alerts/index.d.ts +1 -0
  41. package/dist/esm/types/components/data-display/board/board.d.ts +10 -0
  42. package/dist/esm/types/components/data-display/board/index.d.ts +1 -0
  43. package/dist/esm/types/components/data-display/index.d.ts +2 -0
  44. package/dist/esm/types/components/data-display/markdown/index.d.ts +1 -0
  45. package/dist/esm/types/components/data-display/markdown/markdown.d.ts +7 -0
  46. package/dist/esm/types/components/dialogs/bootstrap-dialog/bootstrap-dialog.d.ts +2 -0
  47. package/dist/esm/types/components/dialogs/bootstrap-dialog/index.d.ts +1 -0
  48. package/dist/esm/types/components/dialogs/confirm-dialog/confirm-dialog.d.ts +10 -0
  49. package/dist/esm/types/components/dialogs/confirm-dialog/index.d.ts +1 -0
  50. package/dist/esm/types/components/dialogs/dialog-hooks/index.d.ts +1 -0
  51. package/dist/esm/types/components/dialogs/dialog-hooks/use-dialog.d.ts +7 -0
  52. package/dist/esm/types/components/dialogs/dialog.types.d.ts +26 -0
  53. package/dist/esm/types/components/dialogs/form-dialog/form-dialog.d.ts +10 -0
  54. package/dist/esm/types/components/dialogs/form-dialog/index.d.ts +1 -0
  55. package/dist/esm/types/components/dialogs/index.d.ts +5 -0
  56. package/dist/esm/types/components/index.d.ts +2 -0
  57. package/dist/esm/types/components/inputs/enhanced-autocomplete/enhanced-autocomplete.d.ts +10 -0
  58. package/dist/esm/types/components/inputs/enhanced-autocomplete/index.d.ts +1 -0
  59. package/dist/esm/types/components/inputs/enhanced-text-field/enhanced-text-field.d.ts +7 -0
  60. package/dist/esm/types/components/inputs/enhanced-text-field/index.d.ts +1 -0
  61. package/dist/esm/types/components/inputs/index.d.ts +1 -0
  62. package/dist/esm/types/components/value-displays/index.d.ts +2 -1
  63. package/dist/esm/types/components/value-displays/value-base/index.d.ts +2 -0
  64. package/dist/esm/types/components/value-displays/{value-displays.types.d.ts → value-base/value-displays.types.d.ts} +10 -0
  65. package/dist/esm/types/components/value-displays/value-base/value-edit.d.ts +19 -0
  66. package/dist/esm/types/components/value-displays/value-boolean/value-boolean.d.ts +3 -3
  67. package/dist/esm/types/components/value-displays/value-datetime/value-datetime.d.ts +8 -3
  68. package/dist/esm/types/components/value-displays/value-image/value-image.d.ts +1 -1
  69. package/dist/esm/types/components/value-displays/value-rating/value-rating.d.ts +3 -3
  70. package/dist/esm/types/components/value-displays/value-text/value-text.d.ts +3 -17
  71. package/dist/index.d.ts +130 -22
  72. package/package.json +34 -32
  73. package/src/components/alerts/alerts.stories.mdx +10 -0
  74. package/src/components/alerts/expandable-alert/expandable-alert.stories.tsx +48 -0
  75. package/src/components/alerts/expandable-alert/expandable-alert.test.tsx +114 -0
  76. package/src/components/alerts/expandable-alert/expandable-alert.tsx +71 -0
  77. package/src/components/alerts/expandable-alert/index.ts +1 -0
  78. package/src/components/alerts/index.ts +1 -0
  79. package/src/components/components.stories.mdx +2 -0
  80. package/src/components/data-display/board/board.stories.tsx +54 -0
  81. package/src/components/data-display/board/board.test.tsx +100 -0
  82. package/src/components/data-display/board/board.tsx +63 -0
  83. package/src/components/data-display/board/index.ts +1 -0
  84. package/src/components/data-display/data-display.stories.mdx +2 -0
  85. package/src/components/data-display/index.ts +2 -0
  86. package/src/components/data-display/markdown/index.ts +1 -0
  87. package/src/components/data-display/markdown/markdown.stories.tsx +25 -0
  88. package/src/components/data-display/markdown/markdown.test.tsx +64 -0
  89. package/src/components/data-display/markdown/markdown.tsx +38 -0
  90. package/src/components/dialogs/bootstrap-dialog/bootstrap-dialog.stories.tsx +81 -0
  91. package/src/components/dialogs/bootstrap-dialog/bootstrap-dialog.test.tsx +233 -0
  92. package/src/components/dialogs/bootstrap-dialog/bootstrap-dialog.tsx +95 -0
  93. package/src/components/dialogs/bootstrap-dialog/index.ts +1 -0
  94. package/src/components/dialogs/confirm-dialog/confirm-dialog.stories.tsx +43 -0
  95. package/src/components/dialogs/confirm-dialog/confirm-dialog.test.tsx +150 -0
  96. package/src/components/dialogs/confirm-dialog/confirm-dialog.tsx +51 -0
  97. package/src/components/dialogs/confirm-dialog/index.ts +1 -0
  98. package/src/components/dialogs/dialog-hooks/index.ts +1 -0
  99. package/src/components/dialogs/dialog-hooks/use-dialog.ts +10 -0
  100. package/src/components/dialogs/dialog.types.ts +27 -0
  101. package/src/components/dialogs/dialogs.stories.mdx +12 -0
  102. package/src/components/dialogs/form-dialog/form-dialog.stories.tsx +52 -0
  103. package/src/components/dialogs/form-dialog/form-dialog.test.tsx +164 -0
  104. package/src/components/dialogs/form-dialog/form-dialog.tsx +69 -0
  105. package/src/components/dialogs/form-dialog/index.ts +1 -0
  106. package/src/components/dialogs/index.ts +5 -0
  107. package/src/components/drawers/drawer-collapsable-item/drawer-collapsable-item.tsx +1 -3
  108. package/src/components/index.ts +2 -0
  109. package/src/components/inputs/enhanced-autocomplete/enhanced-autocomplete.stories.tsx +109 -0
  110. package/src/components/inputs/enhanced-autocomplete/enhanced-autocomplete.test.tsx +74 -0
  111. package/src/components/inputs/enhanced-autocomplete/enhanced-autocomplete.tsx +64 -0
  112. package/src/components/inputs/enhanced-autocomplete/index.ts +1 -0
  113. package/src/components/inputs/enhanced-text-field/enhanced-text-field.stories.tsx +120 -0
  114. package/src/components/inputs/enhanced-text-field/enhanced-text-field.test.tsx +63 -0
  115. package/src/components/inputs/enhanced-text-field/enhanced-text-field.tsx +101 -0
  116. package/src/components/inputs/enhanced-text-field/index.ts +1 -0
  117. package/src/components/inputs/index.ts +1 -0
  118. package/src/components/inputs/inputs.stories.mdx +2 -0
  119. package/src/components/value-displays/index.ts +2 -1
  120. package/src/components/value-displays/value-base/index.ts +2 -0
  121. package/src/components/value-displays/{value-displays.types.ts → value-base/value-displays.types.ts} +12 -2
  122. package/src/components/value-displays/value-base/value-edit.tsx +59 -0
  123. package/src/components/value-displays/value-boolean/value-boolean.stories.tsx +7 -0
  124. package/src/components/value-displays/value-boolean/value-boolean.test.tsx +81 -2
  125. package/src/components/value-displays/value-boolean/value-boolean.tsx +33 -8
  126. package/src/components/value-displays/value-datetime/value-datetime.stories.tsx +29 -2
  127. package/src/components/value-displays/value-datetime/value-datetime.test.tsx +103 -5
  128. package/src/components/value-displays/value-datetime/value-datetime.tsx +67 -6
  129. package/src/components/value-displays/value-image/value-image.tsx +1 -1
  130. package/src/components/value-displays/value-rating/value-rating.stories.tsx +8 -0
  131. package/src/components/value-displays/value-rating/value-rating.test.tsx +71 -2
  132. package/src/components/value-displays/value-rating/value-rating.tsx +29 -5
  133. package/src/components/value-displays/value-text/value-text.test.tsx +5 -8
  134. package/src/components/value-displays/value-text/value-text.tsx +16 -60
  135. package/src/generators/generators.mock.ts +3 -14
  136. package/src/generators/model-form/model-form.tsx +3 -6
  137. package/src/storybook.tsx +34 -2
  138. package/src/tests/actions.ts +3 -2
  139. package/src/tests/assertions.ts +17 -5
  140. package/src/tests/datatable-placeholder/datatable-placeholder.tsx +2 -4
  141. package/src/tests/mocks/markdown.mock.ts +25 -0
  142. package/src/types/index.d.ts +6 -0
  143. package/dist/cjs/types/generators/generators.model.test.d.ts +0 -1
  144. package/dist/cjs/types/utils/arrays.test.d.ts +0 -1
  145. package/dist/esm/types/generators/generators.model.test.d.ts +0 -1
  146. package/dist/esm/types/utils/arrays.test.d.ts +0 -1
@@ -1,11 +1,18 @@
1
- import { Typography, useTheme } from "@mui/material";
1
+ import { Box, IconButton, Switch, Typography, useTheme } from "@mui/material";
2
2
  import React from "react";
3
3
  import CheckIcon from "@mui/icons-material/Check";
4
4
  import CloseIcon from "@mui/icons-material/Close";
5
- import { BaseValueProps, DefaultPlaceholder } from "../value-displays.types";
5
+ import {
6
+ BaseValueProps,
7
+ DefaultPlaceholder,
8
+ EditableValueProps,
9
+ useEditableValueDisplay,
10
+ ValueEditButtons,
11
+ } from "../value-base";
6
12
  import { ValueContent } from "../value-content";
13
+ import EditIcon from "@mui/icons-material/Edit";
7
14
 
8
- export type ValueBooleanProps = BaseValueProps<boolean>;
15
+ export type ValueBooleanProps = BaseValueProps<boolean> & EditableValueProps<boolean>;
9
16
 
10
17
  /**
11
18
  * Displays a boolean value with a label
@@ -14,19 +21,37 @@ export const ValueBoolean = ({
14
21
  label,
15
22
  value,
16
23
  placeholder = DefaultPlaceholder,
24
+ editable,
25
+ onEdit = () => null,
17
26
  }: ValueBooleanProps) => {
18
27
  const { typography } = useTheme();
28
+ const { isEditing, editValue, startEdit, cancelEdit, setEditValue, submitEdit } =
29
+ useEditableValueDisplay(value, onEdit);
19
30
 
20
31
  const iconSx = { fontSize: typography.h5.fontSize };
21
32
 
22
33
  return (
23
34
  <ValueContent label={label}>
24
- {value === undefined ? (
25
- <Typography variant="h5">{placeholder}</Typography>
26
- ) : value ? (
27
- <CheckIcon color="success" sx={iconSx} />
35
+ {isEditing ? (
36
+ <Box display="flex" alignItems="center">
37
+ <Switch checked={editValue} onChange={(e) => setEditValue(e.target.checked)} />
38
+ <ValueEditButtons onClickCancel={cancelEdit} onSubmitEdit={submitEdit} />
39
+ </Box>
28
40
  ) : (
29
- <CloseIcon color="error" sx={iconSx} />
41
+ <Box display="flex" alignItems="center">
42
+ {value === undefined ? (
43
+ <Typography variant="h5">{placeholder}</Typography>
44
+ ) : value ? (
45
+ <CheckIcon color="success" sx={iconSx} />
46
+ ) : (
47
+ <CloseIcon color="error" sx={iconSx} />
48
+ )}
49
+ {editable && (
50
+ <IconButton size="small" onClick={startEdit} sx={{ ml: 1 }}>
51
+ <EditIcon />
52
+ </IconButton>
53
+ )}
54
+ </Box>
30
55
  )}
31
56
  </ValueContent>
32
57
  );
@@ -1,11 +1,11 @@
1
1
  import { ComponentMeta } from "@storybook/react";
2
2
  import { ValueDatetime } from "./value-datetime";
3
- import { createTemplate, withContainer } from "../../../storybook";
3
+ import { createTemplate, withContainer, withLocalizationProvider } from "../../../storybook";
4
4
 
5
5
  export default {
6
6
  title: "Components/Value displays/ValueDatetime",
7
7
  component: ValueDatetime,
8
- decorators: [withContainer({ width: 200 })],
8
+ decorators: [withContainer({ width: 600 }), withLocalizationProvider],
9
9
  parameters: {
10
10
  layout: "centered",
11
11
  },
@@ -32,3 +32,30 @@ CustomPlaceholder.args = {
32
32
  format: "yyyy/MM/dd",
33
33
  placeholder: ".",
34
34
  };
35
+
36
+ export const EditableDatetime = Template.bind({});
37
+ EditableDatetime.args = {
38
+ label: "Lorem",
39
+ value: new Date(2022, 8, 22, 12, 25),
40
+ format: "yyyy/MM/dd HH:mm",
41
+ editable: true,
42
+ editInputType: "datetime",
43
+ };
44
+
45
+ export const EditableTime = Template.bind({});
46
+ EditableTime.args = {
47
+ label: "Lorem",
48
+ value: new Date(2022, 8, 22, 12, 25),
49
+ format: "HH:mm",
50
+ editable: true,
51
+ editInputType: "time",
52
+ };
53
+
54
+ export const EditableDate = Template.bind({});
55
+ EditableDate.args = {
56
+ label: "Lorem",
57
+ value: new Date(2022, 8, 22),
58
+ format: "yyyy/MM/dd",
59
+ editable: true,
60
+ editInputType: "date",
61
+ };
@@ -1,19 +1,45 @@
1
1
  import React from "react";
2
2
  import { render, screen } from "~/tests/testing-library";
3
- import { ValueDatetime } from "./value-datetime";
3
+ import { EditInputType, ValueDatetime } from "./value-datetime";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { pickDatetime } from "~/tests/actions";
6
+ import { assertDatetimeInputValue } from "~/tests/assertions";
4
7
 
5
- const DummyValue = new Date(2022, 7, 10);
8
+ const DummyValue = new Date(2022, 7, 10, 0, 0);
9
+ const NewValue = new Date(2021, 8, 9, 11, 21);
10
+ const NewDateValue = new Date(2018, 2, 17);
11
+ const NewTimeValue = new Date(2021, 8, 9, 11, 21);
12
+ const datetimeFormat = "dd-MM-yyyy HH:mm";
13
+ const dateFormat = "dd-MM-yyyy";
14
+ const timeFormat = "HH:mm";
6
15
 
7
16
  describe("ValueDatetime", () => {
8
- const renderComponent = ({ value, placeholder }: { value?: Date; placeholder?: string }) => {
9
- return render(
17
+ const renderComponent = ({
18
+ value,
19
+ placeholder,
20
+ editable,
21
+ fmt = datetimeFormat,
22
+ editInputType = "datetime",
23
+ }: {
24
+ value?: Date;
25
+ placeholder?: string;
26
+ editable?: boolean;
27
+ fmt?: string;
28
+ editInputType?: EditInputType;
29
+ }) => {
30
+ const onEdit = jest.fn();
31
+ render(
10
32
  <ValueDatetime
11
33
  label="Hello world"
12
34
  value={value}
13
35
  placeholder={placeholder}
14
- format="dd-MM-yyyy"
36
+ format={fmt}
37
+ editable={editable}
38
+ editInputType={editInputType}
39
+ onEdit={onEdit}
15
40
  />,
16
41
  );
42
+ return { onEdit };
17
43
  };
18
44
 
19
45
  it("would render the label", () => {
@@ -39,4 +65,76 @@ describe("ValueDatetime", () => {
39
65
 
40
66
  expect(screen.getByText(/_/i)).toBeInTheDocument();
41
67
  });
68
+
69
+ describe("editable", () => {
70
+ it("shouldn't render an option to edit if editable is false", () => {
71
+ renderComponent({ value: DummyValue, editable: false });
72
+
73
+ expect(screen.queryByTestId("EditIcon")).not.toBeInTheDocument();
74
+ });
75
+
76
+ it("should render an option to edit if editable is true", () => {
77
+ renderComponent({ value: DummyValue, editable: true });
78
+
79
+ expect(screen.getByTestId("EditIcon")).toBeVisible();
80
+ });
81
+
82
+ it("should render an input with the value if the edit button is clicked", async () => {
83
+ renderComponent({ value: DummyValue, editable: true });
84
+
85
+ await userEvent.click(screen.getByTestId("EditIcon"));
86
+
87
+ assertDatetimeInputValue(screen.getByRole("textbox"), {
88
+ value: DummyValue,
89
+ fmt: datetimeFormat,
90
+ });
91
+ });
92
+
93
+ it.each([
94
+ ["datetime" as EditInputType, NewValue, new Date(2021, 8, 9, 11, 21), datetimeFormat],
95
+ ["date" as EditInputType, NewDateValue, new Date(2018, 2, 17), dateFormat],
96
+ ["time" as EditInputType, NewTimeValue, new Date(2022, 7, 10, 11, 21), timeFormat],
97
+ ])(
98
+ "should submit the new value if is edited with type %s",
99
+ async (editInputType: EditInputType, newValue: Date, expectedDate: Date, fmt: string) => {
100
+ const { onEdit } = renderComponent({
101
+ value: DummyValue,
102
+ editable: true,
103
+ editInputType,
104
+ fmt,
105
+ });
106
+
107
+ await userEvent.click(screen.getByTestId("EditIcon"));
108
+ pickDatetime(screen.getByRole("textbox"), newValue, fmt);
109
+ await userEvent.click(screen.getByTestId("CheckIcon"));
110
+
111
+ expect(onEdit).toHaveBeenCalledTimes(1);
112
+ expect(onEdit).toHaveBeenCalledWith(expectedDate);
113
+ },
114
+ );
115
+
116
+ it("should not call onEdit if the edition is cancelled", async () => {
117
+ const { onEdit } = renderComponent({ value: DummyValue, editable: true });
118
+
119
+ await userEvent.click(screen.getByTestId("EditIcon"));
120
+ pickDatetime(screen.getByRole("textbox"), NewValue, datetimeFormat);
121
+ await userEvent.click(screen.getByTestId("ClearIcon"));
122
+
123
+ expect(onEdit).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("should have the original value if is edited again after clear a change", async () => {
127
+ renderComponent({ value: DummyValue, editable: true });
128
+
129
+ await userEvent.click(screen.getByTestId("EditIcon"));
130
+ pickDatetime(screen.getByRole("textbox"), NewValue, datetimeFormat);
131
+ await userEvent.click(screen.getByTestId("ClearIcon"));
132
+ await userEvent.click(screen.getByTestId("EditIcon"));
133
+
134
+ assertDatetimeInputValue(screen.getByRole("textbox"), {
135
+ value: DummyValue,
136
+ fmt: datetimeFormat,
137
+ });
138
+ });
139
+ });
42
140
  });
@@ -1,14 +1,28 @@
1
- import { Typography } from "@mui/material";
1
+ import { Box, IconButton, TextField, Typography } from "@mui/material";
2
2
  import React from "react";
3
3
  import { format } from "date-fns";
4
- import { BaseValueProps, DefaultPlaceholder } from "../value-displays.types";
4
+ import {
5
+ BaseValueProps,
6
+ DefaultPlaceholder,
7
+ EditableValueProps,
8
+ } from "../value-base/value-displays.types";
5
9
  import { getValueContentLabelId, ValueContent } from "../value-content";
10
+ import { useEditableValueDisplay, ValueEditButtons } from "../value-base";
11
+ import EditIcon from "@mui/icons-material/Edit";
12
+ import { DatePicker, DateTimePicker, TimePicker } from "@mui/x-date-pickers";
6
13
 
7
- export interface ValueDatetimeProps extends BaseValueProps<Date> {
14
+ export type EditInputType = "datetime" | "date" | "time";
15
+
16
+ export interface ValueDatetimeProps extends BaseValueProps<Date>, EditableValueProps<Date> {
8
17
  /**
9
18
  * Datetime format
10
19
  */
11
20
  format: string;
21
+
22
+ /**
23
+ * Edit input type
24
+ */
25
+ editInputType?: EditInputType;
12
26
  }
13
27
 
14
28
  /**
@@ -19,15 +33,62 @@ export const ValueDatetime = ({
19
33
  value: valueProp,
20
34
  format: fmt,
21
35
  placeholder = DefaultPlaceholder,
36
+ editable,
37
+ editInputType = "datetime",
38
+ onEdit = () => null,
22
39
  }: ValueDatetimeProps) => {
40
+ const { isEditing, editValue, startEdit, cancelEdit, setEditValue, submitEdit } =
41
+ useEditableValueDisplay(valueProp, onEdit);
23
42
  const id = getValueContentLabelId(label);
24
43
  const value = (valueProp && format(valueProp, fmt)) || placeholder;
25
44
 
45
+ const EditPickerComponent =
46
+ editInputType === "datetime"
47
+ ? DateTimePicker
48
+ : editInputType === "time"
49
+ ? TimePicker
50
+ : DatePicker;
51
+
26
52
  return (
27
53
  <ValueContent label={label} tooltip={value}>
28
- <Typography variant="h5" noWrap aria-labelledby={id}>
29
- {value}
30
- </Typography>
54
+ {isEditing ? (
55
+ <EditPickerComponent
56
+ value={editValue}
57
+ format={fmt}
58
+ onChange={(newValue) => setEditValue(newValue ? newValue : undefined)}
59
+ slots={{
60
+ textField: (params) => (
61
+ <TextField
62
+ {...params}
63
+ InputProps={{
64
+ ...params.InputProps,
65
+ endAdornment: (
66
+ <>
67
+ {params.InputProps?.endAdornment}
68
+ <ValueEditButtons
69
+ onClickCancel={cancelEdit}
70
+ onSubmitEdit={submitEdit}
71
+ sx={{ ml: 2 }}
72
+ />
73
+ </>
74
+ ),
75
+ }}
76
+ />
77
+ ),
78
+ }}
79
+ />
80
+ ) : (
81
+ <Box display="flex" alignItems="center">
82
+ <Typography variant="h5" noWrap aria-labelledby={id}>
83
+ {value}
84
+ </Typography>
85
+ {editable && (
86
+ <IconButton size="small" onClick={startEdit} sx={{ ml: 1 }}>
87
+ <EditIcon />
88
+ </IconButton>
89
+ )}
90
+ </Box>
91
+ )}
31
92
  </ValueContent>
32
93
  );
33
94
  };
@@ -1,7 +1,7 @@
1
1
  import { Box } from "@mui/material";
2
2
  import React from "react";
3
3
  import { ValueContent } from "../value-content";
4
- import { BaseValueProps } from "../value-displays.types";
4
+ import { BaseValueProps } from "../value-base/value-displays.types";
5
5
 
6
6
  export interface ValueImageProps extends BaseValueProps<string> {
7
7
  /**
@@ -25,3 +25,11 @@ Max.args = {
25
25
  value: 4,
26
26
  maxRating: 7,
27
27
  };
28
+
29
+ export const Editable = Template.bind({});
30
+ Editable.args = {
31
+ label: "Lorem",
32
+ value: 4,
33
+ maxRating: 7,
34
+ editable: true,
35
+ };
@@ -1,10 +1,25 @@
1
1
  import React from "react";
2
2
  import { render, screen } from "~/tests/testing-library";
3
3
  import { ValueRating } from "./value-rating";
4
+ import userEvent from "@testing-library/user-event";
4
5
 
5
6
  describe("ValueRating", () => {
6
- const renderComponent = ({ maxRating }: { maxRating?: number } = {}) => {
7
- render(<ValueRating label="hello world" value={3} maxRating={maxRating} />);
7
+ const renderComponent = ({
8
+ maxRating,
9
+ value = 3,
10
+ editable,
11
+ }: { maxRating?: number; value?: number; editable?: boolean } = {}) => {
12
+ const onEdit = jest.fn();
13
+ render(
14
+ <ValueRating
15
+ label="hello world"
16
+ value={value}
17
+ maxRating={maxRating}
18
+ editable={editable}
19
+ onEdit={onEdit}
20
+ />,
21
+ );
22
+ return { onEdit };
8
23
  };
9
24
 
10
25
  it("should render a label", () => {
@@ -32,4 +47,58 @@ describe("ValueRating", () => {
32
47
 
33
48
  expect(screen.getAllByTestId("StarIcon")).toHaveLength(3);
34
49
  });
50
+
51
+ describe("editable", () => {
52
+ it("shouldn't render an option to edit if editable is false", () => {
53
+ renderComponent({ editable: false });
54
+
55
+ expect(screen.queryByTestId("EditIcon")).not.toBeInTheDocument();
56
+ });
57
+
58
+ it("should render an option to edit if editable is true", () => {
59
+ renderComponent({ editable: true });
60
+
61
+ expect(screen.getByTestId("EditIcon")).toBeVisible();
62
+ });
63
+
64
+ it("should render an input for each star + 1 for the zero value if the edit button is clicked", async () => {
65
+ renderComponent({ editable: true });
66
+
67
+ await userEvent.click(screen.getByTestId("EditIcon"));
68
+
69
+ expect(screen.getAllByRole("radio")).toHaveLength(6);
70
+ });
71
+
72
+ it("should submit the new value if is edited", async () => {
73
+ const { onEdit } = renderComponent({ value: 2, editable: true });
74
+
75
+ await userEvent.click(screen.getByTestId("EditIcon"));
76
+ await userEvent.click(screen.getAllByRole("radio")[3]); // Click the fourth star
77
+ await userEvent.click(screen.getByTestId("CheckIcon"));
78
+
79
+ expect(onEdit).toHaveBeenCalledTimes(1);
80
+ expect(onEdit).toHaveBeenCalledWith(4);
81
+ });
82
+
83
+ it("should not call onEdit if the edition is cancelled", async () => {
84
+ const { onEdit } = renderComponent({ editable: true });
85
+
86
+ await userEvent.click(screen.getByTestId("EditIcon"));
87
+ await userEvent.click(screen.getAllByRole("radio")[3]);
88
+ await userEvent.click(screen.getByTestId("ClearIcon"));
89
+
90
+ expect(onEdit).not.toHaveBeenCalled();
91
+ });
92
+
93
+ it("should have the original value if is edited again after clear a change", async () => {
94
+ renderComponent({ editable: true });
95
+
96
+ await userEvent.click(screen.getByTestId("EditIcon"));
97
+ await userEvent.click(screen.getAllByRole("radio")[3]);
98
+ await userEvent.click(screen.getByTestId("ClearIcon"));
99
+ await userEvent.click(screen.getByTestId("EditIcon"));
100
+
101
+ expect(screen.getAllByTestId("StarIcon")).toHaveLength(3);
102
+ });
103
+ });
35
104
  });
@@ -1,21 +1,45 @@
1
- import { Rating, Typography } from "@mui/material";
1
+ import { Box, IconButton, Rating } from "@mui/material";
2
2
  import React from "react";
3
- import { BaseValueProps, DefaultPlaceholder } from "../value-displays.types";
3
+ import { useEditableValueDisplay, ValueEditButtons } from "../value-base";
4
+ import { BaseValueProps, EditableValueProps } from "../value-base/value-displays.types";
4
5
  import { getValueContentLabelId, ValueContent } from "../value-content";
6
+ import EditIcon from "@mui/icons-material/Edit";
5
7
 
6
8
  export type ValueRatingProps = BaseValueProps<number> & {
7
9
  maxRating?: number;
8
- };
10
+ } & EditableValueProps<number>;
9
11
 
10
12
  /**
11
13
  * Displays a number using stars
12
14
  */
13
- export const ValueRating = ({ label, value = 0, maxRating = 5 }: ValueRatingProps) => {
15
+ export const ValueRating = ({
16
+ label,
17
+ value = 0,
18
+ maxRating = 5,
19
+ editable,
20
+ onEdit = () => null,
21
+ }: ValueRatingProps) => {
22
+ const { isEditing, editValue, startEdit, cancelEdit, setEditValue, submitEdit } =
23
+ useEditableValueDisplay(value, onEdit);
14
24
  const id = getValueContentLabelId(label);
15
25
 
16
26
  return (
17
27
  <ValueContent label={label} tooltip={value.toString()}>
18
- <Rating aria-labelledby={id} readOnly max={maxRating} value={value} />
28
+ <Box display="flex" alignItems="center">
29
+ <Rating
30
+ aria-labelledby={id}
31
+ readOnly={!isEditing}
32
+ max={maxRating}
33
+ value={isEditing ? editValue : value}
34
+ onChange={(_, newValue) => setEditValue(newValue!)}
35
+ />
36
+ {editable && !isEditing && (
37
+ <IconButton size="small" onClick={startEdit} sx={{ ml: 1 }}>
38
+ <EditIcon />
39
+ </IconButton>
40
+ )}
41
+ {isEditing && <ValueEditButtons onClickCancel={cancelEdit} onSubmitEdit={submitEdit} />}
42
+ </Box>
19
43
  </ValueContent>
20
44
  );
21
45
  };
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { render, screen } from "~/tests/testing-library";
3
- import { TextValue, ValueText } from "./value-text";
3
+ import { ValueText } from "./value-text";
4
4
  import userEvent from "@testing-library/user-event";
5
5
 
6
6
  const DummyValue = "Lorem ipsum sit amet";
@@ -11,12 +11,11 @@ describe("ValueText", () => {
11
11
  placeholder,
12
12
  editable,
13
13
  }: {
14
- value?: TextValue;
14
+ value?: string | number;
15
15
  placeholder?: string;
16
16
  editable?: boolean;
17
17
  }) => {
18
18
  const onEdit = jest.fn();
19
- const onCancelEdit = jest.fn();
20
19
  render(
21
20
  <ValueText
22
21
  label="Hello world"
@@ -24,10 +23,9 @@ describe("ValueText", () => {
24
23
  placeholder={placeholder}
25
24
  editable={editable}
26
25
  onEdit={onEdit}
27
- onCancelEdit={onCancelEdit}
28
26
  />,
29
27
  );
30
- return { onEdit, onCancelEdit };
28
+ return { onEdit };
31
29
  };
32
30
 
33
31
  it("would render the label", () => {
@@ -93,8 +91,8 @@ describe("ValueText", () => {
93
91
  expect(onEdit).toHaveBeenCalledWith("new value");
94
92
  });
95
93
 
96
- it("should call onCancelEdit without calling onEdit if the edition is cancelled", async () => {
97
- const { onEdit, onCancelEdit } = renderComponent({ value: DummyValue, editable: true });
94
+ it("should not call onEdit if the edition is cancelled", async () => {
95
+ const { onEdit } = renderComponent({ value: DummyValue, editable: true });
98
96
 
99
97
  await userEvent.click(screen.getByTestId("EditIcon"));
100
98
  await userEvent.clear(screen.getByRole("textbox"));
@@ -102,7 +100,6 @@ describe("ValueText", () => {
102
100
  await userEvent.click(screen.getByTestId("ClearIcon"));
103
101
 
104
102
  expect(onEdit).not.toHaveBeenCalled();
105
- expect(onCancelEdit).toHaveBeenCalledTimes(1);
106
103
  });
107
104
 
108
105
  it("should have the original value if is edited again after clear a change", async () => {
@@ -1,57 +1,32 @@
1
- import { Button, IconButton, InputAdornment, TextField, Typography } from "@mui/material";
2
- import React, { useState } from "react";
3
- import { BaseValueProps, DefaultPlaceholder } from "../value-displays.types";
1
+ import { IconButton, TextField, Typography } from "@mui/material";
2
+ import React from "react";
3
+ import {
4
+ BaseValueProps,
5
+ DefaultPlaceholder,
6
+ EditableValueProps,
7
+ useEditableValueDisplay,
8
+ ValueEditButtons,
9
+ } from "../value-base";
4
10
  import { getValueContentLabelId, ValueContent } from "../value-content";
5
- import CheckIcon from "@mui/icons-material/Check";
6
11
  import EditIcon from "@mui/icons-material/Edit";
7
- import ClearIcon from "@mui/icons-material/Clear";
8
12
 
9
- export type TextValue = string | number;
10
-
11
- export interface ValueTextProps<T extends TextValue> extends BaseValueProps<T> {
12
- /**
13
- * This field can be edited or not
14
- */
15
- editable?: boolean;
16
-
17
- /**
18
- * Callback executed when the value is edited
19
- */
20
- onEdit?: (value?: string) => void;
21
-
22
- /**
23
- * Callback called when the user cancels an edition
24
- */
25
- onCancelEdit?: () => void;
26
- }
13
+ export type ValueTextProps = BaseValueProps<string | number> & EditableValueProps<string>;
27
14
 
28
15
  /**
29
16
  * Displays a string value with a label
30
17
  */
31
- export const ValueText = <T extends TextValue>({
18
+ export const ValueText = ({
32
19
  label,
33
20
  value: valueProp,
34
21
  placeholder = DefaultPlaceholder,
35
22
  editable,
36
23
  onEdit = () => null,
37
- onCancelEdit = () => null,
38
- }: ValueTextProps<T>) => {
39
- const [isEditing, setIsEditing] = useState(false);
40
- const [editValue, setEditValue] = useState<string | undefined>(valueProp?.toString());
24
+ }: ValueTextProps) => {
25
+ const { isEditing, editValue, startEdit, cancelEdit, setEditValue, submitEdit } =
26
+ useEditableValueDisplay(valueProp?.toString(), onEdit);
41
27
  const id = getValueContentLabelId(label);
42
28
  const value = valueProp?.toString() || placeholder;
43
29
 
44
- const handleCancelEdit = () => {
45
- setIsEditing(false);
46
- setEditValue(valueProp?.toString());
47
- onCancelEdit();
48
- };
49
-
50
- const handleEdit = () => {
51
- onEdit(editValue);
52
- setIsEditing(false);
53
- };
54
-
55
30
  return (
56
31
  <ValueContent label={label} tooltip={value}>
57
32
  {isEditing ? (
@@ -60,33 +35,14 @@ export const ValueText = <T extends TextValue>({
60
35
  size="small"
61
36
  onChange={(e) => setEditValue(e.target.value)}
62
37
  InputProps={{
63
- endAdornment: (
64
- <InputAdornment position="end">
65
- <Button
66
- variant="contained"
67
- size="small"
68
- color="error"
69
- startIcon={<ClearIcon />}
70
- onClick={handleCancelEdit}
71
- sx={{ paddingRight: 0, minWidth: 0, marginRight: 1 }}
72
- />
73
- <Button
74
- variant="contained"
75
- size="small"
76
- color="primary"
77
- startIcon={<CheckIcon />}
78
- onClick={handleEdit}
79
- sx={{ paddingRight: 0, minWidth: 0 }}
80
- />
81
- </InputAdornment>
82
- ),
38
+ endAdornment: <ValueEditButtons onClickCancel={cancelEdit} onSubmitEdit={submitEdit} />,
83
39
  }}
84
40
  />
85
41
  ) : (
86
42
  <Typography variant="h5" noWrap aria-labelledby={id}>
87
43
  {value}
88
44
  {editable && (
89
- <IconButton size="small" onClick={() => setIsEditing(true)} sx={{ ml: 1 }}>
45
+ <IconButton size="small" onClick={startEdit} sx={{ ml: 1 }}>
90
46
  <EditIcon />
91
47
  </IconButton>
92
48
  )}