Skip to content
Open
20 changes: 19 additions & 1 deletion dashboard/src/components/skills/SkillPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useProjConfig } from '@/stores/UseProjConfig.js'
import ShowMore from '@/components/skills/selfReport/ShowMore.vue'
import EditSkill from '@/components/skills/EditSkill.vue'
import { useAppConfig } from '@/common-components/stores/UseAppConfig.js'
import SkillNavigation from "@/skills-display/components/utilities/SkillNavigation.vue";

const route = useRoute()
const router = useRouter()
Expand Down Expand Up @@ -158,10 +159,26 @@ const buildHeaderOptions = () => {
const skillId = computed(() => {
return skillsState.skill ? `ID: ${SkillReuseIdUtil.removeTag(skillsState.skill.skillId)}` : 'Loading...'
})

const prevButtonClicked = () => {
const params = { skillId: skillsState.skill.prevSkillId, projectId: route.params.projectId }
router.push({ name: route.name, params: params })
}

const nextButtonClicked = () => {
const params = { skillId: skillsState.skill.nextSkillId, projectId: route.params.projectId }
router.push({ name: route.name, params: params })
}

</script>

<template>
<div>
<div class="mt-2">
<Card class="p-2" :pt="{ body: { class: 'p-0!' } }" v-if="skillsState.skill && (skillsState.skill.prevSkillId || skillsState.skill.nextSkillId)" >
<template #content>
<skill-navigation @prevButtonClicked="prevButtonClicked" @nextButtonClicked="nextButtonClicked" :skill="skillsState.skill" buttonSeverity="info" />
</template>
</Card>
<page-header :loading="isLoading" :options="headerOptions">
<template #subTitle v-if="skillsState.skill">
<div v-for="(tag) in skillsState.skill.tags" :key="tag.tagId" class="h6 mr-2 d-inline-block"
Expand Down Expand Up @@ -207,6 +224,7 @@ const skillId = computed(() => {
class="fas fa-eye-slash mr-1" aria-hidden="true"></i> DISABLED</Tag>
</template>
</page-header>

<navigation :nav-items="navItems">
</navigation>

Expand Down
68 changes: 2 additions & 66 deletions dashboard/src/skills-display/components/skill/SkillPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,14 @@ import {useSkillsDisplayAttributesState} from '@/skills-display/stores/UseSkills
const Prerequisites = defineAsyncComponent(() => import('@/skills-display/components/skill/prerequisites/Prerequisites.vue'))
import SkillAchievementMsg from "@/skills-display/components/progress/celebration/SkillAchievementMsg.vue";
import MarkdownText from "@/common-components/utilities/markdown/MarkdownText.vue";
import {useMagicKeys, watchDebounced} from "@vueuse/core";
import {useUserPreferences} from "@/stores/UseUserPreferences.js";
import {useLog} from "@/components/utils/misc/useLog.js";
import SkillNavigation from "@/skills-display/components/utilities/SkillNavigation.vue";

const attributes = useSkillsDisplayAttributesState()
const skillsDisplayService = useSkillsDisplayService()
const skillsDisplayInfo = useSkillsDisplayInfo()
const scrollIntoViewState = useScrollSkillsIntoViewState()
const route = useRoute()
const skillState = useSkillsDisplaySubjectState()
const keys = useMagicKeys()
const userPreferences = useUserPreferences()
const log = useLog()
const skill = computed(() => skillState.skillSummary)
const loadingSkill = ref(true)
const displayGroupDescription = ref(false);
Expand Down Expand Up @@ -70,34 +65,6 @@ const loadSkillSummary = () => {
}
})
}
const nextButtonShortcut = ref('Ctrl+Alt+N')
const previousButtonShortcut = ref('Ctrl+Alt+P')
userPreferences.afterUserPreferencesLoaded().then((options) => {
const debounceOptions = { debounce: 250, maxWait: 1000 }
if (options.sd_next_skill_keyboard_shortcut) {
nextButtonShortcut.value = options.sd_next_skill_keyboard_shortcut?.toLowerCase().replace(/ /g, '')
}
if (options.sd_previous_skill_keyboard_shortcut) {
previousButtonShortcut.value = options.sd_previous_skill_keyboard_shortcut?.toLowerCase().replace(/ /g, '')
}

log.debug(`Next shortcut is : ${nextButtonShortcut.value}`)
log.debug(`Previous shortcut is : ${previousButtonShortcut.value}`)
watchDebounced(
keys[nextButtonShortcut.value],
() => {
nextButtonClicked()
},
debounceOptions
)
watchDebounced(
keys[previousButtonShortcut.value],
() => {
prevButtonClicked()
},
debounceOptions
)
})

