Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,7 @@ export const demoData = {
name: "Split Panes",
colNumber: 41,
rowNumber: 60,
isLocked: true,
rows: {},
cols: {
0: {
Expand Down
3 changes: 2 additions & 1 deletion packages/o-spreadsheet-engine/src/collaborative/ot/ot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ function transformSheetId(
}

const deleteSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
if (toTransform.sheetId === deleteSheet) {
const lockSheet = executed.type === "LOCK_SHEET" && executed.sheetId;
if (toTransform.sheetId === deleteSheet || toTransform.sheetId === lockSheet) {
return "IGNORE_COMMAND";
} else if (
toTransform.type === "CREATE_SHEET" ||
Expand Down
17 changes: 17 additions & 0 deletions packages/o-spreadsheet-engine/src/helpers/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,23 @@ export function batched(callback: () => void): () => void {
};
}

/** Returns a copy of the function `callback` that can only be called
* at most once every `delay` milliseconds.
*/
export function throttle<T extends (...args: any[]) => any>(
callback: T,
delay: number
): (...args: Parameters<T>) => ReturnType<T> {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return callback(...args);
}
};
}

/*
* Concatenate an array of strings.
*/
Expand Down
19 changes: 13 additions & 6 deletions packages/o-spreadsheet-engine/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
CoreCommand,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo in commit message

DispatchResult,
isCoreCommand,
LocalCommand,
} from "./types/commands";
import { CoreGetters } from "./types/core_getters";
import { Getters } from "./types/getters";
Expand Down Expand Up @@ -333,7 +332,7 @@ export class Model extends EventBus<any> implements CommandDispatcher {
initialRevisionId: revisionId,
recordChanges: this.state.recordChanges.bind(this.state),
dispatch: (command: CoreCommand) => {
const result = this.checkDispatchAllowed(command);
const result = this.checkDispatchAllowedRemoteCommand(command);
if (!result.isSuccessful) {
// core views plugins need to be invalidated
this.dispatchToHandlers(this.coreHandlers, {
Expand Down Expand Up @@ -454,19 +453,26 @@ export class Model extends EventBus<any> implements CommandDispatcher {
const results = isCoreCommand(command)
? this.checkDispatchAllowedCoreCommand(command)
: this.checkDispatchAllowedLocalCommand(command);
return this.processCommandResults(results);
}

private processCommandResults(results: (CommandResult | CommandResult[])[]): DispatchResult {
if (results.some((r) => r !== CommandResult.Success)) {
return new DispatchResult(results.flat());
}
return DispatchResult.Success;
}

private checkDispatchAllowedRemoteCommand(command: CoreCommand): DispatchResult {
const results = this.coreHandlers.map((handler) => handler.allowDispatch(command));
return this.processCommandResults(results);
}

private checkDispatchAllowedCoreCommand(command: CoreCommand) {
const results = this.corePlugins.map((handler) => handler.allowDispatch(command));
results.push(this.range.allowDispatch(command));
return results;
return this.handlers.map((handler) => handler.allowDispatch(command));
}

private checkDispatchAllowedLocalCommand(command: LocalCommand) {
private checkDispatchAllowedLocalCommand(command: Command) {
return this.uiHandlers.map((handler) => handler.allowDispatch(command));
}

Expand Down Expand Up @@ -515,6 +521,7 @@ export class Model extends EventBus<any> implements CommandDispatcher {
const result = this.checkDispatchAllowed(command);
if (!result.isSuccessful) {
this.trigger("update");
this.trigger("command-rejected", { command, result });
return result;
}
this.status = Status.Running;
Expand Down
16 changes: 16 additions & 0 deletions packages/o-spreadsheet-engine/src/plugins/core/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
"getUnboundedZone",
"checkElementsIncludeAllNonFrozenHeaders",
"getDuplicateSheetName",
"isSheetLocked",
] as const;

readonly sheetIdsMapName: Record<string, UID | undefined> = {};
Expand Down Expand Up @@ -247,6 +248,13 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
case "UNFREEZE_COLUMNS_ROWS":
this.setPaneDivisions(cmd.sheetId, 0, "COL");
this.setPaneDivisions(cmd.sheetId, 0, "ROW");
break;
case "LOCK_SHEET":
this.history.update("sheets", cmd.sheetId, "isLocked", true);
break;
case "UNLOCK_SHEET":
this.history.update("sheets", cmd.sheetId, "isLocked", false);
break;
}
}

Expand Down Expand Up @@ -278,6 +286,7 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
ySplit: sheetData.panes?.ySplit || 0,
},
color: sheetData.color,
isLocked: sheetData.isLocked,
};
this.orderedSheetIds.push(sheet.id);
this.sheets[sheet.id] = sheet;
Expand Down Expand Up @@ -306,6 +315,7 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
areGridLinesVisible:
sheet.areGridLinesVisible === undefined ? true : sheet.areGridLinesVisible,
isVisible: sheet.isVisible,
isLocked: sheet.isLocked,
color: sheet.color,
};
if (sheet.panes.xSplit || sheet.panes.ySplit) {
Expand All @@ -331,6 +341,10 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
return this.getSheet(sheetId).areGridLinesVisible;
}

isSheetLocked(sheetId: UID): boolean {
return this.tryGetSheet(sheetId)?.isLocked || false;
}

tryGetSheet(sheetId: UID): Sheet | undefined {
return this.sheets[sheetId];
}
Expand Down Expand Up @@ -609,6 +623,7 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
xSplit: 0,
ySplit: 0,
},
isLocked: false,
};
const orderedSheetIds = this.orderedSheetIds.slice();
orderedSheetIds.splice(position, 0, sheet.id);
Expand Down Expand Up @@ -737,6 +752,7 @@ export class SheetPlugin extends CorePlugin<SheetState> implements SheetState {
const newSheet: Sheet = deepCopy(sheet);
newSheet.id = toId;
newSheet.name = toName;
newSheet.isLocked = false;
for (let col = 0; col <= newSheet.numberOfCols; col++) {
for (let row = 0; row <= newSheet.rows.length; row++) {
if (newSheet.rows[row]) {
Expand Down
4 changes: 3 additions & 1 deletion packages/o-spreadsheet-engine/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { GeoFeaturePlugin } from "./ui_feature/geo_features";
import { HeaderVisibilityUIPlugin } from "./ui_feature/header_visibility_ui";
import { InsertPivotPlugin } from "./ui_feature/insert_pivot";
import { HistoryPlugin } from "./ui_feature/local_history";
import { LockSheetPlugin } from "./ui_feature/lock_sheet";
import { PivotPresencePlugin } from "./ui_feature/pivot_presence_plugin";
import { SortPlugin } from "./ui_feature/sort";
import { SplitToColumnsPlugin } from "./ui_feature/split_to_columns";
Expand Down Expand Up @@ -110,7 +111,8 @@ export const statefulUIPluginRegistry = new Registry<UIPluginConstructor>()
.add("header_positions", HeaderPositionsUIPlugin)
.add("viewport", SheetViewPlugin)
.add("clipboard", ClipboardPlugin)
.add("carousel_ui", CarouselUIPlugin);
.add("carousel_ui", CarouselUIPlugin)
.add("lock_sheet", LockSheetPlugin);

// Plugins which have a derived state from core data
export const coreViewsPluginRegistry = new Registry<CoreViewPluginConstructor>()
Expand Down
34 changes: 34 additions & 0 deletions packages/o-spreadsheet-engine/src/plugins/ui_feature/lock_sheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { _t } from "../../translation";
import { Command, CommandResult, lockedSheetAllowedCommands } from "../../types/commands";
import { UIPlugin } from "../ui_plugin";

export class LockSheetPlugin extends UIPlugin {
static getters = ["isCurrentSheetLocked"] as const;

allowDispatch(cmd: Command): CommandResult | CommandResult[] {
/**
* isDashboard() implies that the user is not connected
* to other users and can do any operation and can do core modifications that will only affect them
* It is acceptable to bypass the locked sheet restriction in this case
*/
Comment on lines +9 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't we way that we would add a commit to ensure that ? we can have a dashboard mode with still a collaborative session

if (lockedSheetAllowedCommands.has(cmd.type) || this.getters.isDashboard()) {
return CommandResult.Success;
}
if (
("sheetId" in cmd && this.getters.isSheetLocked(cmd.sheetId)) ||
this.getters.isSheetLocked(this.getters.getActiveSheetId())
) {
this.ui.notifyUI({
type: "info",
text: _t("This sheet is locked and cannot be modified. Please unlock it first."),
sticky: false,
});
return CommandResult.SheetLocked;
}
return CommandResult.Success;
}

isCurrentSheetLocked() {
return this.getters.isSheetLocked(this.getters.getActiveSheetId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ export class SheetUIPlugin extends UIPlugin {
* sheet.
*/
private checkSheetExists(cmd: Command): CommandResult {
if ("sheetId" in cmd && this.getters.tryGetSheet(cmd.sheetId) === undefined) {
if (
"sheetId" in cmd &&
this.getters.tryGetSheet(cmd.sheetId) === undefined &&
cmd.type !== "CREATE_SHEET"
) {
return CommandResult.InvalidSheetId;
}
return CommandResult.Success;
Expand Down
47 changes: 44 additions & 3 deletions packages/o-spreadsheet-engine/src/types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ export interface SheetDependentCommand {
sheetId: UID;
}

export function isSheetDependent(
cmd: CoreCommand
): cmd is Extract<CoreCommand, SheetDependentCommand> {
export function isSheetDependent(cmd: Command): cmd is Extract<CoreCommand, SheetDependentCommand> {
return "sheetId" in cmd;
}

Expand Down Expand Up @@ -221,6 +219,36 @@ export const readonlyAllowedCommands = new Set<CommandTypes>([
"UPDATE_PIVOT",
]);

export const lockedSheetAllowedCommands = new Set<Command["type"]>([
"LOCK_SHEET",
"UNLOCK_SHEET",
"UPDATE_LOCALE",
"MOVE_SHEET",
"DUPLICATE_SHEET",
"CREATE_SHEET",
"COPY",
"START",
"SCROLL_TO_CELL",
"ACTIVATE_SHEET",
"RESIZE_SHEETVIEW",
"SET_VIEWPORT_OFFSET",
"SET_FORMULA_VISIBILITY",
"SELECT_FIGURE", // not sure
"EVALUATE_CHARTS",
"EVALUATE_CELLS",
"UNDO",
"REDO",
"REQUEST_UNDO",
"REQUEST_REDO",
"REPLACE_SEARCH",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowing replace on a locked sheet is strange. Does the command contains the sheetId if we replace on all sheet?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the plugin computes all positions eligible for a replace and will skip the ones on a locked sheet, the same it skips the cells which contain a formula and for which we don't specify the searchFormula argument.

"HIDE_SHEET",
"SHOW_SHEET",
"UPDATE_PIVOT",
"INSERT_NEW_PIVOT",
"ADD_PIVOT",
"REMOVE_PIVOT",
Comment on lines +246 to +249
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are those even sheet dependent ?

]);

export const coreTypes = new Set<CoreCommandTypes>([
/** CELLS */
"UPDATE_CELL",
Expand Down Expand Up @@ -255,6 +283,8 @@ export const coreTypes = new Set<CoreCommandTypes>([
"COLOR_SHEET",
"HIDE_SHEET",
"SHOW_SHEET",
"LOCK_SHEET",
"UNLOCK_SHEET",

/** RANGES MANIPULATION */
"MOVE_RANGES",
Expand Down Expand Up @@ -834,6 +864,14 @@ export interface RemoveDataValidationCommand extends SheetDependentCommand {
id: string;
}

export interface LockSheetCommand extends SheetDependentCommand {
type: "LOCK_SHEET";
}

export interface UnlockSheetCommand extends SheetDependentCommand {
type: "UNLOCK_SHEET";
}

//#endregion

//#region Local Commands
Expand Down Expand Up @@ -1164,6 +1202,8 @@ export type CoreCommand =
| ColorSheetCommand
| HideSheetCommand
| ShowSheetCommand
| LockSheetCommand
| UnlockSheetCommand

/** RANGES MANIPULATION */
| MoveRangeCommand
Expand Down Expand Up @@ -1462,6 +1502,7 @@ export const enum CommandResult {
InvalidPivotCustomField = "InvalidPivotCustomField",
MissingFigureArguments = "MissingFigureArguments",
InvalidCarouselItem = "InvalidCarouselItem",
SheetLocked = "SheetLocked",
}

export interface CommandHandler<T> {
Expand Down
4 changes: 3 additions & 1 deletion packages/o-spreadsheet-engine/src/types/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { DynamicTranslate } from "../plugins/ui_feature/dynamic_translate";
import { GeoFeaturePlugin } from "../plugins/ui_feature/geo_features";
import { HeaderVisibilityUIPlugin } from "../plugins/ui_feature/header_visibility_ui";
import { HistoryPlugin } from "../plugins/ui_feature/local_history";
import { LockSheetPlugin } from "../plugins/ui_feature/lock_sheet";
import { PivotPresencePlugin } from "../plugins/ui_feature/pivot_presence_plugin";
import { SortPlugin } from "../plugins/ui_feature/sort";
import { SplitToColumnsPlugin } from "../plugins/ui_feature/split_to_columns";
Expand Down Expand Up @@ -72,4 +73,5 @@ export type Getters = {
PluginGetters<typeof CheckboxTogglePlugin> &
PluginGetters<typeof CellIconPlugin> &
PluginGetters<typeof DynamicTranslate> &
PluginGetters<typeof CarouselUIPlugin>;
PluginGetters<typeof CarouselUIPlugin> &
PluginGetters<typeof LockSheetPlugin>;
1 change: 1 addition & 0 deletions packages/o-spreadsheet-engine/src/types/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export interface Sheet {
isVisible: boolean;
panes: PaneDivision;
color?: Color;
isLocked?: boolean;
}

export interface CellPosition {
Expand Down
1 change: 1 addition & 0 deletions packages/o-spreadsheet-engine/src/types/workbook_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface SheetData {
panes?: PaneDivision;
headerGroups?: Record<Dimension, HeaderGroup[]>;
color?: Color;
isLocked?: boolean;
}

interface WorkbookSettings {
Expand Down
2 changes: 2 additions & 0 deletions packages/o-spreadsheet-engine/src/types/xlsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { Alias, PaneDivision, UID } from "./misc";
* - pivot table location (XLSXPivotTableLocation): §18.10.1.49 (location)
* - pivot table style info (XLSXPivotTableStyleInfo): §18.10.7.74 (pivotTableStyleInfo)
* - rows (XLSXRow): §18.3.1.73 (row)
* - sheet Protection (XLSXSheetProtection): $18.3.1.85
* - sheet (XLSXWorksheet): §18.3.1.99 (worksheet)
* - sheet format (XLSXSheetFormat): §18.3.1.81 (sheetFormatPr)
* - sheet properties (XLSXSheetProperties): §18.3.1.82 (sheetPr)
Expand Down Expand Up @@ -241,6 +242,7 @@ export interface XLSXWorksheet {
hyperlinks: XLSXHyperLink[];
tables: XLSXTable[];
pivotTables: XLSXPivotTable[];
isLocked: boolean;
}

export interface XLSXSheetView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function convertSheets(
tables: [],
headerGroups: { COL: colHeaderGroups, ROW: rowHeaderGroups },
color: convertColor(sheet.sheetProperties?.tabColor),
isLocked: sheet.isLocked,
};
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export class XlsxSheetExtractor extends XlsxBaseExtractor {
tables: this.extractTables(sheetElement),
pivotTables: this.extractPivotTables(),
isVisible: sheetWorkbookInfo.state === "visible",
isLocked: this.extractProtection(sheetElement),
};
}
)[0];
Expand Down Expand Up @@ -382,4 +383,11 @@ export class XlsxSheetExtractor extends XlsxBaseExtractor {
}
return sfs;
}

private extractProtection(worksheet: Element): boolean {
const sheetProtectionElement = this.querySelector(worksheet, "sheetProtection");
if (!sheetProtectionElement) return false;

return this.extractAttr(sheetProtectionElement, "sheet", { default: false }).asBool()!;
}
}
Loading