Skip to content

Commit

Permalink
185: Initial setup for generic data model editor (#186)
Browse files Browse the repository at this point in the history
Enabled ModelEditor for Course for the time being
  • Loading branch information
jamesaorson authored Apr 29, 2024
1 parent 5023935 commit 0789936
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 147 deletions.
89 changes: 0 additions & 89 deletions client/src/components/CourseEditor.vue

This file was deleted.

1 change: 0 additions & 1 deletion client/src/components/CourseLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

<script lang="ts">
import { defineComponent } from 'vue';
import { RouterLink } from 'vue-router';
export default defineComponent({
name: 'CourseLink',
Expand Down
5 changes: 2 additions & 3 deletions client/src/components/CoursePost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
v-html="state.description"></p>
<div v-if="auth0.isAuthenticated" class="text-xl border-slate-400 rounded border-2 p-1 pl-2 my-2"
v-html="props.templatedContent"></div>
<CourseEditor :handler="saveCourseAsync" handlerText="Update" :courseId="state.id" :courseContent="state.content"
:courseDescription="state.description" :courseName="state.name"></CourseEditor>
<!-- <CourseEditor :handler="saveCourseAsync" handlerText="Update" :courseId="state.id" :courseContent="state.content"
:courseDescription="state.description" :courseName="state.name"></CourseEditor> -->
</div>
</div>
</template>

<script setup lang="ts">
import AuthService from '@/services/AuthService';
import CourseService from '@/services/CourseService';
import CourseEditor from '@/components/CourseEditor.vue';
import type { Course, CourseEditorState } from '@/models';
import { reactive } from 'vue';
import { useAuth0 } from '@auth0/auth0-vue';
Expand Down
61 changes: 61 additions & 0 deletions client/src/components/ModelEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<div v-if="AuthService.isAdmin(auth0)"
class="bg-mysticStone text-white border-slate-400 border-2 rounded p-1 pl-2 my-2">
<div class="text-2xl">Model Editor</div>
<input type="checkbox" id="edit-mode" name="editMode" v-model="state.isEditMode">
<label for="edit-mode"> {{ handlerText }} mode?</label>
<span class="inline-block" :class="{ invisible: !state.isEditMode }">
<Button :handler="() => handler(state.viewModel)" :text="handlerText"></Button>
</span>
<div :class="{ invisible: !state.isEditMode }">
<input type="checkbox" id="show-preview" name="showPreview"
v-model="state.showPreview">
<label for="show-preview"> Show preview?</label>
</div>
<div v-if="AuthService.isAdmin(auth0) && state.isEditMode">
<div v-for="viewKey in modelService.objectViewKeys">
<div class="capitalize text-white border-white border-2 rounded p-1 pl-2 my-2 mr-12">{{ viewKey.key }}</div>
<input v-if="viewKey.kind !== 'code'" v-model="(state.viewModel as any)[viewKey.key]" :type="viewKey.kind">
<CodeEditor v-if="viewKey.kind === 'code'" v-model="(state.viewModel as any)[viewKey.key]" :height="viewKey.height ?? 2"></CodeEditor>
</div>

<!-- TODO: Enable preview by embedding the View component for the type -->
</div>
</div>
</template>

<script setup lang="ts">
import Button from '@/components/Button.vue';
import CodeEditor from '@/components/CodeEditor.vue';
import { reactive, type UnwrapRef } from 'vue';
import AuthService from '@/services/AuthService';
import { useAuth0 } from '@auth0/auth0-vue';
import { useToast } from 'vue-toastification';
import ModelService from '@/services/ModelService';
import type { ViewModel } from '@/models';
const auth0 = useAuth0();
const toast = useToast();
const props = defineProps<{
handler: (viewModel: UnwrapRef<ViewModel>) => void,
handlerText: string,
modelKind: string
}>();
type EditorState = {
isEditMode: boolean,
showPreview: boolean,
viewModel: ViewModel,
};
const modelService = ModelService.inject(props.modelKind) as ModelService<ViewModel>;
const state = reactive<EditorState>({
isEditMode: false,
showPreview: false,
viewModel: modelService.make()
});
const token = await AuthService.getAccessTokenAsync(auth0, { toast: toast });
</script>
8 changes: 7 additions & 1 deletion client/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export type ViewKey = {
height?: number
}

export interface ModelEditorState {
isEditMode: boolean
showPreview: boolean
id: string
};

export abstract class ViewModel {
id: Id;

Expand Down Expand Up @@ -78,7 +84,7 @@ export class Course extends ViewModel {
}
};

export class CourseEditorState {
export class CourseEditorState implements ModelEditorState {
isEditMode: boolean = false
showPreview: boolean = false
id: string = ''
Expand Down
24 changes: 2 additions & 22 deletions client/src/services/AssignmentService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import BlobService from './BlobService';
import HttpServiceV1, { type HttpOptions } from './HttpServiceV1';
import { Assignment, AssignmentMetadata, type AssignmentIndex, type Id, type ViewKey } from '@/models';
import type ModelService from './ModelService';

export default class AssignmentService {
export default class AssignmentService implements ModelService<Assignment> {
objectViewKeys: ViewKey[] = [
{ key: 'id', kind: 'text' },
];
Expand Down Expand Up @@ -33,26 +33,6 @@ export default class AssignmentService {
}
}

static async fillTemplateAsync(template: string, options: HttpOptions = {}): Promise<string> {
if (!template) {
return '';
}
// NOTE: Match and captures what is between ${}, to replace with presigned URLss
const re = /"\${([0-9a-zA-Z_\-\/\.]+)}"/g;
const presignedUrls = new Map<string, string>();
for (let match of template.matchAll(re)) {
const textToReplace = match[0];
const filePath = match[1];
if (!(textToReplace in presignedUrls)) {
presignedUrls.set(textToReplace, await BlobService.getPresignedUrlAsync(filePath, options));
}
}
for (let [key, value] of presignedUrls) {
template = template.replace(key, value);
}
return template;
}

static async getAsync(id: Id, options: HttpOptions = {}): Promise<Assignment> {
try {
return new Assignment(
Expand Down
2 changes: 1 addition & 1 deletion client/src/services/BlobService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Id } from '@/models';
import HttpServiceV1, { type HttpOptions } from './HttpServiceV1';

export default class BlobService {
export default class BlobService {
static async getPresignedUrlAsync(id: Id, options: HttpOptions = {}): Promise<string> {
try {
return await HttpServiceV1.getTextAsync(`blob?url=${id}`, options);
Expand Down
3 changes: 2 additions & 1 deletion client/src/services/BlogService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { BlogIndex, Id, ViewKey } from '@/models';
import { Blog, BlogMetadata } from '@/models';
import HttpServiceV1, { type HttpOptions } from './HttpServiceV1';
import type ModelService from './ModelService';

export default class BlogService {
export default class BlogService implements ModelService<Blog> {
objectViewKeys: ViewKey[] = [
{ key: 'id', kind: 'text' },
];
Expand Down
27 changes: 3 additions & 24 deletions client/src/services/CourseService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Course, CourseMetadata, type CourseIndex, type Id, type ViewKey } from '@/models';
import BlobService from './BlobService';
import HttpServiceV1, { type HttpOptions } from './HttpServiceV1';
import ModelService from './ModelService';

export default class CourseService {
export default class CourseService implements ModelService<Course> {
objectViewKeys: ViewKey[] = [
{ key: 'id', kind: 'text' },
{ key: 'name', kind: 'code' },
Expand Down Expand Up @@ -35,33 +35,12 @@ export default class CourseService {
throw err;
}
}

static async fillTemplateAsync(template: string, options: HttpOptions = {}): Promise<string> {
if (!template) {
return '';
}
// NOTE: Match and captures what is between ${}, to replace with presigned URLss
const re = /"\${([0-9a-zA-Z_\-\/\.]+)}"/g;
const presignedUrls = new Map<string, string>();
for (let match of template.matchAll(re)) {
const textToReplace = match[0];
const filePath = match[1];
if (!(textToReplace in presignedUrls)) {
presignedUrls.set(textToReplace, await BlobService.getPresignedUrlAsync(filePath, options));
}
}
for (let [key, value] of presignedUrls) {
template = template.replace(key, value);
}
return template;
}

static async getAsync(id: Id, options: HttpOptions = {}): Promise<Course> {
try {
const course = new Course(
await HttpServiceV1.getAsync('course', id, options));
try {
course.templatedContent = await CourseService.fillTemplateAsync(course.content, options);
course.templatedContent = await ModelService.fillTemplateAsync(course.content, options);
}
catch (err: any) {
options.toast?.error(`Failed to fill template for course content: ${err}`);
Expand Down
50 changes: 50 additions & 0 deletions client/src/services/ModelService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ViewKey } from "@/models";
import BlobService from "./BlobService";
import type { HttpOptions } from "./HttpServiceV1";
import SectionService from "./SectionService";
import CourseService from "./CourseService";
import AssignmentService from "./AssignmentService";
import BlogService from "./BlogService";

abstract class ModelService<T> {
abstract objectViewKeys: ViewKey[];

static inject(kind: string) {
switch (kind) {
case 'assignment':
return new AssignmentService();
case 'blog':
return new BlogService();
case 'course':
return new CourseService();
case 'section':
return new SectionService();
default:
throw new Error(`Unknown kind: ${kind}`);
}
}

abstract make(): T;

static async fillTemplateAsync(template: string, options: HttpOptions = {}): Promise<string> {
if (!template) {
return '';
}
// NOTE: Match and captures what is between ${}, to replace with presigned URLs
const re = /"\${([0-9a-zA-Z_\-\/\.]+)}"/g;
const presignedUrls = new Map<string, string>();
for (let match of template.matchAll(re)) {
const textToReplace = match[0];
const filePath = match[1];
if (!(textToReplace in presignedUrls)) {
presignedUrls.set(textToReplace, await BlobService.getPresignedUrlAsync(filePath, options));
}
}
for (let [key, value] of presignedUrls) {
template = template.replace(key, value);
}
return template;
}
}

export default ModelService;
3 changes: 2 additions & 1 deletion client/src/services/SectionService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Section, type ViewKey, type Id, type SectionIndex, SectionMetadata } from '@/models';
import HttpServiceV1, { type HttpOptions } from './HttpServiceV1';
import type ModelService from './ModelService';

export default class SectionService {
export default class SectionService implements ModelService<Section> {
objectViewKeys: ViewKey[] = [
{ key: 'id', kind: 'text' },
];
Expand Down
2 changes: 1 addition & 1 deletion client/src/views/AssignmentView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import AssignmentEditor, { type AssignmentEditorState } from '@/components/Assig
import AssignmentLink from '@/components/AssignmentLink.vue';
import AssignmentService from '@/services/AssignmentService';
import Spinner from '@/components/Spinner.vue';
import type { AssignmentIndex, AssignmentMetadata, Id } from '@/models';
import type { AssignmentIndex } from '@/models';
import { onMounted, reactive } from 'vue';
import { useAuth0 } from '@auth0/auth0-vue';
import { useToast } from "vue-toastification";
Expand Down
5 changes: 2 additions & 3 deletions client/src/views/CourseView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
<Button v-if="AuthService.isAdmin(auth0)" :handler="async () => await deleteCourseAsync(id)"
text="Delete?" class="w-20"></Button>
</span>
<CourseEditor :handler="createCourseAsync" handlerText="Create" courseId="" courseContent=""
courseDescription="" courseName=""></CourseEditor>
<ModelEditor :handler="createCourseAsync" handlerText="Create" modelKind="course"></ModelEditor>
</div>
</div>
</div>
Expand All @@ -28,7 +27,7 @@ import Spinner from '@/components/Spinner.vue';
import { onMounted, reactive } from 'vue';
import { useAuth0 } from '@auth0/auth0-vue';
import { useToast } from "vue-toastification";
import CourseEditor from '@/components/CourseEditor.vue';
import ModelEditor from '@/components/ModelEditor.vue';
import type { CourseIndex } from '@/models';
Expand Down

0 comments on commit 0789936

Please sign in to comment.