const prevButtonClicked = () => {
const params = { skillId: skillState.skillSummary.prevSkillId, projectId: route.params.projectId }
Expand Down Expand Up @@ -132,38 +99,7 @@ const descriptionToggled = () => {
<skills-title>{{ attributes.skillDisplayName }} Overview</skills-title>
<Card class="mt-4" :pt="{ content: { class: 'p-0' }}">
<template #content>
<div class="flex-col sm:flex-row items-center flex gap-2 mb-6" v-if="skill && (skill.prevSkillId || skill.nextSkillId) && !skillsDisplayInfo.isCrossProject()">
<div class="w-28">
<SkillsButton
@click="prevButtonClicked" v-if="skill.prevSkillId"
outlined
size="small"
:title="`Previous ${attributes.skillDisplayName} (${previousButtonShortcut})`"
class="skills-theme-btn"
data-cy="prevSkill"
aria-label="previous skill">
<i class="fas fa-arrow-alt-circle-left mr-1" aria-hidden="true"></i> Previous
</SkillsButton>
</div>
<div class="flex-1 text-center " style="font-size: 0.9rem;" data-cy="skillOrder"><span
class="italic">{{ attributes.skillDisplayName }}</span> <span class="font-semibold">{{ skill.orderInGroup
}}</span> <span class="italic">of</span> <span class="font-semibold">{{ skill.totalSkills }}</span>
</div>
<div class="w-28 text-right">
<SkillsButton
@click="nextButtonClicked"
v-if="skill.nextSkillId"
class="skills-theme-btn"
data-cy="nextSkill"
outlined
size="small"
:title="`Next ${attributes.skillDisplayName} (${nextButtonShortcut})`"
aria-label="next skill">
Next
<i class="fas fa-arrow-alt-circle-right ml-1" aria-hidden="true"></i>
</SkillsButton>
</div>
</div>
<skill-navigation class="mb-6" :skill="skill" @prevButtonClicked="prevButtonClicked" @nextButtonClicked="nextButtonClicked" v-if="skill && (skill.prevSkillId || skill.nextSkillId) && !skillsDisplayInfo.isCrossProject()" />
<div v-if="!attributes.groupInfoOnSkillPage && skill.groupName" class="mt-4 p-1 mb-4" data-cy="groupInformationSection">
<div class="flex">
<div class="mr-2 mt-1 text-xl">
Expand Down
105 changes: 105 additions & 0 deletions dashboard/src/skills-display/components/utilities/SkillNavigation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
Copyright 2024 SkillTree

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
<script setup>
import {ref} from "vue";
import {useMagicKeys, watchDebounced} from "@vueuse/core";
import {useSkillsDisplayAttributesState} from "@/skills-display/stores/UseSkillsDisplayAttributesState.js";
import {useUserPreferences} from "@/stores/UseUserPreferences.js";
import {useLog} from "@/components/utils/misc/useLog.js";

const props = defineProps(['skill', 'buttonSeverity'])
const emit = defineEmits(['prevButtonClicked', 'nextButtonClicked'])
const attributes = useSkillsDisplayAttributesState()
const keys = useMagicKeys()
const userPreferences = useUserPreferences()
const log = useLog()

const nextButtonShortcut = ref('Ctrl+Alt+N')
const previousButtonShortcut = ref('Ctrl+Alt+P')
userPreferences.afterUserPreferencesLoaded().then((options) => {
const debounceOptions = { debounce: 250, maxWait: 1000 }
if (options.sd_next_skill_keyboard_shortcut) {
nextButtonShortcut.value = options.sd_next_skill_keyboard_shortcut?.toLowerCase().replace(/ /g, '')
}
if (options.sd_previous_skill_keyboard_shortcut) {
previousButtonShortcut.value = options.sd_previous_skill_keyboard_shortcut?.toLowerCase().replace(/ /g, '')
}

log.debug(`Next shortcut is : ${nextButtonShortcut.value}`)
log.debug(`Previous shortcut is : ${previousButtonShortcut.value}`)
watchDebounced(
keys[nextButtonShortcut.value],
() => {
nextButtonClicked()
},
debounceOptions
)
watchDebounced(
keys[previousButtonShortcut.value],
() => {
prevButtonClicked()
},
debounceOptions
)
})

const prevButtonClicked = () => {
emit('prevButtonClicked')
}
const nextButtonClicked = () => {
emit('nextButtonClicked')
}
</script>

<template>
<div class="flex-row sm:flex-row items-center flex gap-2 w-full" v-if="skill && (skill.prevSkillId || skill.nextSkillId)">
<div class="w-28">
<SkillsButton size="small"
outlined
id="prevSkillButton"
:title="`Previous ${attributes.skillDisplayName} (${previousButtonShortcut})`"
class="skills-theme-btn"
:severity="buttonSeverity"
data-cy="prevSkill"
@click="prevButtonClicked"
aria-label="previous skill"
v-if="skill.prevSkillId">
<i class="fas fa-arrow-alt-circle-left mr-1" aria-hidden="true"></i> Previous
</SkillsButton>
</div>
<div class="flex-1 text-center " style="font-size: 0.9rem;" data-cy="skillOrder">
<span class="italic">{{ attributes.skillDisplayName }}</span> <span class="font-semibold">{{ skill.orderInGroup }}</span> <span class="italic">of</span> <span class="font-semibold">{{ skill.totalSkills }}</span>
</div>
<div class="w-28 text-right">
<SkillsButton size="small"
outlined
id="nextSkillButton"
data-cy="nextSkill"
:severity="buttonSeverity"
class="skills-theme-btn"
aria-label="next skill"
:title="`Next ${attributes.skillDisplayName} (${nextButtonShortcut})`"
@click="nextButtonClicked"
v-if="skill.nextSkillId">
Next <i class="fas fa-arrow-alt-circle-right ml-1" aria-hidden="true"></i>
</SkillsButton>
</div>
</div>
</template>

<style scoped>

</style>
36 changes: 36 additions & 0 deletions e2e-tests/cypress/e2e/skills_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1586,5 +1586,41 @@ describe('Skills Tests', () => {
cy.get('[data-cy="descriptionError"]').contains('Mocked up validation failure')
})

it('can navigate between skills on admin skill page', () => {
cy.createSkill(1, 1, 1)
cy.createSkill(1, 1, 2)
cy.createSkill(1, 1, 3)

cy.visit('/administrator/projects/proj1/subjects/subj1/skills/skill1')

cy.get('[data-cy="title"').contains('Very Great Skill 1');
cy.get('[data-cy="skillOrder"]').contains('Skill 1 of 3');
cy.get('[data-cy="prevSkill"]').should('not.exist');
cy.get('[data-cy="nextSkill"]').should('exist');
cy.get('[data-cy="nextSkill"]').click();


cy.get('[data-cy="title"').contains('Very Great Skill 2');
cy.get('[data-cy="skillOrder"]').contains('Skill 2 of 3');
cy.get('[data-cy="prevSkill"]').should('exist');
cy.get('[data-cy="nextSkill"]').should('exist');
cy.get('[data-cy="nextSkill"]').click();

cy.get('[data-cy="title"').contains('Very Great Skill 3');
cy.get('[data-cy="skillOrder"]').contains('Skill 3 of 3');
cy.get('[data-cy="prevSkill"]').should('exist');
cy.get('[data-cy="nextSkill"]').should('not.exist');
cy.get('[data-cy="prevSkill"]').click();

cy.get('[data-cy="title"').contains('Very Great Skill 2');
cy.get('[data-cy="skillOrder"]').contains('Skill 2 of 3');
cy.get('[data-cy="prevSkill"]').should('exist');
cy.get('[data-cy="nextSkill"]').should('exist');
cy.get('[data-cy="prevSkill"]').click();

cy.get('[data-cy="title"').contains('Very Great Skill 1');
cy.get('[data-cy="skillOrder"]').contains('Skill 1 of 3');
cy.get('[data-cy="prevSkill"]').should('not.exist');
cy.get('[data-cy="nextSkill"]').should('exist');
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ class SkillDefRes extends SkillDefPartialRes {

Boolean hasVideoConfigured
String iconClass

String prevSkillId
String nextSkillId
int totalSkills
int orderInGroup
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import skills.services.userActions.DashboardAction
import skills.services.userActions.DashboardItem
import skills.services.userActions.UserActionInfo
import skills.services.userActions.UserActionsHistoryService
import skills.skillLoading.SkillsLoader
import skills.storage.accessors.SkillDefAccessor
import skills.storage.model.*
import skills.storage.model.SkillDef.SelfReportingType
Expand Down Expand Up @@ -134,6 +135,9 @@ class SkillsAdminService {
@Autowired
SkillAttributeService skillAttributeService

@Autowired
private SkillsLoader skillsLoader;

protected static class SaveSkillTmpRes {
// because of the skill re-use it could be imported but NOT available in the catalog
boolean isImportedByOtherProjects = false
Expand Down Expand Up @@ -713,6 +717,14 @@ class SkillsAdminService {

finalRes.thisSkillWasReusedElsewhere = skillDefRepo.wasThisSkillReusedElsewhere(res.id)

DisplayOrderRes orderInfo = skillsLoader.getSkillOrderStats(projectId, subjectId, skillId)
if(orderInfo) {
finalRes.prevSkillId = orderInfo.previousSkillId
finalRes.nextSkillId = orderInfo.nextSkillId
finalRes.totalSkills = orderInfo.totalCount
finalRes.orderInGroup = orderInfo.overallOrder
}

String videoUrl = skillAttributesDefRepo.getVideoUrlBySkillRefId(res.id)
finalRes.hasVideoConfigured = StringUtils.isNotBlank(videoUrl)
return finalRes
Expand Down
Loading
Loading