From fc3f00800bd42bd0bb93c851d05089bc4b141206 Mon Sep 17 00:00:00 2001 From: Khadim Fall Date: Sat, 30 Sep 2023 14:25:37 +0200 Subject: [PATCH] Improvement/refactor course sections (#1156) * started wit hrefactor of course management * add dao & route * update route * started wit hfetching * Add changeset abstraction * cleanup file * remove get all streams again, as there is AdminJson method * Its rendering * further dev :S * added some directive * little fixes * ... * ... no idea... * right path i guess * changeset working except video sections * fix discard files * save from laptop * Add changeset doku * make private works like charm * changing works pretty well * add video upload * add series update * ported transcoding * add readme and lecture hall select * more description * simplified lecture hall set * linter * more fixes * :) * add attachments * :) * add video sections to admin streams * main functionality * using fetch wrtappers * moved uploadFile and postFormData * fix linteer * Impl. Feedback * add video sections to admin streams * main functionality * using fetch wrtappers * moved uploadFile and postFormData * version workers with tag (#1157) * fix linteer * fix delete lecture * reenable page reload on create * some linter fixes * fix merge complications * fix changeset; adding onchange listener * fixed adding sections * added directives * fix typo * improved directives * design improvement * fixed creating and deleting * add sections fixed * cleanup * :) * fix alpine js error issues * remoev console log * nested :) * remove console log * lint-fix * fix admin.LectureList initialisation in settings tab --------- Co-authored-by: Joscha Henningsen <44805696+joschahenningsen@users.noreply.github.com> Co-authored-by: Joscha Henningsen --- api/stream.go | 6 +- dao/courses.go | 3 +- model/stream.go | 13 ++ web/assets/init-admin.js | 106 ++++++++- .../course/manage/course_settings.gohtml | 2 +- .../course/manage/edit-video-sections.gohtml | 206 ++++++++---------- web/ts/api/admin-lecture-list.ts | 119 ++++++++++ web/ts/api/video-sections.ts | 19 -- web/ts/change-set.ts | 42 +++- web/ts/data-store/admin-lecture-list.ts | 62 +++++- web/ts/data-store/video-sections.ts | 35 +-- web/ts/edit-course.ts | 115 +++++++++- web/ts/entry/admins.ts | 2 +- web/ts/entry/video.ts | 1 - web/ts/video-sections.ts | 134 ------------ 15 files changed, 543 insertions(+), 322 deletions(-) delete mode 100644 web/ts/video-sections.ts diff --git a/api/stream.go b/api/stream.go index b50174cad..82b83bb0a 100644 --- a/api/stream.go +++ b/api/stream.go @@ -12,13 +12,13 @@ import ( "strings" "time" - "github.com/getsentry/sentry-go" - "github.com/gin-gonic/gin" "github.com/TUM-Dev/gocast/dao" "github.com/TUM-Dev/gocast/model" "github.com/TUM-Dev/gocast/tools" "github.com/TUM-Dev/gocast/tools/bot" "github.com/TUM-Dev/gocast/voice-service/pb" + "github.com/getsentry/sentry-go" + "github.com/gin-gonic/gin" uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -496,6 +496,8 @@ func (r streamRoutes) createVideoSectionBatch(c *gin.Context) { log.WithError(err).Error("failed to generate video section images") } }() + + c.JSON(http.StatusOK, sections) } type UpdateVideoSectionRequest struct { diff --git a/dao/courses.go b/dao/courses.go index 23978d086..18294dfea 100644 --- a/dao/courses.go +++ b/dao/courses.go @@ -201,6 +201,7 @@ func (d coursesDao) GetCourseByToken(token string) (course model.Course, err err func (d coursesDao) GetCourseById(ctx context.Context, id uint) (course model.Course, err error) { var foundCourse model.Course dbErr := DB.Preload("Streams.TranscodingProgresses"). + Preload("Streams.VideoSections"). Preload("Streams.Files"). Preload("Streams", func(db *gorm.DB) *gorm.DB { return db.Order("streams.start desc") @@ -218,7 +219,7 @@ func (d coursesDao) GetCourseBySlugYearAndTerm(ctx context.Context, slug string, return cachedCourses.(model.Course), nil } var course model.Course - err := DB.Preload("Streams.Units", func(db *gorm.DB) *gorm.DB { + err := DB.Preload("Streams.VideoSections").Preload("Streams.Units", func(db *gorm.DB) *gorm.DB { return db.Order("unit_start desc") }).Preload("Streams", func(db *gorm.DB) *gorm.DB { return db.Order("start desc") diff --git a/model/stream.go b/model/stream.go index 6fac079af..35de2d675 100755 --- a/model/stream.go +++ b/model/stream.go @@ -323,6 +323,18 @@ func (s Stream) getJson(lhs []LectureHall, course Course) gin.H { } } + var videoSections []gin.H + for _, section := range s.VideoSections { + videoSections = append(videoSections, gin.H{ + "id": section.ID, + "description": section.Description, + "startHours": section.StartHours, + "startMinutes": section.StartMinutes, + "startSeconds": section.StartSeconds, + "fileID": section.FileID, + }) + } + return gin.H{ "lectureId": s.Model.ID, "courseId": s.CourseID, @@ -346,6 +358,7 @@ func (s Stream) getJson(lhs []LectureHall, course Course) gin.H { "courseSlug": course.Slug, "private": s.Private, "downloadableVods": s.GetVodFiles(), + "videoSections": videoSections, } } diff --git a/web/assets/init-admin.js b/web/assets/init-admin.js index 172d2513d..f4e8be806 100644 --- a/web/assets/init-admin.js +++ b/web/assets/init-admin.js @@ -15,6 +15,8 @@ document.addEventListener("alpine:init", () => { 'color' ]; + const nativeEventName = "csupdate"; + const convert = (modifiers, value) => { if (modifiers.includes("int")) { return parseInt(value); @@ -79,7 +81,6 @@ document.addEventListener("alpine:init", () => { Alpine.directive("bind-change-set", (el, { expression, value, modifiers }, { evaluate, cleanup }) => { const changeSet = evaluate(expression); const fieldName = value || el.name; - const nativeEventName = "csupdate"; if (el.type === "file") { const isSingle = modifiers.includes("single") @@ -92,7 +93,7 @@ document.addEventListener("alpine:init", () => { if (!data[fieldName]) { el.value = ""; } - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: data[fieldName] } })); }; changeSet.listen(onChangeSetUpdateHandler); @@ -109,7 +110,7 @@ document.addEventListener("alpine:init", () => { const onChangeSetUpdateHandler = (data) => { el.checked = !!data[fieldName]; - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: !!data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: !!data[fieldName] }})); }; changeSet.listen(onChangeSetUpdateHandler); @@ -122,26 +123,29 @@ document.addEventListener("alpine:init", () => { }) } else if (el.tagName === "textarea" || textInputTypes.includes(el.type)) { const keyupHandler = (e) => changeSet.patch(fieldName, convert(modifiers, e.target.value)); + const changeHandler = (e) => changeSet.patch(fieldName, convert(modifiers, e.target.value)); const onChangeSetUpdateHandler = (data) => { el.value = `${data[fieldName]}`; - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: data[fieldName] } })); }; changeSet.listen(onChangeSetUpdateHandler); - el.addEventListener('keyup', keyupHandler) + el.addEventListener('keyup', keyupHandler); + el.addEventListener('change', changeHandler); el.value = `${changeSet.get()[fieldName]}`; cleanup(() => { changeSet.removeListener(onChangeSetUpdateHandler); el.removeEventListener('keyup', keyupHandler) + el.removeEventListener('change', changeHandler) }) } else { const changeHandler = (e) => changeSet.patch(fieldName, convert(modifiers, e.target.value)); const onChangeSetUpdateHandler = (data) => { el.value = `${data[fieldName]}`; - el.dispatchEvent(new CustomEvent(nativeEventName, { detail: data[fieldName] })); + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value: data[fieldName] } })); }; changeSet.listen(onChangeSetUpdateHandler); @@ -154,4 +158,94 @@ document.addEventListener("alpine:init", () => { }) } }); + + /** + * Alpine.js directive for dynamically triggering a custom event and updating an element's inner text + * based on changes to a "change set" object's field. + * + * Syntax: + *
+ * + * Parameters: + * - changeSetExpression: The JavaScript expression evaluating to the change set object + * - fieldName: The specific field within the change set to monitor for changes + * + * Modifiers: + * - "text": When provided, the directive will also update the element's innerText. + * + * Custom Events: + * - "csupdate": Custom event triggered when the change set is updated. + * The detail property of the event object contains the new value of the specified field. + */ + Alpine.directive("change-set-listen", (el, { expression, modifiers }, { effect, evaluate, cleanup }) => { + effect(() => { + const [changeSetExpression, fieldName = null] = expression.split("."); + const changeSet = evaluate(changeSetExpression); + + const onChangeSetUpdateHandler = (data) => { + const value = fieldName != null ? data[fieldName] : data; + if (modifiers.includes("text")) { + el.innerText = `${value}`; + } + el.dispatchEvent(new CustomEvent(nativeEventName, { detail: { changeSet, value } })); + }; + + if (!changeSet) { + return; + } + + changeSet.removeListener(onChangeSetUpdateHandler); + onChangeSetUpdateHandler(changeSet.get()); + changeSet.listen(onChangeSetUpdateHandler); + + cleanup(() => { + changeSet.removeListener(onChangeSetUpdateHandler); + }) + }); + }); + + /** + * Alpine.js directive for executing custom logic in response to the "csupdate" event, + * which is usually triggered by changes in a "change set" object's field. + * + * Syntax: + *
+ * + * Parameters: + * - expression: The JavaScript expression to be evaluated when the "csupdate" event is triggered. + * + * Modifiers: + * - "init": When provided, the directive will execute the expression during initialization (no matter if its dirty or clean). + * - "clean": When provided, the directive will only execute if changeSet is not dirty. + * - "dirty": When provided, the directive will only execute if changeSet is dirty. + * + * Example usage: + *
+ *
+ */ + Alpine.directive("on-change-set-update", (el, { expression, modifiers }, { evaluate, evaluateLater, cleanup }) => { + const onUpdate = evaluateLater(expression); + + const onChangeSetUpdateHandler = (e) => { + const isDirty = e.detail.changeSet.isDirty(); + + if (modifiers.includes("clean") && isDirty) { + return; + } + if (modifiers.includes("dirty") && !isDirty) { + return; + } + onUpdate(); + }; + el.addEventListener(nativeEventName, onChangeSetUpdateHandler); + + if (modifiers.includes("init")) { + evaluate(expression); + } + + cleanup(() => { + el.removeEventListener(nativeEventName, onChangeSetUpdateHandler); + }) + }) }); \ No newline at end of file diff --git a/web/template/partial/course/manage/course_settings.gohtml b/web/template/partial/course/manage/course_settings.gohtml index 1994b0ce3..66fb7b6c9 100644 --- a/web/template/partial/course/manage/course_settings.gohtml +++ b/web/template/partial/course/manage/course_settings.gohtml @@ -5,7 +5,7 @@ }">
+
Video Sections
@@ -29,147 +27,131 @@
-
- - Unsaved Changes -
- +
: : -
- -