Skip to content

Commit

Permalink
Merge branch 'Simon-Initiative:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
dtiwarATS committed Jan 24, 2024
2 parents da52c0b + cce037f commit d8f24b1
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 82 deletions.
135 changes: 106 additions & 29 deletions assets/src/components/editing/elements/table/TableDropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
expandCellRight,
splitCell,
} from './table-cell-merge-operations';
import { getColspan, getRowColumnIndex, getRowspan, getVisualGrid } from './table-util';

// Dropdown menu that appears in each table cell.
interface Props {
Expand All @@ -28,6 +29,18 @@ interface Props {
mode?: 'table' | 'conjugation'; // The conjugation element has a special kind of table that uses most, but not all, of this functionality
}

const Columns = ({ children }: { children: React.ReactNode }) => (
<div className="d-flex flex-row">{children}</div>
);

const LeftColumn = ({ children }: { children: React.ReactNode }) => (
<div className="d-flex flex-column border-r-2">{children}</div>
);

const RightColumn = ({ children }: { children: React.ReactNode }) => (
<div className="d-flex flex-column">{children}</div>
);

export const DropdownMenu: React.FC<Props> = ({ editor, model, mode = 'table' }) => {
const onToggleHeader = () => {
const path = ReactEditor.findPath(editor, model);
Expand Down Expand Up @@ -62,22 +75,75 @@ export const DropdownMenu: React.FC<Props> = ({ editor, model, mode = 'table' })
[editor],
);

const undefinedOrOne = (value: number | undefined) => value === undefined || value === 1;

const onDeleteRow = () => {
const path = ReactEditor.findPath(editor, model);
Transforms.deselect(editor);
Transforms.removeNodes(editor, { at: Path.parent(path) });
Editor.withoutNormalizing(editor, () => {
const path = ReactEditor.findPath(editor, model);
const [, parentPath] = Editor.parent(editor, path);
const [table] = Editor.parent(editor, parentPath);
// When we delete a row, we have to delete the row, and any cells that have a rowspan > 1 in that row should
// have their rowspan reduced by 1.
Transforms.deselect(editor);
Transforms.removeNodes(editor, { at: Path.parent(path) });

const visualGrid = getVisualGrid(table as Table);
const targetCellId = model.id;
const coords = getRowColumnIndex(visualGrid, targetCellId);
if (!coords) return; // This should never happen, but just in case
const { rowIndex } = coords;
const visualRow = visualGrid[rowIndex];
const alreadyModified: TableCell[] = [];

// Go through each cell in the row and reduce the rowspan of any cells that have a rowspan > 1.
for (let columnIndex = 0; columnIndex < visualRow.length; columnIndex++) {
const cell = visualRow[columnIndex];
const cellPath = ReactEditor.findPath(editor, cell);

if (alreadyModified.includes(cell)) continue; // A cell with a bigger rowspan only needs to be deleted/shrunk once
alreadyModified.push(cell);

if (getRowspan(cell) > 1) {
Transforms.setNodes(editor, { rowspan: getRowspan(cell) - 1 }, { at: cellPath });
// Shrinking cell's rowspan
}
}
});
};

const onDeleteColumn = () => {
Editor.withoutNormalizing(editor, () => {
const path = ReactEditor.findPath(editor, model);
const [, parentPath] = Editor.parent(editor, path);
const [table] = Editor.parent(editor, parentPath);

const rows = table.children.length;
for (let i = 0; i < rows; i += 1) {
path[path.length - 2] = i;
Transforms.removeNodes(editor, { at: path });
const visualGrid = getVisualGrid(table as Table);
const targetCellId = model.id;

// Figure out what visual column we want to delete.
const coords = getRowColumnIndex(visualGrid, targetCellId);
if (!coords) return; // This should never happen, but just in case
const { columnIndex } = coords;

const alreadyModified: TableCell[] = [];

// Go through each row and delete the cell at the target column index.
// If the cell is a merged cell, we need to just remove one from it's colspan
// and not delete the cell.
for (let rowIndex = 0; rowIndex < visualGrid.length; rowIndex++) {
const row = visualGrid[rowIndex];
const cell = row[columnIndex];
const cellPath = ReactEditor.findPath(editor, cell);

if (alreadyModified.includes(cell)) continue; // A cell with a bigger rowspan only needs to be deleted/shrunk once
alreadyModified.push(cell);

if (getColspan(cell) > 1) {
Transforms.setNodes(editor, { colspan: getColspan(cell) - 1 }, { at: cellPath });
// Shrinking cell's colspan
} else {
Transforms.removeNodes(editor, { at: cellPath });
// Deleting cell
}
}
});
};
Expand All @@ -98,34 +164,47 @@ export const DropdownMenu: React.FC<Props> = ({ editor, model, mode = 'table' })
<i className="fa-solid fa-ellipsis-vertical"></i>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={onToggleHeader}>Toggle Header</Dropdown.Item>
<Dropdown.Divider />
<Columns>
<LeftColumn>
{mode == 'table' && <AlignmentOptions editor={editor} />}

<Dropdown.Header>Header</Dropdown.Header>
<Dropdown.Item onClick={onToggleHeader}>Toggle Header</Dropdown.Item>
<Dropdown.Divider />

<Dropdown.Header>Border</Dropdown.Header>
<Dropdown.Header>Border</Dropdown.Header>

<Dropdown.Item onClick={onBorderStyle('solid')}>Solid</Dropdown.Item>
<Dropdown.Item onClick={onBorderStyle('solid')}>Solid</Dropdown.Item>

<Dropdown.Item onClick={onBorderStyle('hidden')}>Hidden</Dropdown.Item>
<Dropdown.Item onClick={onBorderStyle('hidden')}>Hidden</Dropdown.Item>

<Dropdown.Divider />
{mode == 'table' && <AddOptions editor={editor} model={model} />}
</LeftColumn>
<RightColumn>
<Dropdown.Header>Row Style</Dropdown.Header>

<Dropdown.Header>Row Style</Dropdown.Header>
<Dropdown.Item onClick={onRowStyle('plain')}>Plain</Dropdown.Item>

<Dropdown.Item onClick={onRowStyle('plain')}>Plain</Dropdown.Item>
<Dropdown.Item onClick={onRowStyle('alternating')}>Alternating Stripes</Dropdown.Item>

<Dropdown.Item onClick={onRowStyle('alternating')}>Alternating Stripes</Dropdown.Item>
<Dropdown.Divider />

{mode == 'table' && <SplitOptions editor={editor} />}
{mode == 'table' && <AlignmentOptions editor={editor} />}
{mode == 'table' && <AddOptions editor={editor} model={model} />}
{mode == 'table' && <SplitOptions editor={editor} />}

<Dropdown.Divider />
<Dropdown.Divider />

<Dropdown.Header>Delete</Dropdown.Header>
<Dropdown.Item onClick={onDeleteRow}>Row</Dropdown.Item>
<Dropdown.Item onClick={onDeleteColumn}>Column</Dropdown.Item>
<Dropdown.Header>Delete</Dropdown.Header>
{undefinedOrOne(model.rowspan) && (
<Dropdown.Item onClick={onDeleteRow}>Row</Dropdown.Item>
)}
{undefinedOrOne(model.colspan) && (
/* Do not allow us to delete rows or columns if starting from a merged cell */
<Dropdown.Item onClick={onDeleteColumn}>Column</Dropdown.Item>
)}

<Dropdown.Item onClick={onDeleteTable}>Table</Dropdown.Item>
<Dropdown.Item onClick={onDeleteTable}>Table</Dropdown.Item>
</RightColumn>
</Columns>
</Dropdown.Menu>
</Dropdown>
);
Expand Down Expand Up @@ -215,8 +294,6 @@ const AlignmentOptions: React.FC<{ editor: Editor }> = ({ editor }) => {
);
return (
<>
<Dropdown.Divider />

<Dropdown.Header>Alignment</Dropdown.Header>

<div className="ml-3 btn-group btn-group-toggle">
Expand All @@ -230,6 +307,8 @@ const AlignmentOptions: React.FC<{ editor: Editor }> = ({ editor }) => {
<i className="fa-solid fa-align-right"></i>
</button>
</div>

<Dropdown.Divider />
</>
);
};
Expand All @@ -241,8 +320,6 @@ const SplitOptions: React.FC<{ editor: Editor }> = ({ editor }) => {

return (
<>
<Dropdown.Divider />

<Dropdown.Header>Split / Merge</Dropdown.Header>

<Dropdown.Item disabled={!canMergeRight} onClick={() => expandCellRight(editor)}>
Expand Down
20 changes: 19 additions & 1 deletion assets/src/components/editing/elements/table/table-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const getRowColspan = (row: ContentModel.TableRow): number => {
return row.children.reduce((sum, cell) => sum + getColspan(cell), 0);
};

type VisualGrid = ContentModel.TableCell[][];

/**
* Given a Table, with cells that may have colspan / rowspan attributes,
* return a 2 dimensional array that represents what cells would be in which position.
Expand All @@ -43,7 +45,7 @@ const getRowColspan = (row: ContentModel.TableRow): number => {
* returned value)
*
*/
export const getVisualGrid = (table: ContentModel.Table): ContentModel.TableCell[][] => {
export const getVisualGrid = (table: ContentModel.Table): VisualGrid => {
const maxColumns = table.children.reduce((max, row) => Math.max(max, getRowColspan(row)), 0);

const grid: ContentModel.TableCell[][] = Array(table.children.length)
Expand Down Expand Up @@ -78,3 +80,19 @@ export const getVisualGrid = (table: ContentModel.Table): ContentModel.TableCell

return gridWithoutNulls;
};

export const getRowColumnIndex = (
grid: VisualGrid,
cellId: string,
): { rowIndex: number; columnIndex: number } | null => {
for (let rowIndex = 0; rowIndex < grid.length; rowIndex++) {
const row = grid[rowIndex];
for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
const cell = row[columnIndex];
if (cell.id === cellId) {
return { rowIndex, columnIndex };
}
}
}
return null;
};
69 changes: 48 additions & 21 deletions lib/oli/authoring/editing/container_editor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,14 @@ defmodule Oli.Authoring.Editing.ContainerEditor do
result
end

def add_new(container, type, %Author{} = author, %Project{} = project, numberings \\ nil)
def add_new(
container,
type,
scored,
%Author{} = author,
%Project{} = project,
numberings \\ nil
)
when is_binary(type) do
attrs = %{
tags: [],
Expand All @@ -135,44 +142,64 @@ defmodule Oli.Authoring.Editing.ContainerEditor do
end,
title:
case type do
"Adaptive" -> "New Adaptive Page"
"Scored" -> "New Assessment"
"Unscored" -> "New Page"
"Container" -> new_container_name(numberings, container)
"Adaptive" ->
case scored do
"Scored" -> "New Adaptive Assessment"
"Unscored" -> "New Adaptive Page"
end

"Basic" ->
case scored do
"Scored" -> "New Assessment"
"Unscored" -> "New Page"
end

"Container" ->
new_container_name(numberings, container)
end,
graded:
case type do
"Adaptive" -> false
"Scored" -> true
"Unscored" -> false
"Container" -> false
"Container" ->
false

_ ->
case scored do
"Scored" -> true
"Unscored" -> false
end
end,
max_attempts:
case type do
"Adaptive" -> 0
"Scored" -> 5
"Unscored" -> 0
"Container" -> nil
"Container" ->
nil

_ ->
case scored do
"Scored" -> 5
"Unscored" -> 0
end
end,
recommended_attempts:
case type do
"Adaptive" -> 0
"Scored" -> 5
"Unscored" -> 0
"Container" -> nil
"Container" ->
nil

_ ->
case scored do
"Scored" -> 5
"Unscored" -> 0
end
end,
scoring_strategy_id:
case type do
"Adaptive" -> ScoringStrategy.get_id_by_type("best")
"Scored" -> ScoringStrategy.get_id_by_type("best")
"Unscored" -> ScoringStrategy.get_id_by_type("best")
"Basic" -> ScoringStrategy.get_id_by_type("best")
"Container" -> nil
end,
resource_type_id:
case type do
"Adaptive" -> Oli.Resources.ResourceType.get_id_by_type("page")
"Scored" -> Oli.Resources.ResourceType.get_id_by_type("page")
"Unscored" -> Oli.Resources.ResourceType.get_id_by_type("page")
"Basic" -> Oli.Resources.ResourceType.get_id_by_type("page")
"Container" -> Oli.Resources.ResourceType.get_id_by_type("container")
end
}
Expand Down
3 changes: 2 additions & 1 deletion lib/oli_web/live/curriculum/container/container_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,11 @@ defmodule OliWeb.Curriculum.ContainerLive do
end

# handle clicking of the "Add Graded Assessment" or "Add Practice Page" buttons
def handle_event("add", %{"type" => type}, socket) do
def handle_event("add", %{"type" => type, "scored" => scored}, socket) do
case ContainerEditor.add_new(
socket.assigns.container,
type,
scored,
socket.assigns.author,
socket.assigns.project,
socket.assigns.numberings
Expand Down
Loading

0 comments on commit d8f24b1

Please sign in to comment.