Skip to content

Commit a4e7cb3

Browse files
Combine organisations and clients
1 parent d2030c8 commit a4e7cb3

File tree

18 files changed

+279
-537
lines changed

18 files changed

+279
-537
lines changed

packages/app/package.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
"dependencies": {
66
"@fortawesome/fontawesome-free": "^6.7.1",
77
"@reconmap/native-components": "workspace:^",
8-
"@types/node": "^22.10.1",
9-
"@types/react": "^18.3.13",
10-
"@types/react-dom": "^18.3.1",
8+
"@types/node": "^22.10.2",
9+
"@types/react": "^19.0.1",
10+
"@types/react-dom": "^19.0.2",
1111
"@uiw/react-md-editor": "^4.0.4",
1212
"bulma": "^1.0.2",
1313
"countries-and-timezones": "^3.7.2",
@@ -16,19 +16,19 @@
1616
"dayjs": "^1.11.13",
1717
"friendly-mimes": "^3.0.1",
1818
"http-status-codes": "^2.3.0",
19-
"i18next": "^24.0.5",
20-
"i18next-browser-languagedetector": "^8.0.0",
19+
"i18next": "^24.1.0",
20+
"i18next-browser-languagedetector": "^8.0.2",
2121
"javascript-time-ago": "^2.5.11",
2222
"keycloak-js": "^26.0.7",
2323
"path-browserify": "^1.0.1",
24-
"react": "^18.3.1",
25-
"react-dom": "^18.3.1",
24+
"react": "^19.0.0",
25+
"react-dom": "^19.0.0",
2626
"react-dropzone": "^14.3.5",
2727
"react-hot-toast": "^2.4.1",
28-
"react-i18next": "^15.1.3",
28+
"react-i18next": "^15.2.0",
2929
"react-markdown": "^9.0.1",
3030
"react-router-dom": "^7.0.2",
31-
"react-select": "^5.8.3",
31+
"react-select": "^5.9.0",
3232
"react-time-ago": "^7.3.3",
3333
"recharts": "^2.14.1",
3434
"typescript": "^5.7.2",
@@ -37,26 +37,26 @@
3737
},
3838
"devDependencies": {
3939
"@testing-library/jest-dom": "^6.6.3",
40-
"@testing-library/react": "^16.0.1",
40+
"@testing-library/react": "^16.1.0",
4141
"@testing-library/user-event": "^14.5.2",
42-
"@typescript-eslint/eslint-plugin": "^8.17.0",
43-
"@typescript-eslint/parser": "^8.17.0",
42+
"@typescript-eslint/eslint-plugin": "^8.18.0",
43+
"@typescript-eslint/parser": "^8.18.0",
4444
"@vitejs/plugin-react": "^4.3.4",
4545
"@vitest/coverage-v8": "^2.1.8",
4646
"eslint": "^9.16.0",
4747
"eslint-config-prettier": "^9.1.0",
4848
"eslint-plugin-import": "^2.31.0",
4949
"eslint-plugin-jsx-a11y": "^6.10.2",
5050
"eslint-plugin-react": "^7.37.2",
51-
"eslint-plugin-react-hooks": "^5.0.0",
51+
"eslint-plugin-react-hooks": "^5.1.0",
5252
"jsdom": "^25.0.1",
5353
"npm-check-updates": "^17.1.11",
5454
"prettier": "^3.4.2",
5555
"stylelint": "^16.11.0",
5656
"stylelint-config-standard": "^36.0.1",
57-
"vite": "^6.0.2",
57+
"vite": "^6.0.3",
5858
"vite-plugin-svgr": "^4.3.0",
59-
"vite-tsconfig-paths": "^5.1.3",
59+
"vite-tsconfig-paths": "^5.1.4",
6060
"vitest": "^2.1.8"
6161
},
6262
"scripts": {

packages/app/src/components/clients/Create.jsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
import { actionCompletedToast, errorToast } from "components/ui/toast";
22
import Client from "models/Client";
33
import { useState } from "react";
4+
import { useTranslation } from "react-i18next";
45
import { Link, useNavigate } from "react-router-dom";
56
import secureApiFetch from "../../services/api";
67
import Breadcrumb from "../ui/Breadcrumb";
78
import Title from "../ui/Title";
89
import ClientForm from "./Form";
10+
import OrganisationsUrls from "./OrganisationsUrls";
911

1012
const ClientCreate = () => {
13+
const [t] = useTranslation();
14+
1115
const navigate = useNavigate();
1216
const [newClient, setNewClient] = useState(Client);
1317

1418
const onFormSubmit = async (ev) => {
1519
ev.preventDefault();
1620

17-
secureApiFetch(`/clients`, {
21+
const form = ev.target.closest("form");
22+
const formData = new FormData(form);
23+
const data = Object.fromEntries(formData.entries());
24+
25+
secureApiFetch("/clients", {
1826
method: "POST",
19-
body: JSON.stringify(newClient),
27+
body: JSON.stringify(data),
2028
}).then((resp) => {
2129
if (resp.status === 201) {
22-
navigate(`/clients`);
30+
navigate(OrganisationsUrls.List);
2331
actionCompletedToast(`The client "${newClient.name}" has been added.`);
2432
} else {
2533
errorToast("The client could not be saved. Review the form data or check the application logs.");
@@ -31,11 +39,11 @@ const ClientCreate = () => {
3139
<div>
3240
<div className="heading">
3341
<Breadcrumb>
34-
<Link to="/clients">Clients</Link>
42+
<Link to={OrganisationsUrls.List}>{t("Organisations")}</Link>
3543
</Breadcrumb>
3644
</div>
3745

38-
<Title title="New client details" />
46+
<Title title={t("New organisation")} />
3947

4048
<ClientForm onFormSubmit={onFormSubmit} client={newClient} clientSetter={setNewClient} />
4149
</div>

packages/app/src/components/clients/Details.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { actionCompletedToast, errorToast } from "components/ui/toast";
1616
import UserLink from "components/users/Link";
1717
import Contact from "models/Contact";
1818
import { useEffect, useState } from "react";
19+
import { useTranslation } from "react-i18next";
1920
import { Link, useNavigate, useParams } from "react-router-dom";
2021
import secureApiFetch from "services/api";
2122
import Breadcrumb from "../ui/Breadcrumb";
@@ -25,6 +26,7 @@ import DeleteButton from "../ui/buttons/Delete";
2526
import useDelete from "./../../hooks/useDelete";
2627
import useFetch from "./../../hooks/useFetch";
2728
import Loading from "./../ui/Loading";
29+
import OrganisationsUrls from "./OrganisationsUrls";
2830

2931
const ClientProjectsTab = ({ clientId }) => {
3032
const [projects] = useFetch(`/projects?clientId=${clientId}`);
@@ -48,6 +50,8 @@ const ClientProjectsTab = ({ clientId }) => {
4850
};
4951

5052
const ClientDetails = () => {
53+
const [t] = useTranslation();
54+
5155
const { clientId } = useParams();
5256
const navigate = useNavigate();
5357

@@ -140,7 +144,9 @@ const ClientDetails = () => {
140144
</Breadcrumb>
141145
<NativeButtonGroup>
142146
<RestrictedComponent roles={["administrator", "superuser", "user"]}>
143-
<LinkButton href={`/clients/${client.id}/edit`}>Edit</LinkButton>
147+
<LinkButton href={OrganisationsUrls.Edit.replace(":organisationId", client.id)}>
148+
{t("Edit")}
149+
</LinkButton>
144150
<DeleteButton onClick={handleDelete} />
145151
</RestrictedComponent>
146152
</NativeButtonGroup>

packages/app/src/components/clients/Form.jsx

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import AttachmentsImageDropzone from "components/attachments/ImageDropzone";
2+
import LabelledField from "components/form/LabelledField";
23
import NativeInput from "components/form/NativeInput";
4+
import NativeSelect from "components/form/NativeSelect";
35
import RestrictedComponent from "components/logic/RestrictedComponent";
46
import { useEffect, useState } from "react";
7+
import { useTranslation } from "react-i18next";
58
import secureApiFetch from "../../services/api";
69
import PrimaryButton from "../ui/buttons/Primary";
710

811
const ClientForm = ({ isEditForm = false, onFormSubmit, client, clientSetter: setClient }) => {
12+
const [t] = useTranslation();
13+
914
const parentType = "client";
1015
const parentId = client.id;
1116
const [logo, setLogo] = useState(null);
@@ -58,21 +63,37 @@ const ClientForm = ({ isEditForm = false, onFormSubmit, client, clientSetter: se
5863
<form onSubmit={onFormSubmit}>
5964
<fieldset>
6065
<legend>Basic information</legend>
61-
<label>
62-
Name
63-
<NativeInput
64-
type="text"
65-
name="name"
66-
onChange={onFormChange}
67-
value={client.name || ""}
68-
required
69-
autoFocus
70-
/>
71-
</label>
72-
<label>
73-
Address
74-
<NativeInput type="text" name="address" onChange={onFormChange} value={client.address || ""} />
75-
</label>
66+
<LabelledField
67+
label={t("Type")}
68+
control={
69+
<NativeSelect name="kind" defaultValue={client.kind}>
70+
<option value="service_provider">{t("Service provider")}</option>
71+
<option value="client">{t("Client")}</option>
72+
</NativeSelect>
73+
}
74+
/>
75+
76+
<LabelledField
77+
label={t("Name")}
78+
control={
79+
<NativeInput
80+
type="text"
81+
name="name"
82+
onChange={onFormChange}
83+
value={client.name || ""}
84+
required
85+
autoFocus
86+
/>
87+
}
88+
/>
89+
90+
<LabelledField
91+
label={t("Address")}
92+
control={
93+
<NativeInput type="text" name="address" onChange={onFormChange} value={client.address || ""} />
94+
}
95+
/>
96+
7697
<label>
7798
URL
7899
<NativeInput type="text" name="url" onChange={onFormChange} value={client.url || ""} />

packages/app/src/components/clients/Link.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { useTranslation } from "react-i18next";
12
import { Link } from "react-router-dom";
3+
import OrganisationsUrls from "./OrganisationsUrls";
24

35
const styles = {
46
badge: {
@@ -9,12 +11,14 @@ const styles = {
911
};
1012

1113
const ClientLink = ({ clientId, children }) => {
14+
const [t] = useTranslation();
15+
1216
if (!clientId) {
13-
return "(not set)";
17+
return t("(not set)");
1418
}
1519

1620
return (
17-
<Link style={styles.badge} to={`/clients/${clientId}`}>
21+
<Link style={styles.badge} to={OrganisationsUrls.Details.replace(":organisationId", clientId)}>
1822
{children}
1923
</Link>
2024
);

packages/app/src/components/clients/List.jsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,39 @@ import DeleteIconButton from "components/ui/buttons/DeleteIconButton";
44
import ExportButton from "components/ui/buttons/ExportButton";
55
import LoadingTableRow from "components/ui/tables/LoadingTableRow";
66
import NoResultsTableRow from "components/ui/tables/NoResultsTableRow";
7-
import { useNavigate } from "react-router-dom";
7+
import { useTranslation } from "react-i18next";
88
import useDelete from "../../hooks/useDelete";
99
import useFetch from "../../hooks/useFetch";
1010
import Breadcrumb from "../ui/Breadcrumb";
1111
import ExternalLink from "../ui/ExternalLink";
12-
import CreateButton from "../ui/buttons/Create";
1312
import LinkButton from "../ui/buttons/Link";
1413
import ClientLink from "./Link";
14+
import OrganisationsUrls from "./OrganisationsUrls";
1515

1616
const ClientsList = () => {
17-
const navigate = useNavigate();
17+
const [t] = useTranslation();
18+
1819
const [clients, updateTasks] = useFetch("/clients");
1920

2021
const destroy = useDelete("/clients/", updateTasks);
2122

22-
const handleCreateClient = () => {
23-
navigate(`/clients/create`);
24-
};
25-
2623
return (
2724
<>
2825
<div className="heading">
2926
<Breadcrumb />
3027

3128
<NativeButtonGroup>
32-
<CreateButton onClick={handleCreateClient}>Add client</CreateButton>
29+
<LinkButton href={OrganisationsUrls.Create}>{t("Add organisation")}</LinkButton>
3330
<ExportButton entity="clients" disabled={clients === null || clients?.length === 0} />
3431
</NativeButtonGroup>
3532
</div>
36-
<Title title="Clients" />
33+
<Title title={t("Organisations")} />
3734

3835
<table className="table is-fullwidth">
3936
<thead>
4037
<tr>
41-
<th>Name</th>
38+
<th>{t("Type")}</th>
39+
<th>{t("Name")}</th>
4240
<th>Address</th>
4341
<th>URL</th>
4442
<th>Number of contacts</th>
@@ -52,6 +50,7 @@ const ClientsList = () => {
5250
0 < clients.length &&
5351
clients.map((client) => (
5452
<tr key={client.id}>
53+
<td>{client.kind}</td>
5554
<td>
5655
<ClientLink clientId={client.id}>{client.name}</ClientLink>
5756
</td>
@@ -61,7 +60,9 @@ const ClientsList = () => {
6160
</td>
6261
<td>{client.num_contacts}</td>
6362
<td>
64-
<LinkButton href={`/clients/${client.id}/edit`}>Edit</LinkButton>
63+
<LinkButton href={OrganisationsUrls.Edit.replace(":organisationId", client.id)}>
64+
{t("Edit")}
65+
</LinkButton>
6566
<DeleteIconButton onClick={() => destroy(client.id)} />
6667
</td>
6768
</tr>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const OrganisationsUrls: Record<string, string> = {
2+
List: "/organisations",
3+
Create: "/organisations/create",
4+
Edit: "/organisations/:organisationId/edit",
5+
Details: "/organisations/:organisationId",
6+
};
7+
8+
export default OrganisationsUrls;

packages/app/src/components/clients/Routes.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import ClientCreate from "./Create";
44
import ClientDetails from "./Details";
55
import EditClientPage from "./Edit";
66
import ClientsList from "./List";
7+
import OrganisationsUrls from "./OrganisationsUrls";
78

89
const ClientsRoutes = [
9-
<Route path={`/clients`} element={<SettingsLayout />}>
10+
<Route path={OrganisationsUrls.List} element={<SettingsLayout />}>
1011
<Route index element={<ClientsList />} />
1112
<Route path={`create`} element={<ClientCreate />} />,
1213
<Route path={`:clientId`} element={<ClientDetails />} />,

packages/app/src/components/form/DynamicForm.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@ const DynamicForm = ({ fields }) => {
44
return fields.map((field) => {
55
if (["text"].includes(field.kind)) {
66
return (
7-
<div>
7+
<div key={`customField-${field.name}`}>
88
<label htmlFor={field.name}>{field.label}</label>
99
<NativeInput id={field.name} type={field.kind} name={field.name} />
1010
</div>
1111
);
1212
}
1313
if (["integer"].includes(field.kind)) {
1414
return (
15-
<div>
15+
<div key={`customField-${field.name}`}>
1616
<label htmlFor={field.name}>{field.label}</label>
1717
<NativeInput id={field.name} type="number" name={field.name} />
1818
</div>
1919
);
2020
}
2121
if (["decimal"].includes(field.kind)) {
2222
return (
23-
<div>
23+
<div key={`customField-${field.name}`}>
2424
<label htmlFor={field.name}>{field.label}</label>
2525
<NativeInput id={field.name} type="number" name={field.name} step="0.001" />
2626
</div>

packages/app/src/components/layout/Header.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import OrganisationsUrls from "components/clients/OrganisationsUrls";
12
import SearchUrls from "components/search/SearchUrls";
23
import ExternalLink from "components/ui/ExternalLink";
34
import Configuration from "Configuration";
@@ -24,14 +25,18 @@ const MenuLinks = [
2425
{ name: t("Commands"), url: "/commands", permissions: "commands.*" },
2526
{ name: t("Vulnerabilities"), url: "/vulnerabilities", permissions: "commands.*" },
2627
{ name: t("Documents"), url: "/documents", permissions: "documents.*" },
28+
null,
29+
{ name: t("Search"), url: SearchUrls.AdvancedSearch },
2730
],
2831
},
2932
{
3033
name: t("Settings"),
3134
items: [
3235
{ name: t("Users"), url: "/users" },
36+
{ name: t("Organisations"), url: OrganisationsUrls.List },
37+
null,
3338
{ name: t("Custom fields"), url: "/settings/custom-fields" },
34-
{ name: t("Search"), url: SearchUrls.AdvancedSearch },
39+
null,
3540
{ name: t("Import data"), url: "/system/import-data" },
3641
{ name: t("Export data"), url: "/system/export-data" },
3742
],

0 commit comments

Comments
 (0)