creae update devices

This commit is contained in:
Vasya Ryzhkoff 2024-07-23 01:43:13 +07:00
parent d9c3bfa35e
commit 65b924c174
23 changed files with 359 additions and 32 deletions

6
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/Yuna.iml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="DBE_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

12
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="bfd16a45-57c4-4cdd-9553-7a07934e4c31">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Yuna.iml" filepath="$PROJECT_DIR$/.idea/Yuna.iml" />
</modules>
</component>
</project>

7
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/bfd16a45-57c4-4cdd-9553-7a07934e4c31/console_1.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="PostgreSQL" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -80,5 +80,27 @@ namespace Yuna.Tests.Repositories
Assert.NotNull(result);
Assert.True(result.All(x => x.UserId == 1));
}
[Fact]
public async Task Update_Updates()
{
//Arrange
Settings.Init();
var _context = new DapperContext(true);
var repo = new DeviceRepository(_context);
//Act
var initial = await repo.GetById(1);
initial!.Name += "1";
var returnResult = await repo.Update(initial);
var realChanged = await repo.GetById(1);
//Assert
Assert.Equal(initial.Name, returnResult!.Name);
Assert.Equal(initial!.Name, realChanged!.Name);
}
}
}

View File

@ -16,9 +16,6 @@ namespace Yuna.Website.Server.API
app.MapPost("/api/device", CreateDevice)
.WithTags("device");
app.MapDelete("/api/device/{id:long}", () => { })
.WithTags("device");
app.MapGet("/api/device/{deviceId:long}", GetById)
.WithTags("device");
@ -32,6 +29,12 @@ namespace Yuna.Website.Server.API
app.MapPut("/api/device/{deviceId:long}", AddSkillsToDevice)
.WithTags("device");
app.MapPut("/api/device", Update)
.WithTags("device");
app.MapDelete("/api/device/{deviceId:long}", Delete)
.WithTags("device");
}
@ -99,10 +102,18 @@ namespace Yuna.Website.Server.API
}
[Authorize]
public async Task<IResult> Delete(IDeviceService deviceService, long id)
public async Task<IResult> Delete(IDeviceService deviceService, HttpContext context, long deviceId)
{
var result = await deviceService.Delete(id);
if (result is null) return Results.NotFound();
var isAdmin = context.GetRoleFromCookie();
var userId = context.GetUserIdFromCookie();
var deviceToDelete = await deviceService.GetById(deviceId);
if(deviceToDelete is null) return Results.NotFound();
if (userId != deviceToDelete.UserId && !isAdmin) return Results.Forbid();
var result = await deviceService.Delete(deviceId);
if (result is null) return Results.Problem(statusCode: 500);
return Results.Ok(result);
}
@ -129,5 +140,40 @@ namespace Yuna.Website.Server.API
return Results.Ok(result);
}
public class UpdateDeviceRequest
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[JsonPropertyName("deviceUrl")]
public string DeviceUrl { get; set; } = null!;
}
[Authorize]
public async Task<IResult> Update([FromBody] UpdateDeviceRequest request, HttpContext context, IDeviceService deviceService)
{
var userId = context.GetUserIdFromCookie();
var isAdmin = context.GetRoleFromCookie();
var device = await deviceService.GetById(request.Id);
if (device is null) return Results.NotFound();
if (device.UserId != userId && !isAdmin) return Results.Forbid();
device.DeviceUrl = request.DeviceUrl;
device.Name = request.Name;
device.Description = request.Description;
var result = await deviceService.Update(device);
return Results.Ok(result);
}
}
}

View File

@ -36,6 +36,11 @@ namespace Yuna.Website.Server.Infrastructure
private static void LoadConnectionStrs(JsonElement connectionStrs)
{
var env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
{
}
DbConnectionStr = connectionStrs.GetProperty("Db").GetString()!;
}

View File

@ -107,9 +107,6 @@ namespace Yuna.Website.Server.Services.DeviceService
public async Task<Device?> Delete(long id)
{
var prop = await _deviceRepository.GetById(id);
if (prop is null) return null;
var result = await _deviceRepository.Delete(id);
return result;
}
@ -130,5 +127,11 @@ namespace Yuna.Website.Server.Services.DeviceService
var result = await _deviceRepository.GetList(userId);
return result ?? [];
}
public async Task<Device?> Update(Device device)
{
var result = await _deviceRepository.Update(device);
return result;
}
}
}

View File

@ -12,5 +12,6 @@ namespace Yuna.Website.Server.Services.DeviceService
public Task<Device?> Delete(long id);
public Task<Device?> AddProps(IReadOnlyList<Prop> props, long deviceId);
public Task<Dictionary<long, string>?> FetchPropsData(Device device);
public Task<Device?> Update(Device device);
}
}

View File

