mirror of
https://github.com/makeplane/plane.git
synced 2026-02-25 04:35:21 +01:00
Merge branch 'preview' of github.com:makeplane/plane into feat-breadcrumbs-revamp
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
@@ -198,6 +196,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||
|
||||
class IssueRecentVisitSerializer(serializers.ModelSerializer):
|
||||
project_identifier = serializers.SerializerMethodField()
|
||||
assignees = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -215,9 +214,15 @@ class IssueRecentVisitSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_project_identifier(self, obj):
|
||||
project = obj.project
|
||||
|
||||
return project.identifier if project else None
|
||||
|
||||
def get_assignees(self, obj):
|
||||
return list(
|
||||
obj.assignees.filter(issue_assignee__deleted_at__isnull=True).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ProjectRecentVisitSerializer(serializers.ModelSerializer):
|
||||
project_members = serializers.SerializerMethodField()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
Project,
|
||||
Issue,
|
||||
User,
|
||||
IssueAssignee,
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.app.serializers.workspace import IssueRecentVisitSerializer
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIssueRecentVisitSerializer:
|
||||
"""Test the IssueRecentVisitSerializer"""
|
||||
|
||||
def test_issue_recent_visit_serializer_fields(self, db):
|
||||
"""Test that the serializer includes the correct fields"""
|
||||
|
||||
test_user_1 = User.objects.create(
|
||||
email="test_user_1@example.com", first_name="Test", last_name="User"
|
||||
)
|
||||
|
||||
# To test for deleted issue assignee
|
||||
test_user_2 = User.objects.create(
|
||||
email="test_user_2@example.com",
|
||||
first_name="Other",
|
||||
last_name="User",
|
||||
username="some user name",
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.create(
|
||||
name="Test Workspace", slug="test-workspace", owner=test_user_1
|
||||
)
|
||||
|
||||
WorkspaceMember.objects.create(member=test_user_2, role=15, workspace=workspace)
|
||||
|
||||
project = Project.objects.create(
|
||||
name="Test Project", identifier="test-project", workspace=workspace
|
||||
)
|
||||
ProjectMember.objects.create(project=project, member=test_user_2)
|
||||
|
||||
issue = Issue.objects.create(
|
||||
name="Test Issue",
|
||||
workspace=workspace,
|
||||
project=project,
|
||||
)
|
||||
|
||||
IssueAssignee.objects.create(issue=issue, assignee=test_user_1, project=project)
|
||||
|
||||
# Deleted issue assignee
|
||||
IssueAssignee.objects.create(
|
||||
issue=issue,
|
||||
assignee=test_user_2,
|
||||
project=project,
|
||||
deleted_at=timezone.now(),
|
||||
)
|
||||
|
||||
serialized_data = IssueRecentVisitSerializer(
|
||||
issue,
|
||||
).data
|
||||
|
||||
# Check fields are present and correct
|
||||
assert "name" in serialized_data
|
||||
assert "assignees" in serialized_data
|
||||
assert "project_identifier" in serialized_data
|
||||
|
||||
assert serialized_data["name"] == "Test Issue"
|
||||
assert serialized_data["project_identifier"] == "TEST-PROJECT"
|
||||
|
||||
# Only including non-deleted issue assignees
|
||||
assert serialized_data["assignees"] == [test_user_1.id]
|
||||
@@ -5,7 +5,6 @@ export * from "./endpoints";
|
||||
export * from "./file";
|
||||
export * from "./filter";
|
||||
export * from "./graph";
|
||||
export * from "./icons";
|
||||
export * from "./instance";
|
||||
export * from "./issue";
|
||||
export * from "./metadata";
|
||||
|
||||
5
packages/types/src/issues.d.ts
vendored
5
packages/types/src/issues.d.ts
vendored
@@ -220,6 +220,11 @@ export type GroupByColumnTypes =
|
||||
| "created_by"
|
||||
| "team_project";
|
||||
|
||||
type TGetColumns = {
|
||||
isWorkspaceLevel?: boolean;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export interface IGroupByColumn {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -152,6 +152,8 @@ import {
|
||||
CircleChevronDown,
|
||||
UsersRound,
|
||||
ToggleLeft,
|
||||
Search,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
export const MATERIAL_ICONS_LIST = [
|
||||
@@ -912,6 +914,8 @@ export const LUCIDE_ICONS_LIST = [
|
||||
{ name: "Minus", element: Minus },
|
||||
{ name: "MinusCircle", element: MinusCircle },
|
||||
{ name: "MinusSquare", element: MinusSquare },
|
||||
{ name: "Search", element: Search },
|
||||
{ name: "ToggleLeft", element: ToggleLeft },
|
||||
{ name: "User", element: User },
|
||||
{ name: "UsersRound", element: UsersRound },
|
||||
];
|
||||
1
packages/ui/src/constants/index.ts
Normal file
1
packages/ui/src/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./icons";
|
||||
@@ -2,7 +2,7 @@ import { Search } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// icons
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
import { MATERIAL_ICONS_LIST } from "@plane/constants";
|
||||
import { MATERIAL_ICONS_LIST } from "..";
|
||||
import { cn } from "../../helpers";
|
||||
import { Input } from "../form-fields";
|
||||
import { InfoIcon } from "../icons";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Emoji } from "emoji-picker-react";
|
||||
import React, { FC } from "react";
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
// plane imports
|
||||
import { LUCIDE_ICONS_LIST } from "@plane/constants";
|
||||
// local imports
|
||||
import { LUCIDE_ICONS_LIST } from "..";
|
||||
import { emojiCodeToUnicode } from "./helpers";
|
||||
|
||||
export type TEmojiLogoProps = {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Search } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// plane imports
|
||||
import { LUCIDE_ICONS_LIST } from "@plane/constants";
|
||||
// local imports
|
||||
import { LUCIDE_ICONS_LIST } from "..";
|
||||
import { cn } from "../../helpers";
|
||||
import { Input } from "../form-fields";
|
||||
import { InfoIcon } from "../icons";
|
||||
|
||||
@@ -2,34 +2,36 @@ export * from "./avatar";
|
||||
export * from "./badge";
|
||||
export * from "./breadcrumbs";
|
||||
export * from "./button";
|
||||
export * from "./calendar";
|
||||
export * from "./card";
|
||||
export * from "./collapsible";
|
||||
export * from "./color-picker";
|
||||
export * from "./constants";
|
||||
export * from "./content-wrapper";
|
||||
export * from "./control-link";
|
||||
export * from "./drag-handle";
|
||||
export * from "./drop-indicator";
|
||||
export * from "./dropdown";
|
||||
export * from "./dropdowns";
|
||||
export * from "./emoji";
|
||||
export * from "./favorite-star";
|
||||
export * from "./form-fields";
|
||||
export * from "./header";
|
||||
export * from "./hooks";
|
||||
export * from "./icons";
|
||||
export * from "./link";
|
||||
export * from "./loader";
|
||||
export * from "./modals";
|
||||
export * from "./popovers";
|
||||
export * from "./progress";
|
||||
export * from "./row";
|
||||
export * from "./scroll-area";
|
||||
export * from "./sortable";
|
||||
export * from "./spinners";
|
||||
export * from "./tables";
|
||||
export * from "./tabs";
|
||||
export * from "./tag";
|
||||
export * from "./toast";
|
||||
export * from "./tooltip";
|
||||
export * from "./typography";
|
||||
export * from "./drag-handle";
|
||||
export * from "./drop-indicator";
|
||||
export * from "./favorite-star";
|
||||
export * from "./loader";
|
||||
export * from "./collapsible";
|
||||
export * from "./popovers";
|
||||
export * from "./tables";
|
||||
export * from "./header";
|
||||
export * from "./row";
|
||||
export * from "./scroll-area";
|
||||
export * from "./content-wrapper";
|
||||
export * from "./card";
|
||||
export * from "./tag";
|
||||
export * from "./tabs";
|
||||
export * from "./calendar";
|
||||
export * from "./color-picker";
|
||||
export * from "./link";
|
||||
export * from "./utils";
|
||||
|
||||
7
packages/ui/src/utils/icons.ts
Normal file
7
packages/ui/src/utils/icons.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LUCIDE_ICONS_LIST } from "..";
|
||||
|
||||
/**
|
||||
* Returns a random icon name from the LUCIDE_ICONS_LIST array
|
||||
*/
|
||||
export const getRandomIconName = (): string =>
|
||||
LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name;
|
||||
1
packages/ui/src/utils/index.ts
Normal file
1
packages/ui/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./icons";
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
// plane imports
|
||||
import { LUCIDE_ICONS_LIST, RANDOM_EMOJI_CODES } from "@plane/constants";
|
||||
import { RANDOM_EMOJI_CODES } from "@plane/constants";
|
||||
|
||||
/**
|
||||
* Converts a hyphen-separated hexadecimal emoji code to its decimal representation
|
||||
@@ -83,9 +83,3 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]:
|
||||
* @returns {string} A random emoji code
|
||||
*/
|
||||
export const getRandomEmoji = (): string => RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)];
|
||||
|
||||
/**
|
||||
* Returns a random icon name from the LUCIDE_ICONS_LIST array
|
||||
*/
|
||||
export const getRandomIconName = (): string =>
|
||||
LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
// types
|
||||
import { IGroupByColumn, IIssueDisplayProperties, TSpreadsheetColumn } from "@plane/types";
|
||||
import { IGroupByColumn, IIssueDisplayProperties, TGetColumns, TSpreadsheetColumn } from "@plane/types";
|
||||
import { DiceIcon, DoubleCircleIcon, ISvgIcons } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
@@ -32,6 +32,36 @@ import {
|
||||
SpreadsheetSubIssueColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "@/components/issues/issue-layouts/spreadsheet";
|
||||
// store
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export type TGetScopeMemberIdsResult = {
|
||||
memberIds: string[];
|
||||
includeNone: boolean;
|
||||
};
|
||||
|
||||
export const getScopeMemberIds = ({ isWorkspaceLevel, projectId }: TGetColumns): TGetScopeMemberIdsResult => {
|
||||
// store values
|
||||
const { workspaceMemberIds } = store.memberRoot.workspace;
|
||||
const { projectMemberIds } = store.memberRoot.project;
|
||||
// derived values
|
||||
const memberIds = workspaceMemberIds;
|
||||
|
||||
if (isWorkspaceLevel) {
|
||||
return { memberIds: memberIds ?? [], includeNone: true };
|
||||
}
|
||||
|
||||
if (projectId || (projectMemberIds && projectMemberIds.length > 0)) {
|
||||
const { getProjectMemberIds } = store.memberRoot.project;
|
||||
const _projectMemberIds = projectId ? getProjectMemberIds(projectId, false) : projectMemberIds;
|
||||
return {
|
||||
memberIds: _projectMemberIds ?? [],
|
||||
includeNone: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { memberIds: [], includeNone: true };
|
||||
};
|
||||
|
||||
export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Emoji } from "emoji-picker-react";
|
||||
// eslint-disable-next-line import/order
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
// plane imports
|
||||
import { LUCIDE_ICONS_LIST } from "@plane/constants";
|
||||
import { TLogoProps } from "@plane/types";
|
||||
import { LUCIDE_ICONS_LIST } from "@plane/ui";
|
||||
import { emojiCodeToUnicode } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { CSSProperties, FC } from "react";
|
||||
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import { isEmpty } from "lodash";
|
||||
import clone from "lodash/clone";
|
||||
import concat from "lodash/concat";
|
||||
import isEqual from "lodash/isEqual";
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
TGroupedIssues,
|
||||
IWorkspaceView,
|
||||
IIssueDisplayFilterOptions,
|
||||
TGetColumns,
|
||||
} from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, CycleGroupIcon, DiceIcon, ISvgIcons, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
@@ -37,7 +37,11 @@ import { Logo } from "@/components/common";
|
||||
// store
|
||||
import { store } from "@/lib/store-context";
|
||||
// plane web store
|
||||
import { getTeamProjectColumns, SpreadSheetPropertyIconMap } from "@/plane-web/components/issues/issue-layouts/utils";
|
||||
import {
|
||||
getScopeMemberIds,
|
||||
getTeamProjectColumns,
|
||||
SpreadSheetPropertyIconMap,
|
||||
} from "@/plane-web/components/issues/issue-layouts/utils";
|
||||
// store
|
||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store";
|
||||
import { DEFAULT_DISPLAY_PROPERTIES } from "@/store/issue/issue-details/sub_issues_filter.store";
|
||||
@@ -60,13 +64,16 @@ export type IssueUpdates = {
|
||||
};
|
||||
};
|
||||
|
||||
type TGetColumns = {
|
||||
isWorkspaceLevel?: boolean;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export const isWorkspaceLevel = (type: EIssuesStoreType) =>
|
||||
[EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false;
|
||||
[
|
||||
EIssuesStoreType.PROFILE,
|
||||
EIssuesStoreType.GLOBAL,
|
||||
EIssuesStoreType.TEAM,
|
||||
EIssuesStoreType.TEAM_VIEW,
|
||||
EIssuesStoreType.PROJECT_VIEW,
|
||||
].includes(type)
|
||||
? true
|
||||
: false;
|
||||
|
||||
type TGetGroupByColumns = {
|
||||
groupBy: GroupByColumnTypes | null;
|
||||
@@ -264,40 +271,28 @@ const getLabelsColumns = ({ isWorkspaceLevel }: TGetColumns): IGroupByColumn[] =
|
||||
};
|
||||
|
||||
const getAssigneeColumns = ({ isWorkspaceLevel, projectId }: TGetColumns): IGroupByColumn[] | undefined => {
|
||||
// store values
|
||||
const { getUserDetails } = store.memberRoot;
|
||||
// derived values
|
||||
const { memberIds, includeNone } = getScopeMemberIds({ isWorkspaceLevel, projectId });
|
||||
const assigneeColumns: IGroupByColumn[] = [];
|
||||
const {
|
||||
project: { projectMemberIds, getProjectMemberIds },
|
||||
getUserDetails,
|
||||
} = store.memberRoot;
|
||||
// if workspace level
|
||||
if (isWorkspaceLevel) {
|
||||
const { workspaceMemberIds } = store.memberRoot.workspace;
|
||||
if (!workspaceMemberIds) return;
|
||||
workspaceMemberIds.forEach((memberId) => {
|
||||
const member = getUserDetails(memberId);
|
||||
assigneeColumns.push({
|
||||
id: memberId,
|
||||
name: member?.display_name || "",
|
||||
icon: <Avatar name={member?.display_name} src={getFileURL(member?.avatar_url ?? "")} size="md" />,
|
||||
payload: { assignee_ids: [memberId] },
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// if project level
|
||||
const _projectMemberIds = projectId ? getProjectMemberIds(projectId, false) : projectMemberIds;
|
||||
if (!_projectMemberIds) return;
|
||||
// Map project member ids to group by assignee columns
|
||||
_projectMemberIds.forEach((memberId) => {
|
||||
const member = getUserDetails(memberId);
|
||||
assigneeColumns.push({
|
||||
id: memberId,
|
||||
name: member?.display_name || "",
|
||||
icon: <Avatar name={member?.display_name} src={getFileURL(member?.avatar_url ?? "")} size="md" />,
|
||||
payload: { assignee_ids: [memberId] },
|
||||
});
|
||||
|
||||
if (!memberIds) return [];
|
||||
|
||||
memberIds.forEach((memberId) => {
|
||||
const member = getUserDetails(memberId);
|
||||
if (!member) return;
|
||||
assigneeColumns.push({
|
||||
id: memberId,
|
||||
name: member?.display_name || "",
|
||||
icon: <Avatar name={member?.display_name} src={getFileURL(member?.avatar_url ?? "")} size="md" />,
|
||||
payload: { assignee_ids: [memberId] },
|
||||
});
|
||||
});
|
||||
if (includeNone) {
|
||||
assigneeColumns.push({ id: "None", name: "None", icon: <Avatar size="md" />, payload: {} });
|
||||
}
|
||||
assigneeColumns.push({ id: "None", name: "None", icon: <Avatar size="md" />, payload: {} });
|
||||
|
||||
return assigneeColumns;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/eleme
|
||||
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
||||
import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { LinkIcon, Settings, Share2, LogOut, MoreHorizontal, ChevronRight } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
@@ -78,6 +77,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||
// router
|
||||
const { workspaceSlug, projectId: URLProjectId } = useParams();
|
||||
const router = useRouter();
|
||||
// derived values
|
||||
const project = getPartialProjectById(projectId);
|
||||
// toggle project list open
|
||||
@@ -353,26 +353,26 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
{isAuthorized && (
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/archives/issues`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("archives")}</span>
|
||||
</div>
|
||||
</Link>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
router.push(`/${workspaceSlug}/projects/${project?.id}/archives/issues`);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2 cursor-pointer">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("archives")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIsMenuActive(false);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${project?.id}`);
|
||||
}}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/settings/projects/${project?.id}`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex items-center justify-start gap-2 cursor-pointer">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{/* leave project */}
|
||||
{!isAuthorized && (
|
||||
|
||||
Reference in New Issue
Block a user