diff --git a/src/main/style/atom/button/_button.scss b/src/main/style/atom/button/_button.scss index f7ef2456424..8ee9f107163 100644 --- a/src/main/style/atom/button/_button.scss +++ b/src/main/style/atom/button/_button.scss @@ -1,2 +1,3 @@ @use 'button-main/button-main'; @use 'button-switch/button-switch'; +@use 'button-toggle/button-toggle'; diff --git a/src/main/style/atom/button/button-toggle/_button-toggle.scss b/src/main/style/atom/button/button-toggle/_button-toggle.scss new file mode 100644 index 00000000000..886b2c0df49 --- /dev/null +++ b/src/main/style/atom/button/button-toggle/_button-toggle.scss @@ -0,0 +1,42 @@ +@use 'sass:math'; +@use '../../../token/colors' as colors; +@use '../../../token/color/brand' as brand; +@use '../button-main/button-main' as button-main; + +$jhlite-button-toggle-padding: math.div(5, 16) * 1rem math.div(15, 16) * 1rem; +$jhlite-button-toggle-border: 1px solid brand.$jhlite-color-brand-600; +$jhlite-button-toggle-font-size: math.div(13, 16) * 1rem; +$jhlite-button-toggle-hover-color-background: rgba(button-main.$jhlite-button-color-background, 0.1); +$jhlite-button-toggle-disabled-color-background: button-main.$jhlite-button-disabled-color-background; +$jhlite-button-toggle-active-color-background: button-main.$jhlite-button-color-background; +$jhlite-button-toggle-active-hover-color-background: button-main.$jhlite-button-hover-color-background; + +.jhlite-button-toggle { + @extend %jhlite-button-main; + + border: $jhlite-button-toggle-border; + background-color: transparent; + padding: $jhlite-button-toggle-padding; + text-transform: none; + color: var(--jhlite-global-color-text); + font-size: $jhlite-button-toggle-font-size; + + &:hover { + background-color: $jhlite-button-toggle-hover-color-background; + } + + &:disabled { + &:hover { + background-color: $jhlite-button-toggle-disabled-color-background; + } + } + + &.-active { + background-color: $jhlite-button-toggle-active-color-background; + color: colors.$jhlite-global-color-text-light; + + &:hover { + background-color: $jhlite-button-toggle-active-hover-color-background; + } + } +} diff --git a/src/main/style/atom/button/button-toggle/button-toggle.code.pug b/src/main/style/atom/button/button-toggle/button-toggle.code.pug new file mode 100644 index 00000000000..19a1b78f671 --- /dev/null +++ b/src/main/style/atom/button/button-toggle/button-toggle.code.pug @@ -0,0 +1,5 @@ +include button-toggle.mixin.pug + ++jhlite-button-toggle Toggle ++jhlite-button-toggle({active: true}) Toggle active ++jhlite-button-toggle({disabled: true}) Toggle disabled diff --git a/src/main/style/atom/button/button-toggle/button-toggle.md b/src/main/style/atom/button/button-toggle/button-toggle.md new file mode 100644 index 00000000000..e9f22293c67 --- /dev/null +++ b/src/main/style/atom/button/button-toggle/button-toggle.md @@ -0,0 +1 @@ +### Button toggle diff --git a/src/main/style/atom/button/button-toggle/button-toggle.mixin.pug b/src/main/style/atom/button/button-toggle/button-toggle.mixin.pug new file mode 100644 index 00000000000..152208ce93f --- /dev/null +++ b/src/main/style/atom/button/button-toggle/button-toggle.mixin.pug @@ -0,0 +1,5 @@ +mixin jhlite-button-toggle(opts) + - const { active, disabled } = opts || {}; + - const activeClass = active ? '-active' : null; + button.jhlite-button-toggle(class=activeClass, disabled=disabled) + block diff --git a/src/main/style/atom/button/button-toggle/button-toggle.render.pug b/src/main/style/atom/button/button-toggle/button-toggle.render.pug new file mode 100644 index 00000000000..2b53d2a1636 --- /dev/null +++ b/src/main/style/atom/button/button-toggle/button-toggle.render.pug @@ -0,0 +1,4 @@ +extends /layout + +block body + include button-toggle.code.pug diff --git a/src/main/style/atom/button/button.pug b/src/main/style/atom/button/button.pug index 0b246723d39..e6e7beba2a1 100644 --- a/src/main/style/atom/button/button.pug +++ b/src/main/style/atom/button/button.pug @@ -5,3 +5,5 @@ include:componentDoc(height=150) button-main/button-main.md .tikui-vertical-spacing--line include:componentDoc(height=150) button-switch/button-switch.md + .tikui-vertical-spacing--line + include:componentDoc(height=150) button-toggle/button-toggle.md diff --git a/src/main/style/organism/_organism.scss b/src/main/style/organism/_organism.scss index 99a46441d9c..b5c61cb5a6f 100644 --- a/src/main/style/organism/_organism.scss +++ b/src/main/style/organism/_organism.scss @@ -9,3 +9,4 @@ @use 'landscape-loader/landscape-loader'; @use 'landscape-minimap/landscape-minimap'; @use 'landscape-preset-configuration/landscape-preset-configuration'; +@use 'landscape-rank-module-filter/landscape-rank-module-filter'; diff --git a/src/main/style/organism/landscape-rank-module-filter/_landscape-rank-module-filter.scss b/src/main/style/organism/landscape-rank-module-filter/_landscape-rank-module-filter.scss new file mode 100644 index 00000000000..ec9600ebd4d --- /dev/null +++ b/src/main/style/organism/landscape-rank-module-filter/_landscape-rank-module-filter.scss @@ -0,0 +1,9 @@ +@use '../../token/size'; + +.jhlite-landscape-rank-module-filter { + &--ranks { + display: flex; + gap: size.$jhlite-global-size-field-padding; + align-items: center; + } +} diff --git a/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.code.pug b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.code.pug new file mode 100644 index 00000000000..8e1b90b5c0e --- /dev/null +++ b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.code.pug @@ -0,0 +1,3 @@ +include landscape-rank-module-filter.mixin.pug + ++jhlite-landscape-rank-module-filter diff --git a/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.md b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.md new file mode 100644 index 00000000000..3474467def7 --- /dev/null +++ b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.md @@ -0,0 +1 @@ +## Landscape rank module filter diff --git a/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.mixin.pug b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.mixin.pug new file mode 100644 index 00000000000..973b52c00f0 --- /dev/null +++ b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.mixin.pug @@ -0,0 +1,10 @@ +include /atom/button/button-toggle/button-toggle.mixin.pug + +mixin jhlite-landscape-rank-module-filter + .jhlite-landscape-rank-module-filter + .jhlite-landscape-rank-module-filter--ranks + +jhlite-button-toggle D + +jhlite-button-toggle({ disabled: true }) C + +jhlite-button-toggle({ disabled: true }) B + +jhlite-button-toggle({ disabled: true }) A + +jhlite-button-toggle({ active: true }) S diff --git a/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.render.pug b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.render.pug new file mode 100644 index 00000000000..f944ccb27d7 --- /dev/null +++ b/src/main/style/organism/landscape-rank-module-filter/landscape-rank-module-filter.render.pug @@ -0,0 +1,4 @@ +extends /layout + +block body + include landscape-rank-module-filter.code.pug diff --git a/src/main/style/organism/landscape/_landscape.scss b/src/main/style/organism/landscape/_landscape.scss index 7993f9cee17..9e720a524a3 100644 --- a/src/main/style/organism/landscape/_landscape.scss +++ b/src/main/style/organism/landscape/_landscape.scss @@ -3,7 +3,10 @@ @use '../../token/size'; $jhlite-landscape-padding: 20px; +$jhipster-landscape-modes-selection-top-position: $jhlite-landscape-padding; $jhipster-landscape-preset-selection-top-position: 85px; +$jhipster-landscape-modes-selection-top-position-large-screen: 85px; +$jhipster-landscape-preset-selection-top-position-large-screen: 150px; $jhipster-landscape-preset-selection-left-position: $jhlite-landscape-padding; $jhlite-landscape-line-color: colors.$jhlite-global-line-color; $jhlite-landscape-box-radius: colors.$jhlite-global-box-radius; @@ -23,7 +26,7 @@ $jhlite-landscape-primary-alternative-color: colors.$jhlite-global-primary-alter .jhipster-landscape-modes-selection { position: absolute; - top: $jhlite-landscape-padding; + top: $jhipster-landscape-modes-selection-top-position; left: $jhlite-landscape-padding; z-index: 3; border: 1px dotted $jhlite-landscape-line-color; @@ -40,6 +43,13 @@ $jhlite-landscape-primary-alternative-color: colors.$jhlite-global-primary-alter background: var(--jhlite-chip-bg-color); } + .jhipster-landscape-rank-module-selection { + display: flex; + justify-content: center; + margin-left: $jhlite-landscape-padding; + background: var(--jhlite-chip-bg-color); + } + .jhipster-landscape-content { &--header { display: none; @@ -184,6 +194,11 @@ $jhlite-landscape-primary-alternative-color: colors.$jhlite-global-primary-alter border-radius: size.$jhlite-global-size-field-radius; box-shadow: 0 0 0 size.$jhlite-global-size-field-border-focus colors.$jhlite-attention-highlight-color; } + + .-diff-rank-minimal-emphasis { + opacity: 0.5; + border: 1px dotted $jhlite-landscape-line-color; + } } .jhipster-landscape { @@ -192,6 +207,14 @@ $jhlite-landscape-primary-alternative-color: colors.$jhlite-global-primary-alter @media screen and (min-width: breakpoint.$jhlite-global-breakpoint-small-medium) { .jhipster-landscape { + .jhipster-landscape-modes-selection { + top: $jhipster-landscape-modes-selection-top-position-large-screen; + } + + .jhipster-landscape-preset-selection { + top: $jhipster-landscape-preset-selection-top-position-large-screen; + } + .jhipster-landscape-content { display: flex; flex-direction: column; diff --git a/src/main/style/organism/organism.pug b/src/main/style/organism/organism.pug index b772faf30ea..9648434611b 100644 --- a/src/main/style/organism/organism.pug +++ b/src/main/style/organism/organism.pug @@ -22,6 +22,8 @@ block content include:componentDoc(height=160) landscape-minimap/landscape-minimap.md .tikui-vertical-spacing--line include:componentDoc(height=160) landscape-preset-configuration/landscape-preset-configuration.md + .tikui-vertical-spacing--line + include:componentDoc(height=160) landscape-rank-module-filter/landscape-rank-module-filter.md .tikui-vertical-spacing--line include:componentDoc(height=230) module-parameters/module-parameters.md .tikui-vertical-spacing--line diff --git a/src/main/webapp/app/module/domain/ModuleRankCount.ts b/src/main/webapp/app/module/domain/ModuleRankCount.ts new file mode 100644 index 00000000000..c7b224fb984 --- /dev/null +++ b/src/main/webapp/app/module/domain/ModuleRankCount.ts @@ -0,0 +1,8 @@ +import { ModuleRank } from '@/module/domain/landscape/ModuleRank'; + +type ModuleRankCountQuantity = number; + +export type ModuleRankCount = { + rank: ModuleRank; + quantity: ModuleRankCountQuantity; +}; diff --git a/src/main/webapp/app/module/domain/ModuleRankStatistics.ts b/src/main/webapp/app/module/domain/ModuleRankStatistics.ts new file mode 100644 index 00000000000..84446b8f90b --- /dev/null +++ b/src/main/webapp/app/module/domain/ModuleRankStatistics.ts @@ -0,0 +1,25 @@ +import { ModuleRankCount } from '@/module/domain/ModuleRankCount'; +import { Landscape } from '@/module/domain/landscape/Landscape'; +import { ModuleRank, RANKS } from '@/module/domain/landscape/ModuleRank'; + +export type ModuleRankStatistics = ModuleRankCount[]; + +export const toModuleRankStatistics = (landscape: Landscape): ModuleRankStatistics => { + const rankCounts = landscape + .standaloneLevels() + .flatMap(level => level.elements) + .flatMap(element => element.allModules()) + .reduce( + (counts, module) => { + const currentCount = counts.get(module.rank())!; + counts.set(module.rank(), currentCount + 1); + return counts; + }, + new Map(RANKS.map(rank => [rank, 0])), + ); + + return RANKS.map(rank => ({ + rank, + quantity: rankCounts.get(rank)!, + })); +}; diff --git a/src/main/webapp/app/module/domain/RankDescription.ts b/src/main/webapp/app/module/domain/RankDescription.ts new file mode 100644 index 00000000000..5aedeecfca1 --- /dev/null +++ b/src/main/webapp/app/module/domain/RankDescription.ts @@ -0,0 +1,3 @@ +export type RankDescription = { + [key: string]: string; +}; diff --git a/src/main/webapp/app/module/domain/landscape/Landscape.ts b/src/main/webapp/app/module/domain/landscape/Landscape.ts index f06d90a2a9b..1f2dc01f215 100644 --- a/src/main/webapp/app/module/domain/landscape/Landscape.ts +++ b/src/main/webapp/app/module/domain/landscape/Landscape.ts @@ -1,3 +1,5 @@ +import { LandscapeElement } from '@/module/domain/landscape/LandscapeElement'; +import { ModuleRank } from '@/module/domain/landscape/ModuleRank'; import { Memoizer } from '@/shared/memoizer/domain/Memoizer'; import { Optional } from '@/shared/optional/domain/Optional'; import { ModulePropertyDefinition } from '../ModulePropertyDefinition'; @@ -404,6 +406,66 @@ export class Landscape { selectedModulesProperties(): ModulePropertyDefinition[] { return this.properties; } + + public hasModuleDifferentRank(module: ModuleSlug, rank: ModuleRank): boolean { + return this.getModule(module) + .map(currentModule => currentModule.rank() !== rank) + .orElse(false); + } + + public filterByRank(rank: Optional): Landscape { + return rank.map(currentRank => this.createFilteredLandscape(currentRank)).orElse(this); + } + + private createFilteredLandscape(rank: ModuleRank): Landscape { + const filteredLevels = this.projections.levels.map(level => this.filterLevel(level, rank)).filter(level => level.elements.length > 0); + + return new Landscape(this.state, new LevelsProjections(filteredLevels)); + } + + private filterLevel(level: LandscapeLevel, rank: ModuleRank): { elements: LandscapeElement[] } { + return { + elements: level.elements.map(element => this.filterElementByRank(element, rank)).flatMap(optional => optional.toArray()), + }; + } + + private filterElementByRank(element: LandscapeElement, rank: ModuleRank): Optional { + return element instanceof LandscapeFeature ? this.filterFeature(element, rank) : this.filterModule(element, rank); + } + + private filterFeature(feature: LandscapeFeature, rank: ModuleRank): Optional { + if (this.dependencyFeatureOfRankedModule(feature.slug(), rank)) { + return Optional.of(feature); + } + + return Optional.of(feature.modules) + .filter(modules => modules.some(module => this.moduleMatchingRank(module, rank))) + .map(modules => modules.filter(module => this.moduleMatchingRank(module, rank))) + .map(filteredModules => new LandscapeFeature(feature.slug(), filteredModules)); + } + + private dependencyFeatureOfRankedModule(featureSlug: LandscapeFeatureSlug, rank: ModuleRank): boolean { + return Array.from(this.modules.values()) + .filter(module => module.rank() === rank) + .some(rankedModule => rankedModule.dependencies().some(dep => dep.get() === featureSlug.get())); + } + + private moduleMatchingRank(module: LandscapeModule, rank: ModuleRank): boolean { + return module.rank() === rank || this.dependencyOfRankedModule(module, rank); + } + + private dependencyOfRankedModule(module: LandscapeModule, rank: ModuleRank): boolean { + return Optional.of(Array.from(this.modules.values())) + .map(modules => modules.filter(m => m.rank() === rank)) + .map(rankedModules => rankedModules.some(rankedModule => rankedModule.dependencies().some(dep => dep.get() === module.slug().get()))) + .orElse(false); + } + + private filterModule(element: LandscapeElement, rank: ModuleRank): Optional { + return Optional.of(element.allModules()[0]) + .filter(module => this.moduleMatchingRank(module, rank)) + .map(() => element); + } } class LandscapeState { diff --git a/src/main/webapp/app/module/domain/landscape/LandscapeModule.ts b/src/main/webapp/app/module/domain/landscape/LandscapeModule.ts index 1795cc8f6d0..0f52f6fd199 100644 --- a/src/main/webapp/app/module/domain/landscape/LandscapeModule.ts +++ b/src/main/webapp/app/module/domain/landscape/LandscapeModule.ts @@ -1,3 +1,4 @@ +import { ModuleRank } from '@/module/domain/landscape/ModuleRank'; import { ModulePropertyDefinition } from '../ModulePropertyDefinition'; import { ModuleSlug } from '../ModuleSlug'; import { LandscapeElement } from './LandscapeElement'; @@ -11,6 +12,7 @@ export interface LandscapeModuleInformation { operation: ModuleOperation; properties: ModulePropertyDefinition[]; dependencies: LandscapeElementId[]; + rank: ModuleRank; } export interface LandscapeModuleContext { @@ -49,6 +51,10 @@ export class LandscapeModule implements LandscapeElement { return this.information.dependencies; } + rank(): ModuleRank { + return this.information.rank; + } + operation(): string { return this.information.operation; } diff --git a/src/main/webapp/app/module/domain/landscape/ModuleRank.ts b/src/main/webapp/app/module/domain/landscape/ModuleRank.ts new file mode 100644 index 00000000000..749b4638ec1 --- /dev/null +++ b/src/main/webapp/app/module/domain/landscape/ModuleRank.ts @@ -0,0 +1,2 @@ +export const RANKS = ['RANK_D', 'RANK_C', 'RANK_B', 'RANK_A', 'RANK_S'] as const; +export type ModuleRank = (typeof RANKS)[number]; diff --git a/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.component.ts b/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.component.ts new file mode 100644 index 00000000000..262e2ffd922 --- /dev/null +++ b/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.component.ts @@ -0,0 +1,54 @@ +import type { ModuleRank } from '@/module/domain/landscape/ModuleRank'; +import { RANKS } from '@/module/domain/landscape/ModuleRank'; +import type { ModuleRankStatistics } from '@/module/domain/ModuleRankStatistics'; +import type { RankDescription } from '@/module/domain/RankDescription'; +import { PropType, defineComponent, ref } from 'vue'; + +export default defineComponent({ + name: 'LandscapeRankModuleFilterVue', + props: { + moduleRankStatistics: { + type: Array as PropType, + required: true, + }, + }, + emits: ['selected'], + setup(props, { emit }) { + const ranks = RANKS; + const selectedRank = ref(undefined); + + const rankDescriptions: RankDescription = { + RANK_D: 'Experimental or advanced module requiring specific expertise', + RANK_C: 'Module without known production usage', + RANK_B: 'Module with at least one confirmed production usage', + RANK_A: 'Module with multiple production usages across different projects and documented through talks, books or blog posts', + RANK_S: 'Production-proven module providing unique features, validated by community feedback (10+ endorsements)', + }; + + const isRankSelected = (rank: ModuleRank): boolean => selectedRank.value === rank; + + const toggleRank = (rank: ModuleRank): void => { + if (selectedRank.value === rank) { + selectedRank.value = undefined; + } else { + selectedRank.value = rank; + } + emit('selected', selectedRank.value); + }; + + const formatRank = (rank: ModuleRank): string => rank.replace('RANK_', ''); + + const getRankDescription = (rank: ModuleRank): string => rankDescriptions[rank]; + + const isRankDisabled = (rank: ModuleRank): boolean => props.moduleRankStatistics.find(ru => ru.rank === rank)?.quantity === 0; + + return { + ranks, + isRankSelected, + toggleRank, + formatRank, + getRankDescription, + isRankDisabled, + }; + }, +}); diff --git a/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.html b/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.html new file mode 100644 index 00000000000..259a3fd9abc --- /dev/null +++ b/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.html @@ -0,0 +1,16 @@ +
+
+ +
+
diff --git a/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.vue b/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.vue new file mode 100644 index 00000000000..2bb55a48920 --- /dev/null +++ b/src/main/webapp/app/module/primary/landscape-rank-module-filter/LandscapeRankModuleFilter.vue @@ -0,0 +1,3 @@ + + + diff --git a/src/main/webapp/app/module/primary/landscape-rank-module-filter/index.ts b/src/main/webapp/app/module/primary/landscape-rank-module-filter/index.ts new file mode 100644 index 00000000000..55a0d7b7b2e --- /dev/null +++ b/src/main/webapp/app/module/primary/landscape-rank-module-filter/index.ts @@ -0,0 +1,3 @@ +import LandscapeRankModuleFilterVue from './LandscapeRankModuleFilter.vue'; + +export { LandscapeRankModuleFilterVue }; diff --git a/src/main/webapp/app/module/primary/landscape/Landscape.component.ts b/src/main/webapp/app/module/primary/landscape/Landscape.component.ts index 48b13eaa701..1ef1b32b71d 100644 --- a/src/main/webapp/app/module/primary/landscape/Landscape.component.ts +++ b/src/main/webapp/app/module/primary/landscape/Landscape.component.ts @@ -8,6 +8,8 @@ import { import { AnchorPointState } from '@/module/domain/AnchorPointState'; import { ModuleParameter } from '@/module/domain/ModuleParameter'; import { ModulePropertyDefinition } from '@/module/domain/ModulePropertyDefinition'; +import type { ModuleRankStatistics } from '@/module/domain/ModuleRankStatistics'; +import { toModuleRankStatistics } from '@/module/domain/ModuleRankStatistics'; import { ModuleSlug } from '@/module/domain/ModuleSlug'; import { Preset } from '@/module/domain/Preset'; import { ProjectHistory } from '@/module/domain/ProjectHistory'; @@ -19,6 +21,8 @@ import { LandscapeFeatureSlug } from '@/module/domain/landscape/LandscapeFeature import { LandscapeLevel } from '@/module/domain/landscape/LandscapeLevel'; import { LandscapeModule } from '@/module/domain/landscape/LandscapeModule'; import { LandscapeSelectionElement } from '@/module/domain/landscape/LandscapeSelectionElement'; +import { ModuleRank } from '@/module/domain/landscape/ModuleRank'; +import { LandscapeRankModuleFilterVue } from '@/module/primary/landscape-rank-module-filter'; import { ALERT_BUS } from '@/shared/alert/application/AlertProvider'; import { IconVue } from '@/shared/icon/infrastructure/primary'; import { Loader } from '@/shared/loader/infrastructure/primary/Loader'; @@ -46,6 +50,7 @@ export default defineComponent({ LandscapeLoaderVue, LandscapeMiniMapVue, LandscapePresetConfigurationVue, + LandscapeRankModuleFilterVue, }, setup() { const applicationListener = inject(APPLICATION_LISTENER); @@ -58,6 +63,7 @@ export default defineComponent({ const selectedMode = ref('COMPACTED'); const landscape = ref(Loader.loading()); + const originalLandscape = ref(Loader.loading()); const levels = ref(Loader.loading()); const canLoadMiniMap = ref(false); @@ -90,6 +96,9 @@ export default defineComponent({ const highlightedModule = ref>(Optional.empty()); + const selectedRank = ref>(Optional.empty()); + const moduleRankStatistics = ref([]); + onMounted(() => { modules .landscape() @@ -142,6 +151,8 @@ export default defineComponent({ }; const loadLandscape = async (response: Landscape): Promise => { + originalLandscape.value.loaded(response); + landscape.value.loaded(response); levels.value.loaded(response.standaloneLevels()); @@ -152,6 +163,7 @@ export default defineComponent({ canLoadMiniMap.value = true; loadAnchorPointModulesMap(); + loadLandscapeRankModuleFilterProperty(); }; const loadAnchorPointModulesMap = (): void => { @@ -178,6 +190,10 @@ export default defineComponent({ }); }; + const loadLandscapeRankModuleFilterProperty = (): void => { + moduleRankStatistics.value = toModuleRankStatistics(landscapeValue()); + }; + type Navigation = 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown' | 'Space'; type NavigationAction = { [key in Navigation]: keyof LandscapeNavigation; @@ -311,6 +327,7 @@ export default defineComponent({ + flavorClass() + anchorPointClass(module) + searchHighlightClass(module) + + diffRankMinimalEmphasisClass(module) ); }; @@ -391,6 +408,17 @@ export default defineComponent({ .orElse(''); }; + const diffRankMinimalEmphasisClass = (module: LandscapeElementId): string => { + if (module instanceof LandscapeFeatureSlug) { + return ''; + } + + return selectedRank.value + .map(rank => landscapeValue().hasModuleDifferentRank(module, rank)) + .map(hasDifferentRank => (hasDifferentRank ? ' -diff-rank-minimal-emphasis' : '')) + .orElse(''); + }; + const modeClass = (): string => { switch (selectedMode.value) { case 'COMPACTED': @@ -620,6 +648,30 @@ export default defineComponent({ } }; + const handleRankFilter = (rank: ModuleRank | undefined): void => { + clearPresetSelection(); + + selectedRank.value = Optional.ofNullable(rank); + void reloadLandscape(originalLandscape.value.value().filterByRank(selectedRank.value)); + }; + + const reloadLandscape = async (response: Landscape): Promise => { + landscape.value.loaded(response); + levels.value.loaded(response.standaloneLevels()); + + await rebuildLandscapeElements(); + + await nextTick().then(updateConnectors); + landscapeNavigation.value.loaded(new LandscapeNavigation(landscapeElements.value, levels.value.value())); + loadAnchorPointModulesMap(); + }; + + const rebuildLandscapeElements = async (): Promise => { + // Wait for DOM update before clearing and rebuilding refs + await nextTick(); + landscapeElements.value = new Map(); + }; + return { levels, isFeature, @@ -662,6 +714,8 @@ export default defineComponent({ canLoadMiniMap, selectedPresetName, performSearch, + handleRankFilter, + moduleRankStatistics, }; }, }); diff --git a/src/main/webapp/app/module/primary/landscape/Landscape.html b/src/main/webapp/app/module/primary/landscape/Landscape.html index 7a26fd98163..f243cfaaef0 100644 --- a/src/main/webapp/app/module/primary/landscape/Landscape.html +++ b/src/main/webapp/app/module/primary/landscape/Landscape.html @@ -28,6 +28,9 @@
+
+ +