feat: workspaces crud (resolves #15)

This commit is contained in:
Anusree Subash 2022-10-08 20:29:50 +05:30
parent a17b6bef7a
commit 3bcf35bc2f
11 changed files with 285 additions and 27 deletions

View File

@ -18,18 +18,18 @@ const ModalContent = ({children}) => (
</div>
);
const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled}) => {
const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled, hideCancel}) => {
confirmText = confirmText || 'Save';
cancelText = cancelText || 'Cancel';
return (
<div className="flex justify-end p-4 bruno-modal-footer">
<span className="mr-2">
<span className={hideCancel ? "hidden" : "mr-2"}>
<button type="button" onClick={handleCancel} className="btn btn-md btn-close">
{cancelText}
</button>
</span>
<span className="">
<span>
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit} >
{confirmText}
</button>
@ -46,7 +46,8 @@ const Modal = ({
handleCancel,
handleConfirm,
children,
confirmDisabled
confirmDisabled,
hideCancel
}) => {
const [isClosing, setIsClosing] = useState(false);
const escFunction = (event) => {
@ -84,6 +85,7 @@ const Modal = ({
handleCancel={() => closeModal()}
handleSubmit={handleConfirm}
confirmDisabled={confirmDisabled}
hideCancel={hideCancel}
/>
</div>
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />

View File

@ -0,0 +1,8 @@
import { createPortal } from 'react-dom';
function Portal({ children, wrapperId }) {
wrapperId = wrapperId || "bruno-app-body";
return createPortal(children, document.getElementById(wrapperId));
}
export default Portal;

View File

@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react';
import Portal from "components/Portal/index";
import Modal from "components/Modal/index";
import { useFormik } from 'formik';
import { addWorkspace } from 'providers/ReduxStore/slices/workspaces';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const AddWorkspace = ({onClose}) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: ""
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(30, 'must be 30 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
dispatch(addWorkspace({name: values.name}));
onClose();
}
});
useEffect(() => {
if(inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => {
formik.handleSubmit();
}
return (
<Portal>
<Modal
size="sm"
title={"Add Workspace"}
confirmText='Add'
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">Workpsace Name</label>
<input
id="workspace-name" type="text" name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
</div>
</form>
</Modal>
</Portal>
);
}
export default AddWorkspace;

View File

@ -0,0 +1,15 @@
import styled from 'styled-components';
const Wrapper = styled.div`
button.submit {
color: white;
background-color: var(--color-background-danger) !important;
border: inherit !important;
&:hover {
border: inherit !important;
}
}
`;
export default Wrapper;

View File

@ -0,0 +1,34 @@
import React from 'react';
import Portal from "components/Portal/index";
import Modal from "components/Modal/index";
import { deleteWorkspace } from 'providers/ReduxStore/slices/workspaces';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const DeleteWorkspace = ({onClose, workspace}) => {
const dispatch = useDispatch();
const onConfirm = () =>{
dispatch(deleteWorkspace({workspaceUid: workspace.uid}))
onClose();
};
return (
<Portal>
<StyledWrapper>
<Modal
size="sm"
title={"Delete Workspace"}
confirmText="Delete"
handleConfirm={onConfirm}
handleCancel={onClose}
>
Are you sure you want to delete <span className="font-semibold">{workspace.name}</span> ?
</Modal>
</StyledWrapper>
</Portal>
);
}
export default DeleteWorkspace;

View File

@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react';
import Portal from "components/Portal/index";
import Modal from "components/Modal/index";
import { useFormik } from 'formik';
import { renameWorkspace } from 'providers/ReduxStore/slices/workspaces';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const EditWorkspace = ({onClose, workspace}) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: workspace.name
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(30, 'must be 30 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
dispatch(renameWorkspace({name: values.name, uid: workspace.uid}));
onClose();
}
});
useEffect(() => {
if(inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => {
formik.handleSubmit();
}
return (
<Portal>
<Modal
size="sm"
title={"Rename Workspace"}
confirmText='Rename'
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">Workpsace Name</label>
<input
id="workspace-name" type="text" name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
</div>
</form>
</Modal>
</Portal>
);
}
export default EditWorkspace;

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
const Wrapper = styled.div`
div {
padding: 4px 6px;
padding-left: 8px;
display: flex;
align-items: center;
border-radius: 3px;
}
div:hover {
background-color: #f4f4f4;
}
`;
export default Wrapper;

View File

@ -0,0 +1,27 @@
import React, { useState } from "react";
import EditModal from "../EditModal";
import DeleteModal from "../DeleteModal";
import { IconEdit, IconTrash } from "@tabler/icons";
import StyledWrapper from "./StyledWrapper";
const WorkspaceItem = ({workspace}) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
return (
<StyledWrapper>
<div className="flex justify-between items-baseline mb-2" key={workspace.uid} >
<li>{workspace.name}</li>
<div className="flex gap-x-4" >
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
</div>
{openEditModal && <EditModal onClose={() => setOpenEditModal(false)} workspace={workspace} />}
{openDeleteModal && <DeleteModal onClose={() => setOpenDeleteModal(false)} workspace={workspace} />}
</div>
</StyledWrapper>
)
}
export default WorkspaceItem;

View File

@ -1,36 +1,28 @@
import Modal from "components/Modal/index";
import React from "react";
import React, { useState } from "react";
import { useSelector } from "react-redux";
import WorkspaceItem from "./WorkspaceItem/index";
import AddModal from "./AddModal";
const WorkspaceConfigurer = ({onClose}) => {
const { workspaces } = useSelector((state) => state.workspaces);
const onSubmit = () => {
onClose();
}
const [openAddModal, setOpenAddModal] = useState(false);
return (
<Modal
size="md"
title="Workspaces"
confirmText="Create"
handleConfirm={onSubmit}
confirmText={"+ New Workspace"}
handleConfirm={() => setOpenAddModal(true)}
handleCancel={onClose}
hideCancel={true}
>
<ul className="mb-2">
{workspaces && workspaces.length && workspaces.map((workspace) => (
<div className="flex justify-between items-baseline w-4/5 mb-2">
<li key={workspace.uid}>{workspace.name}</li>
<button
style={{backgroundColor: "var(--color-brand)"}}
className="flex items-center h-full text-white active:bg-blue-600 font-bold text-xs px-4 py-2 ml-2 uppercase rounded shadow hover:shadow-md outline-none focus:outline-none ease-linear transition-all duration-150"
onClick={() => console.log("delete")}
>
<span style={{marginLeft: 5}}>Delete</span>
</button>
</div>
))}
<ul className="mb-2" >
{workspaces && workspaces.length && workspaces.map((workspace) => (
<WorkspaceItem workspace={workspace} />
))}
</ul>
{openAddModal && <AddModal onClose={() => setOpenAddModal(false)}/>}
</Modal>
)

View File

@ -34,7 +34,7 @@ export default class MyDocument extends Document {
<Head>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
</Head>
<body>
<body id="bruno-app-body">
<Main />
<NextScript />
</body>

View File

@ -1,4 +1,5 @@
import { createSlice } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit';
import { uuid } from 'utils/common';
const initialState = {
workspaces: [{
@ -21,11 +22,33 @@ export const workspacesSlice = createSlice({
selectWorkspace: (state, action) => {
state.activeWorkspaceUid = action.payload.uid;
},
renameWorkspace: (state, action) => {
const { name, uid } = action.payload;
const { workspaces } = state;
const workspaceIndex = workspaces.findIndex(workspace => workspace.uid == uid);
workspaces[workspaceIndex].name = name;
},
deleteWorkspace: (state, action) => {
if(state.activeWorkspaceUid === action.payload.workspaceUid) {
throw new Error("User cannot delete current workspace");
}
state.workspaces = state.workspaces.filter((workspace) => workspace.uid !== action.payload.workspaceUid);
},
addWorkspace: (state, action) => {
const newWorkspace = {
uid: uuid(),
name: action.payload.name
}
state.workspaces.push(newWorkspace);
}
}
});
export const {
selectWorkspace
selectWorkspace,
renameWorkspace,
deleteWorkspace,
addWorkspace
} = workspacesSlice.actions;
export default workspacesSlice.reducer;