Commit a1b49c5d by Mac Stephens

Add rich text functionality to issue and notes, add hashed client password,…

Add rich text functionality to issue and notes, add hashed client password, remove params from url after manual login, update row focus, add insert row button
parent 7fc891cf
...@@ -4,7 +4,7 @@ interface ...@@ -4,7 +4,7 @@ interface
uses uses
SysUtils, Web, JS, SysUtils, Web, JS,
XData.Web.Client; XData.Web.Client, WEBLib.Crypto;
const const
TOKEN_NAME = 'EMT3_WEB_TOKEN'; TOKEN_NAME = 'EMT3_WEB_TOKEN';
...@@ -23,8 +23,8 @@ type ...@@ -23,8 +23,8 @@ type
public public
constructor Create; reintroduce; constructor Create; reintroduce;
destructor Destroy; override; destructor Destroy; override;
procedure Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError); procedure Login(AUserId, ATaskId, AUrlCode: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure WebLogin(AUser, ATaskId, APassword: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError); [async] procedure WebLogin(AUserName, ATaskId, APassword: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure Logout; procedure Logout;
function GetToken: string; function GetToken: string;
function Authenticated: Boolean; function Authenticated: Boolean;
...@@ -91,7 +91,7 @@ begin ...@@ -91,7 +91,7 @@ begin
Result := window.localStorage.getItem(TOKEN_NAME); Result := window.localStorage.getItem(TOKEN_NAME);
end; end;
procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess; procedure TAuthService.Login(AUserId, ATaskId, AUrlCode: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError); AError: TOnLoginError);
procedure OnLoad(Response: TXDataClientResponse); procedure OnLoad(Response: TXDataClientResponse);
...@@ -109,21 +109,24 @@ procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess: ...@@ -109,21 +109,24 @@ procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess:
end; end;
begin begin
if (AUser = '') or (APassword = '') then if (AUserId = '') or (ATaskId = '') or (AUrlCode = '') then
begin begin
AError('Invalid or expired code, please try again.'); AError('Invalid or expired code, please try again.');
Exit; Exit;
end; end;
FClient.RawInvoke( FClient.RawInvoke(
'IAuthService.Login', [AUser, APassword, AClientVersion], 'IAuthService.Login', [AUserId, ATaskId, AUrlCode],
@OnLoad, @OnError @OnLoad, @OnError
); );
end; end;
procedure TAuthService.WebLogin(AUser, ATaskId, APassword: string; ASuccess: TOnLoginSuccess; [async] procedure TAuthService.WebLogin(AUserName, ATaskId, APassword: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError); AError: TOnLoginError);
var
sha: TWebSHAHash;
passwordHash: string;
procedure OnLoad(Response: TXDataClientResponse); procedure OnLoad(Response: TXDataClientResponse);
var var
...@@ -140,14 +143,21 @@ procedure TAuthService.WebLogin(AUser, ATaskId, APassword: string; ASuccess: TOn ...@@ -140,14 +143,21 @@ procedure TAuthService.WebLogin(AUser, ATaskId, APassword: string; ASuccess: TOn
end; end;
begin begin
if (AUser = '') or (ATaskId = '') or (APassword = '') then if (AUserName = '') or (ATaskId = '') or (APassword = '') then
begin begin
AError('Please enter user id, task id, and password.'); AError('Please enter user id, task id, and password.');
Exit; Exit;
end; end;
sha := TWebSHAHash.Create(ehSHA256);
try
passwordHash := Await(string, sha.Hash(UpperCase(Trim(APassword))));
finally
sha.Free;
end;
FClient.RawInvoke( FClient.RawInvoke(
'IAuthService.WebLogin', [AUser, ATaskId, APassword], 'IAuthService.WebLogin', [AUserName, ATaskId, passwordHash],
@OnLoad, @OnError @OnLoad, @OnError
); );
end; end;
...@@ -156,6 +166,7 @@ end; ...@@ -156,6 +166,7 @@ end;
procedure TAuthService.Logout; procedure TAuthService.Logout;
begin begin
DeleteToken; DeleteToken;
DMConnection.currentTaskId := '';
end; end;
procedure TAuthService.SetToken(AToken: string); procedure TAuthService.SetToken(AToken: string);
......
...@@ -20,6 +20,7 @@ type ...@@ -20,6 +20,7 @@ type
FUnauthorizedAccessProc: TUnauthorizedAccessProc; FUnauthorizedAccessProc: TUnauthorizedAccessProc;
public public
currentTaskId: string;
const clientVersion = '0.8.8'; const clientVersion = '0.8.8';
procedure InitApp(SuccessProc: TSuccessProc; procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc); UnauthorizedAccessProc: TUnauthorizedAccessProc);
......
...@@ -49,18 +49,8 @@ uses ...@@ -49,18 +49,8 @@ uses
procedure TFViewLogin.btnLoginClick(Sender: TObject); procedure TFViewLogin.btnLoginClick(Sender: TObject);
procedure LoginSuccess; procedure LoginSuccess;
var
newUrl: string;
begin begin
newUrl := window.location.pathname + DMConnection.currentTaskId := edtTaskId.Text;
'?user_id=' + edtUsername.Text +
'&task_id=' + edtTaskId.Text +
'&url_code=000000';
asm
window.history.replaceState({}, '', newUrl);
end;
FLoginProc; FLoginProc;
end; end;
...@@ -97,10 +87,12 @@ class procedure TFViewLogin.Display(LoginProc: TSuccessProc; AUserId, ATaskId, A ...@@ -97,10 +87,12 @@ class procedure TFViewLogin.Display(LoginProc: TSuccessProc; AUserId, ATaskId, A
end; end;
begin begin
console.log('TFViewLogin.Display start AUserId=' + AUserId + ' ATaskId=' + ATaskId + ' AMsg=' + AMsg);
if Assigned(FViewLogin) then if Assigned(FViewLogin) then
FViewLogin.Free; FViewLogin.Free;
FViewLogin := TFViewLogin.CreateNew(@FormCreate); FViewLogin := TFViewLogin.CreateNew(@FormCreate);
FViewLogin.FLoginProc := LoginProc; FViewLogin.FLoginProc := LoginProc;
console.log('TFViewLogin.Display end');
end; end;
procedure TFViewLogin.HideNotification; procedure TFViewLogin.HideNotification;
...@@ -117,15 +109,15 @@ begin ...@@ -117,15 +109,15 @@ begin
end; end;
end; end;
procedure TFViewLogin.btnCloseNotificationClick(Sender: TObject);
begin
HideNotification;
end;
procedure TFViewLogin.WebFormCreate(Sender: TObject); procedure TFViewLogin.WebFormCreate(Sender: TObject);
begin begin
edtUsername.Text := FUserId; console.log('TFViewLogin.WebFormCreate FUserId=' + FUserId + ' FTaskId=' + FTaskId + ' FMessage=' + FMessage);
edtTaskId.Text := FTaskId; console.log('TFViewLogin.WebFormCreate URL task_id=' + Application.Parameters.Values['task_id']);
edtUsername.Text := '';
edtTaskId.Text := Application.Parameters.Values['task_id'];
console.log('TFViewLogin.WebFormCreate after assign edtUsername=' + edtUsername.Text + ' edtTaskId=' + edtTaskId.Text);
if FMessage <> '' then if FMessage <> '' then
ShowNotification(FMessage) ShowNotification(FMessage)
...@@ -133,4 +125,9 @@ begin ...@@ -133,4 +125,9 @@ begin
HideNotification; HideNotification;
end; end;
procedure TFViewLogin.btnCloseNotificationClick(Sender: TObject);
begin
HideNotification;
end;
end. end.
...@@ -58,6 +58,62 @@ object FTaskItems: TFTaskItems ...@@ -58,6 +58,62 @@ object FTaskItems: TFTaskItems
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnClick = btnInsertRowClick OnClick = btnInsertRowClick
end end
object btnTextColor: TWebButton
Left = 282
Top = 197
Width = 96
Height = 25
Caption = 'Text Color'
ChildOrder = 4
ElementID = 'btn_text_color'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnTextColorClick
end
object btnFmtBold: TWebButton
Left = 282
Top = 228
Width = 96
Height = 25
Caption = 'Bold'
ChildOrder = 4
ElementID = 'btn_fmt_bold'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFmtBoldClick
end
object btnFmtItalic: TWebButton
Left = 282
Top = 259
Width = 96
Height = 25
Caption = 'Italic'
ChildOrder = 4
ElementID = 'btn_fmt_italic'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFmtItalicClick
end
object btnFmtUnderline: TWebButton
Left = 282
Top = 290
Width = 96
Height = 25
Caption = 'Underline'
ChildOrder = 4
ElementID = 'btn_fmt_underline'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFmtUnderlineClick
end
object xdwcTasks: TXDataWebClient object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 506 Left = 506
......
<div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden"> <div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0"> <div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0 gap-3">
<h5 class="mb-0" id="lbl_project_name"></h5> <h5 class="mb-0" id="lbl_project_name"></h5>
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-2 ms-auto flex-nowrap">
<div id="lbl_total_rows"></div> <div id="lbl_total_rows" class="me-2 text-nowrap"></div>
<div class="task-toolbar d-flex align-items-center gap-1 px-2 py-1">
<button id="btn_fmt_bold" type="button" class="btn btn-sm btn-light task-toolbar-btn" title="Bold">
<i class="fas fa-bold"></i>
</button>
<button id="btn_fmt_italic" type="button" class="btn btn-sm btn-light task-toolbar-btn" title="Italic">
<i class="fas fa-italic"></i>
</button>
<button id="btn_fmt_underline" type="button" class="btn btn-sm btn-light task-toolbar-btn" title="Underline">
<i class="fas fa-underline"></i>
</button>
<div class="vr mx-1"></div>
<button id="btn_text_color" type="button" class="btn btn-sm btn-light task-toolbar-btn task-color-btn" title="Text color">
<span class="task-color-glyph">A</span>
<span id="text_color_swatch" class="task-color-swatch"></span>
<input id="issue_notes_text_color" type="color" class="task-hidden-color-input" value="#000000">
</button>
</div>
<div class="d-flex gap-2"> <button id="btn_add_row" class="btn btn-sm btn-success text-nowrap">Add Row</button>
<button id="btn_add_row" class="btn btn-sm btn-success">Add Row</button> <button id="btn_insert_row" class="btn btn-sm btn-success text-nowrap">Insert Row</button>
<button id="btn_delete_row" class="btn btn-sm btn-danger">Delete Row</button> <button id="btn_delete_row" class="btn btn-sm btn-danger text-nowrap">Delete Row</button>
<button id="btn_insert_row" class="btn btn-sm btn-success">Insert Row</button> <button id="btn_reload" class="btn btn-sm btn-primary text-nowrap">Reload</button>
<button id="btn_reload" class="btn btn-sm btn-primary">Reload</button>
</div>
</div> </div>
</div> </div>
......
...@@ -31,11 +31,19 @@ type ...@@ -31,11 +31,19 @@ type
xdwdsTaskstaskItemId: TIntegerField; xdwdsTaskstaskItemId: TIntegerField;
btnDeleteRow: TWebButton; btnDeleteRow: TWebButton;
btnInsertRow: TWebButton; btnInsertRow: TWebButton;
btnTextColor: TWebButton;
btnFmtBold: TWebButton;
btnFmtItalic: TWebButton;
btnFmtUnderline: TWebButton;
[async] procedure btnAddRowClick(Sender: TObject); [async] procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject); procedure btnReloadClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
[async] procedure btnDeleteRowClick(Sender: TObject); [async] procedure btnDeleteRowClick(Sender: TObject);
[async] procedure btnInsertRowClick(Sender: TObject); [async] procedure btnInsertRowClick(Sender: TObject);
procedure btnTextColorClick(Sender: TObject);
procedure btnFmtBoldClick(Sender: TObject);
procedure btnFmtItalicClick(Sender: TObject);
procedure btnFmtUnderlineClick(Sender: TObject);
private private
FTaskId: string; FTaskId: string;
FApplicationOptions: TJSArray; FApplicationOptions: TJSArray;
...@@ -50,6 +58,8 @@ type ...@@ -50,6 +58,8 @@ type
FPendingScrollLeft: Integer; FPendingScrollLeft: Integer;
FPendingFocusItemNum: Integer; FPendingFocusItemNum: Integer;
FPendingFocusTaskField: string; FPendingFocusTaskField: string;
FActiveRichEditorId: string;
FLastSelectionRange: JSValue;
FNameManager: TNameManager; FNameManager: TNameManager;
[async] procedure LoadTasks(const ATaskId: string); [async] procedure LoadTasks(const ATaskId: string);
procedure RenderTable; procedure RenderTable;
...@@ -90,6 +100,12 @@ type ...@@ -90,6 +100,12 @@ type
[async] procedure HandleAddManagedName(const AFieldName: string; const ARowIndex: Integer; const ANewName: string); [async] procedure HandleAddManagedName(const AFieldName: string; const ARowIndex: Integer; const ANewName: string);
[async] procedure HandleRenameManagedName(const AFieldName, AOldName, ANewName: string); [async] procedure HandleRenameManagedName(const AFieldName, AOldName, ANewName: string);
[async] procedure HandleDeleteManagedName(const AFieldName, AName: string); [async] procedure HandleDeleteManagedName(const AFieldName, AName: string);
procedure ColorPickerInput(Event: TJSEvent);
procedure RichEditorFocus(Event: TJSEvent);
procedure RichEditorSelectionChange(Event: TJSEvent);
procedure ApplyRichTextCommand(const commandName: string; const commandValue: string = '');
procedure UpdateToolbarState;
procedure SetToolbarButtonActive(const buttonId: string; const isActive: Boolean);
public public
end; end;
...@@ -107,7 +123,11 @@ uses ...@@ -107,7 +123,11 @@ uses
procedure TFTaskItems.WebFormCreate(Sender: TObject); procedure TFTaskItems.WebFormCreate(Sender: TObject);
begin begin
console.log('TFTaskItems.WebFormCreate fired'); console.log('TFTaskItems.WebFormCreate fired');
FTaskId := Application.Parameters.Values['task_id']; FTaskId := Application.Parameters.Values['task_id'];
if FTaskId = '' then
FTaskId := DMConnection.currentTaskId;
FApplicationOptions := TJSArray.new; FApplicationOptions := TJSArray.new;
FReportedByOptions := TJSArray.new; FReportedByOptions := TJSArray.new;
FAssignedToOptions := TJSArray.new; FAssignedToOptions := TJSArray.new;
...@@ -214,7 +234,10 @@ begin ...@@ -214,7 +234,10 @@ begin
if xdwdsTasks.Eof then if xdwdsTasks.Eof then
Exit; Exit;
newVal := string(TJSObject(el)['value']); if SameText(fieldName, 'issue') or SameText(fieldName, 'notes') then
newVal := string(el.innerHTML)
else
newVal := string(TJSObject(el)['value']);
console.log('EditorInput: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + newVal); console.log('EditorInput: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + newVal);
...@@ -280,6 +303,13 @@ begin ...@@ -280,6 +303,13 @@ begin
el.addEventListener('input', TJSEventHandler(@EditorInput)); el.addEventListener('input', TJSEventHandler(@EditorInput));
el.addEventListener('blur', TJSEventHandler(@EditorBlur)); el.addEventListener('blur', TJSEventHandler(@EditorBlur));
el.addEventListener('keydown', TJSEventHandler(@EditorKeyDown)); el.addEventListener('keydown', TJSEventHandler(@EditorKeyDown));
if el.classList.contains('task-rich-editor') then
begin
el.addEventListener('focus', TJSEventHandler(@RichEditorFocus));
el.addEventListener('mouseup', TJSEventHandler(@RichEditorSelectionChange));
el.addEventListener('keyup', TJSEventHandler(@RichEditorSelectionChange));
end;
end; end;
nodes := document.querySelectorAll('.task-select'); nodes := document.querySelectorAll('.task-select');
...@@ -311,6 +341,10 @@ begin ...@@ -311,6 +341,10 @@ begin
el.addEventListener('click', TJSEventHandler(@RowClick)); el.addEventListener('click', TJSEventHandler(@RowClick));
end; end;
el := TJSHTMLElement(document.getElementById('issue_notes_text_color'));
if Assigned(el) then
el.addEventListener('input', TJSEventHandler(@ColorPickerInput));
ApplySelectedRowState; ApplySelectedRowState;
end; end;
...@@ -402,6 +436,21 @@ begin ...@@ -402,6 +436,21 @@ begin
end; end;
end; end;
procedure TFTaskItems.btnFmtBoldClick(Sender: TObject);
begin
ApplyRichTextCommand('bold');
end;
procedure TFTaskItems.btnFmtItalicClick(Sender: TObject);
begin
ApplyRichTextCommand('italic');
end;
procedure TFTaskItems.btnFmtUnderlineClick(Sender: TObject);
begin
ApplyRichTextCommand('underline');
end;
[async] procedure TFTaskItems.btnInsertRowClick(Sender: TObject); [async] procedure TFTaskItems.btnInsertRowClick(Sender: TObject);
begin begin
Utils.ShowSpinner('spinner'); Utils.ShowSpinner('spinner');
...@@ -555,16 +604,29 @@ begin ...@@ -555,16 +604,29 @@ begin
end; end;
procedure TFTaskItems.btnTextColorClick(Sender: TObject);
var
colorEl: TJSHTMLElement;
begin
colorEl := TJSHTMLElement(document.getElementById('issue_notes_text_color'));
colorEl.click;
end;
procedure TFTaskItems.EnableAutoGrowTextAreas; procedure TFTaskItems.EnableAutoGrowTextAreas;
begin begin
asm asm
(function(){ (function(){
const host = document.getElementById('tasks_table_host'); const host = document.getElementById('tasks_table_host');
if(!host) return; if(!host) return;
host.querySelectorAll('textarea.cell-textarea').forEach(ta => {
const fit = () => { ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px'; }; host.querySelectorAll('.task-rich-editor').forEach(function(editor){
const fit = function() {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
};
fit(); fit();
ta.addEventListener('input', fit); editor.addEventListener('input', fit);
}); });
})(); })();
end; end;
...@@ -693,15 +755,6 @@ var ...@@ -693,15 +755,6 @@ var
'value="' + HtmlEncode(Value) + '"' + w + '>'; 'value="' + HtmlEncode(Value) + '"' + w + '>';
end; end;
function TextArea(const FieldName, Value: string; const AIdx: Integer): string;
begin
Result :=
'<textarea class="form-control form-control-sm cell-textarea task-editor w-100" ' +
'style="height:31px; min-height:31px; overflow:hidden; resize:none;" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'rows="1">' + HtmlEncode(Value) + '</textarea>';
end;
function ItemNumInput(const Value: Integer; const AIdx: Integer): string; function ItemNumInput(const Value: Integer; const AIdx: Integer): string;
begin begin
Result := Result :=
...@@ -786,6 +839,21 @@ var ...@@ -786,6 +839,21 @@ var
'</div>'; '</div>';
end; end;
function RichTextEditor(const FieldName, Value: string; const AIdx: Integer): string;
var
editorId: string;
begin
editorId := 'rtf_' + FieldName + '_' + IntToStr(AIdx);
Result :=
'<div id="' + editorId + '" ' +
'class="form-control form-control-sm task-rich-editor task-editor w-100" ' +
'contenteditable="true" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'style="min-height:31px; overflow:hidden;">' + Value + '</div>';
end;
begin begin
host := TJSHTMLElement(document.getElementById('tasks_table_host')); host := TJSHTMLElement(document.getElementById('tasks_table_host'));
if not Assigned(host) then if not Assigned(host) then
...@@ -837,8 +905,8 @@ begin ...@@ -837,8 +905,8 @@ begin
TdNowrap(SelectList('status', xdwdsTasksstatus.AsString, rowIdx, FStatusOptions, False)) + TdNowrap(SelectList('status', xdwdsTasksstatus.AsString, rowIdx, FStatusOptions, False)) +
TdNowrap(DateInput('statusDate', xdwdsTasksstatusDate.AsString, rowIdx)) + TdNowrap(DateInput('statusDate', xdwdsTasksstatusDate.AsString, rowIdx)) +
TdNowrap(TextInput('formSection', xdwdsTasksformSection.AsString, rowIdx)) + TdNowrap(TextInput('formSection', xdwdsTasksformSection.AsString, rowIdx)) +
TdWrap(TextArea('issue', xdwdsTasksissue.AsString, rowIdx)) + TdWrap(RichTextEditor('issue', xdwdsTasksissue.AsString, rowIdx)) +
TdWrap(TextArea('notes', xdwdsTasksnotes.AsString, rowIdx)) + TdWrap(RichTextEditor('notes', xdwdsTasksnotes.AsString, rowIdx)) +
'</tr>'; '</tr>';
xdwdsTasks.Next; xdwdsTasks.Next;
...@@ -1580,6 +1648,118 @@ begin ...@@ -1580,6 +1648,118 @@ begin
end; end;
procedure TFTaskItems.ColorPickerInput(Event: TJSEvent);
var
pickerEl: TJSHTMLElement;
colorValue: string;
swatchEl: TJSHTMLElement;
begin
pickerEl := TJSHTMLElement(Event.target);
colorValue := string(TJSObject(pickerEl)['value']);
swatchEl := TJSHTMLElement(document.getElementById('text_color_swatch'));
swatchEl.style.setProperty('background', colorValue);
ApplyRichTextCommand('foreColor', colorValue);
end;
procedure TFTaskItems.RichEditorFocus(Event: TJSEvent);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(Event.target);
FActiveRichEditorId := string(el.id);
UpdateToolbarState;
end;
procedure TFTaskItems.RichEditorSelectionChange(Event: TJSEvent);
begin
asm
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return;
const range = sel.getRangeAt(0);
const editor = document.getElementById(this.FActiveRichEditorId);
if (!editor) return;
const container = range.commonAncestorContainer;
if (editor.contains(container.nodeType === 1 ? container : container.parentNode)) {
this.FLastSelectionRange = range.cloneRange();
}
end;
UpdateToolbarState;
end;
procedure TFTaskItems.ApplyRichTextCommand(const commandName: string; const commandValue: string = '');
var
editorEl: TJSHTMLElement;
begin
if not xdwdsTasks.Active then
Exit;
if FActiveRichEditorId = '' then
Exit;
editorEl := TJSHTMLElement(document.getElementById(FActiveRichEditorId));
if not Assigned(editorEl) then
Exit;
asm
const sel = window.getSelection();
if (!sel) return;
editorEl.focus();
if (this.FLastSelectionRange) {
sel.removeAllRanges();
sel.addRange(this.FLastSelectionRange);
}
if (commandName === 'foreColor')
document.execCommand('styleWithCSS', false, true)
else
document.execCommand('styleWithCSS', false, false);
if (commandValue)
document.execCommand(commandName, false, commandValue);
else
document.execCommand(commandName, false, null);
if (sel.rangeCount > 0) {
this.FLastSelectionRange = sel.getRangeAt(0).cloneRange();
}
editorEl.setAttribute('data-unsaved-data', '1');
editorEl.dispatchEvent(new Event('input', { bubbles: true }));
end;
UpdateToolbarState;
end;
procedure TFTaskItems.SetToolbarButtonActive(const buttonId: string; const isActive: Boolean);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById(buttonId));
if isActive then
el.classList.add('active')
else
el.classList.remove('active');
el.setAttribute('aria-pressed', BoolToStr(isActive, True).ToLower);
end;
procedure TFTaskItems.UpdateToolbarState;
begin
asm
this.SetToolbarButtonActive('btn_fmt_bold', document.queryCommandState('bold'));
this.SetToolbarButtonActive('btn_fmt_italic', document.queryCommandState('italic'));
this.SetToolbarButtonActive('btn_fmt_underline', document.queryCommandState('underline'));
end;
end;
end. end.
......
...@@ -118,6 +118,65 @@ span.card { ...@@ -118,6 +118,65 @@ span.card {
z-index: 1055; z-index: 1055;
} }
.task-toolbar {
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
background: var(--bs-body-bg);
white-space: nowrap;
}
.task-toolbar-btn {
min-width: 2rem;
padding: 0.25rem 0.5rem;
}
#lbl_total_rows {
white-space: nowrap;
}
.task-rich-editor {
white-space: pre-wrap;
overflow-wrap: anywhere;
height: auto;
}
.task-rich-editor:focus {
outline: 0;
}
.task-color-btn {
position: relative;
min-width: 2rem;
height: 2rem;
padding: 0.2rem 0.45rem;
}
.task-hidden-color-input {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
width: 1px;
height: 1px;
border: 0;
}
.task-color-glyph {
display: inline-block;
font-weight: 600;
line-height: 1;
}
.task-color-swatch {
position: absolute;
left: 0.35rem;
right: 0.35rem;
bottom: 0.2rem;
height: 0.18rem;
border-radius: 999px;
background: #000000;
}
.task-dd-toggle.status-cannot-duplicate { .task-dd-toggle.status-cannot-duplicate {
--bs-btn-color: #41464b; --bs-btn-color: #41464b;
......
...@@ -82,13 +82,16 @@ begin ...@@ -82,13 +82,16 @@ begin
codeParam := Application.Parameters.Values['url_code']; codeParam := Application.Parameters.Values['url_code'];
if (userIdParam = '') or (taskIdParam = '') or (codeParam = '') then if (userIdParam = '') or (taskIdParam = '') or (codeParam = '') then
DisplayLoginView(userIdParam, taskIdParam) begin
DisplayLoginView(userIdParam, taskIdParam);
end
else else
begin begin
AuthService.Logout; AuthService.Logout;
DMConnection.ApiConnection.Connected := False; DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then if Assigned(FViewMain) then
FViewMain.Free; FViewMain.Free;
DMConnection.currentTaskId := taskIdParam;
Login(userIdParam, taskIdParam, codeParam); Login(userIdParam, taskIdParam, codeParam);
end; end;
end; end;
......
...@@ -99,9 +99,10 @@ ...@@ -99,9 +99,10 @@
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer> <VerInfo_MinorVer>8</VerInfo_MinorVer>
<VerInfo_Release>8</VerInfo_Release> <VerInfo_Release>8</VerInfo_Release>
<TMSWebBrowser>5</TMSWebBrowser> <TMSURLParams>?user_id=1019&amp;task_id=4045&amp;url_code=123456</TMSURLParams>
<TMSUseJSDebugger>2</TMSUseJSDebugger> <TMSWebBrowser>1</TMSWebBrowser>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath> <TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''"> <PropertyGroup Condition="'$(Cfg_2)'!=''">
......
...@@ -104,7 +104,6 @@ object AuthDatabase: TAuthDatabase ...@@ -104,7 +104,6 @@ object AuthDatabase: TAuthDatabase
Database = 'eTask' Database = 'eTask'
Username = 'root' Username = 'root'
Server = '192.168.102.131' Server = '192.168.102.131'
Connected = True
LoginPrompt = False LoginPrompt = False
Left = 71 Left = 71
Top = 133 Top = 133
...@@ -128,12 +127,12 @@ object AuthDatabase: TAuthDatabase ...@@ -128,12 +127,12 @@ object AuthDatabase: TAuthDatabase
' u.PERSPECTIVE_ID,' ' u.PERSPECTIVE_ID,'
' u.LAST_NAME,' ' u.LAST_NAME,'
' u.FIRST_NAME,' ' u.FIRST_NAME,'
' u.PASSWORD,'
' w.WEB_LOGIN' ' w.WEB_LOGIN'
'from web_tasks_url w' 'from web_tasks_url w'
'join users u on u.USER_ID = w.USER_ID' 'join users u on u.USER_ID = w.USER_ID'
'where u.USER_NAME = :USER_NAME' 'where u.USER_NAME = :USER_NAME'
' and w.TASK_ID = :TASK_ID' ' and w.TASK_ID = :TASK_ID'
' and u.PASSWORD = :PASSWORD'
' and w.URL_CODE = :URL_CODE') ' and w.URL_CODE = :URL_CODE')
Left = 194 Left = 194
Top = 44 Top = 44
...@@ -150,11 +149,6 @@ object AuthDatabase: TAuthDatabase ...@@ -150,11 +149,6 @@ object AuthDatabase: TAuthDatabase
end end
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'PASSWORD'
Value = nil
end
item
DataType = ftUnknown
Name = 'URL_CODE' Name = 'URL_CODE'
Value = nil Value = nil
end> end>
...@@ -198,6 +192,10 @@ object AuthDatabase: TAuthDatabase ...@@ -198,6 +192,10 @@ object AuthDatabase: TAuthDatabase
FieldName = 'FIRST_NAME' FieldName = 'FIRST_NAME'
Size = 25 Size = 25
end end
object uqWebLoginPASSWORD: TStringField
FieldName = 'PASSWORD'
Size = 12
end
object uqWebLoginWEB_LOGIN: TStringField object uqWebLoginWEB_LOGIN: TStringField
FieldName = 'WEB_LOGIN' FieldName = 'WEB_LOGIN'
ReadOnly = True ReadOnly = True
......
...@@ -36,6 +36,7 @@ type ...@@ -36,6 +36,7 @@ type
uqWebLoginPERSPECTIVE_ID: TStringField; uqWebLoginPERSPECTIVE_ID: TStringField;
uqWebLoginLAST_NAME: TStringField; uqWebLoginLAST_NAME: TStringField;
uqWebLoginFIRST_NAME: TStringField; uqWebLoginFIRST_NAME: TStringField;
uqWebLoginPASSWORD: TStringField;
uqWebLoginWEB_LOGIN: TStringField; uqWebLoginWEB_LOGIN: TStringField;
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure DataModuleDestroy(Sender: TObject); procedure DataModuleDestroy(Sender: TObject);
......
...@@ -31,6 +31,7 @@ type ...@@ -31,6 +31,7 @@ type
function CheckUrlLogin(userId, taskId, urlCode: string): Integer; function CheckUrlLogin(userId, taskId, urlCode: string): Integer;
function CheckUserLogin(userName, taskId, password: string): Integer; function CheckUserLogin(userName, taskId, password: string): Integer;
function HashPassword(const APassword: string): string;
procedure LoadUserFromUrlLoginQuery; procedure LoadUserFromUrlLoginQuery;
procedure LoadUserFromWebLoginQuery; procedure LoadUserFromWebLoginQuery;
public public
...@@ -298,16 +299,22 @@ begin ...@@ -298,16 +299,22 @@ begin
end; end;
function TAuthService.HashPassword(const APassword: string): string;
begin
Result := LowerCase(THashSHA2.GetHashString(UpperCase(Trim(APassword))));
end;
function TAuthService.CheckUserLogin(userName, taskId, password: string): Integer; function TAuthService.CheckUserLogin(userName, taskId, password: string): Integer;
var var
webLogin: string; webLogin: string;
storedPasswordHash: string;
begin begin
Logger.Log(3, 'TAuthService.CheckUserLogin(const userId, taskId, password: string): Integer'); Logger.Log(3, 'TAuthService.CheckUserLogin(userName, taskId, password: string): Integer');
authDB.uqWebLogin.Close; authDB.uqWebLogin.Close;
authDB.uqWebLogin.ParamByName('USER_NAME').AsString := userName; authDB.uqWebLogin.ParamByName('USER_NAME').AsString := userName;
authDB.uqWebLogin.ParamByName('TASK_ID').AsString := taskId; authDB.uqWebLogin.ParamByName('TASK_ID').AsString := taskId;
authDB.uqWebLogin.ParamByName('PASSWORD').AsString := password;
authDB.uqWebLogin.ParamByName('URL_CODE').AsString := '000000'; authDB.uqWebLogin.ParamByName('URL_CODE').AsString := '000000';
authDB.uqWebLogin.Open; authDB.uqWebLogin.Open;
...@@ -318,6 +325,14 @@ begin ...@@ -318,6 +325,14 @@ begin
Exit; Exit;
end; end;
storedPasswordHash := HashPassword(authDB.uqWebLogin.FieldByName('PASSWORD').AsString);
if storedPasswordHash <> LowerCase(Trim(password)) then
begin
Logger.Log(3, '--Web Login failed 0: password hash mismatch');
Result := 0;
Exit;
end;
if authDB.uqWebLoginSTATUS.AsString <> 'ACTIVE' then if authDB.uqWebLoginSTATUS.AsString <> 'ACTIVE' then
begin begin
Logger.Log(3, '--Web Login failed 1: authDB.uqWebLoginSTATUS.AsString <> ACTIVE'); Logger.Log(3, '--Web Login failed 1: authDB.uqWebLoginSTATUS.AsString <> ACTIVE');
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
MemoLogLevel=4 MemoLogLevel=4
FileLogLevel=4 FileLogLevel=4
webClientVersion=0.8.8 webClientVersion=0.8.8
LogFileNum=174 LogFileNum=187
[Database] [Database]
Server=192.168.102.131 Server=192.168.102.131
......
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