mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-25 01:14:23 +01:00
feat: workspaces crud (resolves #15)
This commit is contained in:
parent
f634839adb
commit
b3a317dc4d
@ -18,18 +18,18 @@ const ModalContent = ({children}) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled}) => {
|
const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled, hideCancel}) => {
|
||||||
confirmText = confirmText || 'Save';
|
confirmText = confirmText || 'Save';
|
||||||
cancelText = cancelText || 'Cancel';
|
cancelText = cancelText || 'Cancel';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end p-4 bruno-modal-footer">
|
<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">
|
<button type="button" onClick={handleCancel} className="btn btn-md btn-close">
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span className="">
|
<span>
|
||||||
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit} >
|
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit} >
|
||||||
{confirmText}
|
{confirmText}
|
||||||
</button>
|
</button>
|
||||||
@ -46,7 +46,8 @@ const Modal = ({
|
|||||||
handleCancel,
|
handleCancel,
|
||||||
handleConfirm,
|
handleConfirm,
|
||||||
children,
|
children,
|
||||||
confirmDisabled
|
confirmDisabled,
|
||||||
|
hideCancel
|
||||||
}) => {
|
}) => {
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const escFunction = (event) => {
|
const escFunction = (event) => {
|
||||||
@ -84,6 +85,7 @@ const Modal = ({
|
|||||||
handleCancel={() => closeModal()}
|
handleCancel={() => closeModal()}
|
||||||
handleSubmit={handleConfirm}
|
handleSubmit={handleConfirm}
|
||||||
confirmDisabled={confirmDisabled}
|
confirmDisabled={confirmDisabled}
|
||||||
|
hideCancel={hideCancel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
|
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
|
||||||
|
8
renderer/components/Portal/index.js
Normal file
8
renderer/components/Portal/index.js
Normal 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;
|
@ -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;
|
||||||
|
|
@ -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;
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
@ -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;
|
@ -1,36 +1,28 @@
|
|||||||
import Modal from "components/Modal/index";
|
import Modal from "components/Modal/index";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import WorkspaceItem from "./WorkspaceItem/index";
|
||||||
|
import AddModal from "./AddModal";
|
||||||
|
|
||||||
const WorkspaceConfigurer = ({onClose}) => {
|
const WorkspaceConfigurer = ({onClose}) => {
|
||||||
const { workspaces } = useSelector((state) => state.workspaces);
|
const { workspaces } = useSelector((state) => state.workspaces);
|
||||||
|
const [openAddModal, setOpenAddModal] = useState(false);
|
||||||
const onSubmit = () => {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
size="md"
|
size="md"
|
||||||
title="Workspaces"
|
title="Workspaces"
|
||||||
confirmText="Create"
|
confirmText={"+ New Workspace"}
|
||||||
handleConfirm={onSubmit}
|
handleConfirm={() => setOpenAddModal(true)}
|
||||||
handleCancel={onClose}
|
handleCancel={onClose}
|
||||||
|
hideCancel={true}
|
||||||
>
|
>
|
||||||
<ul className="mb-2">
|
<ul className="mb-2" >
|
||||||
{workspaces && workspaces.length && workspaces.map((workspace) => (
|
{workspaces && workspaces.length && workspaces.map((workspace) => (
|
||||||
<div className="flex justify-between items-baseline w-4/5 mb-2">
|
<WorkspaceItem workspace={workspace} />
|
||||||
<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>
|
</ul>
|
||||||
|
{openAddModal && <AddModal onClose={() => setOpenAddModal(false)}/>}
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export default class MyDocument extends Document {
|
|||||||
<Head>
|
<Head>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body id="bruno-app-body">
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit'
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { uuid } from 'utils/common';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
workspaces: [{
|
workspaces: [{
|
||||||
@ -21,11 +22,33 @@ export const workspacesSlice = createSlice({
|
|||||||
selectWorkspace: (state, action) => {
|
selectWorkspace: (state, action) => {
|
||||||
state.activeWorkspaceUid = action.payload.uid;
|
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 {
|
export const {
|
||||||
selectWorkspace
|
selectWorkspace,
|
||||||
|
renameWorkspace,
|
||||||
|
deleteWorkspace,
|
||||||
|
addWorkspace
|
||||||
} = workspacesSlice.actions;
|
} = workspacesSlice.actions;
|
||||||
|
|
||||||
export default workspacesSlice.reducer;
|
export default workspacesSlice.reducer;
|
||||||
|
Loading…
Reference in New Issue
Block a user