@ -1,5 +1,6 @@
using Dapper;
using System.Data;
using Yuna.Website.Server.Model;
namespace Yuna.Website.Server.Storage.Repositories.Device
{
@ -36,9 +37,18 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
return result;
}
public Task<Model.Device?> Delete(long id)
public async Task<Model.Device?> Delete(long id)
{
throw new NotImplementedException();
var query =
$@"
UPDATE ""Yuna_Devices""
SET ""IsDeleted"" = TRUE
WHERE ""Id"" = {id}
AND NOT ""IsDeleted""
returning *
";
return await _context.Connection.QuerySingleOrDefaultAsync<Model.Device>(query);
}
public async Task<Model.Device?> GetById(long id)
@ -58,7 +68,8 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
FROM ""Yuna_Devices"" d
LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId""
LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id""
WHERE d.""Id"" = {id}";
WHERE d.""Id"" = {id}
AND NOT d.""IsDeleted""";
var deviceDict = new Dictionary<long, Model.Device>();
var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>(
@ -96,7 +107,9 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
p.""Type"" as {nameof(Model.Prop.Type)}
FROM ""Yuna_Devices"" d
LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId""
LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id""";
LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id""
WHERE NOT d.""IsDeleted""
ORDER BY d.""Id""";
var deviceDict = new Dictionary<long, Model.Device>();
var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>(
@ -135,7 +148,9 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
FROM ""Yuna_Devices"" d
LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId""
LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id""
WHERE d.""UserId"" = {userId};";
WHERE d.""UserId"" = {userId}
AND NOT d.""IsDeleted""
ORDER BY d.""Id""";
var deviceDict = new Dictionary<long, Model.Device>();
var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>(
@ -156,5 +171,22 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
return result.Distinct().ToList();
}
public async Task<Model.Device?> Update(Model.Device device)
{
var query =
$@"
UPDATE public.""Yuna_Devices""
SET ""Name"" = @Name,
""Description"" = @Description,
""DeviceUrl"" = @DeviceUrl,
""UserId"" = @UserId
WHERE ""Id"" = @Id
AND NOT ""IsDeleted""
returning *
";
return await _context.Connection.QuerySingleOrDefaultAsync<Model.Device>(query, device);
}
}
}

View File

@ -8,7 +8,7 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
public Task<IReadOnlyList<Model.Device>> GetList();
public Task<IReadOnlyList<Model.Device>> GetList(long userId);
public Task<Model.Device?> Create(Model.Device device);
//public Task<User?> Update(User user);
public Task<Model.Device?> Update(Model.Device device);
public Task<Model.Device?> Delete(long id);
public Task AddProps(IReadOnlyList<Model.Prop> skills, long deviceId);
}

View File

@ -1,11 +1,14 @@
import { Header } from "../../components/Header"
import { DeviceList } from "./components/DeviceList";
import { EditModal } from "./components/modals/EditModal";
export const HomePage = () => {
return (
<>
<Header />
<DeviceList />
<EditModal />
</>
);
}

View File

