FIX: [App]: Borders done right

This commit is contained in:
Nicolás Hatcher
2024-10-27 16:45:14 +01:00
committed by Nicolás Hatcher Andrés
parent 494a315cbd
commit 2c2228c2c2
6 changed files with 442 additions and 35 deletions

View File

@@ -51,6 +51,84 @@ fn borders_all() {
} }
} }
// let's check the borders around
{
let row = 4;
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: None,
bottom: Some(border_item.clone()),
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
let row = 9;
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: Some(border_item.clone()),
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
}
{
let column = 5;
for row in 5..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: Some(border_item.clone()),
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
let column = 9;
for row in 5..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: Some(border_item.clone()),
right: None,
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
}
// Lets remove all of them: // Lets remove all of them:
let border_area: BorderArea = serde_json::from_str( let border_area: BorderArea = serde_json::from_str(
r##"{ r##"{
@@ -63,8 +141,8 @@ fn borders_all() {
) )
.unwrap(); .unwrap();
model.set_area_with_border(range, &border_area).unwrap(); model.set_area_with_border(range, &border_area).unwrap();
for row in 5..9 { for row in 4..10 {
for column in 6..9 { for column in 5..10 {
let style = model.get_cell_style(0, row, column).unwrap(); let style = model.get_cell_style(0, row, column).unwrap();
let expected_border = Border { let expected_border = Border {
diagonal_up: false, diagonal_up: false,
@@ -229,6 +307,84 @@ fn borders_outer() {
}; };
assert_eq!(style.border, expected_border); assert_eq!(style.border, expected_border);
} }
// let's check the borders around
{
let row = 4;
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: None,
bottom: Some(border_item.clone()),
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
let row = 9;
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: Some(border_item.clone()),
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
}
{
let column = 5;
for row in 5..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: Some(border_item.clone()),
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
let column = 9;
for row in 5..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: Some(border_item.clone()),
right: None,
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
}
} }
#[test] #[test]
@@ -275,6 +431,72 @@ fn borders_top() {
assert_eq!(style.border, expected_border); assert_eq!(style.border, expected_border);
} }
} }
// let's check the borders around
{
let row = 4;
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let border_item = BorderItem {
style: BorderStyle::Thin,
color: Some("#FF5566".to_string()),
};
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: None,
bottom: Some(border_item.clone()),
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
let row = 9;
for column in 6..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
}
{
let column = 5;
for row in 5..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
let column = 9;
for row in 5..9 {
let style = model.get_cell_style(0, row, column).unwrap();
let expected_border = Border {
diagonal_up: false,
diagonal_down: false,
left: None,
right: None,
top: None,
bottom: None,
diagonal: None,
};
assert_eq!(style.border, expected_border);
}
}
} }
#[test] #[test]

View File

@@ -48,6 +48,54 @@ fn tsv_crlf_paste() {
); );
} }
#[test]
fn cut_paste() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1*3+1").unwrap();
// set A1 bold
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
model.update_range_style(&range, "font.b", "true").unwrap();
model
.set_user_input(0, 2, 1, "A season of faith, \"perfection\"")
.unwrap();
// Select A1:B2 and copy
model.set_selected_range(1, 1, 2, 2).unwrap();
let copy = model.copy_to_clipboard().unwrap();
model.set_selected_cell(4, 4).unwrap();
// paste in cell D4 (4, 4)
model
.paste_from_clipboard((1, 1, 2, 2), &copy.data, true)
.unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
assert_eq!(model.get_cell_content(0, 4, 5), Ok("=D4*3+1".to_string()));
assert_eq!(
model.get_formatted_cell_value(0, 4, 5),
Ok("127".to_string())
);
// cell D4 must be bold
let style_d4 = model.get_cell_style(0, 4, 4).unwrap();
assert!(style_d4.font.b);
// range A1:B2 must be empty
assert_eq!(model.get_cell_content(0, 1, 1), Ok("".to_string()));
assert_eq!(model.get_cell_content(0, 1, 2), Ok("".to_string()));
assert_eq!(model.get_cell_content(0, 2, 1), Ok("".to_string()));
assert_eq!(model.get_cell_content(0, 2, 2), Ok("".to_string()));
}
#[test] #[test]
fn copy_paste_internal() { fn copy_paste_internal() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();

View File

@@ -7,7 +7,7 @@ use csv_sniffer::Sniffer;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
constants::{self, DEFAULT_ROW_HEIGHT}, constants::{self, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW},
expressions::{ expressions::{
types::{Area, CellReferenceIndex}, types::{Area, CellReferenceIndex},
utils::{is_valid_column_number, is_valid_row}, utils::{is_valid_column_number, is_valid_row},
@@ -41,7 +41,7 @@ pub struct Clipboard {
pub(crate) range: (i32, i32, i32, i32), pub(crate) range: (i32, i32, i32, i32),
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, PartialEq)]
pub enum BorderType { pub enum BorderType {
All, All,
Inner, Inner,
@@ -796,7 +796,6 @@ impl UserModel {
range: &Area, range: &Area,
border_area: &BorderArea, border_area: &BorderArea,
) -> Result<(), String> { ) -> Result<(), String> {
// FIXME: We need to set the border also in neighbouring cells.
let sheet = range.sheet; let sheet = range.sheet;
let mut diff_list = Vec::new(); let mut diff_list = Vec::new();
let last_row = range.row + range.height - 1; let last_row = range.row + range.height - 1;
@@ -806,12 +805,6 @@ impl UserModel {
let old_value = self.model.get_style_for_cell(sheet, row, column)?; let old_value = self.model.get_style_for_cell(sheet, row, column)?;
let mut style = old_value.clone(); let mut style = old_value.clone();
// First remove all existing borders
style.border.top = None;
style.border.right = None;
style.border.bottom = None;
style.border.left = None;
match border_area.r#type { match border_area.r#type {
BorderType::All => { BorderType::All => {
style.border.top = Some(border_area.item.clone()); style.border.top = Some(border_area.item.clone());
@@ -868,7 +861,10 @@ impl UserModel {
} }
} }
BorderType::None => { BorderType::None => {
// noop, we already removed all the borders style.border.top = None;
style.border.right = None;
style.border.bottom = None;
style.border.left = None;
} }
} }
@@ -882,6 +878,122 @@ impl UserModel {
}); });
} }
} }
// bottom of the cells above the first
if range.row > 1
&& [
BorderType::Top,
BorderType::All,
BorderType::None,
BorderType::Outer,
]
.contains(&border_area.r#type)
{
let row = range.row - 1;
for column in range.column..=last_column {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.bottom = None;
} else {
style.border.bottom = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(old_value),
new_value: Box::new(style),
});
}
}
// Cells to the right
if last_column < LAST_COLUMN
&& [
BorderType::Right,
BorderType::All,
BorderType::None,
BorderType::Outer,
]
.contains(&border_area.r#type)
{
let column = last_column + 1;
for row in range.row..=last_row {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.left = None;
} else {
style.border.left = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(old_value),
new_value: Box::new(style),
});
}
}
// Cells bellow
if last_row < LAST_ROW
&& [
BorderType::Bottom,
BorderType::All,
BorderType::None,
BorderType::Outer,
]
.contains(&border_area.r#type)
{
let row = last_row + 1;
for column in range.column..=last_column {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.top = None;
} else {
style.border.top = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(old_value),
new_value: Box::new(style),
});
}
}
// Cells to the left
if range.column > 1
&& [
BorderType::Left,
BorderType::All,
BorderType::None,
BorderType::Outer,
]
.contains(&border_area.r#type)
{
let column = range.column - 1;
for row in range.row..=last_row {
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
let mut style = old_value.clone();
if border_area.r#type == BorderType::None {
style.border.right = None;
} else {
style.border.right = Some(border_area.item.clone());
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(old_value),
new_value: Box::new(style),
});
}
}
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
Ok(()) Ok(())

View File

@@ -7,7 +7,7 @@ import {
PencilLine, PencilLine,
} from "lucide-react"; } from "lucide-react";
import type React from "react"; import type React from "react";
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
BorderBottomIcon, BorderBottomIcon,
@@ -27,6 +27,7 @@ import ColorPicker from "./colorPicker";
type BorderPickerProps = { type BorderPickerProps = {
className?: string; className?: string;
onChange: (border: BorderOptions) => void; onChange: (border: BorderOptions) => void;
onClose: () => void;
anchorEl: React.RefObject<HTMLElement>; anchorEl: React.RefObject<HTMLElement>;
anchorOrigin?: PopoverOrigin; anchorOrigin?: PopoverOrigin;
transformOrigin?: PopoverOrigin; transformOrigin?: PopoverOrigin;
@@ -36,25 +37,33 @@ type BorderPickerProps = {
const BorderPicker = (properties: BorderPickerProps) => { const BorderPicker = (properties: BorderPickerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [borderSelected, setBorderSelected] = useState(BorderType.None); const [borderSelected, setBorderSelected] = useState<BorderType | null>(null);
const [borderColor, setBorderColor] = useState("#000000"); const [borderColor, setBorderColor] = useState("#000000");
const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin); const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin);
const [colorPickerOpen, setColorPickerOpen] = useState(false); const [colorPickerOpen, setColorPickerOpen] = useState(false);
const [stylePickerOpen, setStylePickerOpen] = useState(false); const [stylePickerOpen, setStylePickerOpen] = useState(false);
const closePicker = (): void => {
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
if (!borderSelected) {
return;
}
properties.onChange({ properties.onChange({
color: borderColor, color: borderColor,
style: borderStyle, style: borderStyle,
border: borderSelected, border: borderSelected,
}); });
}; }, [borderColor, borderStyle, borderSelected]);
const onClose = properties.onClose;
const borderColorButton = useRef(null); const borderColorButton = useRef(null);
const borderStyleButton = useRef(null); const borderStyleButton = useRef(null);
return ( return (
<> <>
<StyledPopover <StyledPopover
open={properties.open} open={properties.open}
onClose={(): void => closePicker()} onClose={onClose}
anchorEl={properties.anchorEl.current} anchorEl={properties.anchorEl.current}
anchorOrigin={ anchorOrigin={
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" } properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
@@ -71,13 +80,13 @@ const BorderPicker = (properties: BorderPickerProps) => {
$pressed={borderSelected === BorderType.All} $pressed={borderSelected === BorderType.All}
onClick={() => { onClick={() => {
if (borderSelected === BorderType.All) { if (borderSelected === BorderType.All) {
setBorderSelected(BorderType.None); setBorderSelected(null);
} else { } else {
setBorderSelected(BorderType.All); setBorderSelected(BorderType.All);
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.all")}
> >
<BorderAllIcon /> <BorderAllIcon />
</Button> </Button>
@@ -86,13 +95,13 @@ const BorderPicker = (properties: BorderPickerProps) => {
$pressed={borderSelected === BorderType.Inner} $pressed={borderSelected === BorderType.Inner}
onClick={() => { onClick={() => {
if (borderSelected === BorderType.Inner) { if (borderSelected === BorderType.Inner) {
setBorderSelected(BorderType.None); setBorderSelected(null);
} else { } else {
setBorderSelected(BorderType.Inner); setBorderSelected(BorderType.Inner);
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.inner")}
> >
<BorderInnerIcon /> <BorderInnerIcon />
</Button> </Button>
@@ -101,13 +110,13 @@ const BorderPicker = (properties: BorderPickerProps) => {
$pressed={borderSelected === BorderType.CenterH} $pressed={borderSelected === BorderType.CenterH}
onClick={() => { onClick={() => {
if (borderSelected === BorderType.CenterH) { if (borderSelected === BorderType.CenterH) {
setBorderSelected(BorderType.None); setBorderSelected(null);
} else { } else {
setBorderSelected(BorderType.CenterH); setBorderSelected(BorderType.CenterH);
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.horizontal")}
> >
<BorderCenterHIcon /> <BorderCenterHIcon />
</Button> </Button>
@@ -116,13 +125,13 @@ const BorderPicker = (properties: BorderPickerProps) => {
$pressed={borderSelected === BorderType.CenterV} $pressed={borderSelected === BorderType.CenterV}
onClick={() => { onClick={() => {
if (borderSelected === BorderType.CenterV) { if (borderSelected === BorderType.CenterV) {
setBorderSelected(BorderType.None); setBorderSelected(null);
} else { } else {
setBorderSelected(BorderType.CenterV); setBorderSelected(BorderType.CenterV);
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.vertical")}
> >
<BorderCenterVIcon /> <BorderCenterVIcon />
</Button> </Button>
@@ -137,7 +146,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.outer")}
> >
<BorderOuterIcon /> <BorderOuterIcon />
</Button> </Button>
@@ -154,7 +163,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.clear")}
> >
<BorderNoneIcon /> <BorderNoneIcon />
</Button> </Button>
@@ -169,7 +178,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.top")}
> >
<BorderTopIcon /> <BorderTopIcon />
</Button> </Button>
@@ -184,7 +193,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.right")}
> >
<BorderRightIcon /> <BorderRightIcon />
</Button> </Button>
@@ -199,7 +208,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.bottom")}
> >
<BorderBottomIcon /> <BorderBottomIcon />
</Button> </Button>
@@ -214,7 +223,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
} }
}} }}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.left")}
> >
<BorderLeftIcon /> <BorderLeftIcon />
</Button> </Button>
@@ -228,7 +237,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
$pressed={false} $pressed={false}
disabled={false} disabled={false}
ref={borderColorButton} ref={borderColorButton}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.color")}
> >
<PencilLine /> <PencilLine />
</Button> </Button>
@@ -243,7 +252,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
type="button" type="button"
$pressed={false} $pressed={false}
disabled={false} disabled={false}
title={t("workbook.toolbar.borders_button_title")} title={t("toolbar.borders.style")}
> >
<BorderStyleIcon /> <BorderStyleIcon />
</Button> </Button>

View File

@@ -325,7 +325,7 @@ function Toolbar(properties: ToolbarProperties) {
onClick={() => setBorderPickerOpen(true)} onClick={() => setBorderPickerOpen(true)}
ref={borderButton} ref={borderButton}
disabled={!canEdit} disabled={!canEdit}
title={t("toolbar.borders")} title={t("toolbar.borders.title")}
> >
<Grid2X2 /> <Grid2X2 />
</StyledButton> </StyledButton>
@@ -369,6 +369,8 @@ function Toolbar(properties: ToolbarProperties) {
<BorderPicker <BorderPicker
onChange={(border): void => { onChange={(border): void => {
properties.onBorderChanged(border); properties.onBorderChanged(border);
}}
onClose={() => {
setBorderPickerOpen(false); setBorderPickerOpen(false);
}} }}
anchorEl={borderButton} anchorEl={borderButton}

View File

@@ -15,7 +15,6 @@
"format_number": "Format number", "format_number": "Format number",
"font_color": "Font color", "font_color": "Font color",
"fill_color": "Fill color", "fill_color": "Fill color",
"borders": "Borders",
"decimal_places_increase": "Increase decimal places", "decimal_places_increase": "Increase decimal places",
"decimal_places_decrease": "Decrease decimal places", "decimal_places_decrease": "Decrease decimal places",
"show_hide_grid_lines": "Show/hide grid lines", "show_hide_grid_lines": "Show/hide grid lines",
@@ -39,6 +38,21 @@
"currency_gbp_example": "£", "currency_gbp_example": "£",
"date_short_example": "09/24/2024", "date_short_example": "09/24/2024",
"date_long_example": "Tuesday, September 24, 2024" "date_long_example": "Tuesday, September 24, 2024"
},
"borders": {
"title": "Borders",
"all": "All borders",
"inner": "Inner borders",
"outer": "Outer borders",
"top": "Top borders",
"bottom": "Bottom borders",
"clear": "Clear borders",
"left": "Left borders",
"right": "Right borders",
"horizontal": "Horizontal borders",
"vertical": "Vertical borders",
"color": "Border color",
"style": "Border style"
} }
}, },
"num_fmt": { "num_fmt": {