Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f1b35e751 | ||
|
|
6f1230145e | ||
|
|
ae3b588081 | ||
|
|
7183cffdb7 | ||
|
|
692e13039c | ||
|
|
bfd4aa1001 | ||
|
|
c779fc095c | ||
|
|
b5493a31f8 | ||
|
|
a20b5051d4 | ||
|
|
25eb727eb9 | ||
|
|
ade4d290f5 | ||
|
|
39ca600091 | ||
|
|
cfdb3373c9 | ||
|
|
95175ab939 | ||
|
|
42e928138c | ||
|
|
8db51ab295 | ||
|
|
58ac1dcba1 | ||
|
|
7430ccf9a8 | ||
|
|
85ee7f9af0 | ||
|
|
06ecbe884c | ||
|
|
8a6a5a8ca7 | ||
|
|
87ea13c32e | ||
|
|
e3ceb4825a | ||
|
|
de009d6d10 | ||
|
|
7b3f206f57 | ||
|
|
2018114543 | ||
|
|
ff8c5ee910 | ||
|
|
2ddd7096e4 | ||
|
|
add35b5ea6 | ||
|
|
448a34aa5f | ||
|
|
8c57543f72 | ||
|
|
9062a7b67d | ||
|
|
89603bb2d6 | ||
|
|
3ab959c682 | ||
|
|
d96ab2e7af | ||
|
|
5d8f66ae22 | ||
|
|
ce4a375b62 | ||
|
|
92645c1cee | ||
|
|
ac14d57f8b | ||
|
|
71ebe5ca36 | ||
|
|
bb71e60fcb | ||
|
|
3691cef351 | ||
|
|
c2d8703acf | ||
|
|
507946886f | ||
|
|
24944a03c6 | ||
|
|
cb045abfe1 | ||
|
|
24cc69fd7b | ||
|
|
88c26b334d | ||
|
|
200be0ac7f | ||
|
|
ae657af958 | ||
|
|
f4c4848a0d | ||
|
|
6914dc9f42 | ||
|
|
742559bc63 | ||
|
|
70dacc5e6a | ||
|
|
5b96c970cd | ||
|
|
906d095a1e | ||
|
|
0af9b09844 | ||
|
|
edb68a1bc6 | ||
|
|
790ecee629 | ||
|
|
0cabde03f0 | ||
|
|
d3d3bf79f9 | ||
|
|
6321977f1f | ||
|
|
208df80c86 | ||
|
|
bc27bc9dd2 | ||
|
+2 |
1acaae9d5e | ||
|
|
fbbca0c519 | ||
|
|
a6216beb7e | ||
|
|
732963b591 | ||
|
|
625cbf872b | ||
|
|
d26fb8ce02 | ||
|
|
e4f9d027fe | ||
|
|
c1407daa47 |
@@ -6,6 +6,7 @@
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center"><b>Plane</b></h1>
|
||||
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.com/invite/A92xrEGCge">
|
||||
@@ -57,7 +58,7 @@ Prefer full control over your data and infrastructure? Install and run Plane on
|
||||
| Docker | [](https://developers.plane.so/self-hosting/methods/docker-compose) |
|
||||
| Kubernetes | [](https://developers.plane.so/self-hosting/methods/kubernetes) |
|
||||
|
||||
`Instance admins` can manage and customize settings using [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
|
||||
`Instance admins` can configure instance settings with [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
@@ -117,9 +118,9 @@ Setting up your local environment is simple and straightforward. Follow these st
|
||||
|
||||
That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉
|
||||
|
||||
## Built with
|
||||
[](https://nextjs.org/)<br/>
|
||||
[](https://www.djangoproject.com/)<br/>
|
||||
## ⚙️ Built with
|
||||
[](https://nextjs.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://nodejs.org/en)
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
FROM node:20-alpine as base
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS builder
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
@@ -13,7 +15,7 @@ RUN turbo prune --scope=admin --docker
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS installer
|
||||
FROM base AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -52,7 +54,7 @@ RUN yarn turbo run build --filter=admin
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS runner
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=installer /app/admin/next.config.js .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:20-alpine
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { InstanceService } from "@plane/services";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// services
|
||||
import { InstanceService } from "@/services/instance.service";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -2,18 +2,16 @@ import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// constants
|
||||
// plane imports
|
||||
import { WEB_BASE_URL, ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
|
||||
// types
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// components
|
||||
import { Button, CustomSelect, getButtonStyling, Input, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
const instanceWorkspaceService = new InstanceWorkspaceService();
|
||||
|
||||
export const WorkspaceCreateForm = () => {
|
||||
// router
|
||||
@@ -40,8 +38,8 @@ export const WorkspaceCreateForm = () => {
|
||||
const workspaceBaseURL = encodeURI(WEB_BASE_URL || window.location.origin + "/");
|
||||
|
||||
const handleCreateWorkspace = async (formData: IWorkspace) => {
|
||||
await workspaceService
|
||||
.workspaceSlugCheck(formData.slug)
|
||||
await instanceWorkspaceService
|
||||
.slugCheck(formData.slug)
|
||||
.then(async (res) => {
|
||||
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
|
||||
@@ -7,12 +7,11 @@ import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import {AuthService } from "@plane/services";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { getFileURL, cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
|
||||
// service initialization
|
||||
const authService = new AuthService();
|
||||
|
||||
@@ -6,12 +6,11 @@ import { useSearchParams } from "next/navigation";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Button, Checkbox, Input, Spinner } from "@plane/ui";
|
||||
import { getPasswordStrength } from "@plane/utils";
|
||||
// components
|
||||
import { Banner, PasswordStrengthMeter } from "@/components/common";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
|
||||
// service initialization
|
||||
const authService = new AuthService();
|
||||
|
||||
@@ -5,13 +5,12 @@ import { useSearchParams } from "next/navigation";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { Banner } from "@/components/common";
|
||||
// helpers
|
||||
import { authErrorHandler } from "@/lib/auth-helpers";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
// local components
|
||||
import { AuthBanner } from "../authentication";
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
// store
|
||||
// import { rootStore } from "@/lib/store-context";
|
||||
|
||||
export abstract class APIService {
|
||||
protected baseURL: string;
|
||||
private axiosInstance: AxiosInstance;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL;
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
private setupInterceptors() {
|
||||
// this.axiosInstance.interceptors.response.use(
|
||||
// (response) => response,
|
||||
// (error) => {
|
||||
// const store = rootStore;
|
||||
// if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset();
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
}
|
||||
|
||||
get<ResponseType>(url: string, params = {}): Promise<AxiosResponse<ResponseType>> {
|
||||
return this.axiosInstance.get(url, { params });
|
||||
}
|
||||
|
||||
post<RequestType, ResponseType>(url: string, data: RequestType, config = {}): Promise<AxiosResponse<ResponseType>> {
|
||||
return this.axiosInstance.post(url, data, config);
|
||||
}
|
||||
|
||||
put<RequestType, ResponseType>(url: string, data: RequestType, config = {}): Promise<AxiosResponse<ResponseType>> {
|
||||
return this.axiosInstance.put(url, data, config);
|
||||
}
|
||||
|
||||
patch<RequestType, ResponseType>(url: string, data: RequestType, config = {}): Promise<AxiosResponse<ResponseType>> {
|
||||
return this.axiosInstance.patch(url, data, config);
|
||||
}
|
||||
|
||||
delete<RequestType>(url: string, data?: RequestType, config = {}) {
|
||||
return this.axiosInstance.delete(url, { data, ...config });
|
||||
}
|
||||
|
||||
request<T>(config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance(config);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
type TCsrfTokenResponse = {
|
||||
csrf_token: string;
|
||||
};
|
||||
|
||||
export class AuthService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async requestCSRFToken(): Promise<TCsrfTokenResponse> {
|
||||
return this.get<TCsrfTokenResponse>("/auth/get-csrf-token/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import type {
|
||||
IFormattedInstanceConfiguration,
|
||||
IInstance,
|
||||
IInstanceAdmin,
|
||||
IInstanceConfiguration,
|
||||
IInstanceInfo,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class InstanceService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getInstanceInfo(): Promise<IInstanceInfo> {
|
||||
return this.get<IInstanceInfo>("/api/instances/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInstanceAdmins(): Promise<IInstanceAdmin[]> {
|
||||
return this.get<IInstanceAdmin[]>("/api/instances/admins/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInstanceInfo(data: Partial<IInstance>): Promise<IInstance> {
|
||||
return this.patch<Partial<IInstance>, IInstance>("/api/instances/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInstanceConfigurations() {
|
||||
return this.get<IInstanceConfiguration[]>("/api/instances/configurations/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInstanceConfigurations(
|
||||
data: Partial<IFormattedInstanceConfiguration>
|
||||
): Promise<IInstanceConfiguration[]> {
|
||||
return this.patch<Partial<IFormattedInstanceConfiguration>, IInstanceConfiguration[]>(
|
||||
"/api/instances/configurations/",
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async sendTestEmail(receiverEmail: string): Promise<undefined> {
|
||||
return this.post<{ receiver_email: string }, undefined>("/api/instances/email-credentials-check/", {
|
||||
receiver_email: receiverEmail,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import type { IUser } from "@plane/types";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
interface IUserSession extends IUser {
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export class UserService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async authCheck(): Promise<IUserSession> {
|
||||
return this.get<any>("/api/instances/admins/me/")
|
||||
.then((response) => ({ ...response?.data, isAuthenticated: true }))
|
||||
.catch(() => ({ isAuthenticated: false }));
|
||||
}
|
||||
|
||||
async currentUser(): Promise<IUser> {
|
||||
return this.get<IUser>("/api/instances/admins/me/")
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import type { IWorkspace, TWorkspacePaginationInfo } from "@plane/types";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class WorkspaceService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Fetches all workspaces
|
||||
* @returns Promise<TWorkspacePaginationInfo>
|
||||
*/
|
||||
async getWorkspaces(nextPageCursor?: string): Promise<TWorkspacePaginationInfo> {
|
||||
return this.get<TWorkspacePaginationInfo>("/api/instances/workspaces/", {
|
||||
cursor: nextPageCursor,
|
||||
})
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Checks if a slug is available
|
||||
* @param slug - string
|
||||
* @returns Promise<any>
|
||||
*/
|
||||
async workspaceSlugCheck(slug: string): Promise<any> {
|
||||
const params = new URLSearchParams({ slug });
|
||||
return this.get(`/api/instances/workspace-slug-check/?${params.toString()}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a new workspace
|
||||
* @param data - IWorkspace
|
||||
* @returns Promise<IWorkspace>
|
||||
*/
|
||||
async createWorkspace(data: IWorkspace): Promise<IWorkspace> {
|
||||
return this.post<IWorkspace, IWorkspace>("/api/instances/workspaces/", data)
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import set from "lodash/set";
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// plane internal packages
|
||||
import { EInstanceStatus, TInstanceStatus } from "@plane/constants";
|
||||
import {InstanceService} from "@plane/services";
|
||||
import {
|
||||
IInstance,
|
||||
IInstanceAdmin,
|
||||
@@ -10,8 +11,6 @@ import {
|
||||
IInstanceInfo,
|
||||
IInstanceConfig,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import { InstanceService } from "@/services/instance.service";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
@@ -96,7 +95,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
try {
|
||||
if (this.instance === undefined) this.isLoading = true;
|
||||
this.error = undefined;
|
||||
const instanceInfo = await this.instanceService.getInstanceInfo();
|
||||
const instanceInfo = await this.instanceService.info();
|
||||
// handling the new user popup toggle
|
||||
if (this.instance === undefined && !instanceInfo?.instance?.workspaces_exist)
|
||||
this.store.theme.toggleNewUserPopup();
|
||||
@@ -125,7 +124,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
*/
|
||||
updateInstanceInfo = async (data: Partial<IInstance>) => {
|
||||
try {
|
||||
const instanceResponse = await this.instanceService.updateInstanceInfo(data);
|
||||
const instanceResponse = await this.instanceService.update(data);
|
||||
if (instanceResponse) {
|
||||
runInAction(() => {
|
||||
if (this.instance) set(this.instance, "instance", instanceResponse);
|
||||
@@ -144,7 +143,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
*/
|
||||
fetchInstanceAdmins = async () => {
|
||||
try {
|
||||
const instanceAdmins = await this.instanceService.getInstanceAdmins();
|
||||
const instanceAdmins = await this.instanceService.admins();
|
||||
if (instanceAdmins) runInAction(() => (this.instanceAdmins = instanceAdmins));
|
||||
return instanceAdmins;
|
||||
} catch (error) {
|
||||
@@ -159,7 +158,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
*/
|
||||
fetchInstanceConfigurations = async () => {
|
||||
try {
|
||||
const instanceConfigurations = await this.instanceService.getInstanceConfigurations();
|
||||
const instanceConfigurations = await this.instanceService.configurations();
|
||||
if (instanceConfigurations) runInAction(() => (this.instanceConfigurations = instanceConfigurations));
|
||||
return instanceConfigurations;
|
||||
} catch (error) {
|
||||
@@ -174,7 +173,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
*/
|
||||
updateInstanceConfigurations = async (data: Partial<IFormattedInstanceConfiguration>) => {
|
||||
try {
|
||||
const response = await this.instanceService.updateInstanceConfigurations(data);
|
||||
const response = await this.instanceService.updateConfigurations(data);
|
||||
runInAction(() => {
|
||||
this.instanceConfigurations = this.instanceConfigurations?.map((config) => {
|
||||
const item = response.find((item) => item.key === config.key);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { action, observable, runInAction, makeObservable } from "mobx";
|
||||
// plane internal packages
|
||||
import { EUserStatus, TUserStatus } from "@plane/constants";
|
||||
import { AuthService, UserService } from "@plane/services";
|
||||
import { IUser } from "@plane/types";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { UserService } from "@/services/user.service";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
@@ -58,7 +56,7 @@ export class UserStore implements IUserStore {
|
||||
fetchCurrentUser = async () => {
|
||||
try {
|
||||
if (this.currentUser === undefined) this.isLoading = true;
|
||||
const currentUser = await this.userService.currentUser();
|
||||
const currentUser = await this.userService.adminDetails();
|
||||
if (currentUser) {
|
||||
await this.store.instance.fetchInstanceAdmins();
|
||||
runInAction(() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import set from "lodash/set";
|
||||
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
||||
// plane imports
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
@@ -29,7 +29,7 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
workspaces: Record<string, IWorkspace> = {};
|
||||
paginationInfo: TPaginationInfo | undefined = undefined;
|
||||
// services
|
||||
workspaceService;
|
||||
instanceWorkspaceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
@@ -48,7 +48,7 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
// curd actions
|
||||
createWorkspace: action,
|
||||
});
|
||||
this.workspaceService = new WorkspaceService();
|
||||
this.instanceWorkspaceService = new InstanceWorkspaceService();
|
||||
}
|
||||
|
||||
// computed
|
||||
@@ -84,7 +84,7 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
} else {
|
||||
this.loader = "init-loader";
|
||||
}
|
||||
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces();
|
||||
const paginatedWorkspaceData = await this.instanceWorkspaceService.list();
|
||||
runInAction(() => {
|
||||
const { results, ...paginationInfo } = paginatedWorkspaceData;
|
||||
results.forEach((workspace: IWorkspace) => {
|
||||
@@ -109,7 +109,7 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
if (!this.paginationInfo || this.paginationInfo.next_page_results === false) return [];
|
||||
try {
|
||||
this.loader = "pagination";
|
||||
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces(this.paginationInfo.next_cursor);
|
||||
const paginatedWorkspaceData = await this.instanceWorkspaceService.list(this.paginationInfo.next_cursor);
|
||||
runInAction(() => {
|
||||
const { results, ...paginationInfo } = paginatedWorkspaceData;
|
||||
results.forEach((workspace: IWorkspace) => {
|
||||
@@ -135,7 +135,7 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
createWorkspace = async (data: IWorkspace): Promise<IWorkspace> => {
|
||||
try {
|
||||
this.loader = "mutation";
|
||||
const workspace = await this.workspaceService.createWorkspace(data);
|
||||
const workspace = await this.instanceWorkspaceService.create(data);
|
||||
runInAction(() => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
|
||||
+2
-1
@@ -18,11 +18,12 @@
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/utils": "*",
|
||||
"@plane/services": "*",
|
||||
"@sentry/nextjs": "^8.32.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.7.9",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.356.0",
|
||||
"mobx": "^6.12.0",
|
||||
|
||||
@@ -6,6 +6,7 @@ from .base import BaseSerializer
|
||||
from plane.db.models import Cycle, CycleIssue
|
||||
from plane.utils.timezone_converter import convert_to_utc
|
||||
|
||||
|
||||
class CycleSerializer(BaseSerializer):
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||
@@ -30,11 +31,20 @@ class CycleSerializer(BaseSerializer):
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
project_id = self.initial_data.get("project_id") or self.instance.project_id
|
||||
is_start_date_end_date_equal = (
|
||||
True
|
||||
if str(data.get("start_date")) == str(data.get("end_date"))
|
||||
else False
|
||||
)
|
||||
data["start_date"] = convert_to_utc(
|
||||
str(data.get("start_date").date()), project_id, is_start_date=True
|
||||
date=str(data.get("start_date").date()),
|
||||
project_id=project_id,
|
||||
is_start_date=True,
|
||||
)
|
||||
data["end_date"] = convert_to_utc(
|
||||
str(data.get("end_date", None).date()), project_id
|
||||
date=str(data.get("end_date", None).date()),
|
||||
project_id=project_id,
|
||||
is_start_date_end_date_equal=is_start_date_end_date_equal,
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@@ -288,16 +288,6 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# Create the triage state in Backlog group
|
||||
State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="triage",
|
||||
description="Default state for managing all Intake Issues",
|
||||
project_id=pk,
|
||||
color="#ff7700",
|
||||
is_triage=True,
|
||||
)
|
||||
|
||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
|
||||
model_activity.delay(
|
||||
|
||||
@@ -20,7 +20,9 @@ from .workspace import (
|
||||
WorkspaceMemberMeSerializer,
|
||||
WorkspaceUserPropertiesSerializer,
|
||||
WorkspaceUserLinkSerializer,
|
||||
WorkspaceRecentVisitSerializer
|
||||
WorkspaceRecentVisitSerializer,
|
||||
WorkspaceHomePreferenceSerializer,
|
||||
StickySerializer,
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
|
||||
@@ -20,12 +20,25 @@ class CycleWriteSerializer(BaseSerializer):
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
project_id = self.initial_data.get("project_id") or self.instance.project_id
|
||||
project_id = (
|
||||
self.initial_data.get("project_id", None)
|
||||
or (self.instance and self.instance.project_id)
|
||||
or self.context.get("project_id", None)
|
||||
)
|
||||
is_start_date_end_date_equal = (
|
||||
True
|
||||
if str(data.get("start_date")) == str(data.get("end_date"))
|
||||
else False
|
||||
)
|
||||
data["start_date"] = convert_to_utc(
|
||||
str(data.get("start_date").date()), project_id, is_start_date=True
|
||||
date=str(data.get("start_date").date()),
|
||||
project_id=project_id,
|
||||
is_start_date=True,
|
||||
)
|
||||
data["end_date"] = convert_to_utc(
|
||||
str(data.get("end_date", None).date()), project_id
|
||||
date=str(data.get("end_date", None).date()),
|
||||
project_id=project_id,
|
||||
is_start_date_end_date_equal=is_start_date_end_date_equal,
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import Dashboard, Widget
|
||||
from plane.db.models import DeprecatedDashboard, DeprecatedWidget
|
||||
|
||||
# Third party frameworks
|
||||
from rest_framework import serializers
|
||||
@@ -8,7 +8,7 @@ from rest_framework import serializers
|
||||
|
||||
class DashboardSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Dashboard
|
||||
model = DeprecatedDashboard
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ class WidgetSerializer(BaseSerializer):
|
||||
widget_filters = serializers.JSONField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Widget
|
||||
model = DeprecatedWidget
|
||||
fields = ["id", "key", "is_visible", "widget_filters"]
|
||||
|
||||
@@ -16,10 +16,12 @@ from plane.db.models import (
|
||||
WorkspaceUserProperties,
|
||||
WorkspaceUserLink,
|
||||
UserRecentVisit,
|
||||
Issue,
|
||||
Page,
|
||||
Issue,
|
||||
Page,
|
||||
Project,
|
||||
ProjectMember
|
||||
ProjectMember,
|
||||
WorkspaceHomePreference,
|
||||
Sticky,
|
||||
)
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
@@ -27,6 +29,7 @@ from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
@@ -119,6 +122,7 @@ class WorkspaceUserPropertiesSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "user"]
|
||||
|
||||
|
||||
class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceUserLink
|
||||
@@ -129,7 +133,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||
url = data.get("url", "")
|
||||
if url and not url.startswith(("http://", "https://")):
|
||||
data["url"] = "http://" + url
|
||||
|
||||
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def validate_url(self, value):
|
||||
@@ -141,74 +145,88 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class IssueRecentVisitSerializer(serializers.ModelSerializer):
|
||||
project_identifier = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = ["name", "state", "priority", "assignees", "type", "sequence_id", "project_id", "project_identifier"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state",
|
||||
"priority",
|
||||
"assignees",
|
||||
"type",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"project_identifier",
|
||||
]
|
||||
|
||||
def get_project_identifier(self, obj):
|
||||
project = obj.project
|
||||
|
||||
return project.identifier if project else None
|
||||
|
||||
class ProjectMemberSerializer(BaseSerializer):
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ["member"]
|
||||
|
||||
class ProjectRecentVisitSerializer(serializers.ModelSerializer):
|
||||
project_members = serializers.SerializerMethodField()
|
||||
|
||||
project_members = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ["id", "name", "logo_props", "project_members", "identifier"]
|
||||
|
||||
def get_project_members(self, obj):
|
||||
members = ProjectMember.objects.filter(project_id=obj.id).select_related('member')
|
||||
members = ProjectMember.objects.filter(
|
||||
project_id=obj.id, member__is_bot=False, is_active=True
|
||||
).values_list("member", flat=True)
|
||||
|
||||
return members
|
||||
|
||||
|
||||
serializer = ProjectMemberSerializer(members, many=True)
|
||||
return serializer.data
|
||||
|
||||
class PageRecentVisitSerializer(serializers.ModelSerializer):
|
||||
project_id = serializers.SerializerMethodField()
|
||||
project_identifier = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = ["id", "name", "logo_props", "project_id", "owned_by", "project_identifier"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"logo_props",
|
||||
"project_id",
|
||||
"owned_by",
|
||||
"project_identifier",
|
||||
]
|
||||
|
||||
def get_project_id(self, obj):
|
||||
return obj.project_id if hasattr(obj, 'project_id') else obj.projects.values_list('id', flat=True).first()
|
||||
|
||||
return (
|
||||
obj.project_id
|
||||
if hasattr(obj, "project_id")
|
||||
else obj.projects.values_list("id", flat=True).first()
|
||||
)
|
||||
|
||||
def get_project_identifier(self, obj):
|
||||
project = obj.projects.first()
|
||||
|
||||
return project.identifier if project else None
|
||||
|
||||
|
||||
def get_entity_model_and_serializer(entity_type):
|
||||
entity_map = {
|
||||
"issue": (Issue, IssueRecentVisitSerializer),
|
||||
"page": (Page, PageRecentVisitSerializer),
|
||||
"project": (Project, ProjectRecentVisitSerializer)
|
||||
"project": (Project, ProjectRecentVisitSerializer),
|
||||
}
|
||||
return entity_map.get(entity_type, (None, None))
|
||||
|
||||
|
||||
class WorkspaceRecentVisitSerializer(BaseSerializer):
|
||||
entity_data = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = UserRecentVisit
|
||||
fields = [
|
||||
"id",
|
||||
"entity_name",
|
||||
"entity_identifier",
|
||||
"entity_data",
|
||||
"visited_at"
|
||||
]
|
||||
fields = ["id", "entity_name", "entity_identifier", "entity_data", "visited_at"]
|
||||
read_only_fields = ["workspace", "owner", "created_by", "updated_by"]
|
||||
|
||||
def get_entity_data(self, obj):
|
||||
@@ -225,3 +243,18 @@ class WorkspaceRecentVisitSerializer(BaseSerializer):
|
||||
except entity_model.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class WorkspaceHomePreferenceSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceHomePreference
|
||||
fields = ["key", "is_enabled", "sort_order"]
|
||||
read_only_fields = ["workspace", "created_by", "updated_by"]
|
||||
|
||||
|
||||
class StickySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Sticky
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "owner"]
|
||||
extra_kwargs = {"name": {"required": False}}
|
||||
|
||||
@@ -28,7 +28,9 @@ from plane.app.views import (
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
WorkspaceDraftIssueViewSet,
|
||||
QuickLinkViewSet,
|
||||
UserRecentVisitViewSet
|
||||
UserRecentVisitViewSet,
|
||||
WorkspaceHomePreferenceViewSet,
|
||||
WorkspaceStickyViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -215,25 +217,45 @@ urlpatterns = [
|
||||
WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}),
|
||||
name="workspace-drafts-issues",
|
||||
),
|
||||
|
||||
# quick link
|
||||
path(
|
||||
"workspaces/<str:slug>/quick-links/",
|
||||
QuickLinkViewSet.as_view({"get": "list", "post": "create"}),
|
||||
name="workspace-quick-links"
|
||||
name="workspace-quick-links",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/quick-links/<uuid:pk>/",
|
||||
QuickLinkViewSet.as_view({
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy"
|
||||
}),
|
||||
name="workspace-quick-links"
|
||||
"workspaces/<str:slug>/quick-links/<uuid:pk>/",
|
||||
QuickLinkViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="workspace-quick-links",
|
||||
),
|
||||
# Widgets
|
||||
path(
|
||||
"workspaces/<str:slug>/home-preferences/",
|
||||
WorkspaceHomePreferenceViewSet.as_view(),
|
||||
name="workspace-home-preference",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/home-preferences/<str:key>/",
|
||||
WorkspaceHomePreferenceViewSet.as_view(),
|
||||
name="workspace-home-preference",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/recent-visits/",
|
||||
UserRecentVisitViewSet.as_view({"get": "list"}),
|
||||
name="workspace-recent-visits"
|
||||
)
|
||||
name="workspace-recent-visits",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/stickies/",
|
||||
WorkspaceStickyViewSet.as_view({"get": "list", "post": "create"}),
|
||||
name="workspace-sticky",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/stickies/<uuid:pk>/",
|
||||
WorkspaceStickyViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="workspace-sticky",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -41,6 +41,8 @@ from .workspace.base import (
|
||||
|
||||
from .workspace.draft import WorkspaceDraftIssueViewSet
|
||||
|
||||
from .workspace.home import WorkspaceHomePreferenceViewSet
|
||||
|
||||
from .workspace.favorite import (
|
||||
WorkspaceFavoriteEndpoint,
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
@@ -74,6 +76,7 @@ from .workspace.estimate import WorkspaceEstimatesEndpoint
|
||||
from .workspace.module import WorkspaceModulesEndpoint
|
||||
from .workspace.cycle import WorkspaceCyclesEndpoint
|
||||
from .workspace.quick_link import QuickLinkViewSet
|
||||
from .workspace.sticky import WorkspaceStickyViewSet
|
||||
|
||||
from .state.base import StateViewSet
|
||||
from .view.base import (
|
||||
|
||||
@@ -54,11 +54,7 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
# Module imports
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.utils.timezone_converter import (
|
||||
convert_utc_to_project_timezone,
|
||||
convert_to_utc,
|
||||
user_timezone_converter,
|
||||
)
|
||||
from plane.utils.timezone_converter import convert_to_utc, user_timezone_converter
|
||||
|
||||
|
||||
class CycleViewSet(BaseViewSet):
|
||||
@@ -143,10 +139,7 @@ class CycleViewSet(BaseViewSet):
|
||||
& Q(end_date__gte=current_time_in_utc),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(
|
||||
start_date__gt=current_time_in_utc,
|
||||
then=Value("UPCOMING"),
|
||||
),
|
||||
When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")),
|
||||
When(end_date__lt=current_time_in_utc, then=Value("COMPLETED")),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
@@ -259,7 +252,9 @@ class CycleViewSet(BaseViewSet):
|
||||
"created_by",
|
||||
)
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
|
||||
data = user_timezone_converter(
|
||||
data, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@@ -271,7 +266,9 @@ class CycleViewSet(BaseViewSet):
|
||||
request.data.get("start_date", None) is not None
|
||||
and request.data.get("end_date", None) is not None
|
||||
):
|
||||
serializer = CycleWriteSerializer(data=request.data)
|
||||
serializer = CycleWriteSerializer(
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, owned_by=request.user)
|
||||
cycle = (
|
||||
@@ -306,6 +303,11 @@ class CycleViewSet(BaseViewSet):
|
||||
.first()
|
||||
)
|
||||
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
cycle = user_timezone_converter(
|
||||
cycle, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
model_name="cycle",
|
||||
@@ -358,7 +360,9 @@ class CycleViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
|
||||
serializer = CycleWriteSerializer(
|
||||
cycle, data=request.data, partial=True, context={"project_id": project_id}
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
cycle = queryset.values(
|
||||
@@ -388,6 +392,11 @@ class CycleViewSet(BaseViewSet):
|
||||
"created_by",
|
||||
).first()
|
||||
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
cycle = user_timezone_converter(
|
||||
cycle, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
model_name="cycle",
|
||||
@@ -457,7 +466,9 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
queryset = queryset.first()
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
|
||||
data = user_timezone_converter(
|
||||
data, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
@@ -533,8 +544,17 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
start_date = convert_to_utc(str(start_date), project_id, is_start_date=True)
|
||||
end_date = convert_to_utc(str(end_date), project_id)
|
||||
is_start_date_end_date_equal = (
|
||||
True if str(start_date) == str(end_date) else False
|
||||
)
|
||||
start_date = convert_to_utc(
|
||||
date=str(start_date), project_id=project_id, is_start_date=True
|
||||
)
|
||||
end_date = convert_to_utc(
|
||||
date=str(end_date),
|
||||
project_id=project_id,
|
||||
is_start_date_end_date_equal=is_start_date_end_date_equal,
|
||||
)
|
||||
|
||||
# Check if any cycle intersects in the given interval
|
||||
cycles = Cycle.objects.filter(
|
||||
|
||||
@@ -32,15 +32,15 @@ from plane.app.serializers import (
|
||||
WidgetSerializer,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Dashboard,
|
||||
DashboardWidget,
|
||||
DeprecatedDashboard,
|
||||
DeprecatedDashboardWidget,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
FileAsset,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Project,
|
||||
Widget,
|
||||
DeprecatedWidget,
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
)
|
||||
@@ -687,7 +687,7 @@ class DashboardEndpoint(BaseAPIView):
|
||||
if not dashboard_id:
|
||||
dashboard_type = request.GET.get("dashboard_type", None)
|
||||
if dashboard_type == "home":
|
||||
dashboard, created = Dashboard.objects.get_or_create(
|
||||
dashboard, created = DeprecatedDashboard.objects.get_or_create(
|
||||
type_identifier=dashboard_type,
|
||||
owned_by=request.user,
|
||||
is_default=True,
|
||||
@@ -707,24 +707,24 @@ class DashboardEndpoint(BaseAPIView):
|
||||
|
||||
updated_dashboard_widgets = []
|
||||
for widget_key in widgets_to_fetch:
|
||||
widget = Widget.objects.filter(key=widget_key).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
widget = DeprecatedWidget.objects.filter(
|
||||
key=widget_key
|
||||
).values_list("id", flat=True)
|
||||
if widget:
|
||||
updated_dashboard_widgets.append(
|
||||
DashboardWidget(
|
||||
DeprecatedDashboardWidget(
|
||||
widget_id=widget, dashboard_id=dashboard.id
|
||||
)
|
||||
)
|
||||
|
||||
DashboardWidget.objects.bulk_create(
|
||||
DeprecatedDashboardWidget.objects.bulk_create(
|
||||
updated_dashboard_widgets, batch_size=100
|
||||
)
|
||||
|
||||
widgets = (
|
||||
Widget.objects.annotate(
|
||||
DeprecatedWidget.objects.annotate(
|
||||
is_visible=Exists(
|
||||
DashboardWidget.objects.filter(
|
||||
DeprecatedDashboardWidget.objects.filter(
|
||||
widget_id=OuterRef("pk"),
|
||||
dashboard_id=dashboard.id,
|
||||
is_visible=True,
|
||||
@@ -733,7 +733,7 @@ class DashboardEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
dashboard_filters=Subquery(
|
||||
DashboardWidget.objects.filter(
|
||||
DeprecatedDashboardWidget.objects.filter(
|
||||
widget_id=OuterRef("pk"),
|
||||
dashboard_id=dashboard.id,
|
||||
filters__isnull=False,
|
||||
@@ -792,7 +792,7 @@ class DashboardEndpoint(BaseAPIView):
|
||||
|
||||
class WidgetsEndpoint(BaseAPIView):
|
||||
def patch(self, request, dashboard_id, widget_id):
|
||||
dashboard_widget = DashboardWidget.objects.filter(
|
||||
dashboard_widget = DeprecatedDashboardWidget.objects.filter(
|
||||
widget_id=widget_id, dashboard_id=dashboard_id
|
||||
).first()
|
||||
dashboard_widget.is_visible = request.data.get(
|
||||
|
||||
@@ -120,10 +120,12 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key, file_type=type, file_size=size_limit
|
||||
)
|
||||
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -573,6 +573,8 @@ class PageDuplicateEndpoint(BaseAPIView):
|
||||
page.name = f"{page.name} (Copy)"
|
||||
page.description_binary = None
|
||||
page.owned_by = request.user
|
||||
page.created_by = request.user
|
||||
page.updated_by = request.user
|
||||
page.save()
|
||||
|
||||
for project_id in project_ids:
|
||||
|
||||
@@ -416,16 +416,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# Create the triage state in Backlog group
|
||||
State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="triage",
|
||||
description="Default state for managing all Intake Issues",
|
||||
project_id=pk,
|
||||
color="#ff7700",
|
||||
is_triage=True,
|
||||
)
|
||||
|
||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
|
||||
model_activity.delay(
|
||||
|
||||
@@ -277,28 +277,14 @@ class SearchEndpoint(BaseAPIView):
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
base_filters = Q(
|
||||
q,
|
||||
is_active=True,
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
project_id=project_id,
|
||||
role__gt=10,
|
||||
)
|
||||
if issue_id:
|
||||
issue_created_by = (
|
||||
Issue.objects.filter(id=issue_id)
|
||||
.values_list("created_by_id", flat=True)
|
||||
.first()
|
||||
)
|
||||
# Add condition to include `issue_created_by` in the query
|
||||
filters = Q(member_id=issue_created_by) | base_filters
|
||||
else:
|
||||
filters = base_filters
|
||||
|
||||
# Query to fetch users
|
||||
users = (
|
||||
ProjectMember.objects.filter(filters)
|
||||
ProjectMember.objects.filter(
|
||||
q,
|
||||
is_active=True,
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(
|
||||
member__avatar_url=Case(
|
||||
When(
|
||||
@@ -318,14 +304,35 @@ class SearchEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.values(
|
||||
"member__avatar_url",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)[:count]
|
||||
)
|
||||
|
||||
response_data["user_mention"] = list(users)
|
||||
if issue_id:
|
||||
issue_created_by = (
|
||||
Issue.objects.filter(id=issue_id)
|
||||
.values_list("created_by_id", flat=True)
|
||||
.first()
|
||||
)
|
||||
users = (
|
||||
users.filter(Q(role__gt=10) | Q(member_id=issue_created_by))
|
||||
.distinct()
|
||||
.values(
|
||||
"member__avatar_url",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)
|
||||
)
|
||||
else:
|
||||
users = (
|
||||
users.filter(Q(role__gt=10))
|
||||
.distinct()
|
||||
.values(
|
||||
"member__avatar_url",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)
|
||||
)
|
||||
|
||||
response_data["user_mention"] = list(users[:count])
|
||||
|
||||
elif query_type == "project":
|
||||
fields = ["name", "identifier"]
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Module imports
|
||||
from ..base import BaseAPIView
|
||||
from plane.db.models.workspace import WorkspaceHomePreference
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import Workspace
|
||||
from plane.app.serializers.workspace import WorkspaceHomePreferenceSerializer
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class WorkspaceHomePreferenceViewSet(BaseAPIView):
|
||||
model = WorkspaceHomePreference
|
||||
|
||||
def get_serializer_class(self):
|
||||
return WorkspaceHomePreferenceSerializer
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
get_preference = WorkspaceHomePreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
|
||||
create_preference_keys = []
|
||||
|
||||
keys = [
|
||||
key
|
||||
for key, _ in WorkspaceHomePreference.HomeWidgetKeys.choices
|
||||
if key not in ["quick_tutorial", "new_at_plane"]
|
||||
]
|
||||
|
||||
sort_order_counter = 1
|
||||
|
||||
for preference in keys:
|
||||
if preference not in get_preference.values_list("key", flat=True):
|
||||
create_preference_keys.append(preference)
|
||||
|
||||
sort_order = 1000 - sort_order_counter
|
||||
|
||||
preference = WorkspaceHomePreference.objects.bulk_create(
|
||||
[
|
||||
WorkspaceHomePreference(
|
||||
key=key,
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
sort_order=sort_order,
|
||||
)
|
||||
for key in create_preference_keys
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
sort_order_counter += 1
|
||||
|
||||
preference = WorkspaceHomePreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
|
||||
return Response(
|
||||
preference.values("key", "is_enabled", "config", "sort_order"),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def patch(self, request, slug, key):
|
||||
preference = WorkspaceHomePreference.objects.filter(
|
||||
key=key, workspace__slug=slug, user=request.user
|
||||
).first()
|
||||
|
||||
if preference:
|
||||
serializer = WorkspaceHomePreferenceSerializer(
|
||||
preference, data=request.data, partial=True
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{"detail": "Preference not found"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
@@ -8,7 +8,8 @@ from plane.app.serializers import WorkspaceUserLinkSerializer
|
||||
from ..base import BaseViewSet
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
class QuickLinkViewSet(BaseViewSet):
|
||||
|
||||
class QuickLinkViewSet(BaseViewSet):
|
||||
model = WorkspaceUserLink
|
||||
|
||||
def get_serializer_class(self):
|
||||
@@ -16,52 +17,58 @@ class QuickLinkViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug, owner=request.user)
|
||||
serializer = WorkspaceUserLinkSerializer(data=request.data)
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = WorkspaceUserLinkSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(workspace_id=workspace.id, owner=request.user)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk):
|
||||
quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug, owner=request.user).first()
|
||||
quick_link = WorkspaceUserLink.objects.filter(
|
||||
pk=pk, workspace__slug=slug, owner=request.user
|
||||
).first()
|
||||
|
||||
if quick_link:
|
||||
serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True)
|
||||
if quick_link:
|
||||
serializer = WorkspaceUserLinkSerializer(
|
||||
quick_link, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def retrieve(self, request, slug, pk):
|
||||
try:
|
||||
quick_link = WorkspaceUserLink.objects.get(
|
||||
pk=pk,
|
||||
workspace__slug=slug,
|
||||
owner=request.user
|
||||
pk=pk, workspace__slug=slug, owner=request.user
|
||||
)
|
||||
serializer = WorkspaceUserLinkSerializer(quick_link)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except WorkspaceUserLink.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Quick link not found."},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
{"error": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def destroy(self, request, slug, pk):
|
||||
quick_link = WorkspaceUserLink.objects.get(pk=pk, workspace__slug=slug, owner=request.user)
|
||||
quick_link.delete()
|
||||
quick_link = WorkspaceUserLink.objects.get(
|
||||
pk=pk, workspace__slug=slug, owner=request.user
|
||||
)
|
||||
quick_link.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
quick_links = WorkspaceUserLink.objects.filter(workspace__slug=slug, owner=request.user)
|
||||
|
||||
quick_links = WorkspaceUserLink.objects.filter(
|
||||
workspace__slug=slug, owner=request.user
|
||||
)
|
||||
|
||||
serializer = WorkspaceUserLinkSerializer(quick_links, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -9,6 +9,7 @@ from plane.app.serializers import WorkspaceRecentVisitSerializer
|
||||
from ..base import BaseViewSet
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
|
||||
class UserRecentVisitViewSet(BaseViewSet):
|
||||
model = UserRecentVisit
|
||||
|
||||
@@ -17,15 +18,18 @@ class UserRecentVisitViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug)
|
||||
user_recent_visits = UserRecentVisit.objects.filter(
|
||||
workspace__slug=slug, user=request.user
|
||||
)
|
||||
|
||||
entity_name = request.query_params.get("entity_name")
|
||||
entity_name = request.query_params.get("entity_name")
|
||||
|
||||
if entity_name:
|
||||
user_recent_visits = user_recent_visits.filter(entity_name=entity_name)
|
||||
|
||||
user_recent_visits = user_recent_visits.filter(entity_name__in=["issue","page","project"])
|
||||
|
||||
serializer = WorkspaceRecentVisitSerializer(user_recent_visits[:20], many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
if entity_name:
|
||||
user_recent_visits = user_recent_visits.filter(entity_name=entity_name)
|
||||
|
||||
user_recent_visits = user_recent_visits.filter(
|
||||
entity_name__in=["issue", "page", "project"]
|
||||
)
|
||||
|
||||
serializer = WorkspaceRecentVisitSerializer(user_recent_visits[:20], many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# Module imports
|
||||
from plane.app.views.base import BaseViewSet
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from plane.db.models import Sticky, Workspace
|
||||
from plane.app.serializers import StickySerializer
|
||||
|
||||
|
||||
class WorkspaceStickyViewSet(BaseViewSet):
|
||||
serializer_class = StickySerializer
|
||||
model = Sticky
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(owner_id=self.request.user.id)
|
||||
.select_related("workspace", "owner")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = StickySerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def list(self, request, slug):
|
||||
query = request.query_params.get("query", False)
|
||||
stickies = self.get_queryset()
|
||||
if query:
|
||||
stickies = stickies.filter(name__icontains=query)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(stickies),
|
||||
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
|
||||
default_per_page=20,
|
||||
)
|
||||
|
||||
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
@@ -16,9 +16,9 @@ from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
def workspace_invitation(email, workspace_id, token, current_site, inviter):
|
||||
try:
|
||||
user = User.objects.get(email=invitor)
|
||||
user = User.objects.get(email=inviter)
|
||||
|
||||
workspace = Workspace.objects.get(pk=workspace_id)
|
||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||
@@ -26,7 +26,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
)
|
||||
|
||||
# Relative link
|
||||
relative_link = f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}"
|
||||
relative_link = f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}" # noqa: E501
|
||||
|
||||
# The complete url including the domain
|
||||
abs_url = str(current_site) + relative_link
|
||||
@@ -42,7 +42,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
) = get_email_configuration()
|
||||
|
||||
# Subject of the email
|
||||
subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane"
|
||||
subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane" # noqa: E501
|
||||
|
||||
context = {
|
||||
"email": email,
|
||||
@@ -78,11 +78,9 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
logging.getLogger("plane").info("Email sent succesfully")
|
||||
|
||||
logging.getLogger("plane").info("Email sent successfully")
|
||||
return
|
||||
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e:
|
||||
log_exception(e)
|
||||
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist):
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Generated by Django 4.2.17 on 2025-01-02 07:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0088_sticky_sort_order_workspaceuserlink"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="WorkspaceHomePreference",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"deleted_at",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("key", models.CharField(max_length=255)),
|
||||
("is_enabled", models.BooleanField(default=True)),
|
||||
("config", models.JSONField(default=dict)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_user_home_preferences",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_user_home_preferences",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Workspace Home Preference",
|
||||
"verbose_name_plural": "Workspace Home Preferences",
|
||||
"db_table": "workspace_home_preferences",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="workspacehomepreference",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("deleted_at__isnull", True)),
|
||||
fields=("workspace", "user", "key"),
|
||||
name="workspace_user_home_preferences_unique_workspace_user_key_when_deleted_at_null",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="workspacehomepreference",
|
||||
unique_together={("workspace", "user", "key", "deleted_at")},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="page",
|
||||
name="name",
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sticky",
|
||||
name="name",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacehomepreference',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(default=65535),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,87 @@
|
||||
# Generated by Django 4.2.17 on 2025-01-09 14:43
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0089_workspacehomepreference_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Dashboard',
|
||||
new_name='DeprecatedDashboard',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='DashboardWidget',
|
||||
new_name='DeprecatedDashboardWidget',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Widget',
|
||||
new_name='DeprecatedWidget',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='deprecateddashboard',
|
||||
options={'ordering': ('-created_at',), 'verbose_name': 'DeprecatedDashboard', 'verbose_name_plural': 'DeprecatedDashboards'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='deprecateddashboardwidget',
|
||||
options={'ordering': ('-created_at',), 'verbose_name': 'Deprecated Dashboard Widget', 'verbose_name_plural': 'Deprecated Dashboard Widgets'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='deprecatedwidget',
|
||||
options={'ordering': ('-created_at',), 'verbose_name': 'DeprecatedWidget', 'verbose_name_plural': 'DeprecatedWidgets'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workspacehomepreference',
|
||||
name='sort_order',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='deprecateddashboard',
|
||||
table='deprecated_dashboards',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='deprecateddashboardwidget',
|
||||
table='deprecated_dashboard_widgets',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='deprecatedwidget',
|
||||
table='deprecated_widgets',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkspaceUserPreference',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('key', models.CharField(max_length=255)),
|
||||
('is_pinned', models.BooleanField(default=False)),
|
||||
('sort_order', models.FloatField(default=65535)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_preferences', to=settings.AUTH_USER_MODEL)),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_preferences', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Workspace User Preference',
|
||||
'verbose_name_plural': 'Workspace User Preferences',
|
||||
'db_table': 'workspace_user_preferences',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='workspaceuserpreference',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('workspace', 'user', 'key'), name='workspace_user_preferences_unique_workspace_user_key_when_deleted_at_null'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='workspaceuserpreference',
|
||||
unique_together={('workspace', 'user', 'key', 'deleted_at')},
|
||||
),
|
||||
]
|
||||
@@ -3,7 +3,7 @@ from .api import APIActivityLog, APIToken
|
||||
from .asset import FileAsset
|
||||
from .base import BaseModel
|
||||
from .cycle import Cycle, CycleIssue, CycleUserProperties
|
||||
from .dashboard import Dashboard, DashboardWidget, Widget
|
||||
from .dashboard import DeprecatedDashboard, DeprecatedDashboardWidget, DeprecatedWidget
|
||||
from .deploy_board import DeployBoard
|
||||
from .draft import (
|
||||
DraftIssue,
|
||||
@@ -69,6 +69,7 @@ from .workspace import (
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
WorkspaceUserLink,
|
||||
WorkspaceHomePreference
|
||||
)
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
@@ -8,7 +8,7 @@ from ..mixins import TimeAuditModel
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class Dashboard(BaseModel):
|
||||
class DeprecatedDashboard(BaseModel):
|
||||
DASHBOARD_CHOICES = (
|
||||
("workspace", "Workspace"),
|
||||
("project", "Project"),
|
||||
@@ -36,13 +36,13 @@ class Dashboard(BaseModel):
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Dashboard"
|
||||
verbose_name_plural = "Dashboards"
|
||||
db_table = "dashboards"
|
||||
verbose_name = "DeprecatedDashboard"
|
||||
verbose_name_plural = "DeprecatedDashboards"
|
||||
db_table = "deprecated_dashboards"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class Widget(TimeAuditModel):
|
||||
class DeprecatedWidget(TimeAuditModel):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||
)
|
||||
@@ -55,18 +55,18 @@ class Widget(TimeAuditModel):
|
||||
return f"{self.key}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Widget"
|
||||
verbose_name_plural = "Widgets"
|
||||
db_table = "widgets"
|
||||
verbose_name = "DeprecatedWidget"
|
||||
verbose_name_plural = "DeprecatedWidgets"
|
||||
db_table = "deprecated_widgets"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class DashboardWidget(BaseModel):
|
||||
class DeprecatedDashboardWidget(BaseModel):
|
||||
widget = models.ForeignKey(
|
||||
Widget, on_delete=models.CASCADE, related_name="dashboard_widgets"
|
||||
DeprecatedWidget, on_delete=models.CASCADE, related_name="dashboard_widgets"
|
||||
)
|
||||
dashboard = models.ForeignKey(
|
||||
Dashboard, on_delete=models.CASCADE, related_name="dashboard_widgets"
|
||||
DeprecatedDashboard, on_delete=models.CASCADE, related_name="dashboard_widgets"
|
||||
)
|
||||
is_visible = models.BooleanField(default=True)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
@@ -86,7 +86,7 @@ class DashboardWidget(BaseModel):
|
||||
name="dashboard_widget_unique_widget_dashboard_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Dashboard Widget"
|
||||
verbose_name_plural = "Dashboard Widgets"
|
||||
db_table = "dashboard_widgets"
|
||||
verbose_name = "Deprecated Dashboard Widget"
|
||||
verbose_name_plural = "Deprecated Dashboard Widgets"
|
||||
db_table = "deprecated_dashboard_widgets"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
@@ -20,7 +20,7 @@ class Page(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="pages"
|
||||
)
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
name = models.TextField(blank=True)
|
||||
description = models.JSONField(default=dict, blank=True)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
|
||||
@@ -7,7 +7,7 @@ from .base import BaseModel
|
||||
|
||||
|
||||
class Sticky(BaseModel):
|
||||
name = models.TextField()
|
||||
name = models.TextField(null=True, blank=True)
|
||||
|
||||
description = models.JSONField(blank=True, default=dict)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Python imports
|
||||
from django.db.models.functions import Ln
|
||||
import pytz
|
||||
|
||||
# Django imports
|
||||
@@ -341,4 +342,86 @@ class WorkspaceUserLink(WorkspaceBaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.workspace.id} {self.url}"
|
||||
return f"{self.workspace.id} {self.url}"
|
||||
|
||||
|
||||
class WorkspaceHomePreference(BaseModel):
|
||||
"""Preference for the home page of a workspace for a user"""
|
||||
|
||||
class HomeWidgetKeys(models.TextChoices):
|
||||
QUICK_LINKS = "quick_links", "Quick Links"
|
||||
RECENTS = "recents", "Recents"
|
||||
MY_STICKIES = "my_stickies", "My Stickies"
|
||||
NEW_AT_PLANE = "new_at_plane", "New at Plane"
|
||||
QUICK_TUTORIAL = "quick_tutorial", "Quick Tutorial"
|
||||
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_user_home_preferences",
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_user_home_preferences",
|
||||
)
|
||||
key = models.CharField(max_length=255)
|
||||
is_enabled = models.BooleanField(default=True)
|
||||
config = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "user", "key", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["workspace", "user", "key"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="workspace_user_home_preferences_unique_workspace_user_key_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Workspace Home Preference"
|
||||
verbose_name_plural = "Workspace Home Preferences"
|
||||
db_table = "workspace_home_preferences"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.workspace.name} {self.user.email} {self.key}"
|
||||
|
||||
|
||||
|
||||
class WorkspaceUserPreference(BaseModel):
|
||||
"""Preference for the workspace for a user"""
|
||||
|
||||
class UserPreferenceKeys(models.TextChoices):
|
||||
CYCLES = "cycles", "Cycles"
|
||||
VIEWS = "views", "Views"
|
||||
ANALYTICS = "analytics", "Analytics"
|
||||
PROJECTS = "projects", "Projects"
|
||||
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_user_preferences",
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_user_preferences",
|
||||
)
|
||||
key = models.CharField(max_length=255)
|
||||
is_pinned = models.BooleanField(default=False)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "user", "key", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["workspace", "user", "key"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="workspace_user_preferences_unique_workspace_user_key_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Workspace User Preference"
|
||||
verbose_name_plural = "Workspace User Preferences"
|
||||
db_table = "workspace_user_preferences"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
@@ -361,6 +361,18 @@ ATTACHMENT_MIME_TYPES = [
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"text/plain",
|
||||
"application/rtf",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
"application/vnd.oasis.opendocument.graphics",
|
||||
# Microsoft Visio
|
||||
"application/vnd.visio",
|
||||
# Netpbm format
|
||||
"image/x-portable-graymap",
|
||||
"image/x-portable-bitmap",
|
||||
"image/x-portable-pixmap",
|
||||
# Open Office Bae
|
||||
"application/vnd.oasis.opendocument.database",
|
||||
# Audio
|
||||
"audio/mpeg",
|
||||
"audio/wav",
|
||||
|
||||
@@ -130,15 +130,6 @@ class IntakeIssuePublicViewSet(BaseViewSet):
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Create or get state
|
||||
state, _ = State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="backlog",
|
||||
description="Default state for managing all Intake Issues",
|
||||
project_id=project_deploy_board.project_id,
|
||||
color="#ff7700",
|
||||
)
|
||||
|
||||
# create an issue
|
||||
issue = Issue.objects.create(
|
||||
name=request.data.get("issue", {}).get("name"),
|
||||
@@ -148,7 +139,6 @@ class IntakeIssuePublicViewSet(BaseViewSet):
|
||||
),
|
||||
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||
project_id=project_deploy_board.project_id,
|
||||
state=state,
|
||||
)
|
||||
|
||||
# Create an Issue Activity
|
||||
|
||||
@@ -3,6 +3,7 @@ from plane.db.models import Project
|
||||
from datetime import datetime, time
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
||||
# Create a timezone object for the user's timezone
|
||||
user_tz = pytz.timezone(user_timezone)
|
||||
@@ -28,7 +29,9 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
||||
return queryset_values
|
||||
|
||||
|
||||
def convert_to_utc(date, project_id, is_start_date=False):
|
||||
def convert_to_utc(
|
||||
date, project_id, is_start_date=False, is_start_date_end_date_equal=False
|
||||
):
|
||||
"""
|
||||
Converts a start date string to the project's local timezone at 12:00 AM
|
||||
and then converts it to UTC for storage.
|
||||
@@ -60,7 +63,12 @@ def convert_to_utc(date, project_id, is_start_date=False):
|
||||
|
||||
# If it's an start date, add one minute
|
||||
if is_start_date:
|
||||
localized_datetime += timedelta(minutes=1)
|
||||
localized_datetime += timedelta(minutes=0, seconds=1)
|
||||
|
||||
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
|
||||
# to make it the end of the day
|
||||
if is_start_date_end_date_equal:
|
||||
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
|
||||
|
||||
# Convert the localized datetime to UTC
|
||||
utc_datetime = localized_datetime.astimezone(pytz.utc)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:20-alpine
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine AS base
|
||||
FROM node:20-alpine AS base
|
||||
# The web Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker.
|
||||
# Make sure you update this Dockerfile, the Dockerfile in the web workspace and copy that over to Dockerfile in the docs.
|
||||
|
||||
|
||||
+3
-3
@@ -25,9 +25,9 @@
|
||||
"@plane/types": "*",
|
||||
"@sentry/node": "^8.28.0",
|
||||
"@sentry/profiling-node": "^8.28.0",
|
||||
"@tiptap/core": "^2.4.0",
|
||||
"@tiptap/html": "^2.3.0",
|
||||
"axios": "^1.7.2",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/html": "2.11.0",
|
||||
"axios": "^1.7.9",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// plane editor
|
||||
import {
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData,
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData,
|
||||
getBinaryDataFromDocumentEditorHTMLString,
|
||||
getBinaryDataFromRichTextEditorHTMLString,
|
||||
} from "@plane/editor";
|
||||
// plane types
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
|
||||
type TArgs = {
|
||||
document_html: string;
|
||||
variant: "rich" | "document";
|
||||
};
|
||||
|
||||
export const convertHTMLDocumentToAllFormats = (args: TArgs): TDocumentPayload => {
|
||||
const { document_html, variant } = args;
|
||||
|
||||
let allFormats: TDocumentPayload;
|
||||
|
||||
if (variant === "rich") {
|
||||
const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html);
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else if (variant === "document") {
|
||||
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html);
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Invalid variant provided: ${variant}`);
|
||||
}
|
||||
|
||||
return allFormats;
|
||||
};
|
||||
Vendored
+5
@@ -6,3 +6,8 @@ export type TDocumentTypes = "project_page" | TAdditionalDocumentTypes;
|
||||
export type HocusPocusServerContext = {
|
||||
cookie: string;
|
||||
};
|
||||
|
||||
export type TConvertDocumentRequestBody = {
|
||||
description_html: string;
|
||||
variant: "rich" | "document";
|
||||
};
|
||||
|
||||
+36
-14
@@ -1,20 +1,19 @@
|
||||
import "@/core/config/sentry-config.js";
|
||||
|
||||
import express from "express";
|
||||
import expressWs from "express-ws";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import compression from "compression";
|
||||
import helmet from "helmet";
|
||||
|
||||
// cors
|
||||
import cors from "cors";
|
||||
|
||||
// core hocuspocus server
|
||||
import expressWs from "express-ws";
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
// config
|
||||
import "@/core/config/sentry-config.js";
|
||||
// hocuspocus server
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
|
||||
// helpers
|
||||
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
|
||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||
import { errorHandler } from "@/core/helpers/error-handler.js";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
|
||||
|
||||
const app = express();
|
||||
expressWs(app);
|
||||
@@ -29,7 +28,7 @@ app.use(
|
||||
compression({
|
||||
level: 6,
|
||||
threshold: 5 * 1000,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// Logging middleware
|
||||
@@ -62,6 +61,31 @@ router.ws("/collaboration", (ws, req) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/convert-document", (req, res) => {
|
||||
const { description_html, variant } = req.body as TConvertDocumentRequestBody;
|
||||
try {
|
||||
if (description_html === undefined || variant === undefined) {
|
||||
res.status(400).send({
|
||||
message: "Missing required fields",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { description, description_binary } = convertHTMLDocumentToAllFormats({
|
||||
document_html: description_html,
|
||||
variant,
|
||||
});
|
||||
res.status(200).json({
|
||||
description,
|
||||
description_binary,
|
||||
});
|
||||
} catch (error) {
|
||||
manualLogger.error("Error in /convert-document endpoint:", error);
|
||||
res.status(500).send({
|
||||
message: `Internal server error. ${error}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use(process.env.LIVE_BASE_PATH || "/live", router);
|
||||
|
||||
app.use((_req, res) => {
|
||||
@@ -82,9 +106,7 @@ const gracefulShutdown = async () => {
|
||||
try {
|
||||
// Close the HocusPocus server WebSocket connections
|
||||
await HocusPocusServer.destroy();
|
||||
manualLogger.info(
|
||||
"HocusPocus server WebSocket connections closed gracefully.",
|
||||
);
|
||||
manualLogger.info("HocusPocus server WebSocket connections closed gracefully.");
|
||||
|
||||
// Close the Express server
|
||||
liveServer.close(() => {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// types
|
||||
import { TXAxisValues, TYAxisValues } from "@plane/types";
|
||||
|
||||
export const ANALYTICS_TABS = [
|
||||
{ key: "scope_and_demand", title: "Scope and Demand" },
|
||||
{ key: "custom", title: "Custom Analytics" },
|
||||
];
|
||||
|
||||
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
|
||||
[
|
||||
{
|
||||
value: "state_id",
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
value: "state__group",
|
||||
label: "State group",
|
||||
},
|
||||
{
|
||||
value: "priority",
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: "labels__id",
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
value: "assignees__id",
|
||||
label: "Assignee",
|
||||
},
|
||||
{
|
||||
value: "estimate_point__value",
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: "issue_cycle__cycle_id",
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: "issue_module__module_id",
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
value: "completed_at",
|
||||
label: "Completed date",
|
||||
},
|
||||
{
|
||||
value: "target_date",
|
||||
label: "Due date",
|
||||
},
|
||||
{
|
||||
value: "start_date",
|
||||
label: "Start date",
|
||||
},
|
||||
{
|
||||
value: "created_at",
|
||||
label: "Created date",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
|
||||
[
|
||||
{
|
||||
value: "issue_count",
|
||||
label: "Issue Count",
|
||||
},
|
||||
{
|
||||
value: "estimate",
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_DATE_KEYS = [
|
||||
"completed_at",
|
||||
"target_date",
|
||||
"start_date",
|
||||
"created_at",
|
||||
];
|
||||
@@ -0,0 +1,10 @@
|
||||
export const ISSUE_REACTION_EMOJI_CODES = [
|
||||
"128077",
|
||||
"128078",
|
||||
"128516",
|
||||
"128165",
|
||||
"128533",
|
||||
"129505",
|
||||
"9992",
|
||||
"128064",
|
||||
];
|
||||
@@ -23,3 +23,7 @@ export const WEBSITE_URL =
|
||||
// support email
|
||||
export const SUPPORT_EMAIL =
|
||||
process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "[email protected]";
|
||||
// marketing links
|
||||
export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing";
|
||||
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";
|
||||
export const MARKETING_PLANE_ONE_PAGE_LINK = "https://plane.so/one";
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum E_ARCHIVE_ERROR_CODES {
|
||||
"INVALID_ARCHIVE_STATE_GROUP" = 4091,
|
||||
"INVALID_ISSUE_START_DATE" = 4101,
|
||||
"INVALID_ISSUE_TARGET_DATE" = 4102,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const SIDEBAR_CLICKED = "Sidenav clicked";
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum E_SORT_ORDER {
|
||||
ASC = "asc",
|
||||
DESC = "desc",
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
// nivo
|
||||
import { Theme } from "@nivo/core";
|
||||
|
||||
export const CHARTS_THEME: Theme = {
|
||||
export const CHARTS_THEME = {
|
||||
background: "transparent",
|
||||
text: {
|
||||
color: "rgb(var(--color-text-200))",
|
||||
@@ -29,7 +26,7 @@ export const CHARTS_THEME: Theme = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_MARGIN = {
|
||||
export const CHART_DEFAULT_MARGIN = {
|
||||
top: 50,
|
||||
right: 50,
|
||||
bottom: 50,
|
||||
@@ -1,7 +1,11 @@
|
||||
export * from "./ai";
|
||||
export * from "./analytics";
|
||||
export * from "./auth";
|
||||
export * from "./endpoints";
|
||||
export * from "./event";
|
||||
export * from "./file";
|
||||
export * from "./filter";
|
||||
export * from "./graph";
|
||||
export * from "./instance";
|
||||
export * from "./issue";
|
||||
export * from "./metadata";
|
||||
|
||||
@@ -43,3 +43,26 @@ export const ARCHIVABLE_STATE_GROUPS = [
|
||||
STATE_GROUPS.completed.key,
|
||||
STATE_GROUPS.cancelled.key,
|
||||
];
|
||||
|
||||
export const PROGRESS_STATE_GROUPS_DETAILS = [
|
||||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#16A34A",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#F59E0B",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#3A3A3A",
|
||||
},
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#A3A3A3",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -19,3 +19,20 @@ export type TUserStatus = {
|
||||
status: EUserStatus | undefined;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export enum EUserPermissionsLevel {
|
||||
WORKSPACE = "WORKSPACE",
|
||||
PROJECT = "PROJECT",
|
||||
}
|
||||
|
||||
export enum EUserWorkspaceRoles {
|
||||
ADMIN = 20,
|
||||
MEMBER = 15,
|
||||
GUEST = 5,
|
||||
}
|
||||
|
||||
export enum EUserProjectRoles {
|
||||
ADMIN = 20,
|
||||
MEMBER = 15,
|
||||
GUEST = 5,
|
||||
}
|
||||
|
||||
@@ -40,23 +40,24 @@
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/utils": "*",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-blockquote": "^2.1.13",
|
||||
"@tiptap/extension-character-count": "^2.6.5",
|
||||
"@tiptap/extension-collaboration": "^2.3.2",
|
||||
"@tiptap/extension-image": "^2.1.13",
|
||||
"@tiptap/extension-list-item": "^2.1.13",
|
||||
"@tiptap/extension-mention": "^2.1.13",
|
||||
"@tiptap/extension-placeholder": "^2.3.0",
|
||||
"@tiptap/extension-task-item": "^2.1.13",
|
||||
"@tiptap/extension-task-list": "^2.1.13",
|
||||
"@tiptap/extension-text-align": "^2.8.0",
|
||||
"@tiptap/extension-text-style": "^2.7.1",
|
||||
"@tiptap/extension-underline": "^2.1.13",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/react": "^2.1.13",
|
||||
"@tiptap/starter-kit": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.0.13",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/extension-blockquote": "2.10.4",
|
||||
"@tiptap/extension-character-count": "2.11.0",
|
||||
"@tiptap/extension-collaboration": "2.11.0",
|
||||
"@tiptap/extension-image": "2.11.0",
|
||||
"@tiptap/extension-list-item": "2.11.0",
|
||||
"@tiptap/extension-mention": "2.11.0",
|
||||
"@tiptap/extension-placeholder": "2.11.0",
|
||||
"@tiptap/extension-task-item": "2.11.0",
|
||||
"@tiptap/extension-task-list": "2.11.0",
|
||||
"@tiptap/extension-text-align": "2.11.0",
|
||||
"@tiptap/extension-text-style": "2.11.0",
|
||||
"@tiptap/extension-underline": "2.11.0",
|
||||
"@tiptap/html": "2.11.0",
|
||||
"@tiptap/pm": "2.11.0",
|
||||
"@tiptap/react": "2.11.0",
|
||||
"@tiptap/starter-kit": "2.11.0",
|
||||
"@tiptap/suggestion": "2.11.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"jsx-dom-cjs": "^8.0.3",
|
||||
@@ -65,7 +66,6 @@
|
||||
"lucide-react": "^0.378.0",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"prosemirror-utils": "^1.2.2",
|
||||
"react-moveable": "^0.54.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.9",
|
||||
"uuid": "^10.0.0",
|
||||
|
||||
@@ -71,7 +71,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
|
||||
onClick={() => TextColorItem(editor).command(undefined)}
|
||||
onClick={() => TextColorItem(editor).command({ color: undefined })}
|
||||
>
|
||||
<Ban className="size-4" />
|
||||
</button>
|
||||
@@ -94,7 +94,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
|
||||
onClick={() => BackgroundColorItem(editor).command(undefined)}
|
||||
onClick={() => BackgroundColorItem(editor).command({ color: undefined })}
|
||||
>
|
||||
<Ban className="size-4" />
|
||||
</button>
|
||||
|
||||
@@ -87,12 +87,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
{...bubbleMenuProps}
|
||||
className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg"
|
||||
>
|
||||
<BubbleMenu {...bubbleMenuProps}>
|
||||
{!isSelecting && (
|
||||
<>
|
||||
<div className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg">
|
||||
<div className="px-2">
|
||||
{!props.editor.isActive("table") && (
|
||||
<BubbleMenuNodeSelector
|
||||
@@ -161,7 +158,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
editor.commands.setTextSelection(pos ?? 0);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</BubbleMenu>
|
||||
);
|
||||
|
||||
@@ -51,6 +51,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex } = args;
|
||||
|
||||
return [
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
const { disabledExtensions, fileHandler, mentionHandler } = props;
|
||||
|
||||
return [
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
|
||||
@@ -13,41 +13,51 @@ export const setText = (editor: Editor, range?: Range) => {
|
||||
|
||||
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleHeading({ level: 1 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingTwo = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleHeading({ level: 2 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingThree = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleHeading({ level: 3 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingFour = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 4 }).run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleHeading({ level: 4 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingFive = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 5 }).run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleHeading({ level: 5 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingSix = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 6 }).run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleHeading({ level: 6 }).run();
|
||||
};
|
||||
|
||||
export const toggleBold = (editor: Editor, range?: Range) => {
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleBold().run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleBold().run();
|
||||
};
|
||||
|
||||
export const toggleItalic = (editor: Editor, range?: Range) => {
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleItalic().run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleItalic().run();
|
||||
};
|
||||
|
||||
@@ -86,12 +96,16 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => {
|
||||
};
|
||||
|
||||
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleOrderedList().run();
|
||||
};
|
||||
|
||||
export const toggleBulletList = (editor: Editor, range?: Range) => {
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleBulletList().run();
|
||||
};
|
||||
|
||||
@@ -101,7 +115,9 @@ export const toggleTaskList = (editor: Editor, range?: Range) => {
|
||||
};
|
||||
|
||||
export const toggleStrike = (editor: Editor, range?: Range) => {
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleStrike().run();
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
else editor.chain().focus().toggleStrike().run();
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { getSchema } from "@tiptap/core";
|
||||
import { generateHTML, generateJSON } from "@tiptap/html";
|
||||
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import {
|
||||
CoreEditorExtensionsWithoutProps,
|
||||
DocumentEditorExtensionsWithoutProps,
|
||||
} from "@/extensions/core-without-props";
|
||||
|
||||
// editor extension configs
|
||||
const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
|
||||
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
|
||||
// editor schemas
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
/**
|
||||
* @description apply updates to a doc and return the updated doc in binary format
|
||||
* @param {Uint8Array} document
|
||||
* @param {Uint8Array} updates
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const applyUpdates = (document: Uint8Array, updates?: Uint8Array): Uint8Array => {
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, document);
|
||||
if (updates) {
|
||||
Y.applyUpdate(yDoc, updates);
|
||||
}
|
||||
|
||||
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
|
||||
return encodedDoc;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function encodes binary data to base64 string
|
||||
* @param {Uint8Array} document
|
||||
* @returns {string}
|
||||
*/
|
||||
export const convertBinaryDataToBase64String = (document: Uint8Array): string =>
|
||||
Buffer.from(document).toString("base64");
|
||||
|
||||
/**
|
||||
* @description this function decodes base64 string to binary data
|
||||
* @param {string} document
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64");
|
||||
|
||||
/**
|
||||
* @description this function generates the binary equivalent of html content for the rich text editor
|
||||
* @param {string} descriptionHTML
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
||||
// convert HTML to JSON
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(richTextEditorSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
return encodedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates the binary equivalent of html content for the document editor
|
||||
* @param {string} descriptionHTML
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
||||
// convert HTML to JSON
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
return encodedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates all document formats for the provided binary data for the rich text editor
|
||||
* @param {Uint8Array} description
|
||||
* @returns
|
||||
*/
|
||||
export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = convertBinaryDataToBase64String(description);
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, richTextEditorSchema).toJSON();
|
||||
// convert to HTML
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const contentHTML = generateHTML(contentJSON, RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
contentJSON,
|
||||
contentHTML,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates all document formats for the provided binary data for the document editor
|
||||
* @param {Uint8Array} description
|
||||
* @returns
|
||||
*/
|
||||
export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = convertBinaryDataToBase64String(description);
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
|
||||
// convert to HTML
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
contentJSON,
|
||||
contentHTML,
|
||||
};
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as Y from "yjs";
|
||||
|
||||
/**
|
||||
* @description apply updates to a doc and return the updated doc in base64(binary) format
|
||||
* @param {Uint8Array} document
|
||||
* @param {Uint8Array} updates
|
||||
* @returns {string} base64(binary) form of the updated doc
|
||||
*/
|
||||
export const applyUpdates = (document: Uint8Array, updates: Uint8Array): Uint8Array => {
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, document);
|
||||
Y.applyUpdate(yDoc, updates);
|
||||
|
||||
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
|
||||
return encodedDoc;
|
||||
};
|
||||
@@ -24,7 +24,7 @@ export * from "@/constants/common";
|
||||
// helpers
|
||||
export * from "@/helpers/common";
|
||||
export * from "@/helpers/editor-commands";
|
||||
export * from "@/helpers/yjs";
|
||||
export * from "@/helpers/yjs-utils";
|
||||
export * from "@/extensions/table/table";
|
||||
|
||||
// components
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "@/extensions/core-without-props";
|
||||
export * from "@/constants/document-collaborative-events";
|
||||
export * from "@/helpers/get-document-server-event";
|
||||
export * from "@/helpers/yjs-utils";
|
||||
export * from "@/types/document-collaborative-events";
|
||||
|
||||
@@ -119,13 +119,13 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"][checked] {
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"]:checked {
|
||||
background-color: rgba(var(--color-primary-100)) !important;
|
||||
border-color: rgba(var(--color-primary-100)) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"][checked]:hover {
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"]:checked:hover {
|
||||
background-color: rgba(var(--color-primary-300)) !important;
|
||||
border-color: rgba(var(--color-primary-300)) !important;
|
||||
}
|
||||
@@ -174,7 +174,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
}
|
||||
|
||||
&[checked]::before {
|
||||
&:checked::before {
|
||||
transform: scale(1) translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./use-local-storage";
|
||||
export * from "./use-outside-click-detector";
|
||||
export * from "./use-platform-os";
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const usePlatformOS = () => {
|
||||
const [platformData, setPlatformData] = useState({
|
||||
isMobile: false,
|
||||
platform: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const detectPlatform = () => {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(userAgent);
|
||||
let platform = "";
|
||||
|
||||
if (!isMobile) {
|
||||
if (userAgent.indexOf("Win") !== -1) {
|
||||
platform = "Windows";
|
||||
} else if (userAgent.indexOf("Mac") !== -1) {
|
||||
platform = "MacOS";
|
||||
} else if (userAgent.indexOf("Linux") !== -1) {
|
||||
platform = "Linux";
|
||||
} else {
|
||||
platform = "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
setPlatformData({ isMobile, platform });
|
||||
};
|
||||
|
||||
detectPlatform();
|
||||
}, []);
|
||||
|
||||
return platformData;
|
||||
};
|
||||
@@ -43,6 +43,7 @@
|
||||
"activity": "Activity",
|
||||
"appearance": "Appearance",
|
||||
"notifications": "Notifications",
|
||||
"inbox": "Inbox",
|
||||
"workspaces": "Workspaces",
|
||||
"create_workspace": "Create workspace",
|
||||
"invitations": "Invitations",
|
||||
@@ -282,6 +283,7 @@
|
||||
"create_a_draft": "Create a draft",
|
||||
"save_to_drafts": "Save to Drafts",
|
||||
"save": "Save",
|
||||
"update": "Update",
|
||||
"updating": "Updating",
|
||||
"create_new_issue": "Create new issue",
|
||||
"editor_is_not_ready_to_discard_changes": "Editor is not ready to discard changes",
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
"create_a_draft": "Crear un borrador",
|
||||
"save_to_drafts": "Guardar en borradores",
|
||||
"save": "Guardar",
|
||||
"update": "Actualizar",
|
||||
"updating": "Actualizando",
|
||||
"create_new_issue": "Crear nuevo problema",
|
||||
"editor_is_not_ready_to_discard_changes": "El editor no está listo para descartar los cambios",
|
||||
@@ -314,5 +315,6 @@
|
||||
"change_parent_issue": "Cambiar problema padre",
|
||||
"remove_parent_issue": "Eliminar problema padre",
|
||||
"add_parent": "Agregar padre",
|
||||
"loading_members": "Cargando miembros..."
|
||||
"loading_members": "Cargando miembros...",
|
||||
"inbox": "bandeja de entrada"
|
||||
}
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
"create_a_draft": "Créer un brouillon",
|
||||
"save_to_drafts": "Enregistrer dans les brouillons",
|
||||
"save": "Enregistrer",
|
||||
"update": "Mettre à jour",
|
||||
"updating": "Mise à jour",
|
||||
"create_new_issue": "Créer un nouveau problème",
|
||||
"editor_is_not_ready_to_discard_changes": "L'éditeur n'est pas prêt à annuler les modifications",
|
||||
@@ -314,5 +315,6 @@
|
||||
"change_parent_issue": "Modifier le problème parent",
|
||||
"remove_parent_issue": "Supprimer le problème parent",
|
||||
"add_parent": "Ajouter un parent",
|
||||
"loading_members": "Chargement des membres..."
|
||||
"loading_members": "Chargement des membres...",
|
||||
"inbox": "boîte de réception"
|
||||
}
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
"create_a_draft": "下書きを作成",
|
||||
"save_to_drafts": "下書きに保存",
|
||||
"save": "保存",
|
||||
"update": "更新",
|
||||
"updating": "更新中",
|
||||
"create_new_issue": "新しい問題を作成",
|
||||
"editor_is_not_ready_to_discard_changes": "エディターは変更を破棄する準備ができていません",
|
||||
@@ -314,5 +315,6 @@
|
||||
"change_parent_issue": "親問題を変更",
|
||||
"remove_parent_issue": "親問題を削除",
|
||||
"add_parent": "親問題を追加",
|
||||
"loading_members": "メンバーを読み込んでいます..."
|
||||
"loading_members": "メンバーを読み込んでいます...",
|
||||
"inbox": "受信箱"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/constants": "*",
|
||||
"axios": "^1.4.0"
|
||||
"axios": "^1.7.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// plane web constants
|
||||
import { AI_EDITOR_TASKS, API_BASE_URL } from "@plane/constants";
|
||||
// services
|
||||
import { APIService } from "@/api.service";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Payload type for AI editor tasks
|
||||
|
||||
@@ -34,7 +34,19 @@ export abstract class APIService {
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
const currentPath = window.location.pathname;
|
||||
window.location.replace(`/${currentPath ? `?next_path=${currentPath}` : ``}`);
|
||||
let prefix = "/";
|
||||
let updatedPath = currentPath;
|
||||
|
||||
// Check for special path prefixes
|
||||
if (currentPath.startsWith("/god-mode")) {
|
||||
prefix = "/god-mode";
|
||||
updatedPath = currentPath.replace("/god-mode", "");
|
||||
} else if (currentPath.startsWith("/spaces")) {
|
||||
prefix = "/spaces";
|
||||
updatedPath = currentPath.replace("/spaces", "");
|
||||
}
|
||||
|
||||
window.location.replace(`${prefix}${updatedPath ? `?next_path=${updatedPath}` : ""}`);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { APIService } from "../api.service";
|
||||
* Provides methods for user authentication, password management, and session handling
|
||||
* @extends {APIService}
|
||||
*/
|
||||
export default class AuthService extends APIService {
|
||||
export class AuthService extends APIService {
|
||||
/**
|
||||
* Creates an instance of AuthService
|
||||
* Initializes with the base API URL
|
||||
@@ -22,9 +22,10 @@ export default class AuthService extends APIService {
|
||||
* Requests a CSRF token for form submission security
|
||||
* @returns {Promise<ICsrfTokenData>} Object containing the CSRF token
|
||||
* @throws {Error} Throws the complete error object if the request fails
|
||||
* @remarks This method uses the validateStatus: null option to bypass interceptors for unauthorized errors.
|
||||
*/
|
||||
async requestCSRFToken(): Promise<ICsrfTokenData> {
|
||||
return this.get("/auth/get-csrf-token/")
|
||||
return this.get("/auth/get-csrf-token/", { validateStatus: null })
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./auth.service";
|
||||
export * from "./sites-auth.service";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
// types
|
||||
import { IEmailCheckData, IEmailCheckResponse } from "@plane/types";
|
||||
// services
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for handling authentication-related operations for Plane space application
|
||||
* Provides methods for user authentication, password management, and session handling
|
||||
* @extends {APIService}
|
||||
* @remarks This service is only available for plane sites
|
||||
*/
|
||||
export class SitesAuthService extends APIService {
|
||||
/**
|
||||
* Creates an instance of SitesAuthService
|
||||
* Initializes with the base API URL
|
||||
*/
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an email exists in the system
|
||||
* @param {IEmailCheckData} data - Email data to verify
|
||||
* @returns {Promise<IEmailCheckResponse>} Response indicating email status
|
||||
* @throws {Error} Throws response data if the request fails
|
||||
*/
|
||||
async emailCheck(data: IEmailCheckData): Promise<IEmailCheckResponse> {
|
||||
return this.post("/auth/spaces/email-check/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique code for magic link authentication
|
||||
* @param {{ email: string }} data - Object containing the email address
|
||||
* @returns {Promise<any>} Response containing the generated unique code
|
||||
* @throws {Error} Throws response data if the request fails
|
||||
*/
|
||||
async generateUniqueCode(data: { email: string }): Promise<any> {
|
||||
return this.post("/auth/spaces/magic-generate/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import type { TCycleDistribution, TProgressSnapshot, TCycleEstimateDistribution } from "@plane/types";
|
||||
import { APIService } from "@/api.service";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing cycles within a workspace and project context.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { ICycle } from "@plane/types";
|
||||
import { APIService } from "@/api.service";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing archived cycles in a project
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { APIService } from "@/api.service";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export class CycleOperationsService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import type { CycleDateCheckData, ICycle, TIssuesResponse, IWorkspaceActiveCyclesResponse } from "@plane/types";
|
||||
import { APIService } from "@/api.service";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing cycles within a workspace and project context.
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./cycle-analytics.service";
|
||||
export * from "./cycle-archive.service";
|
||||
export * from "./cycle-operations.service";
|
||||
export * from "./cycle.service";
|
||||
export * from "./sites-cycle.service";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { TPublicCycle } from "@plane/types";
|
||||
// api service
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing cycles within plane sites application.
|
||||
* Extends APIService to handle HTTP requests to the cycle-related endpoints.
|
||||
* @extends {APIService}
|
||||
* @remarks This service is only available for plane sites
|
||||
*/
|
||||
export class SitesCycleService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves list of cycles for a specific anchor.
|
||||
* @param anchor - The anchor identifier for the published entity
|
||||
* @returns {Promise<TPublicCycle[]>} The list of cycles
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async list(anchor: string): Promise<TPublicCycle[]> {
|
||||
return this.get(`/api/public/anchor/${anchor}/cycles/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { IApiToken } from "@plane/types";
|
||||
import { APIService } from "@/api.service";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export class APITokenService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
|
||||
+22
-4
@@ -1,7 +1,12 @@
|
||||
import axios from "axios";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
// api service
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for handling file upload operations
|
||||
* Handles file uploads
|
||||
* @extends {APIService}
|
||||
*/
|
||||
export class FileUploadService extends APIService {
|
||||
private cancelSource: any;
|
||||
|
||||
@@ -9,7 +14,17 @@ export class FileUploadService extends APIService {
|
||||
super("");
|
||||
}
|
||||
|
||||
async uploadFile(url: string, data: FormData): Promise<void> {
|
||||
/**
|
||||
* Uploads a file to the specified signed URL
|
||||
* @param {string} url - The URL to upload the file to
|
||||
* @param {FormData} data - The form data to upload
|
||||
* @returns {Promise<void>} Promise resolving to void
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async uploadFile(
|
||||
url: string,
|
||||
data: FormData,
|
||||
): Promise<void> {
|
||||
this.cancelSource = axios.CancelToken.source();
|
||||
return this.post(url, data, {
|
||||
headers: {
|
||||
@@ -28,7 +43,10 @@ export class FileUploadService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the upload
|
||||
*/
|
||||
cancelUpload() {
|
||||
this.cancelSource.cancel("Upload canceled");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
// api service
|
||||
import { APIService } from "../api.service";
|
||||
// helpers
|
||||
import { getAssetIdFromUrl } from "./helper";
|
||||
|
||||
/**
|
||||
* Service class for managing file operations within plane applications.
|
||||
* Extends APIService to handle HTTP requests to the file-related endpoints.
|
||||
* @extends {APIService}
|
||||
*/
|
||||
export class FileService extends APIService {
|
||||
/**
|
||||
* Creates an instance of FileService
|
||||
* @param {string} BASE_URL - The base URL for API requests
|
||||
*/
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a new asset
|
||||
* @param {string} assetPath - The asset path
|
||||
* @returns {Promise<void>} Promise resolving to void
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async deleteNewAsset(assetPath: string): Promise<void> {
|
||||
return this.delete(assetPath)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an old editor asset
|
||||
* @param {string} workspaceId - The workspace identifier
|
||||
* @param {string} src - The asset source
|
||||
* @returns {Promise<any>} Promise resolving to void
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async deleteOldEditorAsset(workspaceId: string, src: string): Promise<any> {
|
||||
const assetKey = getAssetIdFromUrl(src);
|
||||
return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetKey}/`)
|
||||
.then((response) => response?.status)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores an old editor asset
|
||||
* @param {string} workspaceId - The workspace identifier
|
||||
* @param {string} src - The asset source
|
||||
* @returns {Promise<void>} Promise resolving to void
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async restoreOldEditorAsset(workspaceId: string, src: string): Promise<void> {
|
||||
const assetKey = getAssetIdFromUrl(src);
|
||||
return this.post(`/api/workspaces/file-assets/${workspaceId}/${assetKey}/restore/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types";
|
||||
|
||||
/**
|
||||
* @description from the provided signed URL response, generate a payload to be used to upload the file
|
||||
* @param {TFileSignedURLResponse} signedURLResponse
|
||||
* @param {File} file
|
||||
* @returns {FormData} file upload request payload
|
||||
*/
|
||||
export const generateFileUploadPayload = (signedURLResponse: TFileSignedURLResponse, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
Object.entries(signedURLResponse.upload_data.fields).forEach(([key, value]) => formData.append(key, value));
|
||||
formData.append("file", file);
|
||||
return formData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns the necessary file meta data to upload a file
|
||||
* @param {File} file
|
||||
* @returns {TFileMetaDataLite} payload with file info
|
||||
*/
|
||||
export const getFileMetaDataForUpload = (file: File): TFileMetaDataLite => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description this function returns the assetId from the asset source
|
||||
* @param {string} src
|
||||
* @returns {string} assetId
|
||||
*/
|
||||
export const getAssetIdFromUrl = (src: string): string => {
|
||||
const sourcePaths = src.split("/");
|
||||
const assetUrl = sourcePaths[sourcePaths.length - 1];
|
||||
return assetUrl;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./file-upload.service";
|
||||
export * from "./sites-file.service";
|
||||
export * from "./file.service";
|
||||
+52
-34
@@ -1,22 +1,40 @@
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
// local services
|
||||
import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
|
||||
import { FileUploadService } from "./file-upload.service";
|
||||
// helpers
|
||||
import { generateFileUploadPayload, getAssetIdFromUrl, getFileMetaDataForUpload } from "@/helpers/file.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
import { FileUploadService } from "@/services/file-upload.service";
|
||||
import { FileService } from "./file.service";
|
||||
import { generateFileUploadPayload, getAssetIdFromUrl, getFileMetaDataForUpload } from "./helper";
|
||||
|
||||
export class FileService extends APIService {
|
||||
/**
|
||||
* Service class for managing file operations within plane sites application.
|
||||
* Extends FileService to manage file-related operations.
|
||||
* @extends {FileService}
|
||||
* @remarks This service is only available for plane sites
|
||||
*/
|
||||
export class SitesFileService extends FileService {
|
||||
private cancelSource: any;
|
||||
fileUploadService: FileUploadService;
|
||||
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
/**
|
||||
* Creates an instance of SitesFileService
|
||||
* @param {string} BASE_URL - The base URL for API requests
|
||||
*/
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
this.cancelUpload = this.cancelUpload.bind(this);
|
||||
// services
|
||||
this.fileUploadService = new FileUploadService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the upload status of an asset
|
||||
* @param {string} anchor - The anchor identifier
|
||||
* @param {string} assetId - The asset identifier
|
||||
* @returns {Promise<void>} Promise resolving to void
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
private async updateAssetUploadStatus(anchor: string, assetId: string): Promise<void> {
|
||||
return this.patch(`/api/public/assets/v2/anchor/${anchor}/${assetId}/`)
|
||||
.then((response) => response?.data)
|
||||
@@ -25,6 +43,14 @@ export class FileService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the upload status of multiple assets
|
||||
* @param {string} anchor - The anchor identifier
|
||||
* @param {string} entityId - The entity identifier
|
||||
* @param {Object} data - The data payload
|
||||
* @returns {Promise<void>} Promise resolving to void
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async updateBulkAssetsUploadStatus(
|
||||
anchor: string,
|
||||
entityId: string,
|
||||
@@ -39,6 +65,14 @@ export class FileService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the specified anchor
|
||||
* @param {string} anchor - The anchor identifier
|
||||
* @param {TFileEntityInfo} data - The data payload
|
||||
* @param {File} file - The file to upload
|
||||
* @returns {Promise<TFileSignedURLResponse>} Promise resolving to the signed URL response
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async uploadAsset(anchor: string, data: TFileEntityInfo, file: File): Promise<TFileSignedURLResponse> {
|
||||
const fileMetaData = getFileMetaDataForUpload(file);
|
||||
return this.post(`/api/public/assets/v2/anchor/${anchor}/`, {
|
||||
@@ -57,23 +91,13 @@ export class FileService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteNewAsset(assetPath: string): Promise<void> {
|
||||
return this.delete(assetPath)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteOldEditorAsset(workspaceId: string, src: string): Promise<any> {
|
||||
const assetKey = getAssetIdFromUrl(src);
|
||||
return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetKey}/`)
|
||||
.then((response) => response?.status)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a new asset
|
||||
* @param {string} workspaceSlug - The workspace slug
|
||||
* @param {string} src - The asset source
|
||||
* @returns {Promise<void>} Promise resolving to void
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async restoreNewAsset(workspaceSlug: string, src: string): Promise<void> {
|
||||
// remove the last slash and get the asset id
|
||||
const assetId = getAssetIdFromUrl(src);
|
||||
@@ -84,16 +108,10 @@ export class FileService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async restoreOldEditorAsset(workspaceId: string, src: string): Promise<void> {
|
||||
const assetKey = getAssetIdFromUrl(src);
|
||||
return this.post(`/api/workspaces/file-assets/${workspaceId}/${assetKey}/restore/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the upload
|
||||
*/
|
||||
cancelUpload() {
|
||||
this.cancelSource.cancel("Upload cancelled");
|
||||
this.cancelSource.cancelUpload();
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,7 @@ export * from "./module";
|
||||
export * from "./user";
|
||||
export * from "./project";
|
||||
export * from "./workspace";
|
||||
export * from "./file";
|
||||
export * from "./label";
|
||||
export * from "./state";
|
||||
export * from "./issue";
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import type { IInstanceInfo, TPage } from "@plane/types";
|
||||
import { APIService } from "@/api.service";
|
||||
import type {
|
||||
IFormattedInstanceConfiguration,
|
||||
IInstance,
|
||||
IInstanceAdmin,
|
||||
IInstanceConfiguration,
|
||||
IInstanceInfo,
|
||||
TPage,
|
||||
} from "@plane/types";
|
||||
// api service
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing instance-related operations
|
||||
* Handles retrieval of instance information and changelog
|
||||
* @extends {APIService}
|
||||
*/
|
||||
export default class InstanceService extends APIService {
|
||||
export class InstanceService extends APIService {
|
||||
/**
|
||||
* Creates an instance of InstanceService
|
||||
* Initializes the service with the base API URL
|
||||
@@ -20,12 +29,13 @@ export default class InstanceService extends APIService {
|
||||
* Retrieves information about the current instance
|
||||
* @returns {Promise<IInstanceInfo>} Promise resolving to instance information
|
||||
* @throws {Error} If the API request fails
|
||||
* @remarks This method uses the validateStatus: null option to bypass interceptors for unauthorized errors.
|
||||
*/
|
||||
async info(): Promise<IInstanceInfo> {
|
||||
return this.get("/api/instances/")
|
||||
return this.get("/api/instances/", { validateStatus: null })
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,7 +48,78 @@ export default class InstanceService extends APIService {
|
||||
return this.get("/api/instances/changelog/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of instance admins
|
||||
* @returns {Promise<IInstanceAdmin[]>} Promise resolving to an array of instance admins
|
||||
* @throws {Error} If the API request fails
|
||||
* @remarks This method uses the validateStatus: null option to bypass interceptors for unauthorized errors.
|
||||
*/
|
||||
async admins(): Promise<IInstanceAdmin[]> {
|
||||
return this.get("/api/instances/admins/", { validateStatus: null })
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the instance information
|
||||
* @param {Partial<IInstance>} data Data to update the instance with
|
||||
* @returns {Promise<IInstance>} Promise resolving to the updated instance information
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async update(data: Partial<IInstance>): Promise<IInstance> {
|
||||
return this.patch("/api/instances/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of instance configurations
|
||||
* @returns {Promise<IInstanceConfiguration[]>} Promise resolving to an array of instance configurations
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async configurations(): Promise<IInstanceConfiguration[]> {
|
||||
return this.get("/api/instances/configurations/")
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the instance configurations
|
||||
* @param {Partial<IFormattedInstanceConfiguration>} data Data to update the instance configurations with
|
||||
* @returns {Promise<IInstanceConfiguration[]>} The updated instance configurations
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async updateConfigurations(data: Partial<IFormattedInstanceConfiguration>): Promise<IInstanceConfiguration[]> {
|
||||
return this.patch("/api/instances/configurations/", data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a test email to the specified receiver to test SMTP configuration
|
||||
* @param {string} receiverEmail Email address to send the test email to
|
||||
* @returns {Promise<void>} Promise resolving to void
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async sendTestEmail(receiverEmail: string): Promise<void> {
|
||||
return this.post("/api/instances/email-credentials-check/", {
|
||||
receiver_email: receiverEmail,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { APIService } from "@/api.service";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export default class IntakeService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user