create initial react project for flexitime v2 application.
includes .net webapi backend and ui test stubs
This commit is contained in:
parent
090152ff11
commit
005da7ce2b
@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
9
FlexitimeUI/Flexitime.Interfaces/ITokenFactory.cs
Normal file
9
FlexitimeUI/Flexitime.Interfaces/ITokenFactory.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Flexitime.Interfaces
|
||||
{
|
||||
public interface ITokenFactory
|
||||
{
|
||||
string Generate(int size = 32);
|
||||
}
|
||||
}
|
||||
16
FlexitimeUI/Flexitime.Objects/Application.cs
Normal file
16
FlexitimeUI/Flexitime.Objects/Application.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public class Application
|
||||
{
|
||||
public Application()
|
||||
{
|
||||
Permissions = new List<Permission>();
|
||||
}
|
||||
|
||||
public int Id { get; set; } //TODO make this GUID
|
||||
public string Name { get; set; }
|
||||
public List<Permission> Permissions { get; set; }
|
||||
}
|
||||
}
|
||||
20
FlexitimeUI/Flexitime.Objects/Flexitime.Objects.csproj
Normal file
20
FlexitimeUI/Flexitime.Objects/Flexitime.Objects.csproj
Normal file
@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="cd "$(ProjectDir)"
csharptojs $(ProjectDir)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
10
FlexitimeUI/Flexitime.Objects/Group.cs
Normal file
10
FlexitimeUI/Flexitime.Objects/Group.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public class Group
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int UserCount { get; set; }
|
||||
public bool IsAssociatedToUser { get; set; }
|
||||
}
|
||||
}
|
||||
27
FlexitimeUI/Flexitime.Objects/Identifier.cs
Normal file
27
FlexitimeUI/Flexitime.Objects/Identifier.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public class Identifier
|
||||
{
|
||||
public Identifier() { }
|
||||
public int Id { get; set; }
|
||||
public string UniqueId { get; set; }
|
||||
public bool IsAssociatedToUser { get; set; }
|
||||
public DateTime LastUsed { get; set; }
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var identObj = obj as Identifier;
|
||||
if (identObj == null) return false;
|
||||
|
||||
return identObj.Id == Id
|
||||
&& identObj.IsAssociatedToUser == IsAssociatedToUser
|
||||
&& identObj.UniqueId == UniqueId;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id.GetHashCode() ^ UniqueId.GetHashCode() ^ IsAssociatedToUser.GetHashCode() ^ LastUsed.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
FlexitimeUI/Flexitime.Objects/LoginRequest.cs
Normal file
14
FlexitimeUI/Flexitime.Objects/LoginRequest.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public class LoginRequest
|
||||
{
|
||||
public LoginRequest() { }
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
25
FlexitimeUI/Flexitime.Objects/LoginResponse.cs
Normal file
25
FlexitimeUI/Flexitime.Objects/LoginResponse.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Flexitime.Objects;
|
||||
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public class LoginResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
public LoginResponse() { }
|
||||
|
||||
public LoginResponse(User user, string token)
|
||||
{
|
||||
Id = user.UserId;
|
||||
FirstName = user.FirstName;
|
||||
LastName = user.LastName;
|
||||
Username = user.LoginId;
|
||||
Token = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
FlexitimeUI/Flexitime.Objects/Permission.cs
Normal file
15
FlexitimeUI/Flexitime.Objects/Permission.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public class Permission
|
||||
{
|
||||
public Permission()
|
||||
{
|
||||
Application = new Application();
|
||||
}
|
||||
|
||||
public Application Application { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Tag { get; set; }
|
||||
public int Id { get; set; } //TODO switch to guid
|
||||
}
|
||||
}
|
||||
43
FlexitimeUI/Flexitime.Objects/User.cs
Normal file
43
FlexitimeUI/Flexitime.Objects/User.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public User()
|
||||
{
|
||||
AssociatedIdentifiers = new List<Identifier>();
|
||||
Groups = new List<Group>();
|
||||
Permissions = new List<Permission>();
|
||||
FirstName = string.Empty;
|
||||
LastName = string.Empty;
|
||||
}
|
||||
|
||||
public string LoginId;
|
||||
public int UserId { get; set; } //TODO make this guid
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public float HoursPerWeek { get; set; }
|
||||
public bool IsContractor { get; set; }
|
||||
public int AssociatedIdentifierCount
|
||||
{
|
||||
get { return AssociatedIdentifiers.Count; }
|
||||
}
|
||||
public DateTime LastEventDateTime { get; set; }
|
||||
public List<Identifier> AssociatedIdentifiers { get; set; }
|
||||
public List<Group> Groups { get; set; }
|
||||
public UserState State { get; set; }
|
||||
/// <summary>
|
||||
/// Id of the Users Line Manager
|
||||
/// </summary>
|
||||
public int LineManagerId { get; set; }
|
||||
/// <summary>
|
||||
/// Ids of the users direct reports
|
||||
/// </summary>
|
||||
public int[] DirectReportIds { get; set; }
|
||||
public string Password { get; set; }
|
||||
public List<Permission> Permissions { get; set; }
|
||||
}
|
||||
}
|
||||
11
FlexitimeUI/Flexitime.Objects/UserState.cs
Normal file
11
FlexitimeUI/Flexitime.Objects/UserState.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Flexitime.Objects
|
||||
{
|
||||
public enum UserState
|
||||
{
|
||||
Unknown,
|
||||
In,
|
||||
Out,
|
||||
OutOfOffice,
|
||||
Remote
|
||||
}
|
||||
}
|
||||
12
FlexitimeUI/Flexitime.Objects/csharptojs.config.json
Normal file
12
FlexitimeUI/Flexitime.Objects/csharptojs.config.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"assemblies": [
|
||||
{
|
||||
"name": "Flexitime.Objects",
|
||||
"include": [ "Flexitime.Objects" ]
|
||||
}
|
||||
],
|
||||
"assembliesPath": "./bin/debug/netcoreapp2.2",
|
||||
"outputPath": "../flexitimeui/src/models",
|
||||
"noClean": false,
|
||||
"useNugetCacheResolver": true
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using Flexitime.Objects;
|
||||
using FlexitimeAPI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace FlexitimeAPI.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class AuthenticationController:ControllerBase
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
public AuthenticationController(IUserService userService)
|
||||
{
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult Authenticate(LoginRequest model)
|
||||
{
|
||||
var response = _userService.Authenticate(model);
|
||||
|
||||
if (response == null)
|
||||
return BadRequest(new { message = "Username or password is incorrect" });
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("logout")]
|
||||
public IActionResult Logout()
|
||||
{
|
||||
//should be able to get the id here and log a message of that user logging out..
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
FlexitimeUI/FlexitimeAPI/Controllers/UsersController.cs
Normal file
37
FlexitimeUI/FlexitimeAPI/Controllers/UsersController.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using Flexitime.Objects;
|
||||
using FlexitimeAPI.Helpers;
|
||||
using FlexitimeAPI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FlexitimeAPI.Controllers
|
||||
{
|
||||
[Authorize]//(Permissions=new []{"u.v"})]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public UsersController(ILogger<WeatherForecastController> logger, IUserService userService)
|
||||
{
|
||||
_logger = logger;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<User> Get()
|
||||
{
|
||||
return _userService.GetAll();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
public User Get(int id)
|
||||
{
|
||||
return _userService.GetById(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlexitimeAPI.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<WeatherForecast> Get()
|
||||
{
|
||||
var rng = new Random();
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateTime.Now.AddDays(index),
|
||||
TemperatureC = rng.Next(-20, 55),
|
||||
Summary = Summaries[rng.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
FlexitimeUI/FlexitimeAPI/FlexitimeAPI.csproj
Normal file
18
FlexitimeUI/FlexitimeAPI/FlexitimeAPI.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RandomNameGeneratorLibrary" Version="1.2.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flexitime.Interfaces\Flexitime.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\Flexitime.Objects\Flexitime.Objects.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
95
FlexitimeUI/FlexitimeAPI/Helpers/AuthorizeAttribute.cs
Normal file
95
FlexitimeUI/FlexitimeAPI/Helpers/AuthorizeAttribute.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Flexitime.Objects;
|
||||
using FlexitimeAPI.Models;
|
||||
using FlexitimeAPI.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace FlexitimeAPI.Helpers
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
|
||||
{
|
||||
public string[] Permissions { get; set; }
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
var user = (User)context.HttpContext.Items["User"];
|
||||
if (user == null)
|
||||
{
|
||||
// not logged in
|
||||
context.Result = new JsonResult(
|
||||
new {message = "Unauthorized"})
|
||||
{StatusCode = StatusCodes.Status401Unauthorized};
|
||||
}
|
||||
else if(Permissions.Any()
|
||||
&& !user.Permissions.Select(y=>y.Tag)
|
||||
.Intersect(Permissions)
|
||||
.Any()) //check we have permissions if they have been specified
|
||||
{
|
||||
context.Result =
|
||||
context.Result = new JsonResult(
|
||||
new { message = "Unauthorized" })
|
||||
{ StatusCode = StatusCodes.Status401Unauthorized };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class JwtMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly AppSettings _appSettings;
|
||||
|
||||
public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings)
|
||||
{
|
||||
_next = next;
|
||||
_appSettings = appSettings.Value;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context, IUserService userService)
|
||||
{
|
||||
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
|
||||
|
||||
if (token != null)
|
||||
AttachUserToContext(context, userService, token);
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private void AttachUserToContext(HttpContext context, IUserService userService, string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
|
||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
|
||||
ClockSkew = TimeSpan.Zero
|
||||
}, out SecurityToken validatedToken);
|
||||
|
||||
var jwtToken = (JwtSecurityToken)validatedToken;
|
||||
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
|
||||
|
||||
// attach user to context on successful jwt validation
|
||||
context.Items["User"] = userService.GetById(userId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// do nothing if jwt validation fails
|
||||
// user is not attached to context so request won't have access to secure routes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
FlexitimeUI/FlexitimeAPI/Helpers/TokenGenerator.cs
Normal file
19
FlexitimeUI/FlexitimeAPI/Helpers/TokenGenerator.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Flexitime.Interfaces;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace FlexitimeAPI.Helpers
|
||||
{
|
||||
public class TokenGenerator:ITokenFactory
|
||||
{
|
||||
public string Generate(int size = 32)
|
||||
{
|
||||
var randomNumber = new byte[size];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(randomNumber);
|
||||
return Convert.ToBase64String(randomNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
FlexitimeUI/FlexitimeAPI/Models/AppSettings.cs
Normal file
11
FlexitimeUI/FlexitimeAPI/Models/AppSettings.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace FlexitimeAPI.Models
|
||||
{
|
||||
public class AppSettings
|
||||
{
|
||||
public AppSettings()
|
||||
{
|
||||
Secret = "JiminiyCricket|{08EBD219-AABA-4C33-8312-AE93EECE77D2}";
|
||||
}
|
||||
public string Secret { get; set; }
|
||||
}
|
||||
}
|
||||
26
FlexitimeUI/FlexitimeAPI/Program.cs
Normal file
26
FlexitimeUI/FlexitimeAPI/Program.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlexitimeAPI
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
31
FlexitimeUI/FlexitimeAPI/Properties/launchSettings.json
Normal file
31
FlexitimeUI/FlexitimeAPI/Properties/launchSettings.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:46914",
|
||||
"sslPort": 44370
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"FlexitimeAPI": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
FlexitimeUI/FlexitimeAPI/Services/UserService.cs
Normal file
123
FlexitimeUI/FlexitimeAPI/Services/UserService.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Flexitime.Objects;
|
||||
using FlexitimeAPI.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using RandomNameGeneratorLibrary;
|
||||
|
||||
namespace FlexitimeAPI.Services
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
LoginResponse Authenticate(LoginRequest model);
|
||||
IEnumerable<User> GetAll();
|
||||
User GetById(int id);
|
||||
}
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private PersonNameGenerator _personGenerator;
|
||||
// users hardcoded for simplicity, store in a db with hashed passwords in production applications
|
||||
private List<User> _users;
|
||||
|
||||
private readonly AppSettings _appSettings;
|
||||
|
||||
public UserService(IOptions<AppSettings> appSettings)
|
||||
{
|
||||
_appSettings = appSettings.Value;
|
||||
_personGenerator = new PersonNameGenerator();
|
||||
var random = new Random();
|
||||
var vals = Enum.GetValues(typeof(UserState));
|
||||
|
||||
_users = Enumerable.Range(2, 6).Select(index =>
|
||||
{
|
||||
var first = _personGenerator.GenerateRandomFirstName();
|
||||
var last = _personGenerator.GenerateRandomLastName();
|
||||
|
||||
return new User
|
||||
{
|
||||
UserId = index,
|
||||
LoginId = $"{first}{last}",
|
||||
Password = "12345",
|
||||
FirstName = first,
|
||||
LastName = last,
|
||||
HoursPerWeek = 37,
|
||||
IsContractor = false,
|
||||
State = (UserState)vals.GetValue(random.Next(2, vals.Length))
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
//create default known admin user..
|
||||
_users.Add(new User
|
||||
{
|
||||
UserId = 1,
|
||||
LoginId = "admin",
|
||||
Password = "P@ssw0rd!",
|
||||
FirstName = "Admin",
|
||||
LastName = "User",
|
||||
HoursPerWeek = 37,
|
||||
IsContractor = false,
|
||||
State = UserState.In
|
||||
});
|
||||
}
|
||||
|
||||
public LoginResponse Authenticate(LoginRequest model)
|
||||
{
|
||||
var user = _users.SingleOrDefault(x => x.LoginId == model.Username && x.Password == model.Password);
|
||||
|
||||
// return null if user not found
|
||||
if (user == null) return null;
|
||||
|
||||
// authentication successful so generate jwt token
|
||||
var token = GenerateJwtToken(user);
|
||||
|
||||
return new LoginResponse(user, token);
|
||||
}
|
||||
|
||||
public IEnumerable<User> GetAll()
|
||||
{
|
||||
return _users;
|
||||
}
|
||||
|
||||
public User GetById(int id)
|
||||
{
|
||||
return _users.FirstOrDefault(x => x.UserId == id);
|
||||
}
|
||||
|
||||
// helper methods
|
||||
|
||||
private string GenerateJwtToken(User user)
|
||||
{
|
||||
// generate token that is valid for 7 days
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
|
||||
|
||||
List<Claim> claims = new List<Claim>()
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
//var jwt = new JwtSecurityToken(issuer:"FlexitimeUI",claims:claims);
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
|
||||
Subject = new ClaimsIdentity(new[] { new Claim("id", user.UserId.ToString()) }),
|
||||
Expires = DateTime.UtcNow.AddHours(2),
|
||||
Issuer = "FlexitimeUI",
|
||||
NotBefore = DateTime.UtcNow.AddSeconds(-5),
|
||||
IssuedAt = DateTime.UtcNow,
|
||||
//Claims = claims,
|
||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
//tokenHandler.
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
FlexitimeUI/FlexitimeAPI/Startup.cs
Normal file
61
FlexitimeUI/FlexitimeAPI/Startup.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.HttpsPolicy;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FlexitimeAPI.Helpers;
|
||||
|
||||
namespace FlexitimeAPI
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
|
||||
services.AddControllers();
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "FlexitimeAPI", Version = "v1" });
|
||||
});
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "FlexitimeAPI v1"));
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseMiddleware<JwtMiddleware>();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
15
FlexitimeUI/FlexitimeAPI/WeatherForecast.cs
Normal file
15
FlexitimeUI/FlexitimeAPI/WeatherForecast.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace FlexitimeAPI
|
||||
{
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string Summary { get; set; }
|
||||
}
|
||||
}
|
||||
9
FlexitimeUI/FlexitimeAPI/appsettings.Development.json
Normal file
9
FlexitimeUI/FlexitimeAPI/appsettings.Development.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
FlexitimeUI/FlexitimeAPI/appsettings.json
Normal file
10
FlexitimeUI/FlexitimeAPI/appsettings.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
49
FlexitimeUI/FlexitimeUI.sln
Normal file
49
FlexitimeUI/FlexitimeUI.sln
Normal file
@ -0,0 +1,49 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31005.135
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITests", "UITests\UITests.csproj", "{1EFA5843-5BBA-469F-96A9-2CC65067151D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flexitime.Objects", "Flexitime.Objects\Flexitime.Objects.csproj", "{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flexitime.Interfaces", "Flexitime.Interfaces\Flexitime.Interfaces.csproj", "{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexitimeAPI", "FlexitimeAPI\FlexitimeAPI.csproj", "{A9478B18-94DC-4E90-98BB-67A5343477D2}"
|
||||
EndProject
|
||||
Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "Flexitime.UI", "flexitimeui\Flexitime.UI.njsproj", "{9E4FD3D5-310E-4CE5-9049-F05241E4D464}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {23786E06-E83F-43AB-93EE-E0168FCF8E4A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
5
FlexitimeUI/FlexitimeUI.v3.ncrunchsolution.user
Normal file
5
FlexitimeUI/FlexitimeUI.v3.ncrunchsolution.user
Normal file
@ -0,0 +1,5 @@
|
||||
<SolutionConfiguration>
|
||||
<Settings>
|
||||
<CurrentEngineMode>Run all tests automatically [Global]</CurrentEngineMode>
|
||||
</Settings>
|
||||
</SolutionConfiguration>
|
||||
13
FlexitimeUI/UITests/Features/Calculator.feature
Normal file
13
FlexitimeUI/UITests/Features/Calculator.feature
Normal file
@ -0,0 +1,13 @@
|
||||
Feature: Calculator
|
||||

|
||||
Simple calculator for adding **two** numbers
|
||||
|
||||
Link to a feature: [Calculator](UITests/Features/Calculator.feature)
|
||||
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**
|
||||
|
||||
@mytag
|
||||
Scenario: Add two numbers
|
||||
Given the first number is 50
|
||||
And the second number is 70
|
||||
When the two numbers are added
|
||||
Then the result should be 120
|
||||
151
FlexitimeUI/UITests/Features/Calculator.feature.cs
generated
Normal file
151
FlexitimeUI/UITests/Features/Calculator.feature.cs
generated
Normal file
@ -0,0 +1,151 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by SpecFlow (https://www.specflow.org/).
|
||||
// SpecFlow Version:3.7.0.0
|
||||
// SpecFlow Generator Version:3.7.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
#region Designer generated code
|
||||
#pragma warning disable
|
||||
namespace UITests.Features
|
||||
{
|
||||
using TechTalk.SpecFlow;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.7.0.0")]
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public partial class CalculatorFeature : object, Xunit.IClassFixture<CalculatorFeature.FixtureData>, System.IDisposable
|
||||
{
|
||||
|
||||
private static TechTalk.SpecFlow.ITestRunner testRunner;
|
||||
|
||||
private string[] _featureTags = ((string[])(null));
|
||||
|
||||
private Xunit.Abstractions.ITestOutputHelper _testOutputHelper;
|
||||
|
||||
#line 1 "Calculator.feature"
|
||||
#line hidden
|
||||
|
||||
public CalculatorFeature(CalculatorFeature.FixtureData fixtureData, UITests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
this._testOutputHelper = testOutputHelper;
|
||||
this.TestInitialize();
|
||||
}
|
||||
|
||||
public static void FeatureSetup()
|
||||
{
|
||||
testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner();
|
||||
TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Calculator", @"
|
||||
Simple calculator for adding **two** numbers
|
||||
|
||||
Link to a feature: [Calculator](UITests/Features/Calculator.feature)
|
||||
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**", ProgrammingLanguage.CSharp, ((string[])(null)));
|
||||
testRunner.OnFeatureStart(featureInfo);
|
||||
}
|
||||
|
||||
public static void FeatureTearDown()
|
||||
{
|
||||
testRunner.OnFeatureEnd();
|
||||
testRunner = null;
|
||||
}
|
||||
|
||||
public virtual void TestInitialize()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void TestTearDown()
|
||||
{
|
||||
testRunner.OnScenarioEnd();
|
||||
}
|
||||
|
||||
public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo)
|
||||
{
|
||||
testRunner.OnScenarioInitialize(scenarioInfo);
|
||||
testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs<Xunit.Abstractions.ITestOutputHelper>(_testOutputHelper);
|
||||
}
|
||||
|
||||
public virtual void ScenarioStart()
|
||||
{
|
||||
testRunner.OnScenarioStart();
|
||||
}
|
||||
|
||||
public virtual void ScenarioCleanup()
|
||||
{
|
||||
testRunner.CollectScenarioErrors();
|
||||
}
|
||||
|
||||
void System.IDisposable.Dispose()
|
||||
{
|
||||
this.TestTearDown();
|
||||
}
|
||||
|
||||
[Xunit.SkippableFactAttribute(DisplayName="Add two numbers")]
|
||||
[Xunit.TraitAttribute("FeatureTitle", "Calculator")]
|
||||
[Xunit.TraitAttribute("Description", "Add two numbers")]
|
||||
[Xunit.TraitAttribute("Category", "mytag")]
|
||||
public virtual void AddTwoNumbers()
|
||||
{
|
||||
string[] tagsOfScenario = new string[] {
|
||||
"mytag"};
|
||||
System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
|
||||
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Add two numbers", null, tagsOfScenario, argumentsOfScenario, this._featureTags);
|
||||
#line 9
|
||||
this.ScenarioInitialize(scenarioInfo);
|
||||
#line hidden
|
||||
bool isScenarioIgnored = default(bool);
|
||||
bool isFeatureIgnored = default(bool);
|
||||
if ((tagsOfScenario != null))
|
||||
{
|
||||
isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
|
||||
}
|
||||
if ((this._featureTags != null))
|
||||
{
|
||||
isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
|
||||
}
|
||||
if ((isScenarioIgnored || isFeatureIgnored))
|
||||
{
|
||||
testRunner.SkipScenario();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ScenarioStart();
|
||||
#line 10
|
||||
testRunner.Given("the first number is 50", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
|
||||
#line hidden
|
||||
#line 11
|
||||
testRunner.And("the second number is 70", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
|
||||
#line hidden
|
||||
#line 12
|
||||
testRunner.When("the two numbers are added", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
|
||||
#line hidden
|
||||
#line 13
|
||||
testRunner.Then("the result should be 120", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
|
||||
#line hidden
|
||||
}
|
||||
this.ScenarioCleanup();
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.7.0.0")]
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class FixtureData : System.IDisposable
|
||||
{
|
||||
|
||||
public FixtureData()
|
||||
{
|
||||
CalculatorFeature.FeatureSetup();
|
||||
}
|
||||
|
||||
void System.IDisposable.Dispose()
|
||||
{
|
||||
CalculatorFeature.FeatureTearDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore
|
||||
#endregion
|
||||
58
FlexitimeUI/UITests/Steps/CalculatorStepDefinitions.cs
Normal file
58
FlexitimeUI/UITests/Steps/CalculatorStepDefinitions.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using TechTalk.SpecFlow;
|
||||
|
||||
namespace UITests.Steps
|
||||
{
|
||||
[Binding]
|
||||
public sealed class CalculatorStepDefinitions
|
||||
{
|
||||
|
||||
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
|
||||
|
||||
private readonly ScenarioContext _scenarioContext;
|
||||
|
||||
public CalculatorStepDefinitions(ScenarioContext scenarioContext)
|
||||
{
|
||||
_scenarioContext = scenarioContext;
|
||||
}
|
||||
|
||||
[Given("the first number is (.*)")]
|
||||
public void GivenTheFirstNumberIs(int number)
|
||||
{
|
||||
//TODO: implement arrange (precondition) logic
|
||||
// For storing and retrieving scenario-specific data see https://go.specflow.org/doc-sharingdata
|
||||
// To use the multiline text or the table argument of the scenario,
|
||||
// additional string/Table parameters can be defined on the step definition
|
||||
// method.
|
||||
|
||||
_scenarioContext.Pending();
|
||||
}
|
||||
|
||||
[Given("the second number is (.*)")]
|
||||
public void GivenTheSecondNumberIs(int number)
|
||||
{
|
||||
//TODO: implement arrange (precondition) logic
|
||||
// For storing and retrieving scenario-specific data see https://go.specflow.org/doc-sharingdata
|
||||
// To use the multiline text or the table argument of the scenario,
|
||||
// additional string/Table parameters can be defined on the step definition
|
||||
// method.
|
||||
|
||||
_scenarioContext.Pending();
|
||||
}
|
||||
|
||||
[When("the two numbers are added")]
|
||||
public void WhenTheTwoNumbersAreAdded()
|
||||
{
|
||||
//TODO: implement act (action) logic
|
||||
|
||||
_scenarioContext.Pending();
|
||||
}
|
||||
|
||||
[Then("the result should be (.*)")]
|
||||
public void ThenTheResultShouldBe(int result)
|
||||
{
|
||||
//TODO: implement assert (verification) logic
|
||||
|
||||
_scenarioContext.Pending();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
FlexitimeUI/UITests/UITests.csproj
Normal file
33
FlexitimeUI/UITests/UITests.csproj
Normal file
@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="SpecFlow.Plus.LivingDocPlugin" Version="3.7.10" />
|
||||
|
||||
|
||||
|
||||
|
||||
<PackageReference Include="SpecFlow.xUnit" Version="3.7.13" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Drivers\" />
|
||||
<Folder Include="Hooks\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
23
FlexitimeUI/flexitimeui/.gitignore
vendored
Normal file
23
FlexitimeUI/flexitimeui/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
195
FlexitimeUI/flexitimeui/Flexitime.UI.njsproj
Normal file
195
FlexitimeUI/flexitimeui/Flexitime.UI.njsproj
Normal file
@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{9e4fd3d5-310e-4ce5-9049-f05241e4d464}</ProjectGuid>
|
||||
<ProjectHome>.</ProjectHome>
|
||||
<ProjectView>ProjectFiles</ProjectView>
|
||||
<StartupFile>src\index.js</StartupFile>
|
||||
<WorkingDirectory>.</WorkingDirectory>
|
||||
<OutputPath>.</OutputPath>
|
||||
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">16.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
<LaunchUrl>http://localhost:3000</LaunchUrl>
|
||||
<NodejsPort>3000</NodejsPort>
|
||||
<StartWebBrowser>True</StartWebBrowser>
|
||||
<SaveNodeJsSettingsInProjectFile>True</SaveNodeJsSettingsInProjectFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
|
||||
<ItemGroup>
|
||||
<Content Include="craco.config.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\api\apiUtils.js" />
|
||||
<Content Include="src\api\authorApi.js" />
|
||||
<Content Include="src\api\groupsApi.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\api\userApi.js" />
|
||||
<Content Include="src\components\common\ImagePicker.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\common\NumberInput.jsx" />
|
||||
<Content Include="src\components\common\SelectInput.jsx" />
|
||||
<Content Include="src\components\common\Spinner.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\common\TextInput.jsx" />
|
||||
<Content Include="src\components\common\UserStateIndicator.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\groups\GroupList.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\groups\GroupsPage.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\users\ManageUserPage.jsx" />
|
||||
<Content Include="src\components\users\UserForm.jsx" />
|
||||
<Content Include="src\components\users\UserGrid.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\users\UserList.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\models\Application.js" />
|
||||
<Content Include="src\models\Group.js" />
|
||||
<Content Include="src\models\Identifier.js" />
|
||||
<Content Include="src\models\LoginRequest.js" />
|
||||
<Content Include="src\models\LoginResponse.js" />
|
||||
<Content Include="src\models\Permission.js" />
|
||||
<Content Include="src\models\User.js" />
|
||||
<Content Include="package-lock.json" />
|
||||
<Content Include="package.json" />
|
||||
<Content Include="README.md" />
|
||||
<Content Include="public\robots.txt" />
|
||||
<Content Include="public\index.html" />
|
||||
<Content Include="public\logo192.png" />
|
||||
<Content Include="public\logo512.png" />
|
||||
<Content Include="public\favicon.ico" />
|
||||
<Content Include="public\manifest.json" />
|
||||
<Content Include="src\App.css" />
|
||||
<Content Include="src\models\userState.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\redux\actions\actionTypes.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\redux\actions\apiStatusActions.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\redux\actions\groupActions.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\redux\actions\userActions.js" />
|
||||
<Content Include="src\redux\configureStore.js" />
|
||||
<Content Include="src\redux\reducers\apiStatusReducer.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\redux\reducers\index.js" />
|
||||
<Content Include="src\redux\reducers\groupsReducer.js" />
|
||||
<Content Include="src\redux\reducers\initialState.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\redux\reducers\userReducer.js" />
|
||||
<Content Include="src\components\PageNotFound_404.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\about\AboutPage.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\common\Header.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\home\HomePage.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\components\users\UserPage.jsx">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\index.css" />
|
||||
<Content Include="src\logo.svg" />
|
||||
<Content Include="src\components\App.jsx" />
|
||||
<Content Include="src\App.test.js" />
|
||||
<Content Include="src\index.js" />
|
||||
<Content Include="src\reportWebVitals.js" />
|
||||
<Content Include="src\services\userService.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\setupTests.js" />
|
||||
<Content Include="src\styles\Spinner.css">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\styles\StateIndicator.css">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="src\styles\toggleSlider.css">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="tools\apiServer.js" />
|
||||
<Content Include="tools\createMockDb.js" />
|
||||
<Content Include="tools\mockData.js" />
|
||||
<Content Include="__webpack.config.dev.js" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="public" />
|
||||
<Folder Include="src" />
|
||||
<Folder Include="src\api\" />
|
||||
<Folder Include="src\components\groups\" />
|
||||
<Folder Include="src\models\" />
|
||||
<Folder Include="src\services\" />
|
||||
<Folder Include="src\styles\" />
|
||||
<Folder Include="src\redux\" />
|
||||
<Folder Include="src\redux\actions\" />
|
||||
<Folder Include="src\redux\reducers\" />
|
||||
<Folder Include="src\components\" />
|
||||
<Folder Include="src\components\home\" />
|
||||
<Folder Include="src\components\about\" />
|
||||
<Folder Include="src\components\common\" />
|
||||
<Folder Include="src\components\users\" />
|
||||
<Folder Include="tools\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>0</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:48022/</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>True</UseCustomServer>
|
||||
<CustomServerUrl>http://localhost:1337</CustomServerUrl>
|
||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
|
||||
<WebProjectProperties>
|
||||
<StartPageUrl>
|
||||
</StartPageUrl>
|
||||
<StartAction>CurrentPage</StartAction>
|
||||
<AspNetDebugging>True</AspNetDebugging>
|
||||
<SilverlightDebugging>False</SilverlightDebugging>
|
||||
<NativeDebugging>False</NativeDebugging>
|
||||
<SQLDebugging>False</SQLDebugging>
|
||||
<ExternalProgram>
|
||||
</ExternalProgram>
|
||||
<StartExternalURL>
|
||||
</StartExternalURL>
|
||||
<StartCmdLineArguments>
|
||||
</StartCmdLineArguments>
|
||||
<StartWorkingDirectory>
|
||||
</StartWorkingDirectory>
|
||||
<EnableENC>False</EnableENC>
|
||||
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
6
FlexitimeUI/flexitimeui/Flexitime.UI.njsproj.user
Normal file
6
FlexitimeUI/flexitimeui/Flexitime.UI.njsproj.user
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
70
FlexitimeUI/flexitimeui/README.md
Normal file
70
FlexitimeUI/flexitimeui/README.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
47
FlexitimeUI/flexitimeui/__webpack.config.dev.js
Normal file
47
FlexitimeUI/flexitimeui/__webpack.config.dev.js
Normal file
@ -0,0 +1,47 @@
|
||||
const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
process.env.NODE_ENV = "development";
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
target: "web",
|
||||
devtool: "cheap-module-source-map",
|
||||
entry: "./src/index",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "build"),
|
||||
publicPath: "/",
|
||||
filename: "bundle.js"
|
||||
},
|
||||
devServer: {
|
||||
stats: "minimal",
|
||||
overlay: true,
|
||||
historyApiFallback: true,
|
||||
disableHostCheck: true,
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
https: false
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.API_URL": JSON.stringify("http://localhost:3001")
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
//template: "src/index.html",
|
||||
favicon: "src/favicon.ico"
|
||||
})
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: ["babel-loader", "eslint-loader"]
|
||||
},
|
||||
{
|
||||
test: /(\.css)$/,
|
||||
use: ["style-loader", "css-loader"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
11
FlexitimeUI/flexitimeui/craco.config.js
Normal file
11
FlexitimeUI/flexitimeui/craco.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
const webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.API_URL": JSON.stringify("http://localhost:3001")
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
16951
FlexitimeUI/flexitimeui/package-lock.json
generated
Normal file
16951
FlexitimeUI/flexitimeui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
FlexitimeUI/flexitimeui/package.json
Normal file
53
FlexitimeUI/flexitimeui/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "flexitimeui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"babel": "^6.23.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-toastify": "^7.0.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux-immutable-state-invariant": "^2.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"web-vitals": "^1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "run-p start:dev start:api",
|
||||
"start:dev": "craco start",
|
||||
"prestart:api": "node tools/createMockDb.js",
|
||||
"start:api": "node tools/apiServer.js",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"craco": "0.0.3",
|
||||
"json-server": "^0.16.3"
|
||||
}
|
||||
}
|
||||
BIN
FlexitimeUI/flexitimeui/public/favicon.ico
Normal file
BIN
FlexitimeUI/flexitimeui/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
FlexitimeUI/flexitimeui/public/index.html
Normal file
43
FlexitimeUI/flexitimeui/public/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
FlexitimeUI/flexitimeui/public/logo192.png
Normal file
BIN
FlexitimeUI/flexitimeui/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
FlexitimeUI/flexitimeui/public/logo512.png
Normal file
BIN
FlexitimeUI/flexitimeui/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
FlexitimeUI/flexitimeui/public/manifest.json
Normal file
25
FlexitimeUI/flexitimeui/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
FlexitimeUI/flexitimeui/public/robots.txt
Normal file
3
FlexitimeUI/flexitimeui/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
FlexitimeUI/flexitimeui/src/App.css
Normal file
38
FlexitimeUI/flexitimeui/src/App.css
Normal file
@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
8
FlexitimeUI/flexitimeui/src/App.test.js
Normal file
8
FlexitimeUI/flexitimeui/src/App.test.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
17
FlexitimeUI/flexitimeui/src/api/apiUtils.js
Normal file
17
FlexitimeUI/flexitimeui/src/api/apiUtils.js
Normal file
@ -0,0 +1,17 @@
|
||||
export async function handleResponse(response) {
|
||||
if (response.ok) return response.json();
|
||||
if (response.status === 400) {
|
||||
// So, a server-side validation error occurred.
|
||||
// Server side validation returns a string error message, so parse as text instead of json.
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
throw new Error("Network response was not ok.");
|
||||
}
|
||||
|
||||
// In a real app, would likely call an error logging service.
|
||||
export function handleError(error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("API call failed. " + error);
|
||||
throw error;
|
||||
}
|
||||
8
FlexitimeUI/flexitimeui/src/api/authorApi.js
Normal file
8
FlexitimeUI/flexitimeui/src/api/authorApi.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { handleResponse, handleError } from "./apiUtils";
|
||||
const baseUrl = process.env.API_URL + "/authors/";
|
||||
|
||||
export function getAuthors() {
|
||||
return fetch(baseUrl)
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
30
FlexitimeUI/flexitimeui/src/api/groupsApi.js
Normal file
30
FlexitimeUI/flexitimeui/src/api/groupsApi.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { handleResponse, handleError } from "./apiUtils";
|
||||
const baseUrl = process.env.API_URL + "/groups/";
|
||||
|
||||
export function getGroups() {
|
||||
return fetch(baseUrl)
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
export function getGroup(id) {
|
||||
return fetch(baseUrl + id)
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
export function saveGroup(group) {
|
||||
return fetch(baseUrl + (group.id || ""),
|
||||
{ method: group.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists.
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(group)
|
||||
})
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
export function deleteGroup(groupId) {
|
||||
return fetch(baseUrl + groupId, { method: "DELETE" })
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
30
FlexitimeUI/flexitimeui/src/api/userApi.js
Normal file
30
FlexitimeUI/flexitimeui/src/api/userApi.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { handleResponse, handleError } from "./apiUtils";
|
||||
const baseUrl = process.env.API_URL + "/users/";
|
||||
|
||||
export function getUsers() {
|
||||
return fetch(baseUrl)
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
export function getUser(id) {
|
||||
return fetch(baseUrl + id)
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
export function saveUser(user) {
|
||||
return fetch(baseUrl + (user.id || ""), {
|
||||
method: user.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists.
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(user)
|
||||
})
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
export function deleteUser(id) {
|
||||
return fetch(baseUrl + id, { method: "DELETE" })
|
||||
.then(handleResponse)
|
||||
.catch(handleError);
|
||||
}
|
||||
34
FlexitimeUI/flexitimeui/src/components/App.jsx
Normal file
34
FlexitimeUI/flexitimeui/src/components/App.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import HomePage from "./home/HomePage";
|
||||
import AboutPage from "./about/AboutPage";
|
||||
import Header from "./common/Header";
|
||||
import PageNotFound_404 from "./PageNotFound_404";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
import "./../App.css";
|
||||
import UserPage from "./users/UserPage";
|
||||
import GroupsPage from "./groups/GroupsPage";
|
||||
import ManageUserPage from "./users/ManageUserPage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<Header />
|
||||
<Switch>
|
||||
<Route exact path="/" component={HomePage} />
|
||||
<Route path="/about" component={AboutPage} />
|
||||
<Route path="/users" component={UserPage} />
|
||||
<Route path="/user/:id" component={ManageUserPage} />
|
||||
<Route path="/user" component={ManageUserPage} />
|
||||
<Route path="/groups" component={GroupsPage} />
|
||||
|
||||
<Route component={PageNotFound_404} />
|
||||
</Switch>
|
||||
<ToastContainer autoClose={3000} hideProgressBar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const PageNotFound_404 = () => <h1>Oops! Page Not Found. </h1>
|
||||
|
||||
export default PageNotFound_404;
|
||||
10
FlexitimeUI/flexitimeui/src/components/about/AboutPage.js
Normal file
10
FlexitimeUI/flexitimeui/src/components/about/AboutPage.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
const AboutPage = () => (
|
||||
<div className="jumbotron">
|
||||
<h2>About</h2>
|
||||
<p>This app uses React, Redux, React Router and many other helpful libraries.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AboutPage;
|
||||
69
FlexitimeUI/flexitimeui/src/components/common/Header.jsx
Normal file
69
FlexitimeUI/flexitimeui/src/components/common/Header.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { withRouter } from "react-router";
|
||||
|
||||
function Header(props) {
|
||||
|
||||
|
||||
function getNavLinkClass(path) {
|
||||
return props.location.pathname === path
|
||||
? "nav-item active"
|
||||
: "nav-item";
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<span className="navbar-brand mb-0 h1">Flexitime Tool</span>
|
||||
<div className="collapse navbar-collapse">
|
||||
<ul className="navbar-nav mr-auto">
|
||||
<li className={getNavLinkClass("/")}>
|
||||
<NavLink to="/" className="nav-link" exact>
|
||||
Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className={getNavLinkClass("/users")}>
|
||||
<NavLink to="/users" className="nav-link">
|
||||
User List
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className={getNavLinkClass("/about")}>
|
||||
<NavLink to="/about" className="nav-link" exact>
|
||||
About
|
||||
</NavLink>
|
||||
</li>
|
||||
{/*<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Dropdown
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
</div>
|
||||
</li>*/}
|
||||
</ul>
|
||||
<div className="my-2">
|
||||
<button className="btn btn-outline-light my-2 my-sm-0" type="button">Account</button>
|
||||
</div>
|
||||
{/*<ul>*/}
|
||||
{/* <li className={getNavLinkClass("/about") + " float-right"}>*/}
|
||||
{/* <NavLink to="/about" className="nav-link" exact>*/}
|
||||
{/* About*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* </li>*/}
|
||||
{/* </ul>*/}
|
||||
</div>
|
||||
|
||||
{/*{" | "}*/}
|
||||
{/*<NavLink to="/users" activeStyle={activeStyle}>User List</NavLink>*/}
|
||||
{/*{" | "}*/}
|
||||
{/*<NavLink to="/groups" activeStyle={activeStyle}>Group List</NavLink>*/}
|
||||
{/*{" | "}*/}
|
||||
{/*<NavLink to="/about" activeStyle={activeStyle} exact>About</NavLink>*/}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withRouter(Header);
|
||||
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ImagePicker = ({ name, label, onChange, placeholder, value, error }) => {
|
||||
let wrapperClass = "form-group";
|
||||
if (error && error.length > 0) {
|
||||
wrapperClass += " " + "has-error";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClass}>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
<div className="field">
|
||||
<input
|
||||
type="image"
|
||||
name={name}
|
||||
className="form-control"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ImagePicker.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
error: PropTypes.string
|
||||
};
|
||||
|
||||
export default ImagePicker;
|
||||
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const NumberInput = ({ name, label, onChange, placeholder, value, error }) => {
|
||||
let wrapperClass = "form-group";
|
||||
if (error && error.length > 0) {
|
||||
wrapperClass += " has-error";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClass}>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
<div className="field">
|
||||
<input
|
||||
type="number"
|
||||
name={name}
|
||||
className="form-control"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NumberInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.number,
|
||||
value: PropTypes.number,
|
||||
error: PropTypes.string
|
||||
};
|
||||
|
||||
export default NumberInput;
|
||||
@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SelectInput = ({
|
||||
name,
|
||||
label,
|
||||
onChange,
|
||||
defaultOption,
|
||||
value,
|
||||
error,
|
||||
options
|
||||
}) => {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
<div className="field">
|
||||
{/* Note, value is set here rather than on the option - docs: https://facebook.github.io/react/docs/forms.html */}
|
||||
<select
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="form-control"
|
||||
>
|
||||
<option value="">{defaultOption}</option>
|
||||
{options.map(option => {
|
||||
return (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SelectInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultOption: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
error: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
export default SelectInput;
|
||||
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
import "../../styles/Spinner.css";
|
||||
|
||||
const Spinner = () => {
|
||||
return <div className="loader"> Loading...</div>;
|
||||
}
|
||||
export default Spinner;
|
||||
40
FlexitimeUI/flexitimeui/src/components/common/TextInput.jsx
Normal file
40
FlexitimeUI/flexitimeui/src/components/common/TextInput.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const TextInput = ({ name, label, onChange, placeholder, value, error }) => {
|
||||
let wrapperClass = "form-group";
|
||||
if (error && error.length > 0) {
|
||||
wrapperClass += " has-error";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClass}>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
<div className="field">
|
||||
<input
|
||||
type="text"
|
||||
name={name}
|
||||
className="form-control"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
TextInput.defaultProps = {
|
||||
value: ""
|
||||
};
|
||||
|
||||
TextInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
error: PropTypes.string
|
||||
};
|
||||
|
||||
export default TextInput;
|
||||
@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import userState from "../../models/userState"
|
||||
import PropTypes from "prop-types";
|
||||
import "../../styles/StateIndicator.css";
|
||||
|
||||
class UserState extends React.Component {
|
||||
convertStateToColor(state) {
|
||||
let baseClass = "badge ";
|
||||
switch (state) {
|
||||
case userState.IN:
|
||||
return baseClass + "badge-success";
|
||||
case userState.OUT:
|
||||
return baseClass + "badge-danger";
|
||||
case userState.OUT_OF_OFFICE:
|
||||
return baseClass + "badge-warning";
|
||||
case userState.REMOTE:
|
||||
return baseClass + "badge-info";
|
||||
case userState.UNKNOWN:
|
||||
default:
|
||||
return baseClass + "badge-dark";
|
||||
}
|
||||
}
|
||||
render() {
|
||||
let stateClass = this.convertStateToColor(this.props.state);
|
||||
let classes = stateClass + " stateIndicator p-2";
|
||||
return (
|
||||
<span className={classes}>{this.props.state}</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserState.defaultProps = {
|
||||
state: userState.UNKNOWN
|
||||
};
|
||||
|
||||
UserState.propTypes = {
|
||||
state: PropTypes.string
|
||||
};
|
||||
|
||||
export default UserState;
|
||||
37
FlexitimeUI/flexitimeui/src/components/groups/GroupList.jsx
Normal file
37
FlexitimeUI/flexitimeui/src/components/groups/GroupList.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
class GroupList extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-center">Name</th>
|
||||
<th className="text-center">User Count</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.groups.map(group => {
|
||||
return (
|
||||
<tr key={group.id}>
|
||||
<td className="text-center">{group.name}</td>
|
||||
<td className="text-center">{group.userCount}</td>
|
||||
<td className="text-center">
|
||||
<Link to={"/groups/" + group.id}>View</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
GroupList.propTypes = {
|
||||
groups: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default GroupList;
|
||||
42
FlexitimeUI/flexitimeui/src/components/groups/GroupsPage.jsx
Normal file
42
FlexitimeUI/flexitimeui/src/components/groups/GroupsPage.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as groupActions from "../../redux/actions/groupActions";
|
||||
import Group from "../../models/Group";
|
||||
import PropTypes from "prop-types";
|
||||
import { bindActionCreators } from "redux";
|
||||
import GroupList from "./GroupList";
|
||||
|
||||
class GroupsPage extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.actions.loadGroups().catch(error => {
|
||||
alert("Loading groups failed " + error);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h2>Groups</h2>
|
||||
<GroupList groups={this.props.groups} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupsPage.propTypes = {
|
||||
groups: PropTypes.array.isRequired,
|
||||
actions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
groups: state.groups
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(groupActions, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GroupsPage);
|
||||
14
FlexitimeUI/flexitimeui/src/components/home/HomePage.js
Normal file
14
FlexitimeUI/flexitimeui/src/components/home/HomePage.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const HomePage = () => (
|
||||
<div className="jumbotron">
|
||||
<h1>PluralSight Administration</h1>
|
||||
<p>React, Redux and React Router for ultra-responsive web-pages</p>
|
||||
<Link to="about" className="btn btn-primary btn-lg">
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default HomePage;
|
||||
@ -0,0 +1,99 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { loadUsers, saveUser } from "../../redux/actions/userActions";
|
||||
import PropTypes from "prop-types";
|
||||
import UserForm from "./UserForm";
|
||||
import { User } from "../../models/User";
|
||||
import Spinner from "../common/Spinner";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
function ManagerUserPage({ users, loadUsers, saveUser, history, ...props }) {
|
||||
const [user, setUser] = useState({ ...props.user });
|
||||
const [errors, setErrors] = useState({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (users.length === 0) {
|
||||
loadUsers().catch(error => {
|
||||
alert("Loading users failed " + error);
|
||||
});
|
||||
} else {
|
||||
setUser({ ...props.user });
|
||||
}
|
||||
},
|
||||
[props.user]
|
||||
);
|
||||
|
||||
function formIsValid() {
|
||||
const { firstName, lastName, hoursPerWeek } = user;
|
||||
const errors = {};
|
||||
if (!firstName) errors.firstName = "First Name Is Required";
|
||||
if (!lastName) errors.lastName = "Last Name is Required";
|
||||
if (!hoursPerWeek) errors.hoursPerWeek = "Hours Per Week is Required";
|
||||
setErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
function onChangeHandler(event) {
|
||||
const { name, value } = event.target;
|
||||
setUser(prevUser => ({
|
||||
...prevUser,
|
||||
[name]: name === "hoursPerWeek" ? parseInt(value, 10) : value
|
||||
}));
|
||||
}
|
||||
|
||||
function saveHandler(event) {
|
||||
event.preventDefault();
|
||||
if (!formIsValid()) return;
|
||||
setSaving(true);
|
||||
saveUser(user)
|
||||
.then(() => {
|
||||
toast.success("User Saved");
|
||||
history.push("/users");
|
||||
})
|
||||
.catch(error => {
|
||||
setSaving(false);
|
||||
setErrors({ onSave: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
return users.length === 0 ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<UserForm
|
||||
user={user}
|
||||
errors={errors}
|
||||
onChange={onChangeHandler}
|
||||
onSave={saveHandler}
|
||||
saving={saving}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ManagerUserPage.propTypes = {
|
||||
users: PropTypes.array.isRequired,
|
||||
loadUsers: PropTypes.func.isRequired,
|
||||
saveUser: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
export function getUserById(users, id) {
|
||||
return users.find(usr => usr.id === id) || null;
|
||||
}
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const id = parseInt(ownProps.match.params.id);
|
||||
const user =
|
||||
id && state.users.length > 0 ? getUserById(state.users, id) : User;
|
||||
return {
|
||||
user: user,
|
||||
users: state.users
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadUsers,
|
||||
saveUser
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ManagerUserPage);
|
||||
62
FlexitimeUI/flexitimeui/src/components/users/UserForm.jsx
Normal file
62
FlexitimeUI/flexitimeui/src/components/users/UserForm.jsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import TextInput from "../common/TextInput";
|
||||
import NumberInput from "../common/NumberInput";
|
||||
|
||||
const UserForm = ({
|
||||
user,
|
||||
onSave,
|
||||
onChange,
|
||||
saving = false,
|
||||
errors = {}
|
||||
}) => {
|
||||
return (
|
||||
<form onSubmit={onSave}>
|
||||
<h2>{user.id ? "Edit" : "Add"} User</h2>
|
||||
{errors.onSave && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{errors.onSave}
|
||||
</div>
|
||||
)}
|
||||
<TextInput
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
value={user.firstName}
|
||||
onChange={onChange}
|
||||
error={errors.firstName}
|
||||
/>
|
||||
<TextInput
|
||||
name="lastName"
|
||||
label="Last Name"
|
||||
value={user.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
{/* TODO: datetime input */}
|
||||
{/* TODO: image upload */}
|
||||
{/* TODO: wizard? */}
|
||||
|
||||
<NumberInput
|
||||
name="hoursPerWeek"
|
||||
label="Contracted Hours Per Week"
|
||||
value={user.hoursPerWeek}
|
||||
onChange={onChange}
|
||||
error={errors.hoursPerWeek}
|
||||
/>
|
||||
|
||||
<button type="submit" disabled={saving} className="btn btn-primary">
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
UserForm.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
saving: PropTypes.bool
|
||||
};
|
||||
|
||||
export default UserForm;
|
||||
32
FlexitimeUI/flexitimeui/src/components/users/UserGrid.jsx
Normal file
32
FlexitimeUI/flexitimeui/src/components/users/UserGrid.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router-dom";
|
||||
import UserState from "../common/UserStateIndicator";
|
||||
|
||||
class UserGrid extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="row justify-content-start">
|
||||
{this.props.users.map(user => {
|
||||
return (
|
||||
<div key={user.id} style={{height:120+'px'}} className="align-content-center col-2 border rounded m-1 p-2">
|
||||
<h4 className="h-50 m-1">
|
||||
{user.firstName} {user.lastName}
|
||||
</h4>
|
||||
<div className="h-25 m-2">
|
||||
<UserState state={user.state} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
//maybe justify..-around?{/*className="display-4"text-center */}
|
||||
);
|
||||
}
|
||||
}
|
||||
UserGrid.propTypes = {
|
||||
users: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default UserGrid;
|
||||
53
FlexitimeUI/flexitimeui/src/components/users/UserList.jsx
Normal file
53
FlexitimeUI/flexitimeui/src/components/users/UserList.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router-dom";
|
||||
import UserState from "../common/UserStateIndicator";
|
||||
|
||||
class UserList extends React.Component {
|
||||
render() {
|
||||
const { users, onDeleteClick } = this.props;
|
||||
return (
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-center">First Name</th>
|
||||
<th className="text-center">Last Name</th>
|
||||
<th className="text-center" width="120px;">
|
||||
State
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map(user => {
|
||||
return (
|
||||
<tr key={user.id}>
|
||||
<td className="text-center">{user.firstName}</td>
|
||||
<td className="text-center">{user.lastName}</td>
|
||||
<td>
|
||||
<UserState state={user.state} />
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<Link className="btn btn-info mr-1" to={"/user/" + user.id}>Edit</Link>
|
||||
|
||||
<button
|
||||
className="btn btn-outline-danger ml-1"
|
||||
onClick={() => onDeleteClick(user)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
UserList.propTypes = {
|
||||
users: PropTypes.array.isRequired,
|
||||
onDeleteUser: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default UserList;
|
||||
93
FlexitimeUI/flexitimeui/src/components/users/UserPage.jsx
Normal file
93
FlexitimeUI/flexitimeui/src/components/users/UserPage.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as userActions from "../../redux/actions/userActions";
|
||||
import User from "../../models/User";
|
||||
import PropTypes from "prop-types";
|
||||
import { bindActionCreators } from "redux";
|
||||
import UserList from "./UserList";
|
||||
import UserGrid from "./UserGrid";
|
||||
import { Link, Redirect } from "react-router-dom";
|
||||
import "../../styles/toggleSlider.css";
|
||||
import Spinner from "../common/Spinner";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
class UserPage extends React.Component {
|
||||
state = {
|
||||
viewStyle: false,
|
||||
redirectToAddUserPage: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.users.length === 0) {
|
||||
this.props.actions.loadUsers().catch(error => {
|
||||
alert("Loading users failed " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
viewStyleChanged = () => {
|
||||
this.setState({ viewStyle: !this.state.viewStyle });
|
||||
};
|
||||
|
||||
handleDeleteUser = async user => {
|
||||
toast.success("User " + user.firstName + " deleted");
|
||||
try {
|
||||
await this.props.actions.deleteUser(user);
|
||||
} catch (error) {
|
||||
toast.error("Delete of user: "+user.firstName+" failed. " + error.message, { autoClose: false });
|
||||
}
|
||||
};
|
||||
render() {
|
||||
let isGrid = this.state.viewStyle;
|
||||
let view;
|
||||
if (isGrid) {
|
||||
view = <UserGrid users={this.props.users} />;
|
||||
} else {
|
||||
view = <UserList users={this.props.users} onDeleteClick={this.handleDeleteUser} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{this.props.loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Link to={"/user"} className="btn btn-dark m-1">
|
||||
Add User
|
||||
</Link>
|
||||
<label className="switch">
|
||||
<input
|
||||
ref="viewStyle"
|
||||
type="checkbox"
|
||||
defaultChecked="{isGrid}"
|
||||
onChange={this.viewStyleChanged}
|
||||
/>
|
||||
<span className="slider round" />
|
||||
</label>
|
||||
<h2>Users</h2>
|
||||
{view}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserPage.propTypes = {
|
||||
users: PropTypes.array.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
loading: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
users: state.users,
|
||||
loading: state.apiCallsInProgress > 0
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(userActions, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UserPage);
|
||||
13
FlexitimeUI/flexitimeui/src/index.css
Normal file
13
FlexitimeUI/flexitimeui/src/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
26
FlexitimeUI/flexitimeui/src/index.js
Normal file
26
FlexitimeUI/flexitimeui/src/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import './index.css';
|
||||
import App from './components/App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import './App.css';
|
||||
import configureStore from './redux/configureStore';
|
||||
import { Provider as ReduxProvider } from "react-redux";
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
render(
|
||||
<ReduxProvider store={store} >
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</ReduxProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
1
FlexitimeUI/flexitimeui/src/logo.svg
Normal file
1
FlexitimeUI/flexitimeui/src/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
9
FlexitimeUI/flexitimeui/src/models/Application.js
Normal file
9
FlexitimeUI/flexitimeui/src/models/Application.js
Normal file
@ -0,0 +1,9 @@
|
||||
//CSharpToJs: auto
|
||||
class Application {
|
||||
constructor() {
|
||||
this.id = 0;
|
||||
this.name = null;
|
||||
this.permissions = [];
|
||||
}
|
||||
}
|
||||
export default Application;
|
||||
10
FlexitimeUI/flexitimeui/src/models/Group.js
Normal file
10
FlexitimeUI/flexitimeui/src/models/Group.js
Normal file
@ -0,0 +1,10 @@
|
||||
//CSharpToJs: auto
|
||||
class Group {
|
||||
constructor() {
|
||||
this.id = 0;
|
||||
this.name = null;
|
||||
this.userCount = 0;
|
||||
this.isAssociatedToUser = false;
|
||||
}
|
||||
}
|
||||
export default Group;
|
||||
10
FlexitimeUI/flexitimeui/src/models/Identifier.js
Normal file
10
FlexitimeUI/flexitimeui/src/models/Identifier.js
Normal file
@ -0,0 +1,10 @@
|
||||
//CSharpToJs: auto
|
||||
class Identifier {
|
||||
constructor() {
|
||||
this.id = 0;
|
||||
this.uniqueId = null;
|
||||
this.isAssociatedToUser = false;
|
||||
this.lastUsed = "0001-01-01T00:00:00";
|
||||
}
|
||||
}
|
||||
export default Identifier;
|
||||
8
FlexitimeUI/flexitimeui/src/models/LoginRequest.js
Normal file
8
FlexitimeUI/flexitimeui/src/models/LoginRequest.js
Normal file
@ -0,0 +1,8 @@
|
||||
//CSharpToJs: auto
|
||||
class LoginRequest {
|
||||
constructor() {
|
||||
this.username = null;
|
||||
this.password = null;
|
||||
}
|
||||
}
|
||||
export default LoginRequest;
|
||||
12
FlexitimeUI/flexitimeui/src/models/LoginResponse.js
Normal file
12
FlexitimeUI/flexitimeui/src/models/LoginResponse.js
Normal file
@ -0,0 +1,12 @@
|
||||
//CSharpToJs: auto
|
||||
class LoginResponse {
|
||||
constructor() {
|
||||
this.id = 0;
|
||||
this.firstName = null;
|
||||
this.lastName = null;
|
||||
this.username = null;
|
||||
this.token = null;
|
||||
this.refreshToken = null;
|
||||
}
|
||||
}
|
||||
export default LoginResponse;
|
||||
11
FlexitimeUI/flexitimeui/src/models/Permission.js
Normal file
11
FlexitimeUI/flexitimeui/src/models/Permission.js
Normal file
@ -0,0 +1,11 @@
|
||||
//CSharpToJs: auto
|
||||
import Application from './Application.js';
|
||||
class Permission {
|
||||
constructor() {
|
||||
this.application = new Application();
|
||||
this.name = null;
|
||||
this.tag = null;
|
||||
this.id = 0;
|
||||
}
|
||||
}
|
||||
export default Permission;
|
||||
22
FlexitimeUI/flexitimeui/src/models/User.js
Normal file
22
FlexitimeUI/flexitimeui/src/models/User.js
Normal file
@ -0,0 +1,22 @@
|
||||
//CSharpToJs: auto
|
||||
import userState from "./userState";
|
||||
|
||||
export class User {
|
||||
constructor() {
|
||||
this.id = 0;
|
||||
this.firstName = "";
|
||||
this.lastName = "";
|
||||
this.hoursPerWeek = 0.0;
|
||||
this.isContractor = false;
|
||||
this.associatedIdentifierCount = 0;
|
||||
this.lastEventDateTime = "0001-01-01T00:00:00";
|
||||
this.associatedIdentifiers = [];
|
||||
this.groups = [];
|
||||
this.state = userState.UNKNOWN;
|
||||
this.lineManagerId = 0;
|
||||
this.directReportIds = null;
|
||||
this.password = null;
|
||||
this.permissions = [];
|
||||
}
|
||||
}
|
||||
//export default User;
|
||||
8
FlexitimeUI/flexitimeui/src/models/userState.js
Normal file
8
FlexitimeUI/flexitimeui/src/models/userState.js
Normal file
@ -0,0 +1,8 @@
|
||||
const userState = {
|
||||
UNKNOWN:"Unknown",
|
||||
IN: "In",
|
||||
OUT: "Out",
|
||||
OUT_OF_OFFICE: "OutOfOffice",
|
||||
REMOTE:"Remote"
|
||||
}
|
||||
export default userState;
|
||||
16
FlexitimeUI/flexitimeui/src/redux/actions/actionTypes.js
Normal file
16
FlexitimeUI/flexitimeui/src/redux/actions/actionTypes.js
Normal file
@ -0,0 +1,16 @@
|
||||
export const CREATE_USER = "CREATE_USER";
|
||||
export const GET_USERS = "GET_USER";
|
||||
export const GET_USER_BY_ID = "GET_USER_BY_ID";
|
||||
export const LOAD_USERS_SUCCESS = "LOAD_USERS_SUCCESS";
|
||||
export const CREATE_USER_SUCCESS = "CREATE_USER_SUCCESS";
|
||||
export const UPDATE_USER_SUCCESS = "UPDATE_USER_SUCCESS";
|
||||
export const DELETE_USER_OPTIMISTIC = "DELETE_USER_OPTIMISTIC";
|
||||
|
||||
export const CREATE_GROUP = "CREATE_GROUP";
|
||||
export const GET_GROUP = "GET_GROUP";
|
||||
export const GET_GROUP_BY_ID = "GET_GROUP_BY_ID";
|
||||
export const LOAD_GROUPS_SUCCESS = "LOAD_GROUPS_SUCCESS";
|
||||
|
||||
export const BEGIN_API_CALL = "BEGIN_API_CALL";
|
||||
export const API_CALL_ERROR = "API_CALL_ERROR";
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import * as actionTypes from "./actionTypes";
|
||||
|
||||
export function beginApiCall() {
|
||||
return { type: actionTypes.BEGIN_API_CALL };
|
||||
}
|
||||
export function apiCallError() {
|
||||
return { type: actionTypes.API_CALL_ERROR };
|
||||
}
|
||||
33
FlexitimeUI/flexitimeui/src/redux/actions/groupActions.js
Normal file
33
FlexitimeUI/flexitimeui/src/redux/actions/groupActions.js
Normal file
@ -0,0 +1,33 @@
|
||||
import * as types from "./actionTypes";
|
||||
import * as groupsApi from "../../api/groupsApi";
|
||||
import { beginApiCall, apiCallError } from "./apiStatusActions";
|
||||
|
||||
export function createGroup(group) {
|
||||
return { type: types.CREATE_GROUP, group };
|
||||
}
|
||||
export function getGroups() {
|
||||
return { type: types.GET_GROUP };
|
||||
}
|
||||
|
||||
export function getGroup(id) {
|
||||
return { type: types.GET_GROUP_BY_ID, id };
|
||||
}
|
||||
|
||||
export function loadGroupsSuccess(groups) {
|
||||
return { type: types.LOAD_GROUPS_SUCCESS, groups };
|
||||
}
|
||||
|
||||
export function loadGroups() {
|
||||
return function(dispatch) {
|
||||
dispatch(beginApiCall());
|
||||
return groupsApi
|
||||
.getGroups()
|
||||
.then(groups => {
|
||||
dispatch(loadGroupsSuccess(groups));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(apiCallError(error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
60
FlexitimeUI/flexitimeui/src/redux/actions/userActions.js
Normal file
60
FlexitimeUI/flexitimeui/src/redux/actions/userActions.js
Normal file
@ -0,0 +1,60 @@
|
||||
import * as types from "./actionTypes";
|
||||
import * as userApi from "../../api/userApi";
|
||||
import { beginApiCall, apiCallError } from "./apiStatusActions";
|
||||
|
||||
export function getUsers() {
|
||||
return { type: types.GET_USERS };
|
||||
}
|
||||
export function getUserById(id) {
|
||||
return { type: types.GET_USER_BY_ID, id };
|
||||
}
|
||||
export function loadUsersSuccess(users) {
|
||||
return { type: types.LOAD_USERS_SUCCESS, users };
|
||||
}
|
||||
export function updateUserSuccess(user) {
|
||||
return { type: types.UPDATE_USER_SUCCESS, user };
|
||||
}
|
||||
export function createUserSuccess(user) {
|
||||
return { type: types.CREATE_USER_SUCCESS, user };
|
||||
}
|
||||
export function deleteUserOptimistic(user) {
|
||||
return { type: types.DELETE_USER_OPTIMISTIC, user: user };
|
||||
}
|
||||
|
||||
export function loadUsers() {
|
||||
return function(dispatch) {
|
||||
dispatch(beginApiCall());
|
||||
return userApi
|
||||
.getUsers()
|
||||
.then(users => {
|
||||
dispatch(loadUsersSuccess(users));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(apiCallError(error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function saveUser(user) {
|
||||
return function(dispatch) {
|
||||
dispatch(beginApiCall());
|
||||
return userApi
|
||||
.saveUser(user)
|
||||
.then(savedUser => {
|
||||
user.id
|
||||
? dispatch(updateUserSuccess(savedUser))
|
||||
: dispatch(createUserSuccess(savedUser));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(apiCallError(error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
export function deleteUser(user) {
|
||||
return function(dispatch) {
|
||||
dispatch(deleteUserOptimistic(user));
|
||||
return userApi.deleteUser(user.id);
|
||||
};
|
||||
}
|
||||
14
FlexitimeUI/flexitimeui/src/redux/configureStore.js
Normal file
14
FlexitimeUI/flexitimeui/src/redux/configureStore.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
import rootReducer from './reducers';
|
||||
import reduxImmutableStateInvariant from "redux-immutable-state-invariant";
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
export default function configureStore(initialState) {
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
return createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
composeEnhancers(applyMiddleware(thunk, reduxImmutableStateInvariant()))
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import * as actionTypes from "../actions/actionTypes";
|
||||
import initialState from "./initialState";
|
||||
|
||||
function actionTypeEndsInSuccess(type) {
|
||||
return type.substring(type.length - 8) === "_SUCCESS";
|
||||
}
|
||||
|
||||
export default function apiCallStatusReducer(
|
||||
state = initialState.apiCallsInProcess,
|
||||
action
|
||||
) {
|
||||
if (action.type === actionTypes.BEGIN_API_CALL) {
|
||||
return state + 1;
|
||||
} else if (
|
||||
action.type === actionTypes.API_CALL_ERROR ||
|
||||
actionTypeEndsInSuccess(action.type)
|
||||
) {
|
||||
return state - 1;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
31
FlexitimeUI/flexitimeui/src/redux/reducers/groupsReducer.js
Normal file
31
FlexitimeUI/flexitimeui/src/redux/reducers/groupsReducer.js
Normal file
@ -0,0 +1,31 @@
|
||||
import * as actionTypes from "../actions/actionTypes";
|
||||
import initialState from "./initialState";
|
||||
|
||||
export default function groupsReducer(state = initialState.groups, action) {
|
||||
switch (action.type) {
|
||||
case actionTypes.CREATE_GROUP: {
|
||||
let group = action.group;
|
||||
if (state.length === 0) {
|
||||
group.id = 1;
|
||||
} else {
|
||||
group.id =
|
||||
Math.max.apply(
|
||||
Math,
|
||||
state.map(function(g) {
|
||||
return g.id;
|
||||
})
|
||||
) + 1;
|
||||
}
|
||||
return [...state, { ...group }];
|
||||
}
|
||||
case actionTypes.LOAD_GROUPS_SUCCESS: {
|
||||
return action.groups;
|
||||
}
|
||||
case actionTypes.GET_GROUP_BY_ID: {
|
||||
let group = state.find(x => x.id === action.group.id);
|
||||
return group;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
12
FlexitimeUI/flexitimeui/src/redux/reducers/index.js
Normal file
12
FlexitimeUI/flexitimeui/src/redux/reducers/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { combineReducers } from "redux";
|
||||
import users from "./userReducer";
|
||||
import groups from "./groupsReducer";
|
||||
import apiCallsInProgress from "./apiStatusReducer";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
users,
|
||||
groups,
|
||||
apiCallsInProgress
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
users: [],
|
||||
groups: [],
|
||||
apiCallsInProcess:0
|
||||
};
|
||||
28
FlexitimeUI/flexitimeui/src/redux/reducers/userReducer.js
Normal file
28
FlexitimeUI/flexitimeui/src/redux/reducers/userReducer.js
Normal file
@ -0,0 +1,28 @@
|
||||
import * as actionTypes from "../actions/actionTypes";
|
||||
import initialState from "./initialState";
|
||||
|
||||
export default function userReducer(state = initialState.users, action) {
|
||||
switch (action.type) {
|
||||
case actionTypes.CREATE_USER_SUCCESS: {
|
||||
return [...state, { ...action.user }];
|
||||
}
|
||||
case actionTypes.UPDATE_USER_SUCCESS: {
|
||||
return state.map(
|
||||
user => (user.id === action.user.id ? action.user : user)
|
||||
);
|
||||
}
|
||||
case actionTypes.LOAD_USERS_SUCCESS: {
|
||||
return action.users;
|
||||
}
|
||||
case actionTypes.GET_USER_BY_ID: {
|
||||
let user = state.find(x => x.id === action.user.id);
|
||||
return user;
|
||||
}
|
||||
case actionTypes.DELETE_USER_OPTIMISTIC:
|
||||
{
|
||||
return state.filter(user => user.id !== action.user.id);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
13
FlexitimeUI/flexitimeui/src/reportWebVitals.js
Normal file
13
FlexitimeUI/flexitimeui/src/reportWebVitals.js
Normal file
@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
9
FlexitimeUI/flexitimeui/src/services/userService.js
Normal file
9
FlexitimeUI/flexitimeui/src/services/userService.js
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
function isLoggedIn() {
|
||||
|
||||
}
|
||||
|
||||
function isAuthorisedFor(featureTag) {
|
||||
|
||||
}
|
||||
5
FlexitimeUI/flexitimeui/src/setupTests.js
Normal file
5
FlexitimeUI/flexitimeui/src/setupTests.js
Normal file
@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
93
FlexitimeUI/flexitimeui/src/styles/Spinner.css
Normal file
93
FlexitimeUI/flexitimeui/src/styles/Spinner.css
Normal file
@ -0,0 +1,93 @@
|
||||
/*https://projects.lukehaas.me/css-loaders/*/
|
||||
.loader {
|
||||
color: #ff9700;
|
||||
font-size: 90px;
|
||||
text-indent: -9999em;
|
||||
overflow: hidden;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 50%;
|
||||
margin: 72px auto;
|
||||
position: relative;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: load6 1.7s infinite ease, round 1.7s infinite ease;
|
||||
animation: load6 1.7s infinite ease, round 1.7s infinite ease;
|
||||
}
|
||||
|
||||
@-webkit-keyframes load6 {
|
||||
0% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
|
||||
5%, 95% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
|
||||
10%, 59% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
|
||||
}
|
||||
|
||||
20% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
|
||||
}
|
||||
|
||||
38% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load6 {
|
||||
0% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
|
||||
5%, 95% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
|
||||
10%, 59% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
|
||||
}
|
||||
|
||||
20% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
|
||||
}
|
||||
|
||||
38% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes round {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes round {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
3
FlexitimeUI/flexitimeui/src/styles/StateIndicator.css
Normal file
3
FlexitimeUI/flexitimeui/src/styles/StateIndicator.css
Normal file
@ -0,0 +1,3 @@
|
||||
span.stateIndicator {
|
||||
display: block;
|
||||
}
|
||||
62
FlexitimeUI/flexitimeui/src/styles/toggleSlider.css
Normal file
62
FlexitimeUI/flexitimeui/src/styles/toggleSlider.css
Normal file
@ -0,0 +1,62 @@
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
116
FlexitimeUI/flexitimeui/tools/apiServer.js
Normal file
116
FlexitimeUI/flexitimeui/tools/apiServer.js
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
This uses json-server, but with the module approach: https://github.com/typicode/json-server#module
|
||||
Downside: You can't pass the json-server command line options.
|
||||
Instead, can override some defaults by passing a config object to jsonServer.defaults();
|
||||
You have to check the source code to set some items.
|
||||
Examples:
|
||||
Validation/Customization: https://github.com/typicode/json-server/issues/266
|
||||
Delay: https://github.com/typicode/json-server/issues/534
|
||||
ID: https://github.com/typicode/json-server/issues/613#issuecomment-325393041
|
||||
Relevant source code: https://github.com/typicode/json-server/blob/master/src/cli/run.js
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
const fs = require("fs");
|
||||
const jsonServer = require("json-server");
|
||||
const server = jsonServer.create();
|
||||
const path = require("path");
|
||||
const dbPath = path.join(__dirname, "db.json");
|
||||
const router = jsonServer.router(dbPath);
|
||||
let maxId = getMaxId(dbPath);
|
||||
|
||||
// Can pass a limited number of options to this to override (some) defaults. See https://github.com/typicode/json-server#api
|
||||
const middlewares = jsonServer.defaults({
|
||||
// Display json-server's built in homepage when json-server starts.
|
||||
static: "node_modules/json-server/public"
|
||||
});
|
||||
|
||||
// Set default middlewares (logger, static, cors and no-cache)
|
||||
server.use(middlewares);
|
||||
|
||||
// To handle POST, PUT and PATCH you need to use a body-parser. Using JSON Server's bodyParser
|
||||
server.use(jsonServer.bodyParser);
|
||||
|
||||
// Simulate delay on all requests
|
||||
server.use(function(req, res, next) {
|
||||
setTimeout(next, 2000);
|
||||
});
|
||||
|
||||
// Declaring custom routes below. Add custom routes before JSON Server router
|
||||
|
||||
// Add createdAt to all POSTS
|
||||
server.use((req, res, next) => {
|
||||
//if (req.method === "POST") {
|
||||
// req.body.createdAt = Date.now();
|
||||
//}
|
||||
// Continue to JSON Server router
|
||||
next();
|
||||
});
|
||||
|
||||
//create
|
||||
server.post("/users/", function(req, res, next) {
|
||||
const error = validateUser(req.body);
|
||||
if (error) {
|
||||
res.status(400).send(error);
|
||||
} else {
|
||||
req.body.id = maxId;
|
||||
//req.body.slug = createSlug(req.body.title); // Generate a slug for new courses.
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
//update
|
||||
server.put("/users", function(req, res, next) {
|
||||
const error = validateUser(req.body);
|
||||
if (error) {
|
||||
res.status(400).send(error);
|
||||
} else {
|
||||
req.body.id = req.body.id;
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// Use default router
|
||||
server.use(router);
|
||||
|
||||
// Start server
|
||||
const port = 3001;
|
||||
server.listen(port, () => {
|
||||
console.log(`JSON Server is running on port ${port}`);
|
||||
});
|
||||
|
||||
// Centralized logic
|
||||
|
||||
// Returns a URL friendly slug
|
||||
function createSlug(value) {
|
||||
return value
|
||||
.replace(/[^a-z0-9_]+/gi, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function validateUser(user) {
|
||||
if (!user.id || user.id === 0) {
|
||||
maxId = maxId + 1;
|
||||
user.id = maxId;
|
||||
} else if (user.id < 0 || user.id > maxId) {
|
||||
return "Invalid User Id";
|
||||
}
|
||||
|
||||
if (!user.firstName) return "First Name is required.";
|
||||
if (!user.lastName) return "Last Name is required.";
|
||||
if (user.hoursPerWeek <= 0) return "Hours Per Week must be a positive integer";
|
||||
return "";
|
||||
}
|
||||
|
||||
function getMaxId(dbPath) {
|
||||
let rawData = fs.readFileSync(dbPath);
|
||||
let data = JSON.parse(rawData);
|
||||
let userArray = data.users;
|
||||
return Math.max.apply(
|
||||
Math,
|
||||
userArray.map(function(o) {
|
||||
return o.id;
|
||||
})
|
||||
);
|
||||
}
|
||||
12
FlexitimeUI/flexitimeui/tools/createMockDb.js
Normal file
12
FlexitimeUI/flexitimeui/tools/createMockDb.js
Normal file
@ -0,0 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const mockData = require("./mockData");
|
||||
|
||||
const { users, authors, groups } = mockData;
|
||||
const data = JSON.stringify({ users, authors, groups });
|
||||
const filepath = path.join(__dirname, "db.json");
|
||||
|
||||
fs.writeFile(filepath, data, function(err) {
|
||||
err ? console.log(err) : console.log("Mock DB created.");
|
||||
});
|
||||
51
FlexitimeUI/flexitimeui/tools/db.json
Normal file
51
FlexitimeUI/flexitimeui/tools/db.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"id": 2,
|
||||
"username": "lawrenb",
|
||||
"firstName": "Brett",
|
||||
"lastName": "Lawrence",
|
||||
"hoursPerWeek": 37,
|
||||
"state": "Remote"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"username": "bustamae",
|
||||
"firstName": "Eugene",
|
||||
"lastName": "Bustamante",
|
||||
"hoursPerWeek": 37,
|
||||
"state": "Out"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Cory House"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Scott Allen"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Dan Wahlin"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "RTPS",
|
||||
"userCount": 4
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Embedded Rating",
|
||||
"userCount": 14
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Rules Engine",
|
||||
"userCount": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user