@ -2,8 +2,10 @@ import { useEffect, useMemo } from "react";
import "../../../resources/styles/home.scss"
import { IDeviceDto, IPropDto } from "../types";
import { DeviceCardStore } from "../model/DeviceCardStore";
import { Button, Center, Skeleton, useInterval } from "@chakra-ui/react";
import { Button, Center, IconButton, Skeleton, Spacer, useInterval } from "@chakra-ui/react";
import { observer } from "mobx-react";
import { MdOutlineDelete, MdOutlineSettings } from "react-icons/md";
import { homePageStore } from "../model/HomePageStore";
@ -36,11 +38,25 @@ export const DeviceCard = observer((props: { data: IDeviceDto }) => {
<div className="device_card">
<div className="h1_container">
<h1>{props.data.name}</h1>
<Button>sdfs</Button>
<div className="button_container">
<IconButton
colorScheme="red"
onClick={() => homePageStore.deleteDevice(props.data.id)}
icon={<MdOutlineDelete />}
aria-label={"delete"}
fontSize={"90%"}
size={'sm'} />
<IconButton
onClick={() => homePageStore.setEditModalOpened(props.data.id, true)}
icon={<MdOutlineSettings />}
aria-label={"edit"}
fontSize={"90%"}
size={'sm'} />
</div>
</div>
<p style={{ textAlign: 'center' }}>{props.data.description}</p>
<ul>
{getPropsList(props.data.props, deviceCardStore.isLoading)}
{getPropsList(props.data.props ?? [], deviceCardStore.isLoading)}
</ul>
</div>
</div>

View File

@ -0,0 +1,71 @@
import { observer } from "mobx-react";
import { homePageStore } from "../../model/HomePageStore";
import { Button, Center, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Stack, Textarea } from "@chakra-ui/react";
import { IDeviceDto } from "../../types";
import { useEffect, useState } from "react";
export const EditModal = observer(() => {
const currentDevice = homePageStore.currentEditDevice
const [name, setName] = useState(currentDevice?.name);
const [description, setDescription] = useState(currentDevice?.description);
const [url, setUrl] = useState(currentDevice?.deviceUrl);
useEffect(
() => {
setName(currentDevice?.name);
setDescription(currentDevice?.description);
setUrl(currentDevice?.deviceUrl)
},
[currentDevice]
)
const onUpdateDevice = () => {
homePageStore.setEditModalOpened(currentDevice!.id, false);
homePageStore.updateDevice({
id: currentDevice?.id,
name: name,
description: description,
deviceUrl: url
} as IDeviceDto);
}
return (<Modal
onClose={() => homePageStore.setEditModalOpened(homePageStore.editModalCurrentId!, false)}
isOpen={homePageStore.isEditModalOpened}
isCentered
blockScrollOnMount={false}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Редактировать</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Stack spacing={3}>
<label>Название:</label>
<Input
value={name ?? ""}
onChange={e => setName(e.target.value)}
disabled={homePageStore.isLoading} />
<label>Описание:</label>
<Textarea
value={description ?? ""}
onChange={e => setDescription(e.target.value)}
disabled={homePageStore.isLoading} />
<label>URL:</label>
<Input
value={url ?? ""}
onChange={e => setUrl(e.target.value)}
disabled={homePageStore.isLoading} />
</Stack>
<Center>
<Button
colorScheme="green"
marginTop={10}
onClick={() => onUpdateDevice()}
isLoading={homePageStore.isLoading}>Сохранить</Button>
</Center>
</ModalBody>
</ModalContent>
</Modal>);
});

View File

@ -1,15 +1,29 @@
import { makeAutoObservable } from "mobx";
import { IDeviceDto } from "../types";
import { HomePageService } from "../services/HomePageService";
import { error, info } from "../../../utils/ToastHelper";
class HomePageStore {
devices: IDeviceDto[] = []
isLoading: boolean = false;
isEditModalOpened: boolean = false;
editModalCurrentId: number | null = null;
currentEditDevice: IDeviceDto | undefined;
constructor() {
makeAutoObservable(this);
}
setEditModalOpened(id: number, isOpened: boolean): void {
this.currentEditDevice = { ...this.devices.find(device => device.id === id) } as IDeviceDto;
this.isEditModalOpened = isOpened;
}
setCurrentEditDevice(value: IDeviceDto | undefined) {
this.currentEditDevice = value;
}
setDevices(value: IDeviceDto[]) {
this.devices = value;
@ -21,6 +35,29 @@ class HomePageStore {
this.setDevices(result);
this.isLoading = false;
}
async updateDevice(value: IDeviceDto) {
const result = await HomePageService.updateDevice(value);
if (result === null) {
error("Ошибка", "Запрос не был выполнен");
return;
}
this.setCurrentEditDevice(undefined);
info("Успешно", "Изменения были сохранены")
await this.loadDevices();
}
async deleteDevice(value: number) {
const result = await HomePageService.deleteDevice(value);
if (result === null) {
error("Ошибка", "Запрос не был выполнен");
return;
}
info("Успешно", "Устройство было удалено");
await this.loadDevices();
}
}
export const homePageStore = new HomePageStore();

View File

@ -21,4 +21,30 @@ export class HomePageService {
if (response.status === 200) return response.data;
return null;
}
public static async updateDevice(dto: IDeviceDto): Promise<IDeviceDto | null> {
try {
const response = await api.put<IDeviceDto>(`device`, dto);
if (response.status === 200) return response.data;
}
catch (error) {
console.error("Error: ", error);
}
return null;
}
public static async deleteDevice(id: number): Promise<IDeviceDto | null> {
try {
const response = await api.delete<IDeviceDto>(`device/${id}`);
if (response.status === 200) return response.data;
}
catch (error) {
console.error("Error: ", error);
}
return null;
}
}

View File

@ -1,9 +1,9 @@
export interface IDeviceDto {
id: number;
name: string;
description: string;
props: IPropDto[]
deviceUrl: string;
description?: string;
props?: IPropDto[]
deviceUrl?: string;
}
export enum propType {

View File

@ -6,6 +6,7 @@
width: 80%;
height: 70px;
box-shadow: 5px 5px 0px #319795;
z-index: 1;
/* x-offset y-offset blur-radius color */
h1 {

View File

@ -15,19 +15,26 @@
display: flex;
flex-direction: column;
padding: 0px 50px;
padding: 0px 0px;
.h1_container {
overflow: hidden;
height: 80px;
position: relative;
overflow: visible;
width: 100%;
h1 {}
h1 {
max-width: 450px;
display: block;
margin: auto;
}
button {
position: relative;
top: -50px;
width: 10px;
float: right;
.button_container {
display: flex;
flex-direction: row;
gap: 5px;
position: absolute;
top: 25px;
right: 30px;
}
}

11
package-lock.json generated
View File

@ -5,7 +5,8 @@
"packages": {
"": {
"dependencies": {
"mobx-react": "^9.1.1"
"mobx-react": "^9.1.1",
"react-icons": "^5.2.1"
}
},
"node_modules/js-tokens": {
@ -96,6 +97,14 @@
"node": ">=0.10.0"
}
},
"node_modules/react-icons": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
"integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",

View File

@ -1,5 +1,6 @@
{
"dependencies": {
"mobx-react": "^9.1.1"
"mobx-react": "^9.1.1",
"react-icons": "^5.2.1"
}
}