Add redux
This commit is contained in:
parent
7db2ceca08
commit
7712f63999
@ -7,5 +7,5 @@ namespace Todo.Api.Controllers;
|
||||
[ApiController]
|
||||
public class ApiController : ControllerBase
|
||||
{
|
||||
public IMediator Mediator => HttpContext.RequestServices.GetRequiredService<IMediator>();
|
||||
protected IMediator Mediator => HttpContext.RequestServices.GetRequiredService<IMediator>();
|
||||
}
|
@ -47,8 +47,33 @@ public class TodosController : ApiController
|
||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos()
|
||||
=> Ok(await _todoRepository.GetTodosAsync());
|
||||
|
||||
[HttpGet("not-done")]
|
||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>>
|
||||
GetNotDoneTodos()
|
||||
=> Ok(await _todoRepository.GetNotDoneTodos());
|
||||
[HttpPut("{todoId}")]
|
||||
public async Task<ActionResult> ReplaceTodo([FromRoute] string todoId, [FromBody] ReplaceTodoRequest request)
|
||||
{
|
||||
await Mediator.Send(request.To(todoId));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public record ReplaceTodoRequest
|
||||
{
|
||||
[Required]
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonPropertyName("project")]
|
||||
public string? Project { get; init; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public bool Status { get; init; }
|
||||
|
||||
internal ReplaceTodoCommand To(string id) => new(
|
||||
id,
|
||||
Title,
|
||||
Project,
|
||||
Description,
|
||||
Status);
|
||||
}
|
||||
}
|
@ -27,8 +27,9 @@ public class TodoPublisher : ITodoPublisher
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Publish(
|
||||
string todoId,
|
||||
public async Task Publish<T>(
|
||||
string eventType,
|
||||
T message,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.GetUserId() ??
|
||||
@ -36,14 +37,33 @@ public class TodoPublisher : ITodoPublisher
|
||||
var connections =
|
||||
await _userConnectionStore.GetConnectionsAsync(userId);
|
||||
|
||||
await _hubContext
|
||||
.Clients
|
||||
.Clients(connections)
|
||||
.SendAsync(
|
||||
"todoCreated",
|
||||
todoId,
|
||||
cancellationToken);
|
||||
switch (eventType)
|
||||
{
|
||||
case "todoCreated":
|
||||
await _hubContext
|
||||
.Clients
|
||||
.Clients(connections)
|
||||
.SendAsync(
|
||||
"todoCreated",
|
||||
message,
|
||||
cancellationToken);
|
||||
break;
|
||||
|
||||
_logger.LogInformation("todo created {TodoId}", todoId);
|
||||
case "todoUpdated":
|
||||
await _hubContext
|
||||
.Clients
|
||||
.Clients(connections)
|
||||
.SendAsync(
|
||||
"todoUpdated",
|
||||
message,
|
||||
cancellationToken);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
_logger.LogInformation(
|
||||
"todo event: {EventType} with message: {Message}",
|
||||
eventType,
|
||||
message);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using System.Threading;
|
||||
using MediatR;
|
||||
using Todo.Core.Application.Notifications.Todo;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Core.Interfaces.User;
|
||||
|
||||
namespace Todo.Core.Application.Commands.Todo;
|
||||
|
||||
public record ReplaceTodoCommand(
|
||||
string Id,
|
||||
string Title,
|
||||
string Project,
|
||||
string Description,
|
||||
bool Status) : IRequest<Unit>
|
||||
{
|
||||
internal class Handler : IRequestHandler<ReplaceTodoCommand, Unit>
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly ITodoRepository _todoRepository;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public Handler(
|
||||
ICurrentUserService currentUserService,
|
||||
ITodoRepository todoRepository,
|
||||
IMediator mediator)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_todoRepository = todoRepository;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(ReplaceTodoCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.GetUserId();
|
||||
if (userId is null)
|
||||
throw new InvalidOperationException("User was not found");
|
||||
//TODO: Make sure the author actually owns the todo
|
||||
await _todoRepository.UpdateTodoAsync(request.To(userId));
|
||||
|
||||
await _mediator.Publish(new TodoUpdated(request.Id), cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private Entities.Todo To(string authorId) => new()
|
||||
{
|
||||
Id = Id,
|
||||
Description = Description,
|
||||
Project = Project,
|
||||
Status = Status,
|
||||
Title = Title,
|
||||
AuthorId = authorId
|
||||
};
|
||||
}
|
@ -20,6 +20,7 @@ public record TodoCreated(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await _todoPublisher.Publish(
|
||||
"todoCreated",
|
||||
JsonSerializer.Serialize(notification),
|
||||
cancellationToken);
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using MediatR;
|
||||
using Todo.Core.Interfaces.Publisher;
|
||||
|
||||
namespace Todo.Core.Application.Notifications.Todo;
|
||||
|
||||
public record TodoUpdated(
|
||||
[property: JsonPropertyName("todoId")] string TodoId) : INotification
|
||||
{
|
||||
internal class Handler : INotificationHandler<TodoUpdated>
|
||||
{
|
||||
private readonly ITodoPublisher _todoPublisher;
|
||||
|
||||
public Handler(ITodoPublisher todoPublisher) => _todoPublisher = todoPublisher;
|
||||
|
||||
public async Task Handle(
|
||||
TodoUpdated notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await _todoPublisher.Publish(
|
||||
"todoUpdated",
|
||||
JsonSerializer.Serialize(notification),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,5 +4,5 @@ namespace Todo.Core.Interfaces.Publisher;
|
||||
|
||||
public interface ITodoPublisher
|
||||
{
|
||||
Task Publish(string todoId, CancellationToken cancellationToken = new());
|
||||
Task Publish<T>(string eventType, T message, CancellationToken cancellationToken = new());
|
||||
}
|
@ -16,4 +16,4 @@ public record TodoViewModel(
|
||||
t.AuthorId,
|
||||
t.Project,
|
||||
t.Description);
|
||||
};
|
||||
}
|
@ -39,7 +39,14 @@ internal class InMemoryUserConnectionStore : IUserConnectionStore
|
||||
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
|
||||
|
||||
// remove the connection id from the List
|
||||
existingUserConnectionIds?.Remove(connectionId);
|
||||
try
|
||||
{
|
||||
existingUserConnectionIds?.Remove(connectionId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
|
||||
if (existingUserConnectionIds?.Count == 0)
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using MongoDB.Driver;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Persistence.Mongo.Repositories.Dtos;
|
||||
@ -35,7 +36,8 @@ public class TodoRepository : ITodoRepository
|
||||
Title = todo.Title,
|
||||
Status = false,
|
||||
Project = todo.ProjectName,
|
||||
Description = todo.Description
|
||||
Description = todo.Description,
|
||||
AuthorId = todo.AuthorId
|
||||
};
|
||||
}
|
||||
|
||||
@ -52,7 +54,8 @@ public class TodoRepository : ITodoRepository
|
||||
Title = t.Title,
|
||||
Status = t.Status,
|
||||
Project = t.ProjectName,
|
||||
Description = t.Description
|
||||
Description = t.Description,
|
||||
AuthorId = t.AuthorId
|
||||
});
|
||||
}
|
||||
|
||||
@ -84,7 +87,8 @@ public class TodoRepository : ITodoRepository
|
||||
Project = todo.ProjectName,
|
||||
Status = todo.Status,
|
||||
Title = todo.Title,
|
||||
Description = todo.Description
|
||||
Description = todo.Description,
|
||||
AuthorId = todo.AuthorId
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -9,17 +9,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^6.0.0",
|
||||
"@reduxjs/toolkit": "^1.6.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"axios": "^0.24.0",
|
||||
"next": "12.0.3",
|
||||
"next-pwa": "^5.4.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-textarea-autosize": "^8.3.3",
|
||||
"tailwindcss": "^2.2.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.34",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"cssnano": "^5.0.10",
|
||||
"eslint": "7",
|
||||
|
@ -1,31 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { Todo } from "@src/core/entities/todo";
|
||||
|
||||
const getBaseUrl = () =>
|
||||
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000";
|
||||
|
||||
const createBaseClient = () =>
|
||||
axios.create({
|
||||
baseURL: getBaseUrl(),
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
interface CreateTodoRequest {
|
||||
title: string;
|
||||
description?: string;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
export const createTodoAsync = async (
|
||||
createTodo: CreateTodoRequest
|
||||
): Promise<string> =>
|
||||
await createBaseClient().post<string, string, CreateTodoRequest>(
|
||||
"/api/todos",
|
||||
createTodo
|
||||
);
|
||||
|
||||
interface GetTodoByIdResponse extends Todo {}
|
||||
export const getTodoByIdAsync = async (todoId: string) =>
|
||||
await createBaseClient().get<any, GetTodoByIdResponse>(
|
||||
`/api/todos/${todoId}`
|
||||
);
|
@ -10,7 +10,7 @@ export const PrimaryButton: FC<ButtonHTMLAttributes<HTMLButtonElement>> = (
|
||||
tabIndex={2}
|
||||
disabled={props.disabled}
|
||||
className={
|
||||
"base-button bg-accent-500 disabled:bg-accent-800 active:bg-accent-400 border-none text-white " +
|
||||
"base-button bg-accent-500 disabled:bg-accent-800 dark:bg-accent-500 active:bg-accent-400 border-none text-white " +
|
||||
props.className
|
||||
}
|
||||
>
|
||||
|
@ -3,6 +3,7 @@ import { FC, useState } from "react";
|
||||
import { TodoCheckmark } from "@src/components/todos/todoCheckmark";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import Link from "next/link";
|
||||
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||
|
||||
interface TodoItemProps {
|
||||
todo: Todo;
|
||||
@ -14,10 +15,15 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [menuIsOpen, setMenuIsOpen] = useState(false);
|
||||
|
||||
const prefetchTodo = todosApi.usePrefetch("getTodoById");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="py-3 border-b border-gray-300 dark:border-gray-600 relative"
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseEnter={() => {
|
||||
prefetchTodo(props.todo.id);
|
||||
setIsHovering(true);
|
||||
}}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
onFocus={() => setIsHovering(true)}
|
||||
>
|
||||
@ -32,7 +38,7 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
|
||||
<div className="flex flex-row justify-between items-end">
|
||||
<div className="flex flex-row">
|
||||
{props.todo.description && (
|
||||
<div className="h-3 w-3 bg-gray-100 dark:border-black border border-gray-300 dark:bg-gray-900 rounded-sm"/>
|
||||
<div className="h-3 w-3 bg-gray-100 dark:border-black border border-gray-300 dark:bg-gray-900 rounded-sm" />
|
||||
)}
|
||||
</div>
|
||||
{props.displayProject && props.todo.project && (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Todo } from "@src/core/entities/todo";
|
||||
import { TodoItem } from "@src/components/todos/todoItem";
|
||||
import { AddTodo } from "@src/components/todos/addTodo";
|
||||
import { useUpdateTodoState } from "@src/presentation/hooks/socketHooks";
|
||||
import { useUpdateTodo } from "@src/presentation/hooks/socketHooks";
|
||||
|
||||
export const TodoList = (props: {
|
||||
todos: Todo[];
|
||||
@ -9,19 +9,23 @@ export const TodoList = (props: {
|
||||
hideProject: boolean;
|
||||
project: string;
|
||||
}) => {
|
||||
const { updateTodoState } = useUpdateTodoState();
|
||||
const { updateTodo } = useUpdateTodo();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul id="inbox">
|
||||
{props.todos
|
||||
.filter((t) => t.status == false)
|
||||
.filter((t) => {
|
||||
if (!props.hideDone) return true;
|
||||
|
||||
return t.status == false;
|
||||
})
|
||||
.map((t, i) => (
|
||||
<li key={i}>
|
||||
<TodoItem
|
||||
todo={t}
|
||||
updateTodo={(todo) => {
|
||||
updateTodoState(todo.id, todo.status);
|
||||
updateTodo(todo);
|
||||
}}
|
||||
displayProject={!props.hideProject}
|
||||
/>
|
||||
|
16
src/client/src/core/actions/utility/groupByProjects.ts
Normal file
16
src/client/src/core/actions/utility/groupByProjects.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export const groupByProjects = (arr) => {
|
||||
if (!arr) {
|
||||
return {};
|
||||
}
|
||||
const criteria = "project";
|
||||
|
||||
return arr.reduce(function (acc, currentValue) {
|
||||
const currentValueElement = currentValue[criteria] || "inbox";
|
||||
|
||||
if (!acc[currentValueElement]) {
|
||||
acc[currentValueElement] = [];
|
||||
}
|
||||
acc[currentValueElement].push(currentValue);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
63
src/client/src/infrastructure/apis/todosApi.ts
Normal file
63
src/client/src/infrastructure/apis/todosApi.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQueryWithReauth } from "@src/infrastructure/apis/utilities";
|
||||
import { Todo } from "@src/core/entities/todo";
|
||||
|
||||
export const todosApi = createApi({
|
||||
reducerPath: "api",
|
||||
tagTypes: ["todo"],
|
||||
baseQuery: baseQueryWithReauth,
|
||||
endpoints: (build) => ({
|
||||
getTodoById: build.query<Todo, string>({
|
||||
query: (id) => `api/todos/${id}`,
|
||||
providesTags: (result, error, id) => [{ type: "todo", id }],
|
||||
}),
|
||||
|
||||
getAllTodos: build.query<Todo[], void>({
|
||||
query: () => ({ url: `api/todos` }),
|
||||
providesTags: (result) =>
|
||||
result
|
||||
? [
|
||||
...result.map(({ id }) => ({ type: "todo" as const, id })),
|
||||
{ type: "todo", id: "LIST" },
|
||||
]
|
||||
: [],
|
||||
}),
|
||||
|
||||
createTodo: build.mutation<string, Partial<Todo>>({
|
||||
query(body) {
|
||||
return {
|
||||
url: "/api/todos",
|
||||
method: "POST",
|
||||
body,
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, id) => [{ type: "todo", id: "LIST" }],
|
||||
}),
|
||||
|
||||
replaceTodo: build.mutation<string, Partial<Todo>>({
|
||||
query(data) {
|
||||
const { id, ...body } = data;
|
||||
return {
|
||||
url: `/api/todos/${id}`,
|
||||
method: "PUT",
|
||||
body,
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, { id }) => [{ type: "todo", id }],
|
||||
async onQueryStarted({ id, ...body }, { dispatch, queryFulfilled }) {
|
||||
const replaceResult = dispatch(
|
||||
todosApi.util.updateQueryData("getTodoById", id, (draft) => {
|
||||
Object.assign(draft, body);
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await queryFulfilled;
|
||||
} catch {
|
||||
replaceResult.undo();
|
||||
dispatch(todosApi.util.invalidateTags([{ type: "todo", id }]));
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
27
src/client/src/infrastructure/apis/utilities.ts
Normal file
27
src/client/src/infrastructure/apis/utilities.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {
|
||||
BaseQueryFn,
|
||||
FetchArgs,
|
||||
fetchBaseQuery,
|
||||
FetchBaseQueryError,
|
||||
} from "@reduxjs/toolkit/query/react";
|
||||
|
||||
export const getApiBaseUrl = () =>
|
||||
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000";
|
||||
|
||||
const baseQuery = fetchBaseQuery({
|
||||
baseUrl: getApiBaseUrl(),
|
||||
credentials: "include",
|
||||
});
|
||||
export const baseQueryWithReauth: BaseQueryFn<
|
||||
string | FetchArgs,
|
||||
unknown,
|
||||
FetchBaseQueryError
|
||||
> = async (args, api, extraOptions) => {
|
||||
let result = await baseQuery(args, api, extraOptions);
|
||||
if (result.error && result.error.status === 302) {
|
||||
window.location.href = `${getApiBaseUrl()}/api/auth/login?returnUrl=${
|
||||
window.location.href
|
||||
}`;
|
||||
}
|
||||
return result;
|
||||
};
|
23
src/client/src/infrastructure/state/todo.ts
Normal file
23
src/client/src/infrastructure/state/todo.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createEntityAdapter, createSlice } from "@reduxjs/toolkit";
|
||||
import { Todo } from "@src/core/entities/todo";
|
||||
import { AppState } from "@src/infrastructure/store";
|
||||
|
||||
export const todosAdapter = createEntityAdapter<Todo>({
|
||||
selectId: (todo) => todo.id,
|
||||
sortComparer: (a, b) => a.title.localeCompare(b.title),
|
||||
});
|
||||
|
||||
export const todosSlice = createSlice({
|
||||
name: "todo",
|
||||
initialState: todosAdapter.getInitialState(),
|
||||
reducers: {
|
||||
todoAdded: todosAdapter.addOne,
|
||||
todosReceived(state, action) {
|
||||
todosAdapter.setAll(state, action.payload.todos);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const todosSelectors = todosAdapter.getSelectors<AppState>(
|
||||
(state) => state.todos
|
||||
);
|
6
src/client/src/infrastructure/store/hooks.ts
Normal file
6
src/client/src/infrastructure/store/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { AppDispatch, AppState } from "@src/infrastructure/store/store";
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
|
||||
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
|
4
src/client/src/infrastructure/store/index.ts
Normal file
4
src/client/src/infrastructure/store/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./hooks";
|
||||
export * from "./store";
|
||||
|
||||
export { default } from "./store";
|
30
src/client/src/infrastructure/store/store.ts
Normal file
30
src/client/src/infrastructure/store/store.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Action, configureStore, ThunkAction } from "@reduxjs/toolkit";
|
||||
import { todosSlice } from "@src/infrastructure/state/todo";
|
||||
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
|
||||
export function makeStore() {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
todos: todosSlice.reducer,
|
||||
[todosApi.reducerPath]: todosApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(todosApi.middleware),
|
||||
});
|
||||
}
|
||||
|
||||
const store = makeStore();
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
|
||||
export type AppState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
AppState,
|
||||
unknown,
|
||||
Action<string>
|
||||
>;
|
||||
|
||||
export default store;
|
@ -1,8 +1,10 @@
|
||||
import { AppProps } from "next/app";
|
||||
import type { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
|
||||
import "@src/styles/tailwind.css";
|
||||
import SocketProvider from "@src/presentation/contexts/SocketContext";
|
||||
import store from "@src/infrastructure/store";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
const MyApp = ({ Component, pageProps }: AppProps) => (
|
||||
<>
|
||||
@ -34,9 +36,11 @@ const MyApp = ({ Component, pageProps }: AppProps) => (
|
||||
<meta name="theme-color" content="#317EFB" />
|
||||
</Head>
|
||||
|
||||
<SocketProvider>
|
||||
<Component {...pageProps} />
|
||||
</SocketProvider>
|
||||
<Provider store={store}>
|
||||
<SocketProvider>
|
||||
<Component {...pageProps} />
|
||||
</SocketProvider>
|
||||
</Provider>
|
||||
</>
|
||||
);
|
||||
export default MyApp;
|
||||
|
@ -1,40 +1,33 @@
|
||||
import { PageHeading } from "@src/components/common/headings/pageHeading";
|
||||
import { TodoList } from "@src/components/todos";
|
||||
import { useSelectInboxTodos } from "@src/presentation/hooks/socketHooks";
|
||||
import { ProjectsHeading } from "@src/components/common/headings/projectsHeading";
|
||||
import { groupByProjects } from "@src/core/actions/utility/groupByProjects";
|
||||
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||
import { AddTodo } from "@src/components/todos/addTodo";
|
||||
|
||||
const groupByProjects = (arr) => {
|
||||
if (!arr) {
|
||||
return {};
|
||||
}
|
||||
const criteria = "project";
|
||||
|
||||
return arr.reduce(function (acc, currentValue) {
|
||||
const currentValueElement = currentValue[criteria] || "inbox";
|
||||
|
||||
if (!acc[currentValueElement]) {
|
||||
acc[currentValueElement] = [];
|
||||
}
|
||||
acc[currentValueElement].push(currentValue);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const HomePage = () => {
|
||||
const { todos, loading } = useSelectInboxTodos();
|
||||
const groupedTodos = groupByProjects(todos);
|
||||
const { data, isLoading } = todosApi.useGetAllTodosQuery();
|
||||
|
||||
if (isLoading) {
|
||||
return "loading...";
|
||||
}
|
||||
|
||||
const groupedTodos = groupByProjects(data);
|
||||
const sections = Object.keys(groupedTodos)
|
||||
.filter((s) => s !== "inbox")
|
||||
.sort();
|
||||
|
||||
return (
|
||||
<main className="py-8 px-14 space-y-6">
|
||||
<section className="space-y-8 mx-auto max-w-4xl">
|
||||
{groupedTodos["inbox"] && (
|
||||
<div className="space-y-2">
|
||||
<PageHeading title="Inbox" />
|
||||
<TodoList todos={groupedTodos["inbox"]} hideDone project={""} hideProject={false} />
|
||||
<TodoList
|
||||
todos={groupedTodos["inbox"]}
|
||||
hideDone
|
||||
project={""}
|
||||
hideProject={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{sections &&
|
||||
@ -49,7 +42,7 @@ const HomePage = () => {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{!loading && todos && todos.length === 0 && (
|
||||
{data && data.length === 0 && (
|
||||
<div className="space-y-4">
|
||||
<PageHeading title="You're done!" />
|
||||
<AddTodo project={""} />
|
||||
|
@ -98,7 +98,11 @@ const TodoDetailsPage = () => {
|
||||
const router = useRouter();
|
||||
const { todoId } = router.query as { todoId: string };
|
||||
|
||||
const { todo } = useSelectTodo(todoId);
|
||||
const { todo, isLoading } = useSelectTodo(todoId);
|
||||
|
||||
if (isLoading) {
|
||||
return "loading...";
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="py-8 px-14 space-y-6">
|
||||
|
@ -1,86 +1,40 @@
|
||||
import { createContext, FC, useEffect, useState } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import {
|
||||
HubConnection,
|
||||
HubConnectionBuilder,
|
||||
LogLevel,
|
||||
} from "@microsoft/signalr";
|
||||
import { asTodo, StatusState, Todo } from "@src/core/entities/todo";
|
||||
import { getTodoByIdAsync } from "@src/boundary/todo/todoApi";
|
||||
|
||||
interface SocketContextProps {
|
||||
conn: HubConnection;
|
||||
todos: Todo[];
|
||||
inboxTodos: Todo[];
|
||||
getTodos: () => void;
|
||||
getInboxTodos: () => void;
|
||||
createTodo: (todoName: string, project: string, description: string) => void;
|
||||
updateTodo: (todoId: string, todoStatus: StatusState) => void;
|
||||
getTodoById(todoId: string): void;
|
||||
replaceTodo(todo: Todo): void;
|
||||
}
|
||||
|
||||
export const SocketContext = createContext<SocketContextProps>({
|
||||
conn: void 0,
|
||||
todos: [],
|
||||
inboxTodos: [],
|
||||
getTodos: () => {},
|
||||
getInboxTodos: () => {},
|
||||
createTodo: (todoName, project, description) => {},
|
||||
updateTodo: (todoId, todoStatus) => {},
|
||||
getTodoById(todoId: string) {},
|
||||
replaceTodo(todo: Todo) {},
|
||||
});
|
||||
import { getApiBaseUrl } from "@src/infrastructure/apis/utilities";
|
||||
import { useAppDispatch } from "@src/infrastructure/store";
|
||||
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||
|
||||
export const SocketProvider: FC = (props) => {
|
||||
const [conn, setConn] = useState<HubConnection>();
|
||||
const [todos, setTodos] = useState<Todo[]>([]);
|
||||
const [inboxTodos, setInboxTodos] = useState<Todo[]>([]);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const serverUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000";
|
||||
|
||||
const connection = new HubConnectionBuilder()
|
||||
.withUrl(`${serverUrl}/hubs/todo`, {
|
||||
.withUrl(`${getApiBaseUrl()}/hubs/todo`, {
|
||||
withCredentials: true,
|
||||
})
|
||||
.withAutomaticReconnect()
|
||||
.configureLogging(LogLevel.Information)
|
||||
.build();
|
||||
|
||||
connection.on("getTodos", (rawTodos) => {
|
||||
const newTodos = JSON.parse(rawTodos) as Todo[];
|
||||
const validatedTodos = newTodos.map(asTodo);
|
||||
setTodos(validatedTodos);
|
||||
});
|
||||
|
||||
connection.on("todoCreated", (todoCreated) => {
|
||||
const { todoId } = JSON.parse(todoCreated) as { todoId: string };
|
||||
|
||||
getTodoByIdAsync(todoId).then((response) => {
|
||||
setTodos([...todos.filter((t) => t.id !== response.id), response]);
|
||||
setInboxTodos([
|
||||
...inboxTodos.filter((t) => t.id !== response.id),
|
||||
response,
|
||||
]);
|
||||
});
|
||||
if (todoId) {
|
||||
dispatch(todosApi.endpoints.getTodoById.initiate(todoId));
|
||||
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }]));
|
||||
}
|
||||
});
|
||||
|
||||
connection.on("getInboxTodos", (rawTodos) => {
|
||||
const newTodos = JSON.parse(rawTodos) as Todo[];
|
||||
const validatedTodos = newTodos
|
||||
.map(asTodo)
|
||||
.filter((t) => t.status == false);
|
||||
setInboxTodos(validatedTodos);
|
||||
});
|
||||
|
||||
connection.on("getTodo", (todo) => {
|
||||
const newTodo = JSON.parse(todo) as Todo;
|
||||
const validatedTodo = asTodo(newTodo);
|
||||
setTodos([
|
||||
...todos.filter((t) => t.id !== validatedTodo.id),
|
||||
validatedTodo,
|
||||
]);
|
||||
connection.on("todoUpdated", (todoUpdated) => {
|
||||
const { todoId } = JSON.parse(todoUpdated) as { todoId: string };
|
||||
if (todoId) {
|
||||
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: todoId }]));
|
||||
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }]));
|
||||
}
|
||||
});
|
||||
|
||||
connection
|
||||
@ -89,41 +43,13 @@ export const SocketProvider: FC = (props) => {
|
||||
setConn(connection);
|
||||
})
|
||||
.catch((e) => {
|
||||
window.location.href = `${serverUrl}/api/auth/login?returnUrl=${window.location.href}`;
|
||||
window.location.href = `${getApiBaseUrl()}/api/auth/login?returnUrl=${
|
||||
window.location.href
|
||||
}`;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider
|
||||
value={{
|
||||
conn,
|
||||
todos,
|
||||
inboxTodos,
|
||||
getTodos: () => {
|
||||
conn.invoke("GetTodos").catch(console.error);
|
||||
},
|
||||
getInboxTodos: () => {
|
||||
conn.invoke("GetInboxTodos").catch(console.error);
|
||||
},
|
||||
createTodo: (todoName, project, description) => {
|
||||
conn
|
||||
.invoke("CreateTodo", todoName, project, description)
|
||||
.catch(console.error);
|
||||
},
|
||||
updateTodo: (todoId, todoStatus) => {
|
||||
conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error);
|
||||
},
|
||||
getTodoById(todoId: string) {
|
||||
conn.invoke("GetTodo", todoId).catch(console.error);
|
||||
},
|
||||
replaceTodo: (todo) => {
|
||||
conn.invoke("ReplaceTodo", JSON.stringify(todo)).catch(console.error);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{conn ? props.children : "loading"}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default SocketProvider;
|
||||
|
@ -1,64 +1,51 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
import { SocketContext } from "@src/presentation/contexts/SocketContext";
|
||||
import { StatusState, Todo } from "@src/core/entities/todo";
|
||||
import { createTodoAsync } from "@src/boundary/todo/todoApi";
|
||||
import { useEffect } from "react";
|
||||
import { useAppSelector } from "@src/infrastructure/store";
|
||||
import { todosSelectors } from "@src/infrastructure/state/todo";
|
||||
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||
import { Todo } from "@src/core/entities/todo";
|
||||
|
||||
export const useSelectInboxTodos = () => {
|
||||
const socketContext = useContext(SocketContext);
|
||||
|
||||
useEffect(() => {
|
||||
socketContext.getInboxTodos();
|
||||
}, []);
|
||||
const todos = useAppSelector(todosSelectors.selectAll);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
todos: socketContext.inboxTodos,
|
||||
todos,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCreateTodo = () => {
|
||||
const socketContext = useContext(SocketContext);
|
||||
const [createTodo, { isLoading }] = todosApi.useCreateTodoMutation();
|
||||
|
||||
return {
|
||||
createTodo: (todoName: string, project: string, description: string) => {
|
||||
//socketContext.createTodo(todoName, project, description);
|
||||
createTodoAsync({
|
||||
createTodo({
|
||||
title: todoName,
|
||||
project,
|
||||
description,
|
||||
title: todoName,
|
||||
}).catch(console.error);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useUpdateTodoState = () => {
|
||||
const socketContext = useContext(SocketContext);
|
||||
export const useSelectTodo = (
|
||||
todoId: string
|
||||
): { todo: Todo | undefined; isLoading: boolean } => {
|
||||
const { data, isLoading } = todosApi.useGetTodoByIdQuery(todoId);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
return {
|
||||
updateTodoState: (todoId: string, todoState: StatusState) => {
|
||||
socketContext.updateTodo(todoId, todoState);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useSelectTodo = (todoId: string): { todo: Todo | undefined } => {
|
||||
const socketContext = useContext(SocketContext);
|
||||
|
||||
useEffect(() => {
|
||||
socketContext.getTodoById(todoId);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
todo: socketContext.todos.find((t) => t.id === todoId),
|
||||
todo: data,
|
||||
isLoading: isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUpdateTodo = () => {
|
||||
const socketContext = useContext(SocketContext);
|
||||
const [replaceTodo, { isLoading }] = todosApi.useReplaceTodoMutation();
|
||||
|
||||
return {
|
||||
updateTodo: (todo: Todo) => {
|
||||
socketContext.replaceTodo(todo);
|
||||
replaceTodo(todo);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -875,7 +875,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
|
||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||
@ -1107,6 +1107,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
|
||||
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
|
||||
|
||||
"@reduxjs/toolkit@^1.6.2":
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37"
|
||||
integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==
|
||||
dependencies:
|
||||
immer "^9.0.6"
|
||||
redux "^4.1.0"
|
||||
redux-thunk "^2.3.0"
|
||||
reselect "^4.0.0"
|
||||
|
||||
"@rollup/plugin-babel@^5.2.0":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879"
|
||||
@ -1182,6 +1192,14 @@
|
||||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/hoist-non-react-statics@^3.3.0":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
|
||||
"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
|
||||
version "7.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
||||
@ -1212,6 +1230,25 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||
|
||||
"@types/react-redux@^7.1.20":
|
||||
version "7.1.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.20.tgz#42f0e61ababb621e12c66c96dda94c58423bd7df"
|
||||
integrity sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw==
|
||||
dependencies:
|
||||
"@types/hoist-non-react-statics" "^3.3.0"
|
||||
"@types/react" "*"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
redux "^4.0.0"
|
||||
|
||||
"@types/react@*":
|
||||
version "17.0.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.35.tgz#217164cf830267d56cd1aec09dcf25a541eedd4c"
|
||||
integrity sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^17.0.34":
|
||||
version "17.0.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.34.tgz#797b66d359b692e3f19991b6b07e4b0c706c0102"
|
||||
@ -3060,6 +3097,13 @@ hmac-drbg@^1.0.1:
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||
dependencies:
|
||||
react-is "^16.7.0"
|
||||
|
||||
hsl-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
|
||||
@ -3132,6 +3176,11 @@ image-size@1.0.0:
|
||||
dependencies:
|
||||
queue "6.0.2"
|
||||
|
||||
immer@^9.0.6:
|
||||
version "9.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
||||
integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
|
||||
|
||||
import-cwd@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92"
|
||||
@ -4574,16 +4623,28 @@ react-dom@17.0.2:
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-is@17.0.2:
|
||||
react-is@17.0.2, react-is@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-is@^16.8.1:
|
||||
react-is@^16.7.0, react-is@^16.8.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-redux@^7.2.6:
|
||||
version "7.2.6"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa"
|
||||
integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
"@types/react-redux" "^7.1.20"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
react-refresh@0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
@ -4644,6 +4705,18 @@ reduce-css-calc@^2.1.8:
|
||||
css-unit-converter "^1.1.1"
|
||||
postcss-value-parser "^3.3.0"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.0.tgz#ac89e1d6b9bdb9ee49ce69a69071be41bbd82d67"
|
||||
integrity sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA==
|
||||
|
||||
redux@^4.0.0, redux@^4.1.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
|
||||
integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
|
||||
regenerate-unicode-properties@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
|
||||
@ -4720,6 +4793,11 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
reselect@^4.0.0:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.4.tgz#66df0aff41b6ee0f51e2cc17cfaf2c1995916f32"
|
||||
integrity sha512-i1LgXw8DKSU5qz1EV0ZIKz4yIUHJ7L3bODh+Da6HmVSm9vdL/hG7IpbgzQ3k2XSirzf8/eI7OMEs81gb1VV2fQ==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
|
Loading…
Reference in New Issue
Block a user