Merge branch 'main' into test/rest-playground

This commit is contained in:
Nishchit14
2023-08-04 12:53:00 +05:30
56 changed files with 2211 additions and 836 deletions

View File

@@ -1,17 +1,20 @@
{
"name": "firecamp",
"version": "3.0.0",
"version": "3.2.1",
"private": true,
"description": "Universal API testing and collaboration platform",
"main": "packages/firecamp-desktop-app/dist/services/Main",
"homepage": "./dev",
"scripts": {
"build:workspace": "pnpm --filter=@firecamp/scripts --filter=@firecamp/rest-executor --filter=@firecamp/ws-executor --filter=@firecamp/socket.io-executor build",
"boot": "pnpm install --shamefully-hoist",
"bootstrap": "pnpm install --shamefully-hoist",
"start": "npx webpack serve --config ./webpack.dev.js",
"dev": "APP_VERSION=$npm_package_version AppFormat=webapp && node scripts/build && pnpm build:workspace && pnpm start",
"release:web": "AppFormat=webapp && pnpm build:workspace && node scripts/release",
"dev": "run-p build:workspace webpack:dev",
"build": "run-s validate:release build:workspace webpack:prod",
"build:workspace": "pnpm --filter=@firecamp/scripts --filter=@firecamp/rest-executor --filter=@firecamp/ws-executor --filter=@firecamp/socket.io-executor build",
"webpack:dev": "webpack serve --config ./webpack.dev.js",
"webpack:prod": "webpack --config ./webpack.prod.js",
"validate:release": "node scripts/release",
"release:web": "pnpm build",
"lint": "eslint packages/firecamp-rest/src/**/*.{ts|tsx} packages/*.js packages-clients/*.js scripts webpack/*.js",
"test": "jest",
"prettify": "prettier --write \"platform/firecamp-platform/src/**/*.(ts|tsx)\" \"packages/firecamp-rest/src/**/*.(ts|tsx)\" \"packages/firecamp-graphql/src/**/*.(ts|tsx)\"",
@@ -76,6 +79,7 @@
"jest-css-modules-transform": "^4.4.2",
"lint-staged": "^13.1.2",
"node-polyfill-webpack-plugin": "^2.0.0",
"npm-run-all": "^4.1.5",
"postcss-loader": "^7.0.2",
"prettier": "^2.6.2",
"react-addons-test-utils": "^15.0.2",
@@ -85,6 +89,7 @@
"semver": "^7.3.5",
"shelljs": "^0.8.5",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.9",
"ts-loader": "^9.2.8",
"ts-node": "^10.9.1",
"typescript": "^5.0.2",
@@ -95,6 +100,7 @@
"webpack-hot-middleware": "^2.25.1",
"webpack-html-plugin": "^0.1.1",
"webpack-httpolyglot-server": "^0.3.0",
"webpack-merge": "^5.9.0",
"worker-loader": "^3.0.8"
},
"dependencies": {

View File

@@ -50,7 +50,7 @@
},
"dependencies": {
"@firecamp/agent-manager": "workspace:*",
"@firecamp/cloud-apis": "^0.2.8",
"@firecamp/cloud-apis": "0.2.10",
"@firecamp/cookie-manager": "^0.0.0",
"@firecamp/graphql": "workspace:*",
"@firecamp/rest": "workspace:*",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -26,6 +26,7 @@ import { useTabStore } from '../../../store/tab';
import { ETabEntityTypes } from '../../tabs/types';
import platformContext from '../../../services/platform-context';
import { useExplorerStore } from '../../../store/explorer';
import useExplorerFacade from './useExplorerFacade';
const Explorer: FC<any> = () => {
const explorerTreeRef = useRef();
@@ -43,29 +44,14 @@ const Explorer: FC<any> = () => {
fetchExplorer,
updateCollection,
updateFolder,
moveRequest,
moveFolder,
// moveRequest,
// moveFolder,
changeCollectionChildrenPosition,
changeFolderChildrenPosition,
deleteCollection,
deleteFolder,
deleteRequest,
} = useExplorerStore(
(s) => ({
explorer: s.explorer,
fetchExplorer: s.fetchExplorer,
updateCollection: s.updateCollection,
updateFolder: s.updateFolder,
moveRequest: s.moveRequest,
moveFolder: s.moveFolder,
changeCollectionChildrenPosition: s.changeCollectionChildrenPosition,
changeFolderChildrenPosition: s.changeFolderChildrenPosition,
deleteCollection: s.deleteCollection,
deleteFolder: s.deleteFolder,
deleteRequest: s.deleteRequest,
}),
shallow
);
// deleteCollection,
// deleteFolder,
// deleteRequest,
} = useExplorerFacade();
const { isProgressing, collections, folders, requests } = explorer;
@@ -457,31 +443,6 @@ const Explorer: FC<any> = () => {
</Container>
</div>
);
return (
<div className="w-full h-full flex flex-row">
<Container>
<Container.Body>
<div className="flex flex-col mt-8 p-4 items-center justify-center mt-2">
<div className="fc-sidebar-noproject-icon text-5xl opacity-20 mb-2"></div>
{!!searchString ? (
<div className="text-sm text-app-foreground-inactive text-center mb-1">
<span className="text-base block mb-2">No search found...</span>
Your search is not found within this workspace.
</div>
) : (
<div className="text-sm text-app-foreground-inactive text-center mb-1">
<span className="text-app-foreground text-base block mb-2">
Create your first API collection!
</span>
You don't have any API collection in this workspace.
</div>
)}
</div>
</Container.Body>
</Container>
</div>
);
};
export default Explorer;

View File

@@ -0,0 +1,23 @@
import shallow from 'zustand/shallow';
import { useExplorerStore } from '../../../store/explorer';
const useExplorerFacade = () => {
return useExplorerStore(
(s) => ({
explorer: s.explorer,
fetchExplorer: s.fetchExplorer,
updateCollection: s.updateCollection,
updateFolder: s.updateFolder,
// moveRequest: s.moveRequest,
// moveFolder: s.moveFolder,
changeCollectionChildrenPosition: s.changeCollectionChildrenPosition,
changeFolderChildrenPosition: s.changeFolderChildrenPosition,
// deleteCollection: s.deleteCollection,
// deleteFolder: s.deleteFolder,
// deleteRequest: s.deleteRequest,
}),
shallow
);
};
export default useExplorerFacade;

View File

@@ -1,15 +1,15 @@
import { useState } from 'react';
import classnames from 'classnames';
import { RiBracesLine } from '@react-icons/all-files/ri/RiBracesLine';
import { VscArrowDown } from '@react-icons/all-files/vsc/VscArrowDown';
import { VscFolder } from '@react-icons/all-files/vsc/VscFolder';
import { VscOrganization } from '@react-icons/all-files/vsc/VscOrganization';
import { AiOutlineUserAdd } from '@react-icons/all-files/ai/AiOutlineUserAdd';
import { AiOutlineUserSwitch } from '@react-icons/all-files/ai/AiOutlineUserSwitch';
import { VscMultipleWindows } from '@react-icons/all-files/vsc/VscMultipleWindows';
import { VscWindow } from '@react-icons/all-files/vsc/VscWindow';
import { VscTriangleDown } from '@react-icons/all-files/vsc/VscTriangleDown';
import {
Triangle,
MailOpen,
FolderClosed,
Braces,
ArrowDown,
AppWindow,
UserPlus2,
} from 'lucide-react';
import { Button, DropdownMenu, FcIconGetSquare } from '@firecamp/ui';
import platformContext from '../../services/platform-context';
@@ -27,61 +27,83 @@ enum EMenuOptions {
InviteMembers = 'invite-members',
SwitchOrg = 'switch-org',
SwitchWorkspace = 'switch-workspace',
AllInvitation = 'all-invitation',
}
const options = [
{
id: EMenuOptions.Request,
name: 'New request',
prefix: () => <FcIconGetSquare size={16} className='text-app-foreground-active' />,
prefix: () => (
<FcIconGetSquare size={16} className="text-app-foreground-active" />
),
},
{
id: EMenuOptions.Collection,
name: 'New collection',
prefix: () => <VscFolder size={16} className='text-app-foreground-active' />,
prefix: () => (
<FolderClosed size={16} className="text-app-foreground-active" />
),
},
{
id: EMenuOptions.Environment,
name: 'New environment',
prefix: () => <RiBracesLine size={16} className='text-app-foreground-active' />,
prefix: () => <Braces size={16} className="text-app-foreground-active" />,
},
{
id: EMenuOptions.ImportCollection,
name: 'Import collection',
showSeparator: true,
prefix: () => <VscArrowDown size={16} className='text-app-foreground-active' />,
prefix: () => (
<ArrowDown size={16} className="text-app-foreground-active" />
),
},
{
id: EMenuOptions.Workspace,
name: 'New workspace',
prefix: () => <VscWindow size={16} className='text-app-foreground-active' />,
prefix: () => (
<AppWindow size={16} className="text-app-foreground-active" />
),
},
{
id: EMenuOptions.Organization,
name: 'New organization',
prefix: () => <VscOrganization size={16} className='text-app-foreground-active' />,
prefix: () => (
<VscOrganization size={16} className="text-app-foreground-active" />
),
},
{
id: EMenuOptions.InviteMembers,
name: 'Invite members',
showSeparator: true,
prefix: () => <AiOutlineUserAdd size={16} className='text-app-foreground-active' />,
prefix: () => (
<UserPlus2 size={16} className="text-app-foreground-active" />
),
},
{
id: EMenuOptions.SwitchOrg,
name: 'Switch organization',
prefix: () => <AiOutlineUserSwitch size={16} className='text-app-foreground-active' />,
prefix: () => (
<AiOutlineUserSwitch size={16} className="text-app-foreground-active" />
),
},
{
id: EMenuOptions.SwitchWorkspace,
name: 'Switch workspace',
prefix: () => <VscMultipleWindows size={16} className='text-app-foreground-active' />,
prefix: () => (
<VscMultipleWindows size={16} className="text-app-foreground-active" />
),
showSeparator: true,
},
{
id: EMenuOptions.AllInvitation,
name: 'View invitation',
prefix: () => <MailOpen size={16} className="text-app-foreground-active" />,
},
];
const GlobalCreateDD = ({}) => {
const [isOpen, toggleOpen] = useState(false);
const { open } = useTabStore.getState();
const onSelect = (option) => {
switch (option.id) {
@@ -113,17 +135,19 @@ const GlobalCreateDD = ({}) => {
case EMenuOptions.SwitchWorkspace:
platformContext.app.modals.openSwitchWorkspace();
break;
case EMenuOptions.AllInvitation:
platformContext.app.modals.openAllInvitation();
break;
}
};
return (
<div className="border-l border-b border-tab-border flex items-center pl-1">
<DropdownMenu
onOpenChange={(v) => toggleOpen(v)}
handler={() => (
<Button
text={'Create'}
rightIcon={<VscTriangleDown size={12} className={classnames({'transform rotate-180': isOpen})}/>}
leftIcon={<Triangle size={20} />}
animate={false}
transparent
primary
compact
@@ -131,9 +155,10 @@ const GlobalCreateDD = ({}) => {
/>
)}
options={options}
footer={<div className="mt-1">v{process.env.APP_VERSION}</div>}
onSelect={onSelect}
classNames={{
dropdown: '-ml-[2px]',
dropdown: '-ml-[2px] pb-0',
}}
/>
</div>

View File

@@ -5,12 +5,6 @@ import { Modal } from '@firecamp/ui';
import './ErrorPopup.sass';
const ErrorPopup: FC<FallbackProps> = ({ error }) => {
const bg = {
modal: {
background: '#c84a1782',
color: '#e3dfdf',
},
};
let [isOpen, toggleOpen] = useState(true);
@@ -24,7 +18,6 @@ const ErrorPopup: FC<FallbackProps> = ({ error }) => {
return (
<Modal
opened={isOpen}
styles={{ content: bg.modal }}
onClose={_onClose}
className="fc-error-popup"
>

View File

@@ -240,8 +240,7 @@ const SidebarContainer: FC<any> = () => {
const _setActiveItem = async (selected) => {
if (!selected?.id) return;
else if (selected.item == EActivityBarItems.User) {
window.open('https://app.firecamp.dev/profile/info');
platformContext.app.modals.openSaveRequest(); // openUserProfile();
platformContext.app.modals.openUserProfile();
} else if (selected.item == EActivityBarItems.Settings)
platformContext.app.modals.openWorkspaceManagement();
else if (selected.item == EActivityBarItems.SslNProxy)

View File

@@ -18,6 +18,8 @@ import WorkspaceManagement from './workspace/WorkspaceManagement';
import SwitchWorkspace from './workspace/SwitchWorkspace';
import CloneEnvironment from './environment/CloneEnvironment';
import EditRequest from './request/edit-request/EditRequest';
import ProfileManagement from './profile/ProfileManagement';
import AllInvitation from './invitation/AllInvitation';
export const ModalContainer = () => {
const { currentOpenModal, isOpen, close } = useModalStore(
@@ -58,7 +60,8 @@ export const ModalContainer = () => {
return <CloneEnvironment opened={isOpen} onClose={close} />;
// User
// case EPlatformModalTypes.UserProfile: return <UserProfile isOpen={isOpen} onClose={close} />;
case EPlatformModalTypes.UserProfile:
return <ProfileManagement opened={isOpen} onClose={close} />;
// Auth
case EPlatformModalTypes.SignIn:
@@ -73,6 +76,8 @@ export const ModalContainer = () => {
return <ResetPassword opened={isOpen} onClose={close} />;
case EPlatformModalTypes.RefreshToken:
return <RefreshToken opened={isOpen} onClose={close} />;
case EPlatformModalTypes.AllInvitation:
return <AllInvitation opened={isOpen} onClose={close} />;
default:
return <></>;
}

View File

@@ -51,7 +51,7 @@ const ForgotPassword: FC<IModal> = ({ opened = false, onClose = () => {} }) => {
const _onKeyDown = (e: any) => e.key === 'Enter' && handleSubmit(_onSubmit);
return (
<Drawer opened={opened} onClose={onClose} size={440}>
<Drawer opened={opened} onClose={onClose} size={440} classNames={{body: 'mt-[10vh]'}}>
<Mail
size="48"
className="mb-6 mx-auto text-activityBar-foreground-inactive"

View File

@@ -11,7 +11,7 @@ import platformContext from '../../../services/platform-context';
*/
const SignIn: FC<IModal> = ({ opened, onClose }) => {
return (
<Drawer opened={opened} onClose={onClose} size={440}>
<Drawer opened={opened} onClose={onClose} size={440} classNames={{body: 'mt-[10vh]'}}>
{/* <img className="mx-auto w-12 mb-6" src={'img/firecamp-logo.svg'} /> */}
<div className="mb-4">
<FcLogo className="mx-auto w-14" size={80} />

View File

@@ -62,7 +62,7 @@ const SignInWithEmail: FC<IModal> = ({ opened, onClose }) => {
};
return (
<Drawer opened={opened} onClose={onClose} size={440}>
<Drawer opened={opened} onClose={onClose} size={440} classNames={{body: 'mt-[10vh]'}}>
<div className="mb-2">
<FcLogo className="mx-auto w-14" size={80} />
</div>

View File

@@ -68,7 +68,7 @@ const SignUp: FC<IModal> = ({ opened, onClose }) => {
const _onKeyDown = (e) => e.key === 'Enter' && handleSubmit(_onSignUp);
return (
<Drawer opened={opened} onClose={onClose} size={440}>
<Drawer opened={opened} onClose={onClose} size={440} classNames={{body: 'mt-[10vh]'}}>
{/* <img className="mx-auto w-12 mb-6" src={'img/firecamp-logo.svg'} /> */}
<div className="-mt-4">
<FcLogo className="mx-auto w-14" size={80} />
@@ -101,7 +101,10 @@ const SignUp: FC<IModal> = ({ opened, onClose }) => {
required: true,
maxLength: 50,
minLength: 1,
pattern: /^[0-9a-zA-Z ]+$/,
pattern: {
value: /^[0-9a-zA-Z ]+$/,
message: "The username should not have any special characters"
},
}}
useformRef={form}
onKeyDown={_onKeyDown}
@@ -110,7 +113,7 @@ const SignUp: FC<IModal> = ({ opened, onClose }) => {
? errors?.username?.message || 'Please enter username'
: ''
}
wrapperClassName="!mb-2"
wrapperClassName="!mb-4"
/>
<Input
placeholder="Enter your email"
@@ -132,7 +135,7 @@ const SignUp: FC<IModal> = ({ opened, onClose }) => {
'Please enter valid username or password'
: ''
}
wrapperClassName="!mb-2"
wrapperClassName="!mb-4"
/>
<Input
placeholder="Enter password"
@@ -163,7 +166,7 @@ const SignUp: FC<IModal> = ({ opened, onClose }) => {
? errors?.password?.message || 'Please enter valid password'
: ''
}
wrapperClassName="!mb-3"
wrapperClassName="!mb-4"
/>
<Button

View File

@@ -118,9 +118,6 @@ const CloneEnvironment: FC<IModal> = ({ opened, onClose = () => {} }) => {
opened={opened}
onClose={onClose}
size={500}
classNames={{
content: 'h-[750px]'
}}
title={
!isFetching ? (
<>

View File

@@ -0,0 +1,134 @@
import { FC, useEffect, useState } from 'react';
import { _array } from '@firecamp/utils';
import { Container, Drawer, IModal, ProgressBar } from '@firecamp/ui';
import InvitationCard from './InvitationCard';
import platformContext from '../../../services/platform-context';
import { IInvite } from './InvitationCard.interface';
import { Rest } from '@firecamp/cloud-apis';
const AllInvitation: FC<IModal> = ({ opened, onClose }) => {
const [list, updateList] = useState<Array<IInvite> | []>([]);
const [inviteId, updateInviteId] = useState('');
const [isRequesting, setIsRequesting] = useState(false);
const [isFetching, setIsFetching] = useState(false);
useEffect(() => {
setIsFetching(true);
Rest.invitation
.getMyPendingInvitations()
.then((res) => res.data)
.then((res) => {
const { error } = res;
if (!error) {
updateList(res);
}
})
.finally(() => setIsFetching(false));
}, []);
const switchToWrs = async (wrs: any, org: any) => {
await platformContext.app.switchWorkspace(wrs, org);
platformContext.app.modals.close();
};
const _handleInvitation = async (invite: IInvite) => {
if (isRequesting) return;
setIsRequesting(true);
updateInviteId(invite.token);
Rest.invitation
.accept(invite.token)
.then((res) => res.data)
.then(({ flag, data, message }) => {
if (flag) {
const { org, workspace } = data;
platformContext.app.notify.success(
'You have successfully joined the invitation'
);
platformContext.window.confirm({
message:
'Congratulations on joining the invitation! Are you interested in switching workspaces and start collaboration?',
labels: { confirm: 'Yes, switch workspace.' },
onConfirm: () => switchToWrs(workspace, org),
onCancel: () => {
setIsRequesting(false);
updateInviteId('');
},
});
let Index = list.findIndex((i) => i.token === invite.token);
updateList((list) => [
...list.slice(0, Index),
...list.slice(Index + 1),
]);
} else {
platformContext.app.notify.alert(message);
}
})
.catch((e) => {
platformContext.app.notify.alert(e.response?.data.message || e.message);
})
.finally(() => {
setIsRequesting(false);
updateInviteId('');
});
};
return (
<Drawer
opened={opened}
onClose={onClose}
size={600}
title={
<div className="text-lg leading-5 px-3 flex items-center font-medium">
All Invitation
</div>
}
>
<Container className="py-4">
<ProgressBar active={isFetching} />
{!isFetching ? (
<Container.Body>
{!_array.isEmpty(list) ? (
list.map((invite, index) => (
<InvitationCard
key={index}
inviterName={invite.inviterName}
orgName={invite.orgName}
workspaceName={invite.workspaceName}
role={invite.role}
isRequesting={isRequesting}
disabled={inviteId.length > 0 && inviteId === invite.token}
onAccept={() => _handleInvitation(invite)}
/>
))
) : (
<NoInvitationFound />
)}
</Container.Body>
) : (
<></>
)}
</Container>
</Drawer>
);
};
export default AllInvitation;
const NoInvitationFound = () => {
return (
<div className="p-8 flex flex-col justify-center items-center">
<div className="text-sm max-w-xs mx-auto text-center px-10">
<label className="font-semibold text-app-foreground uppercase">
No Invitation Found
</label>
<span className="block font-normal text-app-foreground-inactive">
You do not have any new invitations at this time.
</span>
</div>
</div>
);
};

View File

@@ -0,0 +1,13 @@
export interface IInvite {
inviterName: string;
orgName: string;
workspaceName: string;
role: number;
token?: string;
}
export type IInvitationCard = IInvite & {
disabled: boolean;
onAccept: () => void;
isRequesting: boolean;
};

View File

@@ -0,0 +1,66 @@
import { FC } from 'react';
import { Button, TabHeader } from '@firecamp/ui';
import { EUserRolesWorkspace } from '../../../types';
import { IInvitationCard } from './InvitationCard.interface';
const RoleOptions = [
{
id: EUserRolesWorkspace.Owner,
name: 'Owner',
},
{
id: EUserRolesWorkspace.Admin,
name: 'Admin',
},
{
id: EUserRolesWorkspace.Collaborator,
name: 'Collaborator',
},
{
id: EUserRolesWorkspace.Viewer,
name: 'Viewer',
},
];
const InvitationCard: FC<IInvitationCard> = ({
inviterName,
workspaceName,
orgName,
role,
disabled = false,
isRequesting = false,
onAccept,
}) => {
const userRole = RoleOptions.find((r) => r.id == role);
// console.log(`user.role`, userRole, role);
return (
<div className="bg-app-background p-4 shadow-sm rounded border mb-4">
{/* <label className="text-sm font-semibold leading-3 block text-app-foreground-inactive uppercase w-full relative py-4">
NEW INVITATION
</label> */}
<div className="my-4">
<span className="font-semibold">{inviterName}</span>
<span> has invited you to collaborate on the </span>
<span className="font-semibold">{orgName}</span>
<span>/</span>
<span className="font-semibold">{workspaceName}</span>
<span> as </span>
<span className="font-semibold">{userRole?.name}</span>
</div>
<TabHeader className="!px-0">
<TabHeader.Right>
<Button
text={isRequesting ? 'Accepting...': 'Accept Invitation'}
disabled={disabled}
onClick={() => onAccept()}
primary
xs
/>
</TabHeader.Right>
</TabHeader>
</div>
);
};
export default InvitationCard;

View File

@@ -1,36 +1,175 @@
import { FC, useState } from 'react';
import {
Input,
TextArea,
TabHeader,
Button,
Modal,
IModal,
SecondaryTab,
} from '@firecamp/ui';
import { FC, useEffect, useState } from 'react';
import { Rest } from '@firecamp/cloud-apis';
import { _misc } from '@firecamp/utils';
import { VscEdit } from '@react-icons/all-files/vsc/VscEdit';
import { IModal, SecondaryTab, Drawer, ProgressBar } from '@firecamp/ui';
import EditOrganization from './tabs/EditOrganization';
import Members from './tabs/Members';
import BillingTab from './tabs/Billing';
import Workspaces from './tabs/Workspaces';
import { usePlatformStore } from '../../../store/platform';
import platformContext from '../../../services/platform-context';
import { Regex } from '../../../constants';
const OrgManagement: FC<IModal> = ({ opened = false, onClose = () => {} }) => {
// let { create, checkNameAvailability } = useWorkspaceStore((s: IWorkspaceStore)=>({
// create: s.create,
// checkNameAvailability: s.checkNameAvailability
// }))
enum ETabTypes {
Overview = 'overview',
Workspaces = 'workspaces',
Members = 'members',
Billing = 'billing',
}
const tabs = [
{ name: 'Overview', id: ETabTypes.Overview },
{ name: 'Workspaces', id: ETabTypes.Workspaces },
{ name: 'Members', id: ETabTypes.Members },
{ name: 'Billing', id: ETabTypes.Billing },
];
const tabs = [
{ name: 'Edit', id: 'edit' },
{ name: 'Members', id: 'members' },
{ name: 'Billing', id: 'billing' },
];
let [activeTab, setActiveTab] = useState<string>(tabs[0].id);
// to convert date into readable date
export const getFormalDate = (convertDate: string) => {
let date = new Date(convertDate);
if (date.toString() === 'Invalid Date') return 'N/A';
let dateString = date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
return dateString;
};
const OrgManagement: FC<IModal> = ({ opened = false, onClose = () => { } }) => {
const { organization, setOrg } = usePlatformStore((s) => ({
organization: s.organization,
setOrg: s.setOrg,
}));
const [activeTab, setActiveTab] = useState<string>(tabs[0].id);
const [isFetching, setIsFetching] = useState(false);
const [workspaces, updateWorkspaces] = useState([]);
const [members, updateMembers] = useState([]);
const [org, updOrg] = useState(organization);
const [error, setError] = useState({ name: '' });
const [isRequesting, setIsRequesting] = useState(false);
// getting organization's workspaces / members listing once
useEffect(() => {
if (activeTab === ETabTypes.Workspaces && workspaces.length === 0) {
setIsFetching(true);
Rest.organization
.getMyWorkspacesOfOrg(organization.__ref.id)
.then((res) => res.data)
.then((list) => {
updateWorkspaces(list);
})
.finally(() => setIsFetching(false));
} else if (activeTab === ETabTypes.Members && members.length === 0) {
setIsFetching(true);
Rest.organization
.getMembers(organization.__ref.id)
.then((res) => res.data)
.then((list) => {
updateMembers(list);
})
.finally(() => setIsFetching(false));
}
}, [activeTab]);
const onChange = (e, reset) => {
if (reset) {
updOrg(organization);
if (error.name) setError({ name: '' });
} else {
const { name, value } = e.target;
if (error.name) setError({ name: '' });
updOrg((o) => ({ ...o, [name]: value }));
}
};
const onUpdate = () => {
if (isRequesting) return;
const name = org.name.trim();
const description = org.description?.trim();
if (!name || name.length < 4) {
setError({
name: 'The organization name must have minimum 4 characters',
});
return;
}
const isValid = Regex.OrgName.test(name);
if (!isValid) {
setError({
name: 'The org name must not contain any spaces or special characters.',
});
return;
}
if (
organization.name === org.name &&
organization.description === org.description
)
return;
const _org: { name?: string; description?: string } = {};
if (organization.name !== name) {
_org.name = name;
}
if (organization.description !== description) {
_org.description = description;
}
setIsRequesting(true);
Rest.organization
.update(organization.__ref.id, _org)
.then((res) => res.data)
.then(({ error, message }) => {
if (!error) {
platformContext.app.notify.success(
"The organization's detail has been updated successfully."
);
setOrg({ ...organization, ..._org });
} else {
platformContext.app.notify.alert(message);
}
})
.catch((e) => {
platformContext.app.notify.alert(e.response?.data.message || e.message);
})
.finally(() => {
setIsRequesting(false);
});
};
const renderTab = (tabId: string) => {
switch (tabId) {
case 'edit':
return <EditInfoTab />;
case 'members':
return <MembersTab />;
case 'billing':
case ETabTypes.Overview:
return (
<EditOrganization
organization={org}
error={error}
isRequesting={isRequesting}
onSubmit={onUpdate}
onChange={onChange}
enableReset={
organization.name !== org.name ||
organization.description !== org.description
}
// disabled={true} // TODO: only allow owner
/>
);
case ETabTypes.Workspaces:
return <Workspaces workspaces={workspaces} isFetching={isFetching} />;
case ETabTypes.Members:
return (
<Members
members={members}
updateMembers={updateMembers}
isFetching={isFetching}
organizationId={organization.__ref.id}
/>
);
case ETabTypes.Billing:
return <BillingTab />;
default:
return <></>;
@@ -38,128 +177,25 @@ const OrgManagement: FC<IModal> = ({ opened = false, onClose = () => {} }) => {
};
return (
<Modal
<Drawer
opened={opened}
onClose={onClose}
size={600}
classNames={{
body: 'h-[80vh]',
}}
title={
<div className="text-lg leading-5 px-6 flex items-center font-medium">
<div className="text-lg leading-5 px-3 flex items-center font-medium">
Organization Management
</div>
}
>
<>
<SecondaryTab
className="flex items-center p-4 pb-0"
list={tabs}
activeTab={'edit'}
onSelect={setActiveTab}
/>
{renderTab(activeTab)}
</>
<div className="!py-3 border-t border-app-border ">
<TabHeader>
<TabHeader.Right>
<Button text="Cancel" onClick={(e) => onClose(e)} ghost xs />
<Button
text={false ? 'Creating...' : 'Create'}
onClick={() => {}}
disabled={false}
primary
xs
/>
</TabHeader.Right>
</TabHeader>
</div>
</Modal>
<ProgressBar active={isFetching} />
<SecondaryTab
className="pt-4"
list={tabs}
activeTab={activeTab}
onSelect={setActiveTab}
/>
{renderTab(activeTab)}
</Drawer>
);
};
export default OrgManagement;
const EditInfoTab: FC<any> = () => {
let [org, setOrg] = useState({
name: '',
description: '',
});
return (
<div className="px-6 py-3">
<label className="text-sm font-semibold leading-3 block text-app-foreground-inactive uppercase w-full relative mb-2">
ADD NEW WORKSPACE INFO
</label>
<div className="mt-4">
<Input
autoFocus={true}
label="Name"
placeholder="Organization name"
name={'name'}
value={org.name || ''}
onChange={() => {}}
onKeyDown={() => {}}
onBlur={() => {}}
disabled={true}
iconPosition="right"
icon={<VscEdit />}
// error={error.name}
/>
{/* {
flag_wrsNameCheckInProgress === false && flag_isWrsNameAvailable === undefined
? <Alert withBorder text="please type the workspace name to check its availability" info/>: <></>
}
{
flag_wrsNameCheckInProgress === true? <Alert withBorder text={`checking workspace name availability - ${workspace?.name}`} warning/>: <></>
}
{
flag_wrsNameCheckInProgress === false && flag_isWrsNameAvailable === true
? <Alert withBorder text={`the workspace name is available - ${workspace?.name}`} success/>: <></>
}
{
flag_wrsNameCheckInProgress === false && flag_isWrsNameAvailable === false
? <Alert withBorder text={`the workspace name is not available - ${workspace?.name}`} error/>: <></>
}
{
error.name?.length
? <Alert withBorder text={error.name} error/>: <></>
} */}
</div>
<TextArea
type="text"
minHeight="200px"
label="Description (optional)"
labelClassName="fc-input-label"
placeholder="Description"
note="Markdown supported in description"
name={'description'}
value={org.description || ''}
onChange={() => {}}
disabled={true}
iconPosition="right"
icon={<VscEdit />}
/>
{/* {error.global?.length ? (
<TabHeader.Left>
<div
style={{
fontSize: '12px',
color: 'red' //'green'
}}
>
{error.global}
</div>
</TabHeader.Left>
) : <></>} */}
</div>
);
};
const MembersTab = () => {
return <div className="p-6"> Members Tab</div>;
};
const BillingTab = () => {
return <div className="p-6"> Billing Tab</div>;
};

View File

@@ -0,0 +1,9 @@
const BillingTab = () => {
return <div className="p-4 text-activityBar-foreground-inactive">
<span className="my-4">
Coming soon...
</span>
</div>;
};
export default BillingTab;

View File

@@ -0,0 +1,65 @@
import { FC } from 'react';
import { Button, Container, Input, TabHeader, TextArea } from '@firecamp/ui';
const EditOrganization: FC<any> = ({
organization,
error,
isRequesting,
onSubmit,
onChange,
enableReset = true,
disabled = false
}) => {
return (
<Container className="py-6 px-3 flex-1 flex flex-col h-full">
<Container.Body>
<label className="text-sm font-semibold leading-3 block text-app-foreground-inactive uppercase w-full relative mb-4">
UPDATE ORGANIZATION INFO
</label>
<div>
<Input
autoFocus={true}
label="Name"
placeholder="Organization name"
name={'name'}
defaultValue={organization.name || ''}
onChange={onChange}
onKeyDown={() => {}}
onBlur={() => {}}
error={error.name}
wrapperClassName="!mb-3"
/>
</div>
<TextArea
type="text"
minHeight="240px"
label="Description (optional)"
labelClassName="fc-input-label"
placeholder="Description"
note="Markdown supported in description"
name={'description'}
defaultValue={organization.description || ''}
onChange={onChange}
/>
</Container.Body>
<Container.Footer>
<TabHeader className="!px-0">
<TabHeader.Left></TabHeader.Left>
<TabHeader.Right>
{/* {enableReset ? <Button text="Undo" onClick={(e) => onChange(e, true)} ghost xs /> : <></>} */}
<Button
text={isRequesting ? 'Updating...' : 'Update'}
onClick={onSubmit}
disabled={isRequesting || disabled || !enableReset}
primary
xs
/>
</TabHeader.Right>
</TabHeader>
</Container.Footer>
</Container>
);
};
export default EditOrganization;

View File

@@ -0,0 +1,196 @@
import { FC, useEffect, useRef, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import cx from 'classnames';
import {
Button,
Container,
DropdownMenu,
PrimitiveTable,
TTableApi,
} from '@firecamp/ui';
import { _array } from '@firecamp/utils';
import { Rest } from '@firecamp/cloud-apis';
import platformContext from '../../../../services/platform-context';
import { EUserRolesWorkspace } from '../../../../types';
import { getFormalDate } from '../OrgManagement';
const columns = [
{ id: 'index', name: 'No.', key: 'index', width: '35px', fixedWidth: true },
{
id: 'name',
name: 'Name',
key: 'name',
width: '100px',
resizeWithContainer: true,
},
{
id: 'role',
name: 'Role',
key: 'role',
width: '100px',
fixedWidth: true,
},
// {
// id: 'joinedAt',
// name: 'Joined Date',
// key: 'joinedAt',
// width: '130px',
// fixedWidth: true,
// },
];
const RoleOptions = [
{
id: EUserRolesWorkspace.Owner,
name: 'Owner',
},
{
id: EUserRolesWorkspace.Admin,
name: 'Admin',
},
{
id: EUserRolesWorkspace.Collaborator,
name: 'Collaborator',
},
];
const Members = ({
organizationId = '',
members = [],
updateMembers,
isFetching = false,
}) => {
const tableApi = useRef<TTableApi>(null);
useEffect(() => {
if (!_array.isEmpty(members)) {
const memberList = members.map((m, i) => {
return {
id: m.id,
name: m.username,
role: m.role,
joinedAt: getFormalDate(m.__ref.joinedAt),
};
});
tableApi.current.initialize(memberList);
}
}, [members]);
const onChangeRole = (row) => {
platformContext.window.confirm({
message: `Please confirm, You're assigning ${row.role.name} role to ${row.name}, right?`,
labels: {
cancel: 'Cancel',
confirm: 'Yes, change the role.',
},
onConfirm: () => {
Rest.organization
.changeMemberRole(organizationId, row.id, row.role.id)
.then((res) => res.data)
.then(({ error, message }) => {
if (!error) {
tableApi.current.setRow({ ...row, role: row.role.id });
// update the member listing after update
let Index = members.findIndex((m) => m.id == row.id);
updateMembers([
...members.slice(0, Index),
{ ...members[Index], role: row.role.id },
...members.slice(Index + 1),
]);
platformContext.app.notify.success(
"The member's role has been changed successfully."
);
} else {
platformContext.app.notify.alert(message);
}
})
.catch((e) => {
platformContext.app.notify.alert(
e.response?.data.message || e.message
);
});
},
});
};
const renderCell = (column, cellValue, rowIndex, row, tableApi, onChange) => {
switch (column.id) {
case 'index':
return <div className="px-2 text-base"> {rowIndex + 1} </div>;
break;
case 'name':
case 'joinedAt':
return <div className="p-1 text-base">{cellValue}</div>;
break;
case 'role':
return (
<div className="p-1 text-center">
<RoleDD
role={row.role}
onSelect={(role) => onChangeRole({ ...row, role })}
/>
</div>
);
break;
default:
return <></>;
}
};
return (
<Container className="gap-2 pt-2 !h-[80vh]">
<Container.Body>
<PrimitiveTable
classes={{
container: 'h-full',
}}
columns={columns}
rows={[]}
showDefaultEmptyRows={false}
renderColumn={(c) => c.name}
renderCell={renderCell}
onChange={console.log}
onMount={(api) => (tableApi.current = api)}
/>
</Container.Body>
</Container>
);
};
export default Members;
const RoleDD: FC<{
role: number;
onSelect: (role: { name: string; id: number }) => void;
}> = ({ role, onSelect }) => {
const [isOpen, toggleOpen] = useState(false);
const _role = RoleOptions.find((r) => r.id == role);
if (!_role) return <></>;
return (
<DropdownMenu
onOpenChange={(v) => toggleOpen(v)}
handler={() => (
<Button
text={_role.name}
rightIcon={
<ChevronDown
size={12}
className={cx({ 'transform rotate-180': isOpen })}
/>
}
compact
ghost
sm
/>
)}
options={RoleOptions}
onSelect={onSelect}
disabled={role === EUserRolesWorkspace.Owner}
width={115}
sm
/>
);
};

View File

@@ -0,0 +1,86 @@
import { FC, useEffect, useRef } from 'react';
import {
Container,
PrimitiveTable,
TTableApi,
} from '@firecamp/ui';
import { _array } from '@firecamp/utils';
import { getFormalDate } from '../OrgManagement';
const columns = [
{ id: 'index', name: 'No.', key: 'index', width: '35px', fixedWidth: true },
{
id: 'name',
name: 'Name',
key: 'name',
width: '100px',
resizeWithContainer: true,
},
{
id: 'created',
name: 'Created Date',
key: 'created',
width: '130px',
fixedWidth: true,
},
{
id: 'members',
name: 'Members',
key: 'members',
width: '100px',
fixedWidth: true,
},
];
const Workspaces: FC<{workspaces: Array<any>, isFetching: boolean}> = ({ workspaces = [], isFetching = false }) => {
const tableApi = useRef<TTableApi>(null);
useEffect(() => {
if (!_array.isEmpty(workspaces)) {
const workspaceList = workspaces.map((m, i) => {
return {
id: m.__ref?.id,
name: m.name,
created: getFormalDate(m.__ref.createdAt),
members: m.members.length,
};
});
tableApi.current.initialize(workspaceList);
}
}, [workspaces]);
const renderCell = (column, cellValue, rowIndex, row, tableApi, onChange) => {
switch (column.id) {
case 'index':
return <div className="px-2"> {rowIndex + 1} </div>;
break;
case 'name':
case 'members':
case 'created':
return <div className="p-1 text-base">{cellValue}</div>;
break;
default:
return <></>;
}
};
return (
<Container className="gap-2 pt-2 !h-[80vh]">
<Container.Body>
<PrimitiveTable
classes={{
container: 'h-full',
}}
columns={columns}
rows={[]}
showDefaultEmptyRows={false}
renderColumn={(c) => c.name}
renderCell={renderCell}
onChange={console.log}
onMount={(api) => (tableApi.current = api)}
/>
</Container.Body>
</Container>
);
};
export default Workspaces;

View File

@@ -0,0 +1,178 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input } from '@firecamp/ui';
import { VscEye } from '@react-icons/all-files/vsc/VscEye';
import { Rest } from '@firecamp/cloud-apis';
import platformContext from '../../../services/platform-context';
/**
* Change Password component
*/
const ChangePassword = () => {
const [isRequesting, setFlagIsRequesting] = useState(false);
const [oldPassword, toggleOldPassword] = useState(false);
const [showPassword, toggleShowPassword] = useState(false);
const [confirmPassword, toggleConfirmPassword] = useState(false);
const form = useForm();
const { handleSubmit, errors, getValues, reset } = form;
const _onSubmit = async (payload: {
currentPassword: string;
newPassword: string;
}) => {
if (isRequesting) return;
setFlagIsRequesting(true);
await Rest.user
.changePassword({
currentPassword: payload.currentPassword,
newPassword: payload.newPassword,
})
.then((res) => res.data)
.then(({ error, message }) => {
if (!error) {
reset({ currentPassword: '', newPassword: '', confirmPassword: '' });
platformContext.app.notify.success(message);
} else {
platformContext.app.notify.alert(
message ?? `Failed to change password!`
);
}
})
.catch((e) => {
platformContext.app.notify.alert(
e?.response?.data?.message || e.message,
{
labels: { alert: 'error!' },
}
);
})
.finally(() => {
setFlagIsRequesting(false);
});
};
const _onKeyDown = (e: any) => e.key === 'Enter' && handleSubmit(_onSubmit);
return (
<>
<form onSubmit={handleSubmit(_onSubmit)} className="mx-2 mt-4">
<Input
placeholder="Enter old password"
key={'currentPassword'}
name={'currentPassword'}
type={oldPassword ? 'text' : 'password'}
label="Old Password"
iconPosition="right"
icon={
<VscEye
title="password"
size={16}
onClick={() => {
toggleOldPassword(!oldPassword);
}}
/>
}
registerMeta={{
required: 'Please enter your current password',
minLength: {
value: 8,
message: 'Password should be at least 8 character',
},
maxLength: {
value: 50,
message: 'Password should not exceed 50 character',
},
}}
useformRef={form}
onKeyDown={_onKeyDown}
error={
errors?.currentPassword
? errors?.currentPassword?.message || 'Invalid password'
: ''
}
/>
<Input
placeholder="Enter new password"
key={'newPassword'}
name={'newPassword'}
type={showPassword ? 'text' : 'password'}
label="New Password"
iconPosition="right"
icon={
<VscEye
title="password"
size={16}
onClick={() => {
toggleShowPassword(!showPassword);
}}
/>
}
registerMeta={{
required: 'Please enter your new password',
minLength: {
value: 8,
message: 'Password should be at least 8 character',
},
maxLength: {
value: 50,
message: 'Password should not exceed 50 character',
},
}}
useformRef={form}
onKeyDown={_onKeyDown}
error={
errors?.newPassword
? errors?.newPassword?.message || 'Invalid password'
: ''
}
/>
<Input
placeholder="Enter password again"
key={'confirmPassword'}
name={'confirmPassword'}
type={confirmPassword ? 'text' : 'password'}
label="Confirm Password"
iconPosition="right"
icon={
<VscEye
title="password"
size={16}
onClick={() => {
toggleConfirmPassword(!confirmPassword);
}}
/>
}
registerMeta={{
required: 'Please enter password again',
validate: (value) => {
const { newPassword } = getValues();
return newPassword === value || 'Passwords should match';
},
}}
useformRef={form}
onKeyDown={_onKeyDown}
error={
errors?.confirmPassword
? errors?.confirmPassword?.message || 'Invalid password'
: ''
}
/>
<Button
type="submit"
text={isRequesting ? 'Updating Password...' : 'Update Password'}
onClick={handleSubmit(_onSubmit)}
fullWidth
primary
sm
/>
</form>
</>
);
};
export default ChangePassword;

View File

@@ -0,0 +1,46 @@
import { FC, useState } from 'react';
import { Drawer, IModal, SecondaryTab } from '@firecamp/ui';
import ChangePassword from './ChangePassword';
import UpdateProfile from './UpdateProfile';
const tabs = [
{ name: 'Profile', id: 'profile' },
{ name: 'Change Password', id: 'password' },
];
const ProfileManagement: FC<IModal> = ({ opened, onClose }) => {
let [activeTab, setActiveTab] = useState<string>(tabs[0].id);
const renderTab = (tabId: string) => {
switch (tabId) {
case 'profile':
return <UpdateProfile />;
case 'password':
return <ChangePassword />;
default:
return <></>;
}
};
return (
<Drawer
opened={opened}
onClose={onClose}
size={600}
// classNames={{
// body: 'h-[80vh]',
// }}
title={
<div className="text-lg leading-5 px-6 flex items-center font-medium">
Profile Management
</div>
}
>
<SecondaryTab
className="py-4"
list={tabs}
activeTab={activeTab}
onSelect={setActiveTab}
/>
{renderTab(activeTab)}
</Drawer>
);
};
export default ProfileManagement;

View File

@@ -0,0 +1,113 @@
import { FC, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input } from '@firecamp/ui';
import { Rest } from '@firecamp/cloud-apis';
import platformContext from '../../../services/platform-context';
import { useUserStore } from '../../../store/user';
/**
* Update Profile component
*/
const UpdateProfile = () => {
const [isRequesting, setFlagIsRequesting] = useState(false);
const form = useForm();
let { handleSubmit, errors, setValue } = form;
const { user, setUser } = useUserStore((s) => ({
user: s.user,
setUser: s.setUser
}));
// set the initial value for the form
useEffect(() => {
setValue("name", user.name);
},[user]);
const _onSubmit = async (payload: { name: string }) => {
let { name } = payload;
if (isRequesting || user.name === name) return;
setFlagIsRequesting(true);
await Rest.user
.updateProfile({ name })
.then((res) => res.data)
.then(({ error, message }) => {
if (!error) {
platformContext.app.notify.success(
`Your profile details are updated`
);
// update the user store
setUser({...user, name})
} else {
platformContext.app.notify.alert(message);
}
})
.catch((e) => {
platformContext.app.notify.alert(
e?.response?.data?.message || e.message,
{
labels: { alert: 'error!' },
}
);
})
.finally(() => {
setFlagIsRequesting(false);
});
};
const _onKeyDown = (e: any) => e.key === 'Enter' && handleSubmit(_onSubmit);
return (
<>
<form onSubmit={handleSubmit(_onSubmit)} className="mx-2 mt-4">
<Input
placeholder="Enter name"
key={'name'}
name={'name'}
type={'text'}
label="Name"
registerMeta={{
required: true,
}}
useformRef={form}
onKeyDown={_onKeyDown}
error={
errors?.name ? errors?.name?.message || 'Please enter name' : ''
}
/>
<Input
placeholder="Enter Username"
key={'username'}
name={'username'}
label="Username"
value={user.username}
disabled
/>
<Input
placeholder="Enter email"
key={'email'}
name={'email'}
label="Email"
value={user.email}
disabled
/>
<Button
type="submit"
text={isRequesting ? 'Updating...' : 'Update Name'}
onClick={handleSubmit(_onSubmit)}
fullWidth
primary
sm
/>
</form>
</>
);
};
export default UpdateProfile;

View File

@@ -92,7 +92,6 @@ const EditRequest: FC<IModal> = ({
</div>
}
classNames={{
content: 'h-[600px]',
body: 'p-0',
}}
>

View File

@@ -2,7 +2,6 @@ import { FC, useEffect, useState } from 'react';
import {
Container,
Drawer,
Modal,
IModal,
SecondaryTab,
ProgressBar,
@@ -12,23 +11,29 @@ import { Rest } from '@firecamp/cloud-apis';
import { useWorkspaceStore, IWorkspaceStore } from '../../../store/workspace';
import EditInfoTab from './tabs/EditInfoTab';
import MembersTab from './tabs/MembersTab';
import platformContext from '../../../services/platform-context';
import './workspace.scss';
import { Regex } from '../../../constants';
import PendingInviteMembersTab from './tabs/PendingInviteMembersTab';
enum ETabTypes {
Edit = 'edit',
Members = 'members',
PendingInvitation = 'pending_invitation',
}
const WorkspaceManagement: FC<IModal> = ({
opened = false,
onClose = () => {},
}) => {
let { workspace } = useWorkspaceStore((s: IWorkspaceStore) => ({
let { workspace, setWorkspace } = useWorkspaceStore((s: IWorkspaceStore) => ({
workspace: s.workspace,
setWorkspace: s.setWorkspace,
}));
const [wrs, setWrs] = useState(workspace);
const [isRequesting, setIsRequesting] = useState(false);
const [wrsMembers, setWrsMembers] = useState([]);
const [wrsInviteMembers, setWrsInviteMembers] = useState([]);
const [isFetchingMembers, setIsFetchingMembers] = useState(false);
const [error, setError] = useState({ name: '' });
const [activeTab, setActiveTab] = useState<ETabTypes>(ETabTypes.Edit);
@@ -36,16 +41,22 @@ const WorkspaceManagement: FC<IModal> = ({
const tabs = [
{ name: 'Edit', id: ETabTypes.Edit },
{ name: 'Members', id: ETabTypes.Members },
{ name: 'Pending Invitation', id: ETabTypes.PendingInvitation },
];
/** fetch wrs members to be shown on second tab activated, only fetch once */
useEffect(() => {
if (activeTab === ETabTypes.Members && wrsMembers.length === 0) {
if (
[ETabTypes.Members, ETabTypes.PendingInvitation].includes(activeTab) &&
activeTab === ETabTypes.Members
? wrsMembers.length === 0
: wrsInviteMembers.length === 0
) {
setIsFetchingMembers(true);
Rest.workspace
.getMembers(workspace.__ref.id)
.then((res) => res.data)
.then(({ members = [], invited }) => {
.then(({ members = [], invited = [] }) => {
const memberList = members.map((m, i) => {
return {
id: m.__ref?.id ?? i,
@@ -54,51 +65,117 @@ const WorkspaceManagement: FC<IModal> = ({
role: m.role,
};
});
const invitedMemberList = invited.map((m, i) => {
return {
id: m.__ref?.id ?? i,
name: m.name || m.username,
email: m.email,
role: m.role,
};
});
setWrsMembers(memberList);
setWrsInviteMembers(invitedMemberList);
})
.finally(() => setIsFetchingMembers(false));
}
}, [activeTab]);
const onChange = (e) => {
const { name, value } = e.target;
if (error.name) setError({ name: '' });
setWrs((w) => ({ ...w, [name]: value }));
const onChange = (e, reset) => {
if (reset) {
if (error.name) setError({ name: '' });
setWrs(workspace);
} else {
const { name, value } = e.target;
if (error.name) setError({ name: '' });
setWrs((w) => ({ ...w, [name]: value }));
}
};
const onUpdate = () => {
if (isRequesting) return;
const name = wrs.name.trim();
const description = wrs.description?.trim();
if (!name || name.length < 6) {
setError({ name: 'The workspace name must have minimum 6 characters' });
return;
}
const _wrs = { name, description: wrs?.description?.trim() };
// TODO: workspace update API call
const isValid = Regex.WorkspaceName.test(name);
if (!isValid) {
setError({
name: 'The workspace name must not contain any spaces or special characters.',
});
return;
}
if (
workspace.name === wrs.name &&
workspace.description === wrs.description
)
return;
const _wrs: { name?: string; description?: string } = {};
if (workspace.name !== name) {
_wrs.name = name;
}
if (workspace.description !== description) {
_wrs.description = description;
}
setIsRequesting(true);
Rest.workspace
.update(workspace.__ref.id, _wrs)
.then(({ error, message }) => {
if (!error) {
platformContext.app.notify.success(
"The workspace's detail has been changed successfully."
);
setWorkspace({ ...workspace, ..._wrs });
} else {
platformContext.app.notify.alert(message);
}
})
.catch((e) => {
platformContext.app.notify.alert(e.response?.data.message || e.message);
})
.finally(() => {
setIsRequesting(false);
});
};
const renderTab = (tabId: string) => {
// console.log(wrs, "wrs....")
switch (tabId) {
case 'edit':
case ETabTypes.Edit:
return (
<EditInfoTab
workspace={wrs}
error={error}
isRequesting={isRequesting}
onChange={onChange}
close={onClose}
onSubmit={onUpdate}
enableReset={
workspace.name !== wrs.name ||
workspace.description !== wrs.description
}
// disabled={true} // TODO: only allowed for owner & admin
/>
);
case 'members':
case ETabTypes.Members:
return (
<MembersTab
members={wrsMembers}
isFetchingMembers={isFetchingMembers}
/>
);
case ETabTypes.PendingInvitation:
return (
<PendingInviteMembersTab
members={wrsInviteMembers}
isFetchingMembers={isFetchingMembers}
/>
);
default:
return <></>;
}
@@ -115,7 +192,7 @@ const WorkspaceManagement: FC<IModal> = ({
}
size={550}
classNames={{
body: '!p-4 h-[450px]'
body: '!p-4 h-[90vh]',
}}
>
<>

View File

@@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react';
import { Drawer, IModal, SecondaryTab } from '@firecamp/ui';
import { Container, Drawer, IModal, Notes, SecondaryTab } from '@firecamp/ui';
import { _array, _misc } from '@firecamp/utils';
import { Rest } from '@firecamp/cloud-apis';
import InviteNonOrgMembers from './tabs/InviteNonOrgMembers';
@@ -13,6 +13,11 @@ enum EInviteMemberTabs {
}
const InviteMembers: FC<IModal> = ({ opened = false, onClose = () => {} }) => {
const { scope, organization } = usePlatformStore(s => ({
scope: s.scope,
organization: s.organization
}));
const [isFetchingMembers, setIsFetchingMembers] = useState(false);
const [orgMembers, setOrgMembers] = useState([]);
@@ -24,7 +29,11 @@ const InviteMembers: FC<IModal> = ({ opened = false, onClose = () => {} }) => {
}>;
}>({
role: EUserRolesWorkspace.Collaborator,
usersList: [{ name: '', email: '' }],
usersList: [
{ name: '', email: '' },
{ name: '', email: '' },
{ name: '', email: '' },
],
});
const [orgTabState, setOrgTabState] = useState<{
id: string;
@@ -57,7 +66,7 @@ const InviteMembers: FC<IModal> = ({ opened = false, onClose = () => {} }) => {
/** fetch org members to be invited on second tab activated, only fetch once */
useEffect(() => {
if (activeTab === EInviteMemberTabs.OrgMembers && orgMembers.length === 0) {
const { scope, organization } = usePlatformStore.getState();
if (scope == EPlatformScope.Person) return;
setIsFetchingMembers(true);
Rest.organization
@@ -90,38 +99,62 @@ const InviteMembers: FC<IModal> = ({ opened = false, onClose = () => {} }) => {
opened={opened}
onClose={onClose}
size={576}
classNames={{
content: 'h-[700px]',
body: 'h-[480px]'
}}
title={
<div className="text-lg leading-5 px-1 flex items-center font-medium">
Invite Members To Join The Workspace
</div>
}
classNames={{
body: 'pb-4 h-[90vh]',
}}
>
<div className="!pt-4 h-fit flex flex-col">
<SecondaryTab
className="flex items-center pb-6 -ml-2"
list={tabs}
activeTab={activeTab}
onSelect={(tabId: EInviteMemberTabs) => setActiveTab(tabId)}
/>
{activeTab == EInviteMemberTabs.NewMembers ? (
<InviteNonOrgMembers
state={nonOrgTabState}
onChange={changeNonOrgTabState}
/>
) : (
<InviteOrgMembers
state={orgTabState}
members={orgMembers}
isFetchingMembers={isFetchingMembers}
onChange={changeOrgTabState}
/>
)}
</div>
<Container>
{(scope == EPlatformScope.Person) ? (
<Container.Body className="mt-8">
<InviteNotAllowed />
</Container.Body>
) : (
<>
<Container.Header className="!pt-4">
<SecondaryTab
className="flex items-center pb-6 -ml-2"
list={tabs}
activeTab={activeTab}
onSelect={(tabId: EInviteMemberTabs) => setActiveTab(tabId)}
/>
</Container.Header>
<Container.Body>
{activeTab == EInviteMemberTabs.NewMembers ? (
<InviteNonOrgMembers
state={nonOrgTabState}
onChange={changeNonOrgTabState}
/>
) : (
<InviteOrgMembers
state={orgTabState}
members={orgMembers}
isFetchingMembers={isFetchingMembers}
onChange={changeOrgTabState}
/>
)}
</Container.Body>
</>
)}
</Container>
</Drawer>
);
};
export default InviteMembers;
const InviteNotAllowed = () => {
return (
<Notes
title={
'Inviting users to your personal workspace is currently unavailable.'
}
description={`But don't worry! <br/>
You can still collaborate effectively by taking the first step to create an organization. 🤝 <br/>
Start working together seamlessly and efficiently with your team in no time!`}
/>
);
};

View File

@@ -2,6 +2,7 @@ import cx from 'classnames';
import { VscAdd } from '@react-icons/all-files/vsc/VscAdd';
import { VscClose } from '@react-icons/all-files/vsc/VscClose';
import { FormField, Input } from '@firecamp/ui';
import { _array } from '@firecamp/utils';
const InviteUsersForm = ({ usersList, onChange, error }) => {
const _handleNameChange = (e, position) => {
@@ -73,14 +74,8 @@ const InviteUsersForm = ({ usersList, onChange, error }) => {
<VscClose size={20} className="text-error" />
)}
</span>
{error[index]?.message.length > 0 ? (
<div
className={cx(
'text-sm font-light text-error absolute left-0 bottom-0'
)}
>
{error[index].message}
</div>
{!_array.isEmpty(error) ? (
<Error error={error} index={index} />
) : (
<></>
)}
@@ -91,3 +86,17 @@ const InviteUsersForm = ({ usersList, onChange, error }) => {
};
export default InviteUsersForm;
const Error = ({ error, index }) => {
let errorObject = error.find((m) => m.index === index);
if (errorObject?.message.length > 0)
return (
<div
className={cx('text-sm font-light text-error absolute left-0 bottom-0')}
>
{errorObject.message}
</div>
);
return <></>;
};

View File

@@ -47,6 +47,7 @@ const InviteNonOrgMembers = ({ state, onChange }) => {
const { usersList, role } = state;
const inviteMembers = useCallback(() => {
setInvitingFlag(true);
const { success, error } = validateMembersDetail(usersList);
if (error?.length) {
setError(error);
@@ -118,17 +119,17 @@ const InviteNonOrgMembers = ({ state, onChange }) => {
</ScrollBar>
</Container.Body>
<Container.Footer className="flex items-center">
<a
className="!text-link hover:!text-link hover:underline cursor-pointer text-sm px-2 pl-0"
target="_blank"
href="#"
onClick={(e) => {
e.preventDefault();
<Button
onClick={() => {
platformContext.app.modals.openWorkspaceManagement();
}}
>
Open Workspace Management
</a>
text='Open Workspace Management'
// classNames={{
// root: '!text-link hover:!text-link hover:underline'
// }}
ghost
xs
/>
<Button
text={isInvitingMembers ? 'Sending invitation...' : 'Send Invitation'}
classNames={{

View File

@@ -86,7 +86,7 @@ const InviteOrgMembers: FC<IProps> = ({
onSelect={(m) => onChange({ ...member, ...m })}
classNames={{
trigger: 'block',
dropdown: '-mt-2 overflow-y-scroll invisible-scrollbar h-[200px]',
dropdown: '-mt-2 overflow-y-scroll invisible-scrollbar max-h-[200px]',
item: '!px-4',
}}
width={512}
@@ -127,17 +127,19 @@ const InviteOrgMembers: FC<IProps> = ({
<RolesCallout role={_role.id} />
</Container.Body>
<Container.Footer className="flex items-center">
<a
className="!text-link hover:!text-link hover:underline cursor-pointer text-sm px-2 pl-0"
target="_blank"
href="#"
onClick={(e) => {
e.preventDefault();
<Button
onClick={() => {
platformContext.app.modals.openWorkspaceManagement();
}}
>
Open Workspace Management
</a>
// classNames={{
// root: '!text-link hover:!text-link hover:underline'
// }}
text='Open Workspace Management'
ghost
xs
/>
<Button
text={'Send Invitation'}
disabled={!member.name || !member.role || isInvitingMembers}

View File

@@ -1,5 +1,5 @@
import { FC } from 'react';
import { Button, Input, TabHeader, TextArea } from '@firecamp/ui';
import { Button, Container, Input, TabHeader, TextArea } from '@firecamp/ui';
import platformContext from '../../../../services/platform-context';
const EditInfoTab: FC<any> = ({
@@ -8,76 +8,74 @@ const EditInfoTab: FC<any> = ({
isRequesting,
onSubmit,
onChange,
close,
disabled = false,
enableReset = false,
}) => {
return (
<div className="p-3 flex-1 flex flex-col">
<label className="text-sm font-semibold leading-3 block text-app-foreground-inactive uppercase w-full relative mb-2">
UPDATE WORKSPACE INFO
</label>
<div>
<Input
autoFocus={true}
label="Name"
placeholder="Workspace name"
name={'name'}
defaultValue={workspace.name || ''}
<Container className="pt-3 px-3 flex-1 flex flex-col h-full">
<Container.Body>
<label className="text-sm font-semibold leading-3 block text-app-foreground-inactive uppercase w-full relative mb-2">
UPDATE WORKSPACE INFO
</label>
<div>
<Input
autoFocus={true}
label="Name"
placeholder="Workspace name"
name={'name'}
defaultValue={workspace.name || ''}
onChange={onChange}
onKeyDown={() => {}}
onBlur={() => {}}
error={error.name}
// error={error.name}
// iconPosition="right"
// icon={<VscEdit />}
wrapperClassName="!mb-3"
/>
</div>
<TextArea
type="text"
minHeight="240px"
label="Description (optional)"
labelClassName="fc-input-label"
placeholder="Description"
note="Markdown supported in description"
name={'description'}
defaultValue={workspace.description || ''}
onChange={onChange}
onKeyDown={() => {}}
onBlur={() => {}}
error={error.name}
// error={error.name}
// disabled={true}
// iconPosition="right"
// icon={<VscEdit />}
wrapperClassName='!mb-3'
/>
</div>
<TextArea
type="text"
minHeight="180px"
label="Description (optional)"
labelClassName="fc-input-label"
placeholder="Description"
note="Markdown supported in description"
name={'description'}
defaultValue={workspace.description || ''}
onChange={onChange}
// disabled={true}
// iconPosition="right"
// icon={<VscEdit />}
/>
<TabHeader className="!px-0">
<TabHeader.Left>
<a
className="!text-link hover:!text-link hover:underline cursor-pointer text-sm px-2 pl-0"
target="_blank"
href="#"
onClick={(e) => {
e.preventDefault();
platformContext.app.modals.openInviteMembers();
}}
>
Invite New Members
</a>
</TabHeader.Left>
<TabHeader.Right>
<Button
text="Cancel"
onClick={(e) => close(e)}
ghost
xs
/>
<Button
text={isRequesting ? 'Updating...' : 'Update'}
onClick={onSubmit}
disabled={isRequesting}
primary
xs
/>
</TabHeader.Right>
</TabHeader>
</div>
</Container.Body>
<Container.Footer>
<TabHeader className="!px-0">
<TabHeader.Left>
<Button
onClick={() => {
platformContext.app.modals.openInviteMembers();
}}
text="Invite New Members"
ghost
xs
/>
</TabHeader.Left>
<TabHeader.Right>
{/* TODO: update details */}
{/* {enableReset ? <Button text="Undo" onClick={(e) => onChange(e, true)} ghost xs /> : <></>} */}
<Button
text={isRequesting ? 'Updating...' : 'Update'}
onClick={onSubmit}
disabled={isRequesting || disabled || !enableReset}
primary
xs
/>
</TabHeader.Right>
</TabHeader>
</Container.Footer>
</Container>
);
};
export default EditInfoTab;

View File

@@ -84,7 +84,7 @@ const MembersTab = ({ members = [], isFetchingMembers = false }) => {
e.response?.data.message || e.message
);
});
}
},
});
};
@@ -151,8 +151,8 @@ const MembersTab = ({ members = [], isFetchingMembers = false }) => {
};
return (
<Container className="gap-2">
<Container.Body className="pt-2 visible-scrollbar">
<Container className="gap-2 pt-2">
<Container.Body className="visible-scrollbar">
<ProgressBar active={isFetchingMembers} className={'top-auto'} />
<PrimitiveTable
classes={{
@@ -167,18 +167,15 @@ const MembersTab = ({ members = [], isFetchingMembers = false }) => {
onMount={(api) => (tableApi.current = api)}
/>
</Container.Body>
<Container.Footer className="flex items-center">
<a
className="!text-link hover:!text-link hover:underline cursor-pointer text-sm px-2 pl-0"
target="_blank"
href="#"
onClick={(e) => {
e.preventDefault();
<Container.Footer className="px-3 h-[34px] flex items-center">
<Button
onClick={() => {
platformContext.app.modals.openInviteMembers();
}}
>
Invite New Members
</a>
text="Invite New Members"
ghost
xs
/>
</Container.Footer>
</Container>
);

View File

@@ -0,0 +1,219 @@
import { FC, useEffect, useRef, useState } from 'react';
import { VscTrash } from '@react-icons/all-files/vsc/VscTrash';
import { VscTriangleDown } from '@react-icons/all-files/vsc/VscTriangleDown';
import cx from 'classnames';
import {
Button,
Container,
DropdownMenu,
PrimitiveTable,
ProgressBar,
TTableApi,
} from '@firecamp/ui';
import { _array } from '@firecamp/utils';
import { Rest } from '@firecamp/cloud-apis';
import platformContext from '../../../../services/platform-context';
import { useWorkspaceStore } from '../../../../store/workspace';
import { EUserRolesWorkspace } from '../../../../types';
const columns = [
{ id: 'index', name: 'No.', key: 'index', width: '35px', fixedWidth: true },
{ id: 'name', name: 'Name', key: 'name', width: '100px' },
{
id: 'email',
name: 'Email',
key: 'email',
resizeWithContainer: true,
width: '180px',
},
{ id: 'role', name: 'Role', key: 'role', width: '115px', fixedWidth: true },
// { id: 'action', name: '', key: '', width: '35px', fixedWidth: true },
];
const RoleOptions = [
{
id: EUserRolesWorkspace.Owner,
name: 'Owner',
},
{
id: EUserRolesWorkspace.Admin,
name: 'Admin',
},
{
id: EUserRolesWorkspace.Collaborator,
name: 'Collaborator',
},
];
const PendingInviteMembersTab = ({ members = [], isFetchingMembers = false }) => {
const workspace = useWorkspaceStore.getState().workspace;
const tableApi = useRef<TTableApi>(null);
useEffect(() => {
if (!_array.isEmpty(members)) {
const memberList = members.map((m, i) => {
return {
id: m.id,
name: m.name || m.username,
email: m.email,
role: m.role,
};
});
tableApi.current.initialize(memberList);
}
}, [members]);
const onRemoveMember = (row) => {
platformContext.window.confirm({
message: `You're sure to remove ${row.name} from the workspace?`,
labels: {
cancel: 'Cancel',
confirm: 'Yes, remove the member.',
},
onConfirm: () => {
Rest.workspace
.removeMember(workspace.__ref.id, row.id)
.then(() => {
tableApi.current.removeRow(row.id);
platformContext.app.notify.success(
'The member has been removed successfully.'
);
})
.catch((e) => {
platformContext.app.notify.alert(
e.response?.data.message || e.message
);
});
},
});
};
const onChangeRole = (row) => {
platformContext.window.confirm({
message: `Please confirm, You're assigning ${row.role.name} role to ${row.name}, right?`,
labels: {
cancel: 'Cancel',
confirm: 'Yes, change the role.',
},
onConfirm: () => {
Rest.workspace
.changeMemberRole(workspace.__ref.id, row.id, row.role.id)
.then(() => {
tableApi.current.setRow({ ...row, role: row.role.id });
platformContext.app.notify.success(
"The member's role has been changed successfully."
);
})
.catch((e) => {
platformContext.app.notify.alert(
e.response?.data.message || e.message
);
});
},
});
};
const renderCell = (column, cellValue, rowIndex, row, tableApi, onChange) => {
switch (column.id) {
case 'index':
return <div className="px-2"> {rowIndex + 1} </div>;
break;
case 'name':
return <div className="p-1">{cellValue}</div>;
break;
case 'email':
return <div className="p-1">{cellValue}</div>;
break;
case 'role':
return (
<div className="p-1 text-center">
<RoleDD
role={row.role}
onSelect={(role) => onChangeRole({ ...row, role })}
/>
</div>
);
break;
// case 'action':
// return (
// <div className="px-2">
// <VscTrash
// size={14}
// className="text-error cursor-pointer"
// onClick={() => onRemoveMember(row)}
// />
// </div>
// );
// break;
default:
return column.key;
}
};
return (
<Container className="gap-2 pt-2">
<Container.Body className="visible-scrollbar">
<ProgressBar active={isFetchingMembers} className={'top-auto'} />
<PrimitiveTable
classes={{
container: 'h-full',
}}
columns={columns}
rows={[]}
showDefaultEmptyRows={false}
renderColumn={(c) => c.name}
renderCell={renderCell}
onChange={console.log}
onMount={(api) => (tableApi.current = api)}
/>
</Container.Body>
<Container.Footer className="px-3 h-[34px] flex items-center">
<Button
onClick={() => {
platformContext.app.modals.openInviteMembers();
}}
text="Invite New Members"
ghost
xs
/>
</Container.Footer>
</Container>
);
};
export default PendingInviteMembersTab;
const RoleDD: FC<{
role: number;
onSelect: (role: { name: string; id: number }) => void;
}> = ({ role, onSelect }) => {
const [isOpen, toggleOpen] = useState(false);
const _role = RoleOptions.find((r) => r.id == role);
if (!_role) return <></>;
return (
<DropdownMenu
onOpenChange={(v) => toggleOpen(v)}
handler={() => (
<Button
text={_role.name}
rightIcon={
<VscTriangleDown
size={12}
className={cx({ 'transform rotate-180': isOpen })}
/>
}
disabled
ghost
xs
/>
)}
options={RoleOptions}
onSelect={onSelect}
disabled={true}
width={115}
sm
/>
);
};

View File

@@ -65,22 +65,6 @@ const UserDDMenus: FC<{ title: string; isGuest: boolean }> = ({
isGuest,
}) => {
const guestOptions = [
{
name: title,
isLabel: true,
postfix: () => (
<>
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
User
</div>
</>
),
},
{
name: 'Sign in',
postfix: () => (
@@ -102,23 +86,6 @@ const UserDDMenus: FC<{ title: string; isGuest: boolean }> = ({
];
const userOptions = [
{
name: title,
isLabel: true,
postfix: () => (
<>
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
User
</div>
</>
),
},
// TODO: add the onClick action
{
name: 'User Profile',
postfix: () => (
@@ -126,6 +93,9 @@ const UserDDMenus: FC<{ title: string; isGuest: boolean }> = ({
<VscAccount size={14} />
</div>
),
onClick: () => {
platformContext.app.modals.openUserProfile();
},
},
{
name: 'Create new organization',
@@ -175,15 +145,24 @@ const UserDDMenus: FC<{ title: string; isGuest: boolean }> = ({
return (
<DropdownMenu
handler={() => (
<span className="pl-1 cursor-pointer">{title}</span>
)}
handler={() => <span className="pl-1 cursor-pointer">{title}</span>}
options={isGuest ? guestOptions : userOptions}
header={
<div className="!capitalize !pt-[0.2rem] !pb-2 !px-3 !block !bg-focus2 ">
{title}
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
User
</div>
</div>
}
onSelect={(v) => v.onClick()}
classNames={{
dropdown: '!pt-0 mt-2 min-w-fit',
label:
'!capitalize flex items-center text-app-foreground !pt-[0.2rem] !pb-2 !px-3 !block !text-base leading-6 !bg-focus2 ',
item: '!py-1 !px-3',
}}
width={150}
@@ -197,22 +176,6 @@ const WorkspaceDDMenus: FC<{ title: string; disabled?: boolean }> = ({
disabled = false,
}) => {
const options = [
{
name: title,
isLabel: true,
postfix: () => (
<>
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
Workspace
</div>
</>
),
},
{
name: 'Workspace Management',
disabled,
@@ -265,15 +228,24 @@ const WorkspaceDDMenus: FC<{ title: string; disabled?: boolean }> = ({
return (
<DropdownMenu
handler={() => (
<span className="pl-1 cursor-pointer">{title}</span>
)}
handler={() => <span className="pl-1 cursor-pointer">{title}</span>}
options={options}
header={
<div className="!capitalize !pt-[0.2rem] !pb-2 !px-3 !block !bg-focus2 ">
{title}
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
Workspace
</div>
</div>
}
onSelect={(v) => v.onClick()}
classNames={{
dropdown: '!pt-0 mt-2 min-w-fit',
label:
'!capitalize flex items-center text-app-foreground !pt-[0.2rem] !pb-2 !px-3 !block !text-base leading-6 !bg-focus2 ',
item: '!py-1 !px-3',
}}
width={150}
@@ -287,22 +259,6 @@ const OrgDDMenus: FC<{ title: string; disabled?: boolean }> = ({
disabled = false,
}) => {
const options = [
{
name: title,
isLabel: true,
postfix: () => (
<>
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
Organization
</div>
</>
),
},
{
name: 'Org Management',
disabled,
@@ -343,15 +299,24 @@ const OrgDDMenus: FC<{ title: string; disabled?: boolean }> = ({
return (
<DropdownMenu
handler={() => (
<span className="pl-1 cursor-pointer">{title}</span>
)}
handler={() => <span className="pl-1 cursor-pointer">{title}</span>}
options={options}
header={
<div className="!capitalize !pt-[0.2rem] !pb-2 !px-3 !block !bg-focus2 ">
{title}
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
Organization
</div>
</div>
}
onSelect={(v) => v.onClick()}
classNames={{
dropdown: '!pt-0 mt-2 min-w-fit',
label:
'!capitalize flex items-center text-app-foreground !pt-[0.2rem] !pb-2 !px-3 !block !text-base leading-6 !bg-focus2 ',
item: '!py-1 !px-3',
}}
width={140}

View File

@@ -5,18 +5,19 @@ export const Regex = {
),
/**
* allow alphanumeric and underscore
* allow alphanumeric and single hyphen
* don't allow spaces and special characters
* characters range between 6 to 20
* @ref: https://github.com/shinnn/github-username-regex/blob/master/index.js
*/
Username: /^[a-zA-Z0-9\_]{6,20}$/,
WorkspaceName: /^[a-zA-Z0-9\_]{6,20}$/,
OrgName: /^[a-zA-Z0-9\_]{4,20}$/,
Username: /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){5,19}$/i,
WorkspaceName: /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){5,19}$/i,
OrgName: /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){3,19}$/i,
/**
* don't allow any special character
* @ref: https://stackoverflow.com/a/23127284
* allows: colName, colName_, _colNmae, col_name
* allows: colName, colName_, _colName, col_name
* not allow" colName. , colName?, colName/@ or any special character in the name
* TODO: add character range
*/

View File

@@ -116,7 +116,8 @@ const initApp = async () => {
} catch (e) {
console.log(e, 'error while connecting the socket');
}
});
})
.catch(console.log);
};
const initUser = (user: any) => {
const { setUser } = useUserStore.getState();

View File

@@ -78,6 +78,12 @@ const modalService = {
open(EPlatformModalTypes.SwitchOrg);
},
openAllInvitation: () => {
const { isGuest } = useUserStore.getState();
if (isGuest) return modalService.openSignIn();
open(EPlatformModalTypes.AllInvitation);
},
// Cookie, Ssl, Proxy
openCookieManager: () => {
const { isGuest } = useUserStore.getState();

View File

@@ -36,7 +36,7 @@ const promptInput: TOpenPromptInput = (props) => {
size: 400,
classNames: {
header: 'border-0 pb-0',
body: 'px-6',
body: 'px-6 relative',
content: 'min-h-0',
}
});
@@ -68,6 +68,7 @@ const promptSaveItem: TOpenPromptSaveItem = (props) => {
size: 400,
classNames: {
header: 'border-0 px-6 pt-6 pb-0',
body: 'relative'
}
});
});
@@ -101,7 +102,7 @@ const confirm: TConfirmApi = (props) => {
size: 400,
classNames: {
header: 'border-0',
body: 'px-6',
body: 'px-6 relative',
content: 'min-h-0',
},
});

View File

@@ -48,6 +48,7 @@ export enum EPlatformModalTypes {
// user
UserProfile = 'userProfile',
AllInvitation = 'allInvitation',
// cookie
CookieManager = 'cookieManager',

View File

@@ -1,6 +1,5 @@
import { FC } from 'react';
import { Drawer as MantineDrawer, DrawerProps, ScrollArea, createStyles } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
export interface IDrawer extends DrawerProps { }

View File

@@ -1,6 +1,15 @@
import type { MenuProps } from '@mantine/core';
import { ReactNode } from 'react';
export interface IDropdownMenu {
/**
* Add the custom header for options
*/
header?: ReactNode;
/**
* Add the custom footer for options
*/
footer?: ReactNode;
/**
* Add id to the dropdown wrapper
*/

View File

@@ -98,9 +98,22 @@ Example.args = {
prefix: () => <VscMultipleWindows size={18} />,
},
],
header:
<div className="!capitalize !pt-[0.2rem] !pb-2 !px-3 !block !bg-focus2 ">
Header title
<br />
<div
className={
'text-sm font-light leading-3 text-app-foreground-inactive'
}
>
User
</div>
</div>,
footer: <div className='text-center'>3.0.0</div>,
handler: () => <Button text={'Create'} primary ghost xs />,
classNames: {
dropdown: '-ml-[2px]',
dropdown: '-ml-[2px] pt-0',
},
onSelect: (value: any) => console.log(`selected item :`, value),
};

View File

@@ -12,12 +12,16 @@ enum EDefaultStyles {
divider = 'bg-app-border border-app-border',
disabled = 'opacity-50 cursor-default',
disabledItem = '!text-activityBar-foreground-inactive !cursor-default',
header = 'text-app-foreground p-0 text-base leading-6',
footer = 'text-activityBar-foreground-inactive px-5 py-2 text-sm leading-3',
}
const DropdownMenu: FC<IDropdownMenu> = ({
id = '',
selected = '',
width = 200,
header,
footer,
options = [],
handler,
classNames = {},
@@ -25,7 +29,7 @@ const DropdownMenu: FC<IDropdownMenu> = ({
onOpenChange = () => {},
disabled = false,
menuProps = {},
sm = false
sm = false,
}) => {
return (
<Menu
@@ -33,7 +37,9 @@ const DropdownMenu: FC<IDropdownMenu> = ({
shadow="md"
width={width}
classNames={classNames}
onChange={(v) => disabled || options.length === 0 ? {} : onOpenChange(v)}
onChange={(v) =>
disabled || options.length === 0 ? {} : onOpenChange(v)
}
disabled={disabled}
{...menuProps}
>
@@ -51,13 +57,21 @@ const DropdownMenu: FC<IDropdownMenu> = ({
</Menu.Target>
<Menu.Dropdown
className={cx(EDefaultStyles.dropdown,
className={cx(
EDefaultStyles.dropdown,
{ 'py-2.5': sm },
{ 'py-[15px]': !sm },
{
'hidden border-0': options.length === 0,
})}
{
'hidden border-0': options.length === 0,
}
)}
>
{!!header ? (
<Menu.Label className={EDefaultStyles.header}>{header}</Menu.Label>
) : (
<></>
)}
{options.map((item, i) => {
return (
<Fragment key={`menu-item-${i}`}>
@@ -70,10 +84,10 @@ const DropdownMenu: FC<IDropdownMenu> = ({
<Menu.Item
className={cx(
{
[EDefaultStyles.item]: !sm
[EDefaultStyles.item]: !sm,
},
{
[EDefaultStyles.itemSmall]: sm
[EDefaultStyles.itemSmall]: sm,
},
{
'font-bold': selected === item.name,
@@ -114,6 +128,11 @@ const DropdownMenu: FC<IDropdownMenu> = ({
</Fragment>
);
})}
{!!footer ? (
<Menu.Label className={EDefaultStyles.footer}>{footer}</Menu.Label>
) : (
<></>
)}
</Menu.Dropdown>
</Menu>
);

View File

@@ -547,6 +547,10 @@
.\!mb-3{
margin-bottom: 0.75rem !important;
}
.\!mb-4{
margin-bottom: 1rem !important;
}
.\!ml-auto{
margin-left: auto !important;
@@ -751,6 +755,10 @@
.mt-8{
margin-top: 2rem;
}
.mt-\[10vh\]{
margin-top: 10vh;
}
.mt-\[50\%\]{
margin-top: 50%;
@@ -831,6 +839,10 @@
.\!h-80{
height: 20rem !important;
}
.\!h-\[80vh\]{
height: 80vh !important;
}
.\!h-fit{
height: -moz-fit-content !important;
@@ -913,37 +925,25 @@
height: 340px;
}
.h-\[450px\]{
height: 450px;
}
.h-\[480px\]{
height: 480px;
.h-\[34px\]{
height: 34px;
}
.h-\[50vh\]{
height: 50vh;
}
.h-\[600px\]{
height: 600px;
}
.h-\[700px\]{
height: 700px;
}
.h-\[70vh\]{
height: 70vh;
}
.h-\[750px\]{
height: 750px;
}
.h-\[80vh\]{
height: 80vh;
}
.h-\[90vh\]{
height: 90vh;
}
.h-fit{
height: -moz-fit-content;
@@ -973,6 +973,10 @@
.max-h-56{
max-height: 14rem;
}
.max-h-\[200px\]{
max-height: 200px;
}
.min-h-0{
min-height: 0px;
@@ -2423,6 +2427,12 @@
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-sm{
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.\!shadow-popover-shadow{
--tw-shadow-color: var(--popover-shadow) !important;

View File

@@ -106,8 +106,6 @@ const BodyTab: FC<any> = () => {
return <BinaryBody body={body || {}} onChange={changeBodyValue} />;
case ERestBodyTypes.GraphQL:
return <GraphQLBody body={body || {}} onChange={changeBodyValue} />;
case ERestBodyTypes.None:
return <NoBodyTab selectBodyType={_selectBodyType} />;
default:
return <></>;
}

View File

@@ -112,6 +112,7 @@ const SIOVersionDropDown: FC<any> = ({
className={cx({ 'transform rotate-180': isDropDownOpen })}
/>
}
animate={false}
secondary
xs
/>

165
pnpm-lock.yaml generated
View File

@@ -135,6 +135,9 @@ importers:
node-polyfill-webpack-plugin:
specifier: ^2.0.0
version: 2.0.1(webpack@5.75.0)
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
postcss-loader:
specifier: ^7.0.2
version: 7.0.2(postcss@8.4.27)(webpack@5.75.0)
@@ -162,6 +165,9 @@ importers:
style-loader:
specifier: ^3.3.1
version: 3.3.1(webpack@5.75.0)
terser-webpack-plugin:
specifier: ^5.3.9
version: 5.3.9(webpack@5.75.0)
ts-loader:
specifier: ^9.2.8
version: 9.4.2(typescript@5.0.2)(webpack@5.75.0)
@@ -192,6 +198,9 @@ importers:
webpack-httpolyglot-server:
specifier: ^0.3.0
version: 0.3.0(webpack@5.75.0)
webpack-merge:
specifier: ^5.9.0
version: 5.9.0
worker-loader:
specifier: ^3.0.8
version: 3.0.8(webpack@5.75.0)
@@ -478,8 +487,8 @@ importers:
specifier: workspace:*
version: link:../../packages/firecamp-agent-manager
'@firecamp/cloud-apis':
specifier: ^0.2.8
version: 0.2.8
specifier: 0.2.10
version: 0.2.10
'@firecamp/cookie-manager':
specifier: ^0.0.0
version: link:../../packages/firecamp-cookie-manager
@@ -5011,8 +5020,8 @@ packages:
- supports-color
dev: true
/@firecamp/cloud-apis@0.2.8:
resolution: {integrity: sha512-RYgD/d39YyN91chDESwdGDUdK6Ph9oX14y1HXnMfACnEnviOtCQkMkjBLu0RoSPfqHiLJe8PopgASfv/XOAT7g==}
/@firecamp/cloud-apis@0.2.10:
resolution: {integrity: sha512-aWTxsmfUygfa0Tv9TaRUhbNPTP9I6CVjJhrY9kLJwdtzRcbg5jW7oqnD1HEl0662pwPyYJ1weC3XOo2/cJJ+JQ==}
engines: {node: '>=10'}
dependencies:
axios: 0.21.4
@@ -10220,6 +10229,7 @@ packages:
/async-each@1.0.3:
resolution: {integrity: sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==}
requiresBuild: true
dev: true
optional: true
@@ -10831,6 +10841,7 @@ packages:
/big-integer@1.6.51:
resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
engines: {node: '>=0.6'}
requiresBuild: true
dev: true
optional: true
@@ -10845,6 +10856,7 @@ packages:
/binary-extensions@1.13.1:
resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: true
optional: true
@@ -10945,6 +10957,7 @@ packages:
/bplist-parser@0.1.1:
resolution: {integrity: sha512-2AEM0FXy8ZxVLBuqX0hqt1gDwcnz2zygEkQ6zaD5Wko/sB9paUNwlpawrFtKeHUAQUOzjVy9AO4oeonqIHKA9Q==}
requiresBuild: true
dependencies:
big-integer: 1.6.51
dev: true
@@ -11289,6 +11302,7 @@ packages:
/camelcase-keys@2.1.0:
resolution: {integrity: sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
camelcase: 2.1.1
map-obj: 1.0.1
@@ -11312,6 +11326,7 @@ packages:
/camelcase@2.1.1:
resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: true
optional: true
@@ -11476,6 +11491,7 @@ packages:
/chokidar@2.1.8:
resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==}
deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
requiresBuild: true
dependencies:
anymatch: 2.0.0
async-each: 1.0.3
@@ -11927,7 +11943,7 @@ packages:
- supports-color
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
/concat-stream@1.4.11:
resolution: {integrity: sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==}
@@ -14564,6 +14580,7 @@ packages:
/find-up@1.1.2:
resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
path-exists: 2.1.0
pinkie-promise: 2.0.1
@@ -15030,6 +15047,7 @@ packages:
/get-stdin@4.0.1:
resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: true
optional: true
@@ -16071,6 +16089,7 @@ packages:
/indent-string@2.1.0:
resolution: {integrity: sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
repeating: 2.0.1
dev: true
@@ -16220,6 +16239,7 @@ packages:
/is-binary-path@1.0.1:
resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
binary-extensions: 1.13.1
dev: true
@@ -16353,6 +16373,7 @@ packages:
/is-finite@1.1.0:
resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: true
optional: true
@@ -16584,6 +16605,7 @@ packages:
/is-utf8@0.2.1:
resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
requiresBuild: true
dev: true
optional: true
@@ -18469,6 +18491,7 @@ packages:
/load-json-file@1.1.0:
resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
graceful-fs: 4.2.11
parse-json: 2.2.0
@@ -18980,6 +19003,7 @@ packages:
/meow@3.7.0:
resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
camelcase-keys: 2.1.0
decamelize: 1.2.0
@@ -20172,6 +20196,7 @@ packages:
/parse-json@2.2.0:
resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
error-ex: 1.3.2
dev: true
@@ -20256,11 +20281,13 @@ packages:
/path-dirname@1.0.2:
resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
requiresBuild: true
dev: true
/path-exists@2.1.0:
resolution: {integrity: sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
pinkie-promise: 2.0.1
dev: true
@@ -20310,6 +20337,7 @@ packages:
/path-type@1.1.0:
resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
graceful-fs: 4.2.11
pify: 2.3.0
@@ -20402,6 +20430,7 @@ packages:
/pinkie-promise@2.0.1:
resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
pinkie: 2.0.4
dev: true
@@ -20415,6 +20444,7 @@ packages:
/pinkie@2.0.4:
resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: true
optional: true
@@ -21583,6 +21613,7 @@ packages:
/read-pkg-up@1.0.1:
resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
find-up: 1.1.2
read-pkg: 1.1.0
@@ -21609,6 +21640,7 @@ packages:
/read-pkg@1.1.0:
resolution: {integrity: sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
load-json-file: 1.1.0
normalize-package-data: 2.5.0
@@ -21689,6 +21721,7 @@ packages:
/readdirp@2.2.1:
resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==}
engines: {node: '>=0.10'}
requiresBuild: true
dependencies:
graceful-fs: 4.2.11
micromatch: 3.1.10
@@ -21720,6 +21753,7 @@ packages:
/redent@1.0.0:
resolution: {integrity: sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
indent-string: 2.1.0
strip-indent: 1.0.1
@@ -21931,6 +21965,7 @@ packages:
/repeating@2.0.1:
resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
is-finite: 1.1.0
dev: true
@@ -23255,6 +23290,7 @@ packages:
/strip-bom@2.0.0:
resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
is-utf8: 0.2.1
dev: true
@@ -23288,6 +23324,7 @@ packages:
resolution: {integrity: sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==}
engines: {node: '>=0.10.0'}
hasBin: true
requiresBuild: true
dependencies:
get-stdin: 4.0.1
dev: true
@@ -23659,6 +23696,53 @@ packages:
serialize-javascript: 6.0.1
terser: 5.16.1
webpack: 5.75.0(webpack-cli@5.0.1)
dev: true
/terser-webpack-plugin@5.3.9(webpack@5.75.0):
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==}
engines: {node: '>= 10.13.0'}
peerDependencies:
'@swc/core': '*'
esbuild: '*'
uglify-js: '*'
webpack: ^5.1.0
peerDependenciesMeta:
'@swc/core':
optional: true
esbuild:
optional: true
uglify-js:
optional: true
dependencies:
'@jridgewell/trace-mapping': 0.3.17
jest-worker: 27.5.1
schema-utils: 3.1.1
serialize-javascript: 6.0.1
terser: 5.19.2
webpack: 5.75.0(webpack-cli@5.0.1)
/terser-webpack-plugin@5.3.9(webpack@5.88.2):
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==}
engines: {node: '>= 10.13.0'}
peerDependencies:
'@swc/core': '*'
esbuild: '*'
uglify-js: '*'
webpack: ^5.1.0
peerDependenciesMeta:
'@swc/core':
optional: true
esbuild:
optional: true
uglify-js:
optional: true
dependencies:
'@jridgewell/trace-mapping': 0.3.18
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.19.2
webpack: 5.88.2(webpack-cli@5.0.1)
/terser-webpack-plugin@5.3.9(webpack@5.88.2):
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==}
@@ -23703,6 +23787,27 @@ packages:
acorn: 8.8.2
commander: 2.20.3
source-map-support: 0.5.21
dev: true
/terser@5.19.2:
resolution: {integrity: sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==}
engines: {node: '>=10'}
hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.5
acorn: 8.8.2
commander: 2.20.3
source-map-support: 0.5.21
/terser@5.19.2:
resolution: {integrity: sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==}
engines: {node: '>=10'}
hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.5
acorn: 8.10.0
commander: 2.20.3
source-map-support: 0.5.21
/terser@5.19.2:
resolution: {integrity: sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==}
@@ -23897,6 +24002,7 @@ packages:
/trim-newlines@1.0.0:
resolution: {integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: true
optional: true
@@ -24593,6 +24699,7 @@ packages:
/untildify@2.1.0:
resolution: {integrity: sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
os-homedir: 1.0.2
dev: true
@@ -25059,7 +25166,7 @@ packages:
webpack: 5.75.0(webpack-cli@5.0.1)
webpack-bundle-analyzer: 4.7.0
webpack-dev-server: 4.11.1(webpack-cli@5.0.1)(webpack@5.75.0)
webpack-merge: 5.8.0
webpack-merge: 5.9.0
/webpack-dev-middleware@2.0.6(webpack@5.75.0):
resolution: {integrity: sha512-tj5LLD9r4tDuRIDa5Mu9lnY2qBBehAITv6A9irqXhw/HQquZgTx3BCd57zYbU2gMDnncA49ufK2qVQSbaKJwOw==}
@@ -25226,8 +25333,8 @@ packages:
uuid: 3.4.0
dev: true
/webpack-merge@5.8.0:
resolution: {integrity: sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==}
/webpack-merge@5.9.0:
resolution: {integrity: sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==}
engines: {node: '>=10.0.0'}
dependencies:
clone-deep: 4.0.1
@@ -25328,7 +25435,47 @@ packages:
neo-async: 2.6.2
schema-utils: 3.1.1
tapable: 2.2.1
terser-webpack-plugin: 5.3.6(webpack@5.75.0)
terser-webpack-plugin: 5.3.9(webpack@5.75.0)
watchpack: 2.4.0
webpack-cli: 5.0.1(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.11.1)(webpack@5.75.0)
webpack-sources: 3.2.3
transitivePeerDependencies:
- '@swc/core'
- esbuild
- uglify-js
/webpack@5.88.2(webpack-cli@5.0.1):
resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==}
engines: {node: '>=10.13.0'}
hasBin: true
peerDependencies:
webpack-cli: '*'
peerDependenciesMeta:
webpack-cli:
optional: true
dependencies:
'@types/eslint-scope': 3.7.4
'@types/estree': 1.0.1
'@webassemblyjs/ast': 1.11.6
'@webassemblyjs/wasm-edit': 1.11.6
'@webassemblyjs/wasm-parser': 1.11.6
acorn: 8.10.0
acorn-import-assertions: 1.9.0(acorn@8.10.0)
browserslist: 4.21.9
chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0
es-module-lexer: 1.3.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
loader-runner: 4.3.0
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.9(webpack@5.88.2)
watchpack: 2.4.0
webpack-cli: 5.0.1(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.11.1)(webpack@5.75.0)
webpack-sources: 3.2.3

View File

@@ -1,45 +0,0 @@
/* eslint-disable no-console */
require('dotenv').config();
const fs = require('fs');
const path = require('path');
require('shelljs/global');
const { Environment, AppFormat } = require('./constants');
const build = require('../webpack.prod');
const env = process.env.NODE_ENV;
module.exports = async () => {
try {
// hold the build path as per the environment mode
const buildPath = path.join(`${__dirname}/../build/${env}`);
// copy project assets and generate config.
const directoryPaths = [path.join(`${__dirname}/../build`), buildPath];
// Remove build before start bundle
rm('-rf', buildPath);
// Create build directories
directoryPaths.forEach((directoryPath) => {
if (!fs.existsSync(directoryPath)) mkdir(directoryPath);
});
// Copy react app assets
cp(
'-R',
path.join(`${__dirname}/../platform/firecamp-platform/public/assets/*`),
buildPath
);
// generate package.json and manifest based on app environment
// exec(`node ${buildPath}/build-scripts/init-package.js`);
if (env === Environment.Production || env === Environment.Staging) {
await build();
}
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
};
if (env === Environment.Development) module.exports();

View File

@@ -1,21 +1,12 @@
/* eslint-disable no-console */
require('dotenv-vault-core').config();
const { red, yellow } = require('colors');
const semver = require('semver');
// const semver = require('semver');
require('shelljs/global');
const build = require('./build');
const { version } = require('../package.json');
const { Environment, AppFormat } = require('./constants');
const { Environment } = require('./constants');
const env = process.env.NODE_ENV;
const helper = {
buildWebApp: async () => {
process.env.NODE_OPTIONS = '--max-old-space-size=4096';
await build();
},
};
// set app version in the environment
process.env.APP_VERSION = version;
// check FIRECAMP_API_HOST env. variable value does not contains invalid value
@@ -33,20 +24,5 @@ if (
process.env.FIRECAMP_API_HOST
)})`
);
process.exit();
}
const preBuildCliCommands = async () => {
// pre conditions can be validated here
return Promise.resolve();
};
if ([Environment.Production, Environment.Staging].includes(env)) {
try {
preBuildCliCommands().then(async () => {
await helper.buildWebApp();
});
} catch (error) {
console.error(error);
}
process.exit(1);
}

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<title>Firecamp, API campsite for developers.</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta NAME="robots" CONTENT="noindex,nofollow">
</head>
<body>
<div id="root"></div>

View File

@@ -2,11 +2,47 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Firecamp, API campsite for developers.</title>
<!-- HTML Meta Tags -->
<title>Firecamp, The API Campsite for Developers.</title>
<meta
name="description"
content="Build APIs 3x faster with Firecamp's features like team collaboration, API documentation, API collection etc. Boost your confidence to build APIs for millions of users with Firecamp's simple, powerful, and open-source platform."
/>
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://firecamp.dev/" />
<meta property="og:type" content="website" />
<meta
property="og:title"
content="Firecamp, The API Campsite for Developers."
/>
<meta
property="og:description"
content="Build APIs 3x faster with Firecamp's features like team collaboration, API documentation, API collection etc. Boost your confidence to build APIs for millions of users with Firecamp's simple, powerful, and open-source platform."
/>
<meta property="og:image" content="https://firecamp.io/og.png" />
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="firecamp.dev" />
<meta property="twitter:url" content="https://firecamp.dev/" />
<meta
name="twitter:title"
content="Firecamp, The API Campsite for Developers."
/>
<meta
name="twitter:description"
content="Build APIs 3x faster with Firecamp's features like team collaboration, API documentation, API collection etc. Boost your confidence to build APIs for millions of users with Firecamp's simple, powerful, and open-source platform."
/>
<meta name="twitter:image" content="https://firecamp.io/og.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
<meta
http-equiv="Content-Security-Policy"
content="upgrade-insecure-requests"
/>
</head>
<body>
<div id="root"></div>
</body>
</html>
</html>

View File

@@ -2,132 +2,26 @@
require('dotenv').config();
/* eslint-disable no-console */
require('dotenv-vault-core').config();
console.log(process.env.FIRECAMP_API_HOST, 'FIRECAMP_API_HOST'); // for debugging purposes. remove when ready.
const webpack = require('webpack');
const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies
// const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { Environment } = require('./scripts/constants');
const env = process.env.NODE_ENV;
const CopyPlugin = require('copy-webpack-plugin');
const metadata = require('./package.json');
exports.common = {
entry: {
index: path.join(
__dirname,
'./platform/firecamp-platform/src/containers/index.tsx'
),
identity: path.join(
__dirname,
'./platform/firecamp-platform/src/containers/identity.tsx'
),
},
optimization: {
nodeEnv: process.env.NODE_ENV,
minimize: true,
runtimeChunk: 'single',
splitChunks: {
// name: 'vendor',
// chunks(chunk) {
// // To prevent generate separate chunk for background script
// // Because all node_modules not needed in background script
// return !chunk?.name?.includes('background');
// },
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
return 'vender';
const NodeEnv = process.env.NODE_ENV;
console.log(process.env.FIRECAMP_API_HOST, 'FIRECAMP_API_HOST'); // for debugging purposes. remove when ready.
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
// const packageName = module.context.match(
// /[\\/]node_modules[\\/](.*?)([\\/]|$)/
// )[1];
// // npm package names are URL-safe, but some servers don't like @ symbols
// return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
resolve: {
extensions: ['*', '.mjs', '.js', '.json', '.jsx', '.ts', '.tsx', '.svg'],
alias: {
// faker: path.resolve('./node_modules/faker'),
react: path.resolve('./node_modules/react'),
'react-dom': path.resolve('./node_modules/react-dom'),
lodash: path.resolve('./node_modules/lodash'),
nanoid: path.resolve('./node_modules/nanoid'),
'awesome-notifications': path.resolve(
'./node_modules/awesome-notifications'
),
'@babel/runtime': path.resolve('./node_modules/@babel/runtime'),
'monaco-editor': path.resolve('./node_modules/monaco-editor'),
},
fallback: {
fs: false,
},
},
};
const outputPath = `${__dirname}/build/${env}`;
const publicPath = '';
exports.output = {
globalObject: 'this',
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
path: outputPath,
publicPath,
};
// exports.output.path = path.join(__dirname, `./build/${env}`);
if (env === Environment.Development) exports.output.clean = true;
exports.env = {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
FIRECAMP_API_HOST: JSON.stringify(process.env.FIRECAMP_API_HOST),
FIRECAMP_CLOUD_AGENT: JSON.stringify(process.env.FIRECAMP_CLOUD_AGENT),
FIRECAMP_EXTENSION_AGENT_ID: JSON.stringify(
process.env.FIRECAMP_EXTENSION_AGENT_ID
),
APP_VERSION: JSON.stringify(metadata.version),
AppFormat: JSON.stringify(process.env.AppFormat),
SENTRY_DSN: JSON.stringify(process.env.SENTRY_DSN),
CRISP_WEBSITE_ID: JSON.stringify(process.env.CRISP_WEBSITE_ID),
CRISP_FIRECAMP_DEV: JSON.stringify(process.env.CRISP_FIRECAMP_DEV),
GOOGLE_OAUTH2_CLIENT_ID: JSON.stringify(process.env.GOOGLE_OAUTH2_CLIENT_ID),
GOOGLE_OAUTH2_REDIRECT_URI: JSON.stringify(
process.env.GOOGLE_OAUTH2_REDIRECT_URI
),
GITHUB_OAUTH2_CLIENT_ID: JSON.stringify(process.env.GITHUB_OAUTH2_CLIENT_ID),
GITHUB_OAUTH2_REDIRECT_URI: JSON.stringify(
process.env.GITHUB_OAUTH2_REDIRECT_URI
),
GOOGLE_ANALYTICS_CHROME_ID: JSON.stringify(
process.env.GOOGLE_ANALYTICS_CHROME_ID
),
GOOGLE_ANALYTICS_ELECTRON_ID: JSON.stringify(
process.env.GOOGLE_ANALYTICS_ELECTRON_ID
),
};
exports.plugins = [
const plugins = [
new HtmlWebpackPlugin({
inject: true,
chunks: ['index'],
filename: 'index.html',
template: 'templates/index.html',
favicon: 'templates/favicon.png',
hash: true,
}),
new HtmlWebpackPlugin({
inject: true,
@@ -156,10 +50,79 @@ exports.plugins = [
// publicPath: '/js',
// filename: '[name].worker.bundle.js',
// languages: ['javascript', 'html', 'typescript', 'json'],
// }),
// }),11
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
FIRECAMP_API_HOST: JSON.stringify(process.env.FIRECAMP_API_HOST),
FIRECAMP_CLOUD_AGENT: JSON.stringify(process.env.FIRECAMP_CLOUD_AGENT),
FIRECAMP_EXTENSION_AGENT_ID: JSON.stringify(
process.env.FIRECAMP_EXTENSION_AGENT_ID
),
APP_VERSION: JSON.stringify(metadata.version),
AppFormat: JSON.stringify(process.env.AppFormat),
SENTRY_DSN: JSON.stringify(process.env.SENTRY_DSN),
CRISP_WEBSITE_ID: JSON.stringify(process.env.CRISP_WEBSITE_ID),
CRISP_FIRECAMP_DEV: JSON.stringify(process.env.CRISP_FIRECAMP_DEV),
GOOGLE_OAUTH2_CLIENT_ID: JSON.stringify(
process.env.GOOGLE_OAUTH2_CLIENT_ID
),
GOOGLE_OAUTH2_REDIRECT_URI: JSON.stringify(
process.env.GOOGLE_OAUTH2_REDIRECT_URI
),
GITHUB_OAUTH2_CLIENT_ID: JSON.stringify(
process.env.GITHUB_OAUTH2_CLIENT_ID
),
GITHUB_OAUTH2_REDIRECT_URI: JSON.stringify(
process.env.GITHUB_OAUTH2_REDIRECT_URI
),
GOOGLE_ANALYTICS_CHROME_ID: JSON.stringify(
process.env.GOOGLE_ANALYTICS_CHROME_ID
),
GOOGLE_ANALYTICS_ELECTRON_ID: JSON.stringify(
process.env.GOOGLE_ANALYTICS_ELECTRON_ID
),
},
}),
new CopyPlugin({
patterns: [
{
from: `${__dirname}/platform/firecamp-platform/public/assets`,
to: `${__dirname}/build/${NodeEnv}`,
},
],
}),
];
exports.rules = [
const rules = [
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [
'@babel/preset-env',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
'@babel/preset-typescript',
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
regenerator: true,
},
],
['@babel/plugin-proposal-export-default-from'],
'add-module-exports',
],
},
},
{ test: /\.flow$/, loader: 'ignore-loader' },
{
test: /\.css$/,
@@ -195,3 +158,68 @@ exports.rules = [
},
},
];
module.exports = {
entry: {
index: path.join(
__dirname,
'./platform/firecamp-platform/src/containers/index.tsx'
),
identity: path.join(
__dirname,
'./platform/firecamp-platform/src/containers/identity.tsx'
),
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
// name: 'vendor',
// chunks(chunk) {
// // To prevent generate separate chunk for background script
// // Because all node_modules not needed in background script
// return !chunk?.name?.includes('background');
// },
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
// eslint-disable-next-line no-unused-vars
name(module) {
return 'vender';
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
// const packageName = module.context.match(
// /[\\/]node_modules[\\/](.*?)([\\/]|$)/
// )[1];
// // npm package names are URL-safe, but some servers don't like @ symbols
// return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
resolve: {
extensions: ['*', '.mjs', '.js', '.json', '.jsx', '.ts', '.tsx', '.svg'],
alias: {
// faker: path.resolve('./node_modules/faker'),
react: path.resolve('./node_modules/react'),
'react-dom': path.resolve('./node_modules/react-dom'),
lodash: path.resolve('./node_modules/lodash'),
nanoid: path.resolve('./node_modules/nanoid'),
'awesome-notifications': path.resolve(
'./node_modules/awesome-notifications'
),
'@babel/runtime': path.resolve('./node_modules/@babel/runtime'),
'monaco-editor': path.resolve('./node_modules/monaco-editor'),
},
fallback: {
fs: false,
},
},
plugins,
module: { rules },
};

View File

@@ -1,18 +1,36 @@
// imports environment
require('dotenv').config();
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { common, env, plugins, rules, output } = require('./webpack.config');
const { merge } = require('webpack-merge');
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const TerserPlugin = require('terser-webpack-plugin');
const base = require('./webpack.common');
const withReport = process.env.npm_config_withReport;
// const withReport = process.env.npm_config_withReport;
const nodeEnv = process.env.NODE_ENV;
module.exports = {
...common,
module.exports = merge(base, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devtool: 'cheap-module-source-map',
output: {
clean: true,
globalObject: 'this',
filename: '[name].dev.js',
chunkFilename: '[name].dev.js',
path: `${__dirname}/build/${nodeEnv}`,
publicPath: '',
},
optimization: {
nodeEnv: 'development',
minimizer: [
new TerserPlugin({
parallel: 4,
minify: TerserPlugin.esbuildMinify,
// terserOptions: {
// sourceMap: 'external',
// },
}),
],
},
devServer: {
//server: 'https',
static: path.join(__dirname, './build/development'),
@@ -28,51 +46,9 @@ module.exports = {
'X-Requested-With, content-type, Authorization',
},
},
output,
plugins: [
...plugins,
new webpack.HotModuleReplacementPlugin(),
new webpack.IgnorePlugin({ resourceRegExp: /[^/]+\/[\S]+.dev$/ }),
new webpack.DefinePlugin({
'process.env': {
...env,
FIRECAMP_EXTENSION_AGENT_ID: JSON.stringify(
process.env.FIRECAMP_EXTENSION_AGENT_ID
),
},
}),
// new BundleAnalyzerPlugin(),
],
module: {
rules: [
...rules,
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
'@babel/preset-typescript',
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
regenerator: true,
},
],
['@babel/plugin-proposal-export-default-from'],
'add-module-exports',
],
},
},
],
},
};
});

View File

@@ -1,94 +1,37 @@
/* eslint-disable no-console */
const path = require('path');
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
const { common, env, plugins, rules } = require('./webpack.config');
const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
// const CompressionPlugin = require('compression-webpack-plugin');
const base = require('./webpack.common');
const nodeEnv = process.env.NODE_ENV;
const config = {
...common,
module.exports = merge(base, {
mode: 'production',
output: {
clean: true,
globalObject: 'this',
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
filename: '[name].min.js',
chunkFilename: '[name].min.js',
path: path.join(__dirname, `./build/${nodeEnv}`),
},
plugins: [
...plugins,
new webpack.ProvidePlugin({
React: 'react',
}),
new webpack.IgnorePlugin({ resourceRegExp: /[^/]+\/[\S]+.prod$/ }),
new webpack.DefinePlugin({
'process.env': env,
}),
new CompressionPlugin(),
],
module: {
rules: [
...rules,
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
'@babel/preset-typescript',
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
regenerator: true,
},
],
['@babel/plugin-proposal-export-default-from'],
'add-module-exports',
['transform-remove-console', { exclude: ['info'] }],
],
},
},
optimization: {
nodeEnv: 'production',
// minimize: true,
minimizer: [
new TerserPlugin({
parallel: 4,
minify: TerserPlugin.esbuildMinify,
// terserOptions: {
// sourceMap: 'external',
// },
}),
],
},
};
module.exports = () =>
new Promise((resolve, reject) => {
console.log('[Webpack Build]');
console.log('-'.repeat(80));
const compiler = webpack(config);
compiler.run((err, stats) => {
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
reject(err.stack || err.details || err);
}
const info = stats.toJson();
if (stats.hasErrors()) {
console.error(info.errors);
reject(info.errors);
}
if (stats.hasWarnings()) {
console.warn(info.warnings);
}
resolve();
});
});
plugins: [
new webpack.ProvidePlugin({ React: 'react' }),
new webpack.IgnorePlugin({ resourceRegExp: /[^/]+\/[\S]+.prod$/ }),
// new CompressionPlugin(),
],
});