Add color to accommodations
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing

This commit is contained in:
Pierre HUBERT 2024-06-20 21:46:45 +02:00
parent 337f6ced5d
commit 3cdfc4d3c7
14 changed files with 128 additions and 7 deletions

View File

@ -37,6 +37,7 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"filesize": "^10.1.2", "filesize": "^10.1.2",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"mui-color-input": "^2.0.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-easy-crop": "^5.0.7", "react-easy-crop": "^5.0.7",
@ -519,6 +520,14 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@ctrl/tinycolor": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz",
"integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==",
"engines": {
"node": ">=14"
}
},
"node_modules/@emotion/babel-plugin": { "node_modules/@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@ -3455,6 +3464,27 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/mui-color-input": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-2.0.3.tgz",
"integrity": "sha512-rAd040qQ0Y+8dk4gE8kkCiJ/vCgA0j4vv1quJ43BfORTFE3uHarHj0xY1Vo9CPbojtx1f5vW+CjckYPRIZPIRg==",
"dependencies": {
"@ctrl/tinycolor": "^4.0.3"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material": "^5.0.0",
"@types/react": "^18.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",

View File

@ -33,6 +33,7 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"filesize": "^10.1.2", "filesize": "^10.1.2",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"mui-color-input": "^2.0.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-easy-crop": "^5.0.7", "react-easy-crop": "^5.0.7",

View File

@ -8,7 +8,8 @@ export interface Accommodation {
time_update: number; time_update: number;
name: string; name: string;
need_validation: boolean; need_validation: boolean;
description: string; description?: string;
color?: string;
open_to_reservations: boolean; open_to_reservations: boolean;
} }
@ -58,6 +59,7 @@ export interface UpdateAccommodation {
name: string; name: string;
need_validation: boolean; need_validation: boolean;
description?: string; description?: string;
color?: string;
open_to_reservations: boolean; open_to_reservations: boolean;
} }

View File

@ -12,6 +12,7 @@ import { UpdateAccommodation } from "../../api/accommodations/AccommodationListA
import { checkConstraint } from "../../utils/from_utils"; import { checkConstraint } from "../../utils/from_utils";
import { PropCheckbox } from "../../widgets/forms/PropCheckbox"; import { PropCheckbox } from "../../widgets/forms/PropCheckbox";
import { PropEdit } from "../../widgets/forms/PropEdit"; import { PropEdit } from "../../widgets/forms/PropEdit";
import { PropColorPicker } from "../../widgets/forms/PropColorPicker";
export function UpdateAccommodationDialog(p: { export function UpdateAccommodationDialog(p: {
open: boolean; open: boolean;
@ -89,6 +90,20 @@ export function UpdateAccommodationDialog(p: {
helperText={descriptionErr} helperText={descriptionErr}
/> />
<PropColorPicker
editable
label="Couleur"
value={accommodation?.color}
onChange={(s) =>
setAccommodation((a) => {
return {
...a!,
color: s!,
};
})
}
/>
<PropCheckbox <PropCheckbox
editable editable
label="Ouvert aux réservations" label="Ouvert aux réservations"

View File

@ -159,6 +159,12 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
key={a.id} key={a.id}
control={ control={
<Checkbox <Checkbox
sx={{
color: "#" + a.color,
"&.Mui-checked": {
color: "#" + a.color,
},
}}
checked={!hiddenAccommodations.has(a.id)} checked={!hiddenAccommodations.has(a.id)}
onChange={(_ev, v) => { onChange={(_ev, v) => {
if (v) hiddenAccommodations.delete(a.id); if (v) hiddenAccommodations.delete(a.id);

View File

@ -1,6 +1,7 @@
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import HouseIcon from "@mui/icons-material/House";
import { import {
Button, Button,
Card, Card,
@ -62,6 +63,7 @@ function AccommodationsListCard(): React.ReactElement {
name: "", name: "",
open_to_reservations: true, open_to_reservations: true,
need_validation: false, need_validation: false,
color: "2196f3",
}, },
true true
); );
@ -178,6 +180,7 @@ function AccommodationCard(p: {
Mis à jour il y a <TimeWidget time={p.accommodation.time_update} /> Mis à jour il y a <TimeWidget time={p.accommodation.time_update} />
</Typography> </Typography>
<Typography variant="h5" component="div"> <Typography variant="h5" component="div">
<HouseIcon sx={{ color: "#" + p.accommodation.color }} />{" "}
{p.accommodation.name} {p.accommodation.name}
</Typography> </Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary"> <Typography sx={{ mb: 1.5 }} color="text.secondary">

View File

@ -0,0 +1,24 @@
import { MuiColorInput } from "mui-color-input";
import { PropEdit } from "./PropEdit";
export function PropColorPicker(p: {
editable: boolean;
label: string;
value?: string;
onChange: (v: string | undefined) => void;
}): React.ReactElement {
if (!p.editable) {
if (!p.value) return <></>;
return <PropEdit editable={false} label={p.label} value={`#${p.value}`} />;
}
return (
<MuiColorInput
value={"#" + (p.value ?? "")}
fallbackValue="#ffffff"
format="hex"
onChange={(_v, c) => p.onChange(c.hex.substring(1))}
/>
);
}

View File

@ -1415,6 +1415,7 @@ dependencies = [
"httpdate", "httpdate",
"ical", "ical",
"image", "image",
"lazy-regex",
"lazy_static", "lazy_static",
"lettre", "lettre",
"light-openid", "light-openid",
@ -1952,6 +1953,29 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "lazy-regex"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c"
dependencies = [
"lazy-regex-proc_macros",
"once_cell",
"regex",
]
[[package]]
name = "lazy-regex-proc_macros"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.63",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"

View File

@ -10,6 +10,7 @@ log = "0.4.21"
env_logger = "0.11.3" env_logger = "0.11.3"
clap = { version = "4.5.4", features = ["derive", "env"] } clap = { version = "4.5.4", features = ["derive", "env"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
lazy-regex = "3.1.0"
anyhow = "1.0.83" anyhow = "1.0.83"
actix-web = "4.5.1" actix-web = "4.5.1"
actix-cors = "0.7.0" actix-cors = "0.7.0"

View File

@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS accommodations_list
name VARCHAR(50) NOT NULL, name VARCHAR(50) NOT NULL,
need_validation BOOLEAN NOT NULL DEFAULT true, need_validation BOOLEAN NOT NULL DEFAULT true,
description text NULL, description text NULL,
color VARCHAR(6) NULL,
open_to_reservations BOOLEAN NOT NULL DEFAULT false open_to_reservations BOOLEAN NOT NULL DEFAULT false
); );

View File

@ -8,10 +8,12 @@ use actix_web::{web, HttpResponse};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
enum AccommodationListControllerErr { enum AccommodationListControllerErr {
#[error("Malformed name!")] #[error("Invalid name length!")]
MalformedName, InvalidNameLength,
#[error("Malformed description!")] #[error("Invalid description length!")]
MalformedDescription, InvalidDescriptionLength,
#[error("Malformed color!")]
MalformedColor,
} }
#[derive(serde::Deserialize, Clone)] #[derive(serde::Deserialize, Clone)]
@ -19,6 +21,7 @@ pub struct AccommodationRequest {
pub name: String, pub name: String,
pub need_validation: bool, pub need_validation: bool,
pub description: Option<String>, pub description: Option<String>,
pub color: Option<String>,
pub open_to_reservations: bool, pub open_to_reservations: bool,
} }
@ -27,17 +30,24 @@ impl AccommodationRequest {
let c = StaticConstraints::default(); let c = StaticConstraints::default();
if !c.accommodation_name_len.validate(&self.name) { if !c.accommodation_name_len.validate(&self.name) {
return Err(AccommodationListControllerErr::MalformedName.into()); return Err(AccommodationListControllerErr::InvalidNameLength.into());
} }
accommodation.name = self.name; accommodation.name = self.name;
if let Some(d) = &self.description { if let Some(d) = &self.description {
if !c.accommodation_description_len.validate(d) { if !c.accommodation_description_len.validate(d) {
return Err(AccommodationListControllerErr::MalformedDescription.into()); return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
} }
} }
accommodation.description.clone_from(&self.description); accommodation.description.clone_from(&self.description);
if let Some(c) = &self.color {
if !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
return Err(AccommodationListControllerErr::MalformedColor.into());
}
}
accommodation.color.clone_from(&self.color);
accommodation.need_validation = self.need_validation; accommodation.need_validation = self.need_validation;
accommodation.open_to_reservations = self.open_to_reservations; accommodation.open_to_reservations = self.open_to_reservations;
Ok(()) Ok(())

View File

@ -459,6 +459,7 @@ pub struct Accommodation {
pub name: String, pub name: String,
pub need_validation: bool, pub need_validation: bool,
pub description: Option<String>, pub description: Option<String>,
pub color: Option<String>,
pub open_to_reservations: bool, pub open_to_reservations: bool,
} }

View File

@ -10,6 +10,8 @@ diesel::table! {
name -> Varchar, name -> Varchar,
need_validation -> Bool, need_validation -> Bool,
description -> Nullable<Text>, description -> Nullable<Text>,
#[max_length = 6]
color -> Nullable<Varchar>,
open_to_reservations -> Bool, open_to_reservations -> Bool,
} }
} }

View File

@ -71,6 +71,7 @@ pub async fn update(accommodation: &mut Accommodation) -> anyhow::Result<()> {
accommodations_list::dsl::name.eq(accommodation.name.to_string()), accommodations_list::dsl::name.eq(accommodation.name.to_string()),
accommodations_list::dsl::need_validation.eq(accommodation.need_validation), accommodations_list::dsl::need_validation.eq(accommodation.need_validation),
accommodations_list::dsl::description.eq(accommodation.description.clone()), accommodations_list::dsl::description.eq(accommodation.description.clone()),
accommodations_list::dsl::color.eq(accommodation.color.clone()),
accommodations_list::dsl::open_to_reservations.eq(accommodation.open_to_reservations), accommodations_list::dsl::open_to_reservations.eq(accommodation.open_to_reservations),
)) ))
.execute(conn) .execute(conn)