Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,21 @@ jobs:

- name: Run Linter
run: npm run lint
test:
name: Run Tests
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "18"

- name: Install dependencies
run: npm install --legacy-peer-deps

- name: Run Tests
run: npm run test
44 changes: 44 additions & 0 deletions __mocks__/compostjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Jest mock for the ESM-only "compostjs" package
// Exposes named exports { compost, scale } used in the codebase.

const makeShape = (type, payload = {}) => ({
__mockShape: true,
type,
...payload,
});

const scale = {
continuous: (min, max) => ({
__mockScale: true,
kind: "continuous",
min,
max,
}),
categorical: (names) => ({ __mockScale: true, kind: "categorical", names }),
};

const compost = {
text: (x, y, content, font, color) =>
makeShape("TEXT", { x, y, content, font, color }),
bubble: (point, height, width) =>
makeShape("BUBBLE", { point, height, width }),
shape: (points) => makeShape("SHAPE", { points }),
line: (points) => makeShape("LINE", { points }),
column: (name, size) => makeShape("COLUMN", { name, size }),
bar: (size, name) => makeShape("BAR", { size, name }),
fillColor: (color, shape) => makeShape("FILLCOLOR", { color, shape }),
strokeColor: (color, shape) => makeShape("STROKECOLOR", { color, shape }),
font: (font, color, shape) => makeShape("FONT", { font, color, shape }),
nest: (x1, x2, y1, y2, shape) =>
makeShape("NEST", { x1, x2, y1, y2, shape }),
nestX: (start, end, shape) => makeShape("NESTX", { start, end, shape }),
nestY: (start, end, shape) => makeShape("NESTY", { start, end, shape }),
scale: (sx, sy, shape) => makeShape("SCALE", { sx, sy, shape }),
scaleX: (s, shape) => makeShape("SCALEX", { s, shape }),
scaleY: (s, shape) => makeShape("SCALEY", { s, shape }),
padding: (t, r, b, l, shape) => makeShape("PADDING", { t, r, b, l, shape }),
overlay: (shapes) => makeShape("OVERLAY", { shapes }),
axes: (config, shape) => makeShape("AXES", { config, shape }),
};

module.exports = { compost, scale };
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const config: Config = {
coverageProvider: "v8",
testEnvironment: "jsdom",
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
moduleNameMapper: {
"^compostjs$": "<rootDir>/__mocks__/compostjs.js",
},
Expand Down
1 change: 1 addition & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
21 changes: 21 additions & 0 deletions src/components/spreadsheet/spreadsheet.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ interface UseRenderedPanelsProps {
containerRef: MutableRefObject<HTMLDivElement>;
}

/**
* Hook that calculates the indexes of spreadsheet panels visible in the viewport
*
* @param containerRef - React Ref object to the container element
* @returns indexes of visible spreadsheet panels
*/
const useRenderedPanels = ({ containerRef }: UseRenderedPanelsProps) => {
const [renderedPanels, setRenderedPanels] = useState(new Set<number>());

Expand All @@ -44,6 +50,7 @@ const useRenderedPanels = ({ containerRef }: UseRenderedPanelsProps) => {

const panelBounds = [];

// map panel indixes to pixel bounds in the DOM (x, y, width, height)
for (let y = 0; y < PANEL_COUNT; y++) {
for (let x = 0; x < PANEL_COUNT; x++) {
const bounds = {
Expand All @@ -59,6 +66,7 @@ const useRenderedPanels = ({ containerRef }: UseRenderedPanelsProps) => {

const newRenderedPanels = new Set<number>();

// calculate panel/viewport intersections
for (let i = 0; i < PANEL_COUNT * PANEL_COUNT; i++) {
const bounds = panelBounds[i];

Expand All @@ -73,6 +81,7 @@ const useRenderedPanels = ({ containerRef }: UseRenderedPanelsProps) => {
}
}

// update the rendered panels
setRenderedPanels((prevRenderedPanels) => {
if (
prevRenderedPanels.size === newRenderedPanels.size &&
Expand Down Expand Up @@ -104,12 +113,14 @@ const useRenderedPanels = ({ containerRef }: UseRenderedPanelsProps) => {
return { renderedPanels };
};

// returns panel row and column indexes based on the panel index
const getPanelRowAndColumn = (panelIndex: number) => {
const panelRow = Math.floor(panelIndex / PANEL_COUNT);
const panelCol = panelIndex % PANEL_COUNT;
return { panelRow, panelCol };
};

// returns cell row and column indexes based on the panel row and column indexes
const getCellRowAndColumn = (
cellIndex: number,
panelRow: number,
Expand All @@ -130,6 +141,14 @@ interface SpreadsheetSpecificProps {
type SpreadsheetProps = SpreadsheetSpecificProps &
Omit<ComponentProps<"div">, "children">;

/**
* Renders the spreadsheet grid with the ability to render only the panels visible in the viewport
*
* @param children - function that renders the cell component
* @param selectedRows - set of indexes of selected rows
* @param selectedCols - set of indexes of selected columns
* @returns Spreadsheet component
*/
export const SpreadsheetComponent = ({
children,
selectedRows,
Expand All @@ -139,6 +158,7 @@ export const SpreadsheetComponent = ({
const containerRef = useRef<HTMLDivElement>(null);
const { renderedPanels } = useRenderedPanels({ containerRef });

// renders the column indicators
const ColumnRow = useCallback(() => {
const data = Array.from(Array(SPREADSHEET_SIZE + 1));

Expand All @@ -156,6 +176,7 @@ export const SpreadsheetComponent = ({
);
}, [selectedRows]);

// renders the row indicators
const RowColumn = useCallback(() => {
const data = Array.from(Array(SPREADSHEET_SIZE));

Expand Down
8 changes: 8 additions & 0 deletions src/components/spreadsheet/spreadsheet.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ export const DEFAULT_STEP = 0;
// default number of simulation steps
export const DEFAULT_STEPS = 50;

/**
* Holds cell data in the spreadsheet
*/
export namespace Spreadsheet {
export const data: Map<CellId, SpreadsheetCell> = new Map();

// retrieve cell data by cell id
export const get = (cellId: CellId): SpreadsheetCell => {
return data.get(cellId) ?? { formula: "" };
};

// set cell data by cell id
export const set = (cellId: CellId, cell: SpreadsheetCell): void => {
data.set(cellId, cell);
};

// update cell data by cell id
export const update = (
cellId: CellId,
cell: Partial<SpreadsheetCell>,
Expand All @@ -39,10 +45,12 @@ export namespace Spreadsheet {
Spreadsheet.set(cellId, { ...data, ...cell });
};

// remove cell data by cell id
export const remove = (cellId: CellId): void => {
Spreadsheet.data.delete(cellId);
};

// clear all cell data
export const clear = (): void => {
Spreadsheet.data.clear();
};
Expand Down
12 changes: 12 additions & 0 deletions src/components/spreadsheet/spreadsheet.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export interface UseSelectedRowsAndColsProps {
selectedCells: Set<CellId>;
}

/**
* Hook that calculates the indexes of selected rows and columns
*
* @param selectedCells - set of selected cell ids
* @returns indexes of selected rows and columns
*/
export const useSelectedRowsAndCols = ({
selectedCells,
}: UseSelectedRowsAndColsProps) => {
Expand Down Expand Up @@ -34,6 +40,12 @@ export interface UseFormulaFocusProps {
ref: MutableRefObject<HTMLInputElement>;
}

/**
* Hook that manages the focus state of the formula input
*
* @param ref - React Ref object to the formula input element
* @returns focus state of the formula input
*/
export const useFormulaFocus = ({ ref }: UseFormulaFocusProps) => {
const [focused, setFocused] = useState(false);

Expand Down
4 changes: 4 additions & 0 deletions src/components/spreadsheet/spreadsheet.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Value } from "@/runtime/runtime";

// data of one cell in the spreadsheet
export type SpreadsheetCell = {
formula: string;
color?: string;
Expand All @@ -12,8 +13,11 @@ type SpreadsheetRow = SpreadsheetCell[];

export type SpreadsheetData = SpreadsheetRow[];

// id of a cell (e.g. "A1", "B2", "C3", etc.)
export type CellId = `${string}${number}`;

// coordinates of a cell (e.g. { ri: 0, ci: 0 } for cell "A1")
export type CellCoords = { ri: number; ci: number };

// object holding the history of the spreadsheet
export type History = Map<CellId, Value[]>;
64 changes: 64 additions & 0 deletions src/components/spreadsheet/spreadsheet.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,24 @@ import { Spreadsheet } from "./spreadsheet.constants";
import { CellCoords, CellId, History } from "./spreadsheet.model";

export namespace SpreadsheetUtils {
/**
* Converts cell coordinates to cell id
*
* @param coords - cell coordinates
* @returns cell id
*/
export const cellCoordsToId = ({ ri, ci }: CellCoords): CellId => {
const col = SpreadsheetUtils.columnIndexToText(ci);
const row = ri + 1;
return `${col}${row}`;
};

/**
* Converts cell id to cell coordinates
*
* @param cellId - cell id
* @returns cell coordinates
*/
export const cellIdToCoords = (cellId: CellId): CellCoords => {
const match = cellId.match(/(\D+)(\d+)$/);

Expand All @@ -39,6 +51,12 @@ export namespace SpreadsheetUtils {
return { ri, ci };
};

/**
* Converts column index to column text (e.g. 0 -> "A", 1 -> "B")
*
* @param index - column index
* @returns column text
*/
export const columnIndexToText = (index: number): string => {
let column = "";
index += 1;
Expand All @@ -53,6 +71,12 @@ export namespace SpreadsheetUtils {
return column;
};

/**
* Converts column text to column index (e.g. "A" -> 0, "B" -> 1)
*
* @param text - column text
* @returns column index
*/
export const columnTextToIndex = (text: string): number => {
let index = 0;

Expand All @@ -63,6 +87,12 @@ export namespace SpreadsheetUtils {
return index - 1;
};

/**
* Parses raw cell formula string into the default and primary formula
*
* @param formula - formula string
* @returns default and primary formula
*/
export const getFormula = (
formula: string,
): { defaultFormula?: string; primaryFormula?: string } => {
Expand All @@ -88,6 +118,12 @@ export namespace SpreadsheetUtils {
};
};

/**
* Retrieves the cell ids from a formula string
*
* @param formula - formula string
* @returns cell ids
*/
export const getCellIdsFromFormula = (formula: string): CellId[] => {
const regex = /\$?([A-Z]+)\$?([0-9]+)/g;
const cellIds = [...formula.matchAll(regex)].map((match) => {
Expand All @@ -98,6 +134,13 @@ export namespace SpreadsheetUtils {
return cellIds;
};

/**
* Evaluates the cells in spreadsheet and returns the history object
*
* @param cells - cell ids
* @param steps - number of steps
* @returns history object
*/
export const evaluate = (
cells: CellId[],
steps: number,
Expand Down Expand Up @@ -141,6 +184,14 @@ export namespace SpreadsheetUtils {
return history;
};

/**
* Shifts the cell reference by the given offset
*
* @param ref - cell reference
* @param colOffset - column offset
* @param rowOffset - row offset
* @returns shifted cell reference
*/
export const shiftCellReference = (
ref: string,
colOffset: number,
Expand Down Expand Up @@ -178,6 +229,12 @@ export namespace SpreadsheetUtils {
return `${colDollar}${newColLetters}${rowDollar}${newRowNumber}`;
};

/**
* Parses raw formula input into a runtime value (e.g. "1" to "= 1")
*
* @param input - original formula
* @returns new formula
*/
export const tryGetFormulaFromCellValue = (input: string) => {
if (input.trim() === "") {
return "";
Expand All @@ -198,6 +255,12 @@ export namespace SpreadsheetUtils {
return `= "${input.trim()}"`;
};

/**
* Converts runtime value to textual representation (used in rendering cell text)
*
* @param value - runtime value
* @returns text
*/
export const getValueText = (value: Value) => {
switch (value.type) {
case ValueType.Number:
Expand Down Expand Up @@ -282,6 +345,7 @@ export namespace SpreadsheetUtils {
?.childNodes[0] as HTMLSpanElement;
};

// inserts text into formula element in the DOM
export const updateCellText = (cellId: CellId, value: string) => {
const element = getCellElement(cellId);
if (!element) return;
Expand Down
Loading