diff --git a/package-lock.json b/package-lock.json index 4a56b8d..add9a4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2348,6 +2348,14 @@ "@types/react-router": "*" } }, + "@types/react-toastify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/react-toastify/-/react-toastify-4.1.0.tgz", + "integrity": "sha512-u7Ie/7LHBsPVz/iJxi/WlRDS7Gh9csCJACTDXx+pSLuZCm94xpkwzhM3jV1L5ZxP/in0Gp2tFbJ91VrSGr1gyQ==", + "requires": { + "react-toastify": "*" + } + }, "@types/semver": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.1.0.tgz", @@ -17247,6 +17255,27 @@ "prop-types": "^15.5.8" } }, + "react-toastify": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-6.0.5.tgz", + "integrity": "sha512-1YXSb6Jr478c1TJEyVpxLHFvtmeXGMvdpZc0fke/7lK+MoLBC+NFgB74bq+C2SZe6LdK+K1voEURJoY88WqWvA==", + "requires": { + "classnames": "^2.2.6", + "prop-types": "^15.7.2", + "react-transition-group": "^4.4.1" + } + }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "react-virtualized": { "version": "9.21.2", "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz", diff --git a/package.json b/package.json index c80aa0f..77765b9 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@testing-library/jest-dom": "^5.5.0", "@testing-library/react": "^10.0.2", "@testing-library/user-event": "^10.0.2", + "@types/react-toastify": "^4.1.0", "animejs": "^3.2.0", "aws-sdk": "^2.672.0", "classnames": "^2.2.6", @@ -110,6 +111,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "3.4.1", "react-spring": "^8.0.27", + "react-toastify": "^6.0.5", "react-virtualized": "^9.21.2", "recharts": "^2.0.0-beta.5", "safe-eval": "^0.4.1", diff --git a/src/app/Configuration/Configuration.tsx b/src/app/Configuration/Configuration.tsx index c416a13..d2ef6af 100755 --- a/src/app/Configuration/Configuration.tsx +++ b/src/app/Configuration/Configuration.tsx @@ -2,6 +2,7 @@ import React from "react"; import classNames from "classnames"; import { toJS } from "mobx"; import { useLocalStore, observer } from "mobx-react"; +import { toast } from "react-toastify"; import { HeaderMain } from "~/components/Blocks/HeaderMain"; import { ipc } from "~/shared/ipc"; @@ -13,6 +14,7 @@ import { ConfigurationRepositories } from "./ConfigurationRepositories"; import { ConfigurationForm } from "./ConfigurationForm"; import { ConfigurationAllUsers } from "./ConfigurationAllUsers"; import { useLayoutConfig } from "~/components/Layout/LayoutService"; +import { Config } from "~/shared/Config"; export const Configuration = observer(() => { const state = useLocalStore(() => ({ @@ -93,9 +95,79 @@ export const Configuration = observer(() => { }); return arr; }, + prepareConfig(config: Config) { + config.users.forEach((u) => { + u.refName = u.name; + }); + config.repositories.forEach((u) => { + u.refName = u.name; + }); + return config; + }, + verify(config: Config) { + let errors: string[] = []; + const usedTeamDuplicates: string[] = []; + config.teams.forEach((t) => { + if ( + config.teams.filter((tt) => tt.name === t.name).length > 1 && + !usedTeamDuplicates.includes(t.name) + ) { + errors.push("The team name has duplicates: " + t.name); + usedTeamDuplicates.push(t.name); + } + }); + const usedUserDuplicates: string[] = []; + config.users.forEach((t) => { + if ( + config.users.filter((tt) => tt.name === t.name).length > 1 && + !usedUserDuplicates.includes(t.name) + ) { + errors.push("The user name has duplicates: " + t.name); + usedUserDuplicates.push(t.name); + } + }); + const usedRepositoryDuplicates: string[] = []; + config.repositories.forEach((t) => { + if ( + config.repositories.filter((tt) => tt.name === t.name).length > 1 && + !usedRepositoryDuplicates.includes(t.name) + ) { + errors.push("The repository name has duplicates: " + t.name); + usedRepositoryDuplicates.push(t.name); + } + }); + return errors; + }, + prepareSaveConfig(config: Config) { + config.teams.forEach((t) => { + t.users = t.users.map((name) => { + const u = config.users.find((u) => u.refName === name); + if (u) { + return u.name; + } else { + return name; + } + }); + t.repositories = t.repositories.map((name) => { + const u = config.repositories.find((u) => u.refName === name); + if (u) { + return u.name; + } else { + return name; + } + }); + }); + config.users.forEach((u) => { + delete u.refName; + }); + config.repositories.forEach((u) => { + delete u.refName; + }); + return config; + }, load: async () => { state.isLoading = true; - state.config = await ipc.handlers.GET_CONFIG(true); + state.config = state.prepareConfig(await ipc.handlers.GET_CONFIG(true)); state.allUsers = await ipc.handlers.GET_REPOSITORY_USERS(); state.isDirty = false; state.isLoading = false; @@ -104,12 +176,23 @@ export const Configuration = observer(() => { if (!state.isDirty) { return; } - state.isLoading = true; - await ipc.handlers.SAVE_CONFIG(toJS(state.config)); - state.config = await ipc.handlers.GET_CONFIG(true); - state.allUsers = await ipc.handlers.GET_REPOSITORY_USERS(); - state.isDirty = false; - state.isLoading = false; + const errors = state.verify(toJS(state.config)); + if (errors.length === 0) { + state.isLoading = true; + await ipc.handlers.SAVE_CONFIG( + state.prepareSaveConfig(toJS(state.config)) + ); + state.config = state.prepareConfig(await ipc.handlers.GET_CONFIG(true)); + state.allUsers = await ipc.handlers.GET_REPOSITORY_USERS(); + state.isDirty = false; + state.isLoading = false; + } else { + errors.forEach((error) => { + toast(error, { + type: "error", + }); + }); + } }, })); diff --git a/src/app/Configuration/ConfigurationTeams.tsx b/src/app/Configuration/ConfigurationTeams.tsx index bcecb5f..92642f7 100755 --- a/src/app/Configuration/ConfigurationTeams.tsx +++ b/src/app/Configuration/ConfigurationTeams.tsx @@ -80,7 +80,6 @@ export const ConfigurationTeams = observer( { diff --git a/src/app/Layout/AppLayout.tsx b/src/app/Layout/AppLayout.tsx index 7a36c4d..9f25160 100644 --- a/src/app/Layout/AppLayout.tsx +++ b/src/app/Layout/AppLayout.tsx @@ -1,5 +1,6 @@ import React from "react"; import classNames from "classnames"; +import { ToastContainer } from "react-toastify"; import { LayoutService } from "~/components/Layout/LayoutService"; import { observer } from "mobx-react"; import { useRouteMatch } from "react-router"; @@ -234,27 +235,40 @@ export const AppLayout: React.FC = observer(({ children }) => { const service = React.useContext(LayoutService); const scrollable = service.scrollable && service.nonScrollableStack === 0; return ( -
-
- {service.sidebar && } -
-
- {service.topbar && } - {children} - {service.footer && ( -
- -
- )} + <> + +
+
+ {service.sidebar && } +
+
+ {service.topbar && } + {children} + {service.footer && ( +
+ +
+ )} +
+
- -
+ ); }); diff --git a/src/shared/Config.ts b/src/shared/Config.ts index 6b02493..3d77258 100644 --- a/src/shared/Config.ts +++ b/src/shared/Config.ts @@ -12,11 +12,13 @@ export interface Config { id: string; url: string; name: string; + refName?: string; branch?: string; exclude?: string[]; }[]; users: { id: string; + refName?: string; name: string; associations: string[]; }[]; diff --git a/src/styles/components/toastify.scss b/src/styles/components/toastify.scss new file mode 100644 index 0000000..56f9078 --- /dev/null +++ b/src/styles/components/toastify.scss @@ -0,0 +1,9 @@ +@import "~react-toastify/dist/ReactToastify.css"; + +.Toastify__toast-body { + padding: 0rem 0rem 0rem 0.5rem; +} + +.Toastify__toast { + padding: 1rem 1rem; +} diff --git a/src/styles/main.scss b/src/styles/main.scss index 44033f8..616f3b0 100755 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -6,6 +6,7 @@ @import "./colors.scss"; @import "./base.scss"; +@import "./components/toastify.scss"; @import "./components/bounce-animation.scss"; @import "./components/hr-text.scss"; @import "./components/print.scss";