diff --git a/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java index 994a71c5f58..24f3dbdbbd2 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java @@ -12,6 +12,8 @@ public class ReactJwtModuleFactory { + private static final JHipsterSource ROOT = from("client/react"); + private static final JHipsterSource SOURCE = from("client/react/src"); private static final JHipsterSource APP_SOURCE = SOURCE.append("main/webapp/app"); @@ -40,15 +42,26 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { //@formatter:off return moduleBuilder(properties) .packageJson() + .addType("module") .addDependency(packageName("react-hook-form"), REACT) .addDependency(packageName("axios"), REACT) + .addDependency(packageName("framer-motion"), REACT) .addDependency(packageName("@nextui-org/react"), REACT) + .addDevDependency(packageName("autoprefixer"), REACT) .addDevDependency(packageName("sass"), REACT) + .addDevDependency(packageName("postcss"), REACT) + .addDevDependency(packageName("tailwindcss"), REACT) .and() .files() + .batch(ROOT, to(".")) + .addFile("postcss.config.js") + .addFile("tailwind.config.js") + .and() .add(APP_SOURCE.template("common/services/storage.ts"), COMMON_DESTINATION.append("services/storage.ts")) .add(APP_SOURCE.append("login/primary/loginForm").template("index.tsx"), APP_DESTINATION.append("login/primary/loginForm/index.tsx")) .batch(APP_SOURCE.append("login/primary/loginModal"), APP_DESTINATION.append("login/primary/loginModal/")) + .addTemplate("EyeSlashFilledIcon.tsx") + .addTemplate("EyeFilledIcon.tsx") .addTemplate("index.tsx") .addTemplate("interface.d.ts") .addTemplate("LoginModal.scss") @@ -70,7 +83,15 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { .add(lineBeforeText("function App() {"), "import LoginForm from '@/login/primary/loginForm';" + LINE_BREAK) .add(LOGIN_FORM_MATCHER, properties.indentation().times(4) + "") .and() + .in(path("src/main/webapp/app/index.tsx")) + .add(lineBeforeText("import { createRoot } from 'react-dom/client';"), "import { NextUIProvider } from '@nextui-org/react';") + .add(lineBeforeText(""), "") + .add(lineBeforeText(""), "") + .and() + .in(path("src/main/webapp/app/index.css")) + .add(lineBeforeText("body {"), "@tailwind base;" + LINE_BREAK + "@tailwind components;" + LINE_BREAK + "@tailwind utilities;" + LINE_BREAK) .and() + .and() .optionalReplacements() .in(path("src/main/webapp/app/common/primary/app/App.css")) .add(text(" -moz-osx-font-smoothing: grayscale;"), AUTHENTICATION_STYLE) diff --git a/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java b/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java index 8f15f4b763b..64ab5f3b479 100644 --- a/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java +++ b/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java @@ -12,6 +12,7 @@ public class JHipsterModulePackageJson { private final PackageJsonDependencies dependenciesToRemove; private final PackageJsonDependencies devDependencies; private final PackageJsonDependencies devDependenciesToRemove; + private final PackageJsonType type; private JHipsterModulePackageJson(JHipsterModulePackageJsonBuilder builder) { scripts = new Scripts(builder.scripts); @@ -19,6 +20,7 @@ private JHipsterModulePackageJson(JHipsterModulePackageJsonBuilder builder) { dependenciesToRemove = new PackageJsonDependencies(builder.dependenciesToRemove); devDependencies = new PackageJsonDependencies(builder.devDependencies); devDependenciesToRemove = new PackageJsonDependencies(builder.devDependenciesToRemove); + type = new PackageJsonType(builder.type); } public static JHipsterModulePackageJsonBuilder builder(JHipsterModuleBuilder module) { @@ -55,6 +57,10 @@ public PackageJsonDependencies dependenciesToRemove() { return dependenciesToRemove; } + public PackageJsonType type() { + return type; + } + public static class JHipsterModulePackageJsonBuilder { private final JHipsterModuleBuilder module; @@ -63,6 +69,7 @@ public static class JHipsterModulePackageJsonBuilder { private final Collection devDependencies = new ArrayList<>(); private final Collection dependenciesToRemove = new ArrayList<>(); private final Collection devDependenciesToRemove = new ArrayList<>(); + private String type; private JHipsterModulePackageJsonBuilder(JHipsterModuleBuilder module) { Assert.notNull("module", module); @@ -98,6 +105,12 @@ public JHipsterModulePackageJsonBuilder removeDevDependency(PackageName packageN return this; } + public JHipsterModulePackageJsonBuilder addType(String t) { + type = t; + + return this; + } + public JHipsterModuleBuilder and() { return module; } diff --git a/src/main/java/tech/jhipster/lite/module/domain/packagejson/PackageJsonType.java b/src/main/java/tech/jhipster/lite/module/domain/packagejson/PackageJsonType.java new file mode 100644 index 00000000000..0f5c2952568 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/module/domain/packagejson/PackageJsonType.java @@ -0,0 +1,3 @@ +package tech.jhipster.lite.module.domain.packagejson; + +public record PackageJsonType(String type) {} diff --git a/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java index 67c2f4e0ba4..cc57a3df409 100644 --- a/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java +++ b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java @@ -23,6 +23,7 @@ import tech.jhipster.lite.module.domain.packagejson.JHipsterModulePackageJson; import tech.jhipster.lite.module.domain.packagejson.PackageJsonDependencies; import tech.jhipster.lite.module.domain.packagejson.PackageJsonDependency; +import tech.jhipster.lite.module.domain.packagejson.PackageJsonType; import tech.jhipster.lite.module.domain.packagejson.Scripts; import tech.jhipster.lite.module.domain.properties.JHipsterProjectFolder; @@ -52,6 +53,7 @@ public void handle(Indentation indentation, JHipsterProjectFolder projectFolder, Path file = getPackageJsonFile(projectFolder); String content = readContent(file); + content = replaceType(indentation, packageJson.type(), content); content = replaceScripts(indentation, packageJson.scripts(), content); content = replaceDevDependencies(indentation, packageJson.devDependencies(), content); content = replaceDependencies(indentation, packageJson.dependencies(), content); @@ -124,6 +126,10 @@ private String removeDependencies(Indentation indentation, PackageJsonDependenci .apply(); } + private String replaceType(Indentation indentation, PackageJsonType packageJsonType, String content) { + return JsonAction.replace().blocName("type").jsonContent(content).indentation(indentation).blocValue(packageJsonType.type()).apply(); + } + private List dependenciesEntries(PackageJsonDependencies devDependencies) { return devDependencies.stream().map(dependency -> new JsonEntry(dependency.packageName().get(), getNpmVersion(dependency))).toList(); } @@ -157,6 +163,7 @@ private static class JsonAction { private final Indentation indentation; private final Collection entries; private final JsonActionType action; + private final String blocValue; private JsonAction(JsonActionBuilder builder) { blocName = builder.blocName; @@ -164,6 +171,7 @@ private JsonAction(JsonActionBuilder builder) { indentation = builder.indentation; entries = builder.entries; action = builder.action; + blocValue = builder.blocValue; } public static JsonActionBuilder replace() { @@ -178,7 +186,11 @@ public static JsonActionBuilder remove() { public String handle() { Assert.notNull("action", action); - if (entries.isEmpty()) { + if (blocValue != null) { + return appendNewRootEntry(jsonContent); + } + + if (entries == null || entries.isEmpty()) { return jsonContent; } @@ -217,6 +229,22 @@ private String appendEntries(Matcher blocMatcher) { }); } + private String appendNewRootEntry(String result) { + String jsonBloc = new StringBuilder() + .append(LINE_SEPARATOR) + .append(indentation.spaces()) + .append(QUOTE) + .append(blocName) + .append(QUOTE) + .append(": ") + .append(QUOTE) + .append(blocValue) + .append(QUOTE) + .toString(); + + return result.replaceFirst("(\\s{1,10})\\}(\\s{1,10})$", jsonBloc + "$1}$2"); + } + private String appendNewBlock(String result) { String jsonBloc = new StringBuilder() .append(LINE_SEPARATOR) @@ -274,6 +302,7 @@ private static class JsonActionBuilder { private Indentation indentation; private Collection entries; private JsonActionType action; + private String blocValue; private JsonActionBuilder(JsonActionType action) { this.action = action; @@ -306,6 +335,12 @@ private JsonActionBuilder entries(Collection entries) { private String apply() { return new JsonAction(this).handle(); } + + public JsonActionBuilder blocValue(String blocValue) { + this.blocValue = blocValue; + + return this; + } } } diff --git a/src/main/resources/generator/client/react/postcss.config.js b/src/main/resources/generator/client/react/postcss.config.js new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/src/main/resources/generator/client/react/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginForm/index.tsx.mustache b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginForm/index.tsx.mustache index 5630c7f10e8..6e75d3c5213 100644 --- a/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginForm/index.tsx.mustache +++ b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginForm/index.tsx.mustache @@ -28,7 +28,7 @@ const LoginForm = () => {
{!token ? ( - ) @@ -38,7 +38,7 @@ const LoginForm = () => {

Welcome {username}!

- diff --git a/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/EyeFilledIcon.tsx.mustache b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/EyeFilledIcon.tsx.mustache new file mode 100644 index 00000000000..af8b1510be7 --- /dev/null +++ b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/EyeFilledIcon.tsx.mustache @@ -0,0 +1,21 @@ +export const EyeFilledIcon = (props: any) => ( + +); diff --git a/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/EyeSlashFilledIcon.tsx.mustache b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/EyeSlashFilledIcon.tsx.mustache new file mode 100644 index 00000000000..685d1ed29be --- /dev/null +++ b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/EyeSlashFilledIcon.tsx.mustache @@ -0,0 +1,33 @@ +export const EyeSlashFilledIcon = (props: any) => ( + +); diff --git a/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/index.tsx.mustache b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/index.tsx.mustache index cd6a0e6db5a..7df327a8c98 100644 --- a/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/index.tsx.mustache +++ b/src/main/resources/generator/client/react/src/main/webapp/app/login/primary/loginModal/index.tsx.mustache @@ -1,6 +1,9 @@ import { useCallback, useContext, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { Button, Input, Modal, Spacer, Text } from '@nextui-org/react'; +import { Button, Input, Modal, ModalBody, ModalContent, ModalHeader, Spacer } from '@nextui-org/react'; + +import { EyeSlashFilledIcon } from './EyeSlashFilledIcon'; +import { EyeFilledIcon } from './EyeFilledIcon'; import { login } from '@/login/services/login'; import { UserInfoContext } from '@/login/primary/loginForm'; @@ -10,8 +13,13 @@ import './LoginModal.scss'; const LoginModal = ({ open, onClose }: LoginModalType) => { const { register, handleSubmit } = useForm(); const [error, setError] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [usernameForm, setUsernameForm] = useState(""); + const [passwordForm, setPasswordForm] = useState(""); const { setUsername, setToken } = useContext(UserInfoContext); + const toggleVisibility = () => setIsVisible(!isVisible); + const onSubmit = (loginData: LoginFunctionType) => { if (loginData.username && loginData.password) { login({ ...loginData, setUsername, setToken }); @@ -26,56 +34,64 @@ const LoginModal = ({ open, onClose }: LoginModalType) => { }, []); return ( - - - - Connect - - {' '} - JHipster Lite - - - - -
- - - - - {error && ( - - Please complete fields above - - )} - - - - - - -
+ + + + Connect JHipster Lite + + +
+ + + + {isVisible ? ( + + ) : ( + + )} + + } + {...register('password')} + /> + + {error && ( +

+ Please complete fields above +

+ )} + + + + + + +
+
); }; diff --git a/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginForm/index.test.tsx.mustache b/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginForm/index.test.tsx.mustache index dc309f1755f..65aaa786c1e 100644 --- a/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginForm/index.test.tsx.mustache +++ b/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginForm/index.test.tsx.mustache @@ -11,20 +11,20 @@ const mockPost = () => { }; const login = async () => { - const { getByText, getByLabelText, getByTestId } = render(); + const { getByText, getByPlaceholderText, getByTestId } = render(); const loginButton = getByText('Log in'); fireEvent.click(loginButton); await act(async () => { - fireEvent.change(getByLabelText("Nom d'utilisateur"), { + fireEvent.change(getByPlaceholderText("Nom d'utilisateur"), { target: {value: 'admin'}, }); - fireEvent.change(getByLabelText('Mot de passe'), { + fireEvent.change(getByPlaceholderText('Mot de passe'), { target: {value: 'admin'}, }); const submitButton = getByTestId('submit-button'); fireEvent.click(submitButton); }); - return { getByText, getByLabelText, getByTestId }; + return { getByText, getByPlaceholderText, getByTestId }; }; describe('loginForm', () => { diff --git a/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginModal/index.test.tsx.mustache b/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginModal/index.test.tsx.mustache index 7e3390397a0..f2a85e4a0c6 100644 --- a/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginModal/index.test.tsx.mustache +++ b/src/main/resources/generator/client/react/src/test/javascript/spec/login/primary/loginModal/index.test.tsx.mustache @@ -36,9 +36,13 @@ describe('test login modal', () => { expect(username).toBeTruthy(); }); - it('should contain password input', () => { - const { getByPlaceholderText } = LoginModalRender(true); + it('should contain password input', async () => { + const { getByPlaceholderText, getByTestId } = LoginModalRender(true); const username = getByPlaceholderText('Mot de passe'); + await act(() => { + const displayPasswordButton = getByTestId('display-password'); + fireEvent.click(displayPasswordButton); + }); expect(username).toBeTruthy(); }); @@ -49,14 +53,14 @@ describe('test login modal', () => { }); it('render the modal on login button click and close the modal on submit button click', async () => { - const { getByLabelText, getByTestId } = LoginModalRender(true); + const { getByPlaceholderText, getByTestId } = LoginModalRender(true); const spy = vi.spyOn(axios, 'post'); spy.mockImplementationOnce(() => Promise.resolve({ data: {} })); await act(() => { - fireEvent.change(getByLabelText("Nom d'utilisateur"), { + fireEvent.change(getByPlaceholderText("Nom d'utilisateur"), { target: { value: 'admin' }, }); - fireEvent.change(getByLabelText('Mot de passe'), { + fireEvent.change(getByPlaceholderText('Mot de passe'), { target: { value: 'admin' }, }); const submitButton = getByTestId('submit-button'); @@ -65,6 +69,23 @@ describe('test login modal', () => { expect(spy).toHaveBeenCalledTimes(1); }); + it('render the modal on login button click and close the modal on close button', async () => { + const { getByPlaceholderText, getByRole } = LoginModalRender(true); + const spy = vi.spyOn(axios, 'post'); + spy.mockImplementationOnce(() => Promise.resolve({ data: {} })); + await act(() => { + fireEvent.change(getByPlaceholderText("Nom d'utilisateur"), { + target: { value: 'admin' }, + }); + fireEvent.change(getByPlaceholderText('Mot de passe'), { + target: { value: 'admin' }, + }); + const submitButton = getByRole('button', { name: 'Close' }); + fireEvent.click(submitButton); + }); + expect(spy).toHaveBeenCalledTimes(0); + }); + it('should contain error message when submit button is clicked with empty value', async () => { const { getByTestId } = LoginModalRender(true); await act(async () => { diff --git a/src/main/resources/generator/client/react/tailwind.config.js b/src/main/resources/generator/client/react/tailwind.config.js new file mode 100644 index 00000000000..c53cd060c63 --- /dev/null +++ b/src/main/resources/generator/client/react/tailwind.config.js @@ -0,0 +1,16 @@ +const { nextui } = require("@nextui-org/react"); + +module.exports = { + content: [ + "./src/main/webapp/index.html", + "./src/main/webapp/**/*.{js,ts,jsx,tsx}", + './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: {}, + }, + darkMode: "class", + plugins: [ + nextui(), + ], +} diff --git a/src/main/resources/generator/dependencies/react/package.json b/src/main/resources/generator/dependencies/react/package.json index a741b6c8f39..e649eed2d77 100644 --- a/src/main/resources/generator/dependencies/react/package.json +++ b/src/main/resources/generator/dependencies/react/package.json @@ -3,9 +3,11 @@ "version": "0.0.0", "description": "JHipster Lite : used for Vite+React dependencies", "license": "Apache-2.0", + "type": "module", "dependencies": { - "@nextui-org/react": "1.0.0-beta.13", + "@nextui-org/react": "2.0.8", "axios": "1.4.0", + "framer-motion": "10.15.1", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "7.45.4" @@ -20,11 +22,14 @@ "@typescript-eslint/eslint-plugin": "6.3.0", "@vitejs/plugin-react": "4.0.4", "@vitest/coverage-istanbul": "0.34.1", + "autoprefixer": "10.4.14", "eslint": "8.46.0", "eslint-plugin-react": "7.33.1", "jsdom": "22.1.0", + "postcss": "8.4.27", "react-scripts": "5.0.1", "sass": "1.64.2", + "tailwindcss": "3.3.3", "ts-node": "10.9.1", "typescript": "5.1.6", "vite": "4.4.9", diff --git a/src/test/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactoryTest.java index f625ab0c19c..c3bcf9d9b27 100644 --- a/src/test/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactoryTest.java +++ b/src/test/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactoryTest.java @@ -20,7 +20,7 @@ class ReactJwtModuleFactoryTest { void shouldBuildModule() { JHipsterModule module = factory.buildModule(properties()); - JHipsterModuleAsserter asserter = assertThatModuleWithFiles(module, packageJsonFile(), app(), appCss()); + JHipsterModuleAsserter asserter = assertThatModuleWithFiles(module, packageJsonFile(), app(), appCss(), indexTsx(), indexCss()); assertReactApp(asserter); asserter @@ -44,6 +44,14 @@ private ModuleFile appCss() { return file("src/test/resources/projects/react-app/App.css", "src/main/webapp/app/common/primary/app/App.css"); } + private ModuleFile indexTsx() { + return file("src/test/resources/projects/react-app/index.tsx", "src/main/webapp/app/index.tsx"); + } + + private ModuleFile indexCss() { + return file("src/test/resources/projects/react-app/index.css", "src/main/webapp/app/index.css"); + } + private JHipsterModuleProperties properties() { return JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).build(); } @@ -51,6 +59,10 @@ private JHipsterModuleProperties properties() { private void assertReactApp(JHipsterModuleAsserter asserter) { asserter .hasFile("package.json") + .containing(nodeDependency("autoprefixer")) + .containing(nodeDependency("postcss")) + .containing(nodeDependency("tailwindcss")) + .containing(nodeDependency("framer-motion")) .containing(nodeDependency("react-hook-form")) .containing(nodeDependency("axios")) .containing(nodeDependency("@nextui-org/react")) @@ -63,7 +75,14 @@ private void assertReactApp(JHipsterModuleAsserter asserter) { "app/login/primary/loginModal/index.tsx", "app/login/services/login.ts" ) - .hasPrefixedFiles("src/main/webapp/app/login/primary/loginModal", "index.tsx", "interface.d.ts", "LoginModal.scss") + .hasPrefixedFiles( + "src/main/webapp/app/login/primary/loginModal", + "EyeSlashFilledIcon.tsx", + "EyeFilledIcon.tsx", + "index.tsx", + "interface.d.ts", + "LoginModal.scss" + ) .hasPrefixedFiles( "src/test/javascript/spec", "login/services/login.test.ts", diff --git a/src/test/resources/projects/react-app/index.css b/src/test/resources/projects/react-app/index.css new file mode 100644 index 00000000000..e69792553f0 --- /dev/null +++ b/src/test/resources/projects/react-app/index.css @@ -0,0 +1,7 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/test/resources/projects/react-app/index.tsx b/src/test/resources/projects/react-app/index.tsx new file mode 100644 index 00000000000..44a79759b02 --- /dev/null +++ b/src/test/resources/projects/react-app/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from '@/common/primary/app/App'; + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render( + + + +);