Skip to content

Commit

Permalink
wip: implement basic cru(d) actions and add some helpful hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
lebalz committed Aug 2, 2024
1 parent 6ebe99b commit 728516f
Show file tree
Hide file tree
Showing 24 changed files with 1,066 additions and 109 deletions.
5 changes: 5 additions & 0 deletions docs/code-block.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Code Blocks

```py live_py id=7cf4fb1c-8495-4600-bf67-23fb7bd29deb
print('hello world')
```
55 changes: 38 additions & 17 deletions src/api/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,56 @@ export enum Access {
None = 'None'
}

export interface Document {
export enum DocumentType {
Script = 'script',
TaskState = 'task_state'
}
export interface ScriptData {
code: string;
}

export interface TaskStateData {
state: 'set' | 'unset' | 'question' | string;
}

export interface TypeDataMapping {
[DocumentType.Script]: ScriptData;
[DocumentType.TaskState]: TaskStateData;
// Add more mappings as needed
}

export interface Document<Type extends DocumentType> {
id: string;
type: Type;
authorId: string;
type: string;
data: Object;

parentId: string;
documentRootId: string;

data: TypeDataMapping[Type];

createdAt: string;
updatedAt: string;
}

export interface RootGroupPermission {
id: string;
rootGroupPermissions: string;
access: Access;
}

export interface DocumentRoot {
id: string;
access: Access;
documents: Document[];
export function find<Type extends DocumentType>(
id: string,
signal: AbortSignal
): AxiosPromise<Document<Type>> {
return api.get(`/documents/${id}`, { signal });
}

export function currentUser(signal: AbortSignal): AxiosPromise<Document> {
return api.get('/user', { signal });
export function create<Type extends DocumentType>(
data: Partial<Document<Type>>,
signal: AbortSignal
): AxiosPromise<Document<Type>> {
return api.post(`/documents`, data, { signal });
}

export function logout(signal: AbortSignal): AxiosPromise<void> {
return api.post('/logout', {}, { signal });
export function update<Type extends DocumentType>(
id: string,
data: TypeDataMapping[Type],
signal: AbortSignal
): AxiosPromise<Document<Type>> {
return api.put(`/documents/${id}`, { data }, { signal });
}
39 changes: 39 additions & 0 deletions src/api/documentRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import api from './base';
import { AxiosPromise } from 'axios';
import { Access, Document, DocumentType } from './document';
import { GroupPermissionBase, UserPermissionBase } from './permission';

export interface RootGroupPermission {
id: string;
rootGroupPermissions: string;
access: Access;
}

export interface DocumentRootBase {
id: string;
access: Access;
}

export interface DocumentRoot extends DocumentRootBase {
userPermissions: UserPermissionBase[];
groupPermissions: GroupPermissionBase[];
documents: Document<DocumentType>[];
}

export interface Config {
access?: Access;
userPermissions: Omit<UserPermissionBase, 'id'>[];
groupPermissions: Omit<GroupPermissionBase, 'id'>[];
}

export function find(id: string, signal: AbortSignal): AxiosPromise<DocumentRoot> {
return api.get(`/documentRoots/${id}`, { signal });
}

export function create(
id: string,
data: Partial<Config> = {},
signal: AbortSignal
): AxiosPromise<DocumentRoot> {
return api.post(`/documentRoots/${id}`, data, { signal });
}
23 changes: 23 additions & 0 deletions src/api/permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import api from './base';
import { AxiosPromise } from 'axios';
import { Access } from './document';

export interface GroupPermissionBase {
id: string;
groupId: string;
access: Access;
}

export interface GroupPermission extends GroupPermissionBase {
documentRootId: string;
}

export interface UserPermissionBase {
id: string;
userId: string;
access: Access;
}

export interface UserPermission extends UserPermissionBase {
documentRootId: string;
}
31 changes: 31 additions & 0 deletions src/api/studentGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import api from './base';
import { AxiosPromise } from 'axios';
import { Access } from './document';

export interface StudentGroup {
id: string;
name: string;
description: string;
userIds: string[];

parentId?: string;

createdAt: string;
updatedAt: string;
}

export function all(signal: AbortSignal): AxiosPromise<StudentGroup[]> {
return api.get(`/studentGroups`, { signal });
}

export function create(data: Partial<StudentGroup> = {}, signal: AbortSignal): AxiosPromise<StudentGroup> {
return api.post(`/studentGroups`, data, { signal });
}

export function update(
id: string,
data: Partial<StudentGroup> = {},
signal: AbortSignal
): AxiosPromise<StudentGroup> {
return api.put(`/studentGroups/${id}`, { data }, { signal });
}
78 changes: 78 additions & 0 deletions src/hooks/useDocumentRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useId } from 'react';
import { Access, Document, DocumentType } from '../api/document';
import { rootStore } from '../stores/rootStore';
import { ApiState } from '../stores/iStore';
import DocumentRoot, { TypeMeta } from '../models/DocumentRoot';

export const useDocumentRoot = <Type extends DocumentType>(id: string | undefined, meta: TypeMeta<Type>) => {
const defaultRootDocId = useId();
const defaultDocId = useId();
const [dummyDocumentRoot] = React.useState<DocumentRoot<Type>>(
new DocumentRoot(
{ id: id || defaultRootDocId, access: Access.RW },
meta,
rootStore.documentRootStore,
true
)
);

/** initial load */
React.useEffect(() => {
const rootDoc = rootStore.documentRootStore.find(dummyDocumentRoot.id);
if (rootDoc || rootStore.documentRootStore.apiStateFor(`load-${dummyDocumentRoot.id}`) === ApiState.LOADING) {
return;
}
if (dummyDocumentRoot.isDummy) {
rootStore.documentRootStore.addDocumentRoot(dummyDocumentRoot);
/** add default document when there are no mainDocs */
if (dummyDocumentRoot.mainDocuments.length === 0) {
const now = new Date().toISOString();
rootStore.documentStore.addToStore({
type: meta.type,
data: meta.defaultData,
authorId: rootStore.userStore.current?.id || '',
createdAt: now,
updatedAt: now,
documentRootId: dummyDocumentRoot.id,
id: defaultDocId,
parentId: null
} satisfies Document<Type>);
}
if (dummyDocumentRoot.id === defaultRootDocId) {
/** no according document in the backend can be expected - skip */
return;
}
}

/**
* load the documentRoot and it's documents from the api.
*/
rootStore.documentRootStore
.load(id, meta)
.then((docRoot) => {
if (!docRoot) {
return rootStore.documentRootStore.create(id, meta, {}).then((docRoot) => {
return docRoot;
});
}
return docRoot;
})
.then((docRoot) => {
if (docRoot) {
if (docRoot.mainDocuments.length === 0 && rootStore.userStore.current) {
rootStore.documentStore.create({
documentRootId: docRoot.id,
authorId: rootStore.userStore.current.id,
type: docRoot.type,
data: meta.defaultData
});
}
}
})
.catch((err) => {
console.log('err loading', err);
});
}, [meta, id]);

return dummyDocumentRoot.id;
};
95 changes: 95 additions & 0 deletions src/models/DocumentRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { action, computed, observable } from 'mobx';
import { DocumentRootBase as DocumentRootProps } from '../api/documentRoot';
import { DocumentRootStore } from '../stores/DocumentRootStore';
import { Access, Document, DocumentType, TypeDataMapping } from '../api/document';
import { highestAccess } from './helpers/accessPolicy';
import { TypeModelMapping } from '../stores/DocumentStore';

export abstract class TypeMeta<T extends DocumentType> {
type: T;
access?: Access;
constructor(type: T, access?: Access) {
this.type = type;
this.access = access;
}
abstract get defaultData(): TypeDataMapping[T];
}

class DocumentRoot<T extends DocumentType> {
readonly store: DocumentRootStore;
readonly id: string;
readonly meta: TypeMeta<T>;
/**
* dummy document roots are used to create new documents, which should not be
* persisted to the api.
* This is useful to support interactive behavior even for not logged in users or
* in offline mode.
*/
readonly _isDummy: boolean;

@observable accessor _access: Access;

constructor(
props: DocumentRootProps,
meta: TypeMeta<T>,
store: DocumentRootStore,
isDummy: boolean = false
) {
this.store = store;
this.meta = meta;
this.id = props.id;
this._access = props.access;
this._isDummy = isDummy;
}

@computed
get isDummy() {
return this._isDummy || !this.store.root.sessionStore.isLoggedIn;
}

get type() {
return this.meta.type;
}

get access() {
if (this.meta.access) {
return this.meta.access;
}
return this.meta.access || this._access;
}

@computed
get permissions() {
return this.store.usersPermissions(this.id);
}

@computed
get permission() {
return highestAccess(new Set([...this.permissions.map((p) => p.access)]));
}

get documents() {
return this.store.root.documentStore.findByDocumentRoot(this.id);
}

/**
* All documents which
* - **don't have a parent**
* - having the **same type** as this document root
*
* @returns All main documents, **ordered by creation date**, oldest first.
*/
@computed
get mainDocuments(): TypeModelMapping[T][] {
return this.documents
.filter((d) => d.isRoot && d.type === this.type)
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) as TypeModelMapping[T][];
}

@computed
get firstMainDocument(): TypeModelMapping[T] | undefined {
return this.mainDocuments[0];
}
}

export default DocumentRoot;
42 changes: 42 additions & 0 deletions src/models/PermissionGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { computed, observable } from 'mobx';
import { Access } from '../api/document';
import PermissionStore from '../stores/PermissionStore';
import { GroupPermission } from '../api/permission';
import User from './User';

class PermissionGroup {
readonly store: PermissionStore;

readonly id: string;
readonly documentRootId: string;
readonly groupId: string;

@observable accessor access: Access;

constructor(props: GroupPermission, store: PermissionStore) {
this.store = store;
this.id = props.id;
this.groupId = props.groupId;
}

@computed
get groups() {
return this.store.studentGroups.filter((g) => g.id === this.groupId);
}

@computed
get userIds() {
return new Set(this.groups.flatMap((g) => [...g.userIds]));
}

@computed
get users() {
return this.store.root.userStore.users.filter((u) => this.userIds.has(u.id));
}

isAffectingUser(user: User) {
return this.userIds.has(user.id);
}
}

export default PermissionGroup;
Loading

0 comments on commit 728516f

Please sign in to comment.