forked from github/plane
dev: implemented MobX in workspace settings and create workspace form (#2561)
* dev: implement mobx store for workspace settings * chore: workspace general settings mobx integration * chore: workspace members settings mobx integration
This commit is contained in:
parent
050406b8a4
commit
dcf81e28e4
@ -31,7 +31,7 @@ export const JiraImportUsers: FC = () => {
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug?.toString() ?? "") : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembersWithEmail(workspaceSlug?.toString() ?? "") : null
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug?.toString() ?? "") : null
|
||||
);
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
|
@ -1,6 +1,5 @@
|
||||
export * from "./attachment";
|
||||
export * from "./comment";
|
||||
export * from "./my-issues";
|
||||
export * from "./sidebar-select";
|
||||
export * from "./view-select";
|
||||
export * from "./activity";
|
||||
|
@ -1,3 +0,0 @@
|
||||
export * from "./my-issues-select-filters";
|
||||
export * from "./my-issues-view-options";
|
||||
export * from "./my-issues-view";
|
@ -1,213 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import { IssueLabelService } from "services/issue";
|
||||
// components
|
||||
import { DateFilterModal } from "components/core";
|
||||
// ui
|
||||
import { MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, TStateGroups } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||
|
||||
type Props = {
|
||||
filters: Partial<IIssueFilterOptions> | any;
|
||||
onSelect: (option: any) => void;
|
||||
direction?: "left" | "right";
|
||||
height?: "sm" | "md" | "rg" | "lg";
|
||||
};
|
||||
|
||||
const issueLabelService = new IssueLabelService();
|
||||
|
||||
export const MyIssuesSelectFilters: React.FC<Props> = ({ filters, onSelect, direction = "right", height = "md" }) => {
|
||||
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||
const [dateFilterType, setDateFilterType] = useState<{
|
||||
title: string;
|
||||
type: "start_date" | "target_date";
|
||||
}>({
|
||||
title: "",
|
||||
type: "start_date",
|
||||
});
|
||||
const [fetchLabels, setFetchLabels] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && fetchLabels ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && fetchLabels ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* {isDateFilterModalOpen && (
|
||||
<DateFilterModal
|
||||
title={dateFilterType.title}
|
||||
field={dateFilterType.type}
|
||||
filters={filters as IIssueFilterOptions}
|
||||
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||
isOpen={isDateFilterModalOpen}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
)} */}
|
||||
<MultiLevelDropdown
|
||||
label="Filters"
|
||||
onSelect={onSelect}
|
||||
direction={direction}
|
||||
height={height}
|
||||
options={[
|
||||
{
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...PRIORITIES.map((priority) => ({
|
||||
id: priority === null ? "null" : priority,
|
||||
label: (
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "priority",
|
||||
value: priority === null ? "null" : priority,
|
||||
},
|
||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "state_group",
|
||||
label: "State groups",
|
||||
value: GROUP_CHOICES,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...Object.keys(GROUP_CHOICES).map((key) => ({
|
||||
id: key,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={key as TStateGroups} />
|
||||
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "state_group",
|
||||
value: key,
|
||||
},
|
||||
selected: filters?.state?.includes(key),
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
label: "Labels",
|
||||
onClick: () => setFetchLabels(true),
|
||||
value: labels,
|
||||
hasChildren: true,
|
||||
children: labels?.map((label) => ({
|
||||
id: label.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "labels",
|
||||
value: label.id,
|
||||
},
|
||||
selected: filters?.labels?.includes(label.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "start_date",
|
||||
label: "Start date",
|
||||
value: DATE_FILTER_OPTIONS,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...(DATE_FILTER_OPTIONS?.map((option) => ({
|
||||
id: option.name,
|
||||
label: option.name,
|
||||
value: {
|
||||
key: "start_date",
|
||||
value: option.value,
|
||||
},
|
||||
selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], [option.value]),
|
||||
})) ?? []),
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
value: "custom",
|
||||
element: (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDateFilterModalOpen(true);
|
||||
setDateFilterType({
|
||||
title: "Start date",
|
||||
type: "start_date",
|
||||
});
|
||||
}}
|
||||
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "target_date",
|
||||
label: "Due date",
|
||||
value: DATE_FILTER_OPTIONS,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...(DATE_FILTER_OPTIONS?.map((option) => ({
|
||||
id: option.name,
|
||||
label: option.name,
|
||||
value: {
|
||||
key: "target_date",
|
||||
value: option.value,
|
||||
},
|
||||
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], [option.value]),
|
||||
})) ?? []),
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
value: "custom",
|
||||
element: (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDateFilterModalOpen(true);
|
||||
setDateFilterType({
|
||||
title: "Due date",
|
||||
type: "target_date",
|
||||
});
|
||||
}}
|
||||
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,105 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||
// components
|
||||
import { MyIssuesSelectFilters } from "components/issues";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// icons
|
||||
import { List, Sheet } from "lucide-react";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { TIssueLayouts } from "types";
|
||||
|
||||
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
Icon: List,
|
||||
},
|
||||
{
|
||||
type: "spreadsheet",
|
||||
Icon: Sheet,
|
||||
},
|
||||
];
|
||||
|
||||
export const MyIssuesViewOptions: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, globalViewId } = router.query;
|
||||
|
||||
const { displayFilters, setDisplayFilters, filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
|
||||
|
||||
const workspaceViewPathName = ["workspace-views/all-issues"];
|
||||
|
||||
const isWorkspaceViewPath = workspaceViewPathName.some((pathname) => router.pathname.includes(pathname));
|
||||
|
||||
const showFilters = isWorkspaceViewPath || globalViewId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewOptions.map((option) => (
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
|
||||
position="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-100 duration-300 ${
|
||||
displayFilters?.layout === option.type
|
||||
? "bg-custom-sidebar-background-100 shadow-sm"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setDisplayFilters({ layout: option.type });
|
||||
if (option.type === "spreadsheet") router.push(`/${workspaceSlug}/workspace-views/all-issues`);
|
||||
else router.push(`/${workspaceSlug}/workspace-views`);
|
||||
}}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
className={option.type === "spreadsheet" ? "h-4 w-4" : ""}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
{showFilters && (
|
||||
<MyIssuesSelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
if (key === "start_date" || key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
|
||||
|
||||
setFilters({
|
||||
[key]: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists)
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,296 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import { IssueLabelService } from "services/issue";
|
||||
// hooks
|
||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||
// components
|
||||
import { FiltersList } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// types
|
||||
import { IIssue, IIssueFilterOptions } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
openIssuesListModal?: () => void;
|
||||
disableUserActions?: false;
|
||||
};
|
||||
|
||||
const issueLabelService = new IssueLabelService();
|
||||
|
||||
export const MyIssuesView: React.FC<Props> = () => {
|
||||
// create issue modal
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
const [preloadedData] = useState<(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
// update issue modal
|
||||
const [editIssueModal, setEditIssueModal] = useState(false);
|
||||
const [issueToEdit] = useState<(IIssue & { actionType: "edit" | "delete" }) | undefined>(undefined);
|
||||
|
||||
// delete issue modal
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [issueToDelete] = useState<IIssue | null>(null);
|
||||
|
||||
// trash box
|
||||
// const [trashBox, setTrashBox] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||
const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0 ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
// const handleDeleteIssue = useCallback(
|
||||
// (issue: IIssue) => {
|
||||
// setDeleteIssueModal(true);
|
||||
// setIssueToDelete(issue);
|
||||
// },
|
||||
// [setDeleteIssueModal, setIssueToDelete]
|
||||
// );
|
||||
|
||||
// const handleOnDragEnd = useCallback(
|
||||
// async (result: DropResult) => {
|
||||
// setTrashBox(false);
|
||||
|
||||
// if (!result.destination || !workspaceSlug || !groupedIssues || displayFilters?.group_by !== "priority") return;
|
||||
|
||||
// const { source, destination } = result;
|
||||
|
||||
// if (source.droppableId === destination.droppableId) return;
|
||||
|
||||
// const draggedItem = groupedIssues[source.droppableId][source.index];
|
||||
|
||||
// if (!draggedItem) return;
|
||||
|
||||
// if (destination.droppableId === "trashBox") handleDeleteIssue(draggedItem);
|
||||
// else {
|
||||
// const sourceGroup = source.droppableId;
|
||||
// const destinationGroup = destination.droppableId;
|
||||
|
||||
// draggedItem[displayFilters.group_by] = destinationGroup as TIssuePriorities;
|
||||
|
||||
// mutate<{
|
||||
// [key: string]: IIssue[];
|
||||
// }>(
|
||||
// USER_ISSUES(workspaceSlug.toString(), params),
|
||||
// (prevData) => {
|
||||
// if (!prevData) return prevData;
|
||||
|
||||
// const sourceGroupArray = [...groupedIssues[sourceGroup]];
|
||||
// const destinationGroupArray = [...groupedIssues[destinationGroup]];
|
||||
|
||||
// sourceGroupArray.splice(source.index, 1);
|
||||
// destinationGroupArray.splice(destination.index, 0, draggedItem);
|
||||
|
||||
// return {
|
||||
// ...prevData,
|
||||
// [sourceGroup]: orderArrayBy(sourceGroupArray, displayFilters.order_by ?? "-created_at"),
|
||||
// [destinationGroup]: orderArrayBy(destinationGroupArray, displayFilters.order_by ?? "-created_at"),
|
||||
// };
|
||||
// },
|
||||
// false
|
||||
// );
|
||||
|
||||
// // patch request
|
||||
// issuesService
|
||||
// .patchIssue(
|
||||
// workspaceSlug as string,
|
||||
// draggedItem.project,
|
||||
// draggedItem.id,
|
||||
// {
|
||||
// priority: draggedItem.priority,
|
||||
// },
|
||||
// user
|
||||
// )
|
||||
// .catch(() => mutate(USER_ISSUES(workspaceSlug.toString(), params)));
|
||||
// }
|
||||
// },
|
||||
// [displayFilters, groupedIssues, handleDeleteIssue, params, user, workspaceSlug]
|
||||
// );
|
||||
|
||||
// const addIssueToGroup = useCallback(
|
||||
// (groupTitle: string) => {
|
||||
// setCreateIssueModal(true);
|
||||
|
||||
// let preloadedValue: string | string[] = groupTitle;
|
||||
|
||||
// if (displayFilters?.group_by === "labels") {
|
||||
// if (groupTitle === "None") preloadedValue = [];
|
||||
// else preloadedValue = [groupTitle];
|
||||
// }
|
||||
|
||||
// if (displayFilters?.group_by)
|
||||
// setPreloadedData({
|
||||
// [displayFilters?.group_by]: preloadedValue,
|
||||
// actionType: "createIssue",
|
||||
// });
|
||||
// else setPreloadedData({ actionType: "createIssue" });
|
||||
// },
|
||||
// [setCreateIssueModal, setPreloadedData, displayFilters?.group_by]
|
||||
// );
|
||||
|
||||
// const addIssueToDate = useCallback(
|
||||
// (date: string) => {
|
||||
// setCreateIssueModal(true);
|
||||
// setPreloadedData({
|
||||
// target_date: date,
|
||||
// actionType: "createIssue",
|
||||
// });
|
||||
// },
|
||||
// [setCreateIssueModal, setPreloadedData]
|
||||
// );
|
||||
|
||||
// const makeIssueCopy = useCallback(
|
||||
// (issue: IIssue) => {
|
||||
// setCreateIssueModal(true);
|
||||
|
||||
// setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" });
|
||||
// },
|
||||
// [setCreateIssueModal, setPreloadedData]
|
||||
// );
|
||||
|
||||
// const handleEditIssue = useCallback(
|
||||
// (issue: IIssue) => {
|
||||
// setEditIssueModal(true);
|
||||
// setIssueToEdit({
|
||||
// ...issue,
|
||||
// actionType: "edit",
|
||||
// cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null,
|
||||
// module: issue.issue_module ? issue.issue_module.module : null,
|
||||
// });
|
||||
// },
|
||||
// [setEditIssueModal, setIssueToEdit]
|
||||
// );
|
||||
|
||||
// const handleIssueAction = useCallback(
|
||||
// (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
|
||||
// if (action === "copy") makeIssueCopy(issue);
|
||||
// else if (action === "edit") handleEditIssue(issue);
|
||||
// else if (action === "delete") handleDeleteIssue(issue);
|
||||
// },
|
||||
// [makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||
// );
|
||||
|
||||
const filtersToDisplay = { ...filters, assignees: null, created_by: null, subscriber: null };
|
||||
|
||||
const nullFilters = Object.keys(filtersToDisplay).filter(
|
||||
(key) => filtersToDisplay[key as keyof IIssueFilterOptions] === null
|
||||
);
|
||||
const areFiltersApplied =
|
||||
Object.keys(filtersToDisplay).length > 0 && nullFilters.length !== Object.keys(filtersToDisplay).length;
|
||||
|
||||
// const isSubscribedIssuesRoute = router.pathname.includes("subscribed");
|
||||
// const isMySubscribedIssues =
|
||||
// (filters.subscriber && filters.subscriber.length > 0 && router.pathname.includes("my-issues")) ?? false;
|
||||
|
||||
// const disableAddIssueOption = isSubscribedIssuesRoute || isMySubscribedIssues;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||
handleClose={() => setCreateIssueModal(false)}
|
||||
prePopulateData={{
|
||||
...preloadedData,
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
mutateMyIssues();
|
||||
}}
|
||||
/>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||
handleClose={() => setEditIssueModal(false)}
|
||||
data={issueToEdit}
|
||||
onSubmit={async () => {
|
||||
mutateMyIssues();
|
||||
}}
|
||||
/>
|
||||
{issueToDelete && (
|
||||
<DeleteIssueModal
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
isOpen={deleteIssueModal}
|
||||
data={issueToDelete}
|
||||
onSubmit={async () => {
|
||||
mutateMyIssues();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{areFiltersApplied && (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||
<FiltersList
|
||||
filters={filtersToDisplay}
|
||||
setFilters={setFilters}
|
||||
labels={labels}
|
||||
members={undefined}
|
||||
states={undefined}
|
||||
clearAllFilters={() =>
|
||||
setFilters({
|
||||
labels: null,
|
||||
priority: null,
|
||||
state_group: null,
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||
</>
|
||||
)}
|
||||
{/* <AllViews
|
||||
addIssueToDate={addIssueToDate}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={disableUserActions}
|
||||
dragDisabled={displayFilters?.group_by !== "priority"}
|
||||
emptyState={{
|
||||
title: filters.assignees
|
||||
? "You don't have any issue assigned to you yet"
|
||||
: filters.created_by
|
||||
? "You have not created any issue yet."
|
||||
: "You have not subscribed to any issue yet.",
|
||||
description: "Keep track of your work in a single place.",
|
||||
primaryButton: filters.subscriber
|
||||
? undefined
|
||||
: {
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Issue",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
},
|
||||
}}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
|
||||
removeIssue={null}
|
||||
disableAddIssueOption={disableAddIssueOption}
|
||||
trashBox={trashBox}
|
||||
setTrashBox={setTrashBox}
|
||||
viewProps={{
|
||||
displayFilters,
|
||||
groupedIssues,
|
||||
isEmpty,
|
||||
mutateIssues: mutateMyIssues,
|
||||
params,
|
||||
properties,
|
||||
}}
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
};
|
@ -48,7 +48,6 @@ export const Workspace: React.FC<Props> = ({ finishOnboarding, stepChange, updat
|
||||
onSubmit={completeStep}
|
||||
defaultValues={defaultValues}
|
||||
setDefaultValues={setDefaultValues}
|
||||
user={user}
|
||||
primaryButtonText={{
|
||||
loading: "Creating...",
|
||||
default: "Continue",
|
||||
|
@ -1,6 +1,5 @@
|
||||
export * from "./overview";
|
||||
export * from "./navbar";
|
||||
export * from "./profile-issues-view-options";
|
||||
export * from "./profile-issues-view";
|
||||
export * from "./sidebar";
|
||||
|
||||
|
@ -1,282 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useProfileIssues from "hooks/use-profile-issues";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// components
|
||||
import { MyIssuesSelectFilters } from "components/issues";
|
||||
// ui
|
||||
import { CustomMenu, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// icons
|
||||
import { ChevronDown, Kanban, List } from "lucide-react";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { Properties, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue";
|
||||
|
||||
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
Icon: List,
|
||||
},
|
||||
{
|
||||
type: "kanban",
|
||||
Icon: Kanban,
|
||||
},
|
||||
];
|
||||
|
||||
export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const { displayFilters, setDisplayFilters, filters, displayProperties, setProperties, setFilters } = useProfileIssues(
|
||||
workspaceSlug?.toString(),
|
||||
userId?.toString()
|
||||
);
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
if (
|
||||
!router.pathname.includes("assigned") &&
|
||||
!router.pathname.includes("created") &&
|
||||
!router.pathname.includes("subscribed")
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewOptions.map((option) => (
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>}
|
||||
position="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
displayFilters?.layout === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||
>
|
||||
<option.Icon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<MyIssuesSelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
if (key === "start_date" || key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
|
||||
|
||||
setFilters({
|
||||
[key]: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists)
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||
open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
Display
|
||||
<ChevronDown className="h-3 w-3" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-custom-border-200">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
displayFilters?.group_by === "project"
|
||||
? "Project"
|
||||
: ISSUE_GROUP_BY_OPTIONS.find((option) => option.key === displayFilters?.group_by)
|
||||
?.title ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ISSUE_GROUP_BY_OPTIONS.map((option) => {
|
||||
if (displayFilters?.layout === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by" || option.key === "assignees")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||
>
|
||||
{option.title}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ISSUE_ORDER_BY_OPTIONS.find((option) => option.key === displayFilters?.order_by)
|
||||
?.title ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ISSUE_ORDER_BY_OPTIONS.map((option) => {
|
||||
if (displayFilters?.group_by === "priority" && option.key === "priority") return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setDisplayFilters({ order_by: option.key });
|
||||
}}
|
||||
>
|
||||
{option.title}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
<span className="truncate">
|
||||
{ISSUE_FILTER_OPTIONS.find((option) => option.key === displayFilters?.type)?.title ??
|
||||
"Select"}
|
||||
</span>
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ISSUE_FILTER_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.title}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters?.show_empty_groups ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({
|
||||
show_empty_groups: !displayFilters?.show_empty_groups,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
{displayProperties &&
|
||||
Object.keys(displayProperties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
if (
|
||||
displayFilters?.layout === "spreadsheet" &&
|
||||
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
|
||||
)
|
||||
return null;
|
||||
|
||||
if (
|
||||
displayFilters?.layout !== "spreadsheet" &&
|
||||
(key === "created_on" || key === "updated_on")
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
displayProperties[key as keyof Properties]
|
||||
? "border-custom-primary bg-custom-primary text-white"
|
||||
: "border-custom-border-200"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,29 +1,37 @@
|
||||
import React, { useState } from "react";
|
||||
// headless ui
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDelete: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data, handleDelete }) => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, onClose, data, onSubmit } = props;
|
||||
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
const { user: userStore } = useMobxStore();
|
||||
const user = userStore.currentUser;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setIsDeleteLoading(false);
|
||||
setIsRemoving(false);
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
handleDelete();
|
||||
setIsRemoving(true);
|
||||
|
||||
await onSubmit();
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
@ -61,14 +69,21 @@ const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data,
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Remove {data?.display_name}?
|
||||
{user?.id === data?.memberId ? "Leave workspace?" : `Remove ${data?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
{user?.id === data?.memberId ? (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to leave the workspace? You will no longer have access to this
|
||||
workspace. This action cannot be undone.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{data?.display_name}</span>? They will no longer have access to
|
||||
this workspace. This action cannot be undone.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,8 +92,8 @@ const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data,
|
||||
<Button variant="neutral-primary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDeletion} loading={isDeleteLoading}>
|
||||
{isDeleteLoading ? "Removing..." : "Remove"}
|
||||
<Button variant="danger" onClick={handleDeletion} loading={isRemoving}>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
@ -88,6 +103,4 @@ const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data,
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmWorkspaceMemberRemove;
|
||||
});
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState, FC } from "react";
|
||||
import { mutate } from "swr";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { mutate } from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// hooks
|
||||
@ -9,7 +12,7 @@ import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input } from "@plane/ui";
|
||||
// types
|
||||
import { IUser, IWorkspace } from "types";
|
||||
import { IWorkspace } from "types";
|
||||
// fetch-keys
|
||||
import { USER_WORKSPACES } from "constants/fetch-keys";
|
||||
// constants
|
||||
@ -23,7 +26,6 @@ type Props = {
|
||||
organization_size: string;
|
||||
};
|
||||
setDefaultValues: Dispatch<SetStateAction<any>>;
|
||||
user: IUser | undefined;
|
||||
secondaryButton?: React.ReactNode;
|
||||
primaryButtonText?: {
|
||||
loading: string;
|
||||
@ -49,23 +51,27 @@ const restrictedUrls = [
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const CreateWorkspaceForm: FC<Props> = ({
|
||||
export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
||||
const {
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
setDefaultValues,
|
||||
user,
|
||||
secondaryButton,
|
||||
primaryButtonText = {
|
||||
loading: "Creating...",
|
||||
default: "Create Workspace",
|
||||
},
|
||||
}) => {
|
||||
} = props;
|
||||
|
||||
const [slugError, setSlugError] = useState(false);
|
||||
const [invalidSlug, setInvalidSlug] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const { workspace: workspaceStore } = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
@ -81,8 +87,8 @@ export const CreateWorkspaceForm: FC<Props> = ({
|
||||
if (res.status === true && !restrictedUrls.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
|
||||
await workspaceService
|
||||
.createWorkspace(formData, user)
|
||||
await workspaceStore
|
||||
.createWorkspace(formData)
|
||||
.then(async (res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@ -157,7 +163,7 @@ export const CreateWorkspaceForm: FC<Props> = ({
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<label htmlFor="workspaceUrl">Workspace URL</label>
|
||||
<div className="flex w-full items-center rounded-md border border-custom-border-200 px-3">
|
||||
<div className="flex w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
|
||||
<span className="whitespace-nowrap text-sm text-custom-text-200">{window && window.location.host}/</span>
|
||||
<Controller
|
||||
control={control}
|
||||
@ -200,9 +206,10 @@ export const CreateWorkspaceForm: FC<Props> = ({
|
||||
onChange={onChange}
|
||||
label={
|
||||
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
||||
<span className="text-custom-text-200">Select organization size</span>
|
||||
<span className="text-custom-text-400">Select organization size</span>
|
||||
)
|
||||
}
|
||||
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
|
||||
input
|
||||
width="w-full"
|
||||
>
|
||||
@ -232,4 +239,4 @@ export const CreateWorkspaceForm: FC<Props> = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,31 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// types
|
||||
import type { IUser, IWorkspace } from "types";
|
||||
// fetch-keys
|
||||
import { USER_WORKSPACES } from "constants/fetch-keys";
|
||||
import type { IWorkspace } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
data: IWorkspace | null;
|
||||
onClose: () => void;
|
||||
user: IUser | undefined;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
@ -33,12 +24,13 @@ const defaultValues = {
|
||||
confirmDelete: "",
|
||||
};
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, data, onClose } = props;
|
||||
|
||||
export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, user }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { workspace: workspaceStore } = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
@ -63,15 +55,13 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
|
||||
const onSubmit = async () => {
|
||||
if (!data || !canDelete) return;
|
||||
|
||||
await workspaceService
|
||||
.deleteWorkspace(data.slug, user)
|
||||
await workspaceStore
|
||||
.deleteWorkspace(data.slug)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
|
||||
router.push("/");
|
||||
|
||||
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) => prevData?.filter((workspace) => workspace.id !== data.id));
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
@ -196,4 +186,4 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,13 +1,17 @@
|
||||
export * from "./settings";
|
||||
export * from "./views";
|
||||
export * from "./activity-graph";
|
||||
export * from "./completed-issues-graph";
|
||||
export * from "./confirm-workspace-member-remove";
|
||||
export * from "./create-workspace-form";
|
||||
export * from "./delete-workspace-modal";
|
||||
export * from "./help-section";
|
||||
export * from "./issues-list";
|
||||
export * from "./issues-pie-chart";
|
||||
export * from "./issues-stats";
|
||||
export * from "./member-select";
|
||||
export * from "./send-workspace-invitation-modal";
|
||||
export * from "./sidebar-dropdown";
|
||||
export * from "./sidebar-menu";
|
||||
export * from "./sidebar-quick-action";
|
||||
export * from "./member-select";
|
||||
export * from "./single-invitation";
|
||||
|
@ -14,14 +14,15 @@ import { Plus, X } from "lucide-react";
|
||||
import { IUser } from "types";
|
||||
// constants
|
||||
import { ROLE } from "constants/workspace";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
workspace_slug: string;
|
||||
onClose: () => void;
|
||||
workspaceSlug: string;
|
||||
user: IUser | undefined;
|
||||
onSuccess: () => void;
|
||||
onSuccess?: () => Promise<void>;
|
||||
};
|
||||
|
||||
type EmailRole = {
|
||||
@ -44,8 +45,9 @@ const defaultValues: FormValues = {
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, setIsOpen, workspace_slug, user, onSuccess } = props;
|
||||
export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose, workspaceSlug, user, onSuccess } = props;
|
||||
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
@ -61,42 +63,38 @@ const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
onClose();
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reset(defaultValues);
|
||||
clearTimeout(timeout);
|
||||
}, 500);
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: FormValues) => {
|
||||
if (!workspace_slug) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await workspaceService
|
||||
.inviteWorkspace(workspace_slug, payload, user)
|
||||
.inviteWorkspace(workspaceSlug, formData, user)
|
||||
.then(async () => {
|
||||
setIsOpen(false);
|
||||
if (onSuccess) await onSuccess();
|
||||
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Invitations sent successfully.",
|
||||
});
|
||||
onSuccess();
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch((err) =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: `${err.error}`,
|
||||
});
|
||||
console.log(err);
|
||||
message: `${err.error ?? "Something went wrong. Please try again."}`,
|
||||
})
|
||||
.finally(() => {
|
||||
reset(defaultValues);
|
||||
mutate(WORKSPACE_INVITATIONS);
|
||||
});
|
||||
)
|
||||
.finally(() => mutate(WORKSPACE_INVITATIONS));
|
||||
};
|
||||
|
||||
const appendField = () => {
|
||||
@ -104,9 +102,7 @@ const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (fields.length === 0) {
|
||||
append([{ email: "", role: 15 }]);
|
||||
}
|
||||
if (fields.length === 0) append([{ email: "", role: 15 }]);
|
||||
}, [fields, append]);
|
||||
|
||||
return (
|
||||
@ -249,5 +245,3 @@ const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendWorkspaceInvitationModal;
|
||||
|
3
web/components/workspace/settings/index.ts
Normal file
3
web/components/workspace/settings/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./members-list-item";
|
||||
export * from "./members-list";
|
||||
export * from "./workspace-details";
|
202
web/components/workspace/settings/members-list-item.tsx
Normal file
202
web/components/workspace/settings/members-list-item.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
||||
// ui
|
||||
import { CustomSelect, Tooltip } from "@plane/ui";
|
||||
// icons
|
||||
import { ChevronDown, XCircle } from "lucide-react";
|
||||
// constants
|
||||
import { ROLE } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
member: {
|
||||
id: string;
|
||||
memberId: string;
|
||||
avatar: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string | undefined;
|
||||
display_name: string;
|
||||
role: 5 | 10 | 15 | 20;
|
||||
status: boolean;
|
||||
member: boolean;
|
||||
accountCreated: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const WorkspaceMembersListItem: React.FC<Props> = (props) => {
|
||||
const { member } = props;
|
||||
|
||||
const [removeMemberModal, setRemoveMemberModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { workspace: workspaceStore, user: userStore } = useMobxStore();
|
||||
|
||||
const user = userStore.workspaceMemberInfo;
|
||||
const isAdmin = userStore.workspaceMemberInfo?.role === 20;
|
||||
|
||||
const handleRemoveMember = async () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (member.member)
|
||||
await workspaceStore.removeMember(workspaceSlug.toString(), member.id).catch((err) => {
|
||||
const error = err?.error;
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: error || "Something went wrong",
|
||||
});
|
||||
});
|
||||
else
|
||||
await workspaceService
|
||||
.deleteWorkspaceInvitations(workspaceSlug.toString(), member.id)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success",
|
||||
message: "Member removed successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: error || "Something went wrong",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmWorkspaceMemberRemove
|
||||
isOpen={removeMemberModal}
|
||||
onClose={() => setRemoveMemberModal(false)}
|
||||
data={member}
|
||||
onSubmit={handleRemoveMember}
|
||||
/>
|
||||
<div className="group flex items-center justify-between px-3 py-4 hover:bg-custom-background-90">
|
||||
<div className="flex items-center gap-x-4 gap-y-2">
|
||||
{member.avatar && member.avatar !== "" ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded p-4 capitalize text-white">
|
||||
<img
|
||||
src={member.avatar}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
||||
alt={member.display_name || member.email}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded p-4 capitalize bg-gray-700 text-white">
|
||||
{(member.email ?? member.display_name ?? "?")[0]}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<div>
|
||||
{member.member ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="text-sm font-medium">
|
||||
{member.first_name} {member.last_name}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h4 className="text-sm cursor-default">{member.display_name || member.email}</h4>
|
||||
)}
|
||||
<p className="mt-0.5 text-xs text-custom-sidebar-text-300">{member.email ?? member.display_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{!member?.status && (
|
||||
<div className="flex items-center justify-center rounded bg-yellow-500/20 px-2.5 py-1 text-center text-xs text-yellow-500 font-medium">
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
)}
|
||||
{member?.status && !member?.accountCreated && (
|
||||
<div className="flex items-center justify-center rounded bg-blue-500/20 px-2.5 py-1 text-center text-xs text-blue-500 font-medium">
|
||||
<p>Account not created</p>
|
||||
</div>
|
||||
)}
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
|
||||
<span
|
||||
className={`flex items-center text-xs font-medium rounded ${
|
||||
member.memberId !== user.member ? "" : "text-custom-sidebar-text-400"
|
||||
}`}
|
||||
>
|
||||
{ROLE[member.role as keyof typeof ROLE]}
|
||||
</span>
|
||||
{member.memberId !== user.member && (
|
||||
<span className="grid place-items-center">
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
value={member.role}
|
||||
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
workspaceStore
|
||||
.updateMember(workspaceSlug.toString(), member.id, {
|
||||
role: value,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
member.memberId === user.member || !member.status || (user.role !== 20 && user.role < member.role)
|
||||
}
|
||||
placement="bottom-end"
|
||||
>
|
||||
{Object.keys(ROLE).map((key) => {
|
||||
if (user.role !== 20 && user.role < parseInt(key)) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={parseInt(key, 10)}>
|
||||
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
{isAdmin && (
|
||||
<Tooltip tooltipContent={member.memberId === user.member ? "Leave workspace" : "Remove member"}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRemoveMemberModal(true)}
|
||||
className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
75
web/components/workspace/settings/members-list.tsx
Normal file
75
web/components/workspace/settings/members-list.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// components
|
||||
import { WorkspaceMembersListItem } from "components/workspace";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const WorkspaceMembersList: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { workspace: workspaceStore, user: userStore } = useMobxStore();
|
||||
|
||||
const workspaceMembers = workspaceStore.workspaceMembers;
|
||||
const user = userStore.workspaceMemberInfo;
|
||||
|
||||
const { data: workspaceInvitations } = useSWR(
|
||||
workspaceSlug ? `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}` : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceInvitations(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const members = [
|
||||
...(workspaceInvitations?.map((item) => ({
|
||||
id: item.id,
|
||||
memberId: item.id,
|
||||
avatar: "",
|
||||
first_name: item.email,
|
||||
last_name: "",
|
||||
email: item.email,
|
||||
display_name: item.email,
|
||||
role: item.role,
|
||||
status: item.accepted,
|
||||
member: false,
|
||||
accountCreated: item.accepted,
|
||||
})) || []),
|
||||
...(workspaceMembers?.map((item) => ({
|
||||
id: item.id,
|
||||
memberId: item.member?.id,
|
||||
avatar: item.member?.avatar,
|
||||
first_name: item.member?.first_name,
|
||||
last_name: item.member?.last_name,
|
||||
email: item.member?.email,
|
||||
display_name: item.member?.display_name,
|
||||
role: item.role,
|
||||
status: true,
|
||||
member: true,
|
||||
accountCreated: true,
|
||||
})) || []),
|
||||
];
|
||||
|
||||
if (!workspaceMembers || !workspaceInvitations || !user)
|
||||
return (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y-[0.5px] divide-custom-border-200">
|
||||
{members.length > 0
|
||||
? members.map((member) => <WorkspaceMembersListItem key={member.id} member={member} />)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
});
|
306
web/components/workspace/settings/workspace-details.tsx
Normal file
306
web/components/workspace/settings/workspace-details.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { ChevronDown, ChevronUp, Pencil } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { DeleteWorkspaceModal } from "components/workspace";
|
||||
import { ImageUploadModal } from "components/core";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IWorkspace } from "types";
|
||||
// constants
|
||||
import { ORGANIZATION_SIZE } from "constants/workspace";
|
||||
|
||||
const defaultValues: Partial<IWorkspace> = {
|
||||
name: "",
|
||||
url: "",
|
||||
organization_size: "2-10",
|
||||
logo: null,
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const WorkspaceDetails: React.FC = observer(() => {
|
||||
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
const [isImageRemoving, setIsImageRemoving] = useState(false);
|
||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||
|
||||
const { workspace: workspaceStore, user: userStore } = useMobxStore();
|
||||
const activeWorkspace = workspaceStore.currentWorkspace;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IWorkspace>({
|
||||
defaultValues: { ...defaultValues, ...activeWorkspace },
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: IWorkspace) => {
|
||||
if (!activeWorkspace) return;
|
||||
|
||||
const payload: Partial<IWorkspace> = {
|
||||
logo: formData.logo,
|
||||
name: formData.name,
|
||||
organization_size: formData.organization_size,
|
||||
};
|
||||
|
||||
await workspaceStore
|
||||
.updateWorkspace(activeWorkspace.slug, payload)
|
||||
.then(() =>
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Workspace updated successfully",
|
||||
})
|
||||
)
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
const handleDelete = (url: string | null | undefined) => {
|
||||
if (!activeWorkspace || !url) return;
|
||||
|
||||
setIsImageRemoving(true);
|
||||
|
||||
fileService.deleteFile(activeWorkspace.id, url).then(() => {
|
||||
workspaceStore
|
||||
.updateWorkspace(activeWorkspace.slug, { logo: "" })
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Workspace picture removed successfully.",
|
||||
});
|
||||
setIsImageUploadModalOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "There was some error in deleting your profile picture. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsImageRemoving(false));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeWorkspace) reset({ ...activeWorkspace });
|
||||
}, [activeWorkspace, reset]);
|
||||
|
||||
const isAdmin = userStore.workspaceMemberInfo?.role === 20;
|
||||
|
||||
if (!activeWorkspace)
|
||||
return (
|
||||
<div className="grid place-items-center h-full w-full px-4 sm:px-0">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteWorkspaceModal
|
||||
isOpen={deleteWorkspaceModal}
|
||||
onClose={() => setDeleteWorkspaceModal(false)}
|
||||
data={activeWorkspace}
|
||||
/>
|
||||
<ImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isImageRemoving}
|
||||
handleDelete={() => handleDelete(activeWorkspace?.logo)}
|
||||
onSuccess={(imageUrl) => {
|
||||
setIsImageUploading(true);
|
||||
setValue("logo", imageUrl);
|
||||
setIsImageUploadModalOpen(false);
|
||||
handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
|
||||
}}
|
||||
value={watch("logo")}
|
||||
/>
|
||||
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
|
||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||
<div className="relative mx-auto flex h-14 w-14">
|
||||
<img
|
||||
src={watch("logo")!}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
||||
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
|
||||
<span className="text-sm tracking-tight">{`${
|
||||
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}</span>
|
||||
<div className="flex item-center gap-2.5">
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
||||
onClick={() => setIsImageUploadModalOpen(true)}
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||
<>
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit logo
|
||||
</>
|
||||
) : (
|
||||
"Upload logo"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8 my-10">
|
||||
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Workspace Name</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Workspace name should not exceed 80 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Name"
|
||||
className="rounded-md font-medium w-full"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Company Size</h4>
|
||||
<Controller
|
||||
name="organization_size"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"}
|
||||
width="w-full"
|
||||
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
|
||||
input
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{ORGANIZATION_SIZE.map((item) => (
|
||||
<CustomSelect.Option key={item} value={item}>
|
||||
{item}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Workspace URL</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
render={({ field: { onChange, ref } }) => (
|
||||
<Input
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
value={`${
|
||||
typeof window !== "undefined" &&
|
||||
window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.url)}
|
||||
className="w-full"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isAdmin}>
|
||||
{isSubmitting ? "Updating..." : "Update Workspace"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Disclosure as="div" className="border-t border-custom-border-200">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button as="button" type="button" className="flex items-center justify-between w-full py-4">
|
||||
<span className="text-lg tracking-tight">Delete Workspace</span>
|
||||
{/* <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" /> */}
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the workspace delete page is a critical area that requires careful
|
||||
consideration and attention. When deleting a workspace, all of the data and resources within
|
||||
that workspace will be permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setDeleteWorkspaceModal(true)}>
|
||||
Delete my workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
@ -26,6 +26,7 @@ type Props = {
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
// TODO: remove this context
|
||||
export const WorkspaceMemberProvider: React.FC<Props> = (props) => {
|
||||
const { children } = props;
|
||||
|
||||
@ -40,7 +41,7 @@ export const WorkspaceMemberProvider: React.FC<Props> = (props) => {
|
||||
const loading = !memberDetails && !error;
|
||||
|
||||
return (
|
||||
<WorkspaceMemberContext.Provider value={{ loading, memberDetails, error }}>
|
||||
<WorkspaceMemberContext.Provider value={{ loading, memberDetails: undefined, error }}>
|
||||
{children}
|
||||
</WorkspaceMemberContext.Provider>
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ import { WorkspaceService } from "services/workspace.service";
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueFilterOptions,
|
||||
IWorkspaceMember,
|
||||
IWorkspaceMemberMe,
|
||||
IWorkspaceViewProps,
|
||||
Properties,
|
||||
} from "types";
|
||||
@ -66,7 +66,7 @@ const useMyIssuesFilters = (workspaceSlug: string | undefined) => {
|
||||
|
||||
const oldData = { ...myWorkspace };
|
||||
|
||||
mutate<IWorkspaceMember>(
|
||||
mutate<IWorkspaceMemberMe>(
|
||||
WORKSPACE_MEMBERS_ME(workspaceSlug.toString()),
|
||||
(prevData) => {
|
||||
if (!prevData) return;
|
||||
|
@ -12,6 +12,8 @@ export interface IWorkspaceAuthWrapper {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const HIGHER_ROLES = [20, 15];
|
||||
|
||||
export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props) => {
|
||||
const { children } = props;
|
||||
// store
|
||||
@ -22,7 +24,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
||||
// fetching all workspaces
|
||||
useSWR(`USER_WORKSPACES_LIST`, () => workspaceStore.fetchWorkspaces());
|
||||
// fetching user workspace information
|
||||
useSWR(
|
||||
const { data: workspaceMemberInfo } = useSWR(
|
||||
workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => userStore.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null
|
||||
);
|
||||
@ -33,8 +35,12 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
||||
);
|
||||
// fetch workspace members
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString()) : null
|
||||
workspaceSlug && workspaceMemberInfo && HIGHER_ROLES.includes(workspaceMemberInfo.role)
|
||||
? `WORKSPACE_MEMBERS_${workspaceSlug}`
|
||||
: null,
|
||||
workspaceSlug && workspaceMemberInfo && HIGHER_ROLES.includes(workspaceMemberInfo.role)
|
||||
? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString())
|
||||
: null
|
||||
);
|
||||
// fetch workspace labels
|
||||
useSWR(
|
||||
|
@ -1,362 +1,18 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout";
|
||||
// components
|
||||
import { ImageUploadModal } from "components/core";
|
||||
import { DeleteWorkspaceModal } from "components/workspace";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
// ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
||||
// icons
|
||||
import { ChevronDown, ChevronUp, Pencil } from "lucide-react";
|
||||
import { WorkspaceDetails } from "components/workspace";
|
||||
// types
|
||||
import type { IWorkspace } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_DETAILS, USER_WORKSPACES, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { ORGANIZATION_SIZE } from "constants/workspace";
|
||||
|
||||
const defaultValues: Partial<IWorkspace> = {
|
||||
name: "",
|
||||
url: "",
|
||||
organization_size: "2-10",
|
||||
logo: null,
|
||||
};
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
const fileService = new FileService();
|
||||
|
||||
const WorkspaceSettings: NextPage = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
const [isImageRemoving, setIsImageRemoving] = useState(false);
|
||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { data: memberDetails } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
|
||||
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IWorkspace>({
|
||||
defaultValues: { ...defaultValues, ...activeWorkspace },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (activeWorkspace) reset({ ...activeWorkspace });
|
||||
}, [activeWorkspace, reset]);
|
||||
|
||||
const onSubmit = async (formData: IWorkspace) => {
|
||||
if (!activeWorkspace) return;
|
||||
|
||||
const payload: Partial<IWorkspace> = {
|
||||
logo: formData.logo,
|
||||
name: formData.name,
|
||||
organization_size: formData.organization_size,
|
||||
};
|
||||
|
||||
await workspaceService
|
||||
.updateWorkspace(activeWorkspace.slug, payload, user)
|
||||
.then((res) => {
|
||||
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
|
||||
prevData?.map((workspace) => (workspace.id === res.id ? res : workspace))
|
||||
);
|
||||
mutate<IWorkspace>(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
logo: formData.logo,
|
||||
};
|
||||
});
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Workspace updated successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
const handleDelete = (url: string | null | undefined) => {
|
||||
if (!activeWorkspace || !url) return;
|
||||
|
||||
setIsImageRemoving(true);
|
||||
|
||||
fileService.deleteFile(activeWorkspace.id, url).then(() => {
|
||||
workspaceService
|
||||
.updateWorkspace(activeWorkspace.slug, { logo: "" }, user)
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Workspace picture removed successfully.",
|
||||
});
|
||||
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
|
||||
prevData?.map((workspace) => (workspace.id === res.id ? res : workspace))
|
||||
);
|
||||
mutate<IWorkspace>(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
logo: "",
|
||||
};
|
||||
});
|
||||
setIsImageUploadModalOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "There was some error in deleting your profile picture. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsImageRemoving(false));
|
||||
});
|
||||
};
|
||||
|
||||
const isAdmin = memberDetails?.role === 20;
|
||||
|
||||
return (
|
||||
const WorkspaceSettings: NextPage = () => (
|
||||
<AppLayout header={<WorkspaceSettingHeader title="General Settings" />}>
|
||||
<WorkspaceSettingLayout>
|
||||
<ImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isImageRemoving}
|
||||
handleDelete={() => handleDelete(activeWorkspace?.logo)}
|
||||
onSuccess={(imageUrl) => {
|
||||
setIsImageUploading(true);
|
||||
setValue("logo", imageUrl);
|
||||
setIsImageUploadModalOpen(false);
|
||||
handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
|
||||
}}
|
||||
value={watch("logo")}
|
||||
/>
|
||||
<DeleteWorkspaceModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
data={activeWorkspace ?? null}
|
||||
user={user}
|
||||
/>
|
||||
{activeWorkspace ? (
|
||||
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
|
||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||
<div className="relative mx-auto flex h-14 w-14">
|
||||
<img
|
||||
src={watch("logo")!}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
||||
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
|
||||
<span className="text-sm tracking-tight">{`${
|
||||
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}</span>
|
||||
<div className="flex item-center gap-2.5">
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
||||
onClick={() => setIsImageUploadModalOpen(true)}
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||
<>
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit logo
|
||||
</>
|
||||
) : (
|
||||
"Upload logo"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8 my-10">
|
||||
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Workspace Name</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Workspace name should not exceed 80 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Name"
|
||||
className="rounded-md font-medium w-full"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Company Size</h4>
|
||||
<Controller
|
||||
name="organization_size"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"}
|
||||
width="w-full"
|
||||
input
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{ORGANIZATION_SIZE?.map((item) => (
|
||||
<CustomSelect.Option key={item} value={item}>
|
||||
{item}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Workspace URL</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
render={({ field: { onChange, ref } }) => (
|
||||
<Input
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
value={`${
|
||||
typeof window !== "undefined" &&
|
||||
window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.url)}
|
||||
className="w-full"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isAdmin}>
|
||||
{isSubmitting ? "Updating..." : "Update Workspace"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Disclosure as="div" className="border-t border-custom-border-400">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex items-center justify-between w-full py-4"
|
||||
>
|
||||
<span className="text-xl tracking-tight">Delete Workspace</span>
|
||||
{/* <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" /> */}
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the workspace delete page is a critical area that requires careful
|
||||
consideration and attention. When deleting a workspace, all of the data and resources within
|
||||
that workspace will be permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setIsOpen(true)}>
|
||||
Delete my workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full px-4 sm:px-0">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<WorkspaceDetails />
|
||||
</WorkspaceSettingLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceSettings;
|
||||
|
@ -1,309 +1,45 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout";
|
||||
// components
|
||||
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
|
||||
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
|
||||
// ui
|
||||
import { Button, CustomMenu, CustomSelect, Loader } from "@plane/ui";
|
||||
// icons
|
||||
import { ChevronDown, X } from "lucide-react";
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_INVITATION_WITH_EMAIL, WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { ROLE } from "constants/workspace";
|
||||
// helper
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
const MembersSettings: NextPage = () => {
|
||||
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
|
||||
const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState<string | null>(null);
|
||||
const [inviteModal, setInviteModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { isOwner } = useWorkspaceMembers(workspaceSlug?.toString(), Boolean(workspaceSlug));
|
||||
|
||||
const { data: workspaceMembers, mutate: mutateMembers } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembersWithEmail(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const { data: workspaceInvitations, mutate: mutateInvitations } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_INVITATION_WITH_EMAIL(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceInvitationsWithEmail(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const members = [
|
||||
...(workspaceInvitations?.map((item) => ({
|
||||
id: item.id,
|
||||
memberId: item.id,
|
||||
avatar: "",
|
||||
first_name: item.email,
|
||||
last_name: "",
|
||||
email: item.email,
|
||||
display_name: item.email,
|
||||
role: item.role,
|
||||
status: item.accepted,
|
||||
member: false,
|
||||
accountCreated: item?.accepted ? false : true,
|
||||
})) || []),
|
||||
...(workspaceMembers?.map((item) => ({
|
||||
id: item.id,
|
||||
memberId: item.member?.id,
|
||||
avatar: item.member?.avatar,
|
||||
first_name: item.member?.first_name,
|
||||
last_name: item.member?.last_name,
|
||||
email: item.member?.email,
|
||||
display_name: item.member?.display_name,
|
||||
role: item.role,
|
||||
status: true,
|
||||
member: true,
|
||||
accountCreated: true,
|
||||
})) || []),
|
||||
];
|
||||
|
||||
const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id);
|
||||
|
||||
const handleInviteModalSuccess = () => {
|
||||
mutateInvitations();
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout header={<WorkspaceSettingHeader title="Members Settings" />}>
|
||||
<WorkspaceSettingLayout>
|
||||
<ConfirmWorkspaceMemberRemove
|
||||
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
||||
onClose={() => {
|
||||
setSelectedRemoveMember(null);
|
||||
setSelectedInviteRemoveMember(null);
|
||||
}}
|
||||
data={
|
||||
selectedRemoveMember
|
||||
? members.find((item) => item.id === selectedRemoveMember)
|
||||
: selectedInviteRemoveMember
|
||||
? members.find((item) => item.id === selectedInviteRemoveMember)
|
||||
: null
|
||||
}
|
||||
handleDelete={async () => {
|
||||
if (!workspaceSlug) return;
|
||||
if (selectedRemoveMember) {
|
||||
workspaceService
|
||||
.deleteWorkspaceMember(workspaceSlug as string, selectedRemoveMember)
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: error || "Something went wrong",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
mutateMembers((prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember));
|
||||
});
|
||||
}
|
||||
if (selectedInviteRemoveMember) {
|
||||
mutateInvitations(
|
||||
(prevData: any) => prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
|
||||
false
|
||||
);
|
||||
workspaceService
|
||||
.deleteWorkspaceInvitations(workspaceSlug as string, selectedInviteRemoveMember)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success",
|
||||
message: "Member removed successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: error || "Something went wrong",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
mutateInvitations();
|
||||
});
|
||||
}
|
||||
setSelectedRemoveMember(null);
|
||||
setSelectedInviteRemoveMember(null);
|
||||
}}
|
||||
/>
|
||||
{workspaceSlug && (
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
setIsOpen={setInviteModal}
|
||||
workspace_slug={workspaceSlug as string}
|
||||
onClose={() => setInviteModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
user={user}
|
||||
onSuccess={handleInviteModalSuccess}
|
||||
/>
|
||||
)}
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
|
||||
<div className="flex items-center justify-between gap-4 py-3.5 border-b-[0.5px] border-custom-border-200">
|
||||
<h4 className="text-xl font-medium">Members</h4>
|
||||
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
{!workspaceMembers || !workspaceInvitations ? (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{members.length > 0
|
||||
? members.map((member) => (
|
||||
<div key={member.id} className="group flex items-center justify-between px-3.5 py-[18px]">
|
||||
<div className="flex items-center gap-x-8 gap-y-2">
|
||||
{member.avatar && member.avatar !== "" ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
|
||||
<img
|
||||
src={member.avatar}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
|
||||
alt={member.display_name || member.email}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
) : member.display_name || member.email ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||
{(member.display_name || member.email)?.charAt(0)}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||
?
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{member.member ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="text-sm">
|
||||
<span>
|
||||
{member.first_name} {member.last_name}
|
||||
</span>
|
||||
<span className="text-custom-text-300 text-sm ml-2">({member.display_name})</span>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h4 className="text-sm cursor-default">{member.display_name || member.email}</h4>
|
||||
)}
|
||||
{isOwner && <p className="mt-0.5 text-xs text-custom-sidebar-text-300">{member.email}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{!member?.status && (
|
||||
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
)}
|
||||
{member?.status && !member?.accountCreated && (
|
||||
<div className="mr-2 flex items-center justify-center rounded-full bg-blue-500/20 px-2 py-1 text-center text-xs text-blue-500">
|
||||
<p>Account not created</p>
|
||||
</div>
|
||||
)}
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<div className="flex item-center gap-1">
|
||||
<span
|
||||
className={`flex items-center text-sm font-medium ${
|
||||
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
|
||||
}`}
|
||||
>
|
||||
{ROLE[member.role as keyof typeof ROLE]}
|
||||
</span>
|
||||
{member.memberId !== user?.id && <ChevronDown className="h-4 w-4" />}
|
||||
</div>
|
||||
}
|
||||
value={member.role}
|
||||
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutateMembers(
|
||||
(prevData: any) =>
|
||||
prevData?.map((m: any) => (m.id === member.id ? { ...m, role: value } : m)),
|
||||
false
|
||||
);
|
||||
|
||||
workspaceService
|
||||
.updateWorkspaceMember(workspaceSlug?.toString(), member.id, {
|
||||
role: value,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
member.memberId === currentUser?.member.id ||
|
||||
!member.status ||
|
||||
(currentUser && currentUser.role !== 20 && currentUser.role < member.role)
|
||||
}
|
||||
>
|
||||
{Object.keys(ROLE).map((key) => {
|
||||
if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={key}>
|
||||
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (member.member) {
|
||||
setSelectedRemoveMember(member.id);
|
||||
} else {
|
||||
setSelectedInviteRemoveMember(member.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
|
||||
<span> {user?.id === member.memberId ? "Leave" : "Remove member"}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
<WorkspaceMembersList />
|
||||
</section>
|
||||
</WorkspaceSettingLayout>
|
||||
</AppLayout>
|
||||
|
@ -68,7 +68,6 @@ const CreateWorkspace: NextPage = () => {
|
||||
onSubmit={onSubmit}
|
||||
defaultValues={defaultValues}
|
||||
setDefaultValues={setDefaultValues}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import {
|
||||
IWorkspace,
|
||||
IWorkspaceMemberMe,
|
||||
IWorkspaceMember,
|
||||
IWorkspaceMemberInvitation,
|
||||
ILastActiveWorkspaceDetails,
|
||||
@ -139,15 +140,7 @@ export class WorkspaceService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async workspaceMembersWithEmail(workspaceSlug: string): Promise<IWorkspaceMember[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/members/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async workspaceMemberMe(workspaceSlug: string): Promise<IWorkspaceMember> {
|
||||
async workspaceMemberMe(workspaceSlug: string): Promise<IWorkspaceMemberMe> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
@ -191,14 +184,6 @@ export class WorkspaceService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async workspaceInvitationsWithEmail(workspaceSlug: string): Promise<IWorkspaceMemberInvitation[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/invitations/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceInvitation(invitationId: string): Promise<IWorkspaceMemberInvitation> {
|
||||
return this.get(`/api/users/me/invitations/${invitationId}/`, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
|
@ -6,7 +6,7 @@ import { UserService } from "services/user.service";
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// interfaces
|
||||
import { IUser, IUserSettings } from "types/users";
|
||||
import { IWorkspaceMember, IProjectMember } from "types";
|
||||
import { IWorkspaceMemberMe, IProjectMember } from "types";
|
||||
|
||||
export interface IUserStore {
|
||||
loader: boolean;
|
||||
@ -17,7 +17,7 @@ export interface IUserStore {
|
||||
|
||||
dashboardInfo: any;
|
||||
|
||||
workspaceMemberInfo: any;
|
||||
workspaceMemberInfo: IWorkspaceMemberMe | null;
|
||||
hasPermissionToWorkspace: boolean | null;
|
||||
|
||||
projectMemberInfo: IProjectMember | null;
|
||||
@ -27,7 +27,7 @@ export interface IUserStore {
|
||||
fetchCurrentUser: () => Promise<IUser>;
|
||||
fetchCurrentUserSettings: () => Promise<IUserSettings>;
|
||||
|
||||
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMember>;
|
||||
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMemberMe>;
|
||||
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<IProjectMember>;
|
||||
fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise<any>;
|
||||
|
||||
@ -45,7 +45,7 @@ class UserStore implements IUserStore {
|
||||
|
||||
dashboardInfo: any = null;
|
||||
|
||||
workspaceMemberInfo: any = null;
|
||||
workspaceMemberInfo: IWorkspaceMemberMe | null = null;
|
||||
hasPermissionToWorkspace: boolean | null = null;
|
||||
|
||||
projectMemberInfo: IProjectMember | null = null;
|
||||
|
@ -26,6 +26,15 @@ export interface IWorkspaceStore {
|
||||
fetchWorkspaceLabels: (workspaceSlug: string) => Promise<void>;
|
||||
fetchWorkspaceMembers: (workspaceSlug: string) => Promise<void>;
|
||||
|
||||
// workspace write operations
|
||||
createWorkspace: (data: Partial<IWorkspace>) => Promise<IWorkspace>;
|
||||
updateWorkspace: (workspaceSlug: string, data: Partial<IWorkspace>) => Promise<IWorkspace>;
|
||||
deleteWorkspace: (workspaceSlug: string) => Promise<void>;
|
||||
|
||||
// members write operations
|
||||
updateMember: (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => Promise<void>;
|
||||
removeMember: (workspaceSlug: string, memberId: string) => Promise<void>;
|
||||
|
||||
// computed
|
||||
currentWorkspace: IWorkspace | null;
|
||||
workspaceLabels: IIssueLabels[] | null;
|
||||
@ -72,6 +81,15 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
fetchWorkspaceLabels: action,
|
||||
fetchWorkspaceMembers: action,
|
||||
|
||||
// workspace write operations
|
||||
createWorkspace: action,
|
||||
updateWorkspace: action,
|
||||
deleteWorkspace: action,
|
||||
|
||||
// members write operations
|
||||
updateMember: action,
|
||||
removeMember: action,
|
||||
|
||||
// computed
|
||||
currentWorkspace: computed,
|
||||
workspaceLabels: computed,
|
||||
@ -189,7 +207,6 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
* fetch workspace members using workspace slug
|
||||
* @param workspaceSlug
|
||||
*/
|
||||
|
||||
fetchWorkspaceMembers = async (workspaceSlug: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
@ -214,4 +231,174 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* create workspace using the workspace data
|
||||
* @param data
|
||||
*/
|
||||
createWorkspace = async (data: Partial<IWorkspace>) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
const user = this.rootStore.user.currentUser ?? undefined;
|
||||
|
||||
const response = await this.workspaceService.createWorkspace(data, user);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
this.workspaces = [...this.workspaces, response];
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* update workspace using the workspace slug and new workspace data
|
||||
* @param workspaceSlug
|
||||
* @param data
|
||||
*/
|
||||
updateWorkspace = async (workspaceSlug: string, data: Partial<IWorkspace>) => {
|
||||
const newWorkspaces = this.workspaces?.map((w) => (w.slug === workspaceSlug ? { ...w, ...data } : w));
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
const user = this.rootStore.user.currentUser ?? undefined;
|
||||
|
||||
const response = await this.workspaceService.updateWorkspace(workspaceSlug, data, user);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
this.workspaces = newWorkspaces;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* delete workspace using the workspace slug
|
||||
* @param workspaceSlug
|
||||
*/
|
||||
deleteWorkspace = async (workspaceSlug: string) => {
|
||||
const newWorkspaces = this.workspaces?.filter((w) => w.slug !== workspaceSlug);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
const user = this.rootStore.user.currentUser ?? undefined;
|
||||
|
||||
await this.workspaceService.deleteWorkspace(workspaceSlug, user);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
this.workspaces = newWorkspaces;
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* update workspace member using workspace slug and member id and data
|
||||
* @param workspaceSlug
|
||||
* @param memberId
|
||||
* @param data
|
||||
*/
|
||||
updateMember = async (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => {
|
||||
const members = this.members?.[workspaceSlug];
|
||||
members?.map((m) => (m.id === memberId ? { ...m, ...data } : m));
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
this.members = {
|
||||
...this.members,
|
||||
[workspaceSlug]: members,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* remove workspace member using workspace slug and member id
|
||||
* @param workspaceSlug
|
||||
* @param memberId
|
||||
*/
|
||||
removeMember = async (workspaceSlug: string, memberId: string) => {
|
||||
const members = this.members?.[workspaceSlug];
|
||||
members?.filter((m) => m.id !== memberId);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
this.members = {
|
||||
...this.members,
|
||||
[workspaceSlug]: members,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
IWorkspaceMember,
|
||||
IWorkspaceMemberMe,
|
||||
IWorkspaceViewProps,
|
||||
TIssueParams,
|
||||
} from "types";
|
||||
@ -25,7 +25,7 @@ export interface IWorkspaceFilterStore {
|
||||
workspaceDisplayProperties: IIssueDisplayProperties;
|
||||
|
||||
// actions
|
||||
fetchUserWorkspaceFilters: (workspaceSlug: string) => Promise<IWorkspaceMember>;
|
||||
fetchUserWorkspaceFilters: (workspaceSlug: string) => Promise<IWorkspaceMemberMe>;
|
||||
updateWorkspaceFilters: (workspaceSlug: string, filterToUpdate: Partial<IWorkspaceViewProps>) => Promise<void>;
|
||||
|
||||
// computed
|
||||
|
1
web/types/users.d.ts
vendored
1
web/types/users.d.ts
vendored
@ -56,6 +56,7 @@ export interface IUserLite {
|
||||
avatar: string;
|
||||
created_at: Date;
|
||||
display_name: string;
|
||||
email?: string;
|
||||
first_name: string;
|
||||
readonly id: string;
|
||||
is_bot: boolean;
|
||||
|
27
web/types/workspace.d.ts
vendored
27
web/types/workspace.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import type { IProjectMember, IUser, IUserMemberLite, IWorkspaceViewProps } from "types";
|
||||
import type { IProjectMember, IUser, IUserLite, IUserMemberLite, IWorkspaceViewProps } from "types";
|
||||
|
||||
export interface IWorkspace {
|
||||
readonly id: string;
|
||||
@ -56,16 +56,29 @@ export type Properties = {
|
||||
};
|
||||
|
||||
export interface IWorkspaceMember {
|
||||
readonly id: string;
|
||||
workspace: IWorkspace;
|
||||
member: IUserMemberLite;
|
||||
role: 5 | 10 | 15 | 20;
|
||||
company_role: string | null;
|
||||
view_props: IWorkspaceViewProps;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
created_by: string;
|
||||
id: string;
|
||||
member: IUserLite;
|
||||
role: 5 | 10 | 15 | 20;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: IWorkspaceLite;
|
||||
}
|
||||
|
||||
export interface IWorkspaceMemberMe {
|
||||
company_role: string | null;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
default_props: IWorkspaceViewProps;
|
||||
id: string;
|
||||
member: string;
|
||||
role: 5 | 10 | 15 | 20;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
view_props: IWorkspaceViewProps;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export interface ILastActiveWorkspaceDetails {
|
||||
|
Loading…
Reference in New Issue
Block a user