Skip to content

Commit

Permalink
Merge pull request #75 from GBSL-Informatik/feature/move-files
Browse files Browse the repository at this point in the history
Feature/move files
  • Loading branch information
lebalz authored Jan 6, 2025
2 parents 7f1b4d1 + 5862aa4 commit 5502089
Show file tree
Hide file tree
Showing 24 changed files with 684 additions and 3 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions docs/gallery/dynamic-documents/files-and-folders/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
page_id: 3139de4d-8bd6-4bed-978e-a342519d7c52
---
import Directory from '@tdev-components/documents/FileSystem/Directory';
import BrowserWindow from '@tdev-components/BrowserWindow';

# Dateien und Ordner

```md
import Directory from '@tdev-components/documents/FileSystem/Directory';

<Directory name="Ordner" id="1e79cf9a-b890-4d9e-a5fe-e4b372e233c6"/>
```

<BrowserWindow>
<Directory name="Ordner" id="1e79cf9a-b890-4d9e-a5fe-e4b372e233c6"/>
</BrowserWindow>

## Installation

Expand Down Expand Up @@ -79,3 +87,9 @@ const RecordsToCreate = new Set<DocumentType>([DocumentType.Dir, DocumentType.Fi
/* ... */
```
:::

:::warning[Ordner und Dateien verschieben]
Ordner und Dateien können innerhalb derselben `DocumentRoot` verschoben werden. Dies bedarf jedoch folgende __Erlaubte Aktionen__ im [Admin Panel](/admin?panel=allowedActions)

![--width=250px](./images/move-files-and-folders.jpg)
:::
24 changes: 24 additions & 0 deletions src/api/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import api from './base';
import { AxiosPromise } from 'axios';
import { DocumentType } from './document';

export interface AllowedAction {
id: string;
documentType: DocumentType;
action: `update@${string}`;
}

export function deleteAllowedAction(id: string, signal: AbortSignal): AxiosPromise {
return api.delete(`/admin/allowedActions/${id}`, { signal });
}

export function createAllowedAction(
data: Omit<AllowedAction, 'id'>,
signal: AbortSignal
): AxiosPromise<AllowedAction> {
return api.post('/admin/allowedActions', data, { signal });
}

export function allowedActions(signal: AbortSignal): AxiosPromise<AllowedAction[]> {
return api.get(`/admin/allowedActions`, { signal });
}
8 changes: 8 additions & 0 deletions src/api/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,11 @@ export function allDocuments(documentRootIds: string[], signal: AbortSignal): Ax
signal
});
}

export function linkTo<Type extends DocumentType>(
id: string,
linkToId: string,
signal: AbortSignal
): AxiosPromise<Document<Type>> {
return api.put(`/documents/${id}/linkTo/${linkToId}`, { signal });
}
4 changes: 4 additions & 0 deletions src/components/Admin/AdminPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import StudentGroupPanel from '@tdev-components/Admin/StudentGroupPanel';
import UserTable from '@tdev-components/Admin/UserTable';
import AllowedActions from '../AllowedActions';

const AdminPanel = observer(() => {
const userStore = useStore('userStore');
Expand All @@ -29,6 +30,9 @@ const AdminPanel = observer(() => {
<TabItem value="accounts" label="Accounts">
<UserTable />
</TabItem>
<TabItem value="allowedActions" label="Erlaubte Aktionen">
<AllowedActions />
</TabItem>
</Tabs>
</div>
);
Expand Down
133 changes: 133 additions & 0 deletions src/components/Admin/AllowedActions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react';
import clsx from 'clsx';

import styles from './styles.module.scss';
import { observer } from 'mobx-react-lite';
import Button from '@tdev-components/shared/Button';
import { mdiPlusCircle, mdiSortAscending, mdiSortDescending } from '@mdi/js';
import { useStore } from '@tdev-hooks/useStore';
import _ from 'lodash';
import { Delete } from '@tdev-components/shared/Button/Delete';
import { action } from 'mobx';
import Details from '@theme/Details';
import TextInput from '@tdev-components/shared/TextInput';
import SelectInput from '@tdev-components/shared/SelectInput';
import { DocumentType } from '@tdev-api/document';

const SIZE_S = 0.6;

type SortColumn = 'id' | 'documentType' | 'action';
interface Props {
className?: string;
}

const AllowedActions = observer((props: Props) => {
const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc'>('asc');
const [sortColumn, _setSortColumn] = React.useState<SortColumn>('documentType');
const adminStore = useStore('adminStore');
const [newAction, setNewAction] = React.useState('');
const [newDocType, setNewDocType] = React.useState<DocumentType | undefined>(undefined);

const setSortColumn = (column: SortColumn) => {
if (column === sortColumn) {
setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortDirection('asc');
_setSortColumn(column);
}
};

const icon = sortDirection === 'asc' ? mdiSortAscending : mdiSortDescending;
return (
<div className={clsx(styles.userTable, props.className)}>
<Details summary={<summary>Neue Aktion Hinzufügen</summary>}>
<div className={clsx(styles.newAction)}>
<TextInput
onChange={(text) => setNewAction(text)}
value={newAction}
placeholder="Aktion"
/>
<SelectInput
onChange={(docType) => setNewDocType(docType as DocumentType)}
options={['', ...Object.values(DocumentType)]}
value={newDocType || ''}
/>
<Button
text="Hinzufügen"
onClick={() => {
if (!newDocType || !newAction) {
return;
}
adminStore.createAllowedAction(newAction as `update@${string}`, newDocType);
setNewAction('');
setNewDocType(undefined);
}}
disabled={!(newDocType && newAction)}
color="green"
icon={mdiPlusCircle}
/>
</div>
</Details>
<div className={clsx(styles.tableWrapper)}>
<table className={clsx(styles.table)}>
<thead>
<tr>
<th>
<Button
size={SIZE_S}
iconSide="left"
icon={sortColumn === 'id' && icon}
text="ID"
onClick={() => setSortColumn('id')}
/>
</th>
<th>
<Button
size={SIZE_S}
iconSide="left"
icon={sortColumn === 'documentType' && icon}
text="Dokumenttyp"
onClick={() => setSortColumn('documentType')}
/>
</th>
<th>
<Button
size={SIZE_S}
iconSide="left"
icon={sortColumn === 'action' && icon}
text={'Aktion'}
onClick={() => setSortColumn('action')}
/>
</th>
<th></th>
</tr>
</thead>
<tbody>
{_.orderBy(adminStore.allowedActions, [sortColumn], [sortDirection]).map((a, idx) => {
return (
<tr key={a.id}>
<td>{a.id}</td>
<td>
<span className="badge badge--primary">{a.documentType}</span>
</td>
<td>
<span className="badge badge--secondary">{a.action}</span>
</td>
<td>
<Delete
onDelete={action(() => {
adminStore.destroyAllowedAction(a.id);
})}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
});

export default AllowedActions;
37 changes: 37 additions & 0 deletions src/components/Admin/AllowedActions/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.userTable {
width: 100%;
.tableWrapper {
overflow-x: auto;
table.table {
width: 100%;
border-collapse: collapse;
display: table;
--ifm-table-cell-padding: 0.2em 0.5em;
.user {
.clients {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.groupBadge:not(:last-child) {
display: inline-block;
margin-right: 0.5em;
}
}
td,
.nowrap {
white-space: nowrap;
}
}
}
}

.newAction {
display: flex;
flex-direction: column;
gap: 0.5em;
.button {
align-self: flex-end;
}
}
122 changes: 122 additions & 0 deletions src/components/documents/FileSystem/Actions/MoveItem/DirTree/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
mdiCircle,
mdiClose,
mdiFileMove,
mdiFileMoveOutline,
mdiFolderMove,
mdiFolderMoveOutline,
mdiFolderOpen,
mdiFolderOutline
} from '@mdi/js';
import styles from './styles.module.scss';
import Button from '@tdev-components/shared/Button';
import Directory from '@tdev-models/documents/FileSystem/Directory';
import { observer } from 'mobx-react-lite';
import clsx from 'clsx';
import React from 'react';
import { Document, DocumentType } from '@tdev-api/document';
import Icon, { Stack } from '@mdi/react';
import { getNumericCircleIcon } from '@tdev-components/shared/numberIcons';
import iFileSystem from '@tdev-models/documents/FileSystem/iFileSystem';

interface DirProps {
item: iFileSystem<any>;
dir: Directory;
fileType: DocumentType;
moveTo: (dir: Directory) => void;
children?: React.ReactNode;
}

const DirTree = observer((props: DirProps) => {
const { dir, item } = props;
const [confirmMove, setConfirmMove] = React.useState(false);
const [isOpen, setIsOpen] = React.useState(item.path.some((p) => p.id === dir.id));
const disabled = dir.id === item.id || dir.children.some((c) => c.id === item.id);
return (
<>
<div
className={clsx(styles.moveTo)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsOpen(!isOpen);
}}
>
<div className={clsx(styles.stacked)}>
<Icon
path={isOpen ? mdiFolderOpen : mdiFolderOutline}
size={1}
color={disabled ? 'var(--ifm-color-disabled)' : 'var(--ifm-color-primary)'}
/>
{dir.id !== item.id && (
<Stack className={clsx(styles.topRight)} size={0.7}>
<Icon path={mdiCircle} color="white" size={0.8} />
<Icon
path={getNumericCircleIcon(dir.directories.length)}
color="var(--ifm-color-primary-darkest)"
/>
</Stack>
)}
</div>
<div>{dir.name}</div>
<div className={clsx(styles.spacer)} />
<div className={clsx(styles.move, 'button-group button-group--block')}>
{confirmMove && (
<Button
icon={mdiClose}
iconSide="left"
size={1}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setConfirmMove(false);
}}
/>
)}
<Button
text={confirmMove ? 'Ja' : ''}
color={'primary'}
icon={
props.fileType === DocumentType.Dir
? confirmMove
? mdiFolderMove
: mdiFolderMoveOutline
: confirmMove
? mdiFileMove
: mdiFileMoveOutline
}
title={'Hierhin verschieben?'}
size={1}
disabled={disabled}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (confirmMove) {
props.moveTo(dir);
} else {
setConfirmMove(true);
}
}}
/>
</div>
</div>
{isOpen && dir.id !== item.id && (
<div className={clsx(styles.content)}>
{dir.directories.map((c) => {
return (
<DirTree
key={c.id}
dir={c}
fileType={props.fileType}
moveTo={props.moveTo}
item={item}
/>
);
})}
</div>
)}
</>
);
});

export default DirTree;
Loading

0 comments on commit 5502089

Please sign in to comment.