@pautena/react-design-system 0.1.4 → 0.3.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 (61) hide show
  1. package/dist/cjs/index.js +13 -4
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/value-displays/index.d.ts +1 -0
  4. package/dist/cjs/types/components/value-displays/value-datetime/index.d.ts +1 -0
  5. package/dist/cjs/types/components/value-displays/value-datetime/value-datetime.d.ts +18 -0
  6. package/dist/cjs/types/generators/generators.mock.d.ts +9 -5
  7. package/dist/cjs/types/generators/generators.model.d.ts +25 -1
  8. package/dist/cjs/types/generators/model-router/index.d.ts +1 -0
  9. package/dist/cjs/types/generators/model-router/screens/add-screen.d.ts +1 -1
  10. package/dist/cjs/types/generators/model-router/screens/list-screen.d.ts +1 -1
  11. package/dist/cjs/types/generators/model-router/screens/screens.types.d.ts +20 -0
  12. package/dist/cjs/types/generators/model-router/screens/update-screen.d.ts +1 -1
  13. package/dist/cjs/types/index.d.ts +1 -0
  14. package/dist/esm/index.js +13 -4
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/types/components/value-displays/index.d.ts +1 -0
  17. package/dist/esm/types/components/value-displays/value-datetime/index.d.ts +1 -0
  18. package/dist/esm/types/components/value-displays/value-datetime/value-datetime.d.ts +18 -0
  19. package/dist/esm/types/generators/generators.mock.d.ts +9 -5
  20. package/dist/esm/types/generators/generators.model.d.ts +25 -1
  21. package/dist/esm/types/generators/model-router/index.d.ts +1 -0
  22. package/dist/esm/types/generators/model-router/screens/add-screen.d.ts +1 -1
  23. package/dist/esm/types/generators/model-router/screens/list-screen.d.ts +1 -1
  24. package/dist/esm/types/generators/model-router/screens/screens.types.d.ts +20 -0
  25. package/dist/esm/types/generators/model-router/screens/update-screen.d.ts +1 -1
  26. package/dist/esm/types/index.d.ts +1 -0
  27. package/dist/index.d.ts +98 -4
  28. package/package.json +6 -2
  29. package/src/components/header/header.test.tsx +2 -12
  30. package/src/components/tab/tab-card/tab-card.tsx +2 -2
  31. package/src/components/table-list/table-list.test.tsx +8 -1
  32. package/src/components/table-list/table-list.tsx +3 -3
  33. package/src/components/value-displays/index.ts +1 -0
  34. package/src/components/value-displays/value-datetime/index.ts +1 -0
  35. package/src/components/value-displays/value-datetime/value-datetime.stories.tsx +21 -0
  36. package/src/components/value-displays/value-datetime/value-datetime.test.tsx +23 -0
  37. package/src/components/value-displays/value-datetime/value-datetime.tsx +40 -0
  38. package/src/components/value-displays/value-text/{value-test.test.tsx → value-text.test.tsx} +0 -0
  39. package/src/generators/generators.mock.ts +56 -17
  40. package/src/generators/generators.model.ts +39 -1
  41. package/src/generators/model-form/model-form.stories.tsx +2 -2
  42. package/src/generators/model-form/model-form.test.tsx +39 -22
  43. package/src/generators/model-form/model-form.tsx +220 -33
  44. package/src/generators/model-router/index.ts +1 -0
  45. package/src/generators/model-router/model-router.test.tsx +264 -57
  46. package/src/generators/model-router/model-router.tsx +4 -3
  47. package/src/generators/model-router/screens/add-screen.tsx +2 -1
  48. package/src/generators/model-router/screens/list-screen.tsx +41 -26
  49. package/src/generators/model-router/screens/screens.types.ts +25 -0
  50. package/src/generators/model-router/screens/update-screen.tsx +2 -1
  51. package/src/generators/model-router/stories/list-screen.stories.tsx +51 -0
  52. package/src/generators/model-router/stories/model-router.stories.tsx +66 -3
  53. package/src/generators/object-details/object-details.tsx +5 -4
  54. package/src/index.ts +1 -0
  55. package/src/layouts/app-bar-with-drawer-layout/app-bar-with-drawer-layout.stories.tsx +1 -1
  56. package/src/layouts/app-bar-with-drawer-layout/app-bar-with-drawer-layout.tsx +1 -1
  57. package/src/storybook.tsx +10 -0
  58. package/src/tests/actions.ts +43 -0
  59. package/src/tests/assertions.ts +70 -1
  60. package/src/tests/index.ts +1 -0
  61. package/src/tests/testing-library.tsx +5 -1
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { DummyModelRouter } from "./stories/model-router.stories";
2
+ import { DummyModelRouter, InternalModelRouter } from "./stories/model-router.stories";
3
3
  import {
4
4
  expectModelFieldInputExist,
5
5
  expectProgressIndicator,
@@ -9,12 +9,31 @@ import {
9
9
  TestRouter,
10
10
  expectModelFieldValue,
11
11
  expectModelFieldInputValue,
12
+ selectOption,
12
13
  } from "~/tests";
14
+ import { data as mockData } from "./stories/templates";
13
15
  import userEvent from "@testing-library/user-event";
14
16
  import { getRandomItem } from "../../utils";
15
17
  import { Model } from "../generators.model";
16
- import { createModelInstance, MockInstance, mockModel } from "../generators.mock";
18
+ import {
19
+ BirthDateFormat,
20
+ createModelInstance,
21
+ MockInstance,
22
+ mockModel,
23
+ ReturnTimeFormat,
24
+ TradeDateFormat,
25
+ } from "../generators.mock";
17
26
  import { NotificationCenterProvider } from "../../providers";
27
+ import { Box } from "@mui/system";
28
+ import { Button } from "@mui/material";
29
+ import { useNavigate } from "react-router-dom";
30
+ import {
31
+ clearCheckbox,
32
+ clearMultiSelect,
33
+ expectToHaveBeenCalledOnceWithMockInstance,
34
+ pickDatetime,
35
+ selectOptions,
36
+ } from "../../tests";
18
37
 
19
38
  const REQUEST_TIMEOUT = 20;
20
39
 
@@ -30,16 +49,39 @@ describe("ModelRouter", () => {
30
49
  name: "Add Items",
31
50
  level: 1,
32
51
  }),
52
+ expectNotToBeInAddScreen: () => {
53
+ expect(
54
+ screen.queryByRole("heading", {
55
+ name: "Add Items",
56
+ level: 1,
57
+ }),
58
+ ).not.toBeInTheDocument();
59
+ },
33
60
  expectDetailScreen: async ({ id }: { id: string }) =>
34
61
  await screen.findByRole("heading", {
35
62
  name: id,
36
63
  level: 1,
37
64
  }),
65
+ expectNotToBeInDetailsScreen: ({ id }: { id: string }) => {
66
+ expect(
67
+ screen.queryByRole("heading", {
68
+ name: id,
69
+ level: 1,
70
+ }),
71
+ ).not.toBeInTheDocument();
72
+ },
38
73
  expectUpdateScreen: async ({ id }: { id: string }) =>
39
74
  await screen.findByRole("heading", {
40
75
  name: `Edit ${id}`,
41
76
  level: 1,
42
77
  }),
78
+ expectNotToBeInUpdateScreen: ({ id }: { id: string }) =>
79
+ expect(
80
+ screen.queryByRole("heading", {
81
+ name: `Edit ${id}`,
82
+ level: 1,
83
+ }),
84
+ ).not.toBeInTheDocument(),
43
85
  expectListItems: async ({ data, model }: { data: MockInstance[]; model: Model }) => {
44
86
  for (let i = 0; i < model.fields.length; ++i) {
45
87
  const { id, listable } = model.fields[i];
@@ -52,43 +94,34 @@ describe("ModelRouter", () => {
52
94
  expectMenuOption: async ({ id }: { id: string }) => {
53
95
  await screen.findByTestId(`options-${id}`);
54
96
  },
55
- expectSubmitInstanceCall: (mockFn: jest.Mock, instance: MockInstance) => {
56
- expect(mockFn).toHaveBeenCalledTimes(1);
57
- expect(mockFn).toHaveBeenCalledWith({
58
- id: instance.id,
59
- firstName: instance.firstName,
60
- middleName: instance.middleName,
61
- lastName: instance.lastName,
62
- gender: instance.gender,
63
- age: instance.age.toString(),
64
- birthDate: instance.birthDate,
65
- car: {
66
- model: instance.car.model,
67
- manufacturer: instance.car.manufacturer,
68
- color: instance.car.color,
69
- type: instance.car.type,
70
- vin: instance.car.vin,
71
- vrm: instance.car.vrm,
72
- },
73
- quantity: instance.quantity.toString(),
74
- available: instance.available.toString(),
75
- currency: instance.currency,
76
- tradeDate: instance.tradeDate,
77
- });
97
+ expectNotToHaveMenuOption: ({ id }: { id: string }) => {
98
+ expect(screen.queryByTestId(`options-${id}`)).not.toBeInTheDocument();
78
99
  },
79
100
  };
80
101
 
81
102
  const actions = {
82
103
  navigateToAddScreen: async () => {
83
- await userEvent.click(screen.getByRole("button", { name: /add/i }));
104
+ await userEvent.click(await screen.findByRole("button", { name: "Add" }));
105
+ },
106
+ forceNavigateToAddScreen: async () => {
107
+ await userEvent.click(screen.getByRole("button", { name: /force add/i }));
84
108
  },
85
109
  navigateToUpdateScreen: async ({ id }: { id: string }) => {
86
110
  await actions.openItemOptions({ id });
87
111
  await userEvent.click(screen.getByRole("menuitem", { name: /edit/i }));
88
112
  },
113
+ forceNavigateToUpdateScreen: async () => {
114
+ await userEvent.click(screen.getByRole("button", { name: /force update/i }));
115
+ },
89
116
  navigateToDetailScreen: async ({ name }: { name: string }) => {
90
117
  await userEvent.click(await screen.findByRole("cell", { name }));
91
118
  },
119
+ navigateToInternal: async () => {
120
+ await userEvent.click(screen.getByRole("button", { name: /go to internal/i }));
121
+ },
122
+ forceNavigateToDetailsScreen: async () => {
123
+ await userEvent.click(screen.getByRole("button", { name: /force details/i }));
124
+ },
92
125
  openItemOptions: async ({ id }: { id: string }) => {
93
126
  await userEvent.click(await screen.findByTestId(`options-${id}`));
94
127
  },
@@ -107,17 +140,18 @@ describe("ModelRouter", () => {
107
140
  const firstNameElement = screen.getByRole("textbox", { name: /first name/i });
108
141
  const middleNameElement = screen.getByRole("textbox", { name: /middle name/i });
109
142
  const lastNameElement = screen.getByRole("textbox", { name: /last name/i });
110
- const genderElement = screen.getByRole("textbox", { name: /gender/i });
143
+ const genderElement = screen.getByRole("button", { name: /gender/i });
111
144
  const ageElement = screen.getByRole("spinbutton", { name: /age/i });
112
145
  const birthDateElement = screen.getByRole("textbox", { name: /birth date/i });
113
- const manufacturerElement = screen.getByRole("textbox", { name: /manufacturer/i });
114
- const modelElement = screen.getByRole("textbox", { name: /model/i });
146
+ const manufacturerElement = screen.getByRole("button", { name: /manufacturer/i });
147
+ const modelElement = screen.getByRole("button", { name: /model/i });
115
148
  const colorElement = screen.getByRole("textbox", { name: /color/i });
116
- const typeElement = screen.getByRole("textbox", { name: /type/i });
149
+ const typeElement = screen.getByRole("button", { name: /type/i });
117
150
  const vinElement = screen.getByRole("textbox", { name: /vin/i });
118
151
  const vrmElement = screen.getByRole("textbox", { name: /vrm/i });
152
+ const timeReturnElement = screen.getByRole("textbox", { name: /return time/i });
119
153
  const quantityElement = screen.getByRole("spinbutton", { name: /q/i });
120
- const availableElement = screen.getByRole("textbox", { name: /available/i });
154
+ const availableElement = screen.getByRole("checkbox", { name: /available/i });
121
155
  const currencyElement = screen.getByRole("textbox", { name: /currency/i });
122
156
  const tradeDateElement = screen.getByRole("textbox", { name: /trade date/i });
123
157
 
@@ -126,37 +160,39 @@ describe("ModelRouter", () => {
126
160
  await userEvent.clear(firstNameElement);
127
161
  await userEvent.clear(middleNameElement);
128
162
  await userEvent.clear(lastNameElement);
129
- await userEvent.clear(genderElement);
130
163
  await userEvent.clear(ageElement);
131
164
  await userEvent.clear(birthDateElement);
132
- await userEvent.clear(manufacturerElement);
133
- await userEvent.clear(modelElement);
134
165
  await userEvent.clear(colorElement);
135
- await userEvent.clear(typeElement);
136
166
  await userEvent.clear(vinElement);
137
167
  await userEvent.clear(vrmElement);
138
168
  await userEvent.clear(quantityElement);
139
169
  await userEvent.clear(availableElement);
140
170
  await userEvent.clear(currencyElement);
141
171
  await userEvent.clear(tradeDateElement);
172
+ await userEvent.clear(timeReturnElement);
173
+ await clearCheckbox(availableElement);
174
+ await clearMultiSelect(typeElement);
142
175
  }
143
176
  await userEvent.type(idElement, instance.id);
144
177
  await userEvent.type(firstNameElement, instance.firstName);
145
178
  await userEvent.type(middleNameElement, instance.middleName);
146
179
  await userEvent.type(lastNameElement, instance.lastName);
147
- await userEvent.type(genderElement, instance.gender);
180
+ await selectOption(genderElement, instance.gender);
148
181
  await userEvent.type(ageElement, instance.age.toString());
149
- await userEvent.type(birthDateElement, instance.birthDate);
150
- await userEvent.type(modelElement, instance.car.model);
151
- await userEvent.type(manufacturerElement, instance.car.manufacturer);
182
+ pickDatetime(birthDateElement, instance.birthDate, BirthDateFormat);
183
+ await selectOption(modelElement, instance.car.model);
184
+ await selectOption(manufacturerElement, instance.car.manufacturer);
152
185
  await userEvent.type(colorElement, instance.car.color);
153
- await userEvent.type(typeElement, instance.car.type);
186
+ await selectOptions(typeElement, instance.car.type);
154
187
  await userEvent.type(vinElement, instance.car.vin);
155
188
  await userEvent.type(vrmElement, instance.car.vrm);
189
+ pickDatetime(timeReturnElement, instance.car.returnTime, ReturnTimeFormat);
156
190
  await userEvent.type(quantityElement, instance.quantity.toString());
157
- await userEvent.type(availableElement, instance.available.toString());
191
+ if (instance.available) {
192
+ await userEvent.click(availableElement);
193
+ }
158
194
  await userEvent.type(currencyElement, instance.currency);
159
- await userEvent.type(tradeDateElement, instance.tradeDate);
195
+ pickDatetime(tradeDateElement, instance.tradeDate, TradeDateFormat);
160
196
 
161
197
  submit && (await userEvent.click(screen.getByRole("button", { name: /save/i })));
162
198
 
@@ -167,7 +203,18 @@ describe("ModelRouter", () => {
167
203
  const renderComponent = async ({
168
204
  router = "memory",
169
205
  screen = "initial",
170
- }: { router?: TestRouter; screen?: "initial" | "add" | "details" | "update" } = {}) => {
206
+ deleteFeature,
207
+ updateFeature,
208
+ addFeature,
209
+ detailsFeature,
210
+ }: {
211
+ router?: TestRouter;
212
+ screen?: "initial" | "add" | "details" | "update";
213
+ deleteFeature?: boolean;
214
+ updateFeature?: boolean;
215
+ addFeature?: boolean;
216
+ detailsFeature?: boolean;
217
+ } = {}) => {
171
218
  const onRequestList = jest.fn();
172
219
  const onRequestItem = jest.fn();
173
220
  const onSubmitNewItem = jest.fn();
@@ -175,10 +222,28 @@ describe("ModelRouter", () => {
175
222
  const onSubmitUpdate = jest.fn();
176
223
  const onRequestDelete = jest.fn();
177
224
  const args = DummyModelRouter.args;
225
+ const randomItem = getRandomItem(args.initialData);
226
+
227
+ const ForceNavigationComponent = ({ id }: { id: string }) => {
228
+ const navigate = useNavigate();
229
+
230
+ return (
231
+ <Box>
232
+ <Button onClick={() => navigate(`/${id}/update`)}>Force update</Button>
233
+ <Button onClick={() => navigate("/add")}>Force add</Button>
234
+ <Button onClick={() => navigate(`/${id}`)}>Force details</Button>
235
+ </Box>
236
+ );
237
+ };
238
+
178
239
  const instance = render(
179
240
  <NotificationCenterProvider>
180
241
  <DummyModelRouter
181
242
  {...args}
243
+ deleteFeature={deleteFeature}
244
+ updateFeature={updateFeature}
245
+ addFeature={addFeature}
246
+ detailsFeature={detailsFeature}
182
247
  requestTimeout={REQUEST_TIMEOUT}
183
248
  onRequestListAction={onRequestList}
184
249
  onRequestItem={onRequestItem}
@@ -187,14 +252,13 @@ describe("ModelRouter", () => {
187
252
  onSubmitUpdateAction={onSubmitUpdate}
188
253
  onRequestDeleteAction={onRequestDelete}
189
254
  />
255
+ <ForceNavigationComponent id={randomItem.item.id} />
190
256
  </NotificationCenterProvider>,
191
257
  {
192
258
  router,
193
259
  },
194
260
  );
195
261
 
196
- const randomItem = getRandomItem(args.initialData);
197
-
198
262
  if (screen === "add") {
199
263
  await actions.navigateToAddScreen();
200
264
  } else if (screen === "details") {
@@ -217,6 +281,15 @@ describe("ModelRouter", () => {
217
281
  };
218
282
  };
219
283
 
284
+ const renderComponentInsideRouter = () => {
285
+ render(
286
+ <NotificationCenterProvider>
287
+ <InternalModelRouter />
288
+ </NotificationCenterProvider>,
289
+ { router: "memory" },
290
+ );
291
+ };
292
+
220
293
  describe("router screens", () => {
221
294
  it("would render the list screen by default", async () => {
222
295
  await renderComponent();
@@ -292,6 +365,41 @@ describe("ModelRouter", () => {
292
365
  });
293
366
  });
294
367
 
368
+ describe("inside another router", () => {
369
+ it("would navigate to the add screen", async () => {
370
+ renderComponentInsideRouter();
371
+
372
+ await actions.navigateToInternal();
373
+ await actions.navigateToAddScreen();
374
+
375
+ await assertions.expectAddScreen();
376
+ });
377
+
378
+ it("would navigate to the detail screen", async () => {
379
+ renderComponentInsideRouter();
380
+ const {
381
+ item: { id, firstName },
382
+ } = getRandomItem<MockInstance>(mockData);
383
+
384
+ await actions.navigateToInternal();
385
+ await actions.navigateToDetailScreen({ name: firstName });
386
+
387
+ await assertions.expectDetailScreen({ id });
388
+ });
389
+
390
+ it("would navigate to the update screen", async () => {
391
+ renderComponentInsideRouter();
392
+ const {
393
+ item: { id },
394
+ } = getRandomItem<MockInstance>(mockData);
395
+
396
+ await actions.navigateToInternal();
397
+ await actions.navigateToUpdateScreen({ id });
398
+
399
+ await assertions.expectUpdateScreen({ id });
400
+ });
401
+ });
402
+
295
403
  describe("list screen", () => {
296
404
  it("would call onRequestList when is mounted", async () => {
297
405
  const { onRequestList } = await renderComponent();
@@ -321,13 +429,13 @@ describe("ModelRouter", () => {
321
429
  it("would render an add button", async () => {
322
430
  await renderComponent();
323
431
 
324
- expect(screen.getByRole("button", { name: /add/i })).toBeInTheDocument();
432
+ expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
325
433
  });
326
434
 
327
435
  it("would navigate to the add screen if I press the add button", async () => {
328
436
  const { history } = await renderComponent({ router: "router" });
329
437
 
330
- await userEvent.click(screen.getByRole("button", { name: /add/i }));
438
+ await userEvent.click(screen.getByRole("button", { name: "Add" }));
331
439
 
332
440
  expect(history.location.pathname).toBe("/add");
333
441
  });
@@ -430,7 +538,7 @@ describe("ModelRouter", () => {
430
538
 
431
539
  const newInstance = await actions.fullfillModelForm({ model, submit: true });
432
540
 
433
- assertions.expectSubmitInstanceCall(onSubmitNewItem, newInstance);
541
+ expectToHaveBeenCalledOnceWithMockInstance(onSubmitNewItem, newInstance);
434
542
  });
435
543
 
436
544
  it("would show a loading indicator when the request is in progress", async () => {
@@ -593,12 +701,12 @@ describe("ModelRouter", () => {
593
701
  });
594
702
 
595
703
  it("would make a request with the new values when the form is submitted", async () => {
596
- const { model } = await renderComponent({ screen: "update" });
704
+ const { model, onSubmitUpdate } = await renderComponent({ screen: "update" });
597
705
 
598
706
  await waitForProgressIndicatorToBeRemoved();
599
- const newInstance = await actions.fullfillModelForm({ model, clear: true });
707
+ const newInstance = await actions.fullfillModelForm({ model, clear: true, submit: true });
600
708
 
601
- expectModelFieldInputValue(model.fields, newInstance);
709
+ expectToHaveBeenCalledOnceWithMockInstance(onSubmitUpdate, newInstance);
602
710
  });
603
711
 
604
712
  it("would show a loading indicator while the submit request is in progress", async () => {
@@ -610,14 +718,14 @@ describe("ModelRouter", () => {
610
718
  expectProgressIndicator();
611
719
  });
612
720
 
613
- it("would navigate to the list screen when the submit request finish", async () => {
614
- const { model, onSubmitUpdate } = await renderComponent({ screen: "update" });
721
+ // it("would navigate to the list screen when the submit request finish", async () => {
722
+ // const { model } = await renderComponent({ screen: "update" });
615
723
 
616
- await waitForProgressIndicatorToBeRemoved();
617
- const newInstance = await actions.fullfillModelForm({ model, submit: true, clear: true });
724
+ // await waitForProgressIndicatorToBeRemoved();
725
+ // await actions.fullfillModelForm({ model, submit: true, clear: true });
618
726
 
619
- assertions.expectSubmitInstanceCall(onSubmitUpdate, newInstance);
620
- });
727
+ // await assertions.expectListScreen();
728
+ // });
621
729
  });
622
730
 
623
731
  describe("delete item", () => {
@@ -663,4 +771,103 @@ describe("ModelRouter", () => {
663
771
  expect(screen.queryByRole("cell", { name: firstName })).not.toBeInTheDocument();
664
772
  });
665
773
  });
774
+
775
+ describe("modifying enabled features", () => {
776
+ describe("deleteFeature disabled", () => {
777
+ it("wouldn't have an option to remove an item from the list", async () => {
778
+ const { data } = await renderComponent({ deleteFeature: false });
779
+ const {
780
+ item: { id, firstName },
781
+ } = getRandomItem<MockInstance>(data);
782
+
783
+ await screen.findByRole("cell", { name: firstName });
784
+ await actions.openItemOptions({ id });
785
+
786
+ expect(screen.queryByRole("menuitem", { name: /remove/i })).not.toBeInTheDocument();
787
+ });
788
+ });
789
+
790
+ describe("updateFeature disabled", () => {
791
+ it("wouldn't have an option to remove an item from the list", async () => {
792
+ const { data } = await renderComponent({ updateFeature: false });
793
+ const {
794
+ item: { id, firstName },
795
+ } = getRandomItem<MockInstance>(data);
796
+
797
+ await screen.findByRole("cell", { name: firstName });
798
+ await actions.openItemOptions({ id });
799
+
800
+ expect(screen.queryByRole("menuitem", { name: /edit/i })).not.toBeInTheDocument();
801
+ });
802
+
803
+ it("wouldn't have a path to navigate to edit an item", async () => {
804
+ const {
805
+ randomItem: {
806
+ item: { id },
807
+ },
808
+ } = await renderComponent({
809
+ updateFeature: false,
810
+ });
811
+
812
+ await actions.forceNavigateToUpdateScreen();
813
+
814
+ assertions.expectNotToBeInUpdateScreen({ id });
815
+ });
816
+ });
817
+
818
+ describe("deleteFeature and updateFeature disabled", () => {
819
+ it("wouldn't render an options button in the list", async () => {
820
+ const { data } = await renderComponent({ deleteFeature: false, updateFeature: false });
821
+ const {
822
+ item: { id },
823
+ } = getRandomItem<MockInstance>(data);
824
+
825
+ assertions.expectNotToHaveMenuOption({ id });
826
+ });
827
+ });
828
+
829
+ describe("addFeature disabled", () => {
830
+ it("wouldn't render a button to navigate to the add screen", async () => {
831
+ await renderComponent({ addFeature: false });
832
+
833
+ expect(screen.queryByRole("button", { name: "Add" })).not.toBeInTheDocument();
834
+ });
835
+
836
+ it("wouldn't have a path to navigate to the add screen", async () => {
837
+ await renderComponent({
838
+ addFeature: false,
839
+ });
840
+
841
+ await actions.forceNavigateToAddScreen();
842
+
843
+ assertions.expectNotToBeInAddScreen();
844
+ });
845
+ });
846
+
847
+ describe("detailsFeature disabled", () => {
848
+ it("wouldn't navigate to the details screen if I click a row item", async () => {
849
+ const {
850
+ randomItem: {
851
+ item: { id, firstName },
852
+ },
853
+ } = await renderComponent({ detailsFeature: false });
854
+
855
+ await userEvent.click(await screen.findByRole("cell", { name: firstName }));
856
+
857
+ assertions.expectNotToBeInDetailsScreen({ id });
858
+ });
859
+
860
+ it("wouldn't have a path to navigate to the details screen", async () => {
861
+ const {
862
+ randomItem: {
863
+ item: { id },
864
+ },
865
+ } = await renderComponent({ detailsFeature: false });
866
+
867
+ await actions.forceNavigateToDetailsScreen();
868
+
869
+ assertions.expectNotToBeInDetailsScreen({ id });
870
+ });
871
+ });
872
+ });
666
873
  });
@@ -18,12 +18,13 @@ export type ModelRouterProps<T extends BasicModelInstance> = DetailsScreenProps<
18
18
  UpdateScreenProps<T>;
19
19
 
20
20
  export const ModelRouter = <T extends BasicModelInstance>(props: ModelRouterProps<T>) => {
21
+ const { updateFeature = true, addFeature = true, detailsFeature = true } = props;
21
22
  return (
22
23
  <Routes>
23
24
  <Route path="" element={<ListScreen {...props} />} />
24
- <Route path=":id" element={<DetailsScreen {...props} />} />
25
- <Route path="add" element={<AddScreen {...props} />} />
26
- <Route path=":id/update" element={<UpdateScreen {...props} />} />
25
+ {detailsFeature && <Route path=":id" element={<DetailsScreen {...props} />} />}
26
+ {addFeature && <Route path="add" element={<AddScreen {...props} />} />}
27
+ {updateFeature && <Route path=":id/update" element={<UpdateScreen {...props} />} />}
27
28
  </Routes>
28
29
  );
29
30
  };
@@ -24,6 +24,7 @@ export interface AddScreenProps<T extends BasicModelInstance> extends BaseScreen
24
24
  export const AddScreen = <T extends BasicModelInstance>({
25
25
  model,
26
26
  modelName,
27
+ basePath = "",
27
28
  onSubmitNewItem,
28
29
  newItemRequest,
29
30
  }: AddScreenProps<T>) => {
@@ -33,7 +34,7 @@ export const AddScreen = <T extends BasicModelInstance>({
33
34
  useEffect(() => {
34
35
  if (newItemRequest.success) {
35
36
  show({ message: "Item added successfully", severity: "success" });
36
- navigate("/");
37
+ navigate(`${basePath}/`);
37
38
  }
38
39
  }, [newItemRequest.success]);
39
40
 
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect } from "react";
2
2
  import { useNavigate } from "react-router-dom";
3
- import { Content, Header, TableList } from "~/components";
3
+ import { Content, Header, HeaderAction, TableList, TableRowOption } from "~/components";
4
4
  import { BasicModelInstance } from "~/generators";
5
5
  import { HeaderLayout } from "../../../layouts";
6
6
  import { RequestState } from "../model-router.types";
@@ -41,10 +41,15 @@ export const ListScreen = <T extends BasicModelInstance>({
41
41
  model,
42
42
  modelName,
43
43
  listData,
44
- onRequestList,
45
- onClickDeleteItem,
46
44
  listRequest,
47
45
  deleteRequest,
46
+ basePath = "",
47
+ deleteFeature = true,
48
+ updateFeature = true,
49
+ addFeature = true,
50
+ detailsFeature = true,
51
+ onRequestList,
52
+ onClickDeleteItem,
48
53
  }: ListScreenProps<T>) => {
49
54
  const navigate = useNavigate();
50
55
 
@@ -52,30 +57,51 @@ export const ListScreen = <T extends BasicModelInstance>({
52
57
  onRequestList();
53
58
  }, []);
54
59
 
55
- const handleClickListItem = (item: T) => {
56
- navigate(`/${item.id}`);
57
- };
60
+ const handleClickListItem = detailsFeature
61
+ ? (item: T) => {
62
+ navigate(`${basePath}/${item.id}`);
63
+ }
64
+ : undefined;
58
65
 
59
66
  const handleClickListOption = (optionId: "edit" | "remove", item: T) => {
60
67
  if (optionId === "edit") {
61
- navigate(`/${item.id}/update`);
68
+ navigate(`${basePath}/${item.id}/update`);
62
69
  } else {
63
70
  onClickDeleteItem(item);
64
71
  }
65
72
  };
66
73
 
74
+ const options: TableRowOption<T>[] = [];
75
+
76
+ updateFeature &&
77
+ options.push({
78
+ id: "edit",
79
+ label: "Edit",
80
+ onClick: (item: T) => handleClickListOption("edit", item),
81
+ });
82
+
83
+ deleteFeature &&
84
+ options.push({
85
+ id: "remove",
86
+ label: "Remove",
87
+ onClick: (item: T) => handleClickListOption("remove", item),
88
+ });
89
+
90
+ const actions: HeaderAction[] = [];
91
+
92
+ addFeature &&
93
+ actions.push({
94
+ id: "add",
95
+ text: "Add",
96
+ href: `${basePath}/add`,
97
+ });
98
+
67
99
  return (
68
100
  <HeaderLayout loading={listRequest.loading || deleteRequest.loading}>
69
101
  <Header
70
102
  title={modelName}
71
103
  preset="default"
72
- actions={[
73
- {
74
- id: "add",
75
- text: "Add",
76
- href: "/add",
77
- },
78
- ]}
104
+ actions={actions.length > 0 ? actions : undefined}
79
105
  />
80
106
  <Content>
81
107
  <TableList
@@ -91,18 +117,7 @@ export const ListScreen = <T extends BasicModelInstance>({
91
117
  data={listData}
92
118
  defaultSort={model.fields[0].id}
93
119
  onClick={handleClickListItem}
94
- options={[
95
- {
96
- id: "edit",
97
- label: "Edit",
98
- onClick: (item) => handleClickListOption("edit", item),
99
- },
100
- {
101
- id: "remove",
102
- label: "Remove",
103
- onClick: (item) => handleClickListOption("remove", item),
104
- },
105
- ]}
120
+ options={options.length > 0 ? options : undefined}
106
121
  />
107
122
  </Content>
108
123
  </HeaderLayout>
@@ -10,4 +10,29 @@ export interface BaseScreenProps {
10
10
  * Structure that represents the fields of the model
11
11
  */
12
12
  model: Model;
13
+
14
+ /**
15
+ * Path to attach before each internal ModelRouter path
16
+ */
17
+ basePath?: string;
18
+
19
+ /**
20
+ * If true delete features are enabled
21
+ */
22
+ deleteFeature?: boolean;
23
+
24
+ /**
25
+ * If true update features are enabled
26
+ */
27
+ updateFeature?: boolean;
28
+
29
+ /**
30
+ * If true add features are enabled
31
+ */
32
+ addFeature?: boolean;
33
+
34
+ /**
35
+ * If true details features are enabled
36
+ */
37
+ detailsFeature?: boolean;
13
38
  }
@@ -39,6 +39,7 @@ export interface UpdateScreenProps<T extends BasicModelInstance> extends BaseScr
39
39
  export const UpdateScreen = <T extends BasicModelInstance>({
40
40
  model,
41
41
  modelName,
42
+ basePath = "",
42
43
  submitUpdateItemRequest,
43
44
  updateItemRequest,
44
45
  updateItem,
@@ -61,7 +62,7 @@ export const UpdateScreen = <T extends BasicModelInstance>({
61
62
  message: `The item ${id} has been updated successfully`,
62
63
  severity: "success",
63
64
  });
64
- navigate("/");
65
+ navigate(`${basePath}/`);
65
66
  }
66
67
  }, [submitUpdateItemRequest.success]);
67
68