Merge branch 'develop' of github.com:makeplane/plane into feat/issue_relation

This commit is contained in:
NarayanBavisetti 2023-08-28 11:28:49 +05:30
commit d0bd21eb5b
66 changed files with 1998 additions and 819 deletions

View File

@ -1,12 +1,11 @@
name: Build and Push Backend Docker Image name: Build and Push Backend Docker Image
on: on:
push: pull_request:
branches: types: [closed]
- 'develop' branches: [develop]
- 'master' release:
tags: types: [released]
- '*'
jobs: jobs:
build_push_backend: build_push_backend:
@ -17,61 +16,42 @@ jobs:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0 uses: docker/setup-buildx-action@v2.5.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2.1.0 uses: docker/login-action@v2.1.0
with: with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub) - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
id: ghmeta id: meta_pr
if: ${{ github.event_name == 'pull_request' }}
uses: docker/metadata-action@v4.3.0 uses: docker/metadata-action@v4.3.0
with: with:
images: makeplane/plane-backend images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
tags: type=raw,value=develop
- name: Extract metadata (tags, labels) for Docker (Github) - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: dkrmeta id: meta
uses: docker/metadata-action@v4.3.0 uses: docker/metadata-action@v4.3.0
if: ${{ github.event_name == 'release' }}
with: with:
images: ghcr.io/${{ github.repository }}-backend images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
tags: |
- name: Build and Push to GitHub Container Registry type=ref,event=tag
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Hub - name: Build and Push to Docker Hub
uses: docker/build-push-action@v4.0.0 uses: docker/build-push-action@v4.0.0
with: with:
context: ./apiserver context: ./apiserver
file: ./apiserver/Dockerfile.api file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64 platforms: linux/amd64
push: true push: true
cache-from: type=gha tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
cache-to: type=gha env:
tags: ${{ steps.dkrmeta.outputs.tags }} DOCKER_BUILDKIT: 1
labels: ${{ steps.dkrmeta.outputs.labels }} DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}

57
.github/workflows/push-image-deploy.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: Build and Push Plane Deploy Docker Image
on:
pull_request:
types: [closed]
branches: [develop]
release:
types: [released]
jobs:
build_push_plane_deploy:
name: Build and Push Plane Deploy Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
id: meta_pr
if: ${{ github.event_name == 'pull_request' }}
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
tags: type=raw,value=develop
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: meta
uses: docker/metadata-action@v4.3.0
if: ${{ github.event_name == 'release' }}
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
tags: |
type=ref,event=tag
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/space/Dockerfile.space
platforms: linux/amd64
push: true
tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@ -1,77 +1,60 @@
name: Build and Push Frontend Docker Image name: Build and Push Frontend Docker Image
on: on:
push: pull_request:
branches: types: [closed]
- 'develop' branches: [develop]
- 'master' release:
tags: types: [released]
- '*'
jobs: jobs:
build_push_frontend: build_push_frontend:
name: Build Frontend Docker Image name: Build Frontend Docker Image
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0 uses: docker/setup-buildx-action@v2.5.0
- name: Login to Github Container Registry - name: Setup .npmrc for repository
uses: docker/login-action@v2.1.0 run: |
with: echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2.1.0 uses: docker/login-action@v2.1.0
with: with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub) - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: ghmeta
uses: docker/metadata-action@v4.3.0
with:
images: makeplane/plane-frontend
- name: Extract metadata (tags, labels) for Docker (Github)
id: meta id: meta
if: ${{ github.event_name == 'release' }}
uses: docker/metadata-action@v4.3.0 uses: docker/metadata-action@v4.3.0
with: with:
images: ghcr.io/${{ github.repository }}-frontend images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
tags: |
type=ref,event=tag
- name: Build and Push to GitHub Container Registry - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
uses: docker/build-push-action@v4.0.0 id: meta_pr
if: ${{ github.event_name == 'pull_request' }}
uses: docker/metadata-action@v4.3.0
with: with:
context: . images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
file: ./apps/app/Dockerfile.web tags: type=raw,value=develop
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Container Registry - name: Build and Push to Docker Container Registry
uses: docker/build-push-action@v4.0.0 uses: docker/build-push-action@v4.0.0
with: with:
context: . context: .
file: ./apps/app/Dockerfile.web file: ./apps/app/Dockerfile.web
platforms: linux/arm64,linux/amd64 platforms: linux/amd64
tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
push: true push: true
cache-from: type=gha env:
cache-to: type=gha DOCKER_BUILDKIT: 1
tags: ${{ steps.dkrmeta.outputs.tags }} DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
labels: ${{ steps.dkrmeta.outputs.labels }} DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}

56
.github/workflows/push-image-proxy.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Build and Push Proxy Docker Image
on:
pull_request:
types: [closed]
branches: [develop]
release:
types: [released]
jobs:
build_push_proxy:
name: Build and Push Proxy Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
id: meta_pr
if: ${{ github.event_name == 'pull_request' }}
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
tags: type=raw,value=develop
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: meta
uses: docker/metadata-action@v4.3.0
if: ${{ github.event_name == 'release' }}
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
tags: |
type=ref,event=tag
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@ -112,7 +112,7 @@ class ProjectDetailSerializer(BaseSerializer):
class ProjectMemberSerializer(BaseSerializer): class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True) project = ProjectLiteSerializer(read_only=True)
member = UserLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True)

View File

@ -287,7 +287,10 @@ class ProjectViewSet(BaseViewSet):
) )
data = serializer.data data = serializer.data
# Additional fields of the member
data["sort_order"] = project_member.sort_order data["sort_order"] = project_member.sort_order
data["member_role"] = project_member.role
data["is_member"] = True
return Response(data, status=status.HTTP_201_CREATED) return Response(data, status=status.HTTP_201_CREATED)
return Response( return Response(
serializer.errors, serializer.errors,

View File

@ -1 +1 @@
python-3.11.4 python-3.11.5

View File

@ -53,7 +53,11 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
const activityDetails: { const activityDetails: {
[key: string]: { [key: string]: {
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode; message: (
activity: IIssueActivity,
showIssue: boolean,
workspaceSlug: string
) => React.ReactNode;
icon: React.ReactNode; icon: React.ReactNode;
}; };
} = { } = {
@ -173,26 +177,50 @@ const activityDetails: {
icon: <BlockedIcon height="12" width="12" color="#6b7280" />, icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
}, },
cycles: { cycles: {
message: (activity) => { message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created") if (activity.verb === "created")
return ( return (
<> <>
added this issue to the cycle{" "} added this issue to the cycle{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</> </>
); );
else if (activity.verb === "updated") else if (activity.verb === "updated")
return ( return (
<> <>
set the cycle to{" "} set the cycle to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</> </>
); );
else else
return ( return (
<> <>
removed the issue from the cycle{" "} removed the issue from the cycle{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>. <a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.old_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</> </>
); );
}, },
@ -351,26 +379,50 @@ const activityDetails: {
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
}, },
modules: { modules: {
message: (activity) => { message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created") if (activity.verb === "created")
return ( return (
<> <>
added this issue to the module{" "} added this issue to the module{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <a
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</> </>
); );
else if (activity.verb === "updated") else if (activity.verb === "updated")
return ( return (
<> <>
set the module to{" "} set the module to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <a
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</> </>
); );
else else
return ( return (
<> <>
removed the issue from the module{" "} removed the issue from the module{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>. <a
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.old_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</> </>
); );
}, },
@ -538,8 +590,17 @@ export const ActivityMessage = ({
}: { }: {
activity: IIssueActivity; activity: IIssueActivity;
showIssue?: boolean; showIssue?: boolean;
}) => ( }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<> <>
{activityDetails[activity.field as keyof typeof activityDetails]?.message(activity, showIssue)} {activityDetails[activity.field as keyof typeof activityDetails]?.message(
activity,
showIssue,
workspaceSlug?.toString() ?? ""
)}
</> </>
); );
};

View File

@ -11,15 +11,18 @@ import { Dialog, Transition } from "@headlessui/react";
// hooks // hooks
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
// components // components
import { DueDateFilterSelect } from "./due-date-filter-select"; import { DateFilterSelect } from "./date-filter-select";
// ui // ui
import { PrimaryButton, SecondaryButton } from "components/ui"; import { PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { XMarkIcon } from "@heroicons/react/20/solid"; import { XMarkIcon } from "@heroicons/react/20/solid";
// helpers // helpers
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { IIssueFilterOptions } from "types";
type Props = { type Props = {
title: string;
field: keyof IIssueFilterOptions;
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
}; };
@ -36,7 +39,7 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()), date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
}; };
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => { export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => {
const { filters, setFilters } = useIssuesView(); const { filters, setFilters } = useIssuesView();
const router = useRouter(); const router = useRouter();
@ -51,11 +54,11 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
if (filterType === "range") { if (filterType === "range") {
setFilters( setFilters(
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] }, { [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
!Boolean(viewId) !Boolean(viewId)
); );
} else { } else {
const filteredArray = filters?.target_date?.filter((item) => { const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false; if (item?.includes(filterType)) return false;
return true; return true;
@ -64,13 +67,13 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null; const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne) if (filterOne)
setFilters( setFilters(
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] }, { [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
!Boolean(viewId) !Boolean(viewId)
); );
else else
setFilters( setFilters(
{ {
target_date: [`${renderDateFormat(date1)};${filterType}`], [field]: [`${renderDateFormat(date1)};${filterType}`],
}, },
!Boolean(viewId) !Boolean(viewId)
); );
@ -116,7 +119,7 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
control={control} control={control}
name="filterType" name="filterType"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<DueDateFilterSelect value={value} onChange={onChange} /> <DateFilterSelect title={title} value={value} onChange={onChange} />
)} )}
/> />
<XMarkIcon <XMarkIcon

View File

@ -7,6 +7,7 @@ import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "compon
// fetch-keys // fetch-keys
type Props = { type Props = {
title: string;
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
}; };
@ -19,29 +20,31 @@ type DueDate = {
const dueDateRange: DueDate[] = [ const dueDateRange: DueDate[] = [
{ {
name: "Due date before", name: "before",
value: "before", value: "before",
icon: <CalendarBeforeIcon className="h-4 w-4 " />, icon: <CalendarBeforeIcon className="h-4 w-4 " />,
}, },
{ {
name: "Due date after", name: "after",
value: "after", value: "after",
icon: <CalendarAfterIcon className="h-4 w-4 " />, icon: <CalendarAfterIcon className="h-4 w-4 " />,
}, },
{ {
name: "Due date range", name: "range",
value: "range", value: "range",
icon: <CalendarMonthIcon className="h-4 w-4 " />, icon: <CalendarMonthIcon className="h-4 w-4 " />,
}, },
]; ];
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => ( export const DateFilterSelect: React.FC<Props> = ({ title, value, onChange }) => (
<CustomSelect <CustomSelect
value={value} value={value}
label={ label={
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
{dueDateRange.find((item) => item.value === value)?.icon} {dueDateRange.find((item) => item.value === value)?.icon}
<span>{dueDateRange.find((item) => item.value === value)?.name}</span> <span>
{title} {dueDateRange.find((item) => item.value === value)?.name}
</span>
</div> </div>
} }
onChange={onChange} onChange={onChange}
@ -50,7 +53,7 @@ export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
<CustomSelect.Option key={index} value={option.value}> <CustomSelect.Option key={index} value={option.value}>
<> <>
<span>{option.icon}</span> <span>{option.icon}</span>
{option.name} {title} {option.name}
</> </>
</CustomSelect.Option> </CustomSelect.Option>
))} ))}

View File

@ -240,6 +240,34 @@ export const FiltersList: React.FC<Props> = ({
</div> </div>
); );
}) })
: key === "start_date"
? filters.start_date?.map((date: string) => {
if (filters.start_date && filters.start_date.length <= 0) return null;
const splitDate = date.split(";");
return (
<div
key={date}
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
>
<div className="h-1.5 w-1.5 rounded-full" />
<span className="capitalize">
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
start_date: filters.start_date?.filter((d: any) => d !== date),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "target_date" : key === "target_date"
? filters.target_date?.map((date: string) => { ? filters.target_date?.map((date: string) => {
if (filters.target_date && filters.target_date.length <= 0) return null; if (filters.target_date && filters.target_date.length <= 0) return null;

View File

@ -1,4 +1,4 @@
export * from "./due-date-filter-modal"; export * from "./date-filter-modal";
export * from "./due-date-filter-select"; export * from "./date-filter-select";
export * from "./filters-list"; export * from "./filters-list";
export * from "./issues-view-filter"; export * from "./issues-view-filter";

View File

@ -119,14 +119,11 @@ export const IssuesFilterView: React.FC = () => {
onSelect={(option) => { onSelect={(option) => {
const key = option.key as keyof typeof filters; const key = option.key as keyof typeof filters;
if (key === "target_date") { if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements( const valueExists = checkIfArraysHaveSameElements(filters[key] ?? [], option.value);
filters.target_date ?? [],
option.value
);
setFilters({ setFilters({
target_date: valueExists ? null : option.value, [key]: valueExists ? null : option.value,
}); });
} else { } else {
const valueExists = filters[key]?.includes(option.value); const valueExists = filters[key]?.includes(option.value);

View File

@ -12,7 +12,7 @@ import useProjects from "hooks/use-projects";
// component // component
import { Avatar, Icon } from "components/ui"; import { Avatar, Icon } from "components/ui";
// icons // icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons"; import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
@ -56,10 +56,10 @@ export const BoardHeader: React.FC<Props> = ({
); );
const { data: members } = useSWR( const { data: members } = useSWR(
workspaceSlug && projectId && selectedGroup === "created_by" workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
? PROJECT_MEMBERS(projectId.toString()) ? PROJECT_MEMBERS(projectId.toString())
: null, : null,
workspaceSlug && projectId && selectedGroup === "created_by" workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString()) ? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null : null
); );
@ -79,9 +79,11 @@ export const BoardHeader: React.FC<Props> = ({
case "project": case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None"; title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break; break;
case "assignees":
case "created_by": case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member; const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = member?.display_name ?? ""; title = member ? member.display_name : "None";
break; break;
} }
@ -122,9 +124,10 @@ export const BoardHeader: React.FC<Props> = ({
/> />
); );
break; break;
case "assignees":
case "created_by": case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member; const member = members?.find((member) => member.member.id === groupTitle)?.member;
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />; icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
break; break;
} }

View File

@ -478,6 +478,7 @@ export const IssuesView: React.FC<Props> = ({
labels: null, labels: null,
priority: null, priority: null,
state: null, state: null,
start_date: null,
target_date: null, target_date: null,
type: null, type: null,
}) })
@ -513,7 +514,8 @@ export const IssuesView: React.FC<Props> = ({
dragDisabled={ dragDisabled={
selectedGroup === "created_by" || selectedGroup === "created_by" ||
selectedGroup === "labels" || selectedGroup === "labels" ||
selectedGroup === "state_detail.group" selectedGroup === "state_detail.group" ||
selectedGroup === "assignees"
} }
emptyState={{ emptyState={{
title: cycleId title: cycleId
@ -546,7 +548,7 @@ export const IssuesView: React.FC<Props> = ({
}} }}
handleOnDragEnd={handleOnDragEnd} handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction} handleIssueAction={handleIssueAction}
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null} openIssuesListModal={openIssuesListModal ?? null}
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null} removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
trashBox={trashBox} trashBox={trashBox}
setTrashBox={setTrashBox} setTrashBox={setTrashBox}

View File

@ -94,9 +94,10 @@ export const SingleList: React.FC<Props> = ({
case "project": case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None"; title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break; break;
case "assignees":
case "created_by": case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member; const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = member?.display_name ?? ""; title = member ? member.display_name : "None";
break; break;
} }
@ -137,9 +138,10 @@ export const SingleList: React.FC<Props> = ({
/> />
); );
break; break;
case "assignees":
case "created_by": case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member; const member = members?.find((member) => member.member.id === groupTitle)?.member;
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />; icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
break; break;
} }

View File

@ -1,12 +1,12 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// components // components
import { import {
IssuePeekOverview,
ViewAssigneeSelect, ViewAssigneeSelect,
ViewDueDateSelect, ViewDueDateSelect,
ViewEstimateSelect, ViewEstimateSelect,
@ -75,6 +75,10 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
nestingLevel, nestingLevel,
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
// issue peek overview
const [issuePeekOverview, setIssuePeekOverview] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@ -95,7 +99,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
? VIEW_ISSUES(viewId.toString(), params) ? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
if (issue.parent) { if (issue.parent)
mutate<ISubIssueResponse>( mutate<ISubIssueResponse>(
SUB_ISSUES(issue.parent.toString()), SUB_ISSUES(issue.parent.toString()),
(prevData) => { (prevData) => {
@ -116,7 +120,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
}, },
false false
); );
} else { else
mutate<IIssue[]>( mutate<IIssue[]>(
fetchKey, fetchKey,
(prevData) => (prevData) =>
@ -131,7 +135,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
}), }),
false false
); );
}
issuesService issuesService
.patchIssue( .patchIssue(
@ -179,6 +182,16 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return ( return (
<>
<IssuePeekOverview
handleDeleteIssue={() => handleDeleteIssue(issue)}
handleUpdateIssue={async (formData) => partialUpdateIssue(formData, issue)}
issue={issue}
isOpen={issuePeekOverview}
onClose={() => setIssuePeekOverview(false)}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={isNotAllowed}
/>
<div <div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max" className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }} style={{ gridTemplateColumns }}
@ -264,11 +277,13 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
)} )}
</div> </div>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}> <button
<a className="truncate text-custom-text-100 cursor-pointer w-full text-[0.825rem]"> type="button"
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
onClick={() => setIssuePeekOverview(true)}
>
{issue.name} {issue.name}
</a> </button>
</Link>
</div> </div>
{properties.state && ( {properties.state && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200"> <div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
@ -364,5 +379,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
</div> </div>
)} )}
</div> </div>
</>
); );
}; };

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
// components // components
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
import { CustomMenu, Icon, Spinner } from "components/ui"; import { CustomMenu, Spinner } from "components/ui";
// hooks // hooks
import useIssuesProperties from "hooks/use-issue-properties"; import useIssuesProperties from "hooks/use-issue-properties";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";

View File

@ -0,0 +1,106 @@
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// components
import { AddComment, IssueActivitySection } from "components/issues";
// services
import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// types
import { IIssue, IIssueComment } from "types";
// fetch-keys
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
type Props = { issueDetails: IIssue };
export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
const router = useRouter();
const { workspaceSlug, projectId, inboxIssueId } = router.query;
const { setToastAlert } = useToast();
const { user } = useUser();
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
workspaceSlug && projectId && inboxIssueId
? PROJECT_ISSUES_ACTIVITY(inboxIssueId.toString())
: null,
workspaceSlug && projectId && inboxIssueId
? () =>
issuesService.getIssueActivities(
workspaceSlug.toString(),
projectId.toString(),
inboxIssueId.toString()
)
: null
);
const handleCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !projectId || !inboxIssueId) return;
await issuesService
.patchIssueComment(
workspaceSlug as string,
projectId as string,
inboxIssueId as string,
comment.id,
comment,
user
)
.then(() => mutateIssueActivity());
};
const handleCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !projectId || !inboxIssueId) return;
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
await issuesService
.deleteIssueComment(
workspaceSlug as string,
projectId as string,
inboxIssueId as string,
commentId,
user
)
.then(() => mutateIssueActivity());
};
const handleAddComment = async (formData: IIssueComment) => {
if (!workspaceSlug || !issueDetails) return;
await issuesService
.createIssueComment(
workspaceSlug.toString(),
issueDetails.project,
issueDetails.id,
formData,
user
)
.then(() => {
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Comment could not be posted. Please try again.",
})
);
};
return (
<div className="space-y-5">
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
<IssueActivitySection
activity={issueActivity}
handleCommentUpdate={handleCommentUpdate}
handleCommentDelete={handleCommentDelete}
/>
<AddComment onSubmit={handleAddComment} />
</div>
);
};

View File

@ -14,13 +14,8 @@ import inboxServices from "services/inbox.service";
import useInboxView from "hooks/use-inbox-view"; import useInboxView from "hooks/use-inbox-view";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// components // components
import { import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction } from "components/issues";
AddComment, import { InboxIssueActivity } from "components/inbox";
IssueActivitySection,
IssueDescriptionForm,
IssueDetailsSidebar,
IssueReaction,
} from "components/issues";
// ui // ui
import { Loader } from "components/ui"; import { Loader } from "components/ui";
// icons // icons
@ -42,7 +37,6 @@ import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "cons
const defaultValues = { const defaultValues = {
name: "", name: "",
description: "",
description_html: "", description_html: "",
estimate_point: null, estimate_point: null,
assignees_list: [], assignees_list: [],
@ -296,7 +290,6 @@ export const InboxMainContent: React.FC = () => {
workspaceSlug={workspaceSlug as string} workspaceSlug={workspaceSlug as string}
issue={{ issue={{
name: issueDetails.name, name: issueDetails.name,
description: issueDetails.description,
description_html: issueDetails.description_html, description_html: issueDetails.description_html,
}} }}
handleFormSubmit={submitChanges} handleFormSubmit={submitChanges}
@ -312,11 +305,7 @@ export const InboxMainContent: React.FC = () => {
issueId={issueDetails.id} issueId={issueDetails.id}
/> />
<div className="space-y-5"> <InboxIssueActivity issueDetails={issueDetails} />
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
<IssueActivitySection issueId={issueDetails.id} user={user} />
<AddComment issueId={issueDetails.id} user={user} />
</div>
</div> </div>
<div className="basis-1/3 space-y-5 border-custom-border-200 p-5"> <div className="basis-1/3 space-y-5 border-custom-border-200 p-5">

View File

@ -4,6 +4,7 @@ export * from "./delete-issue-modal";
export * from "./filters-dropdown"; export * from "./filters-dropdown";
export * from "./filters-list"; export * from "./filters-list";
export * from "./inbox-action-headers"; export * from "./inbox-action-headers";
export * from "./inbox-issue-activity";
export * from "./inbox-issue-card"; export * from "./inbox-issue-card";
export * from "./inbox-main-content"; export * from "./inbox-main-content";
export * from "./issues-list-sidebar"; export * from "./issues-list-sidebar";

View File

@ -3,10 +3,6 @@ import React from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issues.service";
// components // components
import { ActivityIcon, ActivityMessage } from "components/core"; import { ActivityIcon, ActivityMessage } from "components/core";
import { CommentCard } from "components/issues/comment"; import { CommentCard } from "components/issues/comment";
@ -15,62 +11,23 @@ import { Icon, Loader } from "components/ui";
// helpers // helpers
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
// types // types
import { ICurrentUserResponse, IIssueComment } from "types"; import { IIssueActivity, IIssueComment } from "types";
// fetch-keys
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
type Props = { type Props = {
issueId: string; activity: IIssueActivity[] | undefined;
user: ICurrentUserResponse | undefined; handleCommentUpdate: (comment: IIssueComment) => Promise<void>;
handleCommentDelete: (commentId: string) => Promise<void>;
}; };
export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => { export const IssueActivitySection: React.FC<Props> = ({
activity,
handleCommentUpdate,
handleCommentDelete,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
const { data: issueActivities, mutate: mutateIssueActivities } = useSWR( if (!activity)
workspaceSlug && projectId ? PROJECT_ISSUES_ACTIVITY(issueId) : null,
workspaceSlug && projectId
? () =>
issuesService.getIssueActivities(workspaceSlug as string, projectId as string, issueId)
: null
);
const handleCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !projectId || !issueId) return;
await issuesService
.patchIssueComment(
workspaceSlug as string,
projectId as string,
issueId as string,
comment.id,
comment,
user
)
.then((res) => mutateIssueActivities());
};
const handleCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !projectId || !issueId) return;
mutateIssueActivities(
(prevData: any) => prevData?.filter((p: any) => p.id !== commentId),
false
);
await issuesService
.deleteIssueComment(
workspaceSlug as string,
projectId as string,
issueId as string,
commentId,
user
)
.then(() => mutateIssueActivities());
};
if (!issueActivities) {
return ( return (
<Loader className="space-y-4"> <Loader className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
@ -87,12 +44,11 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
</div> </div>
</Loader> </Loader>
); );
}
return ( return (
<div className="flow-root"> <div className="flow-root">
<ul role="list" className="-mb-4"> <ul role="list" className="-mb-4">
{issueActivities.map((activityItem, index) => { {activity.map((activityItem, index) => {
// determines what type of action is performed // determines what type of action is performed
const message = activityItem.field ? ( const message = activityItem.field ? (
<ActivityMessage activity={activityItem} /> <ActivityMessage activity={activityItem} />
@ -104,7 +60,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
return ( return (
<li key={activityItem.id}> <li key={activityItem.id}>
<div className="relative pb-1"> <div className="relative pb-1">
{issueActivities.length > 1 && index !== issueActivities.length - 1 ? ( {activity.length > 1 && index !== activity.length - 1 ? (
<span <span
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80" className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
aria-hidden="true" aria-hidden="true"

View File

@ -2,20 +2,13 @@ import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form // react-hook-form
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
// services
import issuesServices from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
// ui // ui
import { SecondaryButton } from "components/ui"; import { SecondaryButton } from "components/ui";
// types // types
import type { ICurrentUserResponse, IIssueComment } from "types"; import type { IIssueComment } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap"; import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>( const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
@ -30,63 +23,37 @@ const defaultValues: Partial<IIssueComment> = {
}; };
type Props = { type Props = {
issueId: string;
user: ICurrentUserResponse | undefined;
disabled?: boolean; disabled?: boolean;
onSubmit: (data: IIssueComment) => Promise<void>;
}; };
export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false }) => { export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
const { const {
handleSubmit,
control, control,
formState: { isSubmitting },
handleSubmit,
reset,
setValue, setValue,
watch, watch,
formState: { isSubmitting },
reset,
} = useForm<IIssueComment>({ defaultValues }); } = useForm<IIssueComment>({ defaultValues });
const editorRef = React.useRef<any>(null); const editorRef = React.useRef<any>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
const { setToastAlert } = useToast(); const handleAddComment = async (formData: IIssueComment) => {
if (!formData.comment_html || !formData.comment_json || isSubmitting) return;
const onSubmit = async (formData: IIssueComment) => { await onSubmit(formData).then(() => {
if (
!workspaceSlug ||
!projectId ||
!issueId ||
isSubmitting ||
!formData.comment_html ||
!formData.comment_json
)
return;
await issuesServices
.createIssueComment(
workspaceSlug as string,
projectId as string,
issueId as string,
formData,
user
)
.then(() => {
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
reset(defaultValues); reset(defaultValues);
editorRef.current?.clearEditor(); editorRef.current?.clearEditor();
}) });
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Comment could not be posted. Please try again.",
})
);
}; };
return ( return (
<div> <div>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(handleAddComment)}>
<div className="issue-comments-section"> <div className="issue-comments-section">
<Controller <Controller
name="comment_html" name="comment_html"

View File

@ -4,24 +4,21 @@ import { FC, useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// hooks // hooks
import useReloadConfirmations from "hooks/use-reload-confirmation"; import useReloadConfirmations from "hooks/use-reload-confirmation";
import { useDebouncedCallback } from "use-debounce";
// components // components
import { TextArea } from "components/ui"; import { TextArea } from "components/ui";
import Tiptap from "components/tiptap";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import Tiptap from "components/tiptap";
import { useDebouncedCallback } from "use-debounce";
export interface IssueDescriptionFormValues { export interface IssueDescriptionFormValues {
name: string; name: string;
description: any;
description_html: string; description_html: string;
} }
export interface IssueDetailsProps { export interface IssueDetailsProps {
issue: { issue: {
name: string; name: string;
description: string;
description_html: string; description_html: string;
}; };
workspaceSlug: string; workspaceSlug: string;
@ -43,7 +40,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
const { const {
handleSubmit, handleSubmit,
watch, watch,
setValue,
reset, reset,
register, register,
control, control,
@ -51,7 +47,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
} = useForm<IIssue>({ } = useForm<IIssue>({
defaultValues: { defaultValues: {
name: "", name: "",
description: "",
description_html: "", description_html: "",
}, },
}); });
@ -62,7 +57,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
await handleFormSubmit({ await handleFormSubmit({
name: formData.name ?? "", name: formData.name ?? "",
description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>", description_html: formData.description_html ?? "<p></p>",
}); });
}, },
@ -80,7 +74,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
} }
}, [isSubmitting, setShowAlert]); }, [isSubmitting, setShowAlert]);
// reset form values // reset form values
useEffect(() => { useEffect(() => {
if (!issue) return; if (!issue) return;
@ -99,6 +92,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
return ( return (
<div className="relative"> <div className="relative">
<div className="relative"> <div className="relative">
{isAllowed ? (
<TextArea <TextArea
id="name" id="name"
name="name" name="name"
@ -115,10 +109,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
role="textbox" role="textbox"
disabled={!isAllowed} disabled={!isAllowed}
/> />
{characterLimit && ( ) : (
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
)}
{characterLimit && isAllowed && (
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs"> <div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
<span <span
className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : "" className={`${
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
}`} }`}
> >
{watch("name").length} {watch("name").length}
@ -133,7 +131,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
name="description_html" name="description_html"
control={control} control={control}
render={({ field: { value, onChange } }) => { render={({ field: { value, onChange } }) => {
if (!value && !watch("description_html")) return <></>; if (!value) return <></>;
return ( return (
<Tiptap <Tiptap
@ -141,30 +139,33 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
!value || !value ||
value === "" || value === "" ||
(typeof value === "object" && Object.keys(value).length === 0) (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html") ? "<p></p>"
: value : value
} }
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
debouncedUpdatesEnabled={true} debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert} setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
customClassName="min-h-[150px] shadow-sm" customClassName={
editorContentCustomClassNames="pb-9" isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"
}
noBorder={!isAllowed}
onChange={(description: Object, description_html: string) => { onChange={(description: Object, description_html: string) => {
setShowAlert(true); setShowAlert(true);
setIsSubmitting("submitting"); setIsSubmitting("submitting");
onChange(description_html); onChange(description_html);
setValue("description", description); handleSubmit(handleDescriptionFormSubmit)().finally(() =>
handleSubmit(handleDescriptionFormSubmit)().finally(() => { setIsSubmitting("submitted")
setIsSubmitting("submitted"); );
});
}} }}
editable={isAllowed}
/> />
); );
}} }}
/> />
<div <div
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === "saved" ? "fadeOut" : "fadeIn" className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
}`} }`}
> >
{isSubmitting === "submitting" ? "Saving..." : "Saved"} {isSubmitting === "submitting" ? "Saving..." : "Saved"}

View File

@ -15,3 +15,4 @@ export * from "./sidebar";
export * from "./sub-issues-list"; export * from "./sub-issues-list";
export * from "./label"; export * from "./label";
export * from "./issue-reaction"; export * from "./issue-reaction";
export * from "./peek-overview";

View File

@ -1,12 +1,13 @@
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR, { mutate } from "swr";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// hooks // hooks
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// contexts // contexts
import { useProjectMyMembership } from "contexts/project-member.context"; import { useProjectMyMembership } from "contexts/project-member.context";
// components // components
@ -25,9 +26,9 @@ import { CustomMenu } from "components/ui";
import { LayerDiagonalIcon } from "components/icons"; import { LayerDiagonalIcon } from "components/icons";
import { MinusCircleIcon } from "@heroicons/react/24/outline"; import { MinusCircleIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssue } from "types"; import { IIssue, IIssueComment } from "types";
// fetch-keys // fetch-keys
import { SUB_ISSUES } from "constants/fetch-keys"; import { PROJECT_ISSUES_ACTIVITY, SUB_ISSUES } from "constants/fetch-keys";
type Props = { type Props = {
issueDetails: IIssue; issueDetails: IIssue;
@ -43,6 +44,8 @@ export const IssueMainContent: React.FC<Props> = ({
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query; const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query;
const { setToastAlert } = useToast();
const { user } = useUserAuth(); const { user } = useUserAuth();
const { memberRole } = useProjectMyMembership(); const { memberRole } = useProjectMyMembership();
@ -59,6 +62,72 @@ export const IssueMainContent: React.FC<Props> = ({
); );
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id); const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null,
workspaceSlug && projectId && issueId
? () =>
issuesService.getIssueActivities(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString()
)
: null
);
const handleCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !projectId || !issueId) return;
await issuesService
.patchIssueComment(
workspaceSlug as string,
projectId as string,
issueId as string,
comment.id,
comment,
user
)
.then(() => mutateIssueActivity());
};
const handleCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !projectId || !issueId) return;
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
await issuesService
.deleteIssueComment(
workspaceSlug as string,
projectId as string,
issueId as string,
commentId,
user
)
.then(() => mutateIssueActivity());
};
const handleAddComment = async (formData: IIssueComment) => {
if (!workspaceSlug || !issueDetails) return;
await issuesService
.createIssueComment(
workspaceSlug.toString(),
issueDetails.project,
issueDetails.id,
formData,
user
)
.then(() => {
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Comment could not be posted. Please try again.",
})
);
};
return ( return (
<> <>
<div className="rounded-lg"> <div className="rounded-lg">
@ -97,7 +166,8 @@ export const IssueMainContent: React.FC<Props> = ({
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={issue.id} key={issue.id}
renderAs="a" renderAs="a"
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
issue.id
}`} }`}
className="flex items-center gap-2 py-2" className="flex items-center gap-2 py-2"
> >
@ -146,14 +216,11 @@ export const IssueMainContent: React.FC<Props> = ({
<div className="space-y-5 pt-3"> <div className="space-y-5 pt-3">
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3> <h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
<IssueActivitySection <IssueActivitySection
issueId={(archivedIssueId as string) ?? (issueId as string)} activity={issueActivity}
user={user} handleCommentUpdate={handleCommentUpdate}
/> handleCommentDelete={handleCommentDelete}
<AddComment
issueId={(archivedIssueId as string) ?? (issueId as string)}
user={user}
disabled={uneditable}
/> />
<AddComment onSubmit={handleAddComment} disabled={uneditable} />
</div> </div>
</> </>
); );

View File

@ -7,7 +7,7 @@ import useSWR from "swr";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// components // components
import { DueDateFilterModal } from "components/core"; import { DateFilterModal } from "components/core";
// ui // ui
import { MultiLevelDropdown } from "components/ui"; import { MultiLevelDropdown } from "components/ui";
// icons // icons
@ -20,7 +20,7 @@ import { IIssueFilterOptions, IQuery } from "types";
import { WORKSPACE_LABELS } from "constants/fetch-keys"; import { WORKSPACE_LABELS } from "constants/fetch-keys";
// constants // constants
import { GROUP_CHOICES, PRIORITIES } from "constants/project"; import { GROUP_CHOICES, PRIORITIES } from "constants/project";
import { DUE_DATES } from "constants/due-dates"; import { DATE_FILTER_OPTIONS } from "constants/filters";
type Props = { type Props = {
filters: Partial<IIssueFilterOptions> | IQuery; filters: Partial<IIssueFilterOptions> | IQuery;
@ -35,7 +35,14 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
direction = "right", direction = "right",
height = "md", height = "md",
}) => { }) => {
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false); 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 [fetchLabels, setFetchLabels] = useState(false);
const router = useRouter(); const router = useRouter();
@ -50,10 +57,12 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
return ( return (
<> <>
{isDueDateFilterModalOpen && ( {isDateFilterModalOpen && (
<DueDateFilterModal <DateFilterModal
isOpen={isDueDateFilterModalOpen} title={dateFilterType.title}
handleClose={() => setIsDueDateFilterModalOpen(false)} field={dateFilterType.type}
isOpen={isDateFilterModalOpen}
handleClose={() => setIsDateFilterModalOpen(false)}
/> />
)} )}
<MultiLevelDropdown <MultiLevelDropdown
@ -132,12 +141,48 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
})), })),
}, },
{ {
id: "target_date", id: "start_date",
label: "Due date", label: "Start date",
value: DUE_DATES, value: DATE_FILTER_OPTIONS,
hasChildren: true, hasChildren: true,
children: [ children: [
...(DUE_DATES?.map((option) => ({ ...(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, id: option.name,
label: option.name, label: option.name,
value: { value: {
@ -152,7 +197,13 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
value: "custom", value: "custom",
element: ( element: (
<button <button
onClick={() => setIsDueDateFilterModalOpen(true)} 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" className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
> >
Custom Custom

View File

@ -89,14 +89,11 @@ export const MyIssuesViewOptions: React.FC = () => {
onSelect={(option) => { onSelect={(option) => {
const key = option.key as keyof typeof filters; const key = option.key as keyof typeof filters;
if (key === "target_date") { if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements( const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
filters?.target_date ?? [],
option.value
);
setFilters({ setFilters({
target_date: valueExists ? null : option.value, [key]: valueExists ? null : option.value,
}); });
} else { } else {
const valueExists = filters[key]?.includes(option.value); const valueExists = filters[key]?.includes(option.value);
@ -159,7 +156,11 @@ export const MyIssuesViewOptions: React.FC = () => {
> >
{GROUP_BY_OPTIONS.map((option) => { {GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null; if (issueView === "kanban" && option.key === null) return null;
if (option.key === "state" || option.key === "created_by") if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null; return null;
return ( return (

View File

@ -249,6 +249,7 @@ export const MyIssuesView: React.FC<Props> = ({
labels: null, labels: null,
priority: null, priority: null,
state_group: null, state_group: null,
start_date: null,
target_date: null, target_date: null,
type: null, type: null,
}) })

View File

@ -0,0 +1,79 @@
import {
PeekOverviewHeader,
PeekOverviewIssueActivity,
PeekOverviewIssueDetails,
PeekOverviewIssueProperties,
TPeekOverviewModes,
} from "components/issues";
import { IIssue } from "types";
type Props = {
handleClose: () => void;
handleDeleteIssue: () => void;
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
issue: IIssue;
mode: TPeekOverviewModes;
readOnly: boolean;
setMode: (mode: TPeekOverviewModes) => void;
workspaceSlug: string;
};
export const FullScreenPeekView: React.FC<Props> = ({
handleClose,
handleDeleteIssue,
handleUpdateIssue,
issue,
mode,
readOnly,
setMode,
workspaceSlug,
}) => (
<div className="h-full w-full grid grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
<div className="h-full w-full flex flex-col col-span-7 overflow-hidden">
<div className="w-full p-5">
<PeekOverviewHeader
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
issue={issue}
mode={mode}
setMode={setMode}
workspaceSlug={workspaceSlug}
/>
</div>
<div className="h-full w-full px-6 overflow-y-auto">
{/* issue title and description */}
<div className="w-full">
<PeekOverviewIssueDetails
handleUpdateIssue={handleUpdateIssue}
issue={issue}
readOnly={readOnly}
workspaceSlug={workspaceSlug}
/>
</div>
{/* divider */}
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
{/* issue activity/comments */}
<div className="w-full">
<PeekOverviewIssueActivity
workspaceSlug={workspaceSlug}
issue={issue}
readOnly={readOnly}
/>
</div>
</div>
</div>
<div className="col-span-3 h-full w-full overflow-y-auto">
{/* issue properties */}
<div className="w-full px-6 py-5">
<PeekOverviewIssueProperties
handleDeleteIssue={handleDeleteIssue}
issue={issue}
mode="full"
onChange={handleUpdateIssue}
readOnly={readOnly}
workspaceSlug={workspaceSlug}
/>
</div>
</div>
</div>
);

View File

@ -0,0 +1,133 @@
// hooks
import useToast from "hooks/use-toast";
// ui
import { CustomSelect, Icon } from "components/ui";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { IIssue } from "types";
import { TPeekOverviewModes } from "./layout";
import { ArrowRightAlt, CloseFullscreen, East, OpenInFull } from "@mui/icons-material";
type Props = {
handleClose: () => void;
handleDeleteIssue: () => void;
issue: IIssue;
mode: TPeekOverviewModes;
setMode: (mode: TPeekOverviewModes) => void;
workspaceSlug: string;
};
const peekModes: {
key: TPeekOverviewModes;
icon: string;
label: string;
}[] = [
{ key: "side", icon: "side_navigation", label: "Side Peek" },
{
key: "modal",
icon: "dialogs",
label: "Modal Peek",
},
{
key: "full",
icon: "nearby",
label: "Full Screen Peek",
},
];
export const PeekOverviewHeader: React.FC<Props> = ({
issue,
handleClose,
handleDeleteIssue,
mode,
setMode,
workspaceSlug,
}) => {
const { setToastAlert } = useToast();
const handleCopyLink = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link copied!",
message: "Issue link copied to clipboard",
});
});
};
return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
{mode === "side" && (
<button type="button" onClick={handleClose}>
<East
sx={{
fontSize: "14px",
}}
/>
</button>
)}
{mode === "modal" || mode === "full" ? (
<button type="button" onClick={() => setMode("side")}>
<CloseFullscreen
sx={{
fontSize: "14px",
}}
/>
</button>
) : (
<button type="button" onClick={() => setMode("modal")}>
<OpenInFull
sx={{
fontSize: "14px",
}}
/>
</button>
)}
<CustomSelect
value={mode}
onChange={(val: TPeekOverviewModes) => setMode(val)}
customButton={
<button
type="button"
className={`grid place-items-center ${mode === "full" ? "rotate-45" : ""}`}
>
<Icon iconName={peekModes.find((m) => m.key === mode)?.icon ?? ""} />
</button>
}
position="left"
>
{peekModes.map((mode) => (
<CustomSelect.Option key={mode.key} value={mode.key}>
<div className="flex items-center gap-1.5">
<Icon
iconName={mode.icon}
className={`!text-base flex-shrink-0 -my-1 ${
mode.key === "full" ? "rotate-45" : ""
}`}
/>
{mode.label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
{(mode === "side" || mode === "modal") && (
<div className="flex items-center gap-2">
<button type="button" onClick={handleCopyLink} className="-rotate-45">
<Icon iconName="link" />
</button>
<button type="button" onClick={handleDeleteIssue}>
<Icon iconName="delete" />
</button>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,7 @@
export * from "./full-screen-peek-view";
export * from "./header";
export * from "./issue-activity";
export * from "./issue-details";
export * from "./issue-properties";
export * from "./layout";
export * from "./side-peek-view";

View File

@ -0,0 +1,90 @@
import useSWR, { mutate } from "swr";
// services
import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// components
import { AddComment, IssueActivitySection } from "components/issues";
// types
import { IIssue, IIssueComment } from "types";
// fetch-keys
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
type Props = {
workspaceSlug: string;
issue: IIssue;
readOnly: boolean;
};
export const PeekOverviewIssueActivity: React.FC<Props> = ({ workspaceSlug, issue, readOnly }) => {
const { setToastAlert } = useToast();
const { user } = useUser();
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
workspaceSlug && issue ? PROJECT_ISSUES_ACTIVITY(issue.id) : null,
workspaceSlug && issue
? () => issuesService.getIssueActivities(workspaceSlug.toString(), issue?.project, issue?.id)
: null
);
const handleCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !issue) return;
await issuesService
.patchIssueComment(
workspaceSlug as string,
issue.project,
issue.id,
comment.id,
comment,
user
)
.then(() => mutateIssueActivity());
};
const handleCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !issue) return;
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
await issuesService
.deleteIssueComment(workspaceSlug as string, issue.project, issue.id, commentId, user)
.then(() => mutateIssueActivity());
};
const handleAddComment = async (formData: IIssueComment) => {
if (!workspaceSlug || !issue) return;
await issuesService
.createIssueComment(workspaceSlug.toString(), issue.project, issue.id, formData, user)
.then(() => {
mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Comment could not be posted. Please try again.",
})
);
};
return (
<div>
<h4 className="font-medium">Activity</h4>
<div className="mt-4">
<IssueActivitySection
activity={issueActivity}
handleCommentUpdate={handleCommentUpdate}
handleCommentDelete={handleCommentDelete}
/>
<div className="mt-4">
<AddComment onSubmit={handleAddComment} />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,34 @@
// components
import { IssueDescriptionForm, IssueReaction } from "components/issues";
// types
import { IIssue } from "types";
type Props = {
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
issue: IIssue;
readOnly: boolean;
workspaceSlug: string;
};
export const PeekOverviewIssueDetails: React.FC<Props> = ({
handleUpdateIssue,
issue,
readOnly,
workspaceSlug,
}) => (
<div className="space-y-2">
<h6 className="font-medium text-custom-text-200">
{issue.project_detail.identifier}-{issue.sequence_id}
</h6>
<IssueDescriptionForm
handleFormSubmit={handleUpdateIssue}
isAllowed={!readOnly}
issue={{
name: issue.name,
description_html: issue.description_html,
}}
workspaceSlug={workspaceSlug}
/>
<IssueReaction workspaceSlug={workspaceSlug} issueId={issue.id} projectId={issue.project} />
</div>
);

View File

@ -0,0 +1,203 @@
// headless ui
import { Disclosure } from "@headlessui/react";
import { getStateGroupIcon } from "components/icons";
// components
import {
SidebarAssigneeSelect,
SidebarEstimateSelect,
SidebarPrioritySelect,
SidebarStateSelect,
TPeekOverviewModes,
} from "components/issues";
// icons
import { CustomDatePicker, Icon } from "components/ui";
import { copyTextToClipboard } from "helpers/string.helper";
import useToast from "hooks/use-toast";
// types
import { IIssue } from "types";
type Props = {
handleDeleteIssue: () => void;
issue: IIssue;
mode: TPeekOverviewModes;
onChange: (issueProperty: Partial<IIssue>) => void;
readOnly: boolean;
workspaceSlug: string;
};
export const PeekOverviewIssueProperties: React.FC<Props> = ({
handleDeleteIssue,
issue,
mode,
onChange,
readOnly,
workspaceSlug,
}) => {
const { setToastAlert } = useToast();
const startDate = issue.start_date;
const targetDate = issue.target_date;
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
const handleCopyLink = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link copied!",
message: "Issue link copied to clipboard",
});
});
};
return (
<div className={mode === "full" ? "divide-y divide-custom-border-200" : ""}>
{mode === "full" && (
<div className="flex justify-between gap-2 pb-3">
<h6 className="flex items-center gap-2 font-medium">
{getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)}
{issue.project_detail.identifier}-{issue.sequence_id}
</h6>
<div className="flex items-center gap-2">
<button type="button" onClick={handleCopyLink} className="-rotate-45">
<Icon iconName="link" />
</button>
<button type="button" onClick={handleDeleteIssue}>
<Icon iconName="delete" />
</button>
</div>
</div>
)}
<div className={`space-y-4 ${mode === "full" ? "pt-3" : ""}`}>
<div className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<Icon iconName="radio_button_checked" className="!text-base flex-shrink-0" />
<span className="flex-grow truncate">State</span>
</div>
<div className="w-3/4">
<SidebarStateSelect
value={issue.state}
onChange={(val: string) => onChange({ state: val })}
disabled={readOnly}
/>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<Icon iconName="group" className="!text-base flex-shrink-0" />
<span className="flex-grow truncate">Assignees</span>
</div>
<div className="w-3/4">
<SidebarAssigneeSelect
value={issue.assignees_list}
onChange={(val: string[]) => onChange({ assignees_list: val })}
disabled={readOnly}
/>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<Icon iconName="signal_cellular_alt" className="!text-base flex-shrink-0" />
<span className="flex-grow truncate">Priority</span>
</div>
<div className="w-3/4">
<SidebarPrioritySelect
value={issue.priority}
onChange={(val: string) => onChange({ priority: val })}
disabled={readOnly}
/>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<Icon iconName="calendar_today" className="!text-base flex-shrink-0" />
<span className="flex-grow truncate">Start date</span>
</div>
<div>
{issue.start_date ? (
<CustomDatePicker
placeholder="Start date"
value={issue.start_date}
onChange={(val) =>
onChange({
start_date: val,
})
}
className="bg-custom-background-100"
wrapperClassName="w-full"
maxDate={maxDate ?? undefined}
disabled={readOnly}
/>
) : (
<span className="text-custom-text-200">Empty</span>
)}
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<Icon iconName="calendar_today" className="!text-base flex-shrink-0" />
<span className="flex-grow truncate">Due date</span>
</div>
<div>
{issue.target_date ? (
<CustomDatePicker
placeholder="Due date"
value={issue.target_date}
onChange={(val) =>
onChange({
target_date: val,
})
}
className="bg-custom-background-100"
wrapperClassName="w-full"
minDate={minDate ?? undefined}
disabled={readOnly}
/>
) : (
<span className="text-custom-text-200">Empty</span>
)}
</div>
</div>
{/* <div className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<Icon iconName="change_history" className="!text-base flex-shrink-0" />
<span className="flex-grow truncate">Estimate</span>
</div>
<div className="w-3/4">
<SidebarEstimateSelect
value={issue.estimate_point}
onChange={(val: number | null) => onChange({ estimate_point: val })}
disabled={readOnly}
/>
</div>
</div> */}
{/* <Disclosure as="div">
{({ open }) => (
<>
<Disclosure.Button
as="button"
type="button"
className="flex items-center gap-1 text-sm text-custom-text-200"
>
Show {open ? "Less" : "More"}
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-base" />
</Disclosure.Button>
<Disclosure.Panel as="div" className="mt-4 space-y-4">
Disclosure Panel
</Disclosure.Panel>
</>
)}
</Disclosure> */}
</div>
</div>
);
};

View File

@ -0,0 +1,107 @@
import React, { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
import { FullScreenPeekView, SidePeekView } from "components/issues";
// types
import { IIssue } from "types";
type Props = {
handleDeleteIssue: () => void;
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
issue: IIssue | null;
isOpen: boolean;
onClose: () => void;
workspaceSlug: string;
readOnly: boolean;
};
export type TPeekOverviewModes = "side" | "modal" | "full";
export const IssuePeekOverview: React.FC<Props> = ({
handleDeleteIssue,
handleUpdateIssue,
issue,
isOpen,
onClose,
workspaceSlug,
readOnly,
}) => {
const [peekOverviewMode, setPeekOverviewMode] = useState<TPeekOverviewModes>("side");
const handleClose = () => {
onClose();
setPeekOverviewMode("side");
};
if (!issue || !isOpen) return null;
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
{/* add backdrop conditionally */}
{(peekOverviewMode === "modal" || peekOverviewMode === "full") && (
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
)}
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="relative h-full w-full">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className={`absolute z-20 bg-custom-background-100 ${
peekOverviewMode === "side"
? "top-0 right-0 h-full w-1/2 shadow-custom-shadow-md"
: peekOverviewMode === "modal"
? "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[70%] w-3/5 rounded-lg shadow-custom-shadow-xl"
: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[95%] w-[95%] rounded-lg shadow-custom-shadow-xl"
}`}
>
{(peekOverviewMode === "side" || peekOverviewMode === "modal") && (
<SidePeekView
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={peekOverviewMode}
readOnly={readOnly}
setMode={(mode) => setPeekOverviewMode(mode)}
workspaceSlug={workspaceSlug}
/>
)}
{peekOverviewMode === "full" && (
<FullScreenPeekView
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={peekOverviewMode}
readOnly={readOnly}
setMode={(mode) => setPeekOverviewMode(mode)}
workspaceSlug={workspaceSlug}
/>
)}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -0,0 +1,75 @@
import {
PeekOverviewHeader,
PeekOverviewIssueActivity,
PeekOverviewIssueDetails,
PeekOverviewIssueProperties,
TPeekOverviewModes,
} from "components/issues";
import { IIssue } from "types";
type Props = {
handleClose: () => void;
handleDeleteIssue: () => void;
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
issue: IIssue;
mode: TPeekOverviewModes;
readOnly: boolean;
setMode: (mode: TPeekOverviewModes) => void;
workspaceSlug: string;
};
export const SidePeekView: React.FC<Props> = ({
handleClose,
handleDeleteIssue,
handleUpdateIssue,
issue,
mode,
readOnly,
setMode,
workspaceSlug,
}) => (
<div className="h-full w-full flex flex-col overflow-hidden">
<div className="w-full p-5">
<PeekOverviewHeader
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
issue={issue}
mode={mode}
setMode={setMode}
workspaceSlug={workspaceSlug}
/>
</div>
<div className="h-full w-full px-6 overflow-y-auto">
{/* issue title and description */}
<div className="w-full">
<PeekOverviewIssueDetails
handleUpdateIssue={handleUpdateIssue}
issue={issue}
readOnly={readOnly}
workspaceSlug={workspaceSlug}
/>
</div>
{/* issue properties */}
<div className="w-full mt-10">
<PeekOverviewIssueProperties
handleDeleteIssue={handleDeleteIssue}
issue={issue}
mode={mode}
onChange={handleUpdateIssue}
readOnly={readOnly}
workspaceSlug={workspaceSlug}
/>
</div>
{/* divider */}
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
{/* issue activity/comments */}
<div className="w-full pb-5">
<PeekOverviewIssueActivity
workspaceSlug={workspaceSlug}
issue={issue}
readOnly={readOnly}
/>
</div>
</div>
</div>
);

View File

@ -9,26 +9,16 @@ import projectService from "services/project.service";
// ui // ui
import { CustomSearchSelect } from "components/ui"; import { CustomSearchSelect } from "components/ui";
import { AssigneesList, Avatar } from "components/ui/avatar"; import { AssigneesList, Avatar } from "components/ui/avatar";
// icons
import { UserGroupIcon } from "@heroicons/react/24/outline";
// types
import { UserAuth } from "types";
// fetch-keys // fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
value: string[]; value: string[];
onChange: (val: string[]) => void; onChange: (val: string[]) => void;
userAuth: UserAuth;
disabled?: boolean; disabled?: boolean;
}; };
export const SidebarAssigneeSelect: React.FC<Props> = ({ export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
value,
onChange,
userAuth,
disabled = false,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -50,36 +40,27 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({
), ),
})); }));
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
return ( return (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p>
</div>
<div className="sm:basis-1/2">
<CustomSearchSelect <CustomSearchSelect
value={value} value={value}
label={ customButton={
<div className="flex items-center gap-2 text-custom-text-200"> <>
{value && value.length > 0 && Array.isArray(value) ? ( {value && value.length > 0 && Array.isArray(value) ? (
<div className="-my-0.5 flex items-center justify-center gap-2"> <div className="-my-0.5 flex items-center gap-2">
<AssigneesList userIds={value} length={3} showLength={false} /> <AssigneesList userIds={value} length={3} showLength={false} />
<span className="text-custom-text-100">{value.length} Assignees</span> <span className="text-custom-text-100 text-sm">{value.length} Assignees</span>
</div> </div>
) : ( ) : (
"No assignees" <button type="button" className="bg-custom-background-80 px-2.5 py-0.5 text-sm rounded">
No assignees
</button>
)} )}
</div> </>
} }
options={options} options={options}
onChange={onChange} onChange={onChange}
position="right"
multiple multiple
disabled={isNotAllowed} disabled={disabled}
/> />
</div>
</div>
); );
}; };

View File

@ -6,53 +6,36 @@ import useEstimateOption from "hooks/use-estimate-option";
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// icons // icons
import { PlayIcon } from "@heroicons/react/24/outline"; import { PlayIcon } from "@heroicons/react/24/outline";
// types
import { UserAuth } from "types";
type Props = { type Props = {
value: number | null; value: number | null;
onChange: (val: number | null) => void; onChange: (val: number | null) => void;
userAuth: UserAuth;
disabled?: boolean; disabled?: boolean;
}; };
export const SidebarEstimateSelect: React.FC<Props> = ({ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
value,
onChange,
userAuth,
disabled = false,
}) => {
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
const { isEstimateActive, estimatePoints } = useEstimateOption(); const { isEstimateActive, estimatePoints } = useEstimateOption();
if (!isEstimateActive) return null; if (!isEstimateActive) return null;
return ( return (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
<p>Estimate</p>
</div>
<div className="sm:basis-1/2">
<CustomSelect <CustomSelect
value={value} value={value}
label={ customButton={
<div className="flex items-center gap-2 text-xs"> <button
type="button"
className="flex items-center gap-1.5 !text-sm bg-custom-background-80 rounded px-2.5 py-0.5"
>
<PlayIcon <PlayIcon
className={`h-4 w-4 -rotate-90 ${ className={`h-4 w-4 -rotate-90 ${
value !== null ? "text-custom-text-100" : "text-custom-text-200" value !== null ? "text-custom-text-100" : "text-custom-text-200"
}`} }`}
/> />
{estimatePoints?.find((e) => e.key === value)?.value ?? ( {estimatePoints?.find((e) => e.key === value)?.value ?? "No estimate"}
<span className="text-custom-text-200">No estimates</span> </button>
)}
</div>
} }
onChange={onChange} onChange={onChange}
position="right" disabled={disabled}
width="w-full"
disabled={isNotAllowed || disabled}
> >
<CustomSelect.Option value={null}> <CustomSelect.Option value={null}>
<> <>
@ -74,7 +57,5 @@ export const SidebarEstimateSelect: React.FC<Props> = ({
</CustomSelect.Option> </CustomSelect.Option>
))} ))}
</CustomSelect> </CustomSelect>
</div>
</div>
); );
}; };

View File

@ -3,51 +3,43 @@ import React from "react";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// icons // icons
import { ChartBarIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon } from "components/icons/priority-icon"; import { getPriorityIcon } from "components/icons/priority-icon";
// types
import { UserAuth } from "types";
// constants // constants
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
type Props = { type Props = {
value: string | null; value: string | null;
onChange: (val: string) => void; onChange: (val: string) => void;
userAuth: UserAuth;
disabled?: boolean; disabled?: boolean;
}; };
export const SidebarPrioritySelect: React.FC<Props> = ({ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabled = false }) => (
value,
onChange,
userAuth,
disabled = false,
}) => {
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
return (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
<p>Priority</p>
</div>
<div className="sm:basis-1/2">
<CustomSelect <CustomSelect
label={ customButton={
<div className="flex items-center gap-2 text-left capitalize"> <button
<span className={`${value ? "text-custom-text-100" : "text-custom-text-200"}`}> type="button"
{getPriorityIcon(value ?? "None", "text-sm")} className={`flex items-center gap-1.5 text-left text-sm capitalize rounded px-2.5 py-0.5 ${
value === "urgent"
? "border-red-500/20 bg-red-500/20 text-red-500"
: value === "high"
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
: value === "medium"
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
: value === "low"
? "border-green-500/20 bg-green-500/20 text-green-500"
: "bg-custom-background-80 border-custom-border-200"
}`}
>
<span className="grid place-items-center -my-1">
{getPriorityIcon(value ?? "None", "!text-sm")}
</span> </span>
<span className={`${value ? "text-custom-text-100" : "text-custom-text-200"}`}> <span>{value ?? "None"}</span>
{value ?? "None"} </button>
</span>
</div>
} }
value={value} value={value}
onChange={onChange} onChange={onChange}
width="w-full" optionsClassName="w-min"
position="right" disabled={disabled}
disabled={isNotAllowed}
> >
{PRIORITIES.map((option) => ( {PRIORITIES.map((option) => (
<CustomSelect.Option key={option} value={option} className="capitalize"> <CustomSelect.Option key={option} value={option} className="capitalize">
@ -58,7 +50,4 @@ export const SidebarPrioritySelect: React.FC<Props> = ({
</CustomSelect.Option> </CustomSelect.Option>
))} ))}
</CustomSelect> </CustomSelect>
</div> );
</div>
);
};

View File

@ -9,29 +9,20 @@ import stateService from "services/state.service";
// ui // ui
import { Spinner, CustomSelect } from "components/ui"; import { Spinner, CustomSelect } from "components/ui";
// icons // icons
import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons"; import { getStateGroupIcon } from "components/icons";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { UserAuth } from "types";
// constants // constants
import { STATES_LIST } from "constants/fetch-keys"; import { STATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
value: string; value: string;
onChange: (val: string) => void; onChange: (val: string) => void;
userAuth: UserAuth;
disabled?: boolean; disabled?: boolean;
}; };
export const SidebarStateSelect: React.FC<Props> = ({ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
value,
onChange,
userAuth,
disabled = false,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxIssueId } = router.query; const { workspaceSlug, projectId, inboxIssueId } = router.query;
@ -45,41 +36,35 @@ export const SidebarStateSelect: React.FC<Props> = ({
const selectedState = states?.find((s) => s.id === value); const selectedState = states?.find((s) => s.id === value);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
return ( return (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
<p>State</p>
</div>
<div className="sm:basis-1/2">
<CustomSelect <CustomSelect
label={ customButton={
selectedState ? ( <button type="button" className="bg-custom-background-80 text-sm rounded px-2.5 py-0.5">
<div className="flex items-center gap-2 text-left text-custom-text-100"> {selectedState ? (
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
{getStateGroupIcon( {getStateGroupIcon(
selectedState?.group ?? "backlog", selectedState?.group ?? "backlog",
"16", "14",
"16", "14",
selectedState?.color ?? "" selectedState?.color ?? ""
)} )}
{addSpaceIfCamelCase(selectedState?.name ?? "")} {addSpaceIfCamelCase(selectedState?.name ?? "")}
</div> </div>
) : inboxIssueId ? ( ) : inboxIssueId ? (
<div className="flex items-center gap-2 text-left text-custom-text-100"> <div className="flex items-center gap-1.5 text-left text-custom-text-100">
{getStateGroupIcon("backlog", "16", "16", "#ff7700")} {getStateGroupIcon("backlog", "14", "14", "#ff7700")}
Triage Triage
</div> </div>
) : ( ) : (
"None" "None"
) )}
</button>
} }
value={value} value={value}
onChange={onChange} onChange={onChange}
width="w-full" optionsClassName="w-min"
position="right" position="left"
disabled={isNotAllowed} disabled={disabled}
> >
{states ? ( {states ? (
states.length > 0 ? ( states.length > 0 ? (
@ -98,7 +83,5 @@ export const SidebarStateSelect: React.FC<Props> = ({
<Spinner /> <Spinner />
)} )}
</CustomSelect> </CustomSelect>
</div>
</div>
); );
}; };

View File

@ -33,7 +33,16 @@ import {
// ui // ui
import { CustomDatePicker, Icon } from "components/ui"; import { CustomDatePicker, Icon } from "components/ui";
// icons // icons
import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/react/24/outline"; import {
LinkIcon,
CalendarDaysIcon,
TrashIcon,
PlusIcon,
Squares2X2Icon,
ChartBarIcon,
UserGroupIcon,
PlayIcon,
} from "@heroicons/react/24/outline";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
@ -332,6 +341,12 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
{showFirstSection && ( {showFirstSection && (
<div className="py-1"> <div className="py-1">
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
<p>State</p>
</div>
<div className="sm:basis-1/2">
<Controller <Controller
control={control} control={control}
name="state" name="state"
@ -339,13 +354,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<SidebarStateSelect <SidebarStateSelect
value={value} value={value}
onChange={(val: string) => submitChanges({ state: val })} onChange={(val: string) => submitChanges({ state: val })}
userAuth={memberRole} disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
disabled={uneditable}
/> />
)} )}
/> />
</div>
</div>
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p>
</div>
<div className="sm:basis-1/2">
<Controller <Controller
control={control} control={control}
name="assignees_list" name="assignees_list"
@ -353,13 +375,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<SidebarAssigneeSelect <SidebarAssigneeSelect
value={value} value={value}
onChange={(val: string[]) => submitChanges({ assignees_list: val })} onChange={(val: string[]) => submitChanges({ assignees_list: val })}
userAuth={memberRole} disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
disabled={uneditable}
/> />
)} )}
/> />
</div>
</div>
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
<p>Priority</p>
</div>
<div className="sm:basis-1/2">
<Controller <Controller
control={control} control={control}
name="priority" name="priority"
@ -367,25 +396,35 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<SidebarPrioritySelect <SidebarPrioritySelect
value={value} value={value}
onChange={(val: string) => submitChanges({ priority: val })} onChange={(val: string) => submitChanges({ priority: val })}
userAuth={memberRole} disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
disabled={uneditable}
/> />
)} )}
/> />
</div>
</div>
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
<p>Estimate</p>
</div>
<div className="sm:basis-1/2">
<Controller <Controller
control={control} control={control}
name="estimate_point" name="estimate_point"
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<SidebarEstimateSelect <SidebarEstimateSelect
value={value} value={value}
onChange={(val: number | null) => submitChanges({ estimate_point: val })} onChange={(val: number | null) =>
userAuth={memberRole} submitChanges({ estimate_point: val })
disabled={uneditable} }
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
/> />
)} )}
/> />
</div>
</div>
)} )}
</div> </div>
)} )}

View File

@ -115,14 +115,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
onSelect={(option) => { onSelect={(option) => {
const key = option.key as keyof typeof filters; const key = option.key as keyof typeof filters;
if (key === "target_date") { if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements( const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
filters?.target_date ?? [],
option.value
);
setFilters({ setFilters({
target_date: valueExists ? null : option.value, [key]: valueExists ? null : option.value,
}); });
} else { } else {
const valueExists = filters[key]?.includes(option.value); const valueExists = filters[key]?.includes(option.value);
@ -186,7 +183,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
> >
{GROUP_BY_OPTIONS.map((option) => { {GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null; if (issueView === "kanban" && option.key === null) return null;
if (option.key === "state" || option.key === "created_by") if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null; return null;
return ( return (

View File

@ -263,6 +263,7 @@ export const ProfileIssuesView = () => {
labels: null, labels: null,
priority: null, priority: null,
state_group: null, state_group: null,
start_date: null,
target_date: null, target_date: null,
type: null, type: null,
}) })

View File

@ -42,7 +42,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
user, user,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -81,6 +81,8 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
); );
if (onSuccess) onSuccess(); if (onSuccess) onSuccess();
if (projectId && projectId === data.id) router.push(`/${workspaceSlug}/projects`);
}) })
.catch(() => .catch(() =>
setToastAlert({ setToastAlert({

View File

@ -2,7 +2,7 @@ import React, { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR from "swr";
import { useForm, Controller, useFieldArray } from "react-hook-form"; import { useForm, Controller, useFieldArray } from "react-hook-form";
@ -28,7 +28,7 @@ import useToast from "hooks/use-toast";
// types // types
import { ICurrentUserResponse } from "types"; import { ICurrentUserResponse } from "types";
// fetch-keys // fetch-keys
import { PROJECT_MEMBERS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
@ -37,6 +37,7 @@ type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
members: any[]; members: any[];
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
onSuccess: () => void;
}; };
type member = { type member = {
@ -57,7 +58,9 @@ const defaultValues: FormValues = {
], ],
}; };
const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, members, user }) => { const SendProjectInvitationModal: React.FC<Props> = (props) => {
const { isOpen, setIsOpen, members, user, onSuccess } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -98,13 +101,13 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
type: "success", type: "success",
message: "Member added successfully", message: "Member added successfully",
}); });
onSuccess();
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
}) })
.finally(() => { .finally(() => {
reset(defaultValues); reset(defaultValues);
mutate(PROJECT_MEMBERS(projectId.toString()));
}); });
}; };

View File

@ -73,8 +73,9 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
}, 500); }, 500);
}, 1000); }, 1000);
const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" ${noBorder ? "" : "border border-custom-border-200"} ${
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
} ${customClassName}`; } ${customClassName}`;
if (!editor) return null; if (!editor) return null;

View File

@ -94,6 +94,7 @@ export const ViewForm: React.FC<Props> = ({
labels: null, labels: null,
priority: null, priority: null,
state: null, state: null,
start_date: null,
target_date: null, target_date: null,
type: null, type: null,
}); });
@ -155,14 +156,15 @@ export const ViewForm: React.FC<Props> = ({
onSelect={(option) => { onSelect={(option) => {
const key = option.key as keyof typeof filters; const key = option.key as keyof typeof filters;
if (key === "target_date") { if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements( const valueExists = checkIfArraysHaveSameElements(
filters?.target_date ?? [], filters?.[key] ?? [],
option.value option.value
); );
setValue("query", { setValue("query", {
target_date: valueExists ? null : option.value, ...filters,
[key]: valueExists ? null : option.value,
} as IQuery); } as IQuery);
} else { } else {
if (!filters?.[key]?.includes(option.value)) if (!filters?.[key]?.includes(option.value))

View File

@ -9,7 +9,7 @@ import stateService from "services/state.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// components // components
import { DueDateFilterModal } from "components/core"; import { DateFilterModal } from "components/core";
// ui // ui
import { Avatar, MultiLevelDropdown } from "components/ui"; import { Avatar, MultiLevelDropdown } from "components/ui";
// icons // icons
@ -23,7 +23,7 @@ import { IIssueFilterOptions, IQuery } from "types";
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
// constants // constants
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
import { DUE_DATES } from "constants/due-dates"; import { DATE_FILTER_OPTIONS } from "constants/filters";
type Props = { type Props = {
filters: Partial<IIssueFilterOptions> | IQuery; filters: Partial<IIssueFilterOptions> | IQuery;
@ -38,7 +38,14 @@ export const SelectFilters: React.FC<Props> = ({
direction = "right", direction = "right",
height = "md", height = "md",
}) => { }) => {
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false); const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
const [dateFilterType, setDateFilterType] = useState<{
title: string;
type: "start_date" | "target_date";
}>({
title: "",
type: "start_date",
});
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -67,10 +74,12 @@ export const SelectFilters: React.FC<Props> = ({
return ( return (
<> <>
{isDueDateFilterModalOpen && ( {isDateFilterModalOpen && (
<DueDateFilterModal <DateFilterModal
isOpen={isDueDateFilterModalOpen} title={dateFilterType.title}
handleClose={() => setIsDueDateFilterModalOpen(false)} field={dateFilterType.type}
isOpen={isDateFilterModalOpen}
handleClose={() => setIsDateFilterModalOpen(false)}
/> />
)} )}
<MultiLevelDropdown <MultiLevelDropdown
@ -183,12 +192,48 @@ export const SelectFilters: React.FC<Props> = ({
})), })),
}, },
{ {
id: "target_date", id: "start_date",
label: "Due date", label: "Start date",
value: DUE_DATES, value: DATE_FILTER_OPTIONS,
hasChildren: true, hasChildren: true,
children: [ children: [
...DUE_DATES.map((option) => ({ ...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, id: option.name,
label: option.name, label: option.name,
value: { value: {
@ -203,7 +248,13 @@ export const SelectFilters: React.FC<Props> = ({
value: "custom", value: "custom",
element: ( element: (
<button <button
onClick={() => setIsDueDateFilterModalOpen(true)} 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" className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
> >
Custom Custom

View File

@ -25,6 +25,7 @@ type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
workspace_slug: string; workspace_slug: string;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
onSuccess: () => void;
}; };
type EmailRole = { type EmailRole = {
@ -45,12 +46,8 @@ const defaultValues: FormValues = {
], ],
}; };
const SendWorkspaceInvitationModal: React.FC<Props> = ({ const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
isOpen, const { isOpen, setIsOpen, workspace_slug, user, onSuccess } = props;
setIsOpen,
workspace_slug,
user,
}) => {
const { const {
control, control,
reset, reset,
@ -88,6 +85,7 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
title: "Success!", title: "Success!",
message: "Invitations sent successfully.", message: "Invitations sent successfully.",
}); });
onSuccess();
}) })
.catch((err) => { .catch((err) => {
setToastAlert({ setToastAlert({

View File

@ -8,6 +8,7 @@ const paramsToKey = (params: any) => {
assignees, assignees,
created_by, created_by,
labels, labels,
start_date,
target_date, target_date,
sub_issue, sub_issue,
start_target_date, start_target_date,
@ -19,6 +20,7 @@ const paramsToKey = (params: any) => {
let createdByKey = created_by ? created_by.split(",") : []; let createdByKey = created_by ? created_by.split(",") : [];
let labelsKey = labels ? labels.split(",") : []; let labelsKey = labels ? labels.split(",") : [];
const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE"; const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE";
const startDateKey = start_date ?? "";
const targetDateKey = target_date ?? ""; const targetDateKey = target_date ?? "";
const type = params.type ? params.type.toUpperCase() : "NULL"; const type = params.type ? params.type.toUpperCase() : "NULL";
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL"; const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
@ -31,7 +33,7 @@ const paramsToKey = (params: any) => {
createdByKey = createdByKey.sort().join("_"); createdByKey = createdByKey.sort().join("_");
labelsKey = labelsKey.sort().join("_"); labelsKey = labelsKey.sort().join("_");
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`; return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`;
}; };
const inboxParamsToKey = (params: any) => { const inboxParamsToKey = (params: any) => {
@ -48,7 +50,16 @@ const inboxParamsToKey = (params: any) => {
}; };
const myIssuesParamsToKey = (params: any) => { const myIssuesParamsToKey = (params: any) => {
const { assignees, created_by, labels, priority, state_group, subscriber, target_date } = params; const {
assignees,
created_by,
labels,
priority,
state_group,
subscriber,
start_date,
target_date,
} = params;
let assigneesKey = assignees ? assignees.split(",") : []; let assigneesKey = assignees ? assignees.split(",") : [];
let createdByKey = created_by ? created_by.split(",") : []; let createdByKey = created_by ? created_by.split(",") : [];
@ -56,6 +67,7 @@ const myIssuesParamsToKey = (params: any) => {
let subscriberKey = subscriber ? subscriber.split(",") : []; let subscriberKey = subscriber ? subscriber.split(",") : [];
let priorityKey = priority ? priority.split(",") : []; let priorityKey = priority ? priority.split(",") : [];
let labelsKey = labels ? labels.split(",") : []; let labelsKey = labels ? labels.split(",") : [];
const startDateKey = start_date ?? "";
const targetDateKey = target_date ?? ""; const targetDateKey = target_date ?? "";
const type = params.type ? params.type.toUpperCase() : "NULL"; const type = params.type ? params.type.toUpperCase() : "NULL";
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL"; const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
@ -69,7 +81,7 @@ const myIssuesParamsToKey = (params: any) => {
priorityKey = priorityKey.sort().join("_"); priorityKey = priorityKey.sort().join("_");
labelsKey = labelsKey.sort().join("_"); labelsKey = labelsKey.sort().join("_");
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`; return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}`;
}; };
export const CURRENT_USER = "CURRENT_USER"; export const CURRENT_USER = "CURRENT_USER";

View File

@ -1,7 +1,7 @@
// helper // helper
import { renderDateFormat } from "helpers/date-time.helper"; import { renderDateFormat } from "helpers/date-time.helper";
export const DUE_DATES = [ export const DATE_FILTER_OPTIONS = [
{ {
name: "Last week", name: "Last week",
value: [ value: [

View File

@ -7,6 +7,7 @@ export const GROUP_BY_OPTIONS: Array<{
{ name: "Priority", key: "priority" }, { name: "Priority", key: "priority" },
{ name: "Project", key: "project" }, { name: "Project", key: "project" },
{ name: "Labels", key: "labels" }, { name: "Labels", key: "labels" },
{ name: "Assignees", key: "assignees" },
{ name: "Created by", key: "created_by" }, { name: "Created by", key: "created_by" },
{ name: "None", key: null }, { name: "None", key: null },
]; ];

View File

@ -94,6 +94,7 @@ export const initialState: StateType = {
state_group: null, state_group: null,
subscriber: null, subscriber: null,
created_by: null, created_by: null,
start_date: null,
target_date: null, target_date: null,
}, },
}; };

View File

@ -72,6 +72,7 @@ export const initialState: StateType = {
state_group: null, state_group: null,
subscriber: null, subscriber: null,
created_by: null, created_by: null,
start_date: null,
target_date: null, target_date: null,
}, },
properties: { properties: {

View File

@ -26,6 +26,7 @@ const initialValues: IWorkspaceViewProps = {
priority: null, priority: null,
state_group: null, state_group: null,
subscriber: null, subscriber: null,
start_date: null,
target_date: null, target_date: null,
type: null, type: null,
}, },

View File

@ -27,6 +27,7 @@ const useMyIssues = (workspaceSlug: string | undefined) => {
priority: filters?.priority ? filters?.priority.join(",") : undefined, priority: filters?.priority ? filters?.priority.join(",") : undefined,
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined, state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined, subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
type: filters?.type ? filters?.type : undefined, type: filters?.type ? filters?.type : undefined,
}; };

View File

@ -58,6 +58,7 @@ const useIssuesView = () => {
type: filters?.type ? filters?.type : undefined, type: filters?.type ? filters?.type : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
sub_issue: showSubIssues, sub_issue: showSubIssues,
}; };

View File

@ -44,6 +44,7 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
order_by: orderBy, order_by: orderBy,
priority: filters?.priority ? filters?.priority.join(",") : undefined, priority: filters?.priority ? filters?.priority.join(",") : undefined,
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined, state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
type: filters?.type ? filters?.type : undefined, type: filters?.type ? filters?.type : undefined,
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined, subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,

View File

@ -183,9 +183,6 @@ const GeneralSettings: NextPage = () => {
data={projectDetails ?? null} data={projectDetails ?? null}
isOpen={Boolean(selectProject)} isOpen={Boolean(selectProject)}
onClose={() => setSelectedProject(null)} onClose={() => setSelectedProject(null)}
onSuccess={() => {
router.push(`/${workspaceSlug}/projects`);
}}
user={user} user={user}
/> />
<form onSubmit={handleSubmit(onSubmit)} className="p-8"> <form onSubmit={handleSubmit(onSubmit)} className="p-8">

View File

@ -108,6 +108,8 @@ const MembersSettings: NextPage = () => {
const currentUser = projectMembers?.find((item) => item.member.id === user?.id); const currentUser = projectMembers?.find((item) => item.member.id === user?.id);
const handleProjectInvitationSuccess = () => {};
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -167,6 +169,7 @@ const MembersSettings: NextPage = () => {
setIsOpen={setInviteModal} setIsOpen={setInviteModal}
members={members} members={members}
user={user} user={user}
onSuccess={() => mutateMembers()}
/> />
<div className="p-8"> <div className="p-8">
<SettingsHeader /> <SettingsHeader />

View File

@ -69,19 +69,6 @@ const MembersSettings: NextPage = () => {
); );
const members = [ const members = [
...(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,
})) || []),
...(workspaceInvitations?.map((item) => ({ ...(workspaceInvitations?.map((item) => ({
id: item.id, id: item.id,
memberId: item.id, memberId: item.id,
@ -95,10 +82,27 @@ const MembersSettings: NextPage = () => {
member: false, member: false,
accountCreated: item?.accepted ? false : true, 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 currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id);
const handleInviteModalSuccess = () => {
mutateInvitations();
};
return ( return (
<WorkspaceAuthorizationLayout <WorkspaceAuthorizationLayout
breadcrumbs={ breadcrumbs={
@ -139,7 +143,8 @@ const MembersSettings: NextPage = () => {
}); });
}) })
.finally(() => { .finally(() => {
mutateMembers((prevData: any) => mutateMembers(
(prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedRemoveMember) prevData?.filter((item: any) => item.id !== selectedRemoveMember)
); );
}); });
@ -180,6 +185,7 @@ const MembersSettings: NextPage = () => {
setIsOpen={setInviteModal} setIsOpen={setInviteModal}
workspace_slug={workspaceSlug as string} workspace_slug={workspaceSlug as string}
user={user} user={user}
onSuccess={handleInviteModalSuccess}
/> />
<div className="p-8"> <div className="p-8">
<SettingsHeader /> <SettingsHeader />

View File

@ -58,6 +58,25 @@
--color-border-300: 212, 212, 212; /* strong border- 1 */ --color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */ --color-border-400: 185, 185, 185; /* strong border- 2 */
--color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06),
0px 1px 2px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.14);
--color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
--color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02),
0px 1px 12px 0px rgba(0, 0, 0, 0.12);
--color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
--color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
--color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
--color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
--color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
0px 12px 24px 0px rgba(16, 24, 40, 0.12), 0px 1px 32px 0px rgba(16, 24, 40, 0.12);
--color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */ --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */ --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */ --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
@ -71,6 +90,16 @@
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */ --color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */ --color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */ --color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
--color-sidebar-shadow-2xs: var(--color-shadow-2xs);
--color-sidebar-shadow-xs: var(--color-shadow-xs);
--color-sidebar-shadow-sm: var(--color-shadow-sm);
--color-sidebar-shadow-rg: var(--color-shadow-rg);
--color-sidebar-shadow-md: var(--color-shadow-md);
--color-sidebar-shadow-lg: var(--color-shadow-lg);
--color-sidebar-shadow-xl: var(--color-shadow-xl);
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
} }
[data-theme="light"], [data-theme="light"],
@ -113,6 +142,16 @@
--color-background-100: 7, 7, 7; /* primary bg */ --color-background-100: 7, 7, 7; /* primary bg */
--color-background-90: 11, 11, 11; /* secondary bg */ --color-background-90: 11, 11, 11; /* secondary bg */
--color-background-80: 23, 23, 23; /* tertiary bg */ --color-background-80: 23, 23, 23; /* tertiary bg */
--color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
--color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
--color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
--color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
--color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
--color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
} }
[data-theme="dark"] { [data-theme="dark"] {

View File

@ -5,6 +5,26 @@ module.exports = {
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"], content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
theme: { theme: {
extend: { extend: {
boxShadow: {
"custom-shadow-2xs": "var(--color-shadow-2xs)",
"custom-shadow-xs": "var(--color-shadow-xs)",
"custom-shadow-sm": "var(--color-shadow-sm)",
"custom-shadow-rg": "var(--color-shadow-rg)",
"custom-shadow-md": "var(--color-shadow-md)",
"custom-shadow-lg": "var(--color-shadow-lg)",
"custom-shadow-xl": "var(--color-shadow-xl)",
"custom-shadow-2xl": "var(--color-shadow-2xl)",
"custom-shadow-3xl": "var(--color-shadow-3xl)",
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
},
colors: { colors: {
custom: { custom: {
primary: { primary: {
@ -182,8 +202,5 @@ module.exports = {
custom: ["Inter", "sans-serif"], custom: ["Inter", "sans-serif"],
}, },
}, },
plugins: [ plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
require("tailwindcss-animate"),
require("@tailwindcss/typography")
],
}; };

View File

@ -215,6 +215,7 @@ export interface IIssueLite {
export interface IIssueFilterOptions { export interface IIssueFilterOptions {
type: "active" | "backlog" | null; type: "active" | "backlog" | null;
assignees: string[] | null; assignees: string[] | null;
start_date: string[] | null;
target_date: string[] | null; target_date: string[] | null;
state: string[] | null; state: string[] | null;
state_group: TStateGroups[] | null; state_group: TStateGroups[] | null;
@ -233,6 +234,7 @@ export type TIssueGroupByOptions =
| "created_by" | "created_by"
| "state_detail.group" | "state_detail.group"
| "project" | "project"
| "assignees"
| null; | null;
export type TIssueOrderByOptions = export type TIssueOrderByOptions =

View File

@ -20,6 +20,7 @@ export interface IQuery {
labels: string[] | null; labels: string[] | null;
priority: string[] | null; priority: string[] | null;
state: string[] | null; state: string[] | null;
start_date: string[] | null;
target_date: string[] | null; target_date: string[] | null;
type: "active" | "backlog" | null; type: "active" | "backlog" | null;
} }