Commit 38ccc4e0 by Mac Stephens

Add task row delete/select workflow with focus retention, sticky column headers,…

Add task row delete/select workflow with focus retention, sticky column headers, and Enter-to-commit item number reordering.
parent 41b5088d
......@@ -56,9 +56,9 @@ object FViewMain: TFViewMain
object lblAppTitle: TWebLabel
Left = 57
Top = 33
Width = 42
Width = 48
Height = 14
Caption = 'App Title'
Caption = 'emT3Web'
ElementID = 'lbl_app_title'
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
......
......@@ -2,7 +2,7 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom shadow-sm">
<div class="container-fluid">
<div class="d-flex align-items-center gap-2">
<a id="lbl_app_title" class="navbar-brand fw-semibold" href="index.html">Koehler-Gibson Orders</a>
<a id="lbl_app_title" class="navbar-brand fw-semibold" href="index.html">emT3Web</a>
<span id="lbl_version" class="badge text-bg-light border text-muted fw-normal"></span>
</div>
......
......@@ -12,6 +12,7 @@ object FTasksHTML: TFTasksHTML
Caption = 'Reload'
ElementID = 'btn_reload'
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnReloadClick
end
......@@ -24,9 +25,25 @@ object FTasksHTML: TFTasksHTML
ChildOrder = 1
ElementID = 'btn_add_row'
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnAddRowClick
end
object btnDeleteRow: TWebButton
Left = 78
Top = 150
Width = 96
Height = 25
Caption = 'Delete Row'
ChildOrder = 2
ElementID = 'btn_delete_row'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnDeleteRowClick
end
object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 506
......
......@@ -3,11 +3,12 @@
<h5 class="mb-0" id="lbl_project_name"></h5>
<div class="d-flex gap-2">
<button id="btn_add_row" class="btn btn-sm btn-outline-success">Add Row</button>
<button id="btn_delete_row" class="btn btn-sm btn-outline-danger">Delete Row</button>
<button id="btn_reload" class="btn btn-sm btn-outline-primary">Reload</button>
</div>
</div>
<div id="tasks_table_host" class="flex-grow-1 min-vh-0"></div>
<div id="tasks_table_host" class="flex-grow-1 min-vh-0 overflow-auto"></div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasNameManager" aria-labelledby="nm_title">
<div class="offcanvas-header">
......
......@@ -29,14 +29,22 @@ type
btnAddRow: TWebButton;
xdwdsTasksitemNum: TIntegerField;
xdwdsTaskstaskItemId: TIntegerField;
btnDeleteRow: TWebButton;
[async] procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
[async] procedure btnDeleteRowClick(Sender: TObject);
private
FTaskId: string;
FReportedByOptions: TJSArray;
FAssignedToOptions: TJSArray;
FStatusOptions: TJSArray;
FPendingFocusTaskItemId: integer;
FPendingFocusField: string;
FSelectedTaskItemId: Integer;
FSelectedTaskId: Integer;
FPendingFocusItemNum: Integer;
FPendingFocusTaskField: string;
FNameManager: TNameManager;
[async] procedure LoadTasks(const ATaskId: string);
procedure RenderTable;
......@@ -51,6 +59,7 @@ type
[async] procedure SaveRow(AIndex: Integer);
procedure EditorBlur(Event: TJSEvent);
[async] function AddTaskRow: Boolean;
[async] function DeleteTaskRow: Boolean;
function ExtractOptionNames(const SourceArray: TJSArray): TJSArray;
function GetOptionsForField(const AFieldName: string): TJSArray;
procedure FocusTrigger(const ATriggerId: string);
......@@ -58,6 +67,11 @@ type
procedure DropdownEditClick(Event: TJSEvent);
function ExtractCodeDescs(const SourceArray: TJSArray): TJSArray;
[async] procedure MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
procedure ApplyPendingFocus;
procedure RowClick(Event: TJSEvent);
procedure ApplySelectedRowState;
procedure ApplyPendingDeleteFocus;
procedure EditorKeyDown(Event: TJSEvent);
public
end;
......@@ -79,6 +93,8 @@ begin
FReportedByOptions := TJSArray.new;
FAssignedToOptions := TJSArray.new;
FStatusOptions := TJSArray.new;
FSelectedTaskItemId := 0;
FSelectedTaskId := 0;
FNameManager := TNameManager.Create(
function(const AFieldName: string): TJSArray
......@@ -104,6 +120,7 @@ begin
end;
btnAddRow.Enabled := False;
btnDeleteRow.Enabled := False;
if not DMConnection.ApiConnection.Connected then
begin
......@@ -234,6 +251,7 @@ begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('input', TJSEventHandler(@EditorInput));
el.addEventListener('blur', TJSEventHandler(@EditorBlur));
el.addEventListener('keydown', TJSEventHandler(@EditorKeyDown));
end;
nodes := document.querySelectorAll('.task-select');
......@@ -257,6 +275,45 @@ begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@DropdownEditClick));
end;
nodes := document.querySelectorAll('.task-row-selectable');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@RowClick));
end;
ApplySelectedRowState;
end;
procedure TFTasksHTML.EditorKeyDown(Event: TJSEvent);
var
el: TJSHTMLElement;
fieldName: string;
newItemNum: Integer;
idx: Integer;
begin
el := TJSHTMLElement(Event.target);
fieldName := string(el.getAttribute('data-field'));
if not SameText(fieldName, 'itemNum') then
Exit;
if TJSKeyboardEvent(Event).key <> 'Enter' then
Exit;
Event.preventDefault;
idx := StrToIntDef(string(el.getAttribute('data-idx')), -1);
if idx < 0 then
Exit;
newItemNum := StrToIntDef(string(TJSObject(el)['value']), 0);
el.removeAttribute('data-unsaved-data');
MoveTaskRow(idx, newItemNum);
end;
......@@ -271,6 +328,81 @@ begin
end;
end;
[async] procedure TFTasksHTML.btnDeleteRowClick(Sender: TObject);
var
deletedItemNum: Integer;
begin
if FSelectedTaskItemId <= 0 then
Exit;
deletedItemNum := 0;
if xdwdsTasks.Active then
begin
xdwdsTasks.First;
while not xdwdsTasks.Eof do
begin
if xdwdsTaskstaskItemId.AsInteger = FSelectedTaskItemId then
begin
deletedItemNum := xdwdsTasksitemNum.AsInteger;
Break;
end;
xdwdsTasks.Next;
end;
end;
Utils.ShowSpinner('spinner');
try
if await(DeleteTaskRow) then
begin
FSelectedTaskItemId := 0;
FSelectedTaskId := 0;
btnDeleteRow.Enabled := False;
FPendingFocusItemNum := deletedItemNum;
FPendingFocusTaskField := 'application';
LoadTasks(FTaskId);
end;
finally
Utils.HideSpinner('spinner');
end;
end;
procedure TFTasksHTML.RowClick(Event: TJSEvent);
var
rowEl: TJSHTMLElement;
taskItemIdStr: string;
taskIdStr: string;
begin
rowEl := TJSHTMLElement(Event.currentTarget);
if not Assigned(rowEl) then
Exit;
taskItemIdStr := string(rowEl.getAttribute('data-task-item-id'));
taskIdStr := string(rowEl.getAttribute('data-task-id'));
FSelectedTaskItemId := StrToIntDef(taskItemIdStr, 0);
FSelectedTaskId := StrToIntDef(taskIdStr, 0);
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
ApplySelectedRowState;
end;
procedure TFTasksHTML.ApplySelectedRowState;
begin
asm
const selectedTaskItemId = this.FSelectedTaskItemId;
document.querySelectorAll('.task-row-selectable').forEach(function(row){
const rowTaskItemId = parseInt(row.getAttribute('data-task-item-id') || '0', 10);
if ((selectedTaskItemId > 0) && (rowTaskItemId === selectedTaskItemId))
row.classList.add('table-active');
else
row.classList.remove('table-active');
});
end;
end;
[async] function TFTasksHTML.AddTaskRow: Boolean;
var
......@@ -387,6 +519,7 @@ begin
xdwdsTasks.Open;
btnAddRow.Enabled := True;
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
RenderTable;
finally
......@@ -549,7 +682,7 @@ begin
html :=
'<div class="tasks-vscroll">' +
'<div class="tasks-hscroll">' +
'<table class="table table-sm table-bordered align-middle mb-0">' +
'<table class="table table-sm align-middle mb-0" style="min-width: 2000px;">' +
'<colgroup>' +
'<col style="width:40px">' +
'<col style="width:240px">' +
......@@ -582,7 +715,7 @@ begin
while not xdwdsTasks.Eof do
begin
html := html +
'<tr>' +
'<tr class="task-row-selectable" data-task-item-id="' + IntToStr(xdwdsTaskstaskItemId.AsInteger) + '" data-task-id="' + xdwdsTaskstaskId.AsString + '" data-item-num="' + IntToStr(xdwdsTasksitemNum.AsInteger) + '">' +
TdNowrap(ItemNumInput(xdwdsTasksitemNum.AsInteger, rowIdx)) +
TdNowrap(TextInput('application', xdwdsTasksapplication.AsString, rowIdx, 180)) +
TdNowrap(TextInput('version', xdwdsTasksversion.AsString, rowIdx, 80)) +
......@@ -606,6 +739,8 @@ begin
BindTableEditors;
EnableAutoGrowTextAreas;
EnableColumnResize;
ApplyPendingFocus;
ApplyPendingDeleteFocus;
end;
......@@ -688,6 +823,7 @@ end;
[async] procedure TFTasksHTML.MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
var
response: TXDataClientResponse;
movedTaskItemId: Integer;
begin
if not xdwdsTasks.Active then
Exit;
......@@ -696,16 +832,22 @@ begin
if xdwdsTasks.Eof then
Exit;
movedTaskItemId := xdwdsTaskstaskItemId.AsInteger;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.MoveTaskRow',
[
StrToIntDef(xdwdsTaskstaskId.AsString, 0),
xdwdsTaskstaskItemId.AsInteger,
movedTaskItemId,
newItemNum
]
));
console.log('MoveTaskRow: response=' + string(TJSJSON.stringify(response.Result)));
FPendingFocusTaskItemId := movedTaskItemId;
FPendingFocusField := 'application';
LoadTasks(FTaskId);
except
on E: EXDataClientRequestException do
......@@ -769,6 +911,31 @@ begin
end;
[async] function TFTasksHTML.DeleteTaskRow: Boolean;
var
response: TXDataClientResponse;
begin
Result := False;
if (FSelectedTaskId <= 0) or (FSelectedTaskItemId <= 0) then
Exit;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteTaskRow',
[FSelectedTaskId, FSelectedTaskItemId]
));
console.log('DeleteTaskRow response=' + string(TJSJSON.stringify(response.Result)));
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('DeleteTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
function TFTasksHTML.ExtractOptionNames(const SourceArray: TJSArray): TJSArray;
var
i: Integer;
......@@ -901,6 +1068,56 @@ begin
end;
procedure TFTasksHTML.ApplyPendingFocus;
var
el: TJSHTMLElement;
selector: string;
begin
if (FPendingFocusTaskItemId <= 0) or (FPendingFocusField = '') then
Exit;
selector :=
'[data-task-item-id="' + IntToStr(FPendingFocusTaskItemId) + '"] ' +
'[data-field="' + FPendingFocusField + '"]';
el := TJSHTMLElement(document.querySelector(selector));
if Assigned(el) then
begin
asm
el.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
el.focus();
end;
end;
FPendingFocusTaskItemId := 0;
FPendingFocusField := '';
end;
procedure TFTasksHTML.ApplyPendingDeleteFocus;
var
el: TJSHTMLElement;
selector: string;
begin
if (FPendingFocusItemNum <= 0) or (FPendingFocusTaskField = '') then
Exit;
selector :=
'tr[data-item-num="' + IntToStr(FPendingFocusItemNum) + '"] ' +
'[data-field="' + FPendingFocusTaskField + '"]';
el := TJSHTMLElement(document.querySelector(selector));
if Assigned(el) then
begin
asm
el.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
el.focus();
end;
end;
FPendingFocusItemNum := 0;
FPendingFocusTaskField := '';
end;
......
......@@ -39,7 +39,8 @@ is-invalid .form-check-input {
left: 50%;
transform: translate(-50%, -50%);
}
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
......@@ -51,4 +52,21 @@ input[data-field="itemNum"] {
appearance: textfield;
}
.tasks-vscroll {
max-height: calc(100vh - 260px);
overflow: auto;
}
.tasks-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
}
.tasks-vscroll thead th.th-resize {
z-index: 3;
}
......@@ -4,7 +4,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="data:;base64,=" rel="icon"/>
<title>EM Systems Template App</title>
<title>emT3Web</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.3.1/css/flag-icon.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
......
......@@ -563,4 +563,45 @@ object ApiDatabase: TApiDatabase
ReadOnly = True
end
end
object uqDeleteTaskRow: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'delete from task_items'
'where TASK_ITEM_ID = :TASK_ITEM_ID'
' and TASK_ID = :TASK_ID')
Left = 408
Top = 182
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ITEM_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
end
object uqShiftTaskRowsAfterDelete: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'update task_items'
'set ITEM_NUM = ITEM_NUM - 1'
'where TASK_ID = :TASK_ID'
' and ITEM_NUM > :OLD_ITEM_NUM')
Left = 408
Top = 130
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'OLD_ITEM_NUM'
Value = nil
end>
end
end
......@@ -64,6 +64,8 @@ type
uqGetTaskMaxItemNumMAX_ITEM_NUM: TIntegerField;
uqTaskItemsTASK_ITEM_ID: TIntegerField;
uqGetTaskRowPositionTASK_ITEM_ID: TIntegerField;
uqDeleteTaskRow: TUniQuery;
uqShiftTaskRowsAfterDelete: TUniQuery;
procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet);
private
......
......@@ -96,6 +96,7 @@ type
[HttpPost] function SaveTaskRow(Item: TTaskRowSave): Boolean;
function TestApi(messageText: string): TJSONObject;
procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
function DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
end;
implementation
......
......@@ -25,6 +25,7 @@ type
function TestApi(messageText: string): TJSONObject;
function GetWebClientVersion: string;
procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
function DeleteTaskRow(const taskId, taskItemId: Integer): Boolean;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
......@@ -393,6 +394,52 @@ begin
end;
function TApiService.DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
var
oldItemNum: Integer;
begin
Logger.Log(4, Format('ApiService.DeleteTaskRow - TASK_ID="%d" TASK_ITEM_ID="%d"', [taskId, taskItemId]));
apiDB.uqGetTaskRowPosition.Close;
apiDB.uqGetTaskRowPosition.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqGetTaskRowPosition.ParamByName('TASK_ITEM_ID').AsInteger := taskItemId;
apiDB.uqGetTaskRowPosition.Open;
if apiDB.uqGetTaskRowPosition.IsEmpty then
begin
Logger.Log(2, Format('ApiService.DeleteTaskRow - row not found TASK_ID="%d" TASK_ITEM_ID="%d"', [taskId, taskItemId]));
raise Exception.Create('Task row not found.');
end;
oldItemNum := apiDB.uqGetTaskRowPositionITEM_NUM.AsInteger;
apiDB.uqGetTaskRowPosition.Connection.StartTransaction;
try
apiDB.uqDeleteTaskRow.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqDeleteTaskRow.ParamByName('TASK_ITEM_ID').AsInteger := taskItemId;
apiDB.uqDeleteTaskRow.ExecSQL;
apiDB.uqShiftTaskRowsAfterDelete.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqShiftTaskRowsAfterDelete.ParamByName('OLD_ITEM_NUM').AsInteger := oldItemNum;
apiDB.uqShiftTaskRowsAfterDelete.ExecSQL;
apiDB.uqGetTaskRowPosition.Connection.Commit;
Result := True;
Logger.Log(4, Format('ApiService.DeleteTaskRow - OK TASK_ID="%d" TASK_ITEM_ID="%d" OLD_ITEM_NUM="%d"', [taskId, taskItemId, oldItemNum]));
except
on E: Exception do
begin
if apiDB.uqGetTaskRowPosition.Connection.InTransaction then
apiDB.uqGetTaskRowPosition.Connection.Rollback;
Logger.Log(2, Format('ApiService.DeleteTaskRow - ERROR TASK_ID="%d" TASK_ITEM_ID="%d" MSG="%s"', [taskId, taskItemId, E.Message]));
raise;
end;
end;
end;
function TApiService.TestApi(messageText: string): TJSONObject;
var
requiredVersion: string;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment