Compare commits

...

6 Commits

23 changed files with 129 additions and 56 deletions

View File

@@ -42,7 +42,7 @@ urlpatterns = [
),
# updated v2 paginated issues
path(
"workspaces/<str:slug>/v2/issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
IssuePaginatedViewSet.as_view({"get": "list"}),
name="project-issues-paginated",
),

View File

@@ -741,17 +741,12 @@ class DeletedIssuesListViewSet(BaseAPIView):
class IssuePaginatedViewSet(BaseViewSet):
def get_queryset(self):
workspace_slug = self.kwargs.get("slug")
# getting the project_id from the request params
project_id = self.request.GET.get("project_id", None)
project_id = self.kwargs.get("project_id")
issue_queryset = Issue.issue_objects.filter(
workspace__slug=workspace_slug
workspace__slug=workspace_slug, project_id=project_id
)
if project_id:
issue_queryset = issue_queryset.filter(project_id=project_id)
return (
issue_queryset.select_related(
"workspace", "project", "state", "parent"
@@ -793,8 +788,8 @@ class IssuePaginatedViewSet(BaseViewSet):
return paginated_data
def list(self, request, slug):
project_id = self.request.GET.get("project_id", None)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
cursor = request.GET.get("cursor", None)
is_description_required = request.GET.get("description", False)
updated_at = request.GET.get("updated_at__gt", None)
@@ -833,14 +828,26 @@ class IssuePaginatedViewSet(BaseViewSet):
required_fields.append("description_html")
# querying issues
base_queryset = Issue.issue_objects.filter(workspace__slug=slug)
if project_id:
base_queryset = base_queryset.filter(project_id=project_id)
base_queryset = Issue.issue_objects.filter(
workspace__slug=slug, project_id=project_id
)
base_queryset = base_queryset.order_by("updated_at")
queryset = self.get_queryset().order_by("updated_at")
# validation for guest user
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project_member = ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project_id,
member=request.user,
role=5,
is_active=True,
)
if project_member.exists() and not project.guest_view_all_features:
base_queryset = base_queryset.filter(created_by=request.user)
queryset = queryset.filter(created_by=request.user)
# filtering issues by greater then updated_at given by the user
if updated_at:
base_queryset = base_queryset.filter(updated_at__gt=updated_at)

View File

@@ -77,7 +77,8 @@ export type TIssueParams =
| "show_empty_groups"
| "cursor"
| "per_page"
| "issue_type";
| "issue_type"
| "layout";
export type TCalendarLayouts = "month" | "week";

View File

@@ -4,6 +4,7 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// mobx store
// components
import { LogoSpinner } from "@/components/common";
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, IssuePeekOverview } from "@/components/issues";
import { EIssuesStoreType } from "@/constants/issue";
// ui
@@ -16,7 +17,7 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
useSWR(
const { isLoading } = useSWR(
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
@@ -26,7 +27,17 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.ARCHIVED}>
<ArchivedIssueAppliedFiltersRoot />

View File

@@ -5,6 +5,7 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// hooks
// components
import { LogoSpinner } from "@/components/common";
import { TransferIssues, TransferIssuesModal } from "@/components/cycles";
import {
CycleAppliedFiltersRoot,
@@ -50,7 +51,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
// state
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
useSWR(
const { isLoading } = useSWR(
workspaceSlug && projectId && cycleId
? `CYCLE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${cycleId.toString()}`
: null,
@@ -62,7 +63,8 @@ export const CycleLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilters = issuesFilter?.getIssueFilters(cycleId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout;
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
@@ -75,6 +77,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
if (!workspaceSlug || !projectId || !cycleId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.CYCLE}>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />

View File

@@ -2,6 +2,7 @@ import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
import { LogoSpinner } from "@/components/common";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
// hooks
@@ -30,7 +31,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
useSWR(
const { isLoading } = useSWR(
workspaceSlug && projectId ? `DRAFT_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
@@ -40,10 +41,18 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.DRAFT}>
<div className="relative flex h-full w-full flex-col overflow-hidden">

View File

@@ -5,6 +5,7 @@ import useSWR from "swr";
// mobx store
// components
import { Row, ERowVariant } from "@plane/ui";
import { LogoSpinner } from "@/components/common";
import {
IssuePeekOverview,
ModuleAppliedFiltersRoot,
@@ -43,7 +44,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.MODULE);
useSWR(
const { isLoading } = useSWR(
workspaceSlug && projectId && moduleId
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
: null,
@@ -55,9 +56,18 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(moduleId?.toString());
if (!workspaceSlug || !projectId || !moduleId) return <></>;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.MODULE}>

View File

@@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// components
import { Spinner } from "@plane/ui";
import { LogoSpinner } from "@/components/common";
import {
ListLayout,
CalendarLayout,
@@ -44,7 +45,7 @@ export const ProjectLayoutRoot: FC = observer(() => {
// hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
useSWR(
const { isLoading } = useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId) {
@@ -54,10 +55,18 @@ export const ProjectLayoutRoot: FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT}>
<div className="relative flex h-full w-full flex-col overflow-hidden">

View File

@@ -53,11 +53,12 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilters = issuesFilter?.getIssueFilters(viewId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId || !viewId) return <></>;
if (isLoading) {
if (isLoading && !issueFilters) {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />

View File

@@ -148,7 +148,7 @@ export class Storage {
};
syncWorkspace = async () => {
if (document.hidden || !rootStore.user.localDBEnabled) return; // return if the window gets hidden
if (document.hidden || !rootStore.user.localDBEnabled || !this.db) return; // return if the window gets hidden
await Sentry.startSpan({ name: "LOAD_WS", attributes: { slug: this.workspaceSlug } }, async () => {
await loadWorkSpaceData(this.workspaceSlug);
});
@@ -173,7 +173,7 @@ export class Storage {
};
syncIssues = async (projectId: string) => {
if (document.hidden || !rootStore.user.localDBEnabled) return false; // return if the window gets hidden
if (document.hidden || !rootStore.user.localDBEnabled || !this.db) return false; // return if the window gets hidden
try {
const sync = Sentry.startSpan({ name: `SYNC_ISSUES` }, () => this._syncIssues(projectId));
@@ -191,7 +191,7 @@ export class Storage {
log("### Sync started");
let status = this.getStatus(projectId);
if (status === "loading" || status === "syncing") {
logInfo(`Project ${projectId} is already loading or syncing`);
log(`Project ${projectId} is already loading or syncing`);
return;
}
const syncPromise = this.getSync(projectId);
@@ -290,7 +290,7 @@ export class Storage {
!rootStore.user.localDBEnabled
) {
if (rootStore.user.localDBEnabled) {
logInfo(`Project ${projectId} is loading, falling back to server`);
log(`Project ${projectId} is loading, falling back to server`);
}
const issueService = new IssueService();
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries);

View File

@@ -8,7 +8,7 @@ import { issueSchema } from "./schemas";
export const PROJECT_OFFLINE_STATUS: Record<string, boolean> = {};
export const addIssue = async (issue: any) => {
if (document.hidden || !rootStore.user.localDBEnabled) return;
if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
persistence.db.exec("BEGIN TRANSACTION;");
stageIssueInserts(issue);
@@ -16,7 +16,7 @@ export const addIssue = async (issue: any) => {
};
export const addIssuesBulk = async (issues: any, batchSize = 100) => {
if (!rootStore.user.localDBEnabled) return;
if (!rootStore.user.localDBEnabled || !persistence.db) return;
for (let i = 0; i < issues.length; i += batchSize) {
const batch = issues.slice(i, i + batchSize);
@@ -32,7 +32,7 @@ export const addIssuesBulk = async (issues: any, batchSize = 100) => {
}
};
export const deleteIssueFromLocal = async (issue_id: any) => {
if (!rootStore.user.localDBEnabled) return;
if (!rootStore.user.localDBEnabled || !persistence.db) return;
const deleteQuery = `delete from issues where id='${issue_id}'`;
const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`;
@@ -44,7 +44,7 @@ export const deleteIssueFromLocal = async (issue_id: any) => {
};
// @todo: Update deletes the issue description from local. Implement a separate update.
export const updateIssue = async (issue: TIssue & { is_local_update: number }) => {
if (document.hidden || !rootStore.user.localDBEnabled) return;
if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
const issue_id = issue.id;
// delete the issue and its meta data
@@ -53,7 +53,7 @@ export const updateIssue = async (issue: TIssue & { is_local_update: number }) =
};
export const syncDeletesToLocal = async (workspaceId: string, projectId: string, queries: any) => {
if (!rootStore.user.localDBEnabled) return;
if (!rootStore.user.localDBEnabled || !persistence.db) return;
const issueService = new IssueService();
const response = await issueService.getDeletedIssues(workspaceId, projectId, queries);

View File

@@ -1,9 +1,7 @@
// import { SQL } from "./sqlite";
import { persistence } from "../storage.sqlite";
export const runQuery = async (sql: string) => {
const data = await persistence.db.exec({
const data = await persistence.db?.exec({
sql,
rowMode: "object",
returnValue: "resultRows",

View File

@@ -4,8 +4,20 @@ import { issueSchema } from "./schemas";
import { wrapDateTime } from "./utils";
export const translateQueryParams = (queries: any) => {
const { group_by, sub_group_by, labels, assignees, state, cycle, module, priority, type, issue_type, ...otherProps } =
queries;
const {
group_by,
layout,
sub_group_by,
labels,
assignees,
state,
cycle,
module,
priority,
type,
issue_type,
...otherProps
} = queries;
const order_by = queries.order_by;
if (state) otherProps.state_id = state;
@@ -33,7 +45,7 @@ export const translateQueryParams = (queries: any) => {
}
// Fix invalid orderby when switching from spreadsheet layout
if ((group_by || sub_group_by) && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
if (layout === "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
otherProps.order_by = "sort_order";
}
// For each property value, replace None with empty string

View File

@@ -15,7 +15,7 @@ export const logError = (e: any) => {
e = parseSQLite3Error(e);
}
Sentry.captureException(e);
console.log(e);
console.error(e);
};
export const logInfo = console.info;

View File

@@ -55,14 +55,7 @@ export class IssueService extends APIService {
queries?: any,
config = {}
): Promise<TIssuesResponse> {
queries.project_id = projectId;
return this.get(
`/api/workspaces/${workspaceSlug}/v2/issues/`,
{
params: queries,
},
config
)
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/v2/issues/`, { params: queries }, config)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;

View File

@@ -33,6 +33,7 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(projectId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (

View File

@@ -31,6 +31,7 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(cycleId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
updateFilters: (

View File

@@ -33,6 +33,7 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(projectId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (

View File

@@ -13,7 +13,7 @@ import {
TStaticViewTypes,
} from "@plane/types";
// constants
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
// helpers
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
// lib
@@ -114,6 +114,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
: nonEmptyArrayValue;
});
if (displayFilters?.layout) issueFiltersParams.layout = displayFilters?.layout;
return issueFiltersParams;
};

View File

@@ -31,6 +31,7 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(moduleId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
updateFilters: (

View File

@@ -31,6 +31,7 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(viewId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>;
updateFilters: (
@@ -264,9 +265,16 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
const currentUserId = this.rootIssueStore.currentUserId;
if (currentUserId)
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROJECT_VIEW, type, workspaceSlug, viewId, currentUserId, {
kanban_filters: _filters.kanbanFilters,
});
this.handleIssuesLocalFilters.set(
EIssuesStoreType.PROJECT_VIEW,
type,
workspaceSlug,
viewId,
currentUserId,
{
kanban_filters: _filters.kanbanFilters,
}
);
runInAction(() => {
Object.keys(updatedKanbanFilters).forEach((_key) => {

View File

@@ -31,6 +31,7 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(projectId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (

View File

@@ -20,8 +20,6 @@ const nextConfig = {
key: "Referrer-Policy",
value: "origin-when-cross-origin",
},
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "credentialless" },
],
},
];