Skip to content

Commit

Permalink
#2490 - implemement Add Skill Event page and add more cypress tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rmmayo committed May 9, 2024
1 parent 72a0c96 commit c8e1d20
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 62 deletions.
205 changes: 204 additions & 1 deletion dashboard-prime/src/components/skills/AddSkillEvent.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,211 @@
<script setup>
import { computed, onMounted, ref, nextTick, watch } from 'vue'
import { useAppConfig } from '@/common-components/stores/UseAppConfig.js';
import { useSkillsState } from '@/stores/UseSkillsState.js'
import { useSkillsAnnouncer } from '@/common-components/utilities/UseSkillsAnnouncer.js'
import { useRoute, useRouter } from 'vue-router'
import SkillsService from '@/components/skills/SkillsService.js'
import SubPageHeader from '@/components/utils/pages/SubPageHeader.vue';
import ExistingUserInput from '@/components/utils/ExistingUserInput.vue';
import SkillsCalendarInput from '@/components/utils/inputForm/SkillsCalendarInput.vue'
import ProjectService from '@/components/projects/ProjectService.js';
const props = defineProps({
projectId: String,
})
const appConfig = useAppConfig()
const skillsState = useSkillsState()
const route = useRoute()
const router = useRouter()
const announcer = useSkillsAnnouncer()
const dateAdded = ref(new Date());
const usersAdded = ref([]);
const isSaving = ref(false);
const isLoading = ref(true);
const currentSelectedUser = ref(null);
const projectTotalPoints = ref(0);
const pkiAuthenticated = ref(false);
const reversedUsersAdded = computed(() => {
return usersAdded.value.map((e) => e)
.reverse();
});
const minimumPoints = computed(() => {
return appConfig.minimumProjectPoints;
});
const disable = computed(() => {
return (!currentSelectedUser || !currentSelectedUser.value || !currentSelectedUser.value.userId || currentSelectedUser.value.userId.length === 0)
|| projectTotalPoints.value < minimumPoints.value
|| (skillsState?.groupId && !skillsState?.enabled);
});
const minPointsTooltip = computed(() => {
let text = '';
if (skillsState?.groupId && !skillsState?.enabled) {
text = 'Unable to add skill for user. This skill belongs to a group whose current status is disabled';
} else if (projectTotalPoints.value < minimumPoints.value) {
text = 'Unable to add skill for user. Insufficient available points in project.';
}
return text;
});
const addButtonIcon = computed(() => {
if (projectTotalPoints.value >= minimumPoints.value) {
return isSaving.value ? 'fa fa-circle-notch fa-spin fa-3x-fa-fw' : 'fas fa-arrow-circle-right';
}
return 'icon-warning fa fa-exclamation-circle text-warning';
});
const newUserObjNoSpacesValidatorInNonPkiMode = (value) => {
console.log(`vee-validating value: ${JSON.stringify(value)}`);
if (pkiAuthenticated.value || !value.userId) {
return true;
}
const hasSpaces = value.userId.indexOf(' ') >= 0;
return !hasSpaces;
}
const schema = yup.object().shape({
'userIdInput': yup.mixed().transform((value, input, ctx) => {
if (typeof value === 'string') {
return {
userId: value,
}
}
return value;
})
.required()
.test('newUserObjNoSpacesValidatorInNonPkiMode', 'User Id may not contain spaces', (value) => newUserObjNoSpacesValidatorInNonPkiMode(value))
.label('User Id'),
'eventDatePicker': yup.date()
.required()
.label('Event Date'),
})
const { values, meta, handleSubmit, isSubmitting, setFieldValue, validate, errors, resetForm } = useForm({
validationSchema: schema,
initialValues: {
userIdInput: null,
eventDatePicker: dateAdded.value
}
})
onMounted(() => {
isLoading.value = true;
loadProject()
pkiAuthenticated.value = appConfig.isPkiAuthenticated.value;
});
const loadProject = () => {
ProjectService.getProject(props.projectId).then((res) => {
projectTotalPoints.value = res.totalPoints;
isLoading.value = false;
});
}
const addSkill = () => {
isSaving.value = true;
SkillsService.saveSkillEvent(route.params.projectId, route.params.skillId, currentSelectedUser.value, dateAdded.value.getTime(), pkiAuthenticated.value)
.then((data) => {
isSaving.value = false;
const historyObj = {
success: data.skillApplied,
msg: data.explanation,
userId: currentSelectedUser.value.userId,
userIdForDisplay: currentSelectedUser.value.userIdForDisplay,
key: currentSelectedUser.value.userId + new Date().getTime() + data.skillApplied,
};
const { userId } = currentSelectedUser.value;
if (!data.skillApplied) {
nextTick(() => announcer.polite(`Could not add Skill event for ${userId}, ${data.explanation}`));
} else {
nextTick(() => announcer.polite(`Skill event has been added for ${userId}`));
}
usersAdded.value.push(historyObj);
// currentSelectedUser.value = null;
resetForm();
})
.catch((e) => {
const hasErrorCode = e.response.data && e.response.data.errorCode;
if (hasErrorCode && (e.response.data.errorCode === 'UserNotFound' || e.response.data.errorCode === 'SkillEventForQuizSkillIsNotAllowed')) {
isSaving.value = false;
const historyObj = {
success: false,
msg: e.response.data.explanation,
userId: currentSelectedUser.value.userId,
userIdForDisplay: currentSelectedUser.value.userIdForDisplay,
key: currentSelectedUser.value.userId + new Date().getTime() + false,
};
usersAdded.value.push(historyObj);
// currentSelectedUser.value = null;
resetForm();
} else {
const errorMessage = (e.response && e.response.data && e.response.data.message) ? e.response.data.message : undefined;
router.push({ name: 'ErrorPage', query: { errorMessage } });
}
})
.finally(() => {
isSaving.value = false;
});
}
</script>
<template>
<div>Add Skill Event</div>
<div>
<SubPageHeader title="Add Skill Events" />
<Card>
<template #content>
<div class="flex flex-wrap align-items-start">
<div class="flex flex-1 px-1">
<existing-user-input class="w-full"
:project-id="projectId"
v-model="currentSelectedUser"
:can-enter-new-user="!pkiAuthenticated"
name="userIdInput"
aria-errormessage="userIdInputError"
aria-describedby="userIdInputError"
:aria-invalid="!meta.valid"
data-cy="userIdInput" />
</div>
<div class="flex">
<SkillsCalendarInput class="mx-2 my-0" selectionMode="single" name="eventDatePicker" v-model="dateAdded" data-cy="eventDatePicker"
:max-date="new Date()" aria-required="true" aria-label="event date" ref="eventDatePicker" />
</div>
<div class="flex">
<SkillsButton
aria-label="Add Specific User"
data-cy="addSkillEventButton"
@click="addSkill"
:disabled="!meta.valid || disable"
:icon="addButtonIcon" label="Add">
</SkillsButton>
</div>
</div>
<Message v-if="!isLoading && minPointsTooltip" severity="warn" :closable="false">{{ minPointsTooltip }}</Message>
<!-- <div>-->
<!-- <div>Errors: {{ JSON.stringify(errors) }}</div>-->
<!-- <div>Meta: {{ JSON.stringify(meta) }}</div>-->
<!-- <div>Values: {{ JSON.stringify(values) }}, disable {{ disable }}</div>-->
<!-- <div>disable: {{ disable }}</div>-->
<!-- <div>currentSelectedUser: {{ currentSelectedUser }}</div>-->
<!-- </div>-->
<div class="mt-2" v-for="(user) in reversedUsersAdded" v-bind:key="user.key" data-cy="addedUserEventsInfo">
<div class="">
<span :class="[user.success ? 'text-green-500' : 'text-red-500']" style="font-weight: bolder">
<i :class="[user.success ? 'fa fa-check' : 'fa fa-info-circle']" aria-hidden="true"/>
<span v-if="user.success">
Added points for
</span>
<span v-else>
Unable to add points for
</span>
<span>[{{user.userIdForDisplay ? user.userIdForDisplay : user.userId }}]</span>
</span><span v-if="!user.success"> - {{user.msg}}</span>
</div>
</div>
</template>
</Card>
</div>
</template>
<style scoped></style>
102 changes: 83 additions & 19 deletions dashboard-prime/src/components/utils/ExistingUserInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { useAppConfig } from '@/common-components/stores/UseAppConfig.js';
import { useUserInfo } from '@/components/utils/UseUserInfo.js';
import { useFocusState } from '@/stores/UseFocusState.js';
import AccessService from '@/components/access/AccessService.js';
import { useField } from 'vee-validate';
import AutoComplete from 'primevue/autocomplete';
import { useSkillsInputFallthroughAttributes } from '@/components/utils/inputForm/UseSkillsInputFallthroughAttributes.js';
// user type constants
const DASHBOARD = 'DASHBOARD';
Expand Down Expand Up @@ -46,9 +49,18 @@ const props = defineProps({
type: Array,
default: () => ([]),
},
canEnterNewUser: {
type: Boolean,
default: false,
},
modelValue: Object,
name: {
type: String,
default: 'userIdInput',
}
});
const model = defineModel()
const emit = defineEmits(['update:modelValue']);
const selectedSuggestOption = ref(null);
const userSuggestOptions = ref([]);
Expand Down Expand Up @@ -110,10 +122,17 @@ const getUserIdForDisplay = (user) => {
}
return user.userIdForDisplay;
}
const suggestUsersFromEvent = ({query}) => {
suggestUsers(query)
}
const suggestUsers = (query) => {
isFetching.value = true;
AccessService.suggestUsers(query, suggestUrl.value)
.then((suggestedUsers) => {
if (query && props.canEnterNewUser) {
// suggestedUsers.push({ userId: query, label: query });
}
suggestions.value = suggestedUsers.filter((suggestedUser) => !props.excludedSuggestions.includes(suggestedUser.userId));
suggestions.value = suggestions.value.map((suggestedUser) => {
const label = getUserIdForDisplay(suggestedUser);
Expand All @@ -129,39 +148,84 @@ const suggestUsers = (query) => {
});
}
const createTagIfNecessary = (userId) => {
console.log(`createTagIfNecessary: ${JSON.stringify(userId)}, type [${typeof userId}]`);
if (!userId) {
value.value = null;
} else if (userId && typeof userId === 'string') {
console.log(`Before string userId: ${userId}, value[${JSON.stringify(value.value)}]`);
value.value = {
userId: userId,
label: userId,
};
console.log(`After string userId: ${userId}, value[${JSON.stringify(value.value)}]`);
} else {
console.log(`Before non-string userId: ${userId}, value[${JSON.stringify(value.value)}]`);
value.value = userId;
console.log(`After string userId: ${userId}, value[${JSON.stringify(value.value)}]`);
}
emit('update:modelValue', value.value);
}
const fallthroughAttributes = useSkillsInputFallthroughAttributes()
const { value, errorMessage } = useField(() => props.name)
// , undefined, {
// syncVModel: true,
// });
const useDropdown = false;
const currentSelectedUser = ref('');
</script>

<template>

<div data-cy="existingUserInput">
<InputGroup class="">
<InputGroupAddon v-if="hasUserSuggestOptions" class="p-1">
<Dropdown class="" v-model="selectedSuggestOption" :options="userSuggestOptions"/>
</InputGroupAddon>
<Dropdown id="existingUserInput"
class="align-items-center"
v-model="model"
<div data-cy="existingUserInput" v-bind="fallthroughAttributes.rootAttrs.value">
<div class="flex">
<!-- <InputGroup class="">-->
<!-- <InputGroupAddon v-if="hasUserSuggestOptions" class="p-0">-->
<Dropdown data-cy="userSuggestOptionsDropdown" v-model="selectedSuggestOption" :options="userSuggestOptions"/>
<!-- </InputGroupAddon>-->
<Dropdown v-if="useDropdown"
v-model="currentSelectedUser"
id="existingUserInput"
data-cy="existingUserInputDropdown"
class="align-items-center w-full"
@update:modelValue="createTagIfNecessary"
:options="suggestions"
:loading="isFetching"
:placeholder="placeholder"
:reset-filter-on-clear="false"
:reset-filter-on-hide="true"
:auto-filter-focus="true"
:auto-filter-focus="!canEnterNewUser"
:editable="canEnterNewUser"
optionLabel="label"
show-clear
@filter="onFilter"
@before-show="onBeforeShow"
filter>
<template #value="slotProps">
<span v-if="slotProps.value">
{{ slotProps.value.label }}
</span>
<span v-else>
{{ slotProps.placeholder }}
</span>
</template>
</Dropdown>
</InputGroup>

<AutoComplete v-if="!useDropdown"
v-bind="fallthroughAttributes.inputAttrs.value"
v-model="currentSelectedUser"
data-cy="existingUserInputDropdown"
class="w-full"
:dropdown="true"
:suggestions="suggestions"
optionLabel="label"
@item-select="(event) => console.log(`item-select: ${JSON.stringify(event)}`)"
@item-unselect="(event) => console.log(`item-unselect: ${JSON.stringify(event)}`)"
@update:modelValue="createTagIfNecessary"
@complete="suggestUsersFromEvent"
@dropdownClick="onBeforeShow">
</AutoComplete>

<!-- </InputGroup>-->
</div>
<small v-if="errorMessage"
role="alert"
class="p-error"
:data-cy="`${name}Error`"
:id="`${name}Error`">{{ errorMessage || '&nbsp;' }}</small>
</div>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const props = defineProps({
},
label: {
type: String,
required: true
required: false
},
isRequired: {
type: Boolean,
Expand Down Expand Up @@ -46,7 +46,7 @@ const handleOnInput = (event) => {

<template>
<div class="field" v-bind="fallthroughAttributes.rootAttrs.value">
<label :for="`input${name}`" class="block"><span v-if="isRequired">*</span> {{ label }} </label>
<label v-if="label" :for="`input${name}`" class="block"><span v-if="isRequired">*</span> {{ label }} </label>
<Calendar v-model="value"
v-bind="fallthroughAttributes.inputAttrs.value"
@keydown.enter="onEnter"
Expand All @@ -55,6 +55,7 @@ const handleOnInput = (event) => {
:id="name"
:data-cy="$attrs['data-cy'] || name" />
<small
v-if="errorMessage"
role="alert"
class="p-error block"
:data-cy="`${name}Error`"
Expand Down

0 comments on commit c8e1d20

Please sign in to comment.