Refactor API structure and update UI components
- Changed namespaces for controllers to better organize API components. - Updated base class for controllers to `BaseController` with standard routing. - Simplified method signatures by removing `id` parameters in entity creation methods. - Introduced new `CreateAsync` methods in service and repository layers. - Removed dependencies on `font-awesome` and `ionicons` from package files. - Migrated UI components to Material-UI, enhancing consistency and design. - Refactored `ForgotPasswordModal` to use Material-UI's `Dialog`. - Implemented `yup` validation in form components for stricter checks. - Cleaned up unused styles in CSS files for a cleaner codebase. - Updated interfaces to require `id` property for stricter type checks. - Improved error handling and user feedback in the `Login` component. - Added action buttons for adding and refreshing data in various components.
This commit is contained in:
parent
d7b00cf335
commit
ef75bdb779
@ -1,13 +1,12 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Surge365.MassEmailReact.API.Controllers;
|
||||
using Surge365.MassEmailReact.Application.DTOs;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class AuthenticationController : ControllerBase
|
||||
public class AuthenticationController : BaseController
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
|
||||
11
Surge365.MassEmailReact.API/Controllers/BaseController.cs
Normal file
11
Surge365.MassEmailReact.API/Controllers/BaseController.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("[controller]")]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class BaseController : ControllerBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class BouncedEmailsController : ControllerBase
|
||||
public class BouncedEmailsController : BaseController
|
||||
{
|
||||
private readonly IBouncedEmailService _bouncedEmailService;
|
||||
|
||||
|
||||
@ -3,11 +3,9 @@ using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class EmailDomainsController : ControllerBase
|
||||
public class EmailDomainsController : BaseController
|
||||
{
|
||||
private readonly IEmailDomainService _emailDomainService;
|
||||
|
||||
@ -34,7 +32,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
|
||||
}
|
||||
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> CreateTarget(int id, [FromBody] EmailDomainUpdateDto emailDomainUpdateDto)
|
||||
public async Task<IActionResult> CreateTarget([FromBody] EmailDomainUpdateDto emailDomainUpdateDto)
|
||||
{
|
||||
if (emailDomainUpdateDto.Id != null && emailDomainUpdateDto.Id > 0)
|
||||
return BadRequest("Id must be null or 0");
|
||||
|
||||
@ -4,12 +4,11 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Surge365.MassEmailReact.Application.DTOs;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
using Surge365.MassEmailReact.Infrastructure.Services;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class ServersController : ControllerBase
|
||||
public class ServersController : BaseController
|
||||
{
|
||||
private readonly IServerService _serverService;
|
||||
|
||||
@ -19,6 +18,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
|
||||
}
|
||||
|
||||
|
||||
[HttpGet()]
|
||||
[HttpGet("GetAll")]
|
||||
public async Task<IActionResult> GetAll([FromQuery] bool? activeOnly, bool? returnPassword = null)
|
||||
{
|
||||
@ -35,6 +35,21 @@ namespace Surge365.MassEmailReact.Server.Controllers
|
||||
var server = await _serverService.GetByIdAsync(id, returnPasswordValue);
|
||||
return server is not null ? Ok(server) : NotFound($"Server with key '{id}' not found.");
|
||||
}
|
||||
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> CreateServer([FromBody] ServerUpdateDto serverUpdateDto)
|
||||
{
|
||||
if (serverUpdateDto.Id != null && serverUpdateDto.Id > 0)
|
||||
return BadRequest("Id must be null or 0");
|
||||
|
||||
var serverId = await _serverService.CreateAsync(serverUpdateDto);
|
||||
if (serverId == null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create server.");
|
||||
|
||||
var createdServer = await _serverService.GetByIdAsync(serverId.Value);
|
||||
|
||||
return Ok(createdServer);
|
||||
}
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateServer(int id, [FromBody] ServerUpdateDto serverUpdateDto)
|
||||
{
|
||||
|
||||
@ -5,11 +5,9 @@ using Surge365.MassEmailReact.Application.DTOs;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class TargetsController : ControllerBase
|
||||
public class TargetsController : BaseController
|
||||
{
|
||||
private readonly ITargetService _targetService;
|
||||
|
||||
@ -33,14 +31,14 @@ namespace Surge365.MassEmailReact.Server.Controllers
|
||||
return target is not null ? Ok(target) : NotFound($"Target with key '{id}' not found.");
|
||||
}
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> CreateTarget(int id, [FromBody] TargetUpdateDto targetUpdateDto)
|
||||
public async Task<IActionResult> CreateTarget([FromBody] TargetUpdateDto targetUpdateDto)
|
||||
{
|
||||
if (targetUpdateDto.Id != null && targetUpdateDto.Id > 0)
|
||||
return BadRequest("Id must be null or 0");
|
||||
|
||||
var targetId = await _targetService.CreateAsync(targetUpdateDto);
|
||||
if (targetId == null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to craete target.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create target.");
|
||||
|
||||
var createdTarget = await _targetService.GetByIdAsync(targetId.Value);
|
||||
|
||||
|
||||
@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class TemplatesController : ControllerBase
|
||||
public class TemplatesController : BaseController
|
||||
{
|
||||
private readonly ITemplateService _templateService;
|
||||
|
||||
|
||||
@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class TestEmailListsController : ControllerBase
|
||||
public class TestEmailListsController : BaseController
|
||||
{
|
||||
private readonly ITestEmailListService _testEmailListService;
|
||||
|
||||
|
||||
@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.DTOs;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
|
||||
namespace Surge365.MassEmailReact.Server.Controllers
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class UnsubscribeUrlsController : ControllerBase
|
||||
public class UnsubscribeUrlsController : BaseController
|
||||
{
|
||||
private readonly IUnsubscribeUrlService _unsubscribeUrlService;
|
||||
|
||||
@ -31,7 +29,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
|
||||
return unsubscribeUrl is not null ? Ok(unsubscribeUrl) : NotFound($"UnsubscribeUrl with key '{id}' not found.");
|
||||
}
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> Create(int id, [FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto)
|
||||
public async Task<IActionResult> Create([FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto)
|
||||
{
|
||||
if (unsubscribeUrlUpdateDto.Id != null && unsubscribeUrlUpdateDto.Id > 0)
|
||||
return BadRequest("Id must be null or 0");
|
||||
|
||||
@ -46,7 +46,6 @@ app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapFallbackToFile("/index.html");
|
||||
|
||||
DapperConfiguration.ConfigureMappings();
|
||||
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
@Surge365.MassEmailReact.Server_HostAddress = http://localhost:5065
|
||||
@Surge365.MassEmailReact.API_HostAddress = http://localhost:5065/api
|
||||
@Surge365.MassEmailReact.UATServer_HostAddress = https://uat.massemail2.surge365.com/api
|
||||
|
||||
GET {{Surge365.MassEmailReact.Server_HostAddress}}/weatherforecast/
|
||||
GET {{Surge365.MassEmailReact.API_HostAddress}}/servers/
|
||||
Accept: application/json
|
||||
###
|
||||
|
||||
GET {{Surge365.MassEmailReact.UATServer_HostAddress}}/servers/get
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
@ -11,6 +11,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
{
|
||||
Task<Server?> GetByIdAsync(int id, bool returnPassword = false);
|
||||
Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false);
|
||||
Task<int?> CreateAsync(Server server);
|
||||
Task<bool> UpdateAsync(Server server);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
{
|
||||
Task<Server?> GetByIdAsync(int id, bool returnPassword = false);
|
||||
Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false);
|
||||
Task<int?> CreateAsync(ServerUpdateDto targetDto);
|
||||
Task<bool> UpdateAsync(ServerUpdateDto serverDto);
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,6 +66,35 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
return servers;
|
||||
}
|
||||
|
||||
public async Task<int?> CreateAsync(Server server)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(server);
|
||||
if (server.Id != null && server.Id > 0)
|
||||
throw new Exception("ID must be null");
|
||||
|
||||
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
|
||||
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@server_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
|
||||
parameters.Add("@name", server.Name, DbType.String);
|
||||
parameters.Add("@server_name", server.ServerName, DbType.String);
|
||||
parameters.Add("@port", server.Port, DbType.Int16);
|
||||
parameters.Add("@username", server.Username, DbType.String);
|
||||
parameters.Add("@password", server.Password, DbType.String);
|
||||
|
||||
// Output parameter
|
||||
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
||||
|
||||
await conn.ExecuteAsync("mem_save_server", parameters, commandType: CommandType.StoredProcedure);
|
||||
|
||||
// Retrieve the output parameter value
|
||||
bool success = parameters.Get<bool>("@success");
|
||||
|
||||
if (success)
|
||||
return parameters.Get<int>("@server_key");
|
||||
|
||||
return null;
|
||||
}
|
||||
public async Task<bool> UpdateAsync(Server server)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(server);
|
||||
|
||||
@ -10,6 +10,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
using System.Security.Cryptography;
|
||||
using Surge365.MassEmailReact.Infrastructure.Repositories;
|
||||
|
||||
|
||||
namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
@ -33,6 +34,22 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
{
|
||||
return await _serverRepository.GetAllAsync(activeOnly, returnPassword);
|
||||
}
|
||||
public async Task<int?> CreateAsync(ServerUpdateDto serverDto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto));
|
||||
if (serverDto.Id != null && serverDto.Id > 0)
|
||||
throw new Exception("ID must be null");
|
||||
|
||||
var server = new Server();
|
||||
|
||||
server.Name = serverDto.Name;
|
||||
server.ServerName = serverDto.ServerName;
|
||||
server.Port = serverDto.Port;
|
||||
server.Username = serverDto.Username;
|
||||
server.Password = serverDto.Password;
|
||||
|
||||
return await _serverRepository.CreateAsync(server);
|
||||
}
|
||||
public async Task<bool> UpdateAsync(ServerUpdateDto serverDto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto));
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="dist\**" />
|
||||
<None Remove="public\content\lib\**" />
|
||||
<TypeScriptConfiguration Remove="dist\**" />
|
||||
<TypeScriptConfiguration Remove="public\content\lib\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
33
Surge365.MassEmailReact.Web/package-lock.json
generated
33
Surge365.MassEmailReact.Web/package-lock.json
generated
@ -20,8 +20,6 @@
|
||||
"admin-lte": "4.0.0-beta3",
|
||||
"bootstrap": "^5.3.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"font-awesome": "^4.7.0",
|
||||
"ionicons": "^7.4.0",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-dom": "^19.0.0",
|
||||
@ -2035,19 +2033,6 @@
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.26.0.tgz",
|
||||
"integrity": "sha512-+0Inu+dJ9/LgWSskcZwx7v17v4GILcwIYxNgD+OuK0U+D5z61WsxWw7yHkYG5OqGPBijsJMVssYRx/Tn+e7F9A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@ -3345,15 +3330,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/font-awesome": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
||||
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==",
|
||||
"license": "(OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=0.10.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -3506,15 +3482,6 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
|
||||
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
|
||||
@ -23,8 +23,6 @@
|
||||
"admin-lte": "4.0.0-beta3",
|
||||
"bootstrap": "^5.3.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"font-awesome": "^4.7.0",
|
||||
"ionicons": "^7.4.0",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@ -228,7 +228,23 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
{ text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },
|
||||
].map((item) => (
|
||||
<ListItem key={item.text} disablePadding>
|
||||
<ListItemButton component={RouterLink} to={item.path} onClick={() => isMobile && handleDrawerClose()}
|
||||
<ListItemButton
|
||||
component={RouterLink}
|
||||
to={item.path}
|
||||
selected={location.pathname === item.path}
|
||||
onClick={() => isMobile && handleDrawerClose()}
|
||||
sx={{
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'& .MuiListItemIcon-root': {
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
},
|
||||
'&.Mui-selected:hover': {
|
||||
backgroundColor: 'primary.dark',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
|
||||
@ -1,29 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
//import { useEffect } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'admin-lte/dist/css/adminlte.min.css';
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
/*import 'ionicons/dist/css/ionicons.min.css';*/
|
||||
|
||||
import '@/css/adminlte-custom.css';
|
||||
import '@/css/surge365.css';
|
||||
|
||||
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
|
||||
import 'admin-lte/dist/js/adminlte.min.js';
|
||||
import 'admin-lte/dist/js/adminlte.min.js';
|
||||
|
||||
|
||||
|
||||
const LayoutLogin = function LayoutLogin({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
};
|
||||
|
||||
LayoutLogin.propTypes = {
|
||||
children: PropTypes.any
|
||||
children: PropTypes.any,
|
||||
};
|
||||
|
||||
export default LayoutLogin;
|
||||
@ -1,37 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
//import { useEffect } from 'react';
|
||||
import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'admin-lte/dist/css/adminlte.min.css';
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
/*import 'ionicons/dist/css/ionicons.min.css';*/
|
||||
|
||||
import '@/css/adminlte-custom.css';
|
||||
import '@/css/surge365.css';
|
||||
|
||||
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
|
||||
import 'admin-lte/dist/js/adminlte.min.js';
|
||||
import 'admin-lte/dist/js/adminlte.min.js';
|
||||
|
||||
|
||||
|
||||
const LayoutLogin = function LayoutLogin({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<Helmet>
|
||||
</Helmet>
|
||||
{children}
|
||||
</HelmetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
LayoutLogin.propTypes = {
|
||||
children: PropTypes.any
|
||||
};
|
||||
|
||||
export default LayoutLogin;
|
||||
@ -1,120 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
//import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'admin-lte/dist/css/adminlte.min.css';
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
/*import 'ionicons/dist/css/ionicons.min.css';*/
|
||||
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
|
||||
import 'admin-lte/dist/js/adminlte.min.js';
|
||||
|
||||
const Layout = function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="wrapper" style={{ overflow: 'initial' }}>
|
||||
<link rel="stylesheet" href="/content/dist/css/skins/_all-skins.min.css" />
|
||||
<link href="/content/plugins/datepicker/v4/bootstrap-datetimepicker.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/content/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css" />
|
||||
<link rel="stylesheet" href="/content/plugins/datatables/dataTables.bootstrap.css" />
|
||||
|
||||
{/*<script src="/content/dist/js/app.min.js"></script>AdminLTE?*/}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js"></script>
|
||||
{/*<script src="/content/plugins/datatables/jquery.dataTables.min.js"></script>*/}
|
||||
{/*<script src="/content/plugins/validator/validator.js"></script>*/}
|
||||
<script src="/content/js/jquery.surge365.utilities-1.0.js" />
|
||||
<script src="/content/js/jquery.surge365.global.js?36"></script>
|
||||
<script src="/content/js/jquery.surge365.webmethods-1.0.js?15"></script>
|
||||
<script src="/content/js/Main-1.0.js?39"></script>
|
||||
{/*<script src="/content/plugins/growl/jquery.bootstrap-growl.min.js"></script>*/}
|
||||
{/*<script src="/content/plugins/input-mask-5/jquery.inputmask.js"></script>*/}
|
||||
{/*<script src="/content/plugins/printThis/printThis.js"></script>*/}
|
||||
{/*<script src="/content/plugins/datatables/dataTables.bootstrap.min.js"></script>*/}
|
||||
<header className="main-header">
|
||||
<a href="Dashboard" className="logo" style={{ backgroundColor: '#333' }}>
|
||||
<span className="logo-mini" style={{ backgroundColor: '#333' }}>
|
||||
<img id="imgLogo-sm" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
||||
</span>
|
||||
<span className="logo-lg" style={{ textAlign: 'center', backgroundColor: '#333' }}>
|
||||
<img id="imgLogo-lg" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
||||
<span style={{ marginLeft: '5px', verticalAlign: 'middle' }}><b>USA</b>Haulers</span>
|
||||
</span>
|
||||
</a>
|
||||
<nav className="navbar navbar-static-top">
|
||||
<a href="#" className="fa5 sidebar-toggle" data-toggle="offcanvas" role="button">
|
||||
<span className="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div className="navbar-custom-menu">
|
||||
<ul className="nav navbar-nav">
|
||||
<li className="dropdown user user-menu">
|
||||
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
|
||||
<span id="spanSpinner" className="fa fa-sync-alt pull-left" style={{ lineHeight: 'unset', display: 'none', fontSize: '16pt', fontWeight: 'bold', marginRight: '10px' }}></span>
|
||||
<img id="imgRightProfile" src="/content/img/generic_avatar.jpg" className="user-image" alt="User Image" />
|
||||
<span id="spanProfileName"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu">
|
||||
<li className="user-header">
|
||||
<img id="imgMainProfile" src="" className="img-circle" alt="User Image" />
|
||||
<p>
|
||||
<span id="spanProfileNameTitle"></span>
|
||||
</p>
|
||||
</li>
|
||||
<li className="user-footer">
|
||||
<div className="pull-left">
|
||||
<a href="Profile" className="btn btn-default btn-flat">Profile</a>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<input type="button" id="btnSignOut" className="btn btn-primary" value="Sign Out" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside className="main-sidebar">
|
||||
<section className="sidebar">
|
||||
<div className="user-panel">
|
||||
<div className="pull-left image">
|
||||
<img id="imgLeftProfile" src="/content/img/generic_avatar.jpg" className="img-circle" alt="User Image" />
|
||||
</div>
|
||||
<div className="pull-left info" style={{ lineHeight: 3 }}>
|
||||
<p><span id="spanMenuName"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="sidebar-menu">
|
||||
<li className="header">MAIN NAVIGATION</li>
|
||||
<li id="liDashboard" className="active treeview">
|
||||
<a href="Dashboard">
|
||||
<i className="fa fa-home"></i><span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="treeview">
|
||||
<a href="#" id="aSignOut">
|
||||
<i className="fa fa-times-circle"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
{children}
|
||||
|
||||
<footer className="main-footer" style={{ padding: '2px' }}>
|
||||
<div className="pull-right hidden-xs">
|
||||
<b>Version</b> 1.0.0
|
||||
</div>
|
||||
<strong>Copyright © 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.any
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@ -1,125 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
//import React from 'react';
|
||||
import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'admin-lte/dist/css/adminlte.min.css';
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
/*import 'ionicons/dist/css/ionicons.min.css';*/
|
||||
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
|
||||
import 'admin-lte/dist/js/adminlte.min.js';
|
||||
|
||||
const Layout = function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<div className="wrapper" style={{ overflow: 'initial' }}>
|
||||
<Helmet>
|
||||
<link rel="stylesheet" href="/content/dist/css/skins/_all-skins.min.css" />
|
||||
<link href="/content/plugins/datepicker/v4/bootstrap-datetimepicker.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/content/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css" />
|
||||
<link rel="stylesheet" href="/content/plugins/datatables/dataTables.bootstrap.css" />
|
||||
|
||||
{/*<script src="/content/dist/js/app.min.js"></script>AdminLTE?*/}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js"></script>
|
||||
{/*<script src="/content/plugins/datatables/jquery.dataTables.min.js"></script>*/}
|
||||
{/*<script src="/content/plugins/validator/validator.js"></script>*/}
|
||||
<script src="/content/js/jquery.surge365.utilities-1.0.js" />
|
||||
<script src="/content/js/jquery.surge365.global.js?36"></script>
|
||||
<script src="/content/js/jquery.surge365.webmethods-1.0.js?15"></script>
|
||||
<script src="/content/js/Main-1.0.js?39"></script>
|
||||
{/*<script src="/content/plugins/growl/jquery.bootstrap-growl.min.js"></script>*/}
|
||||
{/*<script src="/content/plugins/input-mask-5/jquery.inputmask.js"></script>*/}
|
||||
{/*<script src="/content/plugins/printThis/printThis.js"></script>*/}
|
||||
{/*<script src="/content/plugins/datatables/dataTables.bootstrap.min.js"></script>*/}
|
||||
</Helmet>
|
||||
<header className="main-header">
|
||||
<a href="Dashboard" className="logo" style={{ backgroundColor: '#333' }}>
|
||||
<span className="logo-mini" style={{ backgroundColor: '#333' }}>
|
||||
<img id="imgLogo-sm" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
||||
</span>
|
||||
<span className="logo-lg" style={{ textAlign: 'center', backgroundColor: '#333' }}>
|
||||
<img id="imgLogo-lg" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
||||
<span style={{ marginLeft: '5px', verticalAlign: 'middle' }}><b>USA</b>Haulers</span>
|
||||
</span>
|
||||
</a>
|
||||
<nav className="navbar navbar-static-top">
|
||||
<a href="#" className="fa5 sidebar-toggle" data-toggle="offcanvas" role="button">
|
||||
<span className="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div className="navbar-custom-menu">
|
||||
<ul className="nav navbar-nav">
|
||||
<li className="dropdown user user-menu">
|
||||
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
|
||||
<span id="spanSpinner" className="fa fa-sync-alt pull-left" style={{ lineHeight: 'unset', display: 'none', fontSize: '16pt', fontWeight: 'bold', marginRight: '10px' }}></span>
|
||||
<img id="imgRightProfile" src="/content/img/generic_avatar.jpg" className="user-image" alt="User Image" />
|
||||
<span id="spanProfileName"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu">
|
||||
<li className="user-header">
|
||||
<img id="imgMainProfile" src="" className="img-circle" alt="User Image" />
|
||||
<p>
|
||||
<span id="spanProfileNameTitle"></span>
|
||||
</p>
|
||||
</li>
|
||||
<li className="user-footer">
|
||||
<div className="pull-left">
|
||||
<a href="Profile" className="btn btn-default btn-flat">Profile</a>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<input type="button" id="btnSignOut" className="btn btn-primary" value="Sign Out" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside className="main-sidebar">
|
||||
<section className="sidebar">
|
||||
<div className="user-panel">
|
||||
<div className="pull-left image">
|
||||
<img id="imgLeftProfile" src="/content/img/generic_avatar.jpg" className="img-circle" alt="User Image" />
|
||||
</div>
|
||||
<div className="pull-left info" style={{ lineHeight: 3 }}>
|
||||
<p><span id="spanMenuName"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="sidebar-menu">
|
||||
<li className="header">MAIN NAVIGATION</li>
|
||||
<li id="liDashboard" className="active treeview">
|
||||
<a href="Dashboard">
|
||||
<i className="fa fa-home"></i><span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="treeview">
|
||||
<a href="#" id="aSignOut">
|
||||
<i className="fa fa-times-circle"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
{children}
|
||||
|
||||
<footer className="main-footer" style={{ padding: '2px' }}>
|
||||
<div className="pull-right hidden-xs">
|
||||
<b>Version</b> 1.0.0
|
||||
</div>
|
||||
<strong>Copyright © 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
|
||||
</footer>
|
||||
</div>
|
||||
</HelmetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.any
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@ -23,7 +23,7 @@ type EmailDomainEditProps = {
|
||||
};
|
||||
|
||||
const schema = yup.object().shape({
|
||||
id: yup.number(),
|
||||
id: yup.number().default(0),
|
||||
name: yup
|
||||
.string()
|
||||
.required("Name is required")
|
||||
@ -37,7 +37,7 @@ const schema = yup.object().shape({
|
||||
emailAddress: yup.string().email("Invalid email").required("Email address is required"),
|
||||
username: yup.string().required("Username is required"),
|
||||
password: yup.string().default("")
|
||||
.test("required-if-new", "NamePassword is required", function (value) {
|
||||
.test("required-if-new", "Password is required", function (value) {
|
||||
if (this.parent.id > 0) return true;
|
||||
else return value.length > 0;
|
||||
}),
|
||||
@ -60,7 +60,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
|
||||
const setupData: SetupData = useSetupData();
|
||||
const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null;
|
||||
|
||||
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<EmailDomain>({
|
||||
const { register, control, handleSubmit, reset, formState: { errors } } = useForm<EmailDomain>({
|
||||
mode: "onBlur",
|
||||
defaultValues: emailDomain || defaultEmailDomain,
|
||||
resolver: yupResolver(schema) as Resolver<EmailDomain>,
|
||||
@ -89,7 +89,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
|
||||
if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update");
|
||||
|
||||
const updatedEmailDomain = await response.json();
|
||||
onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password);
|
||||
onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password ?? "");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { Modal, Button, Form } from 'react-bootstrap';
|
||||
import { FaExclamationCircle } from 'react-icons/fa'; // For optional font icon
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import utils from '@/ts/utils';
|
||||
|
||||
type FormErrors = Record<string, string>;
|
||||
@ -25,9 +25,8 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFormErrors(errors);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleStartPasswordRecovery = async (e: FormEvent) => {
|
||||
@ -52,48 +51,51 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={onClose} backdrop="static" keyboard={false} centered animation={false}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Forgot your password?</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Dialog open={show} onClose={() => { }}>
|
||||
<DialogTitle>
|
||||
Forgot your password?
|
||||
<IconButton onClick={onClose} sx={{ position: 'absolute', right: 8, top: 8 }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{usernameNotFound && (
|
||||
<span>An email has been sent to the address you provided. Please follow the instructions to reset your password.</span>
|
||||
<Typography color="error" sx={{ mb: 2 }}>
|
||||
An email has been sent to the address you provided. Please follow the instructions to reset your password.
|
||||
</Typography>
|
||||
)}
|
||||
{!recoveryStarted && (
|
||||
<Form onSubmit={handleStartPasswordRecovery}>
|
||||
<Form.Group controlId="formForgotEmail" className="position-relative mb-3">
|
||||
<Form.Label className="mb-4 text-center">Enter your email address below and we'll send you instructions on how to reset your password...</Form.Label>
|
||||
<Form.Label className="visually-hidden">Email</Form.Label>
|
||||
<Form.Control
|
||||
type="username"
|
||||
placeholder="Username"
|
||||
<form onSubmit={handleStartPasswordRecovery}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body1" sx={{ mb: 2, textAlign: 'center' }}>
|
||||
Enter your username below and we'll send you instructions on how to reset your password...
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Username"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={username}
|
||||
isInvalid={!!formErrors.username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
error={!!formErrors.username}
|
||||
helperText={formErrors.username}
|
||||
required
|
||||
autoFocus
|
||||
size="lg"
|
||||
size="small"
|
||||
/>
|
||||
{formErrors.username && (
|
||||
<FaExclamationCircle className="validation-icon text-danger" title={formErrors.username} />
|
||||
)}
|
||||
<Form.Control.Feedback type="invalid">{formErrors.username}</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Button variant="primary" className="bg-orange btn-flat w-100" type="submit">
|
||||
</Box>
|
||||
<Button type="submit" variant="contained" color="primary" fullWidth>
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPasswordModal;
|
||||
|
||||
|
||||
@ -9,35 +9,72 @@ import {
|
||||
} from "@mui/material";
|
||||
import Server from "@/types/server";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
type ServerEditProps = {
|
||||
open: boolean;
|
||||
server: Server;
|
||||
server: Server | null;
|
||||
onClose: () => void;
|
||||
onSave: (updatedServer: Server) => void;
|
||||
onSave: (updatedServer: Server, password: string) => void;
|
||||
};
|
||||
|
||||
const schema = yup.object().shape({
|
||||
id: yup.number().default(0),
|
||||
name: yup
|
||||
.string()
|
||||
.required("Name is required")
|
||||
.test("unique-name", "Name must be unique", function (value) {
|
||||
const setupData = this.options.context?.setupData as { servers: Server[] };
|
||||
if (!setupData) return true;
|
||||
return !setupData.servers.some(
|
||||
(d) => d.name.toLowerCase() === value?.toLowerCase() && (d.id === 0 || d.id !== this.parent.id)
|
||||
);
|
||||
}),
|
||||
serverName: yup.string().required("Server name is required"),
|
||||
port: yup.number().default(1433),
|
||||
username: yup.string().required("Username is required"),
|
||||
password: yup.string().default("")
|
||||
.test("required-if-new", "Password is required", function (value) {
|
||||
if (this.parent.id > 0) return true;
|
||||
else return value.length > 0;
|
||||
})
|
||||
});
|
||||
|
||||
const defaultServer: Server = {
|
||||
id: 0,
|
||||
name: "",
|
||||
serverName: "",
|
||||
port: 1433,
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
||||
const [formData, setFormData] = useState<Server>({ ...server });
|
||||
//const [serverError, setServerError] = useState(false); // Track validation
|
||||
const isNew = !server || server.id === 0;
|
||||
const originalServer: Server | null = server ? { ...server } : null;
|
||||
//const [formData, setFormData] = useState<Server>({ ...server });
|
||||
const { register, handleSubmit, reset, formState: { errors } } = useForm<Server>({
|
||||
mode: "onBlur",
|
||||
defaultValues: server || defaultServer,
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => { //Reset form to unedited state on open or server change
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFormData(server);
|
||||
reset(server || defaultServer, { keepDefaultValues: true });
|
||||
}
|
||||
}, [open, server]);
|
||||
}, [open, server, reset]);
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value || "" }));
|
||||
|
||||
//if (field === "serverId" && value) setServerError(false);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleSave = async (formData: Server) => {
|
||||
const apiUrl = isNew ? "/api/servers" : `/api/servers/${formData.id}`;
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${formData.id}`, {
|
||||
method: "PUT",
|
||||
const response = await fetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
@ -45,7 +82,7 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
||||
if (!response.ok) throw new Error("Failed to update");
|
||||
|
||||
const updatedServer = await response.json();
|
||||
onSave(updatedServer); // Update UI optimistically
|
||||
onSave(updatedServer, isNew || formData.password ? formData.password : originalServer?.password ?? "");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
@ -56,47 +93,52 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Edit Server - id={formData.id}</DialogTitle>
|
||||
<DialogTitle>{isNew ? "Add Server" : "Edit Server id=" + server?.id}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
{...register("name")}
|
||||
label="Name"
|
||||
fullWidth
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange("name", e.target.value)}
|
||||
margin="dense"
|
||||
error={!!errors.name}
|
||||
helperText={errors.name?.message}
|
||||
/>
|
||||
<TextField
|
||||
{...register("serverName")}
|
||||
label="Server Name"
|
||||
fullWidth
|
||||
value={formData.serverName}
|
||||
onChange={(e) => handleChange("serverName", e.target.value)}
|
||||
margin="dense"
|
||||
error={!!errors.serverName}
|
||||
helperText={errors.serverName?.message}
|
||||
/>
|
||||
<TextField
|
||||
{...register("port")}
|
||||
label="Port"
|
||||
fullWidth
|
||||
value={formData.port}
|
||||
onChange={(e) => handleChange("port", e.target.value)}
|
||||
margin="dense"
|
||||
error={!!errors.port}
|
||||
helperText={errors.port?.message}
|
||||
/>
|
||||
<TextField
|
||||
{...register("username")}
|
||||
label="Username"
|
||||
fullWidth
|
||||
value={formData.username}
|
||||
onChange={(e) => handleChange("username", e.target.value)}
|
||||
margin="dense"
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
/>
|
||||
<TextField
|
||||
{...register("password")}
|
||||
label="Password"
|
||||
fullWidth
|
||||
value={formData.password}
|
||||
onChange={(e) => handleChange("password", e.target.value)}
|
||||
margin="dense"
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={loading}>Cancel</Button>
|
||||
<Button onClick={handleSave} color="primary" disabled={loading}>
|
||||
<Button onClick={handleSubmit(handleSave)} color="primary" disabled={loading}>
|
||||
{loading ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@ -94,8 +94,8 @@ const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) =>
|
||||
|
||||
const handleSave = async (formData: Template) => {
|
||||
const domain = setupData.emailDomains.find(el => el.id === formData.domainId);
|
||||
formData.fromEmail = domain.emailAddress;
|
||||
formData.replyToEmail = domain.emailAddress;
|
||||
formData.fromEmail = domain?.emailAddress ?? "";
|
||||
formData.replyToEmail = domain?.emailAddress ?? "";
|
||||
const apiUrl = isNew ? "/api/templates" : `/api/templates/${formData.id}`;
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid';
|
||||
import BouncedEmail from '@/types/bouncedEmail';
|
||||
import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit";
|
||||
@ -22,10 +24,10 @@ function BouncedEmails() {
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams<BouncedEmail>) => (
|
||||
<div>
|
||||
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={(e) => { e.stopPropagation(); handleDelete(params.row); }}>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleDelete(params.row); }}>
|
||||
<GridDeleteIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
@ -111,22 +113,12 @@ function BouncedEmails() {
|
||||
slots={{
|
||||
toolbar: () => (
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => handleNew()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setupData.reloadBouncedEmails()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
|
||||
</Button>
|
||||
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" color="primary" onClick={() => setupData.reloadBouncedEmails()} sx={{ marginLeft: 1 }}>
|
||||
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarDensitySelector />
|
||||
<GridToolbarExport />
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { Lock, LockOpen } from "@mui/icons-material";
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
@ -21,12 +23,13 @@ function EmailDomains() {
|
||||
const columns: GridColDef<EmailDomain>[] = [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Actions",
|
||||
headerName: "",
|
||||
sortable: false,
|
||||
width: 60,
|
||||
renderCell: (params: GridRenderCellParams<EmailDomain>) => (
|
||||
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{ field: "id", headerName: "ID", width: 60 },
|
||||
@ -40,7 +43,7 @@ function EmailDomains() {
|
||||
renderHeader: () => (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
Password
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}>
|
||||
<IconButton color="primary" size="small" onClick={(e) => { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}>
|
||||
{isPasswordVisible ? <LockOpen /> : <Lock />}
|
||||
</IconButton>
|
||||
</div>
|
||||
@ -100,7 +103,7 @@ function EmailDomains() {
|
||||
: [...prev, updatedEmailDomain];
|
||||
});
|
||||
};
|
||||
const displayRows = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains;
|
||||
const displayRows: EmailDomain[] = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains;
|
||||
|
||||
return (
|
||||
<Box ref={gridContainerRef} sx={{
|
||||
@ -133,7 +136,7 @@ function EmailDomains() {
|
||||
<Typography variant="body2">Display Order: {row.displayOrder}</Typography>
|
||||
<Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography>
|
||||
</CardContent>
|
||||
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@ -150,26 +153,12 @@ function EmailDomains() {
|
||||
slots={{
|
||||
toolbar: () => (
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => handleNew()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
Add New
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (isPasswordVisible)
|
||||
loadEmailDomainsWithPasswords();
|
||||
setupData.reloadEmailDomains();
|
||||
}}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.emailDomainsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
|
||||
</Button>
|
||||
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" color="primary" onClick={() => setupData.reloadEmailDomains()} sx={{ marginLeft: 1 }}>
|
||||
{setupData.emailDomainsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarDensitySelector />
|
||||
<GridToolbarExport />
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Form, Spinner } from 'react-bootstrap';
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Typography,
|
||||
Box,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth';
|
||||
//import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||
|
||||
import utils from '@/ts/utils.ts';
|
||||
import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
|
||||
//import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
|
||||
|
||||
type SpinnerState = Record<string, boolean>;
|
||||
type FormErrors = Record<string, string>;
|
||||
@ -15,17 +21,10 @@ function Login() {
|
||||
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
|
||||
//const [user, setUser] = useState<User | null>(null);
|
||||
//const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
|
||||
const [loginError, setLoginError] = useState<boolean>(false);
|
||||
const [loginErrorMessage, setLoginErrorMessage] = useState<string>('');
|
||||
|
||||
//const setSpinners = (newValues: Partial<SpinnerState>) => {
|
||||
// setSpinnersState((prevSpinners) => ({
|
||||
// ...prevSpinners,
|
||||
// ...newValues,
|
||||
// }));
|
||||
//};
|
||||
const setSpinners = (newValues: Partial<SpinnerState>) => {
|
||||
setSpinnersState((prevSpinners) => {
|
||||
const updatedSpinners: SpinnerState = { ...prevSpinners };
|
||||
@ -38,23 +37,19 @@ function Login() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseForgotPasswordModal = () => {
|
||||
setShowForgotPasswordModal(false);
|
||||
};
|
||||
//const handleCloseForgotPasswordModal = () => {
|
||||
// setShowForgotPasswordModal(false);
|
||||
//};
|
||||
|
||||
const validateLoginForm = () => {
|
||||
setFormErrors({});
|
||||
|
||||
const errors: FormErrors = {};
|
||||
if (!username.trim()) {
|
||||
errors.username = 'Username is required';
|
||||
//} else if (!/\S+@\S+\.\S+/.test(email)) {
|
||||
// errors.email = 'Invalid email address';
|
||||
}
|
||||
if (!password.trim()) {
|
||||
errors.password = 'Password is required';
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFormErrors(errors);
|
||||
}
|
||||
@ -63,14 +58,15 @@ function Login() {
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
spinners.Login = true;
|
||||
setSpinners(spinners);
|
||||
setSpinners({ Login: true });
|
||||
|
||||
validateLoginForm();
|
||||
if (Object.keys(formErrors).length > 0) {
|
||||
setIsLoading(false);
|
||||
setSpinners({ Login: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(formErrors).length > 0) return;
|
||||
|
||||
//setUser(null);
|
||||
setLoginError(false);
|
||||
setLoginErrorMessage('');
|
||||
let loggedInUser: User | null = null;
|
||||
@ -83,45 +79,39 @@ function Login() {
|
||||
success: (json: AuthResponse) => {
|
||||
try {
|
||||
loggedInUser = json.user;
|
||||
//setUser(loggedInUser);
|
||||
}
|
||||
catch {
|
||||
const errorMsg: string = "Unexpected Error";
|
||||
} catch {
|
||||
const errorMsg: string = 'Unexpected Error';
|
||||
hadLoginError = true;
|
||||
hadLoginErrorMessage = errorMsg;
|
||||
}
|
||||
},
|
||||
error: (err: unknown) => {
|
||||
let errorMsg: string = "Unexpected Error";
|
||||
let errorMsg: string = 'Unexpected Error';
|
||||
if (isAuthErrorResponse(err)) {
|
||||
if (err && err as AuthErrorResponse) {
|
||||
if (err.data) {
|
||||
if (err.data.message)
|
||||
errorMsg = err.data.message;
|
||||
const errorResponse = err as AuthErrorResponse;
|
||||
if (errorResponse.data?.message) {
|
||||
errorMsg = errorResponse.data.message;
|
||||
}
|
||||
console.error(errorMsg);
|
||||
setLoginErrorMessage(errorMsg);
|
||||
}
|
||||
}
|
||||
hadLoginError = true;
|
||||
hadLoginErrorMessage = errorMsg;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (hadLoginError) {
|
||||
setLoginErrorMessage(hadLoginErrorMessage);
|
||||
setLoginError(true);
|
||||
setIsLoading(false);
|
||||
spinners.Login = false;
|
||||
setSpinners(spinners);
|
||||
setSpinners({ Login: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (loggedInUser == null) {
|
||||
setLoginError(true);
|
||||
setIsLoading(false);
|
||||
spinners.Login = false;
|
||||
setSpinners(spinners);
|
||||
setSpinners({ Login: false });
|
||||
} else {
|
||||
await finishUserLogin(loggedInUser);
|
||||
}
|
||||
@ -129,15 +119,13 @@ function Login() {
|
||||
|
||||
const finishUserLogin = async (loggedInUser: User) => {
|
||||
setIsLoading(false);
|
||||
spinners.Login = false;
|
||||
spinners.LoginWithPasskey = false;
|
||||
setSpinners(spinners);
|
||||
setSpinners({ Login: false, LoginWithPasskey: false });
|
||||
|
||||
utils.localStorage("session_currentUser", loggedInUser);
|
||||
utils.localStorage('session_currentUser', loggedInUser);
|
||||
|
||||
const redirectUrl = utils.sessionStorage("redirect_url");
|
||||
const redirectUrl = utils.sessionStorage('redirect_url');
|
||||
if (redirectUrl) {
|
||||
utils.sessionStorage("redirect_url", null);
|
||||
utils.sessionStorage('redirect_url', null);
|
||||
document.location.href = redirectUrl;
|
||||
} else {
|
||||
document.location.href = '/home';
|
||||
@ -145,54 +133,88 @@ function Login() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row text-center mt-5">
|
||||
<h1>surge365 - React</h1>
|
||||
</div>
|
||||
<div className="row text-center" style={{ maxWidth: '400px', margin: 'auto' }}>
|
||||
<h3 className="form-signin-heading mt-3 mb-1">Please sign in</h3>
|
||||
<Form id="frmLogin" onSubmit={handleLogin}>
|
||||
<Container maxWidth="sm">
|
||||
{/* Main heading */}
|
||||
<Box sx={{ textAlign: 'center', mt: 5 }}>
|
||||
<Typography variant="h4">Surge 365 Mass Email 2</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Login form */}
|
||||
<Box sx={{ maxWidth: 400, margin: 'auto', mt: 3 }}>
|
||||
<Typography variant="h5" align="center">
|
||||
Please sign in
|
||||
</Typography>
|
||||
<form onSubmit={handleLogin}>
|
||||
{/* Login error message */}
|
||||
{loginError && (
|
||||
<Form.Label style={{ color: 'red' }}>{loginErrorMessage ?? "Login error"}</Form.Label>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{loginErrorMessage || 'Login error'}
|
||||
</Alert>
|
||||
)}
|
||||
<Form.Group className="mb-3" controlId="txtUsernamel">
|
||||
<Form.Label className="visually-hidden">Username</Form.Label>
|
||||
<Form.Control
|
||||
type="username"
|
||||
placeholder="Username"
|
||||
|
||||
{/* Username field */}
|
||||
<TextField
|
||||
label="Username"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
error={!!formErrors.username}
|
||||
helperText={formErrors.username}
|
||||
required
|
||||
autoFocus
|
||||
size="sm"
|
||||
size="small"
|
||||
/>
|
||||
{spinners.Username && <Spinner animation="border" size="sm" />}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="txtPassword">
|
||||
<Form.Label className="visually-hidden">Password</Form.Label>
|
||||
<Form.Control
|
||||
{/* Password field */}
|
||||
<TextField
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
error={!!formErrors.password}
|
||||
helperText={formErrors.password}
|
||||
required
|
||||
size="sm"
|
||||
size="small"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Button className="bg-orange w-100" type="submit" disabled={isLoading}>
|
||||
{spinners.Login && <Spinner animation="border" size="sm" className="me-2" />}
|
||||
{/* Sign in button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
startIcon={
|
||||
isLoading && spinners.Login ? <CircularProgress size={24} color="inherit" /> : null
|
||||
}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{isLoading && spinners.Login ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-100 mt-2" onClick={() => setShowForgotPasswordModal(true)}>
|
||||
Forgot Password
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<ForgotPasswordModal show={showForgotPasswordModal} onClose={handleCloseForgotPasswordModal} />
|
||||
</div>
|
||||
{/* Forgot password button */}
|
||||
{/*<Button*/}
|
||||
{/* variant="outlined"*/}
|
||||
{/* fullWidth*/}
|
||||
{/* onClick={() => setShowForgotPasswordModal(true)}*/}
|
||||
{/* sx={{ mt: 1 }}*/}
|
||||
{/*>*/}
|
||||
{/* Forgot Password*/}
|
||||
{/*</Button>*/}
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
{/* Forgot password modal */}
|
||||
{/*<ForgotPasswordModal*/}
|
||||
{/* show={showForgotPasswordModal}*/}
|
||||
{/* onClose={handleCloseForgotPasswordModal}*/}
|
||||
{/*/>*/}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
//import Typography from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import { Lock, LockOpen } from "@mui/icons-material";
|
||||
//import utils from '@/ts/utils';
|
||||
@ -15,7 +17,7 @@ function Servers() {
|
||||
const setupData: SetupData = useSetupData();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const [servers, setServers] = useState<Server[] | null>(null);
|
||||
const [serversWithPasswords, setServersWithPasswords] = useState<Server[] | null>(null);
|
||||
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [selectedRow, setSelectedRow] = useState<Server | null>(null);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
@ -24,14 +26,14 @@ function Servers() {
|
||||
const togglePasswordVisibility = async () => {
|
||||
if (isPasswordVisible) {
|
||||
setIsPasswordVisible(false);
|
||||
setServers(setupData.servers);
|
||||
setServersWithPasswords(setupData.servers);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
setIsPasswordVisible(true);
|
||||
const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true");
|
||||
const serversData = await serversResponse.json();
|
||||
setServers(serversData);
|
||||
setServersWithPasswords(serversData);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error fetching servers:", error);
|
||||
@ -42,12 +44,13 @@ function Servers() {
|
||||
const columns: GridColDef<Server>[] = [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Actions",
|
||||
headerName: "",
|
||||
sortable: false,
|
||||
width: 60,
|
||||
renderCell: (params: GridRenderCellParams<Server>) => (
|
||||
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{ field: "id", headerName: "ID", width: 60 },
|
||||
@ -62,7 +65,7 @@ function Servers() {
|
||||
renderHeader: () => (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
Password
|
||||
<IconButton size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
|
||||
<IconButton size="small" color="primary" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
|
||||
{isPasswordVisible ? <LockOpen /> : <Lock />}
|
||||
</IconButton>
|
||||
</div>
|
||||
@ -73,21 +76,49 @@ function Servers() {
|
||||
];
|
||||
|
||||
|
||||
const handleNew = () => {
|
||||
setSelectedRow(null);
|
||||
setOpen(true);
|
||||
};
|
||||
const handleEdit = (row: GridRowModel<Server>) => {
|
||||
setSelectedRow(row);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleUpdateRow = (updatedRow: Server) => {
|
||||
setupData.setServers(updatedRow);
|
||||
updateServers(updatedRow);
|
||||
//const handleSaveRow = (updatedRow: Server, password: string) => {
|
||||
// setupData.setServersWithPasswords(updatedRow);
|
||||
// if (isPasswordVisible) {
|
||||
// updateServers
|
||||
// }
|
||||
// updateServers(updatedRow);
|
||||
//};
|
||||
//const updateServers = (updatedServer: Server) => {
|
||||
// setServersWithPasswords((prevServers) => {
|
||||
// if (prevServers == null) return null;
|
||||
// return prevServers.map((server) => (server.id === updatedServer.id ? updatedServer : server))
|
||||
// });
|
||||
//};
|
||||
const handleSaveRow = (savedRow: Server, password: string) => {
|
||||
setupData.setServers(savedRow);
|
||||
if (isPasswordVisible) {
|
||||
if (password)
|
||||
updateServers({ ...savedRow, password: password }); // Update local state
|
||||
else
|
||||
updateServers({ ...savedRow });
|
||||
}
|
||||
};
|
||||
const updateServers = (updatedServer: Server) => {
|
||||
setServers((prevServers) => {
|
||||
if (prevServers == null) return null;
|
||||
return prevServers.map((server) => (server.id === updatedServer.id ? updatedServer : server))
|
||||
setServersWithPasswords((prev) => {
|
||||
if (prev == null) return null;
|
||||
|
||||
const exists = prev.some((e) => e.id === updatedServer.id);
|
||||
|
||||
return exists
|
||||
? prev.map((server) => (server.id === updatedServer.id ? updatedServer : server))
|
||||
: [...prev, updatedServer];
|
||||
});
|
||||
};
|
||||
const displayRows: Server[] = isPasswordVisible ? (serversWithPasswords ?? setupData.servers) : setupData.servers;
|
||||
return (
|
||||
<Box ref={gridContainerRef} sx={{
|
||||
position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", overflow: "hidden",
|
||||
@ -99,7 +130,7 @@ function Servers() {
|
||||
<Box sx={{ position: 'absolute', inset: 0 }}>
|
||||
{isMobile ? (
|
||||
<List>
|
||||
{(!isPasswordVisible ? setupData.servers : servers)?.map((row) => (
|
||||
{displayRows.map((row) => (
|
||||
<Card key={row.id} sx={{ marginBottom: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<CardContent>
|
||||
@ -109,11 +140,11 @@ function Servers() {
|
||||
<Typography variant="body2">Port: {row.port}</Typography>
|
||||
<Typography variant="body2">Username: {row.username}</Typography>
|
||||
<Typography variant="body2">Password
|
||||
<IconButton size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
|
||||
<IconButton color="primary" size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
|
||||
{isPasswordVisible ? <LockOpen /> : <Lock />}
|
||||
</IconButton>: {row.password}</Typography>
|
||||
</CardContent>
|
||||
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@ -122,7 +153,7 @@ function Servers() {
|
||||
</List>
|
||||
) : (
|
||||
<DataGrid
|
||||
rows={(!isPasswordVisible ? setupData.servers : servers)!}
|
||||
rows={displayRows}
|
||||
columns={columns}
|
||||
autoPageSize
|
||||
sx={{ minWidth: "600px" }}
|
||||
@ -130,14 +161,12 @@ function Servers() {
|
||||
slots={{
|
||||
toolbar: () => (
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setupData.reloadServers()} // Refresh only active servers
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.serversLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
|
||||
</Button>
|
||||
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" color="primary" onClick={() => setupData.reloadServers()} sx={{ marginLeft: 1 }}>
|
||||
{setupData.serversLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarDensitySelector />
|
||||
<GridToolbarExport />
|
||||
@ -164,12 +193,12 @@ function Servers() {
|
||||
</Box>
|
||||
|
||||
{/* Server Edit Modal */}
|
||||
{selectedRow && (
|
||||
{open && (
|
||||
<ServerEdit
|
||||
open={open}
|
||||
server={selectedRow}
|
||||
onClose={() => setOpen(false)}
|
||||
onSave={handleUpdateRow}
|
||||
onSave={handleSaveRow}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import Target from '@/types/target';
|
||||
import TargetEdit from "@/components/modals/TargetEdit";
|
||||
@ -21,12 +23,13 @@ function Targets() {
|
||||
const columns: GridColDef<Target>[] = [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Actions",
|
||||
headerName: "",
|
||||
sortable: false,
|
||||
width: 60,
|
||||
renderCell: (params: GridRenderCellParams<Target>) => (
|
||||
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{ field: "id", headerName: "ID", width: 60 },
|
||||
@ -119,22 +122,12 @@ function Targets() {
|
||||
slots={{
|
||||
toolbar: () => (
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => handleNew()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setupData.reloadTargets()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
|
||||
</Button>
|
||||
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" color="primary" onClick={() => setupData.reloadTargets()} sx={{ marginLeft: 1 }}>
|
||||
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarDensitySelector />
|
||||
<GridToolbarExport />
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import Template from '@/types/template';
|
||||
import TemplateEdit from "@/components/modals/TemplateEdit";
|
||||
@ -18,12 +20,13 @@ function Templates() {
|
||||
const columns: GridColDef<Template>[] = [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Actions",
|
||||
headerName: "",
|
||||
sortable: false,
|
||||
width:60,
|
||||
renderCell: (params: GridRenderCellParams<Template>) => (
|
||||
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{ field: "id", headerName: "ID", width: 60 },
|
||||
@ -75,7 +78,7 @@ function Templates() {
|
||||
<Typography variant="body2">Description: {row.description}</Typography>
|
||||
<Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography>
|
||||
</CardContent>
|
||||
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@ -91,23 +94,13 @@ function Templates() {
|
||||
minWidth: "600px", maxWidth: getMaxWidth() }}
|
||||
slots={{
|
||||
toolbar: () => (
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => handleNew()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{"Add New"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setupData.reloadTemplates()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.templatesLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
|
||||
</Button>
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center", }}>
|
||||
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" color="primary" onClick={() => setupData.reloadTemplates()} sx={{ marginLeft: 1 }}>
|
||||
{setupData.templatesLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarDensitySelector />
|
||||
<GridToolbarExport />
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import TestEmailList from '@/types/testEmailList';
|
||||
import TestEmailListEdit from "@/components/modals/TestEmailListEdit";
|
||||
@ -18,12 +20,13 @@ function TestEmailLists() {
|
||||
const columns: GridColDef<TestEmailList>[] = [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Actions",
|
||||
headerName: "",
|
||||
sortable: false,
|
||||
width: 60,
|
||||
renderCell: (params: GridRenderCellParams<TestEmailList>) => (
|
||||
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{ field: "id", headerName: "ID", width: 60 },
|
||||
@ -70,7 +73,7 @@ function TestEmailLists() {
|
||||
<Typography variant="body2">ID: {row.id}</Typography>
|
||||
<Typography variant="body2">Email List: {row.emails}</Typography> {/*TODO: Format properly*/}
|
||||
</CardContent>
|
||||
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@ -86,22 +89,12 @@ function TestEmailLists() {
|
||||
slots={{
|
||||
toolbar: () => (
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => handleNew()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setupData.reloadTestEmailLists()}
|
||||
sx={{ marginRight: 2 }}
|
||||
>
|
||||
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
|
||||
</Button>
|
||||
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" color="primary" onClick={() => setupData.reloadTestEmailLists()} sx={{ marginLeft: 1 }}>
|
||||
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarDensitySelector />
|
||||
<GridToolbarExport />
|
||||
|
||||
@ -18,7 +18,7 @@ function UnsubscribeUrls() {
|
||||
const columns: GridColDef<UnsubscribeUrl>[] = [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Actions",
|
||||
headerName: "",
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams<UnsubscribeUrl>) => (
|
||||
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
.has-feedback .form-control {
|
||||
padding-right: 30.5px;
|
||||
}
|
||||
|
||||
.notesStyle {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 200px;
|
||||
min-width: 200px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th, .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td {
|
||||
padding: 3px;
|
||||
line-height: 1.42857143;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.dataTables_wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .sorting_icon {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.no-sort {
|
||||
padding-right: 3px !important;
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 20px auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drop-zone.dragover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.preview div {
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.preview img {
|
||||
max-width: 100px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preview button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: red;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
/* Custom CSS to restore old AdminLTE color classes in AdminLTE 4.x */
|
||||
|
||||
/* Orange Background */
|
||||
.bg-orange {
|
||||
background-color: #ff851b !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Blue Background */
|
||||
.bg-blue {
|
||||
background-color: #0073b7 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Green Background */
|
||||
.bg-green {
|
||||
background-color: #00a65a !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Red Background */
|
||||
.bg-red {
|
||||
background-color: #dd4b39 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Yellow Background */
|
||||
.bg-yellow {
|
||||
background-color: #f39c12 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Purple Background */
|
||||
.bg-purple {
|
||||
background-color: #605ca8 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Light Blue Background */
|
||||
.bg-light-blue {
|
||||
background-color: #3c8dbc !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Navy Background */
|
||||
.bg-navy {
|
||||
background-color: #001f3f !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Teal Background */
|
||||
.bg-teal {
|
||||
background-color: #39cccc !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Maroon Background */
|
||||
.bg-maroon {
|
||||
background-color: #d81b60 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Black Background */
|
||||
.bg-black {
|
||||
background-color: #111 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Olive Background */
|
||||
.bg-olive {
|
||||
background-color: #3d9970 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Lime Background */
|
||||
.bg-lime {
|
||||
background-color: #01ff70 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Fuchsia Background */
|
||||
.bg-fuchsia {
|
||||
background-color: #f012be !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Aqua Background */
|
||||
.bg-aqua {
|
||||
background-color: #00c0ef !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Gray Background */
|
||||
.bg-gray {
|
||||
background-color: #d2d6de !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Flat Button */
|
||||
.btn-flat {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export interface EmailDomain {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
username: string;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface Server {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
serverName: string;
|
||||
port: number;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface Target {
|
||||
id?: number;
|
||||
id: number;
|
||||
serverId: number;
|
||||
name: string;
|
||||
databaseName: string;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface UnsubscribeUrl {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user