diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 3066e10a..6e0b43f2 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/crates/controller/src/api/test.rs b/crates/controller/src/api/test.rs index cf7d5bd5..8e6972dd 100644 --- a/crates/controller/src/api/test.rs +++ b/crates/controller/src/api/test.rs @@ -1,8 +1,10 @@ +use crate::edit_action::{EditPayload, PayloadsAction, SheetRename, WorkbookUpdateType}; + use super::Workbook; #[test] fn new_workbook() { - let wb = Workbook::default(); + let mut wb = Workbook::default(); let ws = wb.get_sheet_by_idx(0).unwrap(); ws.get_cell_position(100, 100).unwrap(); @@ -30,4 +32,21 @@ fn new_workbook() { .into_iter() .fold(0., |p, c| return p + c.width); assert!(v > 100.); + + let result = wb.handle_action(crate::EditAction::Payloads(PayloadsAction { + payloads: vec![EditPayload::SheetRename(SheetRename { + old_name: Some("Sheet1".to_string()), + new_name: "abcd".to_string(), + idx: None, + })], + undoable: true, + })); + + match result.status { + crate::edit_action::StatusCode::Ok(workbook_update_type) => { + println!("{:?}", workbook_update_type); + assert!(matches!(workbook_update_type, WorkbookUpdateType::Sheet)); + } + crate::edit_action::StatusCode::Err(e) => panic!("{:?}", e), + } } diff --git a/crates/controller/src/controller/executor.rs b/crates/controller/src/controller/executor.rs index 8e6172c9..a6ac0209 100644 --- a/crates/controller/src/controller/executor.rs +++ b/crates/controller/src/controller/executor.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use logisheets_base::{Addr, CellId, CubeId, RangeId, SheetId}; +use logisheets_base::{errors::BasicError, Addr, CellId, CubeId, RangeId, SheetId}; use crate::{ async_func_manager::AsyncFuncManager, @@ -11,7 +11,7 @@ use crate::{ }, container::ContainerExecutor, cube_manager::executors::CubeExecutor, - edit_action::{EditPayload, PayloadsAction}, + edit_action::{EditPayload, PayloadsAction, SheetRename}, formula_manager::{FormulaExecutor, Vertex}, navigator::{NavExecutor, Navigator}, range_manager::RangeExecutor, @@ -58,6 +58,35 @@ impl<'a> Executor<'a> { fn execute_payload(self, payload: EditPayload) -> Result { let mut result = self; + + if let EditPayload::SheetRename(rename) = payload { + let manager = &mut result.status.sheet_id_manager; + let SheetRename { + old_name, + idx, + new_name, + } = rename; + if let Some(old_name) = old_name { + manager.rename(&old_name, new_name); + } else { + if let Some(idx) = idx { + let id = result + .status + .sheet_pos_manager + .get_sheet_id(idx) + .ok_or(Error::Basic(BasicError::SheetIdxExceed(idx)))?; + let old_name = manager + .get_string(&id) + .ok_or(Error::Basic(BasicError::SheetIdNotFound(id)))?; + manager.rename(&old_name, new_name); + } else { + return Err(Error::PayloadError("".to_string())); + } + } + result.sheet_updated = true; + return Ok(result); + } + let (sheet_pos_manager, sheet_updated) = result.execute_sheet_pos(&payload)?; result.status.sheet_pos_manager = sheet_pos_manager; diff --git a/crates/controller/src/errors.rs b/crates/controller/src/errors.rs index 30757dc8..17e8e3e3 100644 --- a/crates/controller/src/errors.rs +++ b/crates/controller/src/errors.rs @@ -21,6 +21,8 @@ pub enum Error { Parse(#[from] ParseError), #[error("unavailable sheet idx: {0}")] UnavailableSheetIdx(usize), + #[error("invalid payload: {0}")] + PayloadError(String), } // A cleaner way for users to know about the error and a more convenient @@ -61,6 +63,10 @@ impl From for ErrorMessage { let msg = e.to_string(); ErrorMessage { msg, ty: 5 } } + Error::PayloadError(e) => { + let msg = e; + ErrorMessage { msg, ty: 6 } + } } } } diff --git a/package.json b/package.json index 9f90c8f7..a26db2e0 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "react-modal": "^3.16.3", "react-scripts": "5.0.0", "react-select": "^5.4.0", + "react-toastify": "^11.0.2", "react-transition-group": "^4.4.2", - "react-use-websocket": "^2.9.1", - "rxjs": "^7.5.2", + "rxjs": "^7.8.1", "source-map-loader": "^3.0.1", "ssf": "^0.11.2", "style-loader": "^3.3.1", @@ -98,7 +98,6 @@ "jest": "^27.5.1", "lint-staged": "^13.0.3", "prettier": "^2.7.1", - "react-toastify": "^9.0.5", "reflect-metadata": "^0.1.13", "sass": "^1.83.0", "sass-loader": "^16.0.4", diff --git a/src/components/canvas/store/render.ts b/src/components/canvas/store/render.ts index ea9e2f7c..8cd6455f 100644 --- a/src/components/canvas/store/render.ts +++ b/src/components/canvas/store/render.ts @@ -83,7 +83,8 @@ export class Render { box.position = toCanvasPosition( position, this.store.anchorX, - this.store.anchorY + this.store.anchorY, + 'cell' ) this._fill(box, style) this._border(box, position, style) @@ -264,11 +265,13 @@ export class Render { // } private _text(box: Box, info: StandardCell) { + const t = info.getFormattedText() + if (!t) return const textAttr = new TextAttr() if (info.style) { textAttr.alignment = info.style.alignment textAttr.setFont(info.style.getFont()) } - this._painterService.text(info.getFormattedText(), textAttr, box) + this._painterService.text(t, textAttr, box) } } diff --git a/src/components/root/socket-status.ts b/src/components/root/socket-status.ts deleted file mode 100644 index a7a2e8ce..00000000 --- a/src/components/root/socket-status.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {ReadyState} from 'react-use-websocket' - -export const transform = (readyState: ReadyState) => { - const map = new Map([ - [ReadyState.CLOSED, 'socket closed'], - [ReadyState.CLOSING, 'socket closing'], - [ReadyState.CONNECTING, 'socket connecting'], - [ReadyState.OPEN, 'socket open'], - [ReadyState.UNINSTANTIATED, 'socket uninstantiated'], - ]) - return map.get(readyState) ?? '' -} diff --git a/src/components/root/waiting.tsx b/src/components/root/waiting.tsx deleted file mode 100644 index bf97d13f..00000000 --- a/src/components/root/waiting.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {ReadyState} from 'react-use-websocket' -import {transform} from './socket-status' -export const Waiting = ({status}: {status: ReadyState}) => { - return
{transform(status)} waiting...
-} diff --git a/src/components/sheets-tab/contextmenu.tsx b/src/components/sheets-tab/contextmenu.tsx index 04fe8628..dee46567 100644 --- a/src/components/sheets-tab/contextmenu.tsx +++ b/src/components/sheets-tab/contextmenu.tsx @@ -8,59 +8,107 @@ import Modal from 'react-modal' import {DataService} from '@/core/data2' import {useInjection} from '@/core/ioc/provider' import {TYPES} from '@/core/ioc/types' +import {useToast} from '@/ui/notification/useToast' +import styles from './sheets-tab.module.scss' export interface ContextMenuProps { readonly index: number readonly sheetnames: readonly string[] readonly isOpen: boolean readonly setIsOpen: (isOpen: boolean) => void + readonly left: number + readonly top: number } export const ContextMenuComponent = (props: ContextMenuProps) => { - const {index, sheetnames, isOpen, setIsOpen} = props + const {index, sheetnames: sheetNames, setIsOpen, top, left, isOpen} = props const [renameIsOpen, setRenameIsOpen] = useState(false) - const oldName = sheetnames[index] - const [sheetName, setSheetName] = useState(oldName) + const oldName = sheetNames[index] || '' + const [newName, setNewName] = useState(oldName) const DATA_SERVICE = useInjection(TYPES.Data) + const toast = useToast() + const openRename = () => { setRenameIsOpen(true) setIsOpen(false) } + const rename = () => { - if (sheetName === oldName) return + if (!newName) return + if (newName === oldName) return const sheetRename = new SheetRenameBuilder() .oldName(oldName) - .newName(sheetName) + .newName(newName) .build() DATA_SERVICE.handleTransaction(new Transaction([sheetRename], true)) } const deleteSheet = () => { + if (sheetNames.length === 1) { + toast.toast.error( + 'Deletion failed: A spreadsheet must have at least one sheet.' + ) + return + } const payload = new DeleteSheetBuilder().sheetIdx(index).build() DATA_SERVICE.handleTransaction(new Transaction([payload], true)) } + return ( -
+ <> setIsOpen(false)} + ariaHideApp={false} + style={{ + content: { + width: 'max-content', + height: 'max-content', + left: left, + top: top - 140, + }, + }} > -
重命名
-
删除
+
+
+ ✏️ Rename +
+
+
+ 🗑️ Delete +
+
setRenameIsOpen(false)} + ariaHideApp={false} > - setSheetName(e.target.value)} - /> - +
+ setNewName(e.target.value)} + /> + +
-
+ ) } diff --git a/src/components/sheets-tab/index.tsx b/src/components/sheets-tab/index.tsx index ad1b772a..d87cf640 100644 --- a/src/components/sheets-tab/index.tsx +++ b/src/components/sheets-tab/index.tsx @@ -31,6 +31,7 @@ export const SheetsTabComponent: FC = ({ } const [sheets, setSheets] = useState([] as string[]) const [isOpen, setIsOpen] = useState(false) + const [modalPosition, setModalPosition] = useState({top: 0, left: 0}) useEffect(() => { getSheets().then((v) => { @@ -82,6 +83,10 @@ export const SheetsTabComponent: FC = ({ e.stopPropagation() activeSheet$(i) setIsOpen(true) + setModalPosition({ + top: e.clientY, + left: e.clientX, + }) }} > {sheet} @@ -101,7 +106,7 @@ export const SheetsTabComponent: FC = ({ DATA_SERVICE.handleTransaction( new Transaction([payload], true) ).then((v) => { - if (isErrorMessage(v)) return + if (v) return activeSheet$(newIdx) }) } else if (action === 'remove') { @@ -113,6 +118,8 @@ export const SheetsTabComponent: FC = ({ activeKey={sheets[activeSheet]} /> 重命名 -// -// -// -// -// -// -// -// `, -// }) -export const RenameComponent = () => { - // constructor( - // @Inject(MAT_DIALOG_DATA) private readonly _sheetname: string, - // ) { - // this.sheetname = this._sheetname - // } - // sheetname = '' - return
rename component
-} diff --git a/src/components/sheets-tab/sheets-tab.module.scss b/src/components/sheets-tab/sheets-tab.module.scss index 8af69fa0..699e07c6 100644 --- a/src/components/sheets-tab/sheets-tab.module.scss +++ b/src/components/sheets-tab/sheets-tab.module.scss @@ -37,3 +37,65 @@ cursor: pointer; @include center(); } +.context-menu { + display: flex; + flex-direction: column; +} +.context-menu-item { + padding: 10px 16px; + cursor: pointer; + transition: background 0.2s; + text-align: center; + font-size: 14px; + height: 20px; + + &:hover { + background: #f5f5f5; + } + + &.danger { + color: #e63946; + } +} +.context-menu-divider { + height: 1px; + background: #e0e0e0; + margin: 8px 0; +} + +.rename-modal { + background: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.rename-modal-content { + display: flex; + flex-direction: column; + align-items: center; +} + +.rename-input { + margin: 16px 0; + padding: 8px; + width: 100%; + max-width: 300px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.rename-close-button { + padding: 8px 16px; + background: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s; + + &:hover { + background: #0056b3; + } +} diff --git a/src/core/data2/render.ts b/src/core/data2/render.ts index ede1560f..ea007af3 100644 --- a/src/core/data2/render.ts +++ b/src/core/data2/render.ts @@ -20,11 +20,11 @@ export class RenderCell { return this } setInfo(info: CellInfo) { - let c = this.info - if (!c) c = new StandardCell() + const c = new StandardCell() if (info.style) c.setStyle(info.style) if (info.value) c.value = StandardValue.from(info.value) if (info.formula !== undefined) c.formula = info.formula + this.info = c return this } @@ -70,11 +70,11 @@ export function toCanvasPosition( if (type == 'row') { rowOffset = LeftTop.height - anchorY colOffset = 0 - } else if (type == 'cell') { - rowOffset = LeftTop.height - anchorY + } else if (type == 'col') { + rowOffset = 0 colOffset = LeftTop.width - anchorX } else { - rowOffset = 0 + rowOffset = LeftTop.height - anchorY colOffset = LeftTop.width - anchorX } return new Range() diff --git a/src/core/data2/service.ts b/src/core/data2/service.ts index 5e1b703e..ea2dc481 100644 --- a/src/core/data2/service.ts +++ b/src/core/data2/service.ts @@ -20,7 +20,7 @@ export interface DataService { registryCustomFunc(f: CustomFunc): Resp registryCellUpdatedCallback(f: () => void): void registrySheetUpdatedCallback(f: () => void): void - handleTransaction(t: Transaction): Resp + handleTransaction(t: Transaction): Resp undo(): Resp redo(): Resp @@ -116,7 +116,7 @@ export class DataServiceImpl implements DataService { return this._workbook.registryCellUpdatedCallback(f) } - public handleTransaction(transaction: Transaction): Resp { + public handleTransaction(transaction: Transaction): Resp { return this._workbook.handleTransaction({transaction}) } diff --git a/src/core/data2/workbook/client.ts b/src/core/data2/workbook/client.ts index 5299d088..5b4bbc0f 100644 --- a/src/core/data2/workbook/client.ts +++ b/src/core/data2/workbook/client.ts @@ -38,7 +38,7 @@ export interface IWorkbookClient { undo(): Resp redo(): Resp - handleTransaction(params: HandleTransactionParams): Resp + handleTransaction(params: HandleTransactionParams): Resp loadWorkbook(params: LoadWorkbookParams): Resp @@ -130,7 +130,7 @@ export class WorkbookClient implements IWorkbookClient { ) as Resp } - getCell(params: GetCellParams): Resp { + async getCell(params: GetCellParams): Resp { const result = this._call('getCell', params) as Resp return result.then((v) => { if (!isErrorMessage(v)) return new Cell(v) @@ -138,11 +138,11 @@ export class WorkbookClient implements IWorkbookClient { }) } - getCellPosition(params: GetCellParams): Resp { + async getCellPosition(params: GetCellParams): Resp { return this._call('getCellPosition', params) as Resp } - undo(): Resp { + async undo(): Resp { return this._call('undo', undefined) as Resp } @@ -150,8 +150,8 @@ export class WorkbookClient implements IWorkbookClient { return this._call('redo', undefined) as Resp } - handleTransaction(params: HandleTransactionParams): Resp { - return this._call('handleTransaction', params) as Resp + async handleTransaction(params: HandleTransactionParams): Resp { + return this._call('handleTransaction', params) as Resp } loadWorkbook(params: LoadWorkbookParams): Resp { diff --git a/src/core/data2/workbook/workbook.worker.ts b/src/core/data2/workbook/workbook.worker.ts index edc35995..c395a8d2 100644 --- a/src/core/data2/workbook/workbook.worker.ts +++ b/src/core/data2/workbook/workbook.worker.ts @@ -42,7 +42,7 @@ interface IWorkbookWorker { undo(): Result redo(): Result - handleTransaction(params: HandleTransactionParams): Result + handleTransaction(params: HandleTransactionParams): Result loadWorkbook(params: LoadWorkbookParams): Result } @@ -85,8 +85,9 @@ class WorkerService implements IWorkbookWorker { ctx.postMessage({id: WorkerUpdate.Ready}) } - public handleTransaction(params: HandleTransactionParams): ActionEffect { - return this.workbook.execTransaction(params.transaction) + public handleTransaction(params: HandleTransactionParams): void { + this.workbook.execTransaction(params.transaction) + return } public getAllSheetInfo(): readonly SheetInfo[] { diff --git a/src/core/painter/painter.service.ts b/src/core/painter/painter.service.ts index dcbde063..7ba3d3c0 100644 --- a/src/core/painter/painter.service.ts +++ b/src/core/painter/painter.service.ts @@ -148,6 +148,7 @@ export class PainterService extends CanvasApi { } public text(txt: string, attr: TextAttr, box: Box): void { + if (txt === '') return this.save() const textWidth = attr.font.measureText(txt).width const [tx, textAlign] = box.textX(attr.alignment?.horizontal) diff --git a/yarn.lock b/yarn.lock index 20a8b5ae..9718fe9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6284,10 +6284,10 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.1.1": - version: 1.2.1 - resolution: "clsx@npm:1.2.1" - checksum: 10c0/34dead8bee24f5e96f6e7937d711978380647e936a22e76380290e35486afd8634966ce300fc4b74a32f3762c7d4c0303f442c3e259f4ce02374eb0c82834f27 +"clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839 languageName: node linkType: hard @@ -15001,15 +15001,15 @@ __metadata: languageName: node linkType: hard -"react-toastify@npm:^9.0.5": - version: 9.1.3 - resolution: "react-toastify@npm:9.1.3" +"react-toastify@npm:^11.0.2": + version: 11.0.2 + resolution: "react-toastify@npm:11.0.2" dependencies: - clsx: "npm:^1.1.1" + clsx: "npm:^2.1.1" peerDependencies: - react: ">=16" - react-dom: ">=16" - checksum: 10c0/51de1e51e9357a24773fbcd45a4db18bf74b8ec40d86a2bfb4a4fee23ca4f9fffdac5dfb7a3c21baea39971f72f72dfcdc79403a6de006f74d69e7bc12f8b3e0 + react: ^18 || ^19 + react-dom: ^18 || ^19 + checksum: 10c0/e1ba01846782d47d0bf9cec7e2b4804f9af30e50a203786523f16db33691c1491f2382832e7ec4b69c583d3634ea0e3efb383da00ca9076bcf4bdd906a54a85f languageName: node linkType: hard @@ -15028,16 +15028,6 @@ __metadata: languageName: node linkType: hard -"react-use-websocket@npm:^2.9.1": - version: 2.9.1 - resolution: "react-use-websocket@npm:2.9.1" - peerDependencies: - react: ">= 16.8.0" - react-dom: ">= 16.8.0" - checksum: 10c0/9a9d759d2224ff8925ccaafe675e589a37ecb7c93fde456c699a1dc0b449b6e1dd9d200391702de2a28e812f80238295cecd2b3b50d8dacf3af50f5d3c4440b9 - languageName: node - linkType: hard - "react@npm:^19": version: 19.0.0 resolution: "react@npm:19.0.0" @@ -15628,11 +15618,10 @@ __metadata: react-modal: "npm:^3.16.3" react-scripts: "npm:5.0.0" react-select: "npm:^5.4.0" - react-toastify: "npm:^9.0.5" + react-toastify: "npm:^11.0.2" react-transition-group: "npm:^4.4.2" - react-use-websocket: "npm:^2.9.1" reflect-metadata: "npm:^0.1.13" - rxjs: "npm:^7.5.2" + rxjs: "npm:^7.8.1" sass: "npm:^1.83.0" sass-loader: "npm:^16.0.4" source-map-loader: "npm:^3.0.1" @@ -15682,7 +15671,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.2": +"rxjs@npm:^7.8.1": version: 7.8.1 resolution: "rxjs@npm:7.8.1" dependencies: