Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/api/OfflineApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,10 @@ export default class OfflineApi {
return rejectResponse({} as T, 400, 'Not implemented');
case 'studentGroups':
return resolveResponse(
(await this.upsertStudentGroupRecord(data as Partial<StudentGroup>, id)) as unknown as T
(await this.upsertStudentGroupRecord(
(data as unknown as { data: Partial<StudentGroup> }).data,
id
)) as unknown as T
);
case 'users':
return resolveResponse(data as unknown as T);
Expand Down
90 changes: 18 additions & 72 deletions src/components/Admin/StudentGroupPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import Badge from '@tdev-components/shared/Badge';
const StudentGroupPanel = observer(() => {
const userStore = useStore('userStore');
const groupStore = useStore('studentGroupStore');
const viewStore = useStore('viewStore');
const adminView = viewStore.adminView;
const current = userStore.current;
const [searchFilter, setSearchFilter] = React.useState('');

if (!current?.hasElevatedAccess) {
return null;
Expand All @@ -28,6 +29,7 @@ const StudentGroupPanel = observer(() => {
<div className={clsx(styles.controls)}>
<Button
onClick={() => {
adminView.setGroupSearchFilter();
groupStore.create('', '').then(
action((group) => {
group?.setEditing(true);
Expand All @@ -43,15 +45,19 @@ const StudentGroupPanel = observer(() => {
<div className={clsx(styles.searchInput)}>
<TextInput
placeholder="Lerngruppen filtern..."
value={searchFilter}
value={adminView.groupSearchFilter}
onChange={(val) => {
setSearchFilter(val);
adminView.setGroupSearchFilter(val);
}}
/>
<div className={clsx(styles.btnResetContainer, { [styles.hidden]: !searchFilter })}>
<div
className={clsx(styles.btnResetContainer, {
[styles.hidden]: !adminView.groupSearchFilter
})}
>
<Button
onClick={() => {
setSearchFilter('');
adminView.setGroupSearchFilter();
}}
icon={mdiCloseCircleOutline}
size={0.8}
Expand Down Expand Up @@ -91,73 +97,13 @@ const StudentGroupPanel = observer(() => {
</DefinitionList>
</div>
<div className={clsx(styles.studentGroups)}>
{(() => {
let searchRegex;
try {
searchRegex = new RegExp(searchFilter, 'i');
} catch {
searchRegex = null;
}

const matches = groupStore.managedStudentGroups
.filter((g) => !g.parentId)
.map((group) => {
let matchPriority = 0;

if (searchRegex) {
// Group name, student name or student email starts with the search filter?
const startsWithMatch =
group.name.toLowerCase().startsWith(searchFilter.toLowerCase()) ||
group.students?.some(
(s) =>
s.name.toLowerCase().startsWith(searchFilter.toLowerCase()) ||
s.email.toLowerCase().startsWith(searchFilter.toLowerCase())
);

// Group name matches (RegExp)?
const groupNameMatch = searchRegex.test(group.name);

// Student name or email matches (RegExp)?
const studentMatch = group.students?.some(
(s) => searchRegex.test(s.name) || searchRegex.test(s.email)
);

// Description matches (RegExp)?
const descriptionMatch = searchRegex.test(group.description ?? '');

if (startsWithMatch) {
matchPriority = 0;
} else if (studentMatch) {
matchPriority = 1;
} else if (groupNameMatch) {
matchPriority = 2;
} else if (descriptionMatch) {
matchPriority = 3;
} else {
// We have a search filter and this doesn't match it.
return null;
}
}

return {
group: group,
matchPriority
};
})
.filter((group) => !!group); // Non-matched groups are null - filter them out.

return _.orderBy(
matches,
['matchPriority', '_pristine.name', 'createdAt'],
['asc', 'asc', 'desc']
).map((match) => (
<StudentGroup
key={match.group.id}
studentGroup={match.group}
className={clsx(styles.studentGroup)}
/>
));
})()}
{adminView.filteredStudentGroups.map((match) => (
<StudentGroup
key={match.group.id}
studentGroup={match.group}
className={clsx(styles.studentGroup)}
/>
))}
</div>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/components/StudentGroup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const StudentGroup = observer((props: Props) => {
const adminStore = useStore('adminStore');
const userStore = useStore('userStore');
const groupStore = useStore('studentGroupStore');
const viewStore = useStore('viewStore');
const group = props.studentGroup;
const isAdmin = group.isGroupAdmin;
React.useEffect(() => {
Expand Down Expand Up @@ -269,6 +270,7 @@ const StudentGroup = observer((props: Props) => {
value={group.parentId || ''}
onChange={(e) => {
group.setParentId(e.target.value || null);
viewStore.adminView.setGroupOpen(group.id, true);
}}
>
<option value="">Keine</option>
Expand Down Expand Up @@ -491,6 +493,7 @@ const StudentGroup = observer((props: Props) => {
<span className={clsx('badge badge--primary')}>{group.children.length}</span>
</summary>
}
open={viewStore.adminView.openGroupIds.has(group.id) ?? undefined}
>
<div>
{group.children.map((child) => (
Expand Down
23 changes: 4 additions & 19 deletions src/pages/user/ApiView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { SIZE_M, SIZE_XS } from '@tdev-components/shared/iconSizes';
import { authClient } from '@tdev/auth-client';
import CodeThemeToggle from '@tdev-components/utils/CodeThemeToggle';
import customFields from '@tdev-components/utils/customFields';
import QuickGroupOverview from './QuickGroupOverview';

const { OFFLINE_API } = customFields;

Expand Down Expand Up @@ -110,25 +111,9 @@ const ApiView = observer(() => {
{viewedUser && !userStore.isUserSwitched && (
<>
<dt>In Gruppen</dt>
{groupStore.studentGroups.map((group) => {
return (
<React.Fragment key={group.id}>
<dt className={clsx(styles.studentGroup)}>{group.name}</dt>
<dd className={clsx(styles.reloadAction)}>
<span
className={clsx(
styles.connectedClients,
'badge',
'badge--primary'
)}
>
{socketStore.connectedClients.get(group.id)}
</span>
<NavReloadRequest roomIds={[group.id]} />
</dd>
</React.Fragment>
);
})}
<dd>
<QuickGroupOverview />
</dd>
</>
)}
</DefinitionList>
Expand Down
12 changes: 12 additions & 0 deletions src/pages/user/LocalDbView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import DbImport from '@tdev-components/utils/DbActions/DbImport';
import DbExport from '@tdev-components/utils/DbActions/DbExport';
import DbDestroy from '@tdev-components/utils/DbActions/DbDestroy';
import { IfmColors } from '@tdev-components/shared/Colors';
import QuickGroupOverview from './QuickGroupOverview';

const { OFFLINE_API, tdevConfig } = customFields;

Expand Down Expand Up @@ -82,6 +83,17 @@ const LocalDbView = observer(() => {
{sessionStore.apiMode}
</Badge>
</dd>
{process.env.NODE_ENV === 'development' && (
<>
<dt>In Gruppen</dt>
<dd>
Debug-View: Nur in <code>development</code>-Angezeigt
</dd>
<dd>
<QuickGroupOverview />
</dd>
</>
)}
</DefinitionList>
<h2>Account</h2>
<DefinitionList>
Expand Down
88 changes: 88 additions & 0 deletions src/pages/user/QuickGroupOverview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.scss';
import { observer } from 'mobx-react-lite';
import { useStore } from '@tdev-hooks/useStore';
import NavReloadRequest from '@tdev-components/Admin/ActionRequest/NavReloadRequest';
import Badge from '@tdev-components/shared/Badge';
import { orderBy } from 'es-toolkit/array';
import StudentGroup from '@tdev-models/StudentGroup';
import Button from '@tdev-components/shared/Button';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
import { action } from 'mobx';

interface Props {
groups: StudentGroup[];
showHidden?: boolean;
}

const GroupList = observer((props: Props) => {
const userStore = useStore('userStore');
const socketStore = useStore('socketStore');
const { groups, showHidden } = props;
const url = useBaseUrl('/admin?panel=studentGroups');
const viewStore = useStore('viewStore');
return (
<ul className={clsx(styles.quickGroupActions)}>
{groups.map((group) => {
const children = orderBy(
group.children.filter((child) => showHidden || child.isActive),
['isActive', 'name'],
['desc', 'asc']
);
return (
<li className={clsx(styles.item)} key={group.id}>
<div className={clsx(styles.count)}>
<Badge type="primary">{socketStore.connectedClients.get(group.id) ?? 0}</Badge>
</div>
<div className={clsx(styles.name)}>
{userStore.current?.hasElevatedAccess ? (
<Link
to={url}
onClick={action((e) => {
viewStore.adminView.setGroupSearchFilter(group.name);
[...group.parentIds, group.id].forEach((id) => {
viewStore.adminView.setGroupOpen(id, true);
});
})}
>
{group.name}
</Link>
) : (
<b>{group.name}</b>
)}
<NavReloadRequest roomIds={[group.id]} />
</div>
<GroupList groups={children} />
</li>
);
})}
</ul>
);
});

const QuickGroupOverview = observer(() => {
const [showHidden, setShowHidden] = React.useState(false);
const groupStore = useStore('studentGroupStore');
const hasHidden = groupStore.studentGroups.some((group) => !group.isActive);
const groups = orderBy(
groupStore.studentGroups.filter((group) => !group.parent && (showHidden || group.isActive)),
['isActive', 'name'],
['desc', 'asc']
);

return (
<div>
<GroupList groups={groups} showHidden={showHidden} />
{hasHidden && (
<Button
text={showHidden ? 'Versteckte Gruppen ausblenden' : 'Versteckte Gruppen anzeigen'}
onClick={() => setShowHidden(!showHidden)}
/>
)}
</div>
);
});

export default QuickGroupOverview;
22 changes: 22 additions & 0 deletions src/pages/user/QuickGroupOverview/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.quickGroupActions {
--ifm-list-left-padding: 0.5em;
.item {
list-style: none;
position: relative;
padding-left: 2em;
.count {
position: absolute;
top: 0.25em;
left: -0.5em;
min-width: 32px;
line-height: 0;
display: flex;
justify-content: flex-end;
}
.name {
display: flex;
align-items: center;
gap: 0.5em;
}
}
}
Loading