From 29e259cabbf6d75c7008d748e8f6e89e5bd09d71 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 6 Mar 2025 23:44:45 +0100 Subject: [PATCH] Speed TCC raster up by refactoring components and using v-lazy --- .../TimeboundCourseConfigRaster.vue | 330 ++++++++++-------- .../TimeboundCourseConfigRasterCell.vue | 121 +++++++ 2 files changed, 299 insertions(+), 152 deletions(-) create mode 100644 aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 9eb6360..af32206 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -1,157 +1,187 @@ <script setup> -import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; import ValidityRangeField from "../validity_range/ValidityRangeField.vue"; import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; -import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue" +import TimeboundCourseConfigRasterCell from "./TimeboundCourseConfigRasterCell.vue"; </script> <template> - <div> - <v-data-table - disable-sort - disable-filtering - disable-pagination - hide-default-footer - :headers="headers" - :items="items" - :loading="loading" - > - <template #top> - <v-row> - <v-col - cols="6" - lg="3" - class="d-flex justify-space-between flex-wrap align-center" - > - <v-autocomplete - outlined - filled - multiple - hide-details - :items="groupsForPlanning" - item-text="shortName" - item-value="id" - return-object - :disabled="$apollo.queries.groupsForPlanning.loading" - :label="$t('lesrooster.timebound_course_config.groups')" - :loading="$apollo.queries.groupsForPlanning.loading" - v-model="selectedGroups" - class="mr-4" - /> - </v-col> + <v-data-table + disable-sort + disable-filtering + disable-pagination + hide-default-footer + :headers="headers" + :items="items" + :loading="loading" + > + <template #top> + <v-row> + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-autocomplete + outlined + filled + multiple + hide-details + :items="groupsForPlanning" + item-text="shortName" + item-value="id" + return-object + :disabled="$apollo.queries.groupsForPlanning.loading" + :label="$t('lesrooster.timebound_course_config.groups')" + :loading="$apollo.queries.groupsForPlanning.loading" + v-model="selectedGroups" + class="mr-4" + /> + </v-col> - <v-col - cols="6" - lg="3" - class="d-flex justify-space-between flex-wrap align-center" - > - <validity-range-field - outlined - filled - hide-details - v-model="internalValidityRange" - :loading="$apollo.queries.currentValidityRange.loading" - /> - </v-col> + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" + > + <validity-range-field + outlined + filled + hide-details + v-model="internalValidityRange" + :loading="$apollo.queries.currentValidityRange.loading" + /> + </v-col> - <v-col - cols="6" - lg="2" - class="d-flex justify-space-between flex-wrap align-center" - > - <v-switch - v-model="includeChildGroups" - inset - :label=" - $t( - 'lesrooster.timebound_course_config.filters.include_child_groups', - ) - " - :loading="$apollo.queries.subjects.loading" - ></v-switch> - </v-col> + <v-col + cols="6" + lg="2" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-switch + v-model="includeChildGroups" + inset + :label=" + $t( + 'lesrooster.timebound_course_config.filters.include_child_groups', + ) + " + :loading="$apollo.queries.subjects.loading" + ></v-switch> + </v-col> - <v-spacer /> - </v-row> - </template> + <v-spacer /> + </v-row> + </template> - <!-- eslint-disable-next-line vue/valid-v-slot --> - <template #item.subject="{ item, value }"> - <subject-chip v-if="value" :subject="value" /> - </template> + <!-- eslint-disable-next-line vue/valid-v-slot --> + <template #item.subject="{ item, value }"> + <subject-chip v-if="value" :subject="value" /> + </template> - <template - v-for="(groupHeader, index) in groupHeaders" - #[tableItemSlotName(groupHeader)]="{ item, value, header }" + <template + v-for="(groupHeader, index) in groupHeaders" + #[tableItemSlotName(groupHeader)]="{ item, value, header }" + > + <timebound-course-config-raster-cell + :value="value" + :subject="item.subject" + :header="header" + :loading="loading" + @addCourse="addCourse" + @setCourseConfigData="setCourseConfigData" + /> + </template> + </v-data-table> + + <!-- <div> + <v-row> + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-autocomplete + outlined + filled + multiple + hide-details + :items="groupsForPlanning" + item-text="shortName" + item-value="id" + return-object + :disabled="$apollo.queries.groupsForPlanning.loading" + :label="$t('lesrooster.timebound_course_config.groups')" + :loading="$apollo.queries.groupsForPlanning.loading" + v-model="selectedGroups" + class="mr-4" + /> + </v-col> + + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" > - <div :key="index"> - <div v-if="value.length"> - <v-row - v-for="(course, index) in value" - :key="index" - no-gutters - class="mt-2" - > - <v-col cols="6"> - <positive-small-integer-field - dense - filled - class="mx-1" - :disabled="loading" - :value=" - getCurrentCourseConfig(course) - ? getCurrentCourseConfig(course).lessonQuota - : course.lessonQuota - " - :label="$t('lesrooster.timebound_course_config.lesson_quota')" - @change=" - (event) => - setCourseConfigData(course, item.subject, header, { - lessonQuota: event, - }) - " - /> - </v-col> - <v-col cols="6"> - <teacher-field - dense - filled - class="mx-1" - :disabled="loading" - :label="$t('lesrooster.timebound_course_config.teachers')" - :value=" - getCurrentCourseConfig(course) - ? getCurrentCourseConfig(course).teachers - : course.teachers - " - :show-subjects="true" - :priority-subject="item.subject" - :rules="$rules().isNonEmpty.build()" - @input=" - (event) => - setCourseConfigData(course, item.subject, header, { - teachers: event, - }) - " - /> - </v-col> - </v-row> - </div> - <div v-if="!value.length"> - <v-btn - block - icon - tile - outlined - @click="addCourse(item.subject.id, header.value)" - > - <v-icon>mdi-plus</v-icon> - </v-btn> - </div> - </div> - </template> - </v-data-table> - </div> + <validity-range-field + outlined + filled + hide-details + v-model="internalValidityRange" + :loading="$apollo.queries.currentValidityRange.loading" + /> + </v-col> + + <v-col + cols="6" + lg="2" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-switch + v-model="includeChildGroups" + inset + :label=" + $t( + 'lesrooster.timebound_course_config.filters.include_child_groups', + ) + " + :loading="$apollo.queries.subjects.loading" + ></v-switch> + </v-col> + + <v-spacer /> + </v-row> + <v-simple-table + fixed-header + > + <thead> + <tr> + <th v-for="header in headers" :key="header.value" class="text-left"> + {{ header.text }} + </th> + </tr> + </thead> + <tbody> + <tr + v-for="(item, index) in items" + :key="index" + > + <td> + <subject-chip :subject="item.subject" /> + </td> + <td v-for="header in groupHeaders" :key="header.value"> + <timebound-course-config-raster-cell + :value="item[header.value]" + :subject="item.subject" + :header="header" + :loading="loading" + @addCourse="addCourse" + @setCourseConfigData="setCourseConfigData" + /> + </td> + </tr> + </tbody> + </v-simple-table> + </div> --> </template> <script> @@ -167,11 +197,10 @@ import { currentValidityRange as gqlCurrentValidityRange } from "../validity_ran import { gqlGroupsForPlanning } from "../helper.graphql"; import mutateMixin from "aleksis.core/mixins/mutateMixin.js"; -import formRulesMixin from "aleksis.core/mixins/formRulesMixin"; export default { name: "TimeboungCourseConfigRaster", - mixins: [formRulesMixin, mutateMixin], + mixins: [mutateMixin], data() { return { i18nKey: "lesrooster.timebound_course_config", @@ -210,13 +239,6 @@ export default { tableItemSlotName(header) { return "item." + header.value; }, - getCurrentCourseConfig(course) { - if (course.lrTimeboundCourseConfigs?.length) { - return course.lrTimeboundCourseConfigs[0]; - } else { - return null; - } - }, setCourseConfigData(course, subject, header, newValue) { if (course.newCourse) { let existingCreatedCourse = this.createdCourses.find( @@ -311,7 +333,8 @@ export default { ); }, generateTableItems(subjects) { - return subjects.map((subject) => { + const start = performance.now(); + const subjectsWithSortedCourses = subjects.map((subject) => { let { courses, ...reducedSubject } = subject; let groupCombinations = {}; @@ -359,6 +382,9 @@ export default { ...groupCombinations, }; }); + const end = performance.now(); + console.log(`Execution time: ${end - start} ms`); + return subjectsWithSortedCourses; }, }, computed: { diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue new file mode 100644 index 0000000..71efac9 --- /dev/null +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue @@ -0,0 +1,121 @@ +<script setup> +import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; +import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue" +</script> + +<template> + <v-lazy + v-model="active" + :options="{ + threshold: .5 + }" + transition="fade-transition" + > + <div v-if="value.length"> + <v-row + v-for="(course, index) in value" + :key="index" + no-gutters + class="mt-2" + > + <v-col cols="6"> + <positive-small-integer-field + dense + filled + class="mx-1" + :disabled="loading" + :value=" + getCurrentCourseConfig(course) + ? getCurrentCourseConfig(course).lessonQuota + : course.lessonQuota + " + :label="$t('lesrooster.timebound_course_config.lesson_quota')" + @change=" + (event) => + $emit('setCourseConfigData', course, subject, header, { + lessonQuota: event, + }) + " + /> + </v-col> + <v-col cols="6"> + <teacher-field + dense + filled + class="mx-1" + :disabled="loading" + :label="$t('lesrooster.timebound_course_config.teachers')" + :value=" + getCurrentCourseConfig(course) + ? getCurrentCourseConfig(course).teachers + : course.teachers + " + :show-subjects="true" + :priority-subject="subject" + :rules="$rules().isNonEmpty.build()" + @input=" + (event) => + $emit('setCourseConfigData', course, subject, header, { + teachers: event, + }) + " + /> + </v-col> + </v-row> + </div> + <div v-else> + <v-btn + block + icon + tile + outlined + @click="$emit('addCourse', subject.id, header.value)" + > + <v-icon>mdi-plus</v-icon> + </v-btn> + </div> + </v-lazy> +</template> + +<script> +import formRulesMixin from "aleksis.core/mixins/formRulesMixin"; + +export default { + name: "TimeboundCourseConfigRasterCell", + mixins: [formRulesMixin], + emits: ["addCourse", "setCourseConfigData"], + props: { + value: { + type: Array, + required: true, + }, + subject: { + type: Object, + required: true, + }, + header: { + type: Object, + required: true, + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + active: false, + }; + }, + methods: { + getCurrentCourseConfig(course) { + if (course.lrTimeboundCourseConfigs?.length) { + return course.lrTimeboundCourseConfigs[0]; + } else { + return null; + } + }, + }, +}; +</script> -- GitLab