diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..8bf4d45 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,6 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Yuna.iml b/.idea/Yuna.iml new file mode 100644 index 0000000..0399c4b --- /dev/null +++ b/.idea/Yuna.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..f4f086b --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/ + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6e64bdc --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..f75096d --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Yuna.Tests/Repositories/DeviceRepositoryTests.cs b/Yuna.Tests/Repositories/DeviceRepositoryTests.cs index 107ce4f..0b81aaf 100644 --- a/Yuna.Tests/Repositories/DeviceRepositoryTests.cs +++ b/Yuna.Tests/Repositories/DeviceRepositoryTests.cs @@ -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); + } } } \ No newline at end of file diff --git a/Yuna.Website/Yuna.Website.Server/API/DeviceEndpoints.cs b/Yuna.Website/Yuna.Website.Server/API/DeviceEndpoints.cs index 348f835..ba8c4f9 100644 --- a/Yuna.Website/Yuna.Website.Server/API/DeviceEndpoints.cs +++ b/Yuna.Website/Yuna.Website.Server/API/DeviceEndpoints.cs @@ -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 Delete(IDeviceService deviceService, long id) + public async Task 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 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); + } } } diff --git a/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs b/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs index 96bd448..b6bae6e 100644 --- a/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs +++ b/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs @@ -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()!; } diff --git a/Yuna.Website/Yuna.Website.Server/Services/DeviceService/DeviceService.cs b/Yuna.Website/Yuna.Website.Server/Services/DeviceService/DeviceService.cs index 9cc8b1b..9701855 100644 --- a/Yuna.Website/Yuna.Website.Server/Services/DeviceService/DeviceService.cs +++ b/Yuna.Website/Yuna.Website.Server/Services/DeviceService/DeviceService.cs @@ -107,9 +107,6 @@ namespace Yuna.Website.Server.Services.DeviceService public async Task 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 Update(Device device) + { + var result = await _deviceRepository.Update(device); + return result; + } } } diff --git a/Yuna.Website/Yuna.Website.Server/Services/DeviceService/IDeviceService.cs b/Yuna.Website/Yuna.Website.Server/Services/DeviceService/IDeviceService.cs index a303450..b39afc7 100644 --- a/Yuna.Website/Yuna.Website.Server/Services/DeviceService/IDeviceService.cs +++ b/Yuna.Website/Yuna.Website.Server/Services/DeviceService/IDeviceService.cs @@ -12,5 +12,6 @@ namespace Yuna.Website.Server.Services.DeviceService public Task Delete(long id); public Task AddProps(IReadOnlyList props, long deviceId); public Task?> FetchPropsData(Device device); + public Task Update(Device device); } } diff --git a/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/DeviceRepository.cs b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/DeviceRepository.cs index 786f7fd..db055c0 100644 --- a/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/DeviceRepository.cs +++ b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/DeviceRepository.cs @@ -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 Delete(long id) + public async Task 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(query); } public async Task 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(); var result = await _context.Connection.QueryAsync( @@ -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(); var result = await _context.Connection.QueryAsync( @@ -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(); var result = await _context.Connection.QueryAsync( @@ -156,5 +171,22 @@ namespace Yuna.Website.Server.Storage.Repositories.Device return result.Distinct().ToList(); } + + public async Task 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(query, device); + } } } \ No newline at end of file diff --git a/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/IDeviceRepository.cs b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/IDeviceRepository.cs index 9995c7e..6b5852b 100644 --- a/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/IDeviceRepository.cs +++ b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/Device/IDeviceRepository.cs @@ -8,7 +8,7 @@ namespace Yuna.Website.Server.Storage.Repositories.Device public Task> GetList(); public Task> GetList(long userId); public Task Create(Model.Device device); - //public Task Update(User user); + public Task Update(Model.Device device); public Task Delete(long id); public Task AddProps(IReadOnlyList skills, long deviceId); } diff --git a/Yuna.Website/yuna.website.client/src/pages/home/HomePage.tsx b/Yuna.Website/yuna.website.client/src/pages/home/HomePage.tsx index f37e753..4b2f05b 100644 --- a/Yuna.Website/yuna.website.client/src/pages/home/HomePage.tsx +++ b/Yuna.Website/yuna.website.client/src/pages/home/HomePage.tsx @@ -1,11 +1,14 @@ import { Header } from "../../components/Header" import { DeviceList } from "./components/DeviceList"; +import { EditModal } from "./components/modals/EditModal"; export const HomePage = () => { return ( <> + + > ); } \ No newline at end of file diff --git a/Yuna.Website/yuna.website.client/src/pages/home/components/DeviceCard.tsx b/Yuna.Website/yuna.website.client/src/pages/home/components/DeviceCard.tsx index a607bf8..1ed18aa 100644 --- a/Yuna.Website/yuna.website.client/src/pages/home/components/DeviceCard.tsx +++ b/Yuna.Website/yuna.website.client/src/pages/home/components/DeviceCard.tsx @@ -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 }) => { {props.data.name} - sdfs + + homePageStore.deleteDevice(props.data.id)} + icon={} + aria-label={"delete"} + fontSize={"90%"} + size={'sm'} /> + homePageStore.setEditModalOpened(props.data.id, true)} + icon={} + aria-label={"edit"} + fontSize={"90%"} + size={'sm'} /> + {props.data.description} - {getPropsList(props.data.props, deviceCardStore.isLoading)} + {getPropsList(props.data.props ?? [], deviceCardStore.isLoading)} diff --git a/Yuna.Website/yuna.website.client/src/pages/home/components/modals/EditModal.tsx b/Yuna.Website/yuna.website.client/src/pages/home/components/modals/EditModal.tsx new file mode 100644 index 0000000..7af8fc8 --- /dev/null +++ b/Yuna.Website/yuna.website.client/src/pages/home/components/modals/EditModal.tsx @@ -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 ( homePageStore.setEditModalOpened(homePageStore.editModalCurrentId!, false)} + isOpen={homePageStore.isEditModalOpened} + isCentered + blockScrollOnMount={false} + > + + + Редактировать + + + + Название: + setName(e.target.value)} + disabled={homePageStore.isLoading} /> + Описание: + setDescription(e.target.value)} + disabled={homePageStore.isLoading} /> + URL: + setUrl(e.target.value)} + disabled={homePageStore.isLoading} /> + + + onUpdateDevice()} + isLoading={homePageStore.isLoading}>Сохранить + + + + ); +}); \ No newline at end of file diff --git a/Yuna.Website/yuna.website.client/src/pages/home/model/HomePageStore.ts b/Yuna.Website/yuna.website.client/src/pages/home/model/HomePageStore.ts index 0bf7a30..4d1023a 100644 --- a/Yuna.Website/yuna.website.client/src/pages/home/model/HomePageStore.ts +++ b/Yuna.Website/yuna.website.client/src/pages/home/model/HomePageStore.ts @@ -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(); \ No newline at end of file diff --git a/Yuna.Website/yuna.website.client/src/pages/home/services/HomePageService.tsx b/Yuna.Website/yuna.website.client/src/pages/home/services/HomePageService.tsx index 0b7ad8d..40381fa 100644 --- a/Yuna.Website/yuna.website.client/src/pages/home/services/HomePageService.tsx +++ b/Yuna.Website/yuna.website.client/src/pages/home/services/HomePageService.tsx @@ -21,4 +21,30 @@ export class HomePageService { if (response.status === 200) return response.data; return null; } + + public static async updateDevice(dto: IDeviceDto): Promise { + try { + const response = await api.put(`device`, dto); + if (response.status === 200) return response.data; + } + + catch (error) { + console.error("Error: ", error); + } + + return null; + } + + public static async deleteDevice(id: number): Promise { + try { + const response = await api.delete(`device/${id}`); + if (response.status === 200) return response.data; + } + + catch (error) { + console.error("Error: ", error); + } + + return null; + } } \ No newline at end of file diff --git a/Yuna.Website/yuna.website.client/src/pages/home/types.ts b/Yuna.Website/yuna.website.client/src/pages/home/types.ts index d50b448..981b572 100644 --- a/Yuna.Website/yuna.website.client/src/pages/home/types.ts +++ b/Yuna.Website/yuna.website.client/src/pages/home/types.ts @@ -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 { diff --git a/Yuna.Website/yuna.website.client/src/resources/styles/header.scss b/Yuna.Website/yuna.website.client/src/resources/styles/header.scss index 5858f84..a98d3f3 100644 --- a/Yuna.Website/yuna.website.client/src/resources/styles/header.scss +++ b/Yuna.Website/yuna.website.client/src/resources/styles/header.scss @@ -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 { diff --git a/Yuna.Website/yuna.website.client/src/resources/styles/home.scss b/Yuna.Website/yuna.website.client/src/resources/styles/home.scss index 8d64f1d..da84f1d 100644 --- a/Yuna.Website/yuna.website.client/src/resources/styles/home.scss +++ b/Yuna.Website/yuna.website.client/src/resources/styles/home.scss @@ -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; } } diff --git a/package-lock.json b/package-lock.json index b1a712f..2fdcf61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 873162f..0416b44 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { - "mobx-react": "^9.1.1" + "mobx-react": "^9.1.1", + "react-icons": "^5.2.1" } }
{props.data.description}