[test-entity]: front pt.2
This commit is contained in:
parent
a60304ca0f
commit
658a351d28
@ -3,4 +3,5 @@ export const WIND_ENDPOINTS = {
|
||||
turbineType: 'api/wind/turbine_type',
|
||||
parks: 'api/wind/parks',
|
||||
park: 'api/wind/park',
|
||||
parkTurbine: 'api/wind/park_turbine',
|
||||
};
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { ApiResponse } from '@api/types';
|
||||
import { ParkFormValues } from '@components/pages/park-page/types';
|
||||
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
|
||||
|
||||
import { api } from '../api';
|
||||
import { WIND_ENDPOINTS } from './constants';
|
||||
import { Park, ParkTurbine, ParkWithTurbines, TurbineType } from './types';
|
||||
import { packTurbineTypes } from './utils';
|
||||
import { packPark, packParkTurbine, packTurbineTypes } from './utils';
|
||||
|
||||
export const getTurbineTypes = () => {
|
||||
return api.get<TurbineType[]>(WIND_ENDPOINTS.turbines);
|
||||
@ -63,3 +64,47 @@ export const getParkWithTurbines = async (
|
||||
error: parkPesponse.error || turbinesResponse.error || null,
|
||||
};
|
||||
};
|
||||
|
||||
export const createPark = async (formValues: Partial<ParkFormValues>) => {
|
||||
const parkPesponse = await api.post<Park>(
|
||||
WIND_ENDPOINTS.park,
|
||||
packPark(formValues),
|
||||
);
|
||||
await Promise.all(
|
||||
formValues.turbines?.map((t) => {
|
||||
return api.post(
|
||||
WIND_ENDPOINTS.parkTurbine,
|
||||
packParkTurbine(t, parkPesponse.data.id),
|
||||
);
|
||||
}),
|
||||
);
|
||||
return getParkWithTurbines(String(parkPesponse.data.id));
|
||||
};
|
||||
|
||||
export const updatePark = async (
|
||||
formValues: Partial<ParkFormValues>,
|
||||
id: string,
|
||||
) => {
|
||||
const parkPesponse = await api.put<Park>(
|
||||
`${WIND_ENDPOINTS.park}/${id}`,
|
||||
packPark(formValues),
|
||||
);
|
||||
await Promise.all(
|
||||
formValues.turbines?.map((t) => {
|
||||
if (t.new) {
|
||||
return api.post(
|
||||
WIND_ENDPOINTS.parkTurbine,
|
||||
packParkTurbine(t, parkPesponse.data.id),
|
||||
);
|
||||
}
|
||||
if (t.delete) {
|
||||
return api.delete(`${WIND_ENDPOINTS.parkTurbine}/${t.id}`);
|
||||
}
|
||||
return api.put(
|
||||
`${WIND_ENDPOINTS.parkTurbine}/${parkPesponse.data.id}/${t.id}`,
|
||||
packParkTurbine(t, parkPesponse.data.id),
|
||||
);
|
||||
}),
|
||||
);
|
||||
return getParkWithTurbines(id);
|
||||
};
|
||||
|
@ -1,9 +1,32 @@
|
||||
import {
|
||||
ParkFormTurbine,
|
||||
ParkFormValues,
|
||||
} from '@components/pages/park-page/types';
|
||||
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
|
||||
|
||||
export const packTurbineTypes = (values: Partial<TurbineTypeFormValues>) => {
|
||||
return {
|
||||
Name: values.name ?? '',
|
||||
Height: parseInt(values.height ?? '0'),
|
||||
BladeLength: parseInt(values.bladeLength ?? '0'),
|
||||
Height: parseInt(values.height || '0'),
|
||||
BladeLength: parseInt(values.bladeLength || '0'),
|
||||
};
|
||||
};
|
||||
|
||||
export const packPark = (values: Partial<ParkFormValues>) => {
|
||||
return {
|
||||
Name: values.name ?? '',
|
||||
CenterLatitude: parseInt(values.centerLatitude || '0'),
|
||||
CenterLongitude: parseInt(values.centerLongitude || '0'),
|
||||
};
|
||||
};
|
||||
|
||||
export const packParkTurbine = (turbine: ParkFormTurbine, parkId: number) => {
|
||||
return {
|
||||
wind_park_id: parkId,
|
||||
turbine_id: turbine.id,
|
||||
x_offset: parseInt(turbine.xOffset || '0'),
|
||||
y_offset: parseInt(turbine.yOffset || '0'),
|
||||
angle: parseInt(turbine.angle || '0'),
|
||||
comment: turbine.comment ?? '',
|
||||
};
|
||||
};
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { getParkWithTurbines, ParkWithTurbines } from '@api/wind';
|
||||
import { Button, Heading, NumberInput, TextInput } from '@components/ui';
|
||||
import {
|
||||
createPark,
|
||||
getParkWithTurbines,
|
||||
ParkWithTurbines,
|
||||
updatePark,
|
||||
} from '@api/wind';
|
||||
import { Button, Heading, NumberField, TextInput } from '@components/ui';
|
||||
import { ParkTurbines } from '@components/ux';
|
||||
import { Controller, useForm } from '@utils/form';
|
||||
import { useRoute } from '@utils/route';
|
||||
import { ROUTES, useRoute } from '@utils/route';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { ParkTurbines } from './components';
|
||||
import styles from './styles.module.scss';
|
||||
import { ParkFormValues } from './types';
|
||||
import { unpackPark } from './utils';
|
||||
@ -14,6 +19,7 @@ export function ParkPage() {
|
||||
const [park, setPark] = useState<ParkWithTurbines>(null);
|
||||
const [pending, setPending] = useState<boolean>(false);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const route = useRoute();
|
||||
|
||||
const { register, control, getValues, reset } = useForm<ParkFormValues>({});
|
||||
@ -35,6 +41,20 @@ export function ParkPage() {
|
||||
fetchPark();
|
||||
}, [id]);
|
||||
|
||||
const handleFormSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setPending(true);
|
||||
if (isEdit) {
|
||||
const response = await updatePark(getValues(), id);
|
||||
setPark(response.data);
|
||||
reset(unpackPark(response.data));
|
||||
} else {
|
||||
const response = await createPark(getValues());
|
||||
navigate(ROUTES.park.path.replace(':id', String(response.data.id)));
|
||||
}
|
||||
setPending(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (isEdit) {
|
||||
reset(unpackPark(park));
|
||||
@ -44,7 +64,7 @@ export function ParkPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.page} onSubmit={handleFormSubmit}>
|
||||
<Heading tag="h1">{route.title}</Heading>
|
||||
<form className={styles.form}>
|
||||
<header>
|
||||
@ -55,13 +75,13 @@ export function ParkPage() {
|
||||
<Controller
|
||||
{...control('centerLatitude')}
|
||||
render={(props) => (
|
||||
<NumberInput label={{ text: 'Center Latitude' }} {...props} />
|
||||
<NumberField label={{ text: 'Center Latitude' }} {...props} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
{...control('centerLongitude')}
|
||||
render={(props) => (
|
||||
<NumberInput label={{ text: 'Center Longitude' }} {...props} />
|
||||
<NumberField label={{ text: 'Center Longitude' }} {...props} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@ -76,7 +96,9 @@ export function ParkPage() {
|
||||
</form>
|
||||
<Controller
|
||||
{...control('turbines')}
|
||||
render={(props) => <ParkTurbines {...props} />}
|
||||
render={(props) => (
|
||||
<ParkTurbines savedTurbines={park?.turbines ?? []} {...props} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './park-turbines';
|
@ -1,19 +0,0 @@
|
||||
import { DataGrid } from '@components/ui/data-grid';
|
||||
import React from 'react';
|
||||
|
||||
import { columns } from './constants';
|
||||
import { ParkTurbinesProps } from './types';
|
||||
|
||||
export function ParkTurbines({ value = [] }: ParkTurbinesProps) {
|
||||
return (
|
||||
<div>
|
||||
<div></div>
|
||||
<DataGrid
|
||||
items={value}
|
||||
columns={columns}
|
||||
getItemKey={({ id }) => String(id)}
|
||||
selectedItems={[]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { DataGridColumnConfig } from '@components/ui/data-grid/types';
|
||||
import { ParkTurbine } from 'src/api/wind';
|
||||
|
||||
export const columns: DataGridColumnConfig<ParkTurbine>[] = [
|
||||
{ name: 'Id', getText: (t) => String(t.id) },
|
||||
{ name: 'Name', getText: (t) => t.name },
|
||||
{ name: 'X', getText: (t) => String(t.xOffset) },
|
||||
{ name: 'Y', getText: (t) => String(t.yOffset) },
|
||||
{ name: 'Angle', getText: (t) => String(t.angle) },
|
||||
{ name: 'Comment', getText: (t) => String(t.comment), flex: '2' },
|
||||
];
|
@ -1,6 +0,0 @@
|
||||
import { ParkTurbine } from '@api/wind';
|
||||
|
||||
export type ParkTurbinesProps = {
|
||||
value?: ParkTurbine[];
|
||||
onChange?: (value: ParkTurbine[]) => void;
|
||||
};
|
@ -1,8 +1,17 @@
|
||||
import { ParkTurbine } from '@api/wind';
|
||||
export type ParkFormTurbine = {
|
||||
id: number;
|
||||
name: string;
|
||||
xOffset: string;
|
||||
yOffset: string;
|
||||
angle: string;
|
||||
comment: string;
|
||||
new?: boolean;
|
||||
delete?: boolean;
|
||||
};
|
||||
|
||||
export type ParkFormValues = {
|
||||
name: string;
|
||||
centerLatitude: string;
|
||||
centerLongitude: string;
|
||||
turbines: ParkTurbine[];
|
||||
turbines: ParkFormTurbine[];
|
||||
};
|
||||
|
@ -7,6 +7,13 @@ export const unpackPark = (park: ParkWithTurbines): ParkFormValues => {
|
||||
name: park.name,
|
||||
centerLatitude: String(park.centerLatitude),
|
||||
centerLongitude: String(park.centerLongitude),
|
||||
turbines: park.turbines,
|
||||
turbines: park.turbines.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
xOffset: String(t.xOffset),
|
||||
yOffset: String(t.yOffset),
|
||||
angle: String(t.angle),
|
||||
comment: t.comment,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { DataGridColumnConfig } from '@components/ui/data-grid/types';
|
||||
import { Park } from 'src/api/wind';
|
||||
|
||||
export const columns: DataGridColumnConfig<Park>[] = [
|
||||
{ name: 'Name', getText: (t) => t.name, flex: '2' },
|
||||
{ name: 'Name', getText: (t) => t.name, width: 2 },
|
||||
{ name: 'Center Latitude', getText: (t) => String(t.centerLatitude) },
|
||||
{ name: 'Center Longitude', getText: (t) => String(t.centerLongitude) },
|
||||
];
|
||||
|
@ -12,7 +12,6 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--clr-border-100);
|
||||
border-radius: 15px;
|
||||
background-color: var(--clr-layer-200);
|
||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
getTurbineType,
|
||||
TurbineType,
|
||||
} from '@api/wind';
|
||||
import { Button, Heading, NumberInput, TextInput } from '@components/ui';
|
||||
import { Button, Heading, NumberField, TextInput } from '@components/ui';
|
||||
import { Controller, useForm } from '@utils/form';
|
||||
import { ROUTES, useRoute } from '@utils/route';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@ -47,6 +47,7 @@ export function TurbineTypePage() {
|
||||
if (isEdit) {
|
||||
const response = await editTurbineTypes(getValues(), id);
|
||||
setTurbineType(response.data);
|
||||
reset(unpackTurbineType(response.data));
|
||||
} else {
|
||||
const response = await createTurbineTypes(getValues());
|
||||
navigate(
|
||||
@ -76,13 +77,13 @@ export function TurbineTypePage() {
|
||||
<Controller
|
||||
{...control('height')}
|
||||
render={(props) => (
|
||||
<NumberInput label={{ text: 'Height' }} {...props} />
|
||||
<NumberField label={{ text: 'Height' }} {...props} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
{...control('bladeLength')}
|
||||
render={(props) => (
|
||||
<NumberInput label={{ text: 'Blade length' }} {...props} />
|
||||
<NumberField label={{ text: 'Blade length' }} {...props} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import { DataGridColumnConfig } from '@components/ui/data-grid/types';
|
||||
import { TurbineType } from 'src/api/wind';
|
||||
|
||||
export const columns: DataGridColumnConfig<TurbineType>[] = [
|
||||
{ name: 'Name', getText: (t) => t.name, flex: '2' },
|
||||
{ name: 'Name', getText: (t) => t.name, width: 2 },
|
||||
{ name: 'Height', getText: (t) => String(t.height) },
|
||||
{ name: 'Blade length', getText: (t) => String(t.bladeLength) },
|
||||
];
|
||||
|
@ -12,7 +12,6 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--clr-border-100);
|
||||
border-radius: 15px;
|
||||
background-color: var(--clr-layer-200);
|
||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||
|
@ -15,6 +15,11 @@ export function DataGrid<T>({
|
||||
}: DataGridProps<T>) {
|
||||
const [allItemsSelected, setAllItemsSelected] = useState<boolean>(false);
|
||||
|
||||
const columnsTemplate = useMemo(() => {
|
||||
const main = columns.map((c) => `${c.width ?? 1}fr`).join(' ');
|
||||
return `auto ${main}`;
|
||||
}, []);
|
||||
|
||||
const selectedItemsMap = useMemo(() => {
|
||||
const map: Record<string, T> = {};
|
||||
for (let i = 0; i < selectedItems.length; i += 1) {
|
||||
@ -35,7 +40,7 @@ export function DataGrid<T>({
|
||||
const handleItemSelect = (key: string, item: T) => {
|
||||
const selected = Boolean(selectedItemsMap[key]);
|
||||
if (!multiselect) {
|
||||
onItemsSelect(selected ? [] : [item]);
|
||||
onItemsSelect?.(selected ? [] : [item]);
|
||||
return;
|
||||
}
|
||||
onItemsSelect?.(
|
||||
@ -52,6 +57,7 @@ export function DataGrid<T>({
|
||||
columns={columns}
|
||||
allItemsSelected={allItemsSelected}
|
||||
onSelectAllItems={handleSelectAllItems}
|
||||
columnsTemplate={columnsTemplate}
|
||||
/>
|
||||
{items.map((item) => {
|
||||
const key = String(getItemKey(item));
|
||||
@ -62,6 +68,7 @@ export function DataGrid<T>({
|
||||
selected={Boolean(selectedItemsMap[key])}
|
||||
onSelect={() => handleItemSelect(key, item)}
|
||||
key={getItemKey(item)}
|
||||
columnsTemplate={columnsTemplate}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -14,6 +14,7 @@ export function DataGridHeader<T>({
|
||||
columns,
|
||||
allItemsSelected,
|
||||
onSelectAllItems,
|
||||
columnsTemplate,
|
||||
}: DataGridHeaderProps<T>) {
|
||||
const [sort, setSort] = useState<DataGridSort>({ order: 'asc', column: '' });
|
||||
|
||||
@ -30,7 +31,10 @@ export function DataGridHeader<T>({
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<header
|
||||
className={styles.header}
|
||||
style={{ gridTemplateColumns: columnsTemplate }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={allItemsSelected}
|
||||
onChange={onSelectAllItems}
|
||||
@ -44,7 +48,6 @@ export function DataGridHeader<T>({
|
||||
});
|
||||
return (
|
||||
<RawButton
|
||||
style={{ flex: column.flex }}
|
||||
className={cellClassName}
|
||||
key={column.name}
|
||||
onClick={() => handleSortButtonClick(column.name)}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.header {
|
||||
display: flex;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
@ -13,7 +13,6 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: solid 1px var(--clr-border-100);
|
||||
|
@ -4,4 +4,5 @@ export type DataGridHeaderProps<T> = {
|
||||
columns: DataGridColumnConfig<T>[];
|
||||
allItemsSelected: boolean;
|
||||
onSelectAllItems: () => void;
|
||||
columnsTemplate: string;
|
||||
};
|
||||
|
@ -10,20 +10,20 @@ export function DataGridRow<T>({
|
||||
columns,
|
||||
selected,
|
||||
onSelect,
|
||||
columnsTemplate,
|
||||
}: DataGridRowProps<T>) {
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<div
|
||||
className={styles.row}
|
||||
style={{ gridTemplateColumns: columnsTemplate }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
label={{ className: styles.checkboxLabel }}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
{columns.map((column) => (
|
||||
<div
|
||||
className={styles.cell}
|
||||
style={{ flex: column.flex }}
|
||||
key={column.name}
|
||||
>
|
||||
<div className={styles.cell} key={column.name}>
|
||||
<Span>{column.getText(object)}</Span>
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.row {
|
||||
display: flex;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
@ -11,7 +11,6 @@
|
||||
.cell {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 1 0 0;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: solid 1px var(--clr-border-100);
|
||||
|
@ -5,4 +5,5 @@ export type DataGridRowProps<T> = {
|
||||
columns: DataGridColumnConfig<T>[];
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
columnsTemplate: string;
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ export function DataGridPreview() {
|
||||
|
||||
const columns: DataGridColumnConfig<Cat>[] = [
|
||||
{ name: 'Name', getText: (cat) => cat.name },
|
||||
{ name: 'Breed', getText: (cat) => cat.breed, scale: 2 },
|
||||
{ name: 'Breed', getText: (cat) => cat.breed, width: 2 },
|
||||
{ name: 'Age', getText: (cat) => cat.age },
|
||||
{ name: 'Color', getText: (cat) => cat.color },
|
||||
];
|
||||
|
@ -4,7 +4,7 @@ export type DataGridColumnConfig<T> = {
|
||||
name: string;
|
||||
getText: (object: T) => string;
|
||||
sortable?: boolean;
|
||||
flex?: string;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export type DataGridSort = {
|
||||
|
@ -3,13 +3,14 @@ export { Button } from './button';
|
||||
export { Checkbox } from './checkbox';
|
||||
export { CheckboxGroup } from './checkbox-group';
|
||||
export { DateInput } from './date-input';
|
||||
export { Dialog } from './dialog';
|
||||
export { FileUploader } from './file-uploader';
|
||||
export { Heading } from './heading';
|
||||
export { IconButton } from './icon-button';
|
||||
export { ImageFileManager } from './image-file-manager';
|
||||
export { LinkButton } from './link-button';
|
||||
export { Menu } from './menu';
|
||||
export { Dialog } from './dialog';
|
||||
export { NumberField } from './number-field';
|
||||
export { NumberInput } from './number-input';
|
||||
export { Overlay } from './overlay';
|
||||
export { Pagination } from './pagination';
|
||||
|
28
front/src/components/ui/number-field/component.tsx
Normal file
28
front/src/components/ui/number-field/component.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { ForwardedRef, forwardRef } from 'react';
|
||||
|
||||
import { Label, LabelProps } from '../label';
|
||||
import { NumberInput } from '../number-input';
|
||||
import { NumberFieldProps } from './types';
|
||||
|
||||
function NumberFieldInner(
|
||||
{ scale, label = {}, required, ...props }: Omit<NumberFieldProps, 'ref'>,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const labelProps: LabelProps = {
|
||||
...label,
|
||||
required: { value: required, ...label.required },
|
||||
};
|
||||
return (
|
||||
<Label scale={scale} {...labelProps}>
|
||||
<NumberInput
|
||||
scale={scale}
|
||||
ref={ref}
|
||||
required={required}
|
||||
{...props}
|
||||
invalid={label.error !== undefined}
|
||||
/>
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
export const NumberField = forwardRef(NumberFieldInner);
|
6
front/src/components/ui/number-field/types.ts
Normal file
6
front/src/components/ui/number-field/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { LabelProps } from '../label/types';
|
||||
import { NumberInputProps } from '../number-input/types';
|
||||
|
||||
export type NumberFieldProps = {
|
||||
label?: LabelProps;
|
||||
} & NumberInputProps;
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TextInput } from '../text-input';
|
||||
import { Input } from '../input';
|
||||
import { NumberInputProps } from './types';
|
||||
|
||||
export function NumberInput({
|
||||
@ -34,5 +34,5 @@ export function NumberInput({
|
||||
onChange?.(num);
|
||||
};
|
||||
|
||||
return <TextInput value={value ?? ''} onChange={handleChange} {...props} />;
|
||||
return <Input value={value ?? ''} onChange={handleChange} {...props} />;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export { Header } from './header';
|
||||
export { ParkTurbineTable } from './park-turbine-table';
|
||||
export { ParkTurbines } from './park-turbines';
|
||||
export { Sidebar } from './sidebar';
|
||||
export { SignInForm } from './sign-in-form';
|
||||
export { SignUpForm } from './sign-up-form';
|
||||
export { ThemeSelect } from './theme-select';
|
||||
|
35
front/src/components/ux/park-turbine-table/component.tsx
Normal file
35
front/src/components/ux/park-turbine-table/component.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { ParkFormTurbine } from '@components/pages/park-page/types';
|
||||
import React from 'react';
|
||||
|
||||
import { ParkTurbineTableHeader, ParkTurbineTableRow } from './components';
|
||||
import { ParkTurbineTableProps } from './types';
|
||||
|
||||
export function ParkTurbineTable({
|
||||
turbines,
|
||||
onChange,
|
||||
selectedIDs,
|
||||
onSelect,
|
||||
}: ParkTurbineTableProps) {
|
||||
const handleRowChange = (turbine: ParkFormTurbine, index: number) => {
|
||||
onChange(turbines.with(index, turbine));
|
||||
};
|
||||
|
||||
const handleSelect = (turbineId: number) => {
|
||||
onSelect({ ...selectedIDs, [turbineId]: !selectedIDs[turbineId] });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ParkTurbineTableHeader />
|
||||
{turbines.map((turbine, index) => (
|
||||
<ParkTurbineTableRow
|
||||
turbine={turbine}
|
||||
onChange={(t) => handleRowChange(t, index)}
|
||||
selected={selectedIDs[turbine.id] ?? false}
|
||||
onSelect={() => handleSelect(turbine.id)}
|
||||
key={turbine.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './park-turbine-table-header';
|
||||
export * from './park-turbine-table-row';
|
@ -0,0 +1,35 @@
|
||||
import { Checkbox } from '@components/ui/checkbox';
|
||||
import { Span } from '@components/ui/span';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export function ParkTurbineTableHeader() {
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<Checkbox
|
||||
checked={false}
|
||||
onChange={() => {}}
|
||||
label={{ className: styles.checkboxLabel }}
|
||||
/>
|
||||
<Span color="t300" className={styles.cell}>
|
||||
Id
|
||||
</Span>
|
||||
<Span color="t300" className={styles.cell}>
|
||||
Name
|
||||
</Span>
|
||||
<Span color="t300" className={styles.cell}>
|
||||
X
|
||||
</Span>
|
||||
<Span color="t300" className={styles.cell}>
|
||||
Y
|
||||
</Span>
|
||||
<Span color="t300" className={styles.cell}>
|
||||
Angle
|
||||
</Span>
|
||||
<Span color="t300" className={styles.cell}>
|
||||
Comment
|
||||
</Span>
|
||||
</header>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './component';
|
@ -0,0 +1,26 @@
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr 3fr;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
padding: 10px;
|
||||
border: solid 1px var(--clr-border-100);
|
||||
background-color: var(--clr-layer-300);
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: solid 1px var(--clr-border-100);
|
||||
background-color: var(--clr-layer-300);
|
||||
transition: all var(--td-100) ease-in-out;
|
||||
|
||||
&:last-of-type {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import { NumberInput } from '@components/ui';
|
||||
import { Checkbox } from '@components/ui/checkbox';
|
||||
import { Input } from '@components/ui/input';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { ParkTurbineTableRowProps } from './types';
|
||||
|
||||
export function ParkTurbineTableRow({
|
||||
turbine,
|
||||
onChange,
|
||||
selected,
|
||||
onSelect,
|
||||
}: ParkTurbineTableRowProps) {
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onChange={onSelect}
|
||||
label={{ className: styles.checkboxLabel }}
|
||||
/>
|
||||
<Input value={turbine.id} wrapper={{ className: styles.cell }} disabled />
|
||||
<Input
|
||||
value={turbine.name}
|
||||
wrapper={{ className: styles.cell }}
|
||||
disabled
|
||||
/>
|
||||
<NumberInput
|
||||
value={String(turbine.xOffset)}
|
||||
wrapper={{ className: styles.cell }}
|
||||
onChange={(value) => onChange({ ...turbine, xOffset: value })}
|
||||
/>
|
||||
<NumberInput
|
||||
value={String(turbine.yOffset)}
|
||||
wrapper={{ className: styles.cell }}
|
||||
onChange={(value) => onChange({ ...turbine, yOffset: value })}
|
||||
/>
|
||||
<NumberInput
|
||||
value={String(turbine.angle)}
|
||||
wrapper={{ className: styles.cell }}
|
||||
onChange={(value) => onChange({ ...turbine, angle: value })}
|
||||
/>
|
||||
<Input
|
||||
value={turbine.comment}
|
||||
wrapper={{ className: styles.cell }}
|
||||
onChange={(event) =>
|
||||
onChange({ ...turbine, comment: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './component';
|
@ -0,0 +1,16 @@
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr 3fr;
|
||||
|
||||
.cell {
|
||||
border: solid 1px var(--clr-border-100);
|
||||
border-radius: 0;
|
||||
background-color: var(--clr-layer-200);
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
padding: 10px;
|
||||
border: solid 1px var(--clr-border-100);
|
||||
background-color: var(--clr-layer-200);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { ParkFormTurbine } from '@components/pages/park-page/types';
|
||||
|
||||
export type ParkTurbineTableRowProps = {
|
||||
turbine: ParkFormTurbine;
|
||||
onChange: (turbine: ParkFormTurbine) => void;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
};
|
1
front/src/components/ux/park-turbine-table/index.tsx
Normal file
1
front/src/components/ux/park-turbine-table/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from './component';
|
8
front/src/components/ux/park-turbine-table/types.ts
Normal file
8
front/src/components/ux/park-turbine-table/types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ParkFormTurbine } from '@components/pages/park-page/types';
|
||||
|
||||
export type ParkTurbineTableProps = {
|
||||
turbines: ParkFormTurbine[];
|
||||
onChange: (turbines: ParkFormTurbine[]) => void;
|
||||
selectedIDs: Record<number, boolean>;
|
||||
onSelect: (selected: Record<number, boolean>) => void;
|
||||
};
|
87
front/src/components/ux/park-turbines/component.tsx
Normal file
87
front/src/components/ux/park-turbines/component.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { getTurbineTypes, TurbineType } from '@api/wind';
|
||||
import { ParkFormTurbine } from '@components/pages/park-page/types';
|
||||
import { Autocomplete, Button } from '@components/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { ParkTurbineTable } from '../park-turbine-table';
|
||||
import styles from './styles.module.scss';
|
||||
import { ParkTurbinesProps } from './types';
|
||||
|
||||
export function ParkTurbines({
|
||||
savedTurbines,
|
||||
value = [],
|
||||
onChange,
|
||||
}: ParkTurbinesProps) {
|
||||
const [turbineTypes, setTurbineTypes] = useState<TurbineType[]>([]);
|
||||
const [turbineType, setTurbineType] = useState<TurbineType>(null);
|
||||
const [selectedIDs, setSelectedIDs] = useState<Record<number, boolean>>({});
|
||||
|
||||
const fetchTurbineTypes = async () => {
|
||||
const res = await getTurbineTypes();
|
||||
setTurbineTypes(res.data ?? []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTurbineTypes();
|
||||
}, []);
|
||||
|
||||
const handleAddButtonClick = () => {
|
||||
if (
|
||||
savedTurbines.find((t) => t.id === turbineType.id) ||
|
||||
value.find((t) => t.id === turbineType.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onChange([
|
||||
...value,
|
||||
{
|
||||
id: turbineType.id,
|
||||
name: turbineType.name,
|
||||
xOffset: '',
|
||||
yOffset: '',
|
||||
angle: '',
|
||||
comment: '',
|
||||
new: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleDeleteButtonClick = () => {
|
||||
const newValue: ParkFormTurbine[] = [];
|
||||
value.forEach((turbine) => {
|
||||
if (!selectedIDs[turbine.id]) {
|
||||
newValue.push(turbine);
|
||||
return;
|
||||
}
|
||||
if (savedTurbines.find((t) => t.id === turbine.id)) {
|
||||
newValue.push({ ...turbine, delete: true });
|
||||
}
|
||||
});
|
||||
onChange(newValue);
|
||||
setSelectedIDs({});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.parkTurbines}>
|
||||
<div className={styles.actions}>
|
||||
<Autocomplete
|
||||
options={turbineTypes ?? []}
|
||||
getOptionKey={({ id }) => id}
|
||||
getOptionLabel={({ name }) => name}
|
||||
onChange={(t) => setTurbineType(t)}
|
||||
value={turbineType}
|
||||
/>
|
||||
<Button onClick={handleAddButtonClick}>Add</Button>
|
||||
<Button variant="secondary" onClick={handleDeleteButtonClick}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<ParkTurbineTable
|
||||
turbines={value.filter((t) => !t.delete)}
|
||||
onChange={onChange}
|
||||
selectedIDs={selectedIDs}
|
||||
onSelect={setSelectedIDs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
1
front/src/components/ux/park-turbines/index.ts
Normal file
1
front/src/components/ux/park-turbines/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './component';
|
14
front/src/components/ux/park-turbines/styles.module.scss
Normal file
14
front/src/components/ux/park-turbines/styles.module.scss
Normal file
@ -0,0 +1,14 @@
|
||||
.parkTurbines {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
background-color: var(--clr-layer-200);
|
||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||
gap: 10px;
|
||||
}
|
8
front/src/components/ux/park-turbines/types.ts
Normal file
8
front/src/components/ux/park-turbines/types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ParkTurbine } from '@api/wind';
|
||||
import { ParkFormTurbine } from '@components/pages/park-page/types';
|
||||
|
||||
export type ParkTurbinesProps = {
|
||||
savedTurbines: ParkTurbine[];
|
||||
value?: ParkFormTurbine[];
|
||||
onChange?: (value: ParkFormTurbine[]) => void;
|
||||
};
|
@ -1,40 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Heading,
|
||||
LinkButton,
|
||||
PasswordInput,
|
||||
TextInput,
|
||||
} from '@components/ui';
|
||||
import { useForm } from '@utils/form';
|
||||
import { ROUTES } from '@utils/route';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { SignInFormProps, SignInFormStore } from './types';
|
||||
|
||||
export function SignInForm({ className, ...props }: SignInFormProps) {
|
||||
const { register, getValues } = useForm<SignInFormStore>({});
|
||||
const classNames = clsx(className, styles.form);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
console.log(getValues());
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classNames} {...props}>
|
||||
<Heading tag="h1" className={styles.heading}>
|
||||
Sign in
|
||||
</Heading>
|
||||
<div className={styles.inputBox}>
|
||||
<TextInput {...register('email')} label={{ text: 'Email' }} />
|
||||
<PasswordInput {...register('password')} label={{ text: 'Password' }} />
|
||||
</div>
|
||||
<div className={styles.buttonBox}>
|
||||
<Button type="submit">Sign in</Button>
|
||||
<LinkButton href={ROUTES.signUp.path}>Not a member?</LinkButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { SignInForm } from './component';
|
@ -1,26 +0,0 @@
|
||||
.form {
|
||||
display: grid;
|
||||
padding: 40px 20px 20px;
|
||||
border-radius: 15px;
|
||||
background-color: var(--clr-layer-200);
|
||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||
gap: 30px;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inputBox {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.buttonBox {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export type SignInFormStore = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SignInFormProps = {} & ComponentProps<'form'>;
|
@ -1,40 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Heading,
|
||||
LinkButton,
|
||||
PasswordInput,
|
||||
TextInput,
|
||||
} from '@components/ui';
|
||||
import { useForm } from '@utils/form';
|
||||
import { ROUTES } from '@utils/route';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { SignUpFormProps, SignUpFormStore } from './types';
|
||||
|
||||
export function SignUpForm({ className, ...props }: SignUpFormProps) {
|
||||
const { register, getValues } = useForm<SignUpFormStore>({});
|
||||
const classNames = clsx(className, styles.form);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
console.log(getValues());
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classNames} {...props}>
|
||||
<Heading tag="h1" className={styles.heading}>
|
||||
Sign up
|
||||
</Heading>
|
||||
<div className={styles.inputBox}>
|
||||
<TextInput {...register('email')} label={{ text: 'Email' }} />
|
||||
<PasswordInput {...register('password')} label={{ text: 'Password' }} />
|
||||
</div>
|
||||
<div className={styles.buttonBox}>
|
||||
<Button type="submit">Sign up</Button>
|
||||
<LinkButton href={ROUTES.signIn.path}>Already a member?</LinkButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { SignUpForm } from './component';
|
@ -1,26 +0,0 @@
|
||||
.form {
|
||||
display: grid;
|
||||
padding: 40px 20px 20px;
|
||||
border-radius: 15px;
|
||||
background-color: var(--clr-layer-200);
|
||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
||||
gap: 30px;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inputBox {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.buttonBox {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export type SignUpFormStore = {
|
||||
email: string;
|
||||
password: string;
|
||||
passwordRepeat: string;
|
||||
};
|
||||
|
||||
export type SignUpFormProps = {} & ComponentProps<'form'>;
|
@ -143,12 +143,12 @@ class WindParkTurbineRepository:
|
||||
return db_park_turbine
|
||||
|
||||
@staticmethod
|
||||
def get(db: Session, park_turbine_id: int):
|
||||
return db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first()
|
||||
def get(db: Session, park_id: int, turbine_id: int):
|
||||
return db.query(WindParkTurbine).filter(WindParkTurbine.wind_park_id == park_id, WindParkTurbine.turbine_id == turbine_id).first()
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, park_turbine_id: int, park_turbine: WindParkTurbineCreate):
|
||||
db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first()
|
||||
def update(db: Session, park_id: int, turbine_id: int, park_turbine: WindParkTurbineCreate):
|
||||
db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.wind_park_id == park_id, WindParkTurbine.turbine_id == turbine_id).first()
|
||||
if db_park_turbine:
|
||||
for key, value in park_turbine.dict().items():
|
||||
setattr(db_park_turbine, key, value)
|
||||
|
@ -77,17 +77,17 @@ async def create_park_turbine(park_turbine: WindParkTurbineCreate, db: Session =
|
||||
return WindParkTurbineRepository.create(db=db, park_turbine=park_turbine)
|
||||
|
||||
|
||||
@router.get("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse)
|
||||
async def read_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)):
|
||||
park_turbine = WindParkTurbineRepository.get(db=db, park_turbine_id=park_turbine_id)
|
||||
@router.get("/park_turbine/{park_id}/{turbine_id}", response_model=WindParkTurbineResponse)
|
||||
async def read_park_turbine(park_id: int, turbine_id: int, db: Session = Depends(get_db)):
|
||||
park_turbine = WindParkTurbineRepository.get(db=db, park_id=park_id, turbine_id=turbine_id)
|
||||
if park_turbine is None:
|
||||
raise HTTPException(status_code=404, detail="Park Turbine not found")
|
||||
return park_turbine
|
||||
|
||||
|
||||
@router.put("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse)
|
||||
async def update_park_turbine(park_turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)):
|
||||
updated_park_turbine = WindParkTurbineRepository.update(db=db, park_turbine_id=park_turbine_id,
|
||||
@router.put("/park_turbine/{park_id}/{turbine_id}", response_model=WindParkTurbineResponse)
|
||||
async def update_park_turbine(park_id: int, turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)):
|
||||
updated_park_turbine = WindParkTurbineRepository.update(db=db, park_id=park_id, turbine_id=turbine_id,
|
||||
park_turbine=park_turbine)
|
||||
if updated_park_turbine is None:
|
||||
raise HTTPException(status_code=404, detail="Park Turbine not found")
|
||||
|
Loading…
Reference in New Issue
Block a user