Compare commits

...

10 Commits

Author SHA1 Message Date
grimhilt
8a11b54421 allow to interact with newly added file in playlist 2023-10-25 19:15:36 +02:00
grimhilt
3f57fde410 fix update of playlist when there are multiple time the same file 2023-09-12 14:15:51 +02:00
grimhilt
b75f2e3b49 global changes and test 2023-08-30 14:39:09 +02:00
grimhilt
44cdba6b7d use media player to display video 2023-08-30 11:33:14 +02:00
grimhilt
24fa13c870 add logos 2023-08-30 10:45:49 +02:00
grimhilt
8c80cef8d0 change path of files 2023-08-30 09:44:18 +02:00
grimhilt
35ff9e643d change name and other small things 2023-08-29 21:20:00 +02:00
grimhilt
f8c9b93dad upload large file 2023-08-29 21:18:03 +02:00
grimhilt
25eb25b9c3 update permissions room 2023-08-14 12:22:28 +02:00
grimhilt
30dc2b5f31 fix warning 2023-08-14 12:22:21 +02:00
27 changed files with 335 additions and 130 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "signage", "name": "Artemio",
"version": "1.1.0", "version": "1.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -5,11 +5,11 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="Signage" /> <meta name="description" content="Artemio" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<meta name="darkreader" content="disable" /> <meta name="darkreader" content="disable" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Signage</title> <title>Artemio</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -1,6 +1,6 @@
{ {
"short_name": "signage", "short_name": "artemio",
"name": "signage", "name": "artemio",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -0,0 +1,25 @@
import { Modal, Text } from '@mantine/core';
const ModalEditor = ({ title, opened, handlerClose }) => {
return (
<Modal.Root opened={opened} onClose={handlerClose}>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>
<Text fw={700} fz="lg">
{title}
</Text>
</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
{content}
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
};
export default ModalEditor;

View File

@ -70,7 +70,7 @@ const HeaderSearch = () => {
<Group> <Group>
<Avatar src={Logo} size={30} /> <Avatar src={Logo} size={30} />
<UnstyledButton onClick={() => navigate('/')} className={classes.link}> <UnstyledButton onClick={() => navigate('/')} className={classes.link}>
<Title order={1}>Signage</Title> <Title order={1}>Artemio</Title>
</UnstyledButton> </UnstyledButton>
</Group> </Group>

View File

@ -0,0 +1,24 @@
import { Image } from '@mantine/core';
import { isImage } from '../tools/fileUtil';
const MediaPlayer = ({ file, fileId, shouldContain }) => {
return isImage(file.type) ? (
<Image
src={'/api/files/' + (fileId ?? file.id)}
alt={file?.name ?? ''}
width={shouldContain ? undefined : 150}
fit={shouldContain ? 'contain' : 'cover'}
radius="md"
withPlaceholder
/>
) : (
<>
<video controls width={150}>
<source src={'/api/files/' + (fileId ?? file.id)} type={file.type} />
Sorry, your browser doesn't support videos.
</video>
</>
);
};
export default MediaPlayer;

View File

@ -1,5 +1,6 @@
import { Card, Grid, Text, Image, Button, Center } from '@mantine/core'; import { Card, Grid, Text, Button, Center } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import MediaPlayer from './media-player';
const SelectorItem = ({ file, clickHandler }) => { const SelectorItem = ({ file, clickHandler }) => {
const [selected, setSelected] = useState(false); const [selected, setSelected] = useState(false);
@ -12,14 +13,7 @@ const SelectorItem = ({ file, clickHandler }) => {
<Grid.Col span={1} key={file.id}> <Grid.Col span={1} key={file.id}>
<Card shadow="sm"> <Card shadow="sm">
<Card.Section> <Card.Section>
<Image <MediaPlayer file={file} />
src={"/api/file/"+file.id}
alt={file.name}
height={100}
fit="cover"
radius="md"
withPlaceholder
/>
</Card.Section> </Card.Section>
<Center> <Center>
<Text order={4} mt="md"> <Text order={4} mt="md">

View File

@ -62,7 +62,7 @@ const Authentication = ({ redirect }) => {
align="center" align="center"
sx={(theme) => ({ fontFamily: `Greycliff CF, ${theme.fontFamily}`, fontWeight: 900 })} sx={(theme) => ({ fontFamily: `Greycliff CF, ${theme.fontFamily}`, fontWeight: 900 })}
> >
Connect to signage Connect to Artemio
</Title> </Title>
<Paper withBorder shadow="md" p={30} mt={30} radius="md"> <Paper withBorder shadow="md" p={30} mt={30} radius="md">

View File

@ -25,8 +25,11 @@ const ModalAddFile = ({ opened, handler, addFiles }) => {
const handleSubmit = () => { const handleSubmit = () => {
setIsLoading(true); setIsLoading(true);
const formData = new FormData(); const formData = new FormData();
files.forEach((file) => formData.append('file', file));
API.files.upload(formData) files.forEach((file) => formData.append(`${file.name}`, file, file.name));
API.files
.upload(formData)
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
validate(res.data); validate(res.data);

View File

@ -1,4 +1,5 @@
import { Card, Text, Image, Button, Group } from '@mantine/core'; import { Card, Text, Image, Button, Group, Center } from '@mantine/core';
import MediaPlayer from '../../components/media-player';
const FileView = ({ file, onSelect, onDelete, ...props }) => { const FileView = ({ file, onSelect, onDelete, ...props }) => {
// const deleteHandler = async () => { // const deleteHandler = async () => {
@ -13,7 +14,9 @@ const FileView = ({ file, onSelect, onDelete, ...props }) => {
return ( return (
<Card shadow="sm" padding="md" withBorder> <Card shadow="sm" padding="md" withBorder>
<Card.Section> <Card.Section>
<Image src={'/api/file/' + file?.id ?? ''} alt={file?.name ?? ''} withPlaceholder fit="contain" /> <Center>
<MediaPlayer file={file} shouldContain={true} />
</Center>
</Card.Section> </Card.Section>
<Text>{file?.name ?? 'File Name'}</Text> <Text>{file?.name ?? 'File Name'}</Text>
<Group position="center" grow> <Group position="center" grow>

View File

@ -8,9 +8,14 @@ import { useEffect, useState } from 'react';
import ModalFileSelector from '../files/file-selector'; import ModalFileSelector from '../files/file-selector';
import API from '../../services/api'; import API from '../../services/api';
import setNotification from '../errors/error-notification'; import setNotification from '../errors/error-notification';
import GrantAccess, { Perm, checkPerm } from '../../tools/grant-access'; import { Perm, checkPerm } from '../../tools/grant-access';
import { useAuth } from '../../tools/auth-provider'; import { useAuth } from '../../tools/auth-provider';
import { parseTime } from '../../tools/timeUtil'; import { parseTime } from '../../tools/timeUtil';
import MediaPlayer from '../../components/media-player';
import { isVideo } from '../../tools/fileUtil';
const DEFAULT_FILE_TIME = 2;
const INCREMENT_POSITION = 100;
const Content = ({ form, playlistId, playlist }) => { const Content = ({ form, playlistId, playlist }) => {
const [fileSelector, setFileSelector] = useState(false); const [fileSelector, setFileSelector] = useState(false);
@ -21,32 +26,32 @@ const Content = ({ form, playlistId, playlist }) => {
useEffect(() => { useEffect(() => {
if (!user || !playlist) return; if (!user || !playlist) return;
const canEditTmp = checkPerm(Perm.EDIT_PLAYLIST, user, playlist); const canEditTmp = checkPerm(Perm.EDIT_PLAYLIST, user, playlist);
if (canEditTmp != canEdit) { if (canEditTmp !== canEdit) {
setCanEdit(canEditTmp); setCanEdit(canEditTmp);
} }
return () => {}; return () => {};
// eslint-disable-next-line
}, [playlist, user]); }, [playlist, user]);
const handleAddFiles = (files) => { const handleAddFiles = (files) => {
let formFiles = form.values.files; let formFiles = form.values.files;
let max_position = formFiles[formFiles.length - 1]?.position ?? 0; let max_position = formFiles[formFiles.length - 1]?.position ?? 0;
files.forEach((file) => { files.forEach((file) => {
max_position++; max_position += INCREMENT_POSITION;
file.position = max_position; file.position = max_position;
file.seconds = 10; file.seconds = DEFAULT_FILE_TIME;
const index = form.values.files.length;
form.insertListItem('files', file);
API.playlists API.playlists
.addFile(playlistId, { position: file.position, file_id: file.id, seconds: file.seconds }) .addFile(playlistId, { position: file.position, file_id: file.id, seconds: file.seconds })
.then((res) => { .then((res) => {
if (res.status !== 200) { if (res.status !== 200) {
setNotification(true, `Error when adding file (${res.status})`); setNotification(true, `Error when adding file (${res.status})`);
} else {
file.pfid = res.data.pfid;
form.insertListItem('files', file);
} }
}) })
.catch((err) => { .catch((err) => {
console.log('here'); // form.removeListItem('files', index);
form.removeListItem('files', index);
setNotification(true, err); setNotification(true, err);
}); });
}); });
@ -54,13 +59,15 @@ const Content = ({ form, playlistId, playlist }) => {
const changePositionValue = (from, to) => { const changePositionValue = (from, to) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (from == to)
resolve(true);
const formFiles = form.values.files; const formFiles = form.values.files;
let below_position = to === 0 ? 0 : formFiles[to].position; let below_position = to === 0 ? 0 : formFiles[to - 1].position;
let above_position = formFiles[to].position; let above_position = formFiles[to].position;
if (to > from) { if (to > from) {
if (to === formFiles.length - 1) { if (to === formFiles.length - 1) {
// last element so nothing above // last element so nothing above
above_position = formFiles.length + 1; above_position = formFiles[to].position + INCREMENT_POSITION;
} else { } else {
// not last to taking element above // not last to taking element above
above_position = formFiles[to + 1].position; above_position = formFiles[to + 1].position;
@ -70,9 +77,10 @@ const Content = ({ form, playlistId, playlist }) => {
// sending modification to server // sending modification to server
API.playlists API.playlists
.changeOrder(playlistId, { file_id: formFiles[from].id, position: newPosition }) .changeOrder(playlistId, { pfid: formFiles[from].pfid, position: newPosition })
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
formFiles[from].position = newPosition;
resolve(true); resolve(true);
} else { } else {
setNotification(true, `Error when changing order (${res.status})`); setNotification(true, `Error when changing order (${res.status})`);
@ -109,9 +117,9 @@ const Content = ({ form, playlistId, playlist }) => {
}; };
const changeSeconds = (seconds, index) => { const changeSeconds = (seconds, index) => {
const fileId = form.values.files[index].id; const filePfid = form.values.files[index].pfid;
API.playlists API.playlists
.changeSeconds(playlistId, { file_id: fileId, seconds: seconds }) .changeSeconds(playlistId, { pfid: filePfid, seconds: seconds })
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
setOriginSecs(); setOriginSecs();
@ -128,7 +136,9 @@ const Content = ({ form, playlistId, playlist }) => {
const handleDelete = (index) => { const handleDelete = (index) => {
API.playlists API.playlists
.removeFile(playlistId, { file_id: form.values.files[index].pfid }) .removeFile(playlistId, {
pfid: form.values.files[index].pfid,
})
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
form.removeListItem('files', index); form.removeListItem('files', index);
@ -141,67 +151,82 @@ const Content = ({ form, playlistId, playlist }) => {
}); });
}; };
const fields = form.values.files.map((_, index) => ( const fields = form.values.files.map((_, index) =>
<Draggable key={index} index={index} draggableId={index.toString()}> canEdit ? (
{(provided) => ( <Draggable key={index} index={index} draggableId={index.toString()}>
<Group ref={provided.innerRef} mt="xs" {...provided.draggableProps} position="center"> {(provided) => (
<Paper p="xs" radius="sm" shadow="sm" withBorder spacing="xs" style={{ width: '90%' }}> <Group ref={provided.innerRef} mt="xs" {...provided.draggableProps} position="center">
<Flex direction="row" align="center" gap="lg" justify="flex-end"> <Paper p="xs" radius="sm" shadow="sm" withBorder spacing="xs" style={{ width: '90%' }}>
<Text>{form.getInputProps(`files.${index}.name`).value}</Text> <Flex direction="row" align="center" gap="lg" justify="flex-end">
<Image width={150} src={'/api/file/' + form.getInputProps(`files.${index}.id`).value} /> <Text>{form.getInputProps(`files.${index}.name`).value}</Text>
{ canEdit ? <MediaPlayer file={form.getInputProps(`files.${index}`).value} />
<NumberInput <NumberInput
required required
hideControls {...(isVideo(form.getInputProps(`files.${index}.type`).value)
description="Seconds to display" ? {
value={form.getInputProps(`files.${index}.seconds`).value} disabled: true,
onChange={(secs) => handleChangeSeconds(secs, index)} value: 0,
error={form.getInputProps(`files.${index}.seconds`).errors && 'This field is required'} description: 'Default to video duration',
/> }
: <Text>Display time: {parseTime(form.getInputProps(`files.${index}.seconds`).value)}</Text> : {
} value: form.getInputProps(`files.${index}.seconds`).value,
{canEdit ? ( onChange: (secs) => handleChangeSeconds(secs, index),
})}
hideControls
label="Seconds to display"
error={
form.getInputProps(`files.${index}.seconds`).errors && 'This field is required'
}
/>
<ActionIcon color="red" variant="light" size="lg" onClick={() => handleDelete(index)}> <ActionIcon color="red" variant="light" size="lg" onClick={() => handleDelete(index)}>
<IconTrash size="1rem" /> <IconTrash size="1rem" />
</ActionIcon> </ActionIcon>
) : ( </Flex>
<></> </Paper>
)}
</Flex>
</Paper>
{canEdit ? (
<Center {...provided.dragHandleProps}> <Center {...provided.dragHandleProps}>
<IconGripVertical size="1.2rem" /> <IconGripVertical size="1.2rem" />
</Center> </Center>
) : ( </Group>
<></> )}
)} </Draggable>
</Group> ) : (
)} <Group key={index} mt="xs" position="center">
</Draggable> <Paper p="xs" radius="sm" shadow="sm" withBorder spacing="xs" style={{ width: '90%' }}>
)); <Flex direction="row" align="center" gap="lg" justify="flex-end">
<Text>{form.getInputProps(`files.${index}.name`).value}</Text>
<MediaPlayer file={form.getInputProps(`files.${index}`).value} />
<Text>Display time: {parseTime(form.getInputProps(`files.${index}.seconds`).value)}</Text>
</Flex>
</Paper>
</Group>
)
);
return ( return (
<Box mx="auto" maw={1200}> <Box mx="auto" maw={1200}>
<DragDropContext {canEdit ? (
onDragEnd={({ destination, source }) => { <DragDropContext
form.reorderListItem('files', { from: source.index, to: destination.index }); onDragEnd={({ destination, source }) => {
changePositionValue(source.index, destination.index).then((success) => { form.reorderListItem('files', { from: source.index, to: destination.index });
if (!success) { changePositionValue(source.index, destination.index).then((success) => {
form.reorderListItem('files', { from: destination.index, to: source.index }); if (!success) {
} form.reorderListItem('files', { from: destination.index, to: source.index });
}); }
}} });
> }}
<StrictModeDroppable droppableId="dnd-list" direction="vertical"> >
{(provided) => ( <StrictModeDroppable droppableId="dnd-list" direction="vertical">
<div {...provided.droppableProps} ref={provided.innerRef}> {(provided) => (
{fields} <div {...provided.droppableProps} ref={provided.innerRef}>
{provided.placeholder} {fields}
</div> {provided.placeholder}
)} </div>
</StrictModeDroppable> )}
</DragDropContext> </StrictModeDroppable>
</DragDropContext>
) : (
fields
)}
{canEdit ? ( {canEdit ? (
<> <>

View File

@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import API from '../../services/api'; import API from '../../services/api';
import { parseTime } from '../../tools/timeUtil'; import { parseTime } from '../../tools/timeUtil';
import setNotification from '../errors/error-notification'; import setNotification from '../errors/error-notification';
import ModalUpdate from '../playlists/update'; import ModalUpdatePlaylist from '../playlists/update';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import Content from './content'; import Content from './content';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -22,7 +22,7 @@ const Playlist = (item) => {
const toggleActivate = () => { const toggleActivate = () => {
setIsLoading(true); setIsLoading(true);
(isActive ? API.disactivate : API.activate)(id) (isActive ? API.playlists.disactivate : API.playlists.activate)(id)
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
setIsActive(!isActive); setIsActive(!isActive);
@ -50,10 +50,6 @@ const Playlist = (item) => {
setDuration(duration); setDuration(duration);
}, [form.values]); }, [form.values]);
const updatePlaylist = (playlist) => {
setPlaylist(playlist);
};
useEffect(() => { useEffect(() => {
if (JSON.stringify(item) !== '{}') { if (JSON.stringify(item) !== '{}') {
setPlaylist(item); setPlaylist(item);
@ -119,11 +115,11 @@ const Playlist = (item) => {
<Paper p="xs" radius="sm" shadow="sm" withBorder my="md"> <Paper p="xs" radius="sm" shadow="sm" withBorder my="md">
<Content form={form} playlistId={id} playlist={playlist} /> <Content form={form} playlistId={id} playlist={playlist} />
</Paper> </Paper>
<ModalUpdate <ModalUpdatePlaylist
opened={showUpdate} opened={showUpdate}
handler={toggleUpdate} handler={toggleUpdate}
item={playlist} item={playlist}
updatePlaylist={(playlist) => updatePlaylist(playlist)} updatePlaylist={(playlist) => setPlaylist(playlist)}
/> />
</> </>
); );

View File

@ -3,7 +3,7 @@ import PlaylistViewEditor from './playlist-view-editor';
import API from '../../services/api'; import API from '../../services/api';
const ModalCreatePlaylist = ({ opened, handler, addPlaylist }) => { const ModalCreatePlaylist = ({ opened, handler, addPlaylist }) => {
const validated = (item) => { const validate = (item) => {
if (item) { if (item) {
addPlaylist(item); addPlaylist(item);
} }
@ -26,7 +26,7 @@ const ModalCreatePlaylist = ({ opened, handler, addPlaylist }) => {
<PlaylistViewEditor <PlaylistViewEditor
buttonText="Create" buttonText="Create"
APICall={API.playlists.create} APICall={API.playlists.create}
handler={(item) => validated(item)} handler={(item) => validate(item)}
/> />
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>

View File

@ -8,7 +8,7 @@ import { Button } from '@mantine/core';
import GrantAccess, { Perm } from '../../tools/grant-access'; import GrantAccess, { Perm } from '../../tools/grant-access';
const Playlists = () => { const Playlists = () => {
const [showCreate, setShowCreate] = useState(true); const [showCreate, setShowCreate] = useState(false);
const [showUpdate, setShowUpdate] = useState(false); const [showUpdate, setShowUpdate] = useState(false);
const [, setItem] = useState({}); const [, setItem] = useState({});
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@ -63,7 +63,7 @@ const Playlists = () => {
<PlaylistTable <PlaylistTable
data={playlists} data={playlists}
updateItem={setItem} // todo updateItem={setItem} // todo
// eslint-disable-next-line eqeqeq // eslint-disable-next-line
onDelete={(id) => setPlaylist(playlists.filter((item) => item._id != id))} onDelete={(id) => setPlaylist(playlists.filter((item) => item._id != id))}
updateHandler={toggleModalUpdate} updateHandler={toggleModalUpdate}
loadMore={loadMore} loadMore={loadMore}

View File

@ -2,7 +2,7 @@ import { Button, TextInput, Group, Stack } from '@mantine/core';
import { useForm, isNotEmpty } from '@mantine/form'; import { useForm, isNotEmpty } from '@mantine/form';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import setNotification from '../errors/error-notification'; import setNotification from '../errors/error-notification';
import RoleSelector from './role-selector'; import RoleSelector from '../roles/role-selector';
const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => { const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
const handleClose = (playlist) => { const handleClose = (playlist) => {
@ -11,8 +11,8 @@ const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
}; };
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [rolesView, setRolesView] = useState(item?.view.map((role) => role.id.toString()) ?? []); const [rolesView, setRolesView] = useState(item?.view?.map((role) => role.id.toString()) ?? []);
const [rolesEdit, setRolesEdit] = useState(item?.edit.map((role) => role.id.toString()) ?? []); const [rolesEdit, setRolesEdit] = useState(item?.edit?.map((role) => role.id.toString()) ?? []);
useEffect(() => { useEffect(() => {
if (item) { if (item) {
@ -37,10 +37,10 @@ const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
try { try {
setIsLoading(true); setIsLoading(true);
if (item) { if (item) {
await APICall(item?.id, { name: form.values.name }); const view = rolesView.map((roleId) => parseInt(roleId));
// todo permissions update const edit = rolesEdit.map((roleId) => parseInt(roleId));
item.name = form.values.name; const res = await APICall(item?.id, { name: form.values.name, view: view, edit: edit });
handleClose(item); handleClose(res.data);
} else { } else {
const view = rolesView.map((roleId) => parseInt(roleId)); const view = rolesView.map((roleId) => parseInt(roleId));
const edit = rolesEdit.map((roleId) => parseInt(roleId)); const edit = rolesEdit.map((roleId) => parseInt(roleId));
@ -72,7 +72,7 @@ const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
/> />
</Stack> </Stack>
<Group position="right" mt="md"> <Group position="right" mt="md">
<Button variant="light" color="red" onClick={handleClose}> <Button variant="light" color="red" onClick={() => handleClose()}>
Cancel Cancel
</Button> </Button>
<Button type="submit" variant="light" color="green" loading={isLoading}> <Button type="submit" variant="light" color="green" loading={isLoading}>

View File

@ -3,8 +3,10 @@ import API from '../../services/api';
import PlaylistViewEditor from './playlist-view-editor'; import PlaylistViewEditor from './playlist-view-editor';
const ModalUpdatePlaylist = ({ item, opened, handler, updatePlaylist }) => { const ModalUpdatePlaylist = ({ item, opened, handler, updatePlaylist }) => {
const validated = (playlist) => { const validate = (playlist) => {
updatePlaylist(playlist); if (playlist) {
updatePlaylist(playlist);
}
handler(); handler();
}; };
@ -25,7 +27,7 @@ const ModalUpdatePlaylist = ({ item, opened, handler, updatePlaylist }) => {
item={item} item={item}
buttonText="Update" buttonText="Update"
APICall={API.playlists.update} APICall={API.playlists.update}
handler={(playlist) => validated(playlist)} handler={(playlist) => validate(playlist)}
/> />
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>

View File

@ -0,0 +1,38 @@
import { Modal, Text } from '@mantine/core';
import API from '../../services/api';
import RoleViewEditor from './role-view-editor';
const ModalCreateRole = ({ opened, handler, addRole, item }) => {
const validate = (role) => {
if (role) {
addRole(role);
}
handler();
};
return (
<Modal.Root opened={opened} onClose={handler}>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>
<Text fw={700} fz="lg">
Create Role
</Text>
</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<RoleViewEditor
buttonText="Create"
item={item}
APICall={API.roles.create}
handler={(role) => validate(role)}
/>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
};
export default ModalCreateRole;

View File

@ -2,10 +2,19 @@ import { MultiSelect } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import setNotification from '../errors/error-notification'; import setNotification from '../errors/error-notification';
import API from '../../services/api'; import API from '../../services/api';
import { Perm, checkPerm } from '../../tools/grant-access';
import { useAuth } from '../../tools/auth-provider';
import ModalCreateRole from './create';
const RoleSelector = ({ defaultRoles, label, value, setValue }) => { const RoleSelector = ({ defaultRoles, label, value, setValue }) => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [search, setSearch] = useState(); const [search, setSearch] = useState();
const [showCreateRole, setShowCreateRole] = useState(false);
const toggleCreateRole = () => setShowCreateRole(!showCreateRole);
const [query, setQuery] = useState('');
const { user } = useAuth();
const canCreateRole = checkPerm(Perm.CREATE_ROLE, user);
const addRoles = (roles) => { const addRoles = (roles) => {
if (!roles) return; if (!roles) return;
@ -40,24 +49,33 @@ const RoleSelector = ({ defaultRoles, label, value, setValue }) => {
// eslint-disable-next-line // eslint-disable-next-line
}, [defaultRoles]); }, [defaultRoles]);
// creatable
// getCreateLabel={(query) => `+ Create ${query}`}
// onCreate={(query) => {
// const item = { value: query, label: query };
// setData((current) => [...current, item]);
// return item;
// }}
return ( return (
<MultiSelect <>
label={label} <MultiSelect
data={data} label={label}
searchable data={data}
searchValue={search} searchable
onSearchChange={setSearch} searchValue={search}
value={value} onSearchChange={setSearch}
onChange={setValue} value={value}
maxDropdownHeight={160} onChange={setValue}
/> maxDropdownHeight={160}
creatable={canCreateRole}
getCreateLabel={(query) => `+ Create ${query}`}
onCreate={(query) => {
setQuery(query);
setShowCreateRole(true);
}}
/>
{canCreateRole && (
<ModalCreateRole
opened={showCreateRole}
item={{ name: query }}
addRole={(role) => addRoles([role])}
handler={toggleCreateRole}
/>
)}
</>
); );
}; };

View File

@ -0,0 +1,64 @@
import { Button, TextInput, Group, Stack } from '@mantine/core';
import { useForm, isNotEmpty } from '@mantine/form';
import { useEffect, useState } from 'react';
import setNotification from '../errors/error-notification';
const RoleViewEditor = ({ item, handler, buttonText, APICall }) => {
const handleClose = (role) => {
form.reset();
handler(role);
};
const [isLoading, setIsLoading] = useState(false);
const form = useForm({
initialValues: {
name: item?.name ?? '',
},
validate: {
name: isNotEmpty('Name is required'),
},
});
useEffect(() => {
form.setFieldValue('name', item?.name);
return () => {};
}, [item]);
const handleSubmit = async (event) => {
event.preventDefault();
if (form.validate().hasErrors) return;
try {
setIsLoading(true);
if (item?.id) {
const res = await APICall(item?.id, { name: form.values.name });
handleClose(res.data);
} else {
const res = await APICall({ name: form.values.name });
handleClose(res.data);
}
setIsLoading(false);
} catch (err) {
setIsLoading(false);
setNotification(true, err);
}
};
return (
<form onSubmit={handleSubmit}>
<TextInput label="Name" placeholder="Name" withAsterisk {...form.getInputProps('name')} mb="sm" />
todo parent id
users
<Group position="right" mt="md">
<Button variant="light" color="red" onClick={() => handler()}>
Cancel
</Button>
<Button type="submit" variant="light" color="green" loading={isLoading}>
{buttonText}
</Button>
</Group>
</form>
);
};
export default RoleViewEditor;

View File

@ -64,10 +64,10 @@ const API = {
}, },
files: { files: {
upload(data) { upload(data) {
return caller().post('/file', data); return caller().post('/files', data);
}, },
list() { list() {
return caller().get('/file'); return caller().get('/files');
}, },
}, },
profile() { profile() {

View File

@ -4,7 +4,7 @@ module.exports = function(app) {
app.use( app.use(
'/api', '/api',
createProxyMiddleware({ createProxyMiddleware({
target: 'http://192.168.2.183:5500', target: 'http://127.0.0.1:5500',
changeOrigin: true, changeOrigin: true,
}) })
); );

10
src/tools/fileUtil.js Normal file
View File

@ -0,0 +1,10 @@
export const isImage = (type) => {
if (!type) return false;
return type.split('/')[0] === 'image';
};
export const isVideo = (type) => {
if (!type) return false;
return type.split('/')[0] === 'video';
};