Generate JHipster 9.3.0 baseline application

This commit is contained in:
jieun committed 2026-10-20 17:50:01 +09:00
1 parent 0a9b7b923f
commit 1d97cc4e31
345 files changed
+25563

No files matched your search

@@ -0,0 +1,95 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { type Ref, ref } from 'vue';
import { type RouteLocation } from 'vue-router';
import { createTestingPinia } from '@pinia/testing';
import { type ComponentMountingOptions, shallowMount } from '@vue/test-utils';
import { useLoginModal } from '@/account/login-modal';
import Error from './error.vue';
type ErrorComponentType = InstanceType<typeof Error>;
let route: Partial<RouteLocation>;
vi.mock('vue-router', () => ({
useRoute: () => route,
}));
const customErrorMsg = 'An error occurred.';
describe('Error component', () => {
let error: ErrorComponentType;
let login: ReturnType<typeof useLoginModal>;
let authenticated: Ref<boolean>;
let mountOptions: ComponentMountingOptions<ErrorComponentType>;
beforeEach(() => {
route = {};
authenticated = ref(false);
mountOptions = {
global: {
plugins: [createTestingPinia()],
provide: {
authenticated,
},
},
};
});
it('should have retrieve custom error on routing', () => {
route = {
path: '/custom-error',
name: 'CustomMessage',
meta: { errorMessage: customErrorMsg },
};
const wrapper = shallowMount(Error, mountOptions);
error = wrapper.vm;
login = useLoginModal();
expect(error.errorMessage).toBe(customErrorMsg);
expect(error.error403).toBeFalsy();
expect(error.error404).toBeFalsy();
expect(login.showLogin).not.toHaveBeenCalled();
});
it('should have set forbidden error on routing', () => {
route = {
meta: { error403: true },
};
const wrapper = shallowMount(Error, mountOptions);
error = wrapper.vm;
login = useLoginModal();
expect(error.errorMessage).toBeNull();
expect(error.error403).toBeTruthy();
expect(error.error404).toBeFalsy();
expect(login.showLogin).toHaveBeenCalled();
});
it('should have set not found error on routing', () => {
route = {
meta: { error404: true },
};
const wrapper = shallowMount(Error, mountOptions);
error = wrapper.vm;
login = useLoginModal();
expect(error.errorMessage).toBeNull();
expect(error.error403).toBeFalsy();
expect(error.error404).toBeTruthy();
expect(login.showLogin).not.toHaveBeenCalled();
});
it('should have set default on no error', () => {
const wrapper = shallowMount(Error, mountOptions);
error = wrapper.vm;
login = useLoginModal();
expect(error.errorMessage).toBeNull();
expect(error.error403).toBeFalsy();
expect(error.error404).toBeFalsy();
expect(login.showLogin).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,33 @@
import { type ComputedRef, type Ref, defineComponent, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useLoginModal } from '@/account/login-modal';
export default defineComponent({
name: 'Error',
setup() {
const { showLogin } = useLoginModal();
const authenticated = inject<ComputedRef<boolean>>('authenticated');
const errorMessage: Ref<string> = ref(null);
const error403: Ref<boolean> = ref(false);
const error404: Ref<boolean> = ref(false);
const route = useRoute();
if (route.meta) {
errorMessage.value = route.meta.errorMessage ?? null;
error403.value = route.meta.error403 ?? false;
error404.value = route.meta.error404 ?? false;
if (!authenticated.value && error403.value) {
showLogin();
}
}
return {
errorMessage,
error403,
error404,
t$: useI18n().t,
};
},
});
+20
View File
@@ -0,0 +1,20 @@
<template>
<div>
<div class="row-md">
<div class="col-md-3">
<span class="hipster img-fluid rounded"></span>
</div>
<div class="col-md-9">
<h1>{{ t$('error.title') }}</h1>
<div v-if="errorMessage">
<div class="alert alert-danger">{{ errorMessage }}</div>
</div>
<div v-if="error403" class="alert alert-danger">{{ t$('error.http.403') }}</div>
<div v-if="error404" class="alert alert-warning">{{ t$('error.http.404') }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" src="./error.component.ts"></script>
@@ -0,0 +1,55 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { ref } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { shallowMount } from '@vue/test-utils';
import { useLoginModal } from '@/account/login-modal';
import Home from './home.vue';
type HomeComponentType = InstanceType<typeof Home>;
describe('Home', () => {
let home: HomeComponentType;
let authenticated;
let currentUsername;
let login: ReturnType<typeof useLoginModal>;
beforeEach(() => {
authenticated = ref(false);
currentUsername = ref('');
const wrapper = shallowMount(Home, {
global: {
plugins: [createTestingPinia()],
stubs: {
'router-link': true,
},
provide: {
authenticated,
currentUsername,
},
},
});
home = wrapper.vm;
login = useLoginModal();
});
it('should not have user data set', () => {
expect(home.authenticated).toBeFalsy();
expect(home.username).toBe('');
});
it('should have user data set after authentication', () => {
authenticated.value = true;
currentUsername.value = 'test';
expect(home.authenticated).toBeTruthy();
expect(home.username).toBe('test');
});
it('should use login service', () => {
home.showLogin();
expect(login.showLogin).toHaveBeenCalled();
});
});
@@ -0,0 +1,19 @@
import { type ComputedRef, defineComponent, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import { useLoginModal } from '@/account/login-modal';
export default defineComponent({
setup() {
const { showLogin } = useLoginModal();
const authenticated = inject<ComputedRef<boolean>>('authenticated');
const username = inject<ComputedRef<string>>('currentUsername');
return {
authenticated,
username,
showLogin,
t$: useI18n().t,
};
},
});
+60
View File
@@ -0,0 +1,60 @@
<template>
<div class="home row">
<div class="col-md-3">
<span class="hipster img-fluid rounded"></span>
</div>
<div class="col-md-9">
<h1 class="display-4">{{ t$('home.title') }}</h1>
<p class="lead">{{ t$('home.subtitle') }}</p>
<div>
<div class="alert alert-success" v-if="authenticated">
<span v-if="username">{{ t$('home.logged.message', { username }) }}</span>
</div>
<div class="alert alert-warning" v-if="!authenticated">
<span>{{ t$('global.messages.info.authenticated.prefix') }}</span>
<a class="alert-link" @click="showLogin()">{{ t$('global.messages.info.authenticated.link') }}</a
><span v-html="t$('global.messages.info.authenticated.suffix')"></span>
</div>
<div class="alert alert-warning" v-if="!authenticated">
<span>{{ t$('global.messages.info.register.noaccount') }}</span
>&nbsp;
<router-link class="alert-link" to="/register">{{ t$('global.messages.info.register.link') }}</router-link>
</div>
</div>
<p>{{ t$('home.question') }}</p>
<ul>
<li>
<a href="https://www.jhipster.tech/" target="_blank" rel="noopener noreferrer">{{ t$('home.link.homepage') }}</a>
</li>
<li>
<a href="https://stackoverflow.com/tags/jhipster/info" target="_blank" rel="noopener noreferrer">{{
t$('home.link.stackoverflow')
}}</a>
</li>
<li>
<a href="https://github.com/jhipster/generator-jhipster/issues?state=open" target="_blank" rel="noopener noreferrer">{{
t$('home.link.bugtracker')
}}</a>
</li>
<li>
<a href="https://gitter.im/jhipster/generator-jhipster" target="_blank" rel="noopener noreferrer">{{ t$('home.link.chat') }}</a>
</li>
<li>
<a href="https://twitter.com/jhipster" target="_blank" rel="noopener noreferrer">{{ t$('home.link.follow') }}</a>
</li>
</ul>
<p>
<span>{{ t$('home.like') }}</span>
<a href="https://github.com/jhipster/generator-jhipster" target="_blank" rel="noopener noreferrer">{{ t$('home.github') }}</a
>!
</p>
</div>
</div>
</template>
<script lang="ts" src="./home.component.ts"></script>
@@ -0,0 +1,11 @@
import { defineComponent } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
name: 'JhiFooter',
setup() {
return {
t$: useI18n().t,
};
},
});
@@ -0,0 +1,7 @@
<template>
<div id="footer" class="footer">
<p>{{ t$('footer') }}</p>
</div>
</template>
<script lang="ts" src="./jhi-footer.component.ts"></script>
@@ -0,0 +1,110 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { computed } from 'vue';
import { type Router } from 'vue-router';
import { createTestingPinia } from '@pinia/testing';
import { shallowMount } from '@vue/test-utils';
import { useLoginModal } from '@/account/login-modal';
import { createRouter } from '@/router';
import { useStore } from '@/store';
import JhiNavbar from './jhi-navbar.vue';
type JhiNavbarComponentType = InstanceType<typeof JhiNavbar>;
const pinia = createTestingPinia({ stubActions: false });
const store = useStore();
describe('JhiNavbar', () => {
let jhiNavbar: JhiNavbarComponentType;
let login: ReturnType<typeof useLoginModal>;
const accountService = { hasAnyAuthorityAndCheckAuth: vi.fn().mockImplementation(() => Promise.resolve(true)) };
const changeLanguage = vi.fn();
let router: Router;
beforeEach(() => {
router = createRouter();
const wrapper = shallowMount(JhiNavbar, {
global: {
plugins: [pinia, router],
stubs: {
'font-awesome-icon': true,
'b-navbar': true,
'b-navbar-nav': true,
'b-dropdown-item': true,
'b-collapse': true,
'b-nav-item': true,
'b-nav-item-dropdown': true,
'b-navbar-toggle': true,
'b-navbar-brand': true,
},
provide: {
currentLanguage: computed(() => 'foo'),
changeLanguage,
accountService,
},
},
});
jhiNavbar = wrapper.vm;
login = useLoginModal();
});
it('should not have user data set', () => {
expect(jhiNavbar.authenticated).toBeFalsy();
expect(jhiNavbar.openAPIEnabled).toBeFalsy();
expect(jhiNavbar.inProduction).toBeFalsy();
});
it('should have user data set after authentication', () => {
store.setAuthentication({ login: 'test' });
expect(jhiNavbar.authenticated).toBeTruthy();
});
it('should have profile info set after info retrieved', () => {
store.setActiveProfiles(['prod', 'api-docs']);
expect(jhiNavbar.openAPIEnabled).toBeTruthy();
expect(jhiNavbar.inProduction).toBeTruthy();
});
it('should use login service', () => {
jhiNavbar.showLogin();
expect(login.showLogin).toHaveBeenCalled();
});
it('should use account service', () => {
jhiNavbar.hasAnyAuthority('auth');
expect(accountService.hasAnyAuthorityAndCheckAuth).toHaveBeenCalled();
});
it('logout should clear credentials and return to the home page when no provider URL is returned', async () => {
store.setAuthentication({ login: 'test' });
await jhiNavbar.logout();
expect(jhiNavbar.authenticated).toBeFalsy();
});
it('should determine active route', async () => {
await router.push('/forbidden');
expect(jhiNavbar.subIsActive('/titi')).toBeFalsy();
expect(jhiNavbar.subIsActive('/forbidden')).toBeTruthy();
expect(jhiNavbar.subIsActive(['/forbidden', 'forbidden'])).toBeTruthy();
});
it('should call translationService when changing language', () => {
jhiNavbar.changeLanguage('fr');
expect(changeLanguage).toHaveBeenCalled();
});
it('should check for correct language', () => {
expect(jhiNavbar.isActiveLanguage('en')).toBeFalsy();
expect(jhiNavbar.isActiveLanguage('foo')).toBeTruthy();
});
});
@@ -0,0 +1,79 @@
import { type Ref, computed, defineComponent, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import type AccountService from '@/account/account.service';
import { useLoginModal } from '@/account/login-modal';
import EntitiesMenu from '@/entities/entities-menu.vue';
import languages from '@/shared/config/languages';
import { AUTHENTICATION_TOKEN_KEY } from '@/shared/jhipster/constants';
import { useStore } from '@/store';
export default defineComponent({
name: 'JhiNavbar',
components: {
'entities-menu': EntitiesMenu,
},
setup() {
const { showLogin } = useLoginModal();
const accountService = inject<AccountService>('accountService');
const currentLanguage = inject('currentLanguage', () => computed(() => navigator.language ?? 'en'), true);
const changeLanguage = inject<(string) => Promise<void>>('changeLanguage');
const isActiveLanguage = (key: string) => key === currentLanguage.value;
const router = useRouter();
const store = useStore();
const version = `v${APP_VERSION}`;
const hasAnyAuthorityValues: Ref = ref({});
const openAPIEnabled = computed(() => store.activeProfiles.includes('api-docs'));
const inProduction = computed(() => store.activeProfiles.includes('prod'));
const { authenticated } = storeToRefs(store);
const subIsActive = (input: string | string[]) => {
const paths = Array.isArray(input) ? input : [input];
// current path starts with this path string
return paths.some(path => router.currentRoute.value.path.startsWith(path));
};
const logout = async () => {
localStorage.removeItem(AUTHENTICATION_TOKEN_KEY);
sessionStorage.removeItem(AUTHENTICATION_TOKEN_KEY);
store.logout();
if (router.currentRoute.value.path !== '/') {
await router.push('/');
}
};
return {
logout,
subIsActive,
accountService,
showLogin,
changeLanguage,
languages: languages(),
isActiveLanguage,
version,
currentLanguage,
hasAnyAuthorityValues,
openAPIEnabled,
inProduction,
authenticated,
t$: useI18n().t,
};
},
methods: {
hasAnyAuthority(authorities: any): boolean {
this.accountService.hasAnyAuthorityAndCheckAuth(authorities).then(value => {
if (this.hasAnyAuthorityValues[authorities] !== value) {
this.hasAnyAuthorityValues = { ...this.hasAnyAuthorityValues, [authorities]: value };
}
});
return this.hasAnyAuthorityValues[authorities] ?? false;
},
},
});
@@ -0,0 +1,193 @@
<template>
<b-navbar data-cy="navbar" toggleable="md" variant="dark" data-bs-theme="dark">
<b-navbar-brand class="logo" to="/">
<span class="logo-img"></span>
<span class="navbar-title">{{ t$('global.title') }}</span> <span class="navbar-version">{{ version }}</span>
</b-navbar-brand>
<b-navbar-toggle
right
class="jh-navbar-toggler d-lg-none"
data-toggle="collapse"
target="header-tabs"
aria-expanded="false"
aria-label="Toggle navigation"
>
<font-awesome-icon icon="bars" />
</b-navbar-toggle>
<b-collapse is-nav id="header-tabs">
<b-navbar-nav class="ms-auto">
<b-nav-item to="/" exact>
<span>
<font-awesome-icon icon="fa-solid fa-home" />
<span>{{ t$('global.menu.home') }}</span>
</span>
</b-nav-item>
<b-nav-item-dropdown
:no-size="true"
end
id="entity-menu"
v-if="authenticated"
active-class="active"
class="pointer"
data-cy="entity"
>
<template #button-content>
<span class="navbar-dropdown-menu">
<font-awesome-icon icon="th-list" />
<span class="no-bold">{{ t$('global.menu.entities.main') }}</span>
</span>
</template>
<entities-menu></entities-menu>
<!-- jhipster-needle-add-entity-to-menu - JHipster will add entities to the menu here -->
</b-nav-item-dropdown>
<b-nav-item-dropdown
right
id="admin-menu"
v-if="hasAnyAuthority('ROLE_ADMIN') && authenticated"
:class="{ 'router-link-active': subIsActive('/admin') }"
active-class="active"
class="pointer"
data-cy="adminMenu"
>
<template #button-content>
<span class="navbar-dropdown-menu">
<font-awesome-icon icon="users-cog" />
<span class="no-bold">{{ t$('global.menu.admin.main') }}</span>
</span>
</template>
<b-dropdown-item to="/admin/user-management" active-class="active">
<font-awesome-icon icon="users" />
<span>{{ t$('global.menu.admin.userManagement') }}</span>
</b-dropdown-item>
<b-dropdown-item to="/admin/metrics" active-class="active">
<font-awesome-icon icon="tachometer-alt" />
<span>{{ t$('global.menu.admin.metrics') }}</span>
</b-dropdown-item>
<b-dropdown-item to="/admin/health" active-class="active">
<font-awesome-icon icon="heart" />
<span>{{ t$('global.menu.admin.health') }}</span>
</b-dropdown-item>
<b-dropdown-item to="/admin/configuration" active-class="active">
<font-awesome-icon icon="cogs" />
<span>{{ t$('global.menu.admin.configuration') }}</span>
</b-dropdown-item>
<b-dropdown-item to="/admin/logs" active-class="active">
<font-awesome-icon icon="tasks" />
<span>{{ t$('global.menu.admin.logs') }}</span>
</b-dropdown-item>
<b-dropdown-item v-if="openAPIEnabled" to="/admin/docs" active-class="active">
<font-awesome-icon icon="book" />
<span>{{ t$('global.menu.admin.apidocs') }}</span>
</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item-dropdown id="languagesnavBarDropdown" end v-if="languages && Object.keys(languages).length > 1">
<template #button-content>
<font-awesome-icon icon="flag" />
<span class="no-bold">{{ t$('global.menu.language') }}</span>
</template>
<b-dropdown-item
v-for="(value, key) in languages"
:key="`lang-${key}`"
@click="changeLanguage(key)"
:class="{ active: isActiveLanguage(key) }"
>
{{ value.name }}
</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item-dropdown
right
id="account-menu"
:class="{ 'router-link-active': subIsActive('/account') }"
active-class="active"
class="pointer"
data-cy="accountMenu"
>
<template #button-content>
<span class="navbar-dropdown-menu">
<font-awesome-icon icon="user" />
<span class="no-bold">{{ t$('global.menu.account.main') }}</span>
</span>
</template>
<b-dropdown-item data-cy="settings" to="/account/settings" v-if="authenticated" active-class="active">
<font-awesome-icon icon="wrench" />
<span>{{ t$('global.menu.account.settings') }}</span>
</b-dropdown-item>
<b-dropdown-item data-cy="passwordItem" to="/account/password" v-if="authenticated" active-class="active">
<font-awesome-icon icon="lock" />
<span>{{ t$('global.menu.account.password') }}</span>
</b-dropdown-item>
<b-dropdown-item data-cy="logout" v-if="authenticated" @click="logout()" id="logout" active-class="active">
<font-awesome-icon icon="sign-out-alt" />
<span>{{ t$('global.menu.account.logout') }}</span>
</b-dropdown-item>
<b-dropdown-item data-cy="login" v-if="!authenticated" @click="showLogin()" id="login" active-class="active">
<font-awesome-icon icon="sign-in-alt" />
<span>{{ t$('global.menu.account.login') }}</span>
</b-dropdown-item>
<b-dropdown-item data-cy="register" to="/register" id="register" v-if="!authenticated" active-class="active">
<font-awesome-icon icon="user-plus" />
<span>{{ t$('global.menu.account.register') }}</span>
</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</template>
<script lang="ts" src="./jhi-navbar.component.ts"></script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* ==========================================================================
Navbar
========================================================================== */
.navbar-version {
font-size: 0.65em;
color: #ccc;
}
.navbar .navbar-nav .nav-item {
margin-right: 0.5rem;
}
@media screen and (min-width: 768px) {
.jh-navbar-toggler {
display: none;
}
}
@media screen and (min-width: 768px) and (max-width: 1150px) {
span span {
display: none;
}
}
.navbar-title {
display: inline-block;
color: white;
}
/* ==========================================================================
Logo styles
========================================================================== */
.navbar-brand.logo {
padding: 0 7px;
}
.logo .logo-img {
height: 45px;
display: inline-block;
vertical-align: middle;
width: 45px;
}
.logo-img {
height: 100%;
background: url('/content/images/logo-jhipster.png') no-repeat center center;
background-size: contain;
width: 100%;
filter: drop-shadow(0 0 0.05rem white);
margin: 0 5px;
}
</style>
@@ -0,0 +1,47 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { createTestingPinia } from '@pinia/testing';
import { shallowMount } from '@vue/test-utils';
import { type AccountStore, useStore } from '@/store';
import Ribbon from './ribbon.vue';
type RibbonComponentType = InstanceType<typeof Ribbon>;
const pinia = createTestingPinia({ stubActions: false });
describe('Ribbon', () => {
let ribbon: RibbonComponentType;
let store: AccountStore;
beforeEach(async () => {
const wrapper = shallowMount(Ribbon, {
global: {
plugins: [pinia],
},
});
ribbon = wrapper.vm;
await ribbon.$nextTick();
store = useStore();
store.setRibbonOnProfiles(null);
});
it('should not have ribbonEnabled when no data', () => {
expect(ribbon.ribbonEnabled).toBeFalsy();
});
it('should have ribbonEnabled set to value in store', () => {
const profile = 'dev';
store.setActiveProfiles(['foo', profile, 'bar']);
store.setRibbonOnProfiles(profile);
expect(ribbon.ribbonEnabled).toBeTruthy();
});
it('should not have ribbonEnabled when profile not activated', () => {
const profile = 'dev';
store.setActiveProfiles(['foo', 'bar']);
store.setRibbonOnProfiles(profile);
expect(ribbon.ribbonEnabled).toBeFalsy();
});
});
@@ -0,0 +1,19 @@
import { computed, defineComponent } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from '@/store';
export default defineComponent({
name: 'Ribbon',
setup() {
const store = useStore();
const ribbonEnv = computed(() => store.ribbonOnProfiles);
const ribbonEnabled = computed(() => store.ribbonOnProfiles && store.activeProfiles.indexOf(store.ribbonOnProfiles) > -1);
return {
ribbonEnv,
ribbonEnabled,
t$: useI18n().t,
};
},
});
@@ -0,0 +1,43 @@
<template>
<div class="ribbon" v-if="ribbonEnabled">
<a href="">{{ t$('global.ribbon.' + ribbonEnv) }}</a>
</div>
</template>
<script lang="ts" src="./ribbon.component.ts"></script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* ==========================================================================
Development Ribbon
========================================================================== */
.ribbon {
background-color: rgba(170, 0, 0, 0.5);
left: -3.5em;
-moz-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
overflow: hidden;
position: absolute;
top: 40px;
white-space: nowrap;
width: 15em;
z-index: 9999;
pointer-events: none;
opacity: 0.75;
}
.ribbon a {
color: #fff;
display: block;
font-weight: 400;
margin: 1px 0;
padding: 10px 50px;
text-align: center;
text-decoration: none;
text-shadow: 0 0 5px #444;
pointer-events: none;
}
</style>