Commit 13ffaf77 by Mac Stephens

Update to version 0.9.4.9 - Add device type validation and platform-specific…

Update to version 0.9.4.9 - Add device type validation and platform-specific link handling; standardize archive details styling
parent f1cc5bac
...@@ -37,7 +37,8 @@ type ...@@ -37,7 +37,8 @@ type
[HttpGet] function GetDeviceList: TDeviceList; [HttpGet] function GetDeviceList: TDeviceList;
function RevokeDevice(const CredentialId: string): TJSONObject; function RevokeDevice(const CredentialId: string): TJSONObject;
function UnrevokeDevice(const CredentialId: string): TJSONObject; function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber: string): TJSONObject; function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject; function DeletePendingDevice(const PhoneNumber: string): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject; function DeleteDevice(CredentialId: string): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject; function SendAppLink(const PhoneNumber: string): TJSONObject;
......
...@@ -43,7 +43,8 @@ type ...@@ -43,7 +43,8 @@ type
function GetDeviceList: TDeviceList; function GetDeviceList: TDeviceList;
function RevokeDevice(const CredentialId: string): TJSONObject; function RevokeDevice(const CredentialId: string): TJSONObject;
function UnrevokeDevice(const CredentialId: string): TJSONObject; function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber: string): TJSONObject; function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject; function DeletePendingDevice(const PhoneNumber: string): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject; function DeleteDevice(CredentialId: string): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject; function SendAppLink(const PhoneNumber: string): TJSONObject;
...@@ -1374,7 +1375,7 @@ begin ...@@ -1374,7 +1375,7 @@ begin
try try
q.Connection := conn; q.Connection := conn;
q.SQL.Text := q.SQL.Text :=
'SELECT id, credential_id, device_name, phone_number, username, ' + 'SELECT id, credential_id, device_name, device_type, phone_number, username, ' +
' user_agent, registered_at, revoked_at, revoked_by, status, ' + ' user_agent, registered_at, revoked_at, revoked_by, status, ' +
' verification_code, ' + ' verification_code, ' +
' COALESCE(status = ''pending'' AND verification_expires_at > NOW(), FALSE) ' + ' COALESCE(status = ''pending'' AND verification_expires_at > NOW(), FALSE) ' +
...@@ -1391,6 +1392,7 @@ begin ...@@ -1391,6 +1392,7 @@ begin
item.id := q.FieldByName('id').AsInteger; item.id := q.FieldByName('id').AsInteger;
item.credential_id := q.FieldByName('credential_id').AsString; item.credential_id := q.FieldByName('credential_id').AsString;
item.device_name := q.FieldByName('device_name').AsString; item.device_name := q.FieldByName('device_name').AsString;
item.device_type := q.FieldByName('device_type').AsString;
item.phone_number := q.FieldByName('phone_number').AsString; item.phone_number := q.FieldByName('phone_number').AsString;
item.username := q.FieldByName('username').AsString; item.username := q.FieldByName('username').AsString;
item.user_agent := q.FieldByName('user_agent').AsString; item.user_agent := q.FieldByName('user_agent').AsString;
...@@ -1577,11 +1579,12 @@ begin ...@@ -1577,11 +1579,12 @@ begin
Result := '+1' + digits; Result := '+1' + digits;
end; end;
function TApiService.AddPendingDevice(const DeviceName, PhoneNumber: string): TJSONObject; function TApiService.AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject;
var var
conn: TUniConnection; conn: TUniConnection;
q: TUniQuery; q: TUniQuery;
normalizedPhone: string; normalizedPhone, normalizedDeviceType: string;
begin begin
RequireAdmin; RequireAdmin;
...@@ -1595,6 +1598,17 @@ begin ...@@ -1595,6 +1598,17 @@ begin
Exit; Exit;
end; end;
normalizedDeviceType := LowerCase(Trim(DeviceType));
if (normalizedDeviceType <> 'iphone') and
(normalizedDeviceType <> 'android') and
(normalizedDeviceType <> 'browser') and
(normalizedDeviceType <> 'other') then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Please select a valid device type.');
Exit;
end;
try try
normalizedPhone := NormalizePhoneE164Api(PhoneNumber); normalizedPhone := NormalizePhoneE164Api(PhoneNumber);
except except
...@@ -1628,9 +1642,11 @@ begin ...@@ -1628,9 +1642,11 @@ begin
q.Close; q.Close;
q.SQL.Text := q.SQL.Text :=
'INSERT INTO lems.device_registrations (device_name, phone_number, status) ' + 'INSERT INTO lems.device_registrations ' +
'VALUES (:NAME, :PHONE, ''pending'')'; '(device_name, device_type, phone_number, status) ' +
'VALUES (:NAME, :TYPE, :PHONE, ''pending'')';
q.ParamByName('NAME').AsString := Trim(DeviceName); q.ParamByName('NAME').AsString := Trim(DeviceName);
q.ParamByName('TYPE').AsString := normalizedDeviceType;
q.ParamByName('PHONE').AsString := normalizedPhone; q.ParamByName('PHONE').AsString := normalizedPhone;
q.ExecSQL; q.ExecSQL;
......
...@@ -45,6 +45,7 @@ type ...@@ -45,6 +45,7 @@ type
id: Integer; id: Integer;
credential_id: string; credential_id: string;
device_name: string; device_name: string;
device_type: string;
phone_number: string; phone_number: string;
username: string; username: string;
agency: string; agency: string;
......
[Settings] [Settings]
LogFileNum=148 LogFileNum=154
webClientVersion=0.9.4.8 webClientVersion=0.9.4.9
MemoLogLevel=3 MemoLogLevel=3
FileLogLevel=4 FileLogLevel=4
......
...@@ -108,9 +108,9 @@ ...@@ -108,9 +108,9 @@
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>9</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>4</VerInfo_Release> <VerInfo_Release>4</VerInfo_Release>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.7;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.2.0;Comments=</VerInfo_Keys> <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.9;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.2.0;Comments=</VerInfo_Keys>
<DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> <DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<VerInfo_Build>7</VerInfo_Build> <VerInfo_Build>9</VerInfo_Build>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''"> <PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
......
...@@ -21,7 +21,7 @@ type ...@@ -21,7 +21,7 @@ type
procedure VerifyVersion(SuccessProc: TSuccessProc); procedure VerifyVersion(SuccessProc: TSuccessProc);
public public
property WsUrl: string read FWsUrl; property WsUrl: string read FWsUrl;
const clientVersion = '0.9.4.8'; const clientVersion = '0.9.4.9';
procedure InitApp(SuccessProc: TSuccessProc; procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc); UnauthorizedAccessProc: TUnauthorizedAccessProc);
end; end;
......
...@@ -13,7 +13,7 @@ object FViewComplaintArchive: TFViewComplaintArchive ...@@ -13,7 +13,7 @@ object FViewComplaintArchive: TFViewComplaintArchive
ElementHeaderClassName = 'table-light' ElementHeaderClassName = 'table-light'
ElementTableClassName = ElementTableClassName =
'table table-sm table-striped table-hover table-bordered mb-0 ali' + 'table table-sm table-striped table-hover table-bordered mb-0 ali' +
'gn-middle' 'gn-middle small'
Footer.ButtonActiveElementClassName = 'btn btn-primary' Footer.ButtonActiveElementClassName = 'btn btn-primary'
Footer.ButtonElementClassName = 'btn btn-light' Footer.ButtonElementClassName = 'btn btn-light'
Footer.DropDownElementClassName = 'form-control' Footer.DropDownElementClassName = 'form-control'
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
<div id="cdetails_summary_arc" class="collapse show"> <div id="cdetails_summary_arc" class="collapse show">
<div class="card border-0 shadow-sm mb-2"> <div class="card border-0 shadow-sm mb-2">
<div class="card-body py-2"> <div class="card-body py-2">
<table class="table table-sm mb-0 w-100"> <table class="table table-sm mb-0 w-100 small">
<tbody> <tbody>
<tr> <tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Priority:</th> <th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Priority:</th>
......
...@@ -26,18 +26,36 @@ ...@@ -26,18 +26,36 @@
<!-- Add pending device --> <!-- Add pending device -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="d-flex align-items-center gap-2 flex-wrap"> <div class="d-flex align-items-start gap-2 flex-wrap">
<span class="fw-semibold text-nowrap small">Add Device:</span> <span class="fw-semibold text-nowrap small mt-2">Add Device:</span>
<input type="text" <div style="width:180px;">
id="view.devmgr.newname" <input type="text"
class="form-control form-control-sm" id="view.devmgr.newname"
placeholder="Device name" class="form-control form-control-sm"
style="max-width:180px;"> placeholder="Device name"
<input type="tel" required>
id="view.devmgr.newphone" <div class="invalid-feedback small">Device name is required.</div>
class="form-control form-control-sm" </div>
placeholder="(303) 555-1234" <div style="width:160px;">
style="max-width:160px;"> <input type="tel"
id="view.devmgr.newphone"
class="form-control form-control-sm"
placeholder="(303) 555-1234"
required>
<div class="invalid-feedback small">Phone number is required.</div>
</div>
<div style="width:140px;">
<select id="view.devmgr.newtype"
class="form-select form-select-sm"
required>
<option value="" selected>Device type</option>
<option value="iphone">iPhone</option>
<option value="android">Android</option>
<option value="browser">Browser</option>
<option value="other">Other</option>
</select>
<div class="invalid-feedback small">Device type is required.</div>
</div>
<button id="view.devmgr.btnadd" <button id="view.devmgr.btnadd"
class="btn btn-primary btn-sm">Add</button> class="btn btn-primary btn-sm">Add</button>
</div> </div>
...@@ -69,6 +87,23 @@ ...@@ -69,6 +87,23 @@
</p> </p>
</div> </div>
<!-- Information modal used by Utils.ShowInformationModal -->
<div class="modal fade" id="main_information_modal" tabindex="-1"
aria-labelledby="main_information_modal_title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm mx-auto px-3">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="main_information_modal_title">Information</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="main_information_modal_body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-primary w-100" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<!-- Section: Confirmation modal used by Utils.ShowConfirmationModal --> <!-- Section: Confirmation modal used by Utils.ShowConfirmationModal -->
<div class="modal fade" id="main_confirmation_modal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="main_confirmation_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
......
...@@ -25,8 +25,9 @@ type ...@@ -25,8 +25,9 @@ type
procedure ShowNotification(const AMsg: string; AIsError: Boolean = True); procedure ShowNotification(const AMsg: string; AIsError: Boolean = True);
procedure HideNotification; procedure HideNotification;
procedure ClearTable; procedure ClearTable;
procedure AddDeviceRow(const ACredentialId, AName, APhoneNumber, AUsername, procedure AddDeviceRow(const ACredentialId, AName, ADeviceType,
AUserAgent, ARegisteredAt, AStatus, AVerificationCode: string; APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus,
AVerificationCode: string;
AVerificationCodeActive: Boolean); AVerificationCodeActive: Boolean);
[async] procedure LoadDevices; [async] procedure LoadDevices;
[async] procedure RevokeDevice(const ACredentialId, AName: string); [async] procedure RevokeDevice(const ACredentialId, AName: string);
...@@ -73,10 +74,18 @@ begin ...@@ -73,10 +74,18 @@ begin
LoadDevices; LoadDevices;
asm asm
var nameInput = document.getElementById('view.devmgr.newname');
if (nameInput) {
nameInput.addEventListener('input', function() {
nameInput.classList.remove('is-invalid');
});
}
// Format phone input on-the-fly // Format phone input on-the-fly
var inp = document.getElementById('view.devmgr.newphone'); var inp = document.getElementById('view.devmgr.newphone');
if (inp) { if (inp) {
inp.addEventListener('input', function() { inp.addEventListener('input', function() {
inp.classList.remove('is-invalid');
var digits = inp.value.replace(/\D/g, ''); var digits = inp.value.replace(/\D/g, '');
if (digits.length > 10) digits = digits.slice(0, 10); if (digits.length > 10) digits = digits.slice(0, 10);
var fmt = ''; var fmt = '';
...@@ -89,6 +98,13 @@ begin ...@@ -89,6 +98,13 @@ begin
inp.value = fmt; inp.value = fmt;
}); });
} }
var typeSelect = document.getElementById('view.devmgr.newtype');
if (typeSelect) {
typeSelect.addEventListener('change', function() {
typeSelect.classList.remove('is-invalid');
});
}
end; end;
end; end;
...@@ -135,14 +151,14 @@ begin ...@@ -135,14 +151,14 @@ begin
end; end;
procedure TFViewDeviceManager.AddDeviceRow(const ACredentialId, AName, procedure TFViewDeviceManager.AddDeviceRow(const ACredentialId, AName,
APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus, ADeviceType, APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus,
AVerificationCode: string; AVerificationCodeActive: Boolean); AVerificationCode: string; AVerificationCodeActive: Boolean);
var var
tbody, tr, tdName, tdPhone, tdCode, tdUser, tdAgent, tdReg, tdStatus, tbody, tr, tdName, tdPhone, tdCode, tdUser, tdAgent, tdReg, tdStatus,
tdAccess, tdDelete, tdSend: TJSHTMLElement; tdAccess, tdDelete, tdSend: TJSHTMLElement;
btn, btnSend, inp: TJSHTMLElement; btn, btnSend, inp: TJSHTMLElement;
displayDate, displayPhone: string; displayDate, displayPhone: string;
isPending, isRevoked, isActive: Boolean; isPending, isRevoked, isActive, isIPhone, isAndroid: Boolean;
begin begin
tbody := TJSHTMLElement(document.getElementById('view.devmgr.tbody')); tbody := TJSHTMLElement(document.getElementById('view.devmgr.tbody'));
if not Assigned(tbody) then Exit; if not Assigned(tbody) then Exit;
...@@ -150,6 +166,8 @@ begin ...@@ -150,6 +166,8 @@ begin
isPending := AStatus = 'pending'; isPending := AStatus = 'pending';
isRevoked := AStatus = 'revoked'; isRevoked := AStatus = 'revoked';
isActive := AStatus = 'active'; isActive := AStatus = 'active';
isIPhone := SameText(ADeviceType, 'iphone');
isAndroid := SameText(ADeviceType, 'android');
displayPhone := FormatPhoneDisplay(APhoneNumber); displayPhone := FormatPhoneDisplay(APhoneNumber);
tr := TJSHTMLElement(document.createElement('tr')); tr := TJSHTMLElement(document.createElement('tr'));
...@@ -288,14 +306,18 @@ begin ...@@ -288,14 +306,18 @@ begin
// Section: Send link action // Section: Send link action
tdSend := TJSHTMLElement(document.createElement('td')); tdSend := TJSHTMLElement(document.createElement('td'));
if (not isRevoked) and (APhoneNumber <> '') then if (not isRevoked) and (APhoneNumber <> '') and (isIPhone or isAndroid) then
begin begin
btnSend := TJSHTMLElement(document.createElement('button')); btnSend := TJSHTMLElement(document.createElement('button'));
btnSend.className := 'btn btn-outline-primary btn-sm'; btnSend.className := 'btn btn-outline-primary btn-sm';
btnSend.innerText := 'Send Link'; btnSend.innerText := 'Send Link';
btnSend.addEventListener('click', procedure(Event: TJSMouseEvent) btnSend.addEventListener('click', procedure(Event: TJSMouseEvent)
begin begin
SendAppLink(APhoneNumber, AName); if isAndroid then
ShowInformationModal('Android App',
'Android app installation is not available yet.')
else
SendAppLink(APhoneNumber, AName);
end); end);
tdSend.appendChild(btnSend); tdSend.appendChild(btnSend);
end end
...@@ -337,6 +359,7 @@ begin ...@@ -337,6 +359,7 @@ begin
AddDeviceRow( AddDeviceRow(
string(item['credential_id']), string(item['credential_id']),
string(item['device_name']), string(item['device_name']),
string(item['device_type']),
string(item['phone_number']), string(item['phone_number']),
string(item['username']), string(item['username']),
string(item['user_agent']), string(item['user_agent']),
...@@ -517,27 +540,56 @@ end; ...@@ -517,27 +540,56 @@ end;
procedure TFViewDeviceManager.AddPendingDevice; procedure TFViewDeviceManager.AddPendingDevice;
var var
deviceName, phoneNumber: string; deviceName, phoneNumber, deviceType: string;
nameElement, phoneElement, typeElement: TJSHTMLElement;
resp: TXDataClientResponse; resp: TXDataClientResponse;
res: TJSObject; res: TJSObject;
status: string; status: string;
valid: Boolean;
begin begin
deviceName := Trim(edtNewDeviceName.Text); deviceName := Trim(edtNewDeviceName.Text);
phoneNumber := Trim(edtNewPhoneNumber.Text); phoneNumber := Trim(edtNewPhoneNumber.Text);
nameElement := TJSHTMLElement(document.getElementById('view.devmgr.newname'));
phoneElement := TJSHTMLElement(document.getElementById('view.devmgr.newphone'));
typeElement := TJSHTMLElement(document.getElementById('view.devmgr.newtype'));
deviceType := '';
if Assigned(typeElement) then
deviceType := Trim(string(TJSHTMLSelectElement(typeElement).value));
valid := True;
if Assigned(nameElement) then
nameElement.classList.remove('is-invalid');
if Assigned(phoneElement) then
phoneElement.classList.remove('is-invalid');
if Assigned(typeElement) then
typeElement.classList.remove('is-invalid');
if deviceName = '' then if deviceName = '' then
begin begin
ShowNotification('Please enter a device name.'); if Assigned(nameElement) then
Exit; nameElement.classList.add('is-invalid');
valid := False;
end; end;
if phoneNumber = '' then if phoneNumber = '' then
begin begin
ShowNotification('Please enter a phone number.'); if Assigned(phoneElement) then
Exit; phoneElement.classList.add('is-invalid');
valid := False;
end;
if deviceType = '' then
begin
if Assigned(typeElement) then
typeElement.classList.add('is-invalid');
valid := False;
end; end;
if not valid then
Exit;
try try
resp := await(XDataWebClient.RawInvokeAsync('IApiService.AddPendingDevice', [deviceName, phoneNumber])); resp := await(XDataWebClient.RawInvokeAsync('IApiService.AddPendingDevice',
[deviceName, phoneNumber, deviceType]));
res := TJSObject(resp.Result); res := TJSObject(resp.Result);
status := string(res['status']); status := string(res['status']);
...@@ -545,6 +597,7 @@ begin ...@@ -545,6 +597,7 @@ begin
begin begin
edtNewDeviceName.Text := ''; edtNewDeviceName.Text := '';
edtNewPhoneNumber.Text := ''; edtNewPhoneNumber.Text := '';
TJSHTMLSelectElement(typeElement).value := '';
ShowNotification('Device "' + deviceName + '" added — waiting for user to register.', False); ShowNotification('Device "' + deviceName + '" added — waiting for user to register.', False);
LoadDevices; LoadDevices;
end end
......
...@@ -91,12 +91,12 @@ ...@@ -91,12 +91,12 @@
<DCC_RemoteDebug>false</DCC_RemoteDebug> <DCC_RemoteDebug>false</DCC_RemoteDebug>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo> <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale> <VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.7;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.3.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys> <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.9;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.3.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys>
<AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode>
<VerInfo_MinorVer>9</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_Release>4</VerInfo_Release> <VerInfo_Release>4</VerInfo_Release>
<VerInfo_Build>7</VerInfo_Build> <VerInfo_Build>9</VerInfo_Build>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSWebBrowser>1</TMSWebBrowser> <TMSWebBrowser>1</TMSWebBrowser>
<TMSWebOutputPath>..\emiMobileServer\bin\static</TMSWebOutputPath> <TMSWebOutputPath>..\emiMobileServer\bin\static</TMSWebOutputPath>
......
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