Compare commits

..
66 changed files with 320 additions and 16535 deletions
+2 -1
View File
@@ -75,7 +75,8 @@ package-lock.json
# lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
pnpm-workspace.yaml
.npmrc
.secrets
+1 -1
View File
@@ -72,7 +72,7 @@ docker compose -f docker-compose-local.yml up
4. Start web apps:
```bash
pnpm dev
yarn dev
```
5. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
+10 -10
View File
@@ -16,13 +16,13 @@
},
"dependencies": {
"@headlessui/react": "^1.7.19",
"@plane/constants": "workspace:*",
"@plane/hooks": "workspace:*",
"@plane/propel": "workspace:*",
"@plane/services": "workspace:*",
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@plane/constants": "*",
"@plane/hooks": "*",
"@plane/propel": "*",
"@plane/services": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
@@ -42,9 +42,9 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.16.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
+16 -4
View File
@@ -341,7 +341,10 @@ class ProjectViewSet(BaseViewSet):
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The project name is already taken"},
{
"name": "The project name is already taken",
"code": "PROJECT_NAME_ALREADY_EXIST",
},
status=status.HTTP_409_CONFLICT,
)
except Workspace.DoesNotExist:
@@ -350,7 +353,10 @@ class ProjectViewSet(BaseViewSet):
)
except serializers.ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
{
"identifier": "The project identifier is already taken",
"code": "PROJECT_IDENTIFIER_ALREADY_EXIST",
},
status=status.HTTP_409_CONFLICT,
)
@@ -419,7 +425,10 @@ class ProjectViewSet(BaseViewSet):
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The project name is already taken"},
{
"name": "The project name is already taken",
"code": "PROJECT_NAME_ALREADY_EXIST",
},
status=status.HTTP_409_CONFLICT,
)
except (Project.DoesNotExist, Workspace.DoesNotExist):
@@ -428,7 +437,10 @@ class ProjectViewSet(BaseViewSet):
)
except serializers.ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
{
"identifier": "The project identifier is already taken",
"code": "PROJECT_IDENTIFIER_ALREADY_EXIST",
},
status=status.HTTP_409_CONFLICT,
)
+1 -1
View File
@@ -9,4 +9,4 @@ factory-boy==3.3.0
freezegun==1.2.2
coverage==7.2.7
httpx==0.24.1
requests==2.32.4
requests==2.32.2
+3 -3
View File
@@ -20,9 +20,9 @@
"@hocuspocus/extension-logger": "^2.15.0",
"@hocuspocus/extension-redis": "^2.15.0",
"@hocuspocus/server": "^2.15.0",
"@plane/constants": "workspace:*",
"@plane/editor": "workspace:*",
"@plane/types": "workspace:*",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/types": "*",
"@tiptap/core": "2.10.4",
"@tiptap/html": "2.11.0",
"axios": "^1.8.3",
+16 -1
View File
@@ -5,6 +5,13 @@
"version": "0.26.1",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
"web",
"space",
"admin",
"live",
"packages/*"
],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --concurrency=13",
@@ -19,5 +26,13 @@
"prettier-plugin-tailwindcss": "^0.5.4",
"turbo": "^2.5.4"
},
"packageManager": "pnpm@10.12.1"
"resolutions": {
"nanoid": "3.3.8",
"esbuild": "0.25.0",
"@babel/helpers": "7.26.10",
"@babel/runtime": "7.26.10",
"chokidar": "3.6.0",
"tar-fs": "3.0.9"
},
"packageManager": "yarn@1.22.22"
}
+39 -1
View File
@@ -136,7 +136,45 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state", "cycle", "module", "priority", "labels", "assignees", "created_by", null],
group_by: [
"state",
"cycle",
"module",
"state_detail.group",
"priority",
"labels",
"assignees",
"created_by",
null,
],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
draft_issues: {
list: {
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
kanban: {
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
+2 -2
View File
@@ -21,8 +21,8 @@
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/typescript-config": "*",
"@types/express": "^4.17.21",
"@types/node": "^20.14.9",
"@types/reflect-metadata": "^0.1.0",
+22 -23
View File
@@ -36,27 +36,27 @@
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@hocuspocus/provider": "^2.15.0",
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@tiptap/core": "2.10.4",
"@tiptap/extension-blockquote": "2.10.4",
"@tiptap/extension-character-count": "2.10.4",
"@tiptap/extension-collaboration": "2.10.4",
"@tiptap/extension-image": "2.10.4",
"@tiptap/extension-list-item": "2.10.4",
"@tiptap/extension-mention": "2.10.4",
"@tiptap/extension-placeholder": "2.10.4",
"@tiptap/extension-task-item": "2.10.4",
"@tiptap/extension-task-list": "2.10.4",
"@tiptap/extension-text-align": "2.10.4",
"@tiptap/extension-text-style": "2.10.4",
"@tiptap/extension-underline": "2.10.4",
"@tiptap/html": "2.10.4",
"@tiptap/pm": "2.10.4",
"@tiptap/react": "2.10.4",
"@tiptap/starter-kit": "2.10.4",
"@tiptap/suggestion": "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",
@@ -64,7 +64,6 @@
"lowlight": "^3.0.0",
"lucide-react": "^0.469.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-model": "^1.25.1",
"prosemirror-utils": "^1.2.2",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.10",
@@ -75,9 +74,9 @@
"yjs": "^13.6.20"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.15.3",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
+1 -5
View File
@@ -1,5 +1,4 @@
import { EditorState, Selection } from "@tiptap/pm/state";
import { Node as ProsemirrorNode } from "prosemirror-model";
// plane imports
import { cn } from "@plane/utils";
// constants
@@ -22,10 +21,7 @@ export const getEditorClassNames = ({ noBorder, borderOnFocus, containerClassNam
);
// Helper function to find the parent node of a specific type
export function findParentNodeOfType(selection: Selection, typeName: string): {
node: ProsemirrorNode;
pos: number;
} | null {
export function findParentNodeOfType(selection: Selection, typeName: string) {
let depth = selection.$anchor.depth;
while (depth > 0) {
const node = selection.$anchor.node(depth);
@@ -14,16 +14,21 @@ export const setText = (editor: Editor, range?: Range) => {
export const toggleHeading = (editor: Editor, level: 1 | 2 | 3 | 4 | 5 | 6, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode(CORE_EXTENSIONS.HEADING, { level }).run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleHeading({ level }).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();
};
@@ -62,12 +67,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();
};
@@ -77,7 +86,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();
};
@@ -12,7 +12,9 @@ import {
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);
/**
@@ -54,6 +56,7 @@ export const convertBase64StringToBinaryData = (document: string): ArrayBuffer =
*/
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");
@@ -69,6 +72,7 @@ export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: strin
*/
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");
@@ -97,6 +101,7 @@ export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
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 {
@@ -126,6 +131,7 @@ export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
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 {
+1 -2
View File
@@ -19,8 +19,7 @@
"react": "^18.3.1"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"@types/react": "^18.3.11",
"tsup": "8.4.0",
+2 -2
View File
@@ -11,11 +11,11 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"@plane/utils": "workspace:*",
"@plane/utils": "*",
"intl-messageformat": "^10.7.11"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Nepodařilo se odstranit projekt z oblíbených. Zkuste to prosím znovu.",
"project_created_successfully": "Projekt úspěšně vytvořen",
"project_created_successfully_description": "Projekt byl úspěšně vytvořen. Nyní můžete začít přidávat pracovní položky.",
"project_name_already_taken": "Název projektu už je zabraný.",
"project_identifier_already_taken": "Identifikátor projektu už je zabraný.",
"project_cover_image_alt": "Úvodní obrázek projektu",
"name_is_required": "Název je povinný",
"title_should_be_less_than_255_characters": "Název by měl být kratší než 255 znaků",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Projekt konnte nicht aus den Favoriten entfernt werden. Bitte versuchen Sie es erneut.",
"project_created_successfully": "Projekt erfolgreich erstellt",
"project_created_successfully_description": "Das Projekt wurde erfolgreich erstellt. Sie können nun Arbeitselemente hinzufügen.",
"project_name_already_taken": "Der Projektname ist bereits vergeben.",
"project_identifier_already_taken": "Der Projekt-Identifier ist bereits vergeben.",
"project_cover_image_alt": "Titelbild des Projekts",
"name_is_required": "Name ist erforderlich",
"title_should_be_less_than_255_characters": "Der Titel sollte weniger als 255 Zeichen enthalten",
@@ -2466,4 +2468,4 @@
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane ist nicht gestartet. Dies könnte daran liegen, dass einer oder mehrere Plane-Services nicht starten konnten.",
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wählen Sie View Logs aus setup.sh und Docker-Logs, um sicherzugehen."
}
}
}
@@ -153,6 +153,8 @@
"failed_to_remove_project_from_favorites": "Couldn't remove the project from favorites. Please try again.",
"project_created_successfully": "Project created successfully",
"project_created_successfully_description": "Project created successfully. You can now start adding work items to it.",
"project_name_already_taken": "The project name is already taken.",
"project_identifier_already_taken": "The project identifier is already taken.",
"project_cover_image_alt": "Project cover image",
"name_is_required": "Name is required",
"title_should_be_less_than_255_characters": "Title should be less than 255 characters",
@@ -318,6 +318,8 @@
"failed_to_remove_project_from_favorites": "No se pudo eliminar el proyecto de favoritos. Por favor, inténtalo de nuevo.",
"project_created_successfully": "Proyecto creado exitosamente",
"project_created_successfully_description": "Proyecto creado exitosamente. Ahora puedes comenzar a agregar elementos de trabajo.",
"project_name_already_taken": "El nombre del proyecto ya está en uso.",
"project_identifier_already_taken": "El identificador del proyecto ya está en uso.",
"project_cover_image_alt": "Imagen de portada del proyecto",
"name_is_required": "El nombre es requerido",
"title_should_be_less_than_255_characters": "El título debe tener menos de 255 caracteres",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.",
"project_created_successfully": "Projet créé avec succès",
"project_created_successfully_description": "Projet créé avec succès. Vous pouvez maintenant commencer à ajouter des éléments de travail.",
"project_name_already_taken": "Le nom du projet est déjà pris.",
"project_identifier_already_taken": "Lidentifiant du projet est déjà pris.",
"project_cover_image_alt": "Image de couverture du projet",
"name_is_required": "Le nom est requis",
"title_should_be_less_than_255_characters": "Le titre doit faire moins de 255 caractères",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Tidak dapat menghapus proyek dari favorit. Silakan coba lagi.",
"project_created_successfully": "Proyek berhasil dibuat",
"project_created_successfully_description": "Proyek berhasil dibuat. Anda sekarang dapat mulai menambahkan item kerja ke dalamnya.",
"project_name_already_taken": "Nama project sudah digunakan.",
"project_identifier_already_taken": "Identifier project sudah digunakan.",
"project_cover_image_alt": "Gambar sampul proyek",
"name_is_required": "Nama diperlukan",
"title_should_be_less_than_255_characters": "Judul harus kurang dari 255 karakter",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Impossibile rimuovere il progetto dai preferiti. Per favore, riprova.",
"project_created_successfully": "Progetto creato con successo",
"project_created_successfully_description": "Progetto creato con successo. Ora puoi iniziare ad aggiungere elementi di lavoro.",
"project_name_already_taken": "Il nome del progetto è già stato utilizzato.",
"project_identifier_already_taken": "L'identificatore del progetto è già stato utilizzato.",
"project_cover_image_alt": "Immagine di copertina del progetto",
"name_is_required": "Il nome è obbligatorio",
"title_should_be_less_than_255_characters": "Il titolo deve contenere meno di 255 caratteri",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "プロジェクトをお気に入りから削除できませんでした。もう一度お試しください。",
"project_created_successfully": "プロジェクトが正常に作成されました",
"project_created_successfully_description": "プロジェクトが正常に作成されました。作業項目を追加できるようになりました。",
"project_name_already_taken": "プロジェクト名は既に使用されています。",
"project_identifier_already_taken": "プロジェクト識別子は既に使用されています。",
"project_cover_image_alt": "プロジェクトのカバー画像",
"name_is_required": "名前は必須です",
"title_should_be_less_than_255_characters": "タイトルは255文字未満である必要があります",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "프로젝트를 즐겨찾기에서 제거하지 못했습니다. 다시 시도해주세요.",
"project_created_successfully": "프로젝트가 성공적으로 생성되었습니다",
"project_created_successfully_description": "프로젝트가 성공적으로 생성되었습니다. 이제 작업 항목을 추가할 수 있습니다.",
"project_name_already_taken": "프로젝트 이름이 이미 사용 중입니다.",
"project_identifier_already_taken": "프로젝트 식별자가 이미 사용 중입니다.",
"project_cover_image_alt": "프로젝트 커버 이미지",
"name_is_required": "이름이 필요합니다",
"title_should_be_less_than_255_characters": "제목은 255자 미만이어야 합니다",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Nie udało się usunąć projektu z ulubionych. Spróbuj ponownie.",
"project_created_successfully": "Projekt utworzono pomyślnie",
"project_created_successfully_description": "Projekt został pomyślnie utworzony. Teraz możesz dodawać elementy pracy.",
"project_name_already_taken": "Nazwa projektu jest już zajęta.",
"project_identifier_already_taken": "Identyfikator projektu jest już zajęty.",
"project_cover_image_alt": "Obraz w tle projektu",
"name_is_required": "Nazwa jest wymagana",
"title_should_be_less_than_255_characters": "Nazwa musi mieć mniej niż 255 znaków",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Não foi possível remover o projeto dos favoritos. Por favor, tente novamente.",
"project_created_successfully": "Projeto criado com sucesso",
"project_created_successfully_description": "Projeto criado com sucesso. Agora você pode começar a adicionar itens de trabalho a ele.",
"project_name_already_taken": "O nome do projeto já está em uso.",
"project_identifier_already_taken": "O identificador do projeto já está em uso.",
"project_cover_image_alt": "Imagem de capa do projeto",
"name_is_required": "Nome é obrigatório",
"title_should_be_less_than_255_characters": "O título deve ter menos de 255 caracteres",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Nu s-a putut elimina proiectul din favorite. Încearcă din nou.",
"project_created_successfully": "Proiect creat cu succes",
"project_created_successfully_description": "Proiect creat cu succes. Poți începe să adaugi activități în el.",
"project_name_already_taken": "Numele proiectului este deja folosit.",
"project_identifier_already_taken": "Identificatorul proiectului este deja folosit.",
"project_cover_image_alt": "Coperta proiectului",
"name_is_required": "Numele este obligatoriu",
"title_should_be_less_than_255_characters": "Titlul trebuie să conțină mai puțin de 255 de caractere",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Не удалось удалить проект из избранного. Попробуйте снова.",
"project_created_successfully": "Проект успешно создан",
"project_created_successfully_description": "Проект успешно создан. Теперь вы можете добавлять рабочие элементы.",
"project_name_already_taken": "Имя проекта уже используется.",
"project_identifier_already_taken": "Идентификатор проекта уже используется.",
"project_cover_image_alt": "Обложка проекта",
"name_is_required": "Требуется имя",
"title_should_be_less_than_255_characters": "Заголовок должен быть короче 255 символов",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Nepodarilo sa odstrániť projekt z obľúbených. Skúste to prosím znova.",
"project_created_successfully": "Projekt bol úspešne vytvorený",
"project_created_successfully_description": "Projekt bol úspešne vytvorený. Teraz môžete začať pridávať pracovné položky.",
"project_name_already_taken": "Názov projektu je už použitý.",
"project_identifier_already_taken": "Identifikátor projektu je už použitý.",
"project_cover_image_alt": "Úvodný obrázok projektu",
"name_is_required": "Názov je povinný",
"title_should_be_less_than_255_characters": "Názov by mal byť kratší ako 255 znakov",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Proje favorilerden kaldırılamadı. Lütfen tekrar deneyin.",
"project_created_successfully": "Proje başarıyla oluşturuldu",
"project_created_successfully_description": "Proje başarıyla oluşturuldu. Artık iş öğeleri eklemeye başlayabilirsiniz.",
"project_name_already_taken": "Proje ismi zaten kullanılıyor.",
"project_identifier_already_taken": "Proje kimliği zaten kullanılıyor.",
"project_cover_image_alt": "Proje kapak resmi",
"name_is_required": "Ad gereklidir",
"title_should_be_less_than_255_characters": "Başlık 255 karakterden az olmalı",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Не вдалося видалити проєкт із вибраного. Спробуйте ще раз.",
"project_created_successfully": "Проєкт успішно створено",
"project_created_successfully_description": "Проєкт успішно створений. Тепер ви можете почати додавати робочі одиниці.",
"project_name_already_taken": "Назва проекту вже використовується.",
"project_identifier_already_taken": "Ідентифікатор проекту вже використовується.",
"project_cover_image_alt": "Обкладинка проєкту",
"name_is_required": "Назва є обов’язковою",
"title_should_be_less_than_255_characters": "Назва має бути коротшою за 255 символів",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "Không thể xóa dự án khỏi mục yêu thích. Vui lòng thử lại.",
"project_created_successfully": "Dự án đã được tạo thành công",
"project_created_successfully_description": "Dự án đã được tạo thành công. Bây giờ bạn có thể bắt đầu thêm mục công việc.",
"project_name_already_taken": "Tên project đã được sử dụng.",
"project_identifier_already_taken": "Identifier project đã được sử dụng.",
"project_cover_image_alt": "Ảnh bìa dự án",
"name_is_required": "Tên là bắt buộc",
"title_should_be_less_than_255_characters": "Tiêu đề phải ít hơn 255 ký tự",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "无法从收藏中移除项目。请重试。",
"project_created_successfully": "项目创建成功",
"project_created_successfully_description": "项目创建成功。您现在可以开始添加工作项了。",
"project_name_already_taken": "项目名称已被使用。",
"project_identifier_already_taken": "项目标识符已被使用。",
"project_cover_image_alt": "项目封面图片",
"name_is_required": "名称为必填项",
"title_should_be_less_than_255_characters": "标题应少于255个字符",
@@ -316,6 +316,8 @@
"failed_to_remove_project_from_favorites": "無法從我的最愛移除專案。請再試一次。",
"project_created_successfully": "專案建立成功",
"project_created_successfully_description": "專案建立成功。您現在可以開始新增工作事項。",
"project_name_already_taken": "專案名稱已被使用。",
"project_identifier_already_taken": "專案識別碼已被使用。",
"project_cover_image_alt": "專案封面圖片",
"name_is_required": "名稱為必填",
"title_should_be_less_than_255_characters": "標題不應超過 255 個字元",
+1 -1
View File
@@ -15,7 +15,7 @@
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
+3 -3
View File
@@ -24,9 +24,9 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"typescript": "^5.3.3"
+1 -1
View File
@@ -9,7 +9,7 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"@plane/constants": "workspace:*",
"@plane/constants": "*",
"axios": "^1.8.3"
}
}
+1 -1
View File
@@ -14,7 +14,7 @@
"zod": "^3.22.2"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
+8 -10
View File
@@ -26,15 +26,13 @@
"react-dom": "^18.3.1"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.3",
"@plane/constants": "workspace:*",
"@plane/hooks": "workspace:*",
"@plane/types": "workspace:*",
"@plane/utils": "workspace:*",
"@plane/hooks": "*",
"@plane/utils": "*",
"@popperjs/core": "^2.11.8",
"@radix-ui/react-scroll-area": "^1.2.3",
"clsx": "^2.0.0",
@@ -50,9 +48,9 @@
},
"devDependencies": {
"@chromatic-com/storybook": "^1.4.0",
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@storybook/addon-essentials": "^8.1.1",
"@storybook/addon-interactions": "^8.1.1",
"@storybook/addon-links": "^8.1.1",
@@ -60,14 +58,14 @@
"@storybook/addon-styling-webpack": "^1.0.0",
"@storybook/addon-webpack5-compiler-swc": "^1.0.2",
"@storybook/blocks": "^8.1.1",
"@storybook/react-webpack5": "^8.1.1",
"@storybook/react": "^8.1.1",
"@storybook/react-webpack5": "^8.1.1",
"@storybook/test": "^8.1.1",
"@types/lodash": "^4.17.6",
"@types/node": "^20.5.2",
"@types/react": "^18.3.11",
"@types/react-color": "^3.0.9",
"@types/react-dom": "^18.2.18",
"@types/react": "^18.3.11",
"autoprefixer": "^10.4.19",
"classnames": "^2.3.2",
"postcss-cli": "^11.0.0",
+1 -6
View File
@@ -16,21 +16,16 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"@plane/constants": "workspace:*",
"@plane/types": "workspace:*",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"isomorphic-dompurify": "^2.16.0",
"lodash": "^4.17.21",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"tailwind-merge": "^2.5.5",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@types/lodash": "^4.17.17",
"@plane/eslint-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"@types/react": "^18.3.11",
"@types/zxcvbn": "^4.4.5",
-16263
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
packages:
- web
- space
- admin
- live
- packages/*
onlyBuiltDependencies:
- '@swc/core'
- core-js
- esbuild
- sharp
- unrs-resolver
@@ -40,7 +40,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
const [isFocused, setIsFocused] = useState(true);
const [isFocused, setIsFocused] = useState(true)
const inputRef = useRef<HTMLInputElement>(null);
return (
@@ -54,12 +54,9 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
`relative flex items-center rounded-md bg-onboarding-background-200 border`,
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100`
)}
onFocus={() => {
setIsFocused(true);
}}
onBlur={() => {
setIsFocused(false);
}}
tabIndex={-1}
onFocus={() => {setIsFocused(true)}}
onBlur={() => {setIsFocused(false)}}
>
<Input
id="email"
@@ -73,18 +70,14 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
autoFocus
ref={inputRef}
/>
{email.length > 0 && (
<button
type="button"
aria-label="Clear email"
{email.length > 0 && (
<XCircle
className="h-[46px] w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs"
onClick={() => {
setEmail("");
inputRef.current?.focus();
}}
tabIndex={-1}
>
<XCircle className="h-[46px] w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs" />
</button>
/>
)}
</div>
{emailError?.email && !isFocused && (
@@ -99,4 +92,4 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
</Button>
</form>
);
});
});
+10 -10
View File
@@ -19,13 +19,13 @@
"@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.7.13",
"@mui/material": "^5.14.1",
"@plane/constants": "workspace:*",
"@plane/editor": "workspace:*",
"@plane/i18n": "workspace:*",
"@plane/propel": "workspace:*",
"@plane/services": "workspace:*",
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/i18n": "*",
"@plane/propel": "*",
"@plane/services": "*",
"@plane/types": "*",
"@plane/ui": "*",
"axios": "^1.8.3",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
@@ -51,9 +51,9 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.17.1",
"@types/node": "18.14.1",
@@ -2,7 +2,7 @@
import { CommandPalette } from "@/components/command-palette";
import { ContentWrapper } from "@/components/core";
import { SettingsHeader } from "@/components/settings";
import { SettingsContentLayout, SettingsHeader } from "@/components/settings";
import { AuthenticationWrapper } from "@/lib/wrappers";
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
@@ -15,8 +15,8 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
{/* Header */}
<SettingsHeader />
{/* Content */}
<ContentWrapper className="px-4 md:pl-12 md:flex w-full">
<div className="w-full h-full overflow-hidden">{children}</div>
<ContentWrapper className="px-4 md:pl-12 md:pt-page-y md:flex w-full">
<SettingsContentLayout>{children}</SettingsContentLayout>
</ContentWrapper>
</main>
</WorkspaceAuthWrapper>
@@ -39,13 +39,13 @@ const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) =>
hamburgerContent={WorkspaceSettingsSidebar}
activePath={getWorkspaceActivePath(pathname) || ""}
/>
<div className="inset-y-0 flex flex-row w-full h-full">
<div className="inset-y-0 flex flex-row w-full">
{workspaceUserInfo && !isAuthorized ? (
<NotAuthorizedView section="settings" className="h-auto" />
) : (
<div className="relative flex h-full w-full">
<div className="hidden md:block">{<WorkspaceSettingsSidebar />}</div>
<div className="w-full h-full overflow-y-scroll md:pt-page-y">{children}</div>
{children}
</div>
)}
</div>
@@ -24,9 +24,7 @@ const ProfileSettingsLayout = observer((props: Props) => {
<div className="hidden md:block">
<ProfileSidebar />
</div>
<div className="w-full h-full overflow-y-scroll md:pt-page-y">
<SettingsContentWrapper>{children}</SettingsContentWrapper>
</div>
<SettingsContentWrapper>{children}</SettingsContentWrapper>
</div>
</>
);
@@ -36,7 +36,7 @@ const ProjectSettingsLayout = observer((props: Props) => {
<ProjectAuthWrapper workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()}>
<div className="relative flex h-full w-full">
<div className="hidden md:block">{projectId && <ProjectSettingsSidebar />}</div>
<div className="w-full h-full overflow-y-scroll md:pt-page-y">{children}</div>
{children}
</div>
</ProjectAuthWrapper>
</>
+2 -4
View File
@@ -47,19 +47,17 @@ const WORKSPACE_ACTION_LINKS = [
},
];
const ProjectActionIcons = ({ type, size, className = "" }: { type: string; size?: number; className?: string }) => {
export const ProjectActionIcons = ({ type, size, className }: { type: string; size?: number; className?: string }) => {
const icons = {
profile: CircleUser,
security: KeyRound,
activity: Activity,
preferences: Settings2,
appearance: Settings2,
notifications: Bell,
"api-tokens": KeyRound,
};
if (type === undefined) return null;
const Icon = icons[type as keyof typeof icons];
if (!Icon) return null;
return <Icon size={size} className={className} />;
};
export const ProfileLayoutSidebar = observer(() => {
@@ -39,7 +39,6 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
toggleDeleteIssueModal,
isBulkDeleteIssueModalOpen,
toggleBulkDeleteIssueModal,
createWorkItemAllowedProjectIds,
} = useCommandPalette();
// derived values
const issueDetails = issueId ? getIssueById(issueId) : undefined;
@@ -81,7 +80,6 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
data={getCreateIssueModalData()}
isDraft={isDraftIssue}
onSubmit={handleCreateIssueSubmit}
allowedProjectIds={createWorkItemAllowedProjectIds}
/>
{workspaceSlug && projectId && issueId && issueDetails && (
<DeleteIssueModal
@@ -4,29 +4,21 @@ import { observer } from "mobx-react-lite";
import { ISearchIssueResponse, TIssue } from "@plane/types";
// components
import { IssueModalContext } from "@/components/issues";
// hooks
import { useUser } from "@/hooks/store/user/user-user";
export type TIssueModalProviderProps = {
templateId?: string;
dataForPreload?: Partial<TIssue>;
allowedProjectIds?: string[];
children: React.ReactNode;
};
export const IssueModalProvider = observer((props: TIssueModalProviderProps) => {
const { children, allowedProjectIds } = props;
const { children } = props;
// states
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
// store hooks
const { projectsWithCreatePermissions } = useUser();
// derived values
const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {});
return (
<IssueModalContext.Provider
value={{
allowedProjectIds: allowedProjectIds ?? projectIdsWithCreatePermissions,
workItemTemplateId: null,
setWorkItemTemplateId: () => {},
isApplyingTemplate: false,
+18 -11
View File
@@ -49,7 +49,7 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
title: t("toast.error"),
message: t("failed_to_remove_project_from_favorites"),
});
});
@@ -89,20 +89,27 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
handleNextStep(res.id);
})
.catch((err) => {
Object.keys(err?.data ?? {}).map((key) => {
if (err?.data.code === "PROJECT_NAME_ALREADY_EXIST") {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: err.data[key],
title: t("toast.error"),
message: t("project_name_already_taken"),
});
captureProjectEvent({
eventName: PROJECT_CREATED,
payload: {
...formData,
state: "FAILED",
},
} else if (err?.data.code === "PROJECT_IDENTIFIER_ALREADY_EXIST") {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("project_identifier_already_taken"),
});
});
} else {
Object.keys(err?.data ?? {}).map((key) => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: err.data[key],
});
});
}
});
};
@@ -55,6 +55,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
`relative flex items-center rounded-md bg-onboarding-background-200 border`,
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100`
)}
tabIndex={-1}
onFocus={() => {
setIsFocused(true);
}}
@@ -83,7 +84,6 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
}}
className="absolute right-3 size-5 grid place-items-center"
aria-label={t("aria_labels.auth_forms.clear_email")}
tabIndex={-1}
>
<XCircle className="size-5 stroke-custom-text-400" />
</button>
+4 -10
View File
@@ -14,9 +14,8 @@ import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
// constants
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldRenderProject } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useUser } from "@/hooks/store/user/user-user";
type Props = {
handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
@@ -37,10 +36,7 @@ const defaultValues: Partial<ICycle> = {
export const CycleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props;
// plane hooks
const { t } = useTranslation();
// store hooks
const { projectsWithCreatePermissions } = useUser();
// form data
const {
formState: { errors, isSubmitting, dirtyFields },
@@ -79,14 +75,12 @@ export const CycleForm: React.FC<Props> = (props) => {
<ProjectDropdown
value={value}
onChange={(val) => {
if (!Array.isArray(val)) {
onChange(val);
setActiveProject(val);
}
onChange(val);
setActiveProject(val);
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]}
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}
/>
</div>
@@ -13,12 +13,7 @@ import { CreateIssueToastActionItems, IssuesModalProps } from "@/components/issu
// constants
// hooks
import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useCycle } from "@/hooks/store/use-cycle";
import { useEventTracker } from "@/hooks/store/use-event-tracker";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues";
import { useModule } from "@/hooks/store/use-module";
import { useProject } from "@/hooks/store/use-project";
import { useEventTracker, useCycle, useIssues, useModule, useIssueDetail, useUser, useProject } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
// services
@@ -64,13 +59,14 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
const { t } = useTranslation();
const { captureIssueEvent } = useEventTracker();
const { workspaceSlug, projectId: routerProjectId, cycleId, moduleId, workItem } = useParams();
const { projectsWithCreatePermissions } = useUser();
const { fetchCycleDetails } = useCycle();
const { fetchModuleDetails } = useModule();
const { issues } = useIssues(storeType);
const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT);
const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT);
const { fetchIssue } = useIssueDetail();
const { allowedProjectIds, handleCreateUpdatePropertyValues } = useIssueModal();
const { handleCreateUpdatePropertyValues } = useIssueModal();
const { getProjectByIdentifier } = useProject();
// pathname
const pathname = usePathname();
@@ -80,6 +76,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
const routerProjectIdentifier = workItem?.toString().split("-")[0];
const projectIdFromRouter = getProjectByIdentifier(routerProjectIdentifier)?.id;
const projectId = data?.project_id ?? routerProjectId?.toString() ?? projectIdFromRouter;
const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {});
const fetchIssueDetail = async (issueId: string | undefined) => {
setDescription(undefined);
@@ -117,9 +114,10 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
return;
}
// if data is not present, set active project to the first project in the allowedProjectIds array
if (allowedProjectIds && allowedProjectIds.length > 0 && !activeProjectId)
setActiveProjectId(projectId?.toString() ?? allowedProjectIds?.[0]);
// if data is not present, set active project to the project
// in the url. This has the least priority.
if (projectIdsWithCreatePermissions && projectIdsWithCreatePermissions.length > 0 && !activeProjectId)
setActiveProjectId(projectId?.toString() ?? projectIdsWithCreatePermissions?.[0]);
// clearing up the description state when we leave the component
return () => setDescription(undefined);
@@ -348,7 +346,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
const handleDuplicateIssueModal = (value: boolean) => setIsDuplicateModalOpen(value);
// don't open the modal if there are no projects
if (!allowedProjectIds || allowedProjectIds.length === 0 || !activeProjectId) return null;
if (!projectIdsWithCreatePermissions || projectIdsWithCreatePermissions.length === 0 || !activeProjectId) return null;
const commonIssueModalProps: IssueFormProps = {
issueTitleRef: issueTitleRef,
@@ -10,9 +10,10 @@ import { TIssue } from "@plane/types";
// components
import { ProjectDropdown } from "@/components/dropdowns";
// helpers
import { shouldRenderProject } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useIssueModal } from "@/hooks/context/use-issue-modal";
// store hooks
import { useUser } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TIssueProjectSelectProps = {
@@ -24,9 +25,8 @@ type TIssueProjectSelectProps = {
export const IssueProjectSelect: React.FC<TIssueProjectSelectProps> = observer((props) => {
const { control, disabled = false, handleFormChange } = props;
// store hooks
const { projectsWithCreatePermissions } = useUser();
const { isMobile } = usePlatformOS();
// context hooks
const { allowedProjectIds } = useIssueModal();
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
@@ -37,22 +37,26 @@ export const IssueProjectSelect: React.FC<TIssueProjectSelectProps> = observer((
rules={{
required: true,
}}
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectDropdown
value={value}
onChange={(projectId) => {
onChange(projectId);
handleFormChange();
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => allowedProjectIds.includes(project.id)}
tabIndex={getIndex("project_id")}
disabled={disabled}
/>
</div>
)}
render={({ field: { value, onChange } }) =>
projectsWithCreatePermissions && projectsWithCreatePermissions[value!] ? (
<div className="h-7">
<ProjectDropdown
value={value}
onChange={(projectId) => {
onChange(projectId);
handleFormChange();
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("project_id")}
disabled={disabled}
/>
</div>
) : (
<></>
)
}
/>
);
});
@@ -47,7 +47,6 @@ export type THandleParentWorkItemDetailsProps = {
};
export type TIssueModalContext = {
allowedProjectIds: string[];
workItemTemplateId: string | null;
setWorkItemTemplateId: React.Dispatch<React.SetStateAction<string | null>>;
isApplyingTemplate: boolean;
@@ -29,7 +29,6 @@ export interface IssuesModalProps {
};
isProjectSelectionDisabled?: boolean;
templateId?: string;
allowedProjectIds?: string[];
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
@@ -44,11 +43,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
if (!props.isOpen) return null;
return (
<IssueModalProvider
templateId={props.templateId}
dataForPreload={dataForPreload}
allowedProjectIds={props.allowedProjectIds}
>
<IssueModalProvider templateId={props.templateId} dataForPreload={dataForPreload}>
<CreateUpdateIssueModalBase {...props} />
</IssueModalProvider>
);
+3 -5
View File
@@ -13,9 +13,9 @@ import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "@/components
import { ModuleStatusSelect } from "@/components/modules";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldRenderProject } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useUser } from "@/hooks/store/user/user-user";
// types
type Props = {
handleFormSubmit: (values: Partial<IModule>, dirtyFields: any) => Promise<void>;
@@ -37,8 +37,6 @@ const defaultValues: Partial<IModule> = {
export const ModuleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props;
// store hooks
const { projectsWithCreatePermissions } = useUser();
// form info
const {
formState: { errors, isSubmitting, dirtyFields },
@@ -95,7 +93,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]}
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}
/>
</div>
@@ -12,7 +12,7 @@ export const SettingsContentWrapper = observer((props: TProps) => {
return (
<div
className={cn("flex flex-col w-full items-center mx-auto py-4 md:py-0", {
"md:p-4 max-w-[800px] 2xl:max-w-[1000px]": size === "md",
"p-4 max-w-[800px] 2xl:max-w-[1000px]": size === "md",
"md:px-16": size === "lg",
})}
>
@@ -37,7 +37,7 @@ export const SettingsSidebar = observer((props: SettingsSidebarProps) => {
return (
<div
className={cn("flex w-[250px] flex-col gap-2 flex-shrink-0 overflow-y-scroll h-full md:pt-page-y ", {
className={cn("flex w-[250px] flex-col gap-2 flex-shrink-0 pb-5", {
"absolute left-0 top-[42px] z-50 h-fit max-h-[400px] overflow-scroll bg-custom-background-100 border border-custom-border-100 rounded shadow-sm p-4":
isMobile,
})}
@@ -45,9 +45,9 @@ export const SettingsSidebar = observer((props: SettingsSidebarProps) => {
{/* Header */}
<SettingsSidebarHeader customHeader={customHeader} />
{/* Navigation */}
<div className="divide-y divide-custom-border-100 overflow-x-hidden w-full h-full overflow-y-scroll">
<div className="divide-y divide-custom-border-100 overflow-x-hidden w-full">
{categories.map((category) => (
<div key={category} className="py-3">
<div key={category} className="py-3 h-full">
<span className="text-sm font-semibold text-custom-text-350 capitalize mb-2">{t(category)}</span>
{groupedSettings[category].length > 0 && (
<div className="relative flex flex-col gap-0.5 h-full mt-2">
+2 -7
View File
@@ -26,7 +26,6 @@ export interface IBaseCommandPaletteStore {
isDeleteIssueModalOpen: boolean;
isBulkDeleteIssueModalOpen: boolean;
createIssueStoreType: TCreateModalStoreTypes;
createWorkItemAllowedProjectIds: string[] | undefined;
allStickiesModal: boolean;
projectListOpenMap: Record<string, boolean>;
getIsProjectListOpen: (projectId: string) => boolean;
@@ -37,7 +36,7 @@ export interface IBaseCommandPaletteStore {
toggleCreateCycleModal: (value?: boolean) => void;
toggleCreateViewModal: (value?: boolean) => void;
toggleCreatePageModal: (value?: TCreatePageModal) => void;
toggleCreateIssueModal: (value?: boolean, storeType?: TCreateModalStoreTypes, allowedProjectIds?: string[]) => void;
toggleCreateIssueModal: (value?: boolean, storeType?: TCreateModalStoreTypes) => void;
toggleCreateModuleModal: (value?: boolean) => void;
toggleDeleteIssueModal: (value?: boolean) => void;
toggleBulkDeleteIssueModal: (value?: boolean) => void;
@@ -58,7 +57,6 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor
isBulkDeleteIssueModalOpen: boolean = false;
createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA;
createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT;
createWorkItemAllowedProjectIds: IBaseCommandPaletteStore["createWorkItemAllowedProjectIds"] = undefined;
allStickiesModal: boolean = false;
projectListOpenMap: Record<string, boolean> = {};
@@ -76,7 +74,6 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor
isBulkDeleteIssueModalOpen: observable.ref,
createPageModal: observable,
createIssueStoreType: observable,
createWorkItemAllowedProjectIds: observable,
allStickiesModal: observable,
projectListOpenMap: observable,
// projectPages: computed,
@@ -217,15 +214,13 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor
* @param storeType
* @returns
*/
toggleCreateIssueModal = (value?: boolean, storeType?: TCreateModalStoreTypes, allowedProjectIds?: string[]) => {
toggleCreateIssueModal = (value?: boolean, storeType?: TCreateModalStoreTypes) => {
if (value !== undefined) {
this.isCreateIssueModalOpen = value;
this.createIssueStoreType = storeType || EIssuesStoreType.PROJECT;
this.createWorkItemAllowedProjectIds = allowedProjectIds ?? undefined;
} else {
this.isCreateIssueModalOpen = !this.isCreateIssueModalOpen;
this.createIssueStoreType = EIssuesStoreType.PROJECT;
this.createWorkItemAllowedProjectIds = undefined;
}
};
@@ -5,6 +5,7 @@ import isEmpty from "lodash/isEmpty";
import orderBy from "lodash/orderBy";
import set from "lodash/set";
import uniq from "lodash/uniq";
import { runInAction } from "mobx";
import { ALL_ISSUES, EIssueFilterType, FILTER_TO_ISSUE_MAP, ISSUE_PRIORITIES } from "@plane/constants";
import {
IIssueDisplayFilterOptions,
@@ -317,22 +318,10 @@ export const getGroupedWorkItemIds = (
};
}
// Get the default key for the group by key
const getDefaultGroupKey = (groupByKey: TIssueGroupByOptions) => {
switch (groupByKey) {
case "state_detail.group":
return "state__group";
case null:
return null;
default:
return ISSUE_GROUP_BY_KEY[groupByKey];
}
};
// Group work items
const groupKey = getDefaultGroupKey(groupByKey);
const groupKey = ISSUE_GROUP_BY_KEY[groupByKey];
const groupedWorkItems = groupBy(workItems, (item) => {
const value = groupKey ? item[groupKey] : null;
const value = item[groupKey];
if (Array.isArray(value)) {
if (value.length === 0) return "None";
// Sort & join to build deterministic set-like key
@@ -121,7 +121,7 @@ export interface IBaseIssuesStore {
export const ISSUE_GROUP_BY_KEY: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
project: "project_id",
state: "state_id",
"state_detail.group": "state_id", // state_detail.group is only being used for state_group display,
"state_detail.group": "state__group" as keyof TIssue, // state_detail.group is only being used for state_group display,
priority: "priority",
labels: "label_ids",
created_by: "created_by",
@@ -137,7 +137,7 @@ export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof
cycle: "cycle_id",
module: "module_ids",
state: "state_id",
"state_detail.group": "state__group", // state_detail.group is only being used for state_group display,
"state_detail.group": "state_group" as keyof TIssue, // state_detail.group is only being used for state_group display,
priority: "priority",
labels: "label_ids",
created_by: "created_by",
@@ -1594,11 +1594,11 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
// if unGrouped, then return the path as ALL_ISSUES along with orderByUpdates
if (!this.issueGroupKey) return action ? [{ path: [ALL_ISSUES], action }, ...orderByUpdates] : orderByUpdates;
const issueGroupKeyValue = issue?.[this.issueGroupKey] as string | string[] | null | undefined;
const issueGroupKey = issue?.[this.issueGroupKey] as string | string[] | null | undefined;
const issueBeforeUpdateGroupKey = issueBeforeUpdate?.[this.issueGroupKey] as string | string[] | null | undefined;
// if grouped, the get the Difference between the two issue properties (this.issueGroupKey) on which groupBy is performed
const groupActionsArray = getDifference(
this.getArrayStringArray(issue, issueGroupKeyValue, this.groupBy),
this.getArrayStringArray(issue, issueGroupKey, this.groupBy),
this.getArrayStringArray(issueBeforeUpdate, issueBeforeUpdateGroupKey, this.groupBy),
action
);
@@ -1632,7 +1632,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
groupActionsArray,
subGroupActionsArray,
this.getArrayStringArray(issueBeforeUpdate, issueBeforeUpdateGroupKey, this.groupBy),
this.getArrayStringArray(issue, issueGroupKeyValue, this.groupBy),
this.getArrayStringArray(issue, issueGroupKey, this.groupBy),
this.getArrayStringArray(issueBeforeUpdate, issueBeforeUpdateSubGroupKey, this.subGroupBy),
this.getArrayStringArray(issue, issueSubGroupKey, this.subGroupBy)
),
@@ -1690,13 +1690,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
return issueKeyActions;
}
// /**
// * Normalizes group values into a consistent string array format
// * @param issueObject - The issue object to extract values from
// * @param value - The raw value (string, array, or null/undefined)
// * @param groupByKey - The group by key to handle special cases
// * @returns Normalized array of string values
// */
/**
* get the groupByKey issue property on which actions are to be decided in the form of array
* @param value
* @param groupByKey
* @returns an array of issue property values
*/
getArrayStringArray = (
issueObject: Partial<TIssue> | undefined,
value: string | string[] | undefined | null,
@@ -1709,23 +1708,9 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
// if array return the array
if (Array.isArray(value)) return value;
return this.getDefaultGroupValue(issueObject, value, groupByKey);
};
// /**
// * Gets the default value for a group when the primary value is empty
// * @param issueObject - The issue object to extract fallback values from
// * @param groupByKey - The group by key to determine fallback logic
// * @returns Default group value as string array
// */
private getDefaultGroupValue = (
issueObject: Partial<TIssue>,
value: string,
groupByKey?: TIssueGroupByOptions
): string[] => {
// Handle special case for state group
// if the groupKey is state group then return the group based on state_id
if (groupByKey === "state_detail.group") {
return [this.rootIssueStore.rootStore.state.stateMap?.[value]?.group ?? issueObject.state__group];
return [this.rootIssueStore.rootStore.state.stateMap?.[value]?.group];
}
return [value];
+11 -1
View File
@@ -1,10 +1,12 @@
import sortBy from "lodash/sortBy";
// types
import { EUserPermissions } from "@plane/constants";
import { TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types";
// helpers
import { getDate } from "@/helpers/date-time.helper";
import { satisfiesDateFilter } from "@/helpers/filter.helper";
// plane web imports
// plane web constants
// types
import { TProject } from "@/plane-web/types";
/**
@@ -47,6 +49,14 @@ export const orderJoinedProjects = (
export const projectIdentifierSanitizer = (identifier: string): string =>
identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "");
/**
* @description Checks if the project should be rendered or not based on the user role
* @param {TProject} project
* @returns {boolean}
*/
export const shouldRenderProject = (project: TProject): boolean =>
!!project.member_role && project.member_role >= EUserPermissions.MEMBER;
/**
* @description filters projects based on the filter
* @param {TProject} project
+11 -11
View File
@@ -21,14 +21,14 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@headlessui/react": "^1.7.3",
"@intercom/messenger-js-sdk": "^0.0.12",
"@plane/constants": "workspace:*",
"@plane/editor": "workspace:*",
"@plane/hooks": "workspace:*",
"@plane/i18n": "workspace:*",
"@plane/propel": "workspace:*",
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/hooks": "*",
"@plane/i18n": "*",
"@plane/propel": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@popperjs/core": "^2.11.8",
"@react-pdf/renderer": "^3.4.5",
"@tanstack/react-table": "^8.21.3",
@@ -69,9 +69,9 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.14.202",
"@types/node": "18.16.1",