diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b25a791d0..73d69fb2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like: -- 3rd-party libraries being used and their versions -- a use-case that fails +- 3rd-party libraries being used and their versions +- a use-case that fails Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved. @@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla ### Requirements -- Node.js version v16.18.0 -- Python version 3.8+ -- Postgres version v14 -- Redis version v6.2.7 +- Node.js version v16.18.0 +- Python version 3.8+ +- Postgres version v14 +- Redis version v6.2.7 ### Setup the project @@ -81,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt To ensure consistency throughout the source code, please keep these rules in mind as you are working: -- All features or bug fixes must be tested by one or more specs (unit-tests). -- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier. +- All features or bug fixes must be tested by one or more specs (unit-tests). +- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier. ## Need help? Questions and suggestions @@ -90,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in ## Ways to contribute -- Try Plane Cloud and the self hosting platform and give feedback -- Add new integrations -- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose) -- Share your thoughts and suggestions with us -- Help create tutorials and blog posts -- Request a feature by submitting a proposal -- Report a bug -- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations. +- Try Plane Cloud and the self hosting platform and give feedback +- Add new integrations +- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose) +- Share your thoughts and suggestions with us +- Help create tutorials and blog posts +- Request a feature by submitting a proposal +- Report a bug +- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations. diff --git a/ENV_SETUP.md b/ENV_SETUP.md index 37136ffda..800d0a335 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -1,8 +1,10 @@ # Environment Variables + ​ -Environment variables are distributed in various files. Please refer them carefully. +Environment variables are distributed in various files. Please refer them carefully. ## {PROJECT_FOLDER}/.env + File is available in the project root folder​ ``` @@ -41,25 +43,37 @@ USE_MINIO=1 # Nginx Configuration NGINX_PORT=80 ``` + ​ + ## {PROJECT_FOLDER}/web/.env.example + ​ + ``` # Enable/Disable OAUTH - default 0 for selfhosted instance NEXT_PUBLIC_ENABLE_OAUTH=0 # Public boards deploy URL NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces" ``` + ​ + ## {PROJECT_FOLDER}/spaces/.env.example + ​ + ``` # Flag to toggle OAuth NEXT_PUBLIC_ENABLE_OAUTH=0 ``` + ​ + ## {PROJECT_FOLDER}/apiserver/.env + ​ + ``` # Backend # Debug value for api server use it as 0 for production use @@ -123,7 +137,9 @@ ENABLE_SIGNUP="1" # Email Redirection URL WEB_URL="http://localhost" ``` + ## Updates​ + - The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects. - The naming convention for containers and images has been updated. - The plane-worker image will no longer be maintained, as it has been merged with plane-backend. diff --git a/apiserver/plane/api/serializers/state.py b/apiserver/plane/api/serializers/state.py index 097bc4c93..ad416c340 100644 --- a/apiserver/plane/api/serializers/state.py +++ b/apiserver/plane/api/serializers/state.py @@ -7,8 +7,6 @@ from plane.db.models import State class StateSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") - project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = State diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/api/urls/state.py index bcfd80cd7..94aa55f24 100644 --- a/apiserver/plane/api/urls/state.py +++ b/apiserver/plane/api/urls/state.py @@ -20,11 +20,19 @@ urlpatterns = [ StateViewSet.as_view( { "get": "retrieve", - "put": "update", "patch": "partial_update", "delete": "destroy", } ), name="project-state", ), + path( + "workspaces//projects//states//mark-default/", + StateViewSet.as_view( + { + "post": "mark_as_default", + } + ), + name="project-state", + ), ] diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 063abf0e3..dbb6e1d71 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -47,36 +47,45 @@ class StateViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def list(self, request, slug, project_id): - state_dict = dict() states = StateSerializer(self.get_queryset(), many=True).data + grouped = request.GET.get("grouped", False) + if grouped == "true": + state_dict = {} + for key, value in groupby( + sorted(states, key=lambda state: state["group"]), + lambda state: state.get("group"), + ): + state_dict[str(key)] = list(value) + return Response(state_dict, status=status.HTTP_200_OK) + return Response(states, status=status.HTTP_200_OK) - for key, value in groupby( - sorted(states, key=lambda state: state["group"]), - lambda state: state.get("group"), - ): - state_dict[str(key)] = list(value) - - return Response(state_dict, status=status.HTTP_200_OK) + def mark_as_default(self, request, slug, project_id, pk): + # Select all the states which are marked as default + _ = State.objects.filter( + workspace__slug=slug, project_id=project_id, default=True + ).update(default=False) + _ = State.objects.filter( + workspace__slug=slug, project_id=project_id, pk=pk + ).update(default=True) + return Response(status=status.HTTP_204_NO_CONTENT) def destroy(self, request, slug, project_id, pk): state = State.objects.get( ~Q(name="Triage"), - pk=pk, project_id=project_id, workspace__slug=slug, + pk=pk, + project_id=project_id, + workspace__slug=slug, ) if state.default: - return Response( - {"error": "Default state cannot be deleted"}, status=False - ) + return Response({"error": "Default state cannot be deleted"}, status=False) # Check for any issues in the state issue_exist = Issue.issue_objects.filter(state=pk).exists() if issue_exist: return Response( - { - "error": "The state is not empty, only empty states can be deleted" - }, + {"error": "The state is not empty, only empty states can be deleted"}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/packages/editor/core/Readme.md b/packages/editor/core/Readme.md index 56d1a502c..aafda7008 100644 --- a/packages/editor/core/Readme.md +++ b/packages/editor/core/Readme.md @@ -19,27 +19,27 @@ This allows for extensive customization and flexibility in the Editors created u 1. useEditor - A hook that you can use to extend the Plane editor. - | Prop | Type | Description | - | --- | --- | --- | - | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | - | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | - | `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | - | `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | - | `value` | `html string` | The initial content of the editor. | - | `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | - | `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | - | `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | - | `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert in case of content not being "saved". | - | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | + | Prop | Type | Description | + | ------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | + | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | + | `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | + | `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | + | `value` | `html string` | The initial content of the editor. | + | `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | + | `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | + | `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | + | `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert in case of content not being "saved". | + | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | 2. useReadOnlyEditor - A hook that can be used to extend a Read Only instance of the core editor. - | Prop | Type | Description | - | --- | --- | --- | - | `value` | `string` | The initial content of the editor. | - | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | - | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | - | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | + | Prop | Type | Description | + | -------------- | ------------- | ------------------------------------------------------------------------------------------ | + | `value` | `string` | The initial content of the editor. | + | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | + | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | + | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | 3. Items and Commands - H1, H2, H3, task list, quote, code block, etc's methods. @@ -51,7 +51,11 @@ This allows for extensive customization and flexibility in the Editors created u 5. Extending with Custom Styles ```ts -const customEditorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); +const customEditorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, +}); ``` ## Core features diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 497a63ca6..8f9e36350 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -3,18 +3,36 @@ import { UploadImage } from "../types/upload-image"; import { startImageUpload } from "../ui/plugins/upload-image"; export const toggleHeadingOne = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - else editor.chain().focus().toggleHeading({ level: 1 }).run() + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .setNode("heading", { level: 1 }) + .run(); + 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(); - else editor.chain().focus().toggleHeading({ level: 2 }).run() + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .setNode("heading", { level: 2 }) + .run(); + 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(); - else editor.chain().focus().toggleHeading({ level: 3 }).run() + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .setNode("heading", { level: 3 }) + .run(); + else editor.chain().focus().toggleHeading({ level: 3 }).run(); }; export const toggleBold = (editor: Editor, range?: Range) => { @@ -37,7 +55,8 @@ export const toggleCode = (editor: Editor, range?: Range) => { else editor.chain().focus().toggleCode().run(); }; export const toggleOrderedList = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run(); + if (range) + editor.chain().focus().deleteRange(range).toggleOrderedList().run(); else editor.chain().focus().toggleOrderedList().run(); }; @@ -48,7 +67,7 @@ export const toggleBulletList = (editor: Editor, range?: Range) => { export const toggleTaskList = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).toggleTaskList().run(); - else editor.chain().focus().toggleTaskList().run() + else editor.chain().focus().toggleTaskList().run(); }; export const toggleStrike = (editor: Editor, range?: Range) => { @@ -57,13 +76,37 @@ export const toggleStrike = (editor: Editor, range?: Range) => { }; export const toggleBlockquote = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(); - else editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(); + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .toggleNode("paragraph", "paragraph") + .toggleBlockquote() + .run(); + else + editor + .chain() + .focus() + .toggleNode("paragraph", "paragraph") + .toggleBlockquote() + .run(); }; export const insertTableCommand = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); - else editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) + .run(); + else + editor + .chain() + .focus() + .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) + .run(); }; export const unsetLinkEditor = (editor: Editor) => { @@ -74,7 +117,14 @@ export const setLinkEditor = (editor: Editor, url: string) => { editor.chain().focus().setLink({ href: url }).run(); }; -export const insertImageCommand = (editor: Editor, uploadFile: UploadImage, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void, range?: Range) => { +export const insertImageCommand = ( + editor: Editor, + uploadFile: UploadImage, + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, + range?: Range, +) => { if (range) editor.chain().focus().deleteRange(range).run(); const input = document.createElement("input"); input.type = "file"; @@ -88,4 +138,3 @@ export const insertImageCommand = (editor: Editor, uploadFile: UploadImage, setI }; input.click(); }; - diff --git a/packages/editor/core/src/lib/utils.ts b/packages/editor/core/src/lib/utils.ts index 484674780..f426b70b7 100644 --- a/packages/editor/core/src/lib/utils.ts +++ b/packages/editor/core/src/lib/utils.ts @@ -6,19 +6,24 @@ interface EditorClassNames { customClassName?: string; } -export const getEditorClassNames = ({ noBorder, borderOnFocus, customClassName }: EditorClassNames) => cn( - 'relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md', - noBorder ? '' : 'border border-custom-border-200', - borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0', - customClassName -); +export const getEditorClassNames = ({ + noBorder, + borderOnFocus, + customClassName, +}: EditorClassNames) => + cn( + "relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md", + noBorder ? "" : "border border-custom-border-200", + borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0", + customClassName, + ); export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export const findTableAncestor = ( - node: Node | null + node: Node | null, ): HTMLTableElement | null => { while (node !== null && node.nodeName !== "TABLE") { node = node.parentNode; @@ -27,10 +32,10 @@ export const findTableAncestor = ( }; export const getTrimmedHTML = (html: string) => { - html = html.replace(/^(

<\/p>)+/, ''); - html = html.replace(/(

<\/p>)+$/, ''); + html = html.replace(/^(

<\/p>)+/, ""); + html = html.replace(/(

<\/p>)+$/, ""); return html; -} +}; export const isValidHttpUrl = (string: string): boolean => { let url: URL; @@ -42,4 +47,4 @@ export const isValidHttpUrl = (string: string): boolean => { } return url.protocol === "http:" || url.protocol === "https:"; -} +}; diff --git a/packages/editor/core/src/types/mention-suggestion.ts b/packages/editor/core/src/types/mention-suggestion.ts index 9c9ab7606..dcaa3148d 100644 --- a/packages/editor/core/src/types/mention-suggestion.ts +++ b/packages/editor/core/src/types/mention-suggestion.ts @@ -1,10 +1,10 @@ export type IMentionSuggestion = { - id: string; - type: string; - avatar: string; - title: string; - subtitle: string; - redirect_uri: string; -} + id: string; + type: string; + avatar: string; + title: string; + subtitle: string; + redirect_uri: string; +}; -export type IMentionHighlight = string \ No newline at end of file +export type IMentionHighlight = string; diff --git a/packages/editor/core/src/ui/components/editor-content.tsx b/packages/editor/core/src/ui/components/editor-content.tsx index d0531da01..830b87d9c 100644 --- a/packages/editor/core/src/ui/components/editor-content.tsx +++ b/packages/editor/core/src/ui/components/editor-content.tsx @@ -8,10 +8,16 @@ interface EditorContentProps { children?: ReactNode; } -export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => ( +export const EditorContentWrapper = ({ + editor, + editorContentCustomClassNames = "", + children, +}: EditorContentProps) => (

- {(editor?.isActive("image") && editor?.isEditable) && } + {editor?.isActive("image") && editor?.isEditable && ( + + )} {children}
); diff --git a/packages/editor/core/src/ui/extensions/image/image-resize.tsx b/packages/editor/core/src/ui/extensions/image/image-resize.tsx index 5e86475cf..2545c7e44 100644 --- a/packages/editor/core/src/ui/extensions/image/image-resize.tsx +++ b/packages/editor/core/src/ui/extensions/image/image-resize.tsx @@ -3,7 +3,9 @@ import Moveable from "react-moveable"; export const ImageResizer = ({ editor }: { editor: Editor }) => { const updateMediaSize = () => { - const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement; + const imageInfo = document.querySelector( + ".ProseMirror-selectednode", + ) as HTMLImageElement; if (imageInfo) { const selection = editor.state.selection; editor.commands.setImage({ diff --git a/packages/editor/core/src/ui/extensions/image/index.tsx b/packages/editor/core/src/ui/extensions/image/index.tsx index f9345509d..aea84c6b8 100644 --- a/packages/editor/core/src/ui/extensions/image/index.tsx +++ b/packages/editor/core/src/ui/extensions/image/index.tsx @@ -3,21 +3,28 @@ import TrackImageDeletionPlugin from "../../plugins/delete-image"; import UploadImagesPlugin from "../../plugins/upload-image"; import { DeleteImage } from "../../../types/delete-image"; -const ImageExtension = (deleteImage: DeleteImage) => Image.extend({ - addProseMirrorPlugins() { - return [UploadImagesPlugin(), TrackImageDeletionPlugin(deleteImage)]; - }, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - }; - }, -}); +const ImageExtension = ( + deleteImage: DeleteImage, + cancelUploadImage?: () => any, +) => + Image.extend({ + addProseMirrorPlugins() { + return [ + UploadImagesPlugin(cancelUploadImage), + TrackImageDeletionPlugin(deleteImage), + ]; + }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + height: { + default: null, + }, + }; + }, + }); export default ImageExtension; diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index a7621ab20..3f191a912 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -20,82 +20,89 @@ import { isValidHttpUrl } from "../../lib/utils"; import { IMentionSuggestion } from "../../types/mention-suggestion"; import { Mentions } from "../mentions"; - export const CoreEditorExtensions = ( - mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] }, + mentionConfig: { + mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: string[]; + }, deleteFile: DeleteImage, + cancelUploadImage?: () => any, ) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", - }, + StarterKit.configure({ + bulletList: { + HTMLAttributes: { + class: "list-disc list-outside leading-3 -mt-2", }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", - }, + }, + orderedList: { + HTMLAttributes: { + class: "list-decimal list-outside leading-3 -mt-2", }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", - }, + }, + listItem: { + HTMLAttributes: { + class: "leading-normal -mb-2", }, - blockquote: { - HTMLAttributes: { - class: "border-l-4 border-custom-border-300", - }, + }, + blockquote: { + HTMLAttributes: { + class: "border-l-4 border-custom-border-300", }, - code: { - HTMLAttributes: { - class: - "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", - }, - }, - codeBlock: false, - horizontalRule: false, - dropcursor: { - color: "rgba(var(--color-text-100))", - width: 2, - }, - gapcursor: false, - }), - Gapcursor, - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), + }, + code: { HTMLAttributes: { class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", + "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", + spellcheck: "false", }, - }), - ImageExtension(deleteFile).configure({ - HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", - }, - }), - TiptapUnderline, - TextStyle, - Color, - TaskList.configure({ - HTMLAttributes: { - class: "not-prose pl-2", - }, - }), - TaskItem.configure({ - HTMLAttributes: { - class: "flex items-start my-4", - }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), - Table, - TableHeader, - TableCell, - TableRow, - Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false), - ]; + }, + codeBlock: false, + horizontalRule: false, + dropcursor: { + color: "rgba(var(--color-text-100))", + width: 2, + }, + gapcursor: false, + }), + Gapcursor, + TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), + HTMLAttributes: { + class: + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", + }, + }), + ImageExtension(deleteFile, cancelUploadImage).configure({ + HTMLAttributes: { + class: "rounded-lg border border-custom-border-300", + }, + }), + TiptapUnderline, + TextStyle, + Color, + TaskList.configure({ + HTMLAttributes: { + class: "not-prose pl-2", + }, + }), + TaskItem.configure({ + HTMLAttributes: { + class: "flex items-start my-4", + }, + nested: true, + }), + Markdown.configure({ + html: true, + transformCopiedText: true, + }), + Table, + TableHeader, + TableCell, + TableRow, + Mentions( + mentionConfig.mentionSuggestions, + mentionConfig.mentionHighlights, + false, + ), +]; diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/index.ts b/packages/editor/core/src/ui/extensions/table/table-cell/index.ts index b39fe7104..fb2183381 100644 --- a/packages/editor/core/src/ui/extensions/table/table-cell/index.ts +++ b/packages/editor/core/src/ui/extensions/table/table-cell/index.ts @@ -1 +1 @@ -export { default as default } from "./table-cell" +export { default as default } from "./table-cell"; diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts index ac43875da..1d3e57af9 100644 --- a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts +++ b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts @@ -1,7 +1,7 @@ -import { mergeAttributes, Node } from "@tiptap/core" +import { mergeAttributes, Node } from "@tiptap/core"; export interface TableCellOptions { - HTMLAttributes: Record + HTMLAttributes: Record; } export default Node.create({ @@ -9,8 +9,8 @@ export default Node.create({ addOptions() { return { - HTMLAttributes: {} - } + HTMLAttributes: {}, + }; }, content: "paragraph+", @@ -18,24 +18,24 @@ export default Node.create({ addAttributes() { return { colspan: { - default: 1 + default: 1, }, rowspan: { - default: 1 + default: 1, }, colwidth: { default: null, parseHTML: (element) => { - const colwidth = element.getAttribute("colwidth") - const value = colwidth ? [parseInt(colwidth, 10)] : null + const colwidth = element.getAttribute("colwidth"); + const value = colwidth ? [parseInt(colwidth, 10)] : null; - return value - } + return value; + }, }, background: { - default: "none" - } - } + default: "none", + }, + }; }, tableRole: "cell", @@ -43,16 +43,16 @@ export default Node.create({ isolating: true, parseHTML() { - return [{ tag: "td" }] + return [{ tag: "td" }]; }, renderHTML({ node, HTMLAttributes }) { return [ "td", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - style: `background-color: ${node.attrs.background}` + style: `background-color: ${node.attrs.background}`, }), - 0 - ] - } -}) + 0, + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table-header/index.ts b/packages/editor/core/src/ui/extensions/table/table-header/index.ts index 57137dedd..cb036c505 100644 --- a/packages/editor/core/src/ui/extensions/table/table-header/index.ts +++ b/packages/editor/core/src/ui/extensions/table/table-header/index.ts @@ -1 +1 @@ -export { default as default } from "./table-header" +export { default as default } from "./table-header"; diff --git a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts index 712ca65f0..0148f1a6f 100644 --- a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts +++ b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts @@ -1,15 +1,15 @@ -import { mergeAttributes, Node } from "@tiptap/core" +import { mergeAttributes, Node } from "@tiptap/core"; export interface TableHeaderOptions { - HTMLAttributes: Record + HTMLAttributes: Record; } export default Node.create({ name: "tableHeader", addOptions() { return { - HTMLAttributes: {} - } + HTMLAttributes: {}, + }; }, content: "paragraph+", @@ -17,24 +17,24 @@ export default Node.create({ addAttributes() { return { colspan: { - default: 1 + default: 1, }, rowspan: { - default: 1 + default: 1, }, colwidth: { default: null, parseHTML: (element) => { - const colwidth = element.getAttribute("colwidth") - const value = colwidth ? [parseInt(colwidth, 10)] : null + const colwidth = element.getAttribute("colwidth"); + const value = colwidth ? [parseInt(colwidth, 10)] : null; - return value - } + return value; + }, }, background: { - default: "rgb(var(--color-primary-100))" - } - } + default: "rgb(var(--color-primary-100))", + }, + }; }, tableRole: "header_cell", @@ -42,16 +42,16 @@ export default Node.create({ isolating: true, parseHTML() { - return [{ tag: "th" }] + return [{ tag: "th" }]; }, renderHTML({ node, HTMLAttributes }) { return [ "th", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - style: `background-color: ${node.attrs.background}` + style: `background-color: ${node.attrs.background}`, }), - 0 - ] - } -}) + 0, + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table-row/index.ts b/packages/editor/core/src/ui/extensions/table/table-row/index.ts index 9ecc2c0ae..8c6eb55aa 100644 --- a/packages/editor/core/src/ui/extensions/table/table-row/index.ts +++ b/packages/editor/core/src/ui/extensions/table/table-row/index.ts @@ -1 +1 @@ -export { default as default } from "./table-row" +export { default as default } from "./table-row"; diff --git a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts index e922e7fa1..1b576623b 100644 --- a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts +++ b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts @@ -1,31 +1,31 @@ -import { mergeAttributes, Node } from "@tiptap/core" +import { mergeAttributes, Node } from "@tiptap/core"; export interface TableRowOptions { - HTMLAttributes: Record + HTMLAttributes: Record; } export default Node.create({ - name: "tableRow", + name: "tableRow", - addOptions() { - return { - HTMLAttributes: {} - } - }, + addOptions() { + return { + HTMLAttributes: {}, + }; + }, - content: "(tableCell | tableHeader)*", + content: "(tableCell | tableHeader)*", - tableRole: "row", + tableRole: "row", - parseHTML() { - return [{ tag: "tr" }] - }, + parseHTML() { + return [{ tag: "tr" }]; + }, - renderHTML({ HTMLAttributes }) { - return [ - "tr", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - 0 - ] - } -}) + renderHTML({ HTMLAttributes }) { + return [ + "tr", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + 0, + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table/icons.ts b/packages/editor/core/src/ui/extensions/table/table/icons.ts index d3159d4aa..eda520759 100644 --- a/packages/editor/core/src/ui/extensions/table/table/icons.ts +++ b/packages/editor/core/src/ui/extensions/table/table/icons.ts @@ -38,7 +38,7 @@ const icons = { /> `, - insertBottomTableIcon:` { onSelectColor(value); colorPicker.hide(); @@ -331,7 +333,9 @@ export class TableView implements NodeView { this.rowsControl = h( "div", { className: "rowsControl" }, - h("button", { + h("div", { + itemType: "button", + className: "rowsControlDiv", onClick: () => this.selectRow(), }), ); @@ -339,7 +343,9 @@ export class TableView implements NodeView { this.columnsControl = h( "div", { className: "columnsControl" }, - h("button", { + h("div", { + itemType: "button", + className: "columnsControlDiv", onClick: () => this.selectColumn(), }), ); @@ -352,7 +358,7 @@ export class TableView implements NodeView { ); this.columnsToolbox = createToolbox({ - triggerButton: this.columnsControl.querySelector("button"), + triggerButton: this.columnsControl.querySelector(".columnsControlDiv"), items: columnsToolboxItems, tippyOptions: { ...defaultTippyOptions, diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts index eab3cad92..8571fdfba 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/table.ts @@ -1,298 +1,312 @@ -import { TextSelection } from "@tiptap/pm/state" +import { TextSelection } from "@tiptap/pm/state"; -import { callOrReturn, getExtensionField, mergeAttributes, Node, ParentConfig } from "@tiptap/core" import { - addColumnAfter, - addColumnBefore, - addRowAfter, - addRowBefore, - CellSelection, - columnResizing, - deleteColumn, - deleteRow, - deleteTable, - fixTables, - goToNextCell, - mergeCells, - setCellAttr, - splitCell, - tableEditing, - toggleHeader, - toggleHeaderCell -} from "@tiptap/prosemirror-tables" + callOrReturn, + getExtensionField, + mergeAttributes, + Node, + ParentConfig, +} from "@tiptap/core"; +import { + addColumnAfter, + addColumnBefore, + addRowAfter, + addRowBefore, + CellSelection, + columnResizing, + deleteColumn, + deleteRow, + deleteTable, + fixTables, + goToNextCell, + mergeCells, + setCellAttr, + splitCell, + tableEditing, + toggleHeader, + toggleHeaderCell, +} from "@tiptap/prosemirror-tables"; -import { tableControls } from "./table-controls" -import { TableView } from "./table-view" -import { createTable } from "./utilities/create-table" -import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected" +import { tableControls } from "./table-controls"; +import { TableView } from "./table-view"; +import { createTable } from "./utilities/create-table"; +import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected"; export interface TableOptions { - HTMLAttributes: Record - resizable: boolean - handleWidth: number - cellMinWidth: number - lastColumnResizable: boolean - allowTableNodeSelection: boolean + HTMLAttributes: Record; + resizable: boolean; + handleWidth: number; + cellMinWidth: number; + lastColumnResizable: boolean; + allowTableNodeSelection: boolean; } declare module "@tiptap/core" { - interface Commands { - table: { - insertTable: (options?: { - rows?: number - cols?: number - withHeaderRow?: boolean - }) => ReturnType - addColumnBefore: () => ReturnType - addColumnAfter: () => ReturnType - deleteColumn: () => ReturnType - addRowBefore: () => ReturnType - addRowAfter: () => ReturnType - deleteRow: () => ReturnType - deleteTable: () => ReturnType - mergeCells: () => ReturnType - splitCell: () => ReturnType - toggleHeaderColumn: () => ReturnType - toggleHeaderRow: () => ReturnType - toggleHeaderCell: () => ReturnType - mergeOrSplit: () => ReturnType - setCellAttribute: (name: string, value: any) => ReturnType - goToNextCell: () => ReturnType - goToPreviousCell: () => ReturnType - fixTables: () => ReturnType - setCellSelection: (position: { - anchorCell: number - headCell?: number - }) => ReturnType - } - } + interface Commands { + table: { + insertTable: (options?: { + rows?: number; + cols?: number; + withHeaderRow?: boolean; + }) => ReturnType; + addColumnBefore: () => ReturnType; + addColumnAfter: () => ReturnType; + deleteColumn: () => ReturnType; + addRowBefore: () => ReturnType; + addRowAfter: () => ReturnType; + deleteRow: () => ReturnType; + deleteTable: () => ReturnType; + mergeCells: () => ReturnType; + splitCell: () => ReturnType; + toggleHeaderColumn: () => ReturnType; + toggleHeaderRow: () => ReturnType; + toggleHeaderCell: () => ReturnType; + mergeOrSplit: () => ReturnType; + setCellAttribute: (name: string, value: any) => ReturnType; + goToNextCell: () => ReturnType; + goToPreviousCell: () => ReturnType; + fixTables: () => ReturnType; + setCellSelection: (position: { + anchorCell: number; + headCell?: number; + }) => ReturnType; + }; + } - interface NodeConfig { - tableRole?: - | string - | ((this: { - name: string - options: Options - storage: Storage - parent: ParentConfig>["tableRole"] - }) => string) - } + interface NodeConfig { + tableRole?: + | string + | ((this: { + name: string; + options: Options; + storage: Storage; + parent: ParentConfig>["tableRole"]; + }) => string); + } } export default Node.create({ - name: "table", + name: "table", - addOptions() { - return { - HTMLAttributes: {}, - resizable: true, - handleWidth: 5, - cellMinWidth: 100, - lastColumnResizable: true, - allowTableNodeSelection: true - } - }, + addOptions() { + return { + HTMLAttributes: {}, + resizable: true, + handleWidth: 5, + cellMinWidth: 100, + lastColumnResizable: true, + allowTableNodeSelection: true, + }; + }, - content: "tableRow+", + content: "tableRow+", - tableRole: "table", + tableRole: "table", - isolating: true, + isolating: true, - group: "block", + group: "block", - allowGapCursor: false, + allowGapCursor: false, - parseHTML() { - return [{ tag: "table" }] - }, + parseHTML() { + return [{ tag: "table" }]; + }, - renderHTML({ HTMLAttributes }) { - return [ - "table", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - ["tbody", 0] - ] - }, + renderHTML({ HTMLAttributes }) { + return [ + "table", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + ["tbody", 0], + ]; + }, - addCommands() { - return { - insertTable: - ({ rows = 3, cols = 3, withHeaderRow = true} = {}) => - ({ tr, dispatch, editor }) => { - const node = createTable( - editor.schema, - rows, - cols, - withHeaderRow - ) + addCommands() { + return { + insertTable: + ({ rows = 3, cols = 3, withHeaderRow = true } = {}) => + ({ tr, dispatch, editor }) => { + const node = createTable(editor.schema, rows, cols, withHeaderRow); - if (dispatch) { - const offset = tr.selection.anchor + 1 + if (dispatch) { + const offset = tr.selection.anchor + 1; - tr.replaceSelectionWith(node) - .scrollIntoView() - .setSelection( - TextSelection.near(tr.doc.resolve(offset)) - ) - } + tr.replaceSelectionWith(node) + .scrollIntoView() + .setSelection(TextSelection.near(tr.doc.resolve(offset))); + } - return true - }, - addColumnBefore: - () => - ({ state, dispatch }) => addColumnBefore(state, dispatch), - addColumnAfter: - () => - ({ state, dispatch }) => addColumnAfter(state, dispatch), - deleteColumn: - () => - ({ state, dispatch }) => deleteColumn(state, dispatch), - addRowBefore: - () => - ({ state, dispatch }) => addRowBefore(state, dispatch), - addRowAfter: - () => - ({ state, dispatch }) => addRowAfter(state, dispatch), - deleteRow: - () => - ({ state, dispatch }) => deleteRow(state, dispatch), - deleteTable: - () => - ({ state, dispatch }) => deleteTable(state, dispatch), - mergeCells: - () => - ({ state, dispatch }) => mergeCells(state, dispatch), - splitCell: - () => - ({ state, dispatch }) => splitCell(state, dispatch), - toggleHeaderColumn: - () => - ({ state, dispatch }) => toggleHeader("column")(state, dispatch), - toggleHeaderRow: - () => - ({ state, dispatch }) => toggleHeader("row")(state, dispatch), - toggleHeaderCell: - () => - ({ state, dispatch }) => toggleHeaderCell(state, dispatch), - mergeOrSplit: - () => - ({ state, dispatch }) => { - if (mergeCells(state, dispatch)) { - return true - } + return true; + }, + addColumnBefore: + () => + ({ state, dispatch }) => + addColumnBefore(state, dispatch), + addColumnAfter: + () => + ({ state, dispatch }) => + addColumnAfter(state, dispatch), + deleteColumn: + () => + ({ state, dispatch }) => + deleteColumn(state, dispatch), + addRowBefore: + () => + ({ state, dispatch }) => + addRowBefore(state, dispatch), + addRowAfter: + () => + ({ state, dispatch }) => + addRowAfter(state, dispatch), + deleteRow: + () => + ({ state, dispatch }) => + deleteRow(state, dispatch), + deleteTable: + () => + ({ state, dispatch }) => + deleteTable(state, dispatch), + mergeCells: + () => + ({ state, dispatch }) => + mergeCells(state, dispatch), + splitCell: + () => + ({ state, dispatch }) => + splitCell(state, dispatch), + toggleHeaderColumn: + () => + ({ state, dispatch }) => + toggleHeader("column")(state, dispatch), + toggleHeaderRow: + () => + ({ state, dispatch }) => + toggleHeader("row")(state, dispatch), + toggleHeaderCell: + () => + ({ state, dispatch }) => + toggleHeaderCell(state, dispatch), + mergeOrSplit: + () => + ({ state, dispatch }) => { + if (mergeCells(state, dispatch)) { + return true; + } - return splitCell(state, dispatch) - }, - setCellAttribute: - (name, value) => - ({ state, dispatch }) => setCellAttr(name, value)(state, dispatch), - goToNextCell: - () => - ({ state, dispatch }) => goToNextCell(1)(state, dispatch), - goToPreviousCell: - () => - ({ state, dispatch }) => goToNextCell(-1)(state, dispatch), - fixTables: - () => - ({ state, dispatch }) => { - if (dispatch) { - fixTables(state) - } + return splitCell(state, dispatch); + }, + setCellAttribute: + (name, value) => + ({ state, dispatch }) => + setCellAttr(name, value)(state, dispatch), + goToNextCell: + () => + ({ state, dispatch }) => + goToNextCell(1)(state, dispatch), + goToPreviousCell: + () => + ({ state, dispatch }) => + goToNextCell(-1)(state, dispatch), + fixTables: + () => + ({ state, dispatch }) => { + if (dispatch) { + fixTables(state); + } - return true - }, - setCellSelection: - (position) => - ({ tr, dispatch }) => { - if (dispatch) { - const selection = CellSelection.create( - tr.doc, - position.anchorCell, - position.headCell - ) + return true; + }, + setCellSelection: + (position) => + ({ tr, dispatch }) => { + if (dispatch) { + const selection = CellSelection.create( + tr.doc, + position.anchorCell, + position.headCell, + ); - // @ts-ignore - tr.setSelection(selection) - } + // @ts-ignore + tr.setSelection(selection); + } - return true - } - } - }, + return true; + }, + }; + }, - addKeyboardShortcuts() { - return { - Tab: () => { - if (this.editor.commands.goToNextCell()) { - return true - } - - if (!this.editor.can().addRowAfter()) { - return false - } - - return this.editor.chain().addRowAfter().goToNextCell().run() - }, - "Shift-Tab": () => this.editor.commands.goToPreviousCell(), - Backspace: deleteTableWhenAllCellsSelected, - "Mod-Backspace": deleteTableWhenAllCellsSelected, - Delete: deleteTableWhenAllCellsSelected, - "Mod-Delete": deleteTableWhenAllCellsSelected - } - }, - - addNodeView() { - return ({ editor, getPos, node, decorations }) => { - const { cellMinWidth } = this.options - - return new TableView( - node, - cellMinWidth, - decorations, - editor, - getPos as () => number - ) - } - }, - - addProseMirrorPlugins() { - const isResizable = this.options.resizable && this.editor.isEditable - - const plugins = [ - tableEditing({ - allowTableNodeSelection: this.options.allowTableNodeSelection - }), - tableControls() - ] - - if (isResizable) { - plugins.unshift( - columnResizing({ - handleWidth: this.options.handleWidth, - cellMinWidth: this.options.cellMinWidth, - // View: TableView, - - // @ts-ignore - lastColumnResizable: this.options.lastColumnResizable - }) - ) + addKeyboardShortcuts() { + return { + Tab: () => { + if (this.editor.commands.goToNextCell()) { + return true; } - return plugins - }, - - extendNodeSchema(extension) { - const context = { - name: extension.name, - options: extension.options, - storage: extension.storage + if (!this.editor.can().addRowAfter()) { + return false; } - return { - tableRole: callOrReturn( - getExtensionField(extension, "tableRole", context) - ) - } + return this.editor.chain().addRowAfter().goToNextCell().run(); + }, + "Shift-Tab": () => this.editor.commands.goToPreviousCell(), + Backspace: deleteTableWhenAllCellsSelected, + "Mod-Backspace": deleteTableWhenAllCellsSelected, + Delete: deleteTableWhenAllCellsSelected, + "Mod-Delete": deleteTableWhenAllCellsSelected, + }; + }, + + addNodeView() { + return ({ editor, getPos, node, decorations }) => { + const { cellMinWidth } = this.options; + + return new TableView( + node, + cellMinWidth, + decorations, + editor, + getPos as () => number, + ); + }; + }, + + addProseMirrorPlugins() { + const isResizable = this.options.resizable && this.editor.isEditable; + + const plugins = [ + tableEditing({ + allowTableNodeSelection: this.options.allowTableNodeSelection, + }), + tableControls(), + ]; + + if (isResizable) { + plugins.unshift( + columnResizing({ + handleWidth: this.options.handleWidth, + cellMinWidth: this.options.cellMinWidth, + // View: TableView, + + // @ts-ignore + lastColumnResizable: this.options.lastColumnResizable, + }), + ); } -}) + + return plugins; + }, + + extendNodeSchema(extension) { + const context = { + name: extension.name, + options: extension.options, + storage: extension.storage, + }; + + return { + tableRole: callOrReturn( + getExtensionField(extension, "tableRole", context), + ), + }; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts index a3d7f2da8..7811341e0 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts @@ -1,12 +1,12 @@ -import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model" +import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model"; export function createCell( - cellType: NodeType, - cellContent?: Fragment | ProsemirrorNode | Array + cellType: NodeType, + cellContent?: Fragment | ProsemirrorNode | Array, ): ProsemirrorNode | null | undefined { - if (cellContent) { - return cellType.createChecked(null, cellContent) - } + if (cellContent) { + return cellType.createChecked(null, cellContent); + } - return cellType.createAndFill() + return cellType.createAndFill(); } diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts index 75bf7cb41..5805ecf86 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts @@ -1,45 +1,45 @@ -import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model" +import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model"; -import { createCell } from "./create-cell" -import { getTableNodeTypes } from "./get-table-node-types" +import { createCell } from "./create-cell"; +import { getTableNodeTypes } from "./get-table-node-types"; export function createTable( - schema: Schema, - rowsCount: number, - colsCount: number, - withHeaderRow: boolean, - cellContent?: Fragment | ProsemirrorNode | Array + schema: Schema, + rowsCount: number, + colsCount: number, + withHeaderRow: boolean, + cellContent?: Fragment | ProsemirrorNode | Array, ): ProsemirrorNode { - const types = getTableNodeTypes(schema) - const headerCells: ProsemirrorNode[] = [] - const cells: ProsemirrorNode[] = [] + const types = getTableNodeTypes(schema); + const headerCells: ProsemirrorNode[] = []; + const cells: ProsemirrorNode[] = []; - for (let index = 0; index < colsCount; index += 1) { - const cell = createCell(types.cell, cellContent) + for (let index = 0; index < colsCount; index += 1) { + const cell = createCell(types.cell, cellContent); - if (cell) { - cells.push(cell) - } - - if (withHeaderRow) { - const headerCell = createCell(types.header_cell, cellContent) - - if (headerCell) { - headerCells.push(headerCell) - } - } + if (cell) { + cells.push(cell); } - const rows: ProsemirrorNode[] = [] + if (withHeaderRow) { + const headerCell = createCell(types.header_cell, cellContent); - for (let index = 0; index < rowsCount; index += 1) { - rows.push( - types.row.createChecked( - null, - withHeaderRow && index === 0 ? headerCells : cells - ) - ) + if (headerCell) { + headerCells.push(headerCell); + } } + } - return types.table.createChecked(null, rows) + const rows: ProsemirrorNode[] = []; + + for (let index = 0; index < rowsCount; index += 1) { + rows.push( + types.row.createChecked( + null, + withHeaderRow && index === 0 ? headerCells : cells, + ), + ); + } + + return types.table.createChecked(null, rows); } diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts index dcb20b323..7fed53705 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts @@ -1,39 +1,42 @@ -import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core" +import { + findParentNodeClosestToPos, + KeyboardShortcutCommand, +} from "@tiptap/core"; -import { isCellSelection } from "./is-cell-selection" +import { isCellSelection } from "./is-cell-selection"; export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ - editor + editor, }) => { - const { selection } = editor.state + const { selection } = editor.state; - if (!isCellSelection(selection)) { - return false + if (!isCellSelection(selection)) { + return false; + } + + let cellCount = 0; + const table = findParentNodeClosestToPos( + selection.ranges[0].$from, + (node) => node.type.name === "table", + ); + + table?.node.descendants((node) => { + if (node.type.name === "table") { + return false; } - let cellCount = 0 - const table = findParentNodeClosestToPos( - selection.ranges[0].$from, - (node) => node.type.name === "table" - ) - - table?.node.descendants((node) => { - if (node.type.name === "table") { - return false - } - - if (["tableCell", "tableHeader"].includes(node.type.name)) { - cellCount += 1 - } - }) - - const allCellsSelected = cellCount === selection.ranges.length - - if (!allCellsSelected) { - return false + if (["tableCell", "tableHeader"].includes(node.type.name)) { + cellCount += 1; } + }); - editor.commands.deleteTable() + const allCellsSelected = cellCount === selection.ranges.length; - return true -} + if (!allCellsSelected) { + return false; + } + + editor.commands.deleteTable(); + + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts index 293878cb0..28c322a1f 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts @@ -1,21 +1,21 @@ -import { NodeType, Schema } from "prosemirror-model" +import { NodeType, Schema } from "prosemirror-model"; export function getTableNodeTypes(schema: Schema): { [key: string]: NodeType } { - if (schema.cached.tableNodeTypes) { - return schema.cached.tableNodeTypes + if (schema.cached.tableNodeTypes) { + return schema.cached.tableNodeTypes; + } + + const roles: { [key: string]: NodeType } = {}; + + Object.keys(schema.nodes).forEach((type) => { + const nodeType = schema.nodes[type]; + + if (nodeType.spec.tableRole) { + roles[nodeType.spec.tableRole] = nodeType; } + }); - const roles: { [key: string]: NodeType } = {} + schema.cached.tableNodeTypes = roles; - Object.keys(schema.nodes).forEach((type) => { - const nodeType = schema.nodes[type] - - if (nodeType.spec.tableRole) { - roles[nodeType.spec.tableRole] = nodeType - } - }) - - schema.cached.tableNodeTypes = roles - - return roles + return roles; } diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts index 3c36bf055..28917a299 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts @@ -1,5 +1,5 @@ -import { CellSelection } from "@tiptap/prosemirror-tables" +import { CellSelection } from "@tiptap/prosemirror-tables"; export function isCellSelection(value: unknown): value is CellSelection { - return value instanceof CellSelection + return value instanceof CellSelection; } diff --git a/packages/editor/core/src/ui/hooks/useEditor.tsx b/packages/editor/core/src/ui/hooks/useEditor.tsx index 9fcf200fb..258da8652 100644 --- a/packages/editor/core/src/ui/hooks/useEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useEditor.tsx @@ -29,11 +29,13 @@ interface CustomEditorProps { forwardedRef?: any; mentionHighlights?: string[]; mentionSuggestions?: IMentionSuggestion[]; + cancelUploadImage?: () => any; } export const useEditor = ({ uploadFile, deleteFile, + cancelUploadImage, editorProps = {}, value, extensions = [], @@ -42,7 +44,7 @@ export const useEditor = ({ forwardedRef, setShouldShowAlert, mentionHighlights, - mentionSuggestions + mentionSuggestions, }: CustomEditorProps) => { const editor = useCustomEditor( { @@ -50,7 +52,17 @@ export const useEditor = ({ ...CoreEditorProps(uploadFile, setIsSubmitting), ...editorProps, }, - extensions: [...CoreEditorExtensions({ mentionSuggestions: mentionSuggestions ?? [], mentionHighlights: mentionHighlights ?? []}, deleteFile), ...extensions], + extensions: [ + ...CoreEditorExtensions( + { + mentionSuggestions: mentionSuggestions ?? [], + mentionHighlights: mentionHighlights ?? [], + }, + deleteFile, + cancelUploadImage, + ), + ...extensions, + ], content: typeof value === "string" && value.trim() !== "" ? value : "

", onUpdate: async ({ editor }) => { @@ -82,4 +94,4 @@ export const useEditor = ({ } return editor; -}; \ No newline at end of file +}; diff --git a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx index 9243c2f4e..75ebddd3c 100644 --- a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx @@ -7,7 +7,7 @@ import { } from "react"; import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions"; import { CoreReadOnlyEditorProps } from "../../ui/read-only/props"; -import { EditorProps } from '@tiptap/pm/view'; +import { EditorProps } from "@tiptap/pm/view"; import { IMentionSuggestion } from "../../types/mention-suggestion"; interface CustomReadOnlyEditorProps { @@ -19,7 +19,14 @@ interface CustomReadOnlyEditorProps { mentionSuggestions?: IMentionSuggestion[]; } -export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editorProps = {}, mentionHighlights, mentionSuggestions}: CustomReadOnlyEditorProps) => { +export const useReadOnlyEditor = ({ + value, + forwardedRef, + extensions = [], + editorProps = {}, + mentionHighlights, + mentionSuggestions, +}: CustomReadOnlyEditorProps) => { const editor = useCustomEditor({ editable: false, content: @@ -28,7 +35,13 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor ...CoreReadOnlyEditorProps, ...editorProps, }, - extensions: [...CoreReadOnlyEditorExtensions({ mentionSuggestions: mentionSuggestions ?? [], mentionHighlights: mentionHighlights ?? []}), ...extensions], + extensions: [ + ...CoreReadOnlyEditorExtensions({ + mentionSuggestions: mentionSuggestions ?? [], + mentionHighlights: mentionHighlights ?? [], + }), + ...extensions, + ], }); const hasIntiliazedContent = useRef(false); diff --git a/packages/editor/core/src/ui/mentions/custom.tsx b/packages/editor/core/src/ui/mentions/custom.tsx index c3bfa3703..dc4ab5aad 100644 --- a/packages/editor/core/src/ui/mentions/custom.tsx +++ b/packages/editor/core/src/ui/mentions/custom.tsx @@ -1,11 +1,11 @@ -import { Mention, MentionOptions } from '@tiptap/extension-mention' -import { mergeAttributes } from '@tiptap/core' -import { ReactNodeViewRenderer } from '@tiptap/react' -import mentionNodeView from './mentionNodeView' -import { IMentionHighlight } from '../../types/mention-suggestion' +import { Mention, MentionOptions } from "@tiptap/extension-mention"; +import { mergeAttributes } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import mentionNodeView from "./mentionNodeView"; +import { IMentionHighlight } from "../../types/mention-suggestion"; export interface CustomMentionOptions extends MentionOptions { - mentionHighlights: IMentionHighlight[] - readonly?: boolean + mentionHighlights: IMentionHighlight[]; + readonly?: boolean; } export const CustomMention = Mention.extend({ @@ -21,35 +21,37 @@ export const CustomMention = Mention.extend({ default: null, }, self: { - default: false + default: false, }, redirect_uri: { - default: "/" - } - } + default: "/", + }, + }; }, addNodeView() { - return ReactNodeViewRenderer(mentionNodeView) + return ReactNodeViewRenderer(mentionNodeView); }, parseHTML() { - return [{ - tag: 'mention-component', - getAttrs: (node: string | HTMLElement) => { - if (typeof node === 'string') { - return null; - } - return { - id: node.getAttribute('data-mention-id') || '', - target: node.getAttribute('data-mention-target') || '', - label: node.innerText.slice(1) || '', - redirect_uri: node.getAttribute('redirect_uri') - } + return [ + { + tag: "mention-component", + getAttrs: (node: string | HTMLElement) => { + if (typeof node === "string") { + return null; + } + return { + id: node.getAttribute("data-mention-id") || "", + target: node.getAttribute("data-mention-target") || "", + label: node.innerText.slice(1) || "", + redirect_uri: node.getAttribute("redirect_uri"), + }; + }, }, - }] + ]; }, renderHTML({ HTMLAttributes }) { - return ['mention-component', mergeAttributes(HTMLAttributes)] + return ["mention-component", mergeAttributes(HTMLAttributes)]; }, -}) +}); diff --git a/packages/editor/core/src/ui/mentions/index.tsx b/packages/editor/core/src/ui/mentions/index.tsx index ba1a9ed0b..42ec92554 100644 --- a/packages/editor/core/src/ui/mentions/index.tsx +++ b/packages/editor/core/src/ui/mentions/index.tsx @@ -2,14 +2,21 @@ import suggestion from "./suggestion"; import { CustomMention } from "./custom"; -import { IMentionHighlight, IMentionSuggestion } from "../../types/mention-suggestion"; - -export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) => CustomMention.configure({ - HTMLAttributes: { - 'class' : "mention", - }, - readonly: readonly, - mentionHighlights: mentionHighlights, - suggestion: suggestion(mentionSuggestions), -}) +import { + IMentionHighlight, + IMentionSuggestion, +} from "../../types/mention-suggestion"; +export const Mentions = ( + mentionSuggestions: IMentionSuggestion[], + mentionHighlights: IMentionHighlight[], + readonly, +) => + CustomMention.configure({ + HTMLAttributes: { + class: "mention", + }, + readonly: readonly, + mentionHighlights: mentionHighlights, + suggestion: suggestion(mentionSuggestions), + }); diff --git a/packages/editor/core/src/ui/mentions/suggestion.ts b/packages/editor/core/src/ui/mentions/suggestion.ts index b4bbc53a6..ce09cb092 100644 --- a/packages/editor/core/src/ui/mentions/suggestion.ts +++ b/packages/editor/core/src/ui/mentions/suggestion.ts @@ -1,12 +1,17 @@ -import { ReactRenderer } from '@tiptap/react' +import { ReactRenderer } from "@tiptap/react"; import { Editor } from "@tiptap/core"; -import tippy from 'tippy.js' +import tippy from "tippy.js"; -import MentionList from './MentionList' -import { IMentionSuggestion } from '../../types/mention-suggestion'; +import MentionList from "./MentionList"; +import { IMentionSuggestion } from "../../types/mention-suggestion"; const Suggestion = (suggestions: IMentionSuggestion[]) => ({ - items: ({ query }: { query: string }) => suggestions.filter(suggestion => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5), + items: ({ query }: { query: string }) => + suggestions + .filter((suggestion) => + suggestion.title.toLowerCase().startsWith(query.toLowerCase()), + ) + .slice(0, 5), render: () => { let reactRenderer: ReactRenderer | null = null; let popup: any | null = null; @@ -30,7 +35,7 @@ const Suggestion = (suggestions: IMentionSuggestion[]) => ({ }, onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => { - reactRenderer?.updateProps(props) + reactRenderer?.updateProps(props); popup && popup[0].setProps({ @@ -49,11 +54,10 @@ const Suggestion = (suggestions: IMentionSuggestion[]) => ({ }, onExit: () => { popup?.[0].destroy(); - reactRenderer?.destroy() + reactRenderer?.destroy(); }, - } + }; }, -}) - +}); export default Suggestion; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertBottomTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertBottomTableIcon.tsx deleted file mode 100644 index 0e42ba648..000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertBottomTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -const InsertBottomTableIcon = (props: any) => ( - - - -); - -export default InsertBottomTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertLeftTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertLeftTableIcon.tsx deleted file mode 100644 index 1fd75fe87..000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertLeftTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const InsertLeftTableIcon = (props: any) => ( - - - -); -export default InsertLeftTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertRightTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertRightTableIcon.tsx deleted file mode 100644 index 1a6570969..000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertRightTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -const InsertRightTableIcon = (props: any) => ( - - - -); - -export default InsertRightTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertTopTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertTopTableIcon.tsx deleted file mode 100644 index 8f04f4f61..000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertTopTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const InsertTopTableIcon = (props: any) => ( - - - -); -export default InsertTopTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/tooltip.tsx b/packages/editor/core/src/ui/menus/table-menu/tooltip.tsx deleted file mode 100644 index f29d8a491..000000000 --- a/packages/editor/core/src/ui/menus/table-menu/tooltip.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from 'react'; - -// next-themes -import { useTheme } from "next-themes"; -// tooltip2 -import { Tooltip2 } from "@blueprintjs/popover2"; - -type Props = { - tooltipHeading?: string; - tooltipContent: string | React.ReactNode; - position?: - | "top" - | "right" - | "bottom" - | "left" - | "auto" - | "auto-end" - | "auto-start" - | "bottom-left" - | "bottom-right" - | "left-bottom" - | "left-top" - | "right-bottom" - | "right-top" - | "top-left" - | "top-right"; - children: JSX.Element; - disabled?: boolean; - className?: string; - openDelay?: number; - closeDelay?: number; -}; - -export const Tooltip: React.FC = ({ - tooltipHeading, - tooltipContent, - position = "top", - children, - disabled = false, - className = "", - openDelay = 200, - closeDelay, -}) => { - const { theme } = useTheme(); - - return ( - - {tooltipHeading && ( -
- {tooltipHeading} -
- )} - {tooltipContent} - - } - position={position} - renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => - React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) - } - /> - ); -}; diff --git a/packages/editor/core/src/ui/plugins/delete-image.tsx b/packages/editor/core/src/ui/plugins/delete-image.tsx index 56284472b..48ec244fc 100644 --- a/packages/editor/core/src/ui/plugins/delete-image.tsx +++ b/packages/editor/core/src/ui/plugins/delete-image.tsx @@ -15,7 +15,11 @@ interface ImageNode extends ProseMirrorNode { const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => new Plugin({ key: deleteKey, - appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + appendTransaction: ( + transactions: readonly Transaction[], + oldState: EditorState, + newState: EditorState, + ) => { const newImageSources = new Set(); newState.doc.descendants((node) => { if (node.type.name === IMAGE_NODE_TYPE) { @@ -55,7 +59,10 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => export default TrackImageDeletionPlugin; -async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise { +async function onNodeDeleted( + src: string, + deleteImage: DeleteImage, +): Promise { try { const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); const resStatus = await deleteImage(assetUrlWithWorkspaceId); diff --git a/packages/editor/core/src/ui/plugins/upload-image.tsx b/packages/editor/core/src/ui/plugins/upload-image.tsx index cdd62ae48..256460073 100644 --- a/packages/editor/core/src/ui/plugins/upload-image.tsx +++ b/packages/editor/core/src/ui/plugins/upload-image.tsx @@ -4,7 +4,7 @@ import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; const uploadKey = new PluginKey("upload-image"); -const UploadImagesPlugin = () => +const UploadImagesPlugin = (cancelUploadImage?: () => any) => new Plugin({ key: uploadKey, state: { @@ -21,15 +21,46 @@ const UploadImagesPlugin = () => const placeholder = document.createElement("div"); placeholder.setAttribute("class", "img-placeholder"); const image = document.createElement("img"); - image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300"); + image.setAttribute( + "class", + "opacity-10 rounded-lg border border-custom-border-300", + ); image.src = src; placeholder.appendChild(image); + + // Create cancel button + const cancelButton = document.createElement("button"); + cancelButton.style.position = "absolute"; + cancelButton.style.right = "3px"; + cancelButton.style.top = "3px"; + cancelButton.setAttribute("class", "opacity-90 rounded-lg"); + + cancelButton.onclick = () => { + cancelUploadImage?.(); + }; + + // Create an SVG element from the SVG string + const svgString = ``; + const parser = new DOMParser(); + const svgElement = parser.parseFromString( + svgString, + "image/svg+xml", + ).documentElement; + + cancelButton.appendChild(svgElement); + placeholder.appendChild(cancelButton); const deco = Decoration.widget(pos + 1, placeholder, { id, }); set = set.add(tr.doc, [deco]); } else if (action && action.remove) { - set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id)); + set = set.remove( + set.find( + undefined, + undefined, + (spec) => spec.id == action.remove.id, + ), + ); } return set; }, @@ -48,19 +79,39 @@ function findPlaceholder(state: EditorState, id: {}) { const found = decos.find( undefined, undefined, - (spec: { id: number | undefined }) => spec.id == id + (spec: { id: number | undefined }) => spec.id == id, ); return found.length ? found[0].from : null; } +const removePlaceholder = (view: EditorView, id: {}) => { + const removePlaceholderTr = view.state.tr.setMeta(uploadKey, { + remove: { id }, + }); + view.dispatch(removePlaceholderTr); +}; + export async function startImageUpload( file: File, view: EditorView, pos: number, uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ) { + if (!file) { + alert("No file selected. Please select a file to upload."); + return; + } + if (!file.type.includes("image/")) { + alert("Invalid file type. Please select an image file."); + return; + } + + if (file.size > 5 * 1024 * 1024) { + alert("File size too large. Please select a file smaller than 5MB."); return; } @@ -82,28 +133,42 @@ export async function startImageUpload( view.dispatch(tr); }; + // Handle FileReader errors + reader.onerror = (error) => { + console.error("FileReader error: ", error); + removePlaceholder(view, id); + return; + }; + setIsSubmitting?.("submitting"); - const src = await UploadImageHandler(file, uploadFile); - const { schema } = view.state; - pos = findPlaceholder(view.state, id); - if (pos == null) return; - const imageSrc = typeof src === "object" ? reader.result : src; + try { + const src = await UploadImageHandler(file, uploadFile); + const { schema } = view.state; + pos = findPlaceholder(view.state, id); - const node = schema.nodes.image.create({ src: imageSrc }); - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); + if (pos == null) return; + const imageSrc = typeof src === "object" ? reader.result : src; + + const node = schema.nodes.image.create({ src: imageSrc }); + const transaction = view.state.tr + .replaceWith(pos, pos, node) + .setMeta(uploadKey, { remove: { id } }); + view.dispatch(transaction); + } catch (error) { + console.error("Upload error: ", error); + removePlaceholder(view, id); + } } -const UploadImageHandler = (file: File, - uploadFile: UploadImage +const UploadImageHandler = ( + file: File, + uploadFile: UploadImage, ): Promise => { try { return new Promise(async (resolve, reject) => { try { - const imageUrl = await uploadFile(file) + const imageUrl = await uploadFile(file); const image = new Image(); image.src = imageUrl; @@ -118,9 +183,6 @@ const UploadImageHandler = (file: File, } }); } catch (error) { - if (error instanceof Error) { - console.log(error.message); - } return Promise.reject(error); } }; diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx index 8f002b76c..865e0d2c7 100644 --- a/packages/editor/core/src/ui/props.tsx +++ b/packages/editor/core/src/ui/props.tsx @@ -5,7 +5,9 @@ import { UploadImage } from "../types/upload-image"; export function CoreEditorProps( uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ): EditorProps { return { attributes: { @@ -32,7 +34,11 @@ export function CoreEditorProps( } } } - if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { + if ( + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files[0] + ) { event.preventDefault(); const file = event.clipboardData.files[0]; const pos = view.state.selection.from; @@ -51,7 +57,12 @@ export function CoreEditorProps( } } } - if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { + if ( + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files[0] + ) { event.preventDefault(); const file = event.dataTransfer.files[0]; const coordinates = view.posAtCoords({ @@ -59,7 +70,13 @@ export function CoreEditorProps( top: event.clientY, }); if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting); + startImageUpload( + file, + view, + coordinates.pos - 1, + uploadFile, + setIsSubmitting, + ); } return true; } diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx index 8901d34c5..b8fc9bb95 100644 --- a/packages/editor/core/src/ui/read-only/extensions.tsx +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -18,9 +18,10 @@ import { isValidHttpUrl } from "../../lib/utils"; import { Mentions } from "../mentions"; import { IMentionSuggestion } from "../../types/mention-suggestion"; -export const CoreReadOnlyEditorExtensions = ( - mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] }, -) => [ +export const CoreReadOnlyEditorExtensions = (mentionConfig: { + mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: string[]; +}) => [ StarterKit.configure({ bulletList: { HTMLAttributes: { @@ -57,41 +58,45 @@ export const CoreReadOnlyEditorExtensions = ( }, gapcursor: false, }), - Gapcursor, - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), - ReadOnlyImageExtension.configure({ - HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", - }, - }), - TiptapUnderline, - TextStyle, - Color, - TaskList.configure({ - HTMLAttributes: { - class: "not-prose pl-2", - }, - }), - TaskItem.configure({ - HTMLAttributes: { - class: "flex items-start my-4", - }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), - Table, - TableHeader, - TableCell, - TableRow, - Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true), - ]; + Gapcursor, + TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), + HTMLAttributes: { + class: + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", + }, + }), + ReadOnlyImageExtension.configure({ + HTMLAttributes: { + class: "rounded-lg border border-custom-border-300", + }, + }), + TiptapUnderline, + TextStyle, + Color, + TaskList.configure({ + HTMLAttributes: { + class: "not-prose pl-2", + }, + }), + TaskItem.configure({ + HTMLAttributes: { + class: "flex items-start my-4", + }, + nested: true, + }), + Markdown.configure({ + html: true, + transformCopiedText: true, + }), + Table, + TableHeader, + TableCell, + TableRow, + Mentions( + mentionConfig.mentionSuggestions, + mentionConfig.mentionHighlights, + true, + ), +]; diff --git a/packages/editor/core/src/ui/read-only/props.tsx b/packages/editor/core/src/ui/read-only/props.tsx index 25db2b68c..79f9fcb0d 100644 --- a/packages/editor/core/src/ui/read-only/props.tsx +++ b/packages/editor/core/src/ui/read-only/props.tsx @@ -1,7 +1,6 @@ import { EditorProps } from "@tiptap/pm/view"; -export const CoreReadOnlyEditorProps: EditorProps = -{ +export const CoreReadOnlyEditorProps: EditorProps = { attributes: { class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, }, diff --git a/packages/editor/lite-text-editor/Readme.md b/packages/editor/lite-text-editor/Readme.md index 948e2c34b..1f10f5ff4 100644 --- a/packages/editor/lite-text-editor/Readme.md +++ b/packages/editor/lite-text-editor/Readme.md @@ -10,25 +10,25 @@ The `@plane/lite-text-editor` package extends from the `editor-core` package, in `LiteTextEditor` & `LiteTextEditorWithRef` -- **Read Only Editor Instances**: We have added a really light weight *Read Only* Editor instance for the Lite editor types (with and without Ref) +- **Read Only Editor Instances**: We have added a really light weight _Read Only_ Editor instance for the Lite editor types (with and without Ref) `LiteReadOnlyEditor` &`LiteReadOnlyEditorWithRef` ## LiteTextEditor -| Prop | Type | Description | -| --- | --- | --- | -| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | -| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | -| `value` | `html string` | The initial content of the editor. | -| `onEnterKeyPress` | `(e) => void` | The event that happens on Enter key press | -| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | -| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | -| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | -| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | +| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | +| `value` | `html string` | The initial content of the editor. | +| `onEnterKeyPress` | `(e) => void` | The event that happens on Enter key press | +| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | +| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | +| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | +| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage @@ -36,62 +36,62 @@ The `@plane/lite-text-editor` package extends from the `editor-core` package, in ```tsx { - onChange(comment_html); - }} - /> + onEnterKeyPress={handleSubmit(handleCommentUpdate)} + uploadFile={fileService.getUploadFileFunction(workspaceSlug)} + deleteFile={fileService.deleteImage} + value={value} + debouncedUpdatesEnabled={false} + customClassName="min-h-[50px] p-3 shadow-sm" + onChange={(comment_json: Object, comment_html: string) => { + onChange(comment_html); + }} +/> ``` 2. Example of how to use the `LiteTextEditorWithRef` component ```tsx - const editorRef = useRef(null); +const editorRef = useRef(null); - // can use it to set the editor's value - editorRef.current?.setEditorValue(`${watch("description_html")}`); +// can use it to set the editor's value +editorRef.current?.setEditorValue(`${watch("description_html")}`); - // can use it to clear the editor - editorRef?.current?.clearEditor(); +// can use it to clear the editor +editorRef?.current?.clearEditor(); - return ( - { - onChange(comment_html); - }} - /> -) +return ( + { + onChange(comment_html); + }} + /> +); ``` ## LiteReadOnlyEditor -| Prop | Type | Description | -| --- | --- | --- | -| `value` | `html string` | The initial content of the editor. | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ------------- | --------------------------------------------------------------------- | +| `value` | `html string` | The initial content of the editor. | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage Here is an example of how to use the `RichReadOnlyEditor` component ```tsx - + ``` diff --git a/packages/editor/lite-text-editor/package.json b/packages/editor/lite-text-editor/package.json index 3b6cd720b..52f27fb29 100644 --- a/packages/editor/lite-text-editor/package.json +++ b/packages/editor/lite-text-editor/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@plane/editor-core": "*", + "@plane/ui": "*", "@tiptap/extension-list-item": "^2.1.11", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", diff --git a/packages/editor/lite-text-editor/src/index.ts b/packages/editor/lite-text-editor/src/index.ts index 392928ccf..ba916e666 100644 --- a/packages/editor/lite-text-editor/src/index.ts +++ b/packages/editor/lite-text-editor/src/index.ts @@ -1,3 +1,3 @@ export { LiteTextEditor, LiteTextEditorWithRef } from "./ui"; export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only"; -export type { IMentionSuggestion, IMentionHighlight } from "./ui" +export type { IMentionSuggestion, IMentionHighlight } from "./ui"; diff --git a/packages/editor/lite-text-editor/src/ui/index.tsx b/packages/editor/lite-text-editor/src/ui/index.tsx index ef321d511..e7decbcac 100644 --- a/packages/editor/lite-text-editor/src/ui/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/index.tsx @@ -31,7 +31,7 @@ interface ILiteTextEditor { editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; setIsSubmitting?: ( - isSubmitting: "submitting" | "submitted" | "saved" + isSubmitting: "submitting" | "submitted" | "saved", ) => void; setShouldShowAlert?: (showAlert: boolean) => void; forwardedRef?: any; @@ -47,6 +47,7 @@ interface ILiteTextEditor { }[]; }; onEnterKeyPress?: (e?: any) => void; + cancelUploadImage?: () => any; mentionHighlights?: string[]; mentionSuggestions?: IMentionSuggestion[]; submitButton?: React.ReactNode; @@ -64,6 +65,7 @@ interface EditorHandle { const LiteTextEditor = (props: LiteTextEditorProps) => { const { onChange, + cancelUploadImage, debouncedUpdatesEnabled, setIsSubmitting, setShouldShowAlert, @@ -84,6 +86,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => { const editor = useEditor({ onChange, + cancelUploadImage, debouncedUpdatesEnabled, setIsSubmitting, setShouldShowAlert, @@ -126,7 +129,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => { }; const LiteTextEditorWithRef = React.forwardRef( - (props, ref) => + (props, ref) => , ); LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef"; diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx index c0006b3f2..60878f9bf 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx @@ -6,8 +6,9 @@ type Props = { }; export const Icon: React.FC = ({ iconName, className = "" }) => ( - + {iconName} ); - diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx index cf0d78688..a4fb0479c 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -14,8 +14,8 @@ import { TableItem, UnderLineItem, } from "@plane/editor-core"; -import { Tooltip } from "../../tooltip"; -import { UploadImage } from "../.."; +import { Tooltip } from "@plane/ui"; +import { UploadImage } from "../../"; export interface BubbleMenuItem { name: string; diff --git a/packages/editor/rich-text-editor/Readme.md b/packages/editor/rich-text-editor/Readme.md index c8414f62d..44ed9ba5e 100644 --- a/packages/editor/rich-text-editor/Readme.md +++ b/packages/editor/rich-text-editor/Readme.md @@ -10,24 +10,24 @@ The `@plane/rich-text-editor` package extends from the `editor-core` package, in `RichTextEditor` & `RichTextEditorWithRef` -- **Read Only Editor Instances**: We have added a really light weight *Read Only* Editor instance for the Rich editor types (with and without Ref) +- **Read Only Editor Instances**: We have added a really light weight _Read Only_ Editor instance for the Rich editor types (with and without Ref) `RichReadOnlyEditor` &`RichReadOnlyEditorWithRef` ## RichTextEditor -| Prop | Type | Description | -| --- | --- | --- | -| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | -| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | -| `value` | `html string` | The initial content of the editor. | -| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | -| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | -| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | -| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | +| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | +| `value` | `html string` | The initial content of the editor. | +| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | +| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | +| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | +| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage @@ -57,43 +57,47 @@ The `@plane/rich-text-editor` package extends from the `editor-core` package, in 2. Example of how to use the `RichTextEditorWithRef` component ```tsx - const editorRef = useRef(null); +const editorRef = useRef(null); - // can use it to set the editor's value - editorRef.current?.setEditorValue(`${watch("description_html")}`); +// can use it to set the editor's value +editorRef.current?.setEditorValue(`${watch("description_html")}`); - // can use it to clear the editor - editorRef?.current?.clearEditor(); +// can use it to clear the editor +editorRef?.current?.clearEditor(); - return ( { - onChange(description_html); - // custom stuff you want to do - } } />) +return ( + { + onChange(description_html); + // custom stuff you want to do + }} + /> +); ``` ## RichReadOnlyEditor -| Prop | Type | Description | -| --- | --- | --- | -| `value` | `html string` | The initial content of the editor. | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ------------- | --------------------------------------------------------------------- | +| `value` | `html string` | The initial content of the editor. | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage Here is an example of how to use the `RichReadOnlyEditor` component ```tsx - + ``` diff --git a/packages/editor/rich-text-editor/src/index.ts b/packages/editor/rich-text-editor/src/index.ts index e296a6171..9ea7f9a39 100644 --- a/packages/editor/rich-text-editor/src/index.ts +++ b/packages/editor/rich-text-editor/src/index.ts @@ -2,4 +2,4 @@ import "./styles/github-dark.css"; export { RichTextEditor, RichTextEditorWithRef } from "./ui"; export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only"; -export type { IMentionSuggestion, IMentionHighlight } from "./ui" +export type { IMentionSuggestion, IMentionHighlight } from "./ui"; diff --git a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx index f0f3bed34..a28982da3 100644 --- a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx @@ -1,7 +1,7 @@ import HorizontalRule from "@tiptap/extension-horizontal-rule"; import Placeholder from "@tiptap/extension-placeholder"; import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; -import { common, createLowlight } from 'lowlight' +import { common, createLowlight } from "lowlight"; import { InputRule } from "@tiptap/core"; import ts from "highlight.js/lib/languages/typescript"; @@ -9,51 +9,53 @@ import ts from "highlight.js/lib/languages/typescript"; import SlashCommand from "./slash-command"; import { UploadImage } from "../"; -const lowlight = createLowlight(common) +const lowlight = createLowlight(common); lowlight.register("ts", ts); export const RichTextEditorExtensions = ( uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ) => [ - HorizontalRule.extend({ - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, commands }) => { - commands.splitBlock(); + HorizontalRule.extend({ + addInputRules() { + return [ + new InputRule({ + find: /^(?:---|—-|___\s|\*\*\*\s)$/, + handler: ({ state, range, commands }) => { + commands.splitBlock(); - const attributes = {}; - const { tr } = state; - const start = range.from; - const end = range.to; - // @ts-ignore - tr.replaceWith(start - 1, end, this.type.create(attributes)); - }, - }), - ]; - }, - }).configure({ - HTMLAttributes: { - class: "mb-6 border-t border-custom-border-300", - }, - }), - SlashCommand(uploadFile, setIsSubmitting), - CodeBlockLowlight.configure({ - lowlight, - }), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } - if (node.type.name === "image" || node.type.name === "table") { - return ""; - } + const attributes = {}; + const { tr } = state; + const start = range.from; + const end = range.to; + // @ts-ignore + tr.replaceWith(start - 1, end, this.type.create(attributes)); + }, + }), + ]; + }, + }).configure({ + HTMLAttributes: { + class: "mb-6 border-t border-custom-border-300", + }, + }), + SlashCommand(uploadFile, setIsSubmitting), + CodeBlockLowlight.configure({ + lowlight, + }), + Placeholder.configure({ + placeholder: ({ node }) => { + if (node.type.name === "heading") { + return `Heading ${node.attrs.level}`; + } + if (node.type.name === "image" || node.type.name === "table") { + return ""; + } - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - ]; + return "Press '/' for commands..."; + }, + includeChildren: true, + }), +]; diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index a0dbe7226..2e98a72aa 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -1,8 +1,13 @@ -"use client" -import * as React from 'react'; -import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from '@plane/editor-core'; -import { EditorBubbleMenu } from './menus/bubble-menu'; -import { RichTextEditorExtensions } from './extensions'; +"use client"; +import * as React from "react"; +import { + EditorContainer, + EditorContentWrapper, + getEditorClassNames, + useEditor, +} from "@plane/editor-core"; +import { EditorBubbleMenu } from "./menus/bubble-menu"; +import { RichTextEditorExtensions } from "./extensions"; export type UploadImage = (file: File) => Promise; export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; @@ -14,9 +19,9 @@ export type IMentionSuggestion = { title: string; subtitle: string; redirect_uri: string; -} +}; -export type IMentionHighlight = string +export type IMentionHighlight = string; interface IRichTextEditor { value: string; @@ -24,10 +29,13 @@ interface IRichTextEditor { deleteFile: DeleteImage; noBorder?: boolean; borderOnFocus?: boolean; + cancelUploadImage?: () => any; customClassName?: string; editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void; setShouldShowAlert?: (showAlert: boolean) => void; forwardedRef?: any; debouncedUpdatesEnabled?: boolean; @@ -54,11 +62,12 @@ const RichTextEditor = ({ uploadFile, deleteFile, noBorder, + cancelUploadImage, borderOnFocus, customClassName, forwardedRef, mentionHighlights, - mentionSuggestions + mentionSuggestions, }: RichTextEditorProps) => { const editor = useEditor({ onChange, @@ -67,14 +76,19 @@ const RichTextEditor = ({ setShouldShowAlert, value, uploadFile, + cancelUploadImage, deleteFile, forwardedRef, extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting), mentionHighlights, - mentionSuggestions + mentionSuggestions, }); - const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + const editorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, + }); if (!editor) return null; @@ -82,16 +96,19 @@ const RichTextEditor = ({ {editor && }
- +
-
+ ); }; -const RichTextEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const RichTextEditorWithRef = React.forwardRef( + (props, ref) => , +); RichTextEditorWithRef.displayName = "RichTextEditorWithRef"; -export { RichTextEditor, RichTextEditorWithRef}; +export { RichTextEditor, RichTextEditorWithRef }; diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx index 7dddc9d98..f8f1f17bb 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx @@ -1,7 +1,19 @@ import { Editor } from "@tiptap/core"; import { Check, Trash } from "lucide-react"; -import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; -import { cn, isValidHttpUrl, setLinkEditor, unsetLinkEditor, } from "@plane/editor-core"; +import { + Dispatch, + FC, + SetStateAction, + useCallback, + useEffect, + useRef, +} from "react"; +import { + cn, + isValidHttpUrl, + setLinkEditor, + unsetLinkEditor, +} from "@plane/editor-core"; interface LinkSelectorProps { editor: Editor; @@ -9,7 +21,11 @@ interface LinkSelectorProps { setIsOpen: Dispatch>; } -export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { +export const LinkSelector: FC = ({ + editor, + isOpen, + setIsOpen, +}) => { const inputRef = useRef(null); const onLinkSubmit = useCallback(() => { @@ -31,7 +47,7 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen type="button" className={cn( "flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100", - { "bg-custom-background-100": isOpen } + { "bg-custom-background-100": isOpen }, )} onClick={() => { setIsOpen(!isOpen); diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx index b8b7ffc58..965e7a42e 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx @@ -1,10 +1,16 @@ -import { BulletListItem, cn, CodeItem, HeadingOneItem, HeadingThreeItem, HeadingTwoItem, NumberedListItem, QuoteItem, TodoListItem } from "@plane/editor-core"; -import { Editor } from "@tiptap/react"; import { - Check, - ChevronDown, - TextIcon, -} from "lucide-react"; + BulletListItem, + cn, + CodeItem, + HeadingOneItem, + HeadingThreeItem, + HeadingTwoItem, + NumberedListItem, + QuoteItem, + TodoListItem, +} from "@plane/editor-core"; +import { Editor } from "@tiptap/react"; +import { Check, ChevronDown, TextIcon } from "lucide-react"; import { Dispatch, FC, SetStateAction } from "react"; import { BubbleMenuItem } from "."; @@ -15,12 +21,17 @@ interface NodeSelectorProps { setIsOpen: Dispatch>; } -export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { +export const NodeSelector: FC = ({ + editor, + isOpen, + setIsOpen, +}) => { const items: BubbleMenuItem[] = [ { name: "Text", icon: TextIcon, - command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), + command: () => + editor.chain().focus().toggleNode("paragraph", "paragraph").run(), isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && @@ -63,7 +74,10 @@ export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }} className={cn( "flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100", - { "bg-custom-primary-100/5 text-custom-text-100": activeItem.name === item.name } + { + "bg-custom-primary-100/5 text-custom-text-100": + activeItem.name === item.name, + }, )} >
diff --git a/packages/editor/rich-text-editor/src/ui/read-only/index.tsx b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx index 46905f263..f6ccdddf5 100644 --- a/packages/editor/rich-text-editor/src/ui/read-only/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx @@ -1,6 +1,11 @@ -"use client" -import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from '@plane/editor-core'; -import * as React from 'react'; +"use client"; +import { + EditorContainer, + EditorContentWrapper, + getEditorClassNames, + useReadOnlyEditor, +} from "@plane/editor-core"; +import * as React from "react"; interface IRichTextReadOnlyEditor { value: string; @@ -35,23 +40,31 @@ const RichReadOnlyEditor = ({ mentionHighlights, }); - const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + const editorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, + }); if (!editor) return null; return (
- +
-
+ ); }; -const RichReadOnlyEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const RichReadOnlyEditorWithRef = React.forwardRef< + EditorHandle, + IRichTextReadOnlyEditor +>((props, ref) => ); RichReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef"; -export { RichReadOnlyEditor , RichReadOnlyEditorWithRef }; +export { RichReadOnlyEditor, RichReadOnlyEditorWithRef }; diff --git a/packages/ui/src/avatar/avatar.tsx b/packages/ui/src/avatar/avatar.tsx index 674d82a26..431d693c9 100644 --- a/packages/ui/src/avatar/avatar.tsx +++ b/packages/ui/src/avatar/avatar.tsx @@ -123,7 +123,7 @@ export const Avatar: React.FC = (props) => { size = "md", shape = "circle", src, - className = "" + className = "", } = props; // get size details based on the size prop @@ -157,7 +157,9 @@ export const Avatar: React.FC = (props) => {
( )} ); - } + }, ); Button.displayName = "plane-ui-button"; diff --git a/packages/ui/src/button/helper.tsx b/packages/ui/src/button/helper.tsx index 82489c3e8..48b1fc94a 100644 --- a/packages/ui/src/button/helper.tsx +++ b/packages/ui/src/button/helper.tsx @@ -102,7 +102,7 @@ export const buttonStyling: IButtonStyling = { export const getButtonStyling = ( variant: TButtonVariant, size: TButtonSizes, - disabled: boolean = false + disabled: boolean = false, ): string => { let _variant: string = ``; const currentVariant = buttonStyling[variant]; diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index 8ba95c28c..0fb4c67cf 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -35,7 +35,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState( - null + null, ); const { styles, attributes } = usePopper(referenceElement, popperElement, { @@ -46,7 +46,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { query === "" ? options : options?.filter((option) => - option.query.toLowerCase().includes(query.toLowerCase()) + option.query.toLowerCase().includes(query.toLowerCase()), ); const comboboxProps: any = { @@ -87,8 +87,8 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
- ) + ); }; diff --git a/space/services/file.service.ts b/space/services/file.service.ts index 1ba4cd4d2..ce1f50e70 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -1,5 +1,6 @@ import APIService from "services/api.service"; import { API_BASE_URL } from "helpers/common.helper"; +import axios from "axios"; interface UnSplashImage { id: string; @@ -26,25 +27,37 @@ interface UnSplashImageUrls { } class FileService extends APIService { + private cancelSource: any; + constructor() { super(API_BASE_URL); this.uploadFile = this.uploadFile.bind(this); this.deleteImage = this.deleteImage.bind(this); + this.cancelUpload = this.cancelUpload.bind(this); } async uploadFile(workspaceSlug: string, file: FormData): Promise { + this.cancelSource = axios.CancelToken.source(); return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { headers: { ...this.getHeaders(), "Content-Type": "multipart/form-data", }, + cancelToken: this.cancelSource.token, }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + if (axios.isCancel(error)) { + console.log(error.message); + } else { + throw error?.response?.data; + } }); } + cancelUpload() { + this.cancelSource.cancel("Upload cancelled"); + } getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { return async (file: File) => { const formData = new FormData(); diff --git a/space/store/mentions.store.ts b/space/store/mentions.store.ts index ca4a1a3c1..e890681d3 100644 --- a/space/store/mentions.store.ts +++ b/space/store/mentions.store.ts @@ -3,43 +3,41 @@ import { RootStore } from "./root"; import { computed, makeObservable } from "mobx"; export interface IMentionsStore { - // mentionSuggestions: IMentionSuggestion[]; - mentionHighlights: IMentionHighlight[]; + // mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: IMentionHighlight[]; } -export class MentionsStore implements IMentionsStore{ +export class MentionsStore implements IMentionsStore { + // root store + rootStore; - // root store - rootStore; + constructor(_rootStore: RootStore) { + // rootStore + this.rootStore = _rootStore; - constructor(_rootStore: RootStore ){ + makeObservable(this, { + mentionHighlights: computed, + // mentionSuggestions: computed + }); + } - // rootStore - this.rootStore = _rootStore; + // get mentionSuggestions() { + // const projectMembers = this.rootStore.project.project. - makeObservable(this, { - mentionHighlights: computed, - // mentionSuggestions: computed - }) - } + // const suggestions = projectMembers === null ? [] : projectMembers.map((member) => ({ + // id: member.member.id, + // type: "User", + // title: member.member.display_name, + // subtitle: member.member.email ?? "", + // avatar: member.member.avatar, + // redirect_uri: `/${member.workspace.slug}/profile/${member.member.id}`, + // })) - // get mentionSuggestions() { - // const projectMembers = this.rootStore.project.project. + // return suggestions + // } - // const suggestions = projectMembers === null ? [] : projectMembers.map((member) => ({ - // id: member.member.id, - // type: "User", - // title: member.member.display_name, - // subtitle: member.member.email ?? "", - // avatar: member.member.avatar, - // redirect_uri: `/${member.workspace.slug}/profile/${member.member.id}`, - // })) - - // return suggestions - // } - - get mentionHighlights() { - const user = this.rootStore.user.currentUser; - return user ? [user.id] : [] - } -} \ No newline at end of file + get mentionHighlights() { + const user = this.rootStore.user.currentUser; + return user ? [user.id] : []; + } +} diff --git a/space/styles/table.css b/space/styles/table.css index ad88fd10e..8a47a8c59 100644 --- a/space/styles/table.css +++ b/space/styles/table.css @@ -92,7 +92,7 @@ transform: translateY(-50%); } -.tableWrapper .tableControls .columnsControl > button { +.tableWrapper .tableControls .columnsControl .columnsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); width: 30px; @@ -104,26 +104,42 @@ transform: translateX(-50%); } -.tableWrapper .tableControls .rowsControl > button { +.tableWrapper .tableControls .rowsControl .rowsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); height: 30px; width: 15px; } -.tableWrapper .tableControls button { +.tableWrapper .tableControls .rowsControlDiv { background-color: rgba(var(--color-primary-100)); border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; background-size: 1.25rem; background-repeat: no-repeat; background-position: center; - transition: transform ease-out 100ms, background-color ease-out 100ms; + transition: + transform ease-out 100ms, + background-color ease-out 100ms; outline: none; box-shadow: #000 0px 2px 4px; cursor: pointer; } +.tableWrapper .tableControls .columnsControlDiv { + background-color: rgba(var(--color-primary-100)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 2px; + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: center; + transition: + transform ease-out 100ms, + background-color ease-out 100ms; + outline: none; + box-shadow: #000 0px 2px 4px; + cursor: pointer; +} .tableWrapper .tableControls .tableToolbox, .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300)); diff --git a/web/components/account/github-login-button.tsx b/web/components/account/github-login-button.tsx index 9ea5b7df2..fc140f632 100644 --- a/web/components/account/github-login-button.tsx +++ b/web/components/account/github-login-button.tsx @@ -32,8 +32,7 @@ export const GithubLoginButton: FC = (props) => { }, [code, gitCode, handleSignIn]); useEffect(() => { - const origin = - typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; setLoginCallBackURL(`${origin}/` as any); }, []); diff --git a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx index e58d81666..3544ae46d 100644 --- a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -49,10 +49,7 @@ export const CustomTooltip: React.FC = ({ datum, analytics, params }) => : "" }`} > - {params.segment === "assignees__id" - ? renderAssigneeName(tooltipValue.toString()) - : tooltipValue} - : + {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: {datum.value}
diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 181eec8bd..5fc21a3ec 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -114,6 +114,7 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param y={datum.y} textAnchor="end" fontSize={10} + fill="rgb(var(--color-text-200))" className={`${barGraphData.data.length > 7 ? "-rotate-45" : ""}`} > {generateDisplayName(datum.value, analytics, params, "x_axis")} diff --git a/web/components/analytics/scope-and-demand/scope.tsx b/web/components/analytics/scope-and-demand/scope.tsx index 9231947bd..4c69a23c5 100644 --- a/web/components/analytics/scope-and-demand/scope.tsx +++ b/web/components/analytics/scope-and-demand/scope.tsx @@ -22,9 +22,7 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => ( keys={["count"]} height="250px" colors={() => `#f97316`} - customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => - d.count > 0 ? d.count : 50 - )} + customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => (d.count > 0 ? d.count : 50))} tooltip={(datum) => { const assignee = defaultAnalytics.pending_issue_user.find( (a) => a.assignees__id === `${datum.indexValue}` diff --git a/web/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx index 32d96eff2..509bc1e84 100644 --- a/web/components/auth-screens/not-authorized-view.tsx +++ b/web/components/auth-screens/not-authorized-view.tsx @@ -31,9 +31,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { alt="ProjectSettingImg" /> -

- Oops! You are not authorized to view this page -

+

Oops! You are not authorized to view this page

{user ? ( diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 966892595..1083073da 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; - +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // component -import { CustomSelect, ToggleSwitch } from "@plane/ui"; +import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; import { SelectMonthModal } from "components/automation"; // icon import { ArchiveRestore } from "lucide-react"; @@ -11,15 +13,21 @@ import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; import { IProject } from "types"; type Props = { - projectDetails: IProject | undefined; handleChange: (formData: Partial) => Promise; - disabled?: boolean; }; -export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleChange, disabled = false }) => { +const initialValues: Partial = { archive_in: 1 }; + +export const AutoArchiveAutomation: React.FC = observer((props) => { + const { handleChange } = props; + // states const [monthModal, setmonthModal] = useState(false); - const initialValues: Partial = { archive_in: 1 }; + const { user: userStore, project: projectStore } = useMobxStore(); + + const projectDetails = projectStore.currentProjectDetails; + const userRole = userStore.currentProjectRole; + return ( <> = ({ projectDetails, handleC projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 }) } size="sm" - disabled={disabled} + disabled={userRole !== 20} />
- {projectDetails?.archive_in !== 0 && ( -
-
-
Auto-archive issues that are closed for
-
- { - handleChange({ archive_in: val }); - }} - input - width="w-full" - disabled={disabled} - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} + {projectDetails ? ( + projectDetails.archive_in !== 0 && ( +
+
+
Auto-archive issues that are closed for
+
+ { + handleChange({ archive_in: val }); + }} + input + width="w-full" + disabled={userRole !== 20} + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} - - - + + + +
-
+ ) + ) : ( + + + )}
); -}; +}); diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index b0aad20cd..1f0ef1c31 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -1,42 +1,32 @@ import React, { useState } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // component import { SelectMonthModal } from "components/automation"; -import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon } from "@plane/ui"; +import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui"; // icons import { ArchiveX } from "lucide-react"; -// services -import { ProjectStateService } from "services/project"; -// constants -import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; -import { STATES_LIST } from "constants/fetch-keys"; // types import { IProject } from "types"; -// helper -import { getStatesList } from "helpers/state.helper"; +// fetch keys +import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; type Props = { - projectDetails: IProject | undefined; handleChange: (formData: Partial) => Promise; - disabled?: boolean; }; -const projectStateService = new ProjectStateService(); - -export const AutoCloseAutomation: React.FC = ({ projectDetails, handleChange, disabled = false }) => { +export const AutoCloseAutomation: React.FC = observer((props) => { + const { handleChange } = props; + // states const [monthModal, setmonthModal] = useState(false); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { user: userStore, project: projectStore, projectState: projectStateStore } = useMobxStore(); - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => projectStateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const states = getStatesList(stateGroups); + const userRole = userStore.currentProjectRole; + const projectDetails = projectStore.currentProjectDetails; + // const stateGroups = projectStateStore.groupedProjectStates ?? undefined; + const states = projectStateStore.projectStates; const options = states ?.filter((state) => state.group === "cancelled") @@ -53,7 +43,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha const multipleOptions = (options ?? []).length > 1; - const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null; + const defaultState = states?.find((s) => s.group === "cancelled")?.id || null; const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState); const currentDefaultState = states?.find((s) => s.id === defaultState); @@ -72,8 +62,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha handleClose={() => setmonthModal(false)} handleChange={handleChange} /> - -
+
@@ -82,7 +71,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha

Auto-close issues

- Plane will automatically close issue that haven’t been completed or cancelled. + Plane will automatically close issue that haven{"'"}t been completed or cancelled.

@@ -94,87 +83,93 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha : handleChange({ close_in: 0, default_state: null }) } size="sm" - disabled={disabled} + disabled={userRole !== 20} />
- {projectDetails?.close_in !== 0 && ( -
-
-
-
Auto-close issues that are inactive for
-
- { - handleChange({ close_in: val }); - }} - input - width="w-full" - disabled={disabled} - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} - - - + {projectDetails ? ( + projectDetails.close_in !== 0 && ( +
+
+
+
Auto-close issues that are inactive for
+
+ { + handleChange({ close_in: val }); + }} + input + width="w-full" + disabled={userRole !== 20} + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} + + + +
-
-
-
Auto-close Status
-
- - {selectedOption ? ( - - ) : currentDefaultState ? ( - - ) : ( - - )} - {selectedOption?.name - ? selectedOption.name - : currentDefaultState?.name ?? State} -
- } - onChange={(val: string) => { - handleChange({ default_state: val }); - }} - options={options} - disabled={!multipleOptions} - width="w-full" - input - /> +
+
Auto-close Status
+
+ + {selectedOption ? ( + + ) : currentDefaultState ? ( + + ) : ( + + )} + {selectedOption?.name + ? selectedOption.name + : currentDefaultState?.name ?? State} +
+ } + onChange={(val: string) => { + handleChange({ default_state: val }); + }} + options={options} + disabled={!multipleOptions} + width="w-full" + input + /> +
-
+ ) + ) : ( + + + )}
); -}; +}); diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index 2d65bd58a..3acfb71a6 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; +import { observer } from "mobx-react-lite"; import { FileText, FolderPlus, @@ -16,12 +17,13 @@ import { UserMinus2, UserPlus2, } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { WorkspaceService } from "services/workspace.service"; import { IssueService } from "services/issue"; // hooks import useDebounce from "hooks/use-debounce"; -import useUser from "hooks/use-user"; import useToast from "hooks/use-toast"; // components import { @@ -61,11 +63,8 @@ type Props = { const workspaceService = new WorkspaceService(); const issueService = new IssueService(); -export const CommandModal: React.FC = (props) => { +export const CommandModal: React.FC = observer((props) => { const { deleteIssue, isPaletteOpen, closePalette } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -86,14 +85,19 @@ export const CommandModal: React.FC = (props) => { const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); + const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); + const user = userStore.currentUser ?? undefined; + + // router + const router = useRouter(); + const { workspaceSlug, projectId, issueId } = router.query; + const page = pages[pages.length - 1]; const debouncedSearchTerm = useDebounce(searchTerm, 500); const { setToastAlert } = useToast(); - const { user } = useUser(); - const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId @@ -468,10 +472,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateIssueModal(true); }} className="focus:bg-custom-background-80" > @@ -488,10 +489,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateProjectModal(true); }} className="focus:outline-none" > @@ -510,10 +508,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "q", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateCycleModal(true); }} className="focus:outline-none" > @@ -528,10 +523,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "m", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateModuleModal(true); }} className="focus:outline-none" > @@ -546,10 +538,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "v", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateViewModal(true); }} className="focus:outline-none" > @@ -564,10 +553,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "d", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreatePageModal(true); }} className="focus:outline-none" > @@ -621,10 +607,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "h", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleShortcutModal(true); }} className="focus:outline-none" > @@ -762,4 +745,4 @@ export const CommandModal: React.FC = (props) => { ); -}; +}); diff --git a/web/components/command-palette/issue/change-issue-state.tsx b/web/components/command-palette/issue/change-issue-state.tsx index 03f495603..688aeb49f 100644 --- a/web/components/command-palette/issue/change-issue-state.tsx +++ b/web/components/command-palette/issue/change-issue-state.tsx @@ -1,9 +1,6 @@ import React, { Dispatch, SetStateAction, useCallback } from "react"; - import { useRouter } from "next/router"; - import useSWR, { mutate } from "swr"; - // cmdk import { Command } from "cmdk"; // services @@ -13,8 +10,6 @@ import { ProjectStateService } from "services/project"; import { Spinner, StateGroupIcon } from "@plane/ui"; // icons import { Check } from "lucide-react"; -// helpers -import { getStatesList } from "helpers/state.helper"; // types import { IUser, IIssue } from "types"; // fetch keys @@ -34,11 +29,10 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, use const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; - const { data: stateGroups, mutate: mutateIssueDetails } = useSWR( + const { data: states, mutate: mutateStates } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); - const states = getStatesList(stateGroups); const submitChanges = useCallback( async (formData: Partial) => { @@ -60,14 +54,14 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, use await issueService .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { - mutateIssueDetails(); + mutateStates(); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); }) .catch((e) => { console.error(e); }); }, - [workspaceSlug, issueId, projectId, mutateIssueDetails, user] + [workspaceSlug, issueId, projectId, mutateStates, user] ); const handleIssueState = (stateId: string) => { diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx index cfa28c5e1..8108b87af 100644 --- a/web/components/core/sidebar/progress-chart.tsx +++ b/web/components/core/sidebar/progress-chart.tsx @@ -57,8 +57,7 @@ const ProgressChart: React.FC = ({ distribution, startDate, endDate, tota const interval = Math.ceil(totalDates / maxDates); const limitedDates = []; - for (let i = 0; i < totalDates; i += interval) - limitedDates.push(renderShortNumericDateFormat(dates[i])); + for (let i = 0; i < totalDates; i += interval) limitedDates.push(renderShortNumericDateFormat(dates[i])); if (!limitedDates.includes(renderShortNumericDateFormat(dates[totalDates - 1]))) limitedDates.push(renderShortNumericDateFormat(dates[totalDates - 1])); diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index fc89ac3fb..c18399101 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,10 +1,12 @@ import { MouseEvent } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import useSWR from "swr"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // ui import { SingleProgressStats } from "components/core"; import { @@ -25,7 +27,6 @@ import { ActiveCycleProgressStats } from "components/cycles"; import { ViewIssueLabel } from "components/issues"; // icons import { AlarmClock, AlertTriangle, ArrowRight, CalendarDays, Star, Target } from "lucide-react"; - // helpers import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; @@ -65,12 +66,12 @@ interface IActiveCycleDetails { projectId: string; } -export const ActiveCycleDetails: React.FC = (props) => { +export const ActiveCycleDetails: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId } = props; - const { cycle: cycleStore } = useMobxStore(); + const { cycle: cycleStore, commandPalette: commandPaletteStore } = useMobxStore(); const { setToastAlert } = useToast(); @@ -117,12 +118,7 @@ export const ActiveCycleDetails: React.FC = (props) => { @@ -485,4 +481,4 @@ export const ActiveCycleDetails: React.FC = (props) => {
); -}; +}); diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index fb30150ca..f1e0cf084 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -41,7 +41,7 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa {peekCycle && (
= (props) => { +export const CyclesBoard: FC = observer((props) => { const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; + const { commandPalette: commandPaletteStore } = useMobxStore(); + return ( <> {cycles.length > 0 ? ( @@ -53,12 +58,7 @@ export const CyclesBoard: FC = (props) => { @@ -67,4 +67,4 @@ export const CyclesBoard: FC = (props) => { )} ); -}; +}); diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 097a18070..0d2991933 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -231,7 +231,7 @@ export const CyclesListItem: FC = (props) => { )} - + {!isCompleted && ( <> @@ -243,7 +243,7 @@ export const CyclesListItem: FC = (props) => { - Delete module + Delete cycle diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 03698f1d8..0cff682af 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,7 +1,9 @@ import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { CyclePeekOverview, CyclesListItem } from "components/cycles"; - // ui import { Loader } from "@plane/ui"; // types @@ -14,9 +16,11 @@ export interface ICyclesList { projectId: string; } -export const CyclesList: FC = (props) => { +export const CyclesList: FC = observer((props) => { const { cycles, filter, workspaceSlug, projectId } = props; + const { commandPalette: commandPaletteStore } = useMobxStore(); + return ( <> {cycles ? ( @@ -53,12 +57,7 @@ export const CyclesList: FC = (props) => { @@ -75,4 +74,4 @@ export const CyclesList: FC = (props) => { )} ); -}; +}); diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index b262edb15..7189b2fea 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -135,7 +135,7 @@ export const CycleForm: React.FC = (props) => {
-
+
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index ea154d48b..1c7d0c58b 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -317,11 +317,11 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { {!isCompleted && ( - + setCycleDeleteModal(true)}> - - Delete + + Delete cycle diff --git a/web/components/gantt-chart/chart/month.tsx b/web/components/gantt-chart/chart/month.tsx index 2a4a67daf..ccfcf87d5 100644 --- a/web/components/gantt-chart/chart/month.tsx +++ b/web/components/gantt-chart/chart/month.tsx @@ -34,16 +34,8 @@ export const MonthChartView: FC = () => { style={{ width: `${currentViewData?.data.width}px` }} >
- - {monthDay.dayData.shortTitle[0]} - {" "} - + {monthDay.dayData.shortTitle[0]}{" "} + {monthDay.day}
@@ -63,9 +55,7 @@ export const MonthChartView: FC = () => { >
{/* {monthDay?.today && ( diff --git a/web/components/gantt-chart/data/index.ts b/web/components/gantt-chart/data/index.ts index 4e1921434..58ac6e4b2 100644 --- a/web/components/gantt-chart/data/index.ts +++ b/web/components/gantt-chart/data/index.ts @@ -27,8 +27,7 @@ export const months: WeekMonthDataType[] = [ { key: 11, shortTitle: "dec", title: "december" }, ]; -export const charCapitalize = (word: string) => - `${word.charAt(0).toUpperCase()}${word.substring(1)}`; +export const charCapitalize = (word: string) => `${word.charAt(0).toUpperCase()}${word.substring(1)}`; export const bindZero = (value: number) => (value > 9 ? `${value}` : `0${value}`); @@ -50,9 +49,7 @@ export const datePreview = (date: Date, includeTime: boolean = false) => { month = months[month as number] as WeekMonthDataType; const year = date.getFullYear(); - return `${charCapitalize(month?.shortTitle)} ${day}, ${year}${ - includeTime ? `, ${timePreview(date)}` : `` - }`; + return `${charCapitalize(month?.shortTitle)} ${day}, ${year}${includeTime ? `, ${timePreview(date)}` : ``}`; }; // context data @@ -137,8 +134,6 @@ export const allViewsWithData: ChartDataType[] = [ ]; export const currentViewDataWithView = (view: string = "month") => { - const currentView: ChartDataType | undefined = allViewsWithData.find( - (_viewData) => _viewData.key === view - ); + const currentView: ChartDataType | undefined = allViewsWithData.find((_viewData) => _viewData.key === view); return currentView; }; diff --git a/web/components/gantt-chart/views/bi-week-view.ts b/web/components/gantt-chart/views/bi-week-view.ts index b8d8a0bfa..14c0aad15 100644 --- a/web/components/gantt-chart/views/bi-week-view.ts +++ b/web/components/gantt-chart/views/bi-week-view.ts @@ -3,12 +3,7 @@ import { ChartDataType } from "../types"; // data import { weeks, months } from "../data"; // helpers -import { - generateDate, - getWeekNumberByDate, - getNumberOfDaysInMonth, - getDatesBetweenTwoDates, -} from "./helpers"; +import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; type GetAllDaysInMonthInMonthViewType = { date: any; @@ -34,9 +29,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number) => { title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, active: false, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -72,16 +65,8 @@ export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "l if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -96,16 +81,8 @@ export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "l } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -116,16 +93,8 @@ export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "l } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/gantt-chart/views/day-view.ts b/web/components/gantt-chart/views/day-view.ts index 246ecd8b8..0801b7bb1 100644 --- a/web/components/gantt-chart/views/day-view.ts +++ b/web/components/gantt-chart/views/day-view.ts @@ -10,8 +10,7 @@ export const getWeekNumberByDate = (date: Date) => { const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; const weekStart = new Date(firstWeekStart); - const weekNumber = - Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; return weekNumber; }; @@ -25,8 +24,7 @@ export const getNumberOfDaysInMonth = (month: number, year: number) => { return date.getDate(); }; -export const generateDate = (day: number, month: number, year: number) => - new Date(year, month, day); +export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day); export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { const months = []; @@ -45,8 +43,7 @@ export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { months.push(new Date(currentYear, currentMonth)); currentDate.setMonth(currentDate.getMonth() + 1); } - if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) - months.push(endDate); + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) months.push(endDate); return months; }; @@ -73,9 +70,7 @@ export const getAllDaysInMonth = (month: number, year: number) => { weekNumber: getWeekNumberByDate(date), title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -99,10 +94,7 @@ export const generateMonthDataByMonth = (month: number, year: number) => { return monthPayload; }; -export const generateMonthDataByYear = ( - monthPayload: ChartDataType, - side: null | "left" | "right" -) => { +export const generateMonthDataByYear = (monthPayload: ChartDataType, side: null | "left" | "right") => { let renderState = monthPayload; const renderPayload: any = []; @@ -114,16 +106,8 @@ export const generateMonthDataByYear = ( if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -138,16 +122,8 @@ export const generateMonthDataByYear = ( } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -158,16 +134,8 @@ export const generateMonthDataByYear = ( } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/gantt-chart/views/helpers.ts b/web/components/gantt-chart/views/helpers.ts index bfea64297..94b614286 100644 --- a/web/components/gantt-chart/views/helpers.ts +++ b/web/components/gantt-chart/views/helpers.ts @@ -1,6 +1,5 @@ // Generating the date by using the year, month, and day -export const generateDate = (day: number, month: number, year: number) => - new Date(year, month, day); +export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day); // Getting the number of days in a month export const getNumberOfDaysInMonth = (month: number, year: number) => { @@ -20,8 +19,7 @@ export const getWeekNumberByDate = (date: Date) => { const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; const weekStart = new Date(firstWeekStart); - const weekNumber = - Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; return weekNumber; }; @@ -86,8 +84,7 @@ export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { dates.push(new Date(currentYear, currentMonth)); currentDate.setMonth(currentDate.getMonth() + 1); } - if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) - dates.push(endDate); + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) dates.push(endDate); return dates; }; diff --git a/web/components/gantt-chart/views/hours-view.ts b/web/components/gantt-chart/views/hours-view.ts index 246ecd8b8..0801b7bb1 100644 --- a/web/components/gantt-chart/views/hours-view.ts +++ b/web/components/gantt-chart/views/hours-view.ts @@ -10,8 +10,7 @@ export const getWeekNumberByDate = (date: Date) => { const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; const weekStart = new Date(firstWeekStart); - const weekNumber = - Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; return weekNumber; }; @@ -25,8 +24,7 @@ export const getNumberOfDaysInMonth = (month: number, year: number) => { return date.getDate(); }; -export const generateDate = (day: number, month: number, year: number) => - new Date(year, month, day); +export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day); export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { const months = []; @@ -45,8 +43,7 @@ export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { months.push(new Date(currentYear, currentMonth)); currentDate.setMonth(currentDate.getMonth() + 1); } - if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) - months.push(endDate); + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) months.push(endDate); return months; }; @@ -73,9 +70,7 @@ export const getAllDaysInMonth = (month: number, year: number) => { weekNumber: getWeekNumberByDate(date), title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -99,10 +94,7 @@ export const generateMonthDataByMonth = (month: number, year: number) => { return monthPayload; }; -export const generateMonthDataByYear = ( - monthPayload: ChartDataType, - side: null | "left" | "right" -) => { +export const generateMonthDataByYear = (monthPayload: ChartDataType, side: null | "left" | "right") => { let renderState = monthPayload; const renderPayload: any = []; @@ -114,16 +106,8 @@ export const generateMonthDataByYear = ( if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -138,16 +122,8 @@ export const generateMonthDataByYear = ( } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -158,16 +134,8 @@ export const generateMonthDataByYear = ( } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/gantt-chart/views/month-view.ts b/web/components/gantt-chart/views/month-view.ts index db21e372b..fc145d69c 100644 --- a/web/components/gantt-chart/views/month-view.ts +++ b/web/components/gantt-chart/views/month-view.ts @@ -3,12 +3,7 @@ import { ChartDataType, IGanttBlock } from "../types"; // data import { weeks, months } from "../data"; // helpers -import { - generateDate, - getWeekNumberByDate, - getNumberOfDaysInMonth, - getDatesBetweenTwoDates, -} from "./helpers"; +import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; type GetAllDaysInMonthInMonthViewType = { date: any; @@ -62,9 +57,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number): IMonthChild[ title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, active: false, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -100,16 +93,8 @@ export const generateMonthChart = (monthPayload: ChartDataType, side: null | "le if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -124,16 +109,8 @@ export const generateMonthChart = (monthPayload: ChartDataType, side: null | "le } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -144,16 +121,8 @@ export const generateMonthChart = (monthPayload: ChartDataType, side: null | "le } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -191,10 +160,7 @@ export const getNumberOfDaysBetweenTwoDatesInMonth = (startDate: Date, endDate: }; // calc item scroll position and width -export const getMonthChartItemPositionWidthInMonth = ( - chartData: ChartDataType, - itemData: IGanttBlock -) => { +export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, itemData: IGanttBlock) => { let scrollPosition: number = 0; let scrollWidth: number = 0; @@ -207,9 +173,7 @@ export const getMonthChartItemPositionWidthInMonth = ( // position code starts const positionTimeDifference: number = startDate.getTime() - itemStartDate.getTime(); - const positionDaysDifference: number = Math.abs( - Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24)) - ); + const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24))); scrollPosition = positionDaysDifference * chartData.data.width; var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; @@ -221,9 +185,7 @@ export const getMonthChartItemPositionWidthInMonth = ( // width code starts const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime(); - const widthDaysDifference: number = Math.abs( - Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)) - ); + const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24))); scrollWidth = (widthDaysDifference + 1) * chartData.data.width + 1; // width code ends diff --git a/web/components/gantt-chart/views/quater-view.ts b/web/components/gantt-chart/views/quater-view.ts index 0714cb28a..ed25974a3 100644 --- a/web/components/gantt-chart/views/quater-view.ts +++ b/web/components/gantt-chart/views/quater-view.ts @@ -36,10 +36,7 @@ const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) return weekPayload; }; -export const generateQuarterChart = ( - quarterPayload: ChartDataType, - side: null | "left" | "right" -) => { +export const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right") => { let renderState = quarterPayload; const renderPayload: any = []; diff --git a/web/components/gantt-chart/views/week-view.ts b/web/components/gantt-chart/views/week-view.ts index 024b8d4e1..a65eb70b9 100644 --- a/web/components/gantt-chart/views/week-view.ts +++ b/web/components/gantt-chart/views/week-view.ts @@ -3,12 +3,7 @@ import { ChartDataType } from "../types"; // data import { weeks, months } from "../data"; // helpers -import { - generateDate, - getWeekNumberByDate, - getNumberOfDaysInMonth, - getDatesBetweenTwoDates, -} from "./helpers"; +import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; type GetAllDaysInMonthInMonthViewType = { date: any; @@ -34,9 +29,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number) => { title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, active: false, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -72,16 +65,8 @@ export const generateWeekChart = (monthPayload: ChartDataType, side: null | "lef if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -96,16 +81,8 @@ export const generateWeekChart = (monthPayload: ChartDataType, side: null | "lef } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -116,16 +93,8 @@ export const generateWeekChart = (monthPayload: ChartDataType, side: null | "lef } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 4106f443b..0fb8913d5 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -31,6 +31,8 @@ export const CycleIssuesHeader: React.FC = observer(() => { cycle: cycleStore, cycleIssueFilter: cycleIssueFilterStore, project: projectStore, + projectState: projectStateStore, + commandPalette: commandPaletteStore, } = useMobxStore(); const { currentProjectDetails } = projectStore; @@ -139,7 +141,6 @@ export const CycleIssuesHeader: React.FC = observer(() => { type="component" component={ @@ -148,6 +149,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { } className="ml-1.5 flex-shrink-0" width="auto" + placement="bottom-start" > {cyclesList?.map((cycle) => ( { } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> @@ -194,16 +196,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { -
); -}; +}); diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 7c41ba327..5be966906 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -41,6 +41,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { globalViewFilters: globalViewFiltersStore, workspaceFilter: workspaceFilterStore, workspace: workspaceStore, + workspaceMember: { workspaceMembers }, project: projectStore, } = useMobxStore(); @@ -145,7 +146,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { handleFiltersUpdate={handleFiltersUpdate} layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet} labels={workspaceStore.workspaceLabels ?? undefined} - members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} + members={workspaceMembers?.map((m) => m.member) ?? undefined} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 42c01d531..8364815ca 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -31,6 +31,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => { module: moduleStore, moduleFilter: moduleFilterStore, project: projectStore, + projectState: projectStateStore, + commandPalette: commandPaletteStore, } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; const { currentProjectDetails } = projectStore; @@ -146,6 +148,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { } className="ml-1.5 flex-shrink-0" width="auto" + placement="bottom-start" > {modulesList?.map((module) => ( { } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> @@ -192,16 +195,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { - diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 8b9fa433d..a0bf29d05 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -24,10 +24,11 @@ const pageService = new PageService(); export const PageDetailsHeader: FC = observer((props) => { const { showButton = false } = props; + const router = useRouter(); const { workspaceSlug, pageId } = router.query; - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { currentProjectDetails } = projectStore; const { data: pageDetails } = useSWR( @@ -78,10 +79,7 @@ export const PageDetailsHeader: FC = observer((props) => { variant="primary" prependIcon={} size="sm" - onClick={() => { - const e = new KeyboardEvent("keydown", { key: "d" }); - document.dispatchEvent(e); - }} + onClick={() => commandPaletteStore.toggleCreatePageModal(true)} > Create Page diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 634dd0c38..0a3fd53f6 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -18,7 +18,7 @@ export const PagesHeader: FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { currentProjectDetails } = projectStore; return ( @@ -56,10 +56,7 @@ export const PagesHeader: FC = observer((props) => { variant="primary" prependIcon={} size="sm" - onClick={() => { - const e = new KeyboardEvent("keydown", { key: "d" }); - document.dispatchEvent(e); - }} + onClick={() => commandPaletteStore.toggleCreatePageModal(true)} > Create Page diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index e68f4ce84..7dd06e3a9 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -5,10 +5,8 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -// helper -import { truncateText } from "helpers/string.helper"; // ui -import { Breadcrumbs, BreadcrumbItem, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, LayersIcon } from "@plane/ui"; // icons import { ArrowLeft } from "lucide-react"; // components @@ -22,7 +20,11 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { project: projectStore, archivedIssueFilters: archivedIssueFiltersStore } = useMobxStore(); + const { + project: projectStore, + archivedIssueFilters: archivedIssueFiltersStore, + projectState: projectStateStore, + } = useMobxStore(); const { currentProjectDetails } = projectStore; @@ -118,7 +120,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index f9bf6ec58..e63b348e0 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -23,7 +23,13 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore, inbox: inboxStore } = useMobxStore(); + const { + issueFilter: issueFilterStore, + project: projectStore, + projectState: projectStateStore, + inbox: inboxStore, + commandPalette: commandPaletteStore, + } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; @@ -167,7 +173,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> @@ -198,16 +204,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { -
diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 50d97505c..dfb3a30b2 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -23,6 +23,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { issueFilter: issueFilterStore, projectViewFilters: projectViewFiltersStore, project: projectStore, + projectState: projectStateStore, projectViews: projectViewsStore, } = useMobxStore(); @@ -163,7 +164,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index 3c56f239e..fa51493ed 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -11,7 +11,7 @@ export const ProjectsHeader = observer(() => { const { workspaceSlug } = router.query; // store - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : []; @@ -43,14 +43,7 @@ export const ProjectsHeader = observer(() => {
)} - diff --git a/web/components/icons/completed-cycle-icon.tsx b/web/components/icons/completed-cycle-icon.tsx index 77d30b24b..d16009ad7 100644 --- a/web/components/icons/completed-cycle-icon.tsx +++ b/web/components/icons/completed-cycle-icon.tsx @@ -2,12 +2,7 @@ import React from "react"; import type { Props } from "./types"; -export const CompletedCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const CompletedCycleIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const CurrentCycleIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, - color, -}) => ( +export const DocumentIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const ExternalLinkIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "20", - height = "20", - className, -}) => ( +export const ModuleCancelledIcon: React.FC = ({ width = "20", height = "20", className }) => ( = ({ - width = "20", - height = "20", - className, -}) => ( +export const ModuleCompletedIcon: React.FC = ({ width = "20", height = "20", className }) => ( = ({ - width = "20", - height = "20", - className, -}) => ( +export const ModuleInProgressIcon: React.FC = ({ width = "20", height = "20", className }) => ( = ({ - + = ({ - status, - className, - height = "12px", - width = "12px", -}) => { - if (status === "backlog") - return ; - else if (status === "cancelled") - return ; - else if (status === "completed") - return ; +export const ModuleStatusIcon: React.FC = ({ status, className, height = "12px", width = "12px" }) => { + if (status === "backlog") return ; + else if (status === "cancelled") return ; + else if (status === "completed") return ; else if (status === "in-progress") return ; - else if (status === "paused") - return ; + else if (status === "paused") return ; else return ; }; diff --git a/web/components/icons/pencil-scribble-icon.tsx b/web/components/icons/pencil-scribble-icon.tsx index 561a5bcc3..4d7489049 100644 --- a/web/components/icons/pencil-scribble-icon.tsx +++ b/web/components/icons/pencil-scribble-icon.tsx @@ -2,19 +2,8 @@ import React from "react"; import type { Props } from "./types"; -export const PencilScribbleIcon: React.FC = ({ - width = "20", - height = "20", - className, - color = "#000000", -}) => ( - +export const PencilScribbleIcon: React.FC = ({ width = "20", height = "20", className, color = "#000000" }) => ( + = ({ - width = "24", - height = "24", - className, -}) => ( +export const QuestionMarkCircleIcon: React.FC = ({ width = "24", height = "24", className }) => ( = ({ - width = "24", - height = "24", - className, - color, -}) => ( +export const SingleCommentCard: React.FC = ({ width = "24", height = "24", className, color }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const TagIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, -}) => ( +export const TriangleExclamationIcon: React.FC = ({ width = "24", height = "24", className }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const UpcomingCycleIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, - color, -}) => ( +export const UserGroupIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( = observer((props) => { const editorRef = useRef(null); + const editorSuggestion = useEditorSuggestions(); + const router = useRouter(); const { workspaceSlug, projectId, inboxId } = router.query; @@ -134,6 +137,7 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { control={control} render={({ field: { value, onChange } }) => ( = observer((props) => { onChange={(description, description_html: string) => { onChange(description_html); }} + mentionSuggestions={editorSuggestion.mentionSuggestions} + mentionHighlights={editorSuggestion.mentionHighlights} /> )} /> diff --git a/web/components/integration/jira/confirm-import.tsx b/web/components/integration/jira/confirm-import.tsx index 54c599151..d69009af3 100644 --- a/web/components/integration/jira/confirm-import.tsx +++ b/web/components/integration/jira/confirm-import.tsx @@ -37,9 +37,7 @@ export const JiraConfirmImport: React.FC = () => {

Labels

-

- {watch("data.users").filter((user) => user.import).length} -

+

{watch("data.users").filter((user) => user.import).length}

User

diff --git a/web/components/integration/jira/give-details.tsx b/web/components/integration/jira/give-details.tsx index 622517439..8a7c841de 100644 --- a/web/components/integration/jira/give-details.tsx +++ b/web/components/integration/jira/give-details.tsx @@ -16,15 +16,14 @@ export const JiraGetImportDetail: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); + const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; + const { control, formState: { errors }, } = useFormContext(); - const { project: projectStore } = useMobxStore(); - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; - return (
@@ -190,10 +189,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
} + placement="bottom-start" > {watch("parent") ? ( <> @@ -599,24 +603,27 @@ export const IssueForm: FC = observer((props) => {
-
+
setCreateMore((prevData) => !prevData)} > +
+ {}} size="sm" /> +
Create more - {}} size="md" />
-
)} diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 53ea45001..7d4a5104f 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -26,7 +26,7 @@ export const CycleEmptyState: React.FC = observer((props) => { // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - const { cycleIssue: cycleIssueStore } = useMobxStore(); + const { cycleIssue: cycleIssueStore, commandPalette: commandPaletteStore } = useMobxStore(); const { setToastAlert } = useToast(); @@ -62,12 +62,7 @@ export const CycleEmptyState: React.FC = observer((props) => { primaryButton={{ text: "New issue", icon: , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreateIssueModal(true), }} secondaryButton={
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index 5910eff96..407aa6425 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -1,6 +1,5 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components @@ -12,7 +11,11 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const { project: projectStore, cycleIssueFilter: cycleIssueFilterStore } = useMobxStore(); + const { + project: projectStore, + cycleIssueFilter: cycleIssueFilterStore, + projectState: projectStateStore, + } = useMobxStore(); const userFilters = cycleIssueFilterStore.cycleFilters; @@ -70,7 +73,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} />
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index ad1712b55..9bc94b0c9 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -22,6 +22,7 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { globalViewFilters: globalViewFiltersStore, project: projectStore, workspace: workspaceStore, + workspaceMember: { workspaceMembers }, } = useMobxStore(); const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined; @@ -101,7 +102,7 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={workspaceStore.workspaceLabels ?? undefined} - members={workspaceStore.workspaceMembers?.map((m) => m.member)} + members={workspaceMembers?.map((m) => m.member)} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} /> {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data.filters ?? {}) && ( diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index 6bcfb40f5..7875aecd7 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -12,7 +12,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; - const { project: projectStore, moduleFilter: moduleFilterStore } = useMobxStore(); + const { project: projectStore, moduleFilter: moduleFilterStore, projectState: projectStateStore } = useMobxStore(); const userFilters = moduleFilterStore.moduleFilters; @@ -70,7 +70,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} /> ); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 505649bce..acf985051 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -12,7 +12,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore(); + const { issueFilter: issueFilterStore, project: projectStore, projectState: projectStateStore } = useMobxStore(); const userFilters = issueFilterStore.userFilters; @@ -74,7 +74,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} /> ); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index c8ee36fe7..baa1ce20d 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -19,6 +19,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const { project: projectStore, + projectState: projectStateStore, projectViews: projectViewsStore, projectViewFilters: projectViewFiltersStore, } = useMobxStore(); @@ -99,7 +100,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} /> {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data ?? {}) && ( ) : ( <> - - {label} + + {label} )} = ({ value, onChange }) => { - - +
+ + {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx index 47b1d4e5b..a85b073eb 100644 --- a/web/components/issues/select/label.tsx +++ b/web/components/issues/select/label.tsx @@ -68,10 +68,10 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, />
) : ( - - - Label - +
+ + Label +
)} diff --git a/web/components/issues/select/module.tsx b/web/components/issues/select/module.tsx index c0d9c6c4c..cb9d6252b 100644 --- a/web/components/issues/select/module.tsx +++ b/web/components/issues/select/module.tsx @@ -55,34 +55,24 @@ export const IssueModuleSelect: React.FC = observer((pro query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); const label = selectedModule ? ( -
- - - -
{selectedModule.name}
+
+ + {selectedModule.name}
) : ( - <> +
- Select Module - + Module +
); return ( - onChange(val)} - disabled={false} - > + onChange(val)}> diff --git a/web/components/issues/select/state.tsx b/web/components/issues/select/state.tsx index 024024641..051106acb 100644 --- a/web/components/issues/select/state.tsx +++ b/web/components/issues/select/state.tsx @@ -1,17 +1,12 @@ import React from "react"; - import { useRouter } from "next/router"; - import useSWR from "swr"; - // services import { ProjectStateService } from "services/project"; // ui import { CustomSearchSelect, DoubleCircleIcon, StateGroupIcon } from "@plane/ui"; // icons import { Plus } from "lucide-react"; -// helpers -import { getStatesList } from "helpers/state.helper"; // fetch keys import { STATES_LIST } from "constants/fetch-keys"; @@ -30,11 +25,10 @@ export const IssueStateSelect: React.FC = ({ setIsOpen, value, onChange, const router = useRouter(); const { workspaceSlug } = router.query; - const { data: stateGroups } = useSWR( + const { data: states } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId) : null, workspaceSlug && projectId ? () => projectStateService.getStates(workspaceSlug as string, projectId) : null ); - const states = getStatesList(stateGroups); const options = states?.map((state) => ({ value: state.id, @@ -56,17 +50,15 @@ export const IssueStateSelect: React.FC = ({ setIsOpen, value, onChange, onChange={onChange} options={options} label={ -
+
{selectedOption ? ( ) : currentDefaultState ? ( ) : ( - + )} - {selectedOption?.name - ? selectedOption.name - : currentDefaultState?.name ?? State} + {selectedOption?.name ? selectedOption.name : currentDefaultState?.name ?? State}
} footerOption={ diff --git a/web/components/issues/sidebar-select/state.tsx b/web/components/issues/sidebar-select/state.tsx index a109ddab9..326a5cf48 100644 --- a/web/components/issues/sidebar-select/state.tsx +++ b/web/components/issues/sidebar-select/state.tsx @@ -9,7 +9,6 @@ import { ProjectStateService } from "services/project"; // ui import { CustomSearchSelect, StateGroupIcon } from "@plane/ui"; // helpers -import { getStatesList } from "helpers/state.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // constants import { STATES_LIST } from "constants/fetch-keys"; @@ -27,11 +26,10 @@ export const SidebarStateSelect: React.FC = ({ value, onChange, disabled const router = useRouter(); const { workspaceSlug, projectId, inboxIssueId } = router.query; - const { data: stateGroups } = useSWR( + const { data: states } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); - const states = getStatesList(stateGroups); const selectedOption = states?.find((s) => s.id === value); diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx index 3cb5942ee..3505d1447 100644 --- a/web/components/modules/delete-module-modal.tsx +++ b/web/components/modules/delete-module-modal.tsx @@ -45,6 +45,11 @@ export const DeleteModuleModal: React.FC = observer((props) => { .then(() => { if (moduleId) router.push(`/${workspaceSlug}/projects/${data.project}/modules`); handleClose(); + setToastAlert({ + type: "success", + title: "Success!", + message: "Module deleted successfully.", + }); }) .catch(() => { setToastAlert({ diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx index c7d2630cb..e5528cdcf 100644 --- a/web/components/modules/module-peek-overview.tsx +++ b/web/components/modules/module-peek-overview.tsx @@ -41,7 +41,7 @@ export const ModulePeekOverview: React.FC = observer(({ projectId, worksp {peekModule && (
{ const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; - const { module: moduleStore } = useMobxStore(); + const { module: moduleStore, commandPalette: commandPaletteStore } = useMobxStore(); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); @@ -85,12 +85,7 @@ export const ModulesListView: React.FC = observer(() => { primaryButton={{ icon: , text: "New Module", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "m", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreateModuleModal(true), }} /> )} diff --git a/web/components/modules/select/lead.tsx b/web/components/modules/select/lead.tsx index 9e12e2017..69ba71bf9 100644 --- a/web/components/modules/select/lead.tsx +++ b/web/components/modules/select/lead.tsx @@ -50,9 +50,13 @@ export const ModuleLeadSelect: React.FC = ({ value, onChange }) => { {selectedOption ? ( ) : ( - + + )} + {selectedOption ? ( + selectedOption?.display_name + ) : ( + Lead )} - {selectedOption ? selectedOption?.display_name : Lead}
} onChange={onChange} diff --git a/web/components/modules/select/members.tsx b/web/components/modules/select/members.tsx index 94c74f05b..17b80a471 100644 --- a/web/components/modules/select/members.tsx +++ b/web/components/modules/select/members.tsx @@ -55,9 +55,9 @@ export const ModuleMembersSelect: React.FC = ({ value, onChange }) => { {value.length} Assignees
) : ( -
- - Assignee +
+ + Assignee
)}
diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 825a8cf61..f4c14871b 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -246,11 +246,11 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { - + setModuleDeleteModal(true)}> - - Delete + + Delete module diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index 4cc13a50d..e434bcae8 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -171,7 +171,7 @@ export const SnoozeNotificationModal: FC = (props) => { setValue("time", null); onChange(val); }} - className="px-3 py-2 w-full rounded-md border border-custom-border-300 bg-custom-background-100 text-custom-text-100 focus:outline-none !text-sm" + className="px-3 py-2 w-full rounded-md border border-custom-border-300 bg-custom-background-100 text-custom-text-100 placeholder:!text-custom-text-400 focus:outline-none !text-sm" wrapperClassName="w-full" noBorder minDate={new Date()} diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 5ed28b00b..b7fcb7cb6 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -18,7 +18,8 @@ export const WorkspaceDashboardView = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { user: userStore, project: projectStore } = useMobxStore(); + const { user: userStore, project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); + const user = userStore.currentUser; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; const workspaceDashboardInfo = userStore.dashboardInfo; @@ -67,16 +68,7 @@ export const WorkspaceDashboardView = observer(() => {
Create a project

Manage your projects by creating issues, cycles, modules, views and pages.

-
diff --git a/web/components/pages/create-update-block-inline.tsx b/web/components/pages/create-update-block-inline.tsx index b838da0c9..c1b3c5155 100644 --- a/web/components/pages/create-update-block-inline.tsx +++ b/web/components/pages/create-update-block-inline.tsx @@ -295,6 +295,7 @@ export const CreateUpdateBlockInline: FC = ({ if (!data) return ( = ({ return ( = ({ viewType }) => { +export const RecentPagesList: React.FC = observer((props) => { + const { viewType } = props; + + const { commandPalette: commandPaletteStore } = useMobxStore(); + const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -46,9 +49,7 @@ export const RecentPagesList: React.FC = ({ viewType }) => { return (
-

- {replaceUnderscoreIfSnakeCase(key)} -

+

{replaceUnderscoreIfSnakeCase(key)}

); @@ -61,12 +62,7 @@ export const RecentPagesList: React.FC = ({ viewType }) => { primaryButton={{ icon: , text: "New Page", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "d", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreatePageModal(true), }} /> ) @@ -79,4 +75,4 @@ export const RecentPagesList: React.FC = ({ viewType }) => { )} ); -}; +}); diff --git a/web/components/pages/pages-view.tsx b/web/components/pages/pages-view.tsx index 699cbb582..5aca1de1b 100644 --- a/web/components/pages/pages-view.tsx +++ b/web/components/pages/pages-view.tsx @@ -1,21 +1,20 @@ import { useState } from "react"; - -import useSWR, { mutate } from "swr"; import { useRouter } from "next/router"; - +import { observer } from "mobx-react-lite"; +import useSWR, { mutate } from "swr"; +import { Plus } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { PageService } from "services/page.service"; import { ProjectService } from "services/project"; // hooks import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; // components import { CreateUpdatePageModal, DeletePageModal, SinglePageDetailedItem, SinglePageListItem } from "components/pages"; -// ui import { EmptyState } from "components/common"; +// ui import { Loader } from "@plane/ui"; -// icons -import { Plus } from "lucide-react"; // images import emptyPage from "public/empty-state/page.svg"; // types @@ -37,17 +36,19 @@ type Props = { const pageService = new PageService(); const projectService = new ProjectService(); -export const PagesView: React.FC = ({ pages, viewType }) => { - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; +export const PagesView: React.FC = observer(({ pages, viewType }) => { // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); const [selectedPageToUpdate, setSelectedPageToUpdate] = useState(null); const [deletePageModal, setDeletePageModal] = useState(false); const [selectedPageToDelete, setSelectedPageToDelete] = useState(null); - const { user } = useUserAuth(); + const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); + const user = userStore.currentUser ?? undefined; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; const { setToastAlert } = useToast(); @@ -163,7 +164,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { }; const partialUpdatePage = (page: IPage, formData: Partial) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !user) return; mutate( ALL_PAGES_LIST(projectId.toString()), @@ -264,12 +265,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { primaryButton={{ icon: , text: "New Page", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "d", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreatePageModal(true), }} /> )} @@ -294,4 +290,4 @@ export const PagesView: React.FC = ({ pages, viewType }) => { )} ); -}; +}); diff --git a/web/components/pages/single-page-block.tsx b/web/components/pages/single-page-block.tsx index 542ef78a2..6f6766f84 100644 --- a/web/components/pages/single-page-block.tsx +++ b/web/components/pages/single-page-block.tsx @@ -420,6 +420,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, showBl {showBlockDetails ? block.description_html.length > 7 && ( = ({ stateDistribution, u colors={(datum) => datum.data.color} tooltip={(datum) => (
- - {datum.datum.label} issues: - {" "} + {datum.datum.label} issues:{" "} {datum.datum.value}
)} @@ -59,10 +57,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u
{stateDistribution.map((group) => ( -
+
= observer((props) => { const { workspaceSlug } = props; // store - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; @@ -38,12 +38,16 @@ export const ProjectCardList: FC = observer((props) => { return ( <> {projects.length > 0 ? ( -
-
- {projectStore.searchedProjects.map((project) => ( - - ))} -
+
+ {projectStore.searchedProjects.length == 0 ? ( +
No matching projects
+ ) : ( +
+ {projectStore.searchedProjects.map((project) => ( + + ))} +
+ )}
) : ( = observer((props) => { primaryButton={{ icon: , text: "New Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreateProjectModal(true), }} /> )} diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 6be880dcb..ab734e9b3 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -63,8 +63,10 @@ export interface ICreateProjectForm { export const CreateProjectModal: FC = observer((props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // store - const { project: projectStore, workspace: workspaceStore } = useMobxStore(); - const workspaceMembers = workspaceStore.members[workspaceSlug] || []; + const { + project: projectStore, + workspaceMember: { workspaceMembers }, + } = useMobxStore(); // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); // toast @@ -265,7 +267,7 @@ export const CreateProjectModal: FC = observer((props) => { onChange={handleNameChange(onChange)} hasError={Boolean(errors.name)} placeholder="Project Title" - className="w-full" + className="w-full focus:border-blue-400" /> )} /> @@ -298,7 +300,7 @@ export const CreateProjectModal: FC = observer((props) => { onChange={handleIdentifierChange(onChange)} hasError={Boolean(errors.identifier)} placeholder="Identifier" - className="text-xs w-full" + className="text-xs w-full focus:border-blue-400" /> )} /> @@ -316,7 +318,7 @@ export const CreateProjectModal: FC = observer((props) => { tabIndex={3} placeholder="Description..." onChange={onChange} - className="text-sm !h-24" + className="text-sm !h-24 focus:border-blue-400" hasError={Boolean(errors?.description)} /> )} @@ -370,7 +372,7 @@ export const CreateProjectModal: FC = observer((props) => { )} diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index a40e37403..eb8a21469 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -62,6 +62,11 @@ export const DeleteProjectModal: React.FC = (props) => { if (projectId && projectId.toString() === project.id) router.push(`/${workspaceSlug}/projects`); handleClose(); + setToastAlert({ + type: "success", + title: "Success!", + message: "Project deleted successfully.", + }); }) .catch(() => { setToastAlert({ diff --git a/web/components/project/priority-select.tsx b/web/components/project/priority-select.tsx index 8863d860d..b7323642e 100644 --- a/web/components/project/priority-select.tsx +++ b/web/components/project/priority-select.tsx @@ -107,9 +107,13 @@ export const PrioritySelect: React.FC = ({ ? "border-red-500/20 bg-red-500" : "border-custom-border-300" : "border-custom-border-300" - } ${!disabled ? "hover:bg-custom-background-80" : ""} ${ - disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer" - } ${buttonClassName}`} + } ${ + !disabled + ? `${ + value === "urgent" && highlightUrgentPriority ? "hover:bg-red-400" : "hover:bg-custom-background-80" + }` + : "" + } ${disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"} ${buttonClassName}`} > {label} {!hideDropdownArrow && !disabled &&
+
+ {group === activeGroup && ( + { + setActiveGroup(null); + setSelectedState(null); + }} + selectedGroup={group as keyof StateGroup} + /> + )} + {sortByField(orderedStateGroups[group], "sequence").map((state, index) => + state.id !== selectedState ? ( + setSelectedState(state.id)} + handleDeleteState={() => setSelectDeleteState(state.id)} + /> + ) : ( +
+ { + setActiveGroup(null); + setSelectedState(null); + }} + groupLength={orderedStateGroups[group].length} + data={projectStates?.find((state) => state.id === selectedState) ?? null} + selectedGroup={group as keyof StateGroup} + /> +
+ ) + )} +
+
+ ))} + + ) : ( + + + + + + + )} +
+ + {/*
{states && currentProjectDetails && orderedStateGroups ? ( Object.keys(orderedStateGroups || {}).map((key) => { if (orderedStateGroups[key].length !== 0) @@ -118,7 +177,7 @@ export const ProjectSettingStateList: React.FC = observer(() => { )} -
+
*/} ); }); diff --git a/web/components/ui/date.tsx b/web/components/ui/date.tsx index 31cbdc9fd..acc8eb1ea 100644 --- a/web/components/ui/date.tsx +++ b/web/components/ui/date.tsx @@ -21,22 +21,21 @@ export const DateSelect: React.FC = ({ value, onChange, label, minDate, m {({ close }) => ( <> - - - {value ? ( - <> - {renderShortDateWithYearFormat(value)} - - - ) : ( - <> - - {label} - - )} - + + {value ? ( + <> + + {renderShortDateWithYearFormat(value)} + + + ) : ( + <> + + {label} + + )} {title && ( -

- {title} -

+

{title}

)} {children}
@@ -92,14 +90,7 @@ type MenuItemProps = { Icon?: any; }; -const MenuItem: React.FC = ({ - children, - renderAs, - href = "", - onClick, - className = "", - Icon, -}) => ( +const MenuItem: React.FC = ({ children, renderAs, href = "", onClick, className = "", Icon }) => ( <> {renderAs === "a" ? ( diff --git a/web/components/ui/graphs/bar-graph.tsx b/web/components/ui/graphs/bar-graph.tsx index 565ca43e5..3756b0455 100644 --- a/web/components/ui/graphs/bar-graph.tsx +++ b/web/components/ui/graphs/bar-graph.tsx @@ -32,9 +32,7 @@ export const BarGraph: React.FC, "height" axisLeft={{ tickSize: 0, tickPadding: 10, - tickValues: customYAxisTickValues - ? generateYAxisTickValues(customYAxisTickValues) - : undefined, + tickValues: customYAxisTickValues ? generateYAxisTickValues(customYAxisTickValues) : undefined, }} axisBottom={{ tickSize: 0, diff --git a/web/components/ui/graphs/line-graph.tsx b/web/components/ui/graphs/line-graph.tsx index 7adda5a16..91a19acc3 100644 --- a/web/components/ui/graphs/line-graph.tsx +++ b/web/components/ui/graphs/line-graph.tsx @@ -25,9 +25,7 @@ export const LineGraph: React.FC = ({ axisLeft={{ tickSize: 0, tickPadding: 10, - tickValues: customYAxisTickValues - ? generateYAxisTickValues(customYAxisTickValues) - : undefined, + tickValues: customYAxisTickValues ? generateYAxisTickValues(customYAxisTickValues) : undefined, }} theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} animate diff --git a/web/components/ui/graphs/scatter-plot-graph.tsx b/web/components/ui/graphs/scatter-plot-graph.tsx index a14def580..c6ff5a772 100644 --- a/web/components/ui/graphs/scatter-plot-graph.tsx +++ b/web/components/ui/graphs/scatter-plot-graph.tsx @@ -5,9 +5,13 @@ import { TGraph } from "./types"; // constants import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; -export const ScatterPlotGraph: React.FC< - TGraph & Omit, "height" | "width"> -> = ({ height = "400px", width = "100%", margin, theme, ...rest }) => ( +export const ScatterPlotGraph: React.FC, "height" | "width">> = ({ + height = "400px", + width = "100%", + margin, + theme, + ...rest +}) => (
= (props) => { {labels && ( <> l?.name).join(", ")}> -
+
{`${labels.length} Labels`}
diff --git a/web/components/ui/markdown-to-component.tsx b/web/components/ui/markdown-to-component.tsx index 53b157b19..5d3d1b25c 100644 --- a/web/components/ui/markdown-to-component.tsx +++ b/web/components/ui/markdown-to-component.tsx @@ -41,9 +41,7 @@ const HeadingSecondary: CustomComponent = ({ children }) => (

{children}

); -const Paragraph: CustomComponent = ({ children }) => ( -

{children}

-); +const Paragraph: CustomComponent = ({ children }) =>

{children}

; const OrderedList: CustomComponent = ({ children }) => (
    {children}
diff --git a/web/components/ui/progress-bar.tsx b/web/components/ui/progress-bar.tsx index 8ac0b1e1e..b92b568ac 100644 --- a/web/components/ui/progress-bar.tsx +++ b/web/components/ui/progress-bar.tsx @@ -39,8 +39,7 @@ export const ProgressBar: React.FC = ({ const DIRECTION = -1; // Rotation Calc const primaryRotationAngle = (maxValue - 1) * (360 / maxValue); - const rotationAngle = - -1 * DIRECTION * primaryRotationAngle + i * DIRECTION * primaryRotationAngle; + const rotationAngle = -1 * DIRECTION * primaryRotationAngle + i * DIRECTION * primaryRotationAngle; const rotationTransformation = `rotate(${rotationAngle}, ${radius}, ${radius})`; const pieValue = calculatePieValue(maxValue); const dValue = generatePie(pieValue); diff --git a/web/components/ui/text-area/index.tsx b/web/components/ui/text-area/index.tsx index a962281a4..bd6f749d3 100644 --- a/web/components/ui/text-area/index.tsx +++ b/web/components/ui/text-area/index.tsx @@ -74,9 +74,7 @@ export const TextArea: React.FC = ({ : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme" : "" - } ${error ? "border-red-500" : ""} ${ - error && mode === "primary" ? "bg-red-100" : "" - } ${className}`} + } ${error ? "border-red-500" : ""} ${error && mode === "primary" ? "bg-red-100" : ""} ${className}`} {...rest} /> {error?.message &&
{error.message}
} diff --git a/web/components/ui/toggle-switch.tsx b/web/components/ui/toggle-switch.tsx index 5ad9377de..ffe755853 100644 --- a/web/components/ui/toggle-switch.tsx +++ b/web/components/ui/toggle-switch.tsx @@ -30,11 +30,7 @@ export const ToggleSwitch: React.FC = (props) => { size === "sm" ? "h-2 w-2" : size === "md" ? "h-3 w-3" : "h-4 w-4" } transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${ value - ? (size === "sm" - ? "translate-x-3" - : size === "md" - ? "translate-x-4" - : "translate-x-5") + " bg-white" + ? (size === "sm" ? "translate-x-3" : size === "md" ? "translate-x-4" : "translate-x-5") + " bg-white" : "translate-x-0.5 bg-custom-background-90" } ${disabled ? "cursor-not-allowed" : ""}`} /> diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 55ccb2fee..c448726d4 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -26,7 +26,7 @@ const defaultValues: Partial = { }; export const ProjectViewForm: React.FC = observer(({ handleFormSubmit, handleClose, data, preLoadedData }) => { - const { project: projectStore } = useMobxStore(); + const { project: projectStore, projectState: projectStateStore } = useMobxStore(); const { control, @@ -88,7 +88,7 @@ export const ProjectViewForm: React.FC = observer(({ handleFormSubmit, ha onChange={onChange} hasError={Boolean(errors.name)} placeholder="Title" - className="resize-none text-xl" + className="resize-none w-full text-xl focus:border-blue-400" /> )} /> @@ -138,7 +138,7 @@ export const ProjectViewForm: React.FC = observer(({ handleFormSubmit, ha layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.list} labels={projectStore.projectLabels ?? undefined} members={projectStore.projectMembers?.map((m) => m.member) ?? undefined} - states={projectStore.projectStatesByGroups ?? undefined} + states={projectStateStore.projectStates ?? undefined} /> )} @@ -152,7 +152,7 @@ export const ProjectViewForm: React.FC = observer(({ handleFormSubmit, ha handleRemoveFilter={() => {}} labels={projectStore.projectLabels ?? undefined} members={projectStore.projectMembers?.map((m) => m.member) ?? undefined} - states={projectStore.projectStatesByGroups ?? undefined} + states={projectStateStore.projectStates ?? undefined} />
)} diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index fb1e9c2c6..02a2b8ec7 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -32,7 +32,14 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { const createView = async (payload: IProjectView) => { await projectViewsStore .createView(workspaceSlug, projectId, payload) - .then(() => handleClose()) + .then(() => { + handleClose(); + setToastAlert({ + type: "success", + title: "Success!", + message: "View created successfully.", + }); + }) .catch(() => setToastAlert({ type: "error", diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 6a5a6705b..69ac6aaf2 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -20,7 +20,7 @@ export const ProjectViewsList = observer(() => { const router = useRouter(); const { projectId } = router.query; - const { projectViews: projectViewsStore } = useMobxStore(); + const { projectViews: projectViewsStore, commandPalette: commandPaletteStore } = useMobxStore(); const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined; @@ -66,12 +66,7 @@ export const ProjectViewsList = observer(() => { primaryButton={{ icon: , text: "New View", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "v", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreateViewModal(true), }} /> )} diff --git a/web/components/workspace/confirm-workspace-member-remove.tsx b/web/components/workspace/confirm-workspace-member-remove.tsx index 4b057f1de..c65011d24 100644 --- a/web/components/workspace/confirm-workspace-member-remove.tsx +++ b/web/components/workspace/confirm-workspace-member-remove.tsx @@ -19,8 +19,9 @@ export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) => const [isRemoving, setIsRemoving] = useState(false); - const { user: userStore } = useMobxStore(); - const user = userStore.currentUser; + const { + user: { currentUser }, + } = useMobxStore(); const handleClose = () => { onClose(); @@ -69,10 +70,10 @@ export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) =>
- {user?.id === data?.memberId ? "Leave workspace?" : `Remove ${data?.display_name}?`} + {currentUser?.id === data?.memberId ? "Leave workspace?" : `Remove ${data?.display_name}?`}
- {user?.id === data?.memberId ? ( + {currentUser?.id === data?.memberId ? (

Are you sure you want to leave the workspace? You will no longer have access to this workspace. This action cannot be undone. diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index 41251dac6..67ac0cf58 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -212,9 +212,11 @@ export const CreateWorkspaceForm: FC = observer((props) => { - + {!secondaryButton && ( + + )}

); diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index 87dbbf373..bdbba0de7 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -43,7 +43,7 @@ export interface WorkspaceHelpSectionProps { export const WorkspaceHelpSection: React.FC = observer(() => { // store - const { theme: themeStore } = useMobxStore(); + const { theme: themeStore, commandPalette: commandPaletteStore } = useMobxStore(); // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs @@ -71,12 +71,7 @@ export const WorkspaceHelpSection: React.FC = observe className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${ isCollapsed ? "w-full" : "" }`} - onClick={() => { - const e = new KeyboardEvent("keydown", { - key: "h", - }); - document.dispatchEvent(e); - }} + onClick={() => commandPaletteStore.toggleShortcutModal(true)} > diff --git a/web/components/workspace/issues-pie-chart.tsx b/web/components/workspace/issues-pie-chart.tsx index de1e199ff..594db4175 100644 --- a/web/components/workspace/issues-pie-chart.tsx +++ b/web/components/workspace/issues-pie-chart.tsx @@ -35,8 +35,7 @@ export const IssuesPieChart: React.FC = ({ groupedIssues }) => ( colors={(datum) => datum.data.color} tooltip={(datum) => (
- {datum.datum.label} issues:{" "} - {datum.datum.value} + {datum.datum.label} issues: {datum.datum.value}
)} margin={{ @@ -53,10 +52,7 @@ export const IssuesPieChart: React.FC = ({ groupedIssues }) => (
{groupedIssues?.map((cell) => (
-
+
{cell.state_group}- {cell.state_count}
diff --git a/web/components/workspace/member-select.tsx b/web/components/workspace/member-select.tsx index 3ba1cbd89..bae16de01 100644 --- a/web/components/workspace/member-select.tsx +++ b/web/components/workspace/member-select.tsx @@ -48,10 +48,7 @@ export const WorkspaceMemberSelect: FC = (props) => { : options?.filter((option) => option.member.display_name.toLowerCase().includes(query.toLowerCase())); const label = ( -
+
{value ? ( <> @@ -81,7 +78,7 @@ export const WorkspaceMemberSelect: FC = (props) => {
void; - workspaceSlug: string; - user: IUser | undefined; - onSuccess?: () => Promise; + onSubmit: (data: IWorkspaceBulkInviteFormData) => Promise | undefined; }; type EmailRole = { @@ -43,11 +34,10 @@ const defaultValues: FormValues = { ], }; -const workspaceService = new WorkspaceService(); - export const SendWorkspaceInvitationModal: React.FC = (props) => { - const { isOpen, onClose, workspaceSlug, user, onSuccess } = props; + const { isOpen, onClose, onSubmit } = props; + // form info const { control, reset, @@ -60,8 +50,6 @@ export const SendWorkspaceInvitationModal: React.FC = (props) => { name: "emails", }); - const { setToastAlert } = useToast(); - const handleClose = () => { onClose(); @@ -71,31 +59,29 @@ export const SendWorkspaceInvitationModal: React.FC = (props) => { }, 350); }; - const onSubmit = async (formData: FormValues) => { - if (!workspaceSlug) return; + // const onSubmit = async (formData: FormValues) => { + // if (!workspaceSlug) return; - await workspaceService - .inviteWorkspace(workspaceSlug, formData, user) - .then(async () => { - if (onSuccess) await onSuccess(); + // return workspaceService + // .inviteWorkspace(workspaceSlug, formData, user) - handleClose(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Invitations sent successfully.", - }); - }) - .catch((err) => - setToastAlert({ - type: "error", - title: "Error!", - message: `${err.error ?? "Something went wrong. Please try again."}`, - }) - ) - .finally(() => mutate(WORKSPACE_INVITATIONS)); - }; + // .then(async () => { + // if (onSuccess) await onSuccess(); + // handleClose(); + // setToastAlert({ + // type: "success", + // title: "Success!", + // message: "Invitations sent successfully.", + // }); + // }) + // .catch((err) => + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: `${err.error ?? "Something went wrong. Please try again."}`, + // }) + // ); + // }; const appendField = () => { append({ email: "", role: 15 }); diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index 99c4de088..3129e63d7 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -3,8 +3,6 @@ import Link from "next/link"; import { useRouter } from "next/router"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { WorkspaceService } from "services/workspace.service"; // hooks import useToast from "hooks/use-toast"; // components @@ -33,17 +31,16 @@ type Props = { }; }; -// services -const workspaceService = new WorkspaceService(); - export const WorkspaceMembersListItem: FC = (props) => { const { member } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; // store - const { workspace: workspaceStore, user: userStore } = useMobxStore(); - const { currentWorkspaceMemberInfo, currentWorkspaceRole } = userStore; + const { + workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation }, + user: { currentWorkspaceMemberInfo, currentWorkspaceRole }, + } = useMobxStore(); const isAdmin = currentWorkspaceRole === 20; // states const [removeMemberModal, setRemoveMemberModal] = useState(false); @@ -54,7 +51,7 @@ export const WorkspaceMembersListItem: FC = (props) => { if (!workspaceSlug) return; if (member.member) - await workspaceStore.removeMember(workspaceSlug.toString(), member.id).catch((err) => { + await removeMember(workspaceSlug.toString(), member.id).catch((err) => { const error = err?.error; setToastAlert({ type: "error", @@ -63,8 +60,7 @@ export const WorkspaceMembersListItem: FC = (props) => { }); }); else - await workspaceService - .deleteWorkspaceInvitations(workspaceSlug.toString(), member.id) + await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id) .then(() => { setToastAlert({ type: "success", @@ -157,17 +153,15 @@ export const WorkspaceMembersListItem: FC = (props) => { onChange={(value: TUserWorkspaceRole | undefined) => { if (!workspaceSlug || !value) return; - workspaceStore - .updateMember(workspaceSlug.toString(), member.id, { - role: value, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "An error occurred while updating member role. Please try again.", - }); + updateMember(workspaceSlug.toString(), member.id, { + role: value, + }).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "An error occurred while updating member role. Please try again.", }); + }); }} disabled={ member.memberId === currentWorkspaceMemberInfo.member || diff --git a/web/components/workspace/settings/members-list.tsx b/web/components/workspace/settings/members-list.tsx index d09f2558d..ef94a44e8 100644 --- a/web/components/workspace/settings/members-list.tsx +++ b/web/components/workspace/settings/members-list.tsx @@ -1,64 +1,45 @@ +import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { WorkspaceService } from "services/workspace.service"; // components import { WorkspaceMembersListItem } from "components/workspace"; // ui import { Loader } from "@plane/ui"; -const workspaceService = new WorkspaceService(); -export const WorkspaceMembersList: React.FC<{ searchQuery: string }> = observer(({ searchQuery }) => { +export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ searchQuery }) => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { workspace: workspaceStore, user: userStore } = useMobxStore(); - const workspaceMembers = workspaceStore.workspaceMembers; - const user = userStore.currentWorkspaceMemberInfo; + const { + workspaceMember: { + workspaceMembers, + workspaceMembersWithInvitations, + workspaceMemberInvitations, + fetchWorkspaceMemberInvitations, + }, + user: { currentWorkspaceMemberInfo }, + } = useMobxStore(); // fetching workspace invitations - const { data: workspaceInvitations } = useSWR( + useSWR( workspaceSlug ? `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}` : null, - workspaceSlug ? () => workspaceService.workspaceInvitations(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceMemberInvitations(workspaceSlug.toString()) : null ); - const members = [ - ...(workspaceInvitations?.map((item) => ({ - id: item.id, - memberId: item.id, - avatar: "", - first_name: item.email, - last_name: "", - email: item.email, - display_name: item.email, - role: item.role, - status: item.accepted, - member: false, - accountCreated: item.accepted, - })) || []), - ...(workspaceMembers?.map((item) => ({ - id: item.id, - memberId: item.member?.id, - avatar: item.member?.avatar, - first_name: item.member?.first_name, - last_name: item.member?.last_name, - email: item.member?.email, - display_name: item.member?.display_name, - role: item.role, - status: true, - member: true, - accountCreated: true, - })) || []), - ]; - const searchedMembers = members?.filter((member) => { + const searchedMembers = workspaceMembersWithInvitations?.filter((member: any) => { const fullName = `${member.first_name} ${member.last_name}`.toLowerCase(); const displayName = member.display_name.toLowerCase(); return displayName.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase()); }); - if (!workspaceMembers || !workspaceInvitations || !user) + if ( + !workspaceMembers || + !workspaceMemberInvitations || + !workspaceMembersWithInvitations || + !currentWorkspaceMemberInfo + ) return ( @@ -70,10 +51,10 @@ export const WorkspaceMembersList: React.FC<{ searchQuery: string }> = observer( return (
- {members.length > 0 - ? searchedMembers.map((member) => ) + {workspaceMembersWithInvitations.length > 0 + ? searchedMembers?.map((member) => ) : null} - {searchedMembers.length === 0 && ( + {searchedMembers?.length === 0 && (

No matching member

)}
diff --git a/web/components/workspace/settings/workspace-details.tsx b/web/components/workspace/settings/workspace-details.tsx index 26e1d0262..69034b201 100644 --- a/web/components/workspace/settings/workspace-details.tsx +++ b/web/components/workspace/settings/workspace-details.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, FC } from "react"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; @@ -29,7 +29,7 @@ const defaultValues: Partial = { // services const fileService = new FileService(); -export const WorkspaceDetails: React.FC = observer(() => { +export const WorkspaceDetails: FC = observer(() => { // states const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -37,9 +37,10 @@ export const WorkspaceDetails: React.FC = observer(() => { const [isImageRemoving, setIsImageRemoving] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); // store - const { workspace: workspaceStore, user: userStore } = useMobxStore(); - const activeWorkspace = workspaceStore.currentWorkspace; - const { currentWorkspaceRole } = userStore; + const { + workspace: { currentWorkspace, updateWorkspace }, + user: { currentWorkspaceRole }, + } = useMobxStore(); const isAdmin = currentWorkspaceRole === 20; // hooks const { setToastAlert } = useToast(); @@ -52,11 +53,11 @@ export const WorkspaceDetails: React.FC = observer(() => { setValue, formState: { errors, isSubmitting }, } = useForm({ - defaultValues: { ...defaultValues, ...activeWorkspace }, + defaultValues: { ...defaultValues, ...currentWorkspace }, }); const onSubmit = async (formData: IWorkspace) => { - if (!activeWorkspace) return; + if (!currentWorkspace) return; const payload: Partial = { logo: formData.logo, @@ -64,8 +65,7 @@ export const WorkspaceDetails: React.FC = observer(() => { organization_size: formData.organization_size, }; - await workspaceStore - .updateWorkspace(activeWorkspace.slug, payload) + await updateWorkspace(currentWorkspace.slug, payload) .then(() => setToastAlert({ title: "Success", @@ -77,13 +77,12 @@ export const WorkspaceDetails: React.FC = observer(() => { }; const handleDelete = (url: string | null | undefined) => { - if (!activeWorkspace || !url) return; + if (!currentWorkspace || !url) return; setIsImageRemoving(true); - fileService.deleteFile(activeWorkspace.id, url).then(() => { - workspaceStore - .updateWorkspace(activeWorkspace.slug, { logo: "" }) + fileService.deleteFile(currentWorkspace.id, url).then(() => { + updateWorkspace(currentWorkspace.slug, { logo: "" }) .then(() => { setToastAlert({ type: "success", @@ -104,10 +103,10 @@ export const WorkspaceDetails: React.FC = observer(() => { }; useEffect(() => { - if (activeWorkspace) reset({ ...activeWorkspace }); - }, [activeWorkspace, reset]); + if (currentWorkspace) reset({ ...currentWorkspace }); + }, [currentWorkspace, reset]); - if (!activeWorkspace) + if (!currentWorkspace) return (
@@ -119,13 +118,13 @@ export const WorkspaceDetails: React.FC = observer(() => { setDeleteWorkspaceModal(false)} - data={activeWorkspace} + data={currentWorkspace} /> setIsImageUploadModalOpen(false)} isRemoving={isImageRemoving} - handleDelete={() => handleDelete(activeWorkspace?.logo)} + handleDelete={() => handleDelete(currentWorkspace?.logo)} onSuccess={(imageUrl) => { setIsImageUploading(true); setValue("logo", imageUrl); @@ -148,7 +147,7 @@ export const WorkspaceDetails: React.FC = observer(() => {
) : (
- {activeWorkspace?.name?.charAt(0) ?? "N"} + {currentWorkspace?.name?.charAt(0) ?? "N"}
)} @@ -157,7 +156,7 @@ export const WorkspaceDetails: React.FC = observer(() => {

{watch("name")}

{`${ typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "") - }/${activeWorkspace.slug}`} + }/${currentWorkspace.slug}`}
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( <> -
+
); -}; +}); diff --git a/web/components/workspace/single-invitation.tsx b/web/components/workspace/single-invitation.tsx index 0621c64b3..4db6c5f18 100644 --- a/web/components/workspace/single-invitation.tsx +++ b/web/components/workspace/single-invitation.tsx @@ -9,11 +9,7 @@ type Props = { handleInvitation: any; }; -const SingleInvitation: React.FC = ({ - invitation, - invitationsRespond, - handleInvitation, -}) => ( +const SingleInvitation: React.FC = ({ invitation, invitationsRespond, handleInvitation }) => (
  • diff --git a/web/constants/crisp.tsx b/web/constants/crisp.tsx index d89aaea31..90dfda7c7 100644 --- a/web/constants/crisp.tsx +++ b/web/constants/crisp.tsx @@ -11,8 +11,9 @@ declare global { } const Crisp = observer(() => { - const { user: userStore } = useMobxStore(); - const { currentUser } = userStore; + const { + user: { currentUser }, + } = useMobxStore(); const validateCurrentUser = useCallback(() => { if (currentUser) return currentUser.email; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 144cad190..5ba0f48a0 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -1,4 +1,3 @@ -import { v4 as uuidv4 } from "uuid"; // icons import { Calendar, GanttChartSquare, Kanban, List, Sheet } from "lucide-react"; // types @@ -12,9 +11,6 @@ import { TIssuePriorities, TIssueTypeFilters, TStateGroups, - IIssue, - IProject, - IWorkspace, } from "types"; export const ISSUE_PRIORITIES: { @@ -420,72 +416,3 @@ export const groupReactionEmojis = (reactions: any) => { return _groupedEmojis; }; - -/** - * - * @param workspaceDetail workspace detail to be added in the issue payload - * @param projectDetail project detail to be added in the issue payload - * @param formData partial issue data from the form. This will override the default values - * @returns full issue payload with some default values - */ - -export const createIssuePayload: ( - workspaceDetail: IWorkspace, - projectDetail: IProject, - formData: Partial -) => IIssue = (workspaceDetail: IWorkspace, projectDetail: IProject, formData: Partial) => { - const payload = { - archived_at: null, - assignees: [], - assignee_details: [], - attachment_count: 0, - attachments: [], - issue_relations: [], - related_issues: [], - bridge_id: null, - completed_at: new Date(), - created_at: "", - created_by: "", - cycle: null, - cycle_id: null, - cycle_detail: null, - description: {}, - description_html: "", - description_stripped: "", - estimate_point: null, - issue_cycle: null, - issue_link: [], - issue_module: null, - labels: [], - label_details: [], - is_draft: false, - links_list: [], - link_count: 0, - module: null, - module_id: null, - name: "", - parent: null, - parent_detail: null, - priority: "none", - project: projectDetail.id, - project_detail: projectDetail, - sequence_id: 0, - sort_order: 0, - sprints: null, - start_date: null, - state: projectDetail.default_state, - state_detail: {} as any, - sub_issues_count: 0, - target_date: null, - updated_at: "", - updated_by: "", - workspace: workspaceDetail.id, - workspace_detail: workspaceDetail, - id: uuidv4(), - tempId: uuidv4(), - // to be overridden by the form data - ...formData, - } as IIssue; - - return payload; -}; diff --git a/web/contexts/profile-issues-context.tsx b/web/contexts/profile-issues-context.tsx index 44c17b50a..f7586bdeb 100644 --- a/web/contexts/profile-issues-context.tsx +++ b/web/contexts/profile-issues-context.tsx @@ -3,12 +3,7 @@ import { createContext, useCallback, useReducer } from "react"; // components import ToastAlert from "components/toast-alert"; // types -import { - IIssueFilterOptions, - Properties, - IWorkspaceViewProps, - IIssueDisplayFilterOptions, -} from "types"; +import { IIssueFilterOptions, Properties, IWorkspaceViewProps, IIssueDisplayFilterOptions } from "types"; export const profileIssuesContext = createContext({} as ContextType); @@ -117,9 +112,7 @@ export const reducer: ReducerFunctionType = (state, action) => { } }; -export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { +export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); const setDisplayFilters = useCallback( @@ -133,11 +126,7 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode }, }); - if ( - displayFilter.layout && - displayFilter.layout === "kanban" && - state.display_filters?.group_by === null - ) { + if (displayFilter.layout && displayFilter.layout === "kanban" && state.display_filters?.group_by === null) { dispatch({ type: "SET_DISPLAY_FILTERS", payload: { @@ -154,8 +143,7 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode const setFilters = useCallback( (property: Partial) => { Object.keys(property).forEach((key) => { - if (property[key as keyof typeof property]?.length === 0) - property[key as keyof typeof property] = null; + if (property[key as keyof typeof property]?.length === 0) property[key as keyof typeof property] = null; }); dispatch({ diff --git a/web/contexts/toast.context.tsx b/web/contexts/toast.context.tsx index 69adf0e92..30e100b20 100644 --- a/web/contexts/toast.context.tsx +++ b/web/contexts/toast.context.tsx @@ -72,11 +72,7 @@ export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ }, []); const setToastAlert = useCallback( - (data: { - title: string; - type?: "success" | "error" | "warning" | "info"; - message?: string; - }) => { + (data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => { const id = uuid(); const { title, type, message } = data; dispatch({ diff --git a/web/helpers/array.helper.ts b/web/helpers/array.helper.ts index a682b0a1c..a55ad8fd9 100644 --- a/web/helpers/array.helper.ts +++ b/web/helpers/array.helper.ts @@ -7,11 +7,7 @@ export const groupBy = (array: any[], key: string) => { }, {}); }; -export const orderArrayBy = ( - orgArray: any[], - key: string, - ordering: "ascending" | "descending" = "ascending" -) => { +export const orderArrayBy = (orgArray: any[], key: string, ordering: "ascending" | "descending" = "ascending") => { if (!orgArray || !Array.isArray(orgArray) || orgArray.length === 0) return []; const array = [...orgArray]; @@ -53,3 +49,28 @@ export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | return arr1.length === arr2.length && arr1.every((e) => arr2.includes(e)); }; + +type GroupedItems = { [key: string]: T[] }; + +export const groupByField = (array: T[], field: keyof T): GroupedItems => + array.reduce((grouped: GroupedItems, item: T) => { + const key = String(item[field]); + grouped[key] = (grouped[key] || []).concat(item); + return grouped; + }, {}); + +export const sortByField = (array: any[], field: string): any[] => + array.sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0)); + +export const orderGroupedDataByField = (groupedData: GroupedItems, orderBy: keyof T): GroupedItems => { + for (const key in groupedData) { + if (groupedData.hasOwnProperty(key)) { + groupedData[key] = groupedData[key].sort((a, b) => { + if (a[orderBy] < b[orderBy]) return -1; + if (a[orderBy] > b[orderBy]) return 1; + return 0; + }); + } + } + return groupedData; +}; diff --git a/web/helpers/attachment.helper.ts b/web/helpers/attachment.helper.ts index 24cfb2c49..67e989063 100644 --- a/web/helpers/attachment.helper.ts +++ b/web/helpers/attachment.helper.ts @@ -1,5 +1,4 @@ -export const getFileExtension = (filename: string) => - filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); +export const getFileExtension = (filename: string) => filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); export const getFileName = (fileName: string) => { const dotIndex = fileName.lastIndexOf("."); diff --git a/web/helpers/common.helper.ts b/web/helpers/common.helper.ts index 7fb8896de..ed199fed4 100644 --- a/web/helpers/common.helper.ts +++ b/web/helpers/common.helper.ts @@ -17,6 +17,4 @@ export const debounce = (func: any, wait: number, immediate: boolean = false) => }; }; -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL - ? process.env.NEXT_PUBLIC_API_BASE_URL - : ""; +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : ""; diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index dced747f9..461737915 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -373,7 +373,7 @@ export const findTotalDaysInRange = (startDate: Date | string, endDate: Date | s // find number of days between startDate and endDate const diffInTime = endDate.getTime() - startDate.getTime(); - const diffInDays = diffInTime / (1000 * 3600 * 24); + const diffInDays = Math.floor(diffInTime / (1000 * 3600 * 24)); // if inclusive is true, add 1 to diffInDays if (inclusive) return diffInDays + 1; diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 38b830071..026211634 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -41,13 +41,16 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: reactions: any, key: string ) => { - const groupedReactions = reactions.reduce((acc: any, reaction: any) => { - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, {} as { [key: string]: any[] }); + const groupedReactions = reactions.reduce( + (acc: any, reaction: any) => { + if (!acc[reaction[key]]) { + acc[reaction[key]] = []; + } + acc[reaction[key]].push(reaction); + return acc; + }, + {} as { [key: string]: any[] } + ); return groupedReactions; }; diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 6f4381dec..39424c76e 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -1,7 +1,16 @@ +import { v4 as uuidv4 } from "uuid"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "types"; +import { + IIssue, + TIssueGroupByOptions, + TIssueLayouts, + TIssueOrderByOptions, + TIssueParams, + IProject, + IWorkspace, +} from "types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -109,3 +118,82 @@ export const handleIssueQueryParamsByLayout = ( return queryParams; }; + +/** + * + * @description create a full issue payload with some default values. This function also parse the form field + * like assignees, labels, etc. and add them to the payload + * @param workspaceDetail workspace detail to be added in the issue payload + * @param projectDetail project detail to be added in the issue payload + * @param formData partial issue data from the form. This will override the default values + * @returns full issue payload with some default values + */ + +export const createIssuePayload: ( + workspaceDetail: IWorkspace, + projectDetail: IProject, + formData: Partial +) => IIssue = (workspaceDetail: IWorkspace, projectDetail: IProject, formData: Partial) => { + const payload = { + archived_at: null, + assignee_details: [], + attachment_count: 0, + attachments: [], + issue_relations: [], + related_issues: [], + bridge_id: null, + completed_at: new Date(), + created_at: "", + created_by: "", + cycle: null, + cycle_id: null, + cycle_detail: null, + description: {}, + description_html: "", + description_stripped: "", + estimate_point: null, + issue_cycle: null, + issue_link: [], + issue_module: null, + label_details: [], + is_draft: false, + links_list: [], + link_count: 0, + module: null, + module_id: null, + name: "", + parent: null, + parent_detail: null, + priority: "none", + project: projectDetail.id, + project_detail: projectDetail, + sequence_id: 0, + sort_order: 0, + sprints: null, + start_date: null, + state: projectDetail.default_state, + state_detail: {} as any, + sub_issues_count: 0, + target_date: null, + updated_at: "", + updated_by: "", + workspace: workspaceDetail.id, + workspace_detail: workspaceDetail, + id: uuidv4(), + tempId: uuidv4(), + // to be overridden by the form data + ...formData, + assignees: Array.isArray(formData.assignees) + ? formData.assignees + : formData.assignees && formData.assignees !== "none" && formData.assignees !== null + ? [formData.assignees] + : [], + labels: Array.isArray(formData.labels) + ? formData.labels + : formData.labels && formData.labels !== "none" && formData.labels !== null + ? [formData.labels] + : [], + } as IIssue; + + return payload; +}; diff --git a/web/helpers/state.helper.ts b/web/helpers/state.helper.ts index edf740c10..ef6c3ba77 100644 --- a/web/helpers/state.helper.ts +++ b/web/helpers/state.helper.ts @@ -1,27 +1,7 @@ // types -import { IState, IStateResponse } from "types"; +import { IStateResponse } from "types"; -export const orderStateGroups = ( - unorderedStateGroups: IStateResponse | undefined -): IStateResponse | undefined => { +export const orderStateGroups = (unorderedStateGroups: IStateResponse | undefined): IStateResponse | undefined => { if (!unorderedStateGroups) return undefined; - - return Object.assign( - { backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, - unorderedStateGroups - ); -}; - -export const getStatesList = (stateGroups: IStateResponse | undefined): IState[] | undefined => { - if (!stateGroups) return undefined; - - // order the unordered state groups first - const orderedStateGroups = orderStateGroups(stateGroups); - - if (!orderedStateGroups) return undefined; - - // extract states from the groups and return them - return Object.keys(orderedStateGroups) - .map((group) => [...orderedStateGroups[group].map((state: IState) => state)]) - .flat(); + return Object.assign({ backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, unorderedStateGroups); }; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index e3eca37ff..18dfe1254 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -19,12 +19,14 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { children } = props; // store const { - user: userStore, - project: projectStore, - cycle: cycleStore, - module: moduleStore, - projectViews: projectViewsStore, - inbox: inboxStore, + user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, + project: { fetchProjectDetails, fetchProjectLabels, fetchProjectMembers, fetchProjectEstimates, workspaceProjects }, + projectState: { fetchProjectStates }, + cycle: { fetchCycles }, + module: { fetchModules }, + projectViews: { fetchAllViews }, + inbox: { fetchInboxesList, isInboxEnabled }, + commandPalette: { toggleCreateProjectModal }, } = useMobxStore(); // router const router = useRouter(); @@ -33,70 +35,54 @@ export const ProjectAuthWrapper: FC = observer((props) => { // fetching project details useSWR( workspaceSlug && projectId ? `PROJECT_DETAILS_${workspaceSlug.toString()}_${projectId.toString()}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null ); // fetching user project member information useSWR( workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => userStore.fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project labels useSWR( workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project members useSWR( workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project states useSWR( workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project estimates useSWR( workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project cycles useSWR( workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => cycleStore.fetchCycles(workspaceSlug.toString(), projectId.toString(), "all") - : null + workspaceSlug && projectId ? () => fetchCycles(workspaceSlug.toString(), projectId.toString(), "all") : null ); // fetching project modules useSWR( workspaceSlug && projectId ? `PROJECT_MODULES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => moduleStore.fetchModules(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project views useSWR( workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectViewsStore.fetchAllViews(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null ); // TODO: fetching project pages // fetching project inboxes if inbox is enabled useSWR( - workspaceSlug && projectId && inboxStore.isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId && inboxStore.isInboxEnabled - ? () => inboxStore.fetchInboxesList(workspaceSlug.toString(), projectId.toString()) + workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId && isInboxEnabled + ? () => fetchInboxesList(workspaceSlug.toString(), projectId.toString()) : null, { revalidateOnFocus: false, @@ -104,11 +90,11 @@ export const ProjectAuthWrapper: FC = observer((props) => { } ); - const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + const projectsList = workspaceSlug ? workspaceProjects : null; const projectExists = projectId ? projectsList?.find((project) => project.id === projectId.toString()) : null; // check if the project member apis is loading - if (!userStore.projectMemberInfo && projectId && userStore.hasPermissionToProject[projectId.toString()] === null) + if (!projectMemberInfo && projectId && hasPermissionToProject[projectId.toString()] === null) return (
    @@ -118,11 +104,10 @@ export const ProjectAuthWrapper: FC = observer((props) => { ); // check if the user don't have permission to access the project - if (projectExists && projectId && userStore.hasPermissionToProject[projectId.toString()] === false) - return ; + if (projectExists && projectId && hasPermissionToProject[projectId.toString()] === false) return ; // check if the project info is not found. - if (!projectExists && projectId && userStore.hasPermissionToProject[projectId.toString()] === false) + if (!projectExists && projectId && hasPermissionToProject[projectId.toString()] === false) return (
    = observer((props) => { image={emptyProject} primaryButton={{ text: "Create Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => toggleCreateProjectModal(true), }} />
    diff --git a/web/layouts/auth-layout/user-wrapper.tsx b/web/layouts/auth-layout/user-wrapper.tsx index 596612153..6072f1673 100644 --- a/web/layouts/auth-layout/user-wrapper.tsx +++ b/web/layouts/auth-layout/user-wrapper.tsx @@ -13,19 +13,22 @@ export interface IUserAuthWrapper { export const UserAuthWrapper: FC = (props) => { const { children } = props; // store - const { user: userStore, workspace: workspaceStore } = useMobxStore(); + const { + user: { fetchCurrentUser, fetchCurrentUserSettings }, + workspace: { fetchWorkspaces }, + } = useMobxStore(); // router const router = useRouter(); // fetching user information - const { data: currentUser, error } = useSWR("CURRENT_USER_DETAILS", () => userStore.fetchCurrentUser(), { + const { data: currentUser, error } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), { shouldRetryOnError: false, }); // fetching user settings - useSWR("CURRENT_USER_SETTINGS", () => userStore.fetchCurrentUserSettings(), { + useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), { shouldRetryOnError: false, }); // fetching all workspaces - useSWR(`USER_WORKSPACES_LIST`, () => workspaceStore.fetchWorkspaces(), { + useSWR(`USER_WORKSPACES_LIST`, () => fetchWorkspaces(), { shouldRetryOnError: false, }); diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index 376fe2e28..3cfe4e7c8 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -15,30 +15,35 @@ export interface IWorkspaceAuthWrapper { export const WorkspaceAuthWrapper: FC = observer((props) => { const { children } = props; // store - const { user: userStore, project: projectStore, workspace: workspaceStore } = useMobxStore(); - const { currentWorkspaceMemberInfo, hasPermissionToCurrentWorkspace } = userStore; + const { + user: { currentWorkspaceMemberInfo, hasPermissionToCurrentWorkspace, fetchUserWorkspaceInfo }, + project: { fetchProjects }, + workspace: { fetchWorkspaceLabels }, + workspaceMember: { fetchWorkspaceMembers }, + } = useMobxStore(); + // router const router = useRouter(); const { workspaceSlug } = router.query; // fetching user workspace information useSWR( workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, - workspaceSlug ? () => userStore.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchUserWorkspaceInfo(workspaceSlug.toString()) : null ); // fetching workspace projects useSWR( workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null, - workspaceSlug ? () => projectStore.fetchProjects(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null ); // fetch workspace members useSWR( workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, - workspaceSlug ? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null ); // fetch workspace labels useSWR( workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null, - workspaceSlug ? () => workspaceStore.fetchWorkspaceLabels(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null ); // while data is being loaded diff --git a/web/lib/auth.ts b/web/lib/auth.ts index 56cfab9ae..6f84db402 100644 --- a/web/lib/auth.ts +++ b/web/lib/auth.ts @@ -76,15 +76,12 @@ export const requiredWorkspaceAdmin = async (workspaceSlug: string, cookie?: str let memberDetail: IWorkspaceMember | null = null; try { - const data = await fetch( - `${API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-members/me/`, - { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) + const data = await fetch(`${API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-members/me/`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }) .then((res) => res.json()) .then((data) => data); @@ -143,9 +140,7 @@ export const homePageRedirect = async (cookie?: string) => { }; } - const lastActiveWorkspace = workspaces.find( - (workspace) => workspace.id === user.last_workspace_id - ); + const lastActiveWorkspace = workspaces.find((workspace) => workspace.id === user.last_workspace_id); if (lastActiveWorkspace) { return { diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 66d81b5aa..f89aa72c7 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -62,12 +62,13 @@ const MobxStoreInit = observer(() => { */ useEffect(() => { if (workspaceSlug) setWorkspaceSlug(workspaceSlug.toString()); - if (projectId) setProjectId(projectId.toString()); - if (cycleId) setCycleId(cycleId.toString()); - if (moduleId) setModuleId(moduleId.toString()); - if (globalViewId) setGlobalViewId(globalViewId.toString()); - if (viewId) setViewId(viewId.toString()); - if (inboxId) setInboxId(inboxId.toString()); + + setProjectId(projectId?.toString() ?? null); + setCycleId(cycleId?.toString() ?? null); + setModuleId(moduleId?.toString() ?? null); + setGlobalViewId(globalViewId?.toString() ?? null); + setViewId(viewId?.toString() ?? null); + setInboxId(inboxId?.toString() ?? null); }, [ workspaceSlug, projectId, diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 0faa835ad..a71904e67 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -28,38 +28,36 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { project: projectStore, user: userStore } = useMobxStore(); - - const user = userStore.currentUser; - const projects = workspaceSlug ? projectStore.projects[workspaceSlug?.toString()] : null; + const { + project: { workspaceProjects }, + user: { currentUser }, + commandPalette: { toggleCreateProjectModal }, + } = useMobxStore(); const trackAnalyticsEvent = (tab: string) => { - if (!user) return; - + if (!currentUser) return; const eventPayload = { workspaceSlug: workspaceSlug?.toString(), }; - const eventType = tab === "scope_and_demand" ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS"; - - trackEventService.trackAnalyticsEvent(eventPayload, eventType, user); + trackEventService.trackAnalyticsEvent(eventPayload, eventType, currentUser); }; useEffect(() => { if (!workspaceSlug) return; - if (user && workspaceSlug) + if (currentUser && workspaceSlug) trackEventService.trackAnalyticsEvent( { workspaceSlug: workspaceSlug?.toString() }, "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS", - user + currentUser ); - }, [user, workspaceSlug]); + }, [currentUser, workspaceSlug]); return ( <> - {projects && projects.length > 0 ? ( + {workspaceProjects && workspaceProjects.length > 0 ? (
    @@ -96,12 +94,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { primaryButton={{ icon: , text: "New Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => toggleCreateProjectModal(true), }} /> diff --git a/web/pages/[workspaceSlug]/me/profile/preferences.tsx b/web/pages/[workspaceSlug]/me/profile/preferences.tsx index 5f915ec10..d08b9da3c 100644 --- a/web/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/web/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -18,11 +18,13 @@ import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; import { NextPageWithLayout } from "types/app"; const ProfilePreferencesPage: NextPageWithLayout = observer(() => { - const { user: userStore } = useMobxStore(); + const { + user: { currentUser, updateCurrentUserTheme }, + } = useMobxStore(); // states const [currentTheme, setCurrentTheme] = useState(null); // computed - const userTheme = userStore.currentUser?.theme; + const userTheme = currentUser?.theme; // hooks const { setTheme } = useTheme(); const { setToastAlert } = useToast(); @@ -38,7 +40,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => { const handleThemeChange = (themeOption: I_THEME_OPTION) => { setTheme(themeOption.value); - userStore.updateCurrentUserTheme(themeOption.value).catch(() => { + updateCurrentUserTheme(themeOption.value).catch(() => { setToastAlert({ title: "Failed to Update the theme", type: "error", @@ -48,7 +50,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => { return ( <> - {userStore.currentUser ? ( + {currentUser ? (

    Preferences

    diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 318d61cf5..f0f16fa1c 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -58,7 +58,7 @@ const CycleDetailPage: NextPageWithLayout = () => {
    {cycleId && !isSidebarCollapsed && (
    {
    {moduleId && !isSidebarCollapsed && (
    { const handleChange = async (formData: Partial) => { if (!workspaceSlug || !projectId || !projectDetails) return; - mutate( - PROJECT_DETAILS(projectId as string), - (prevData) => ({ ...(prevData as IProject), ...formData }), - false - ); - - mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), - (prevData) => (prevData ?? []).map((p) => (p.id === projectDetails.id ? { ...p, ...formData } : p)), - false - ); - await projectService .updateProject(workspaceSlug as string, projectId as string, formData, user) .then(() => {}) @@ -72,8 +60,8 @@ const AutomationSettingsPage: NextPageWithLayout = () => {

    Automations

    - - + + ); }; diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index 74a9c6724..303c0a9d1 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -1,7 +1,9 @@ import { useState, ReactElement } from "react"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; // hooks -import useUser from "hooks/use-user"; +import useToast from "hooks/use-toast"; +import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; @@ -14,15 +16,41 @@ import { Button } from "@plane/ui"; import { Search } from "lucide-react"; // types import { NextPageWithLayout } from "types/app"; +import { IWorkspaceBulkInviteFormData } from "types"; -const WorkspaceMembersSettingsPage: NextPageWithLayout = () => { +const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; + // store + const { + workspaceMember: { inviteMembersToWorkspace }, + } = useMobxStore(); // states const [inviteModal, setInviteModal] = useState(false); const [searchQuery, setSearchQuery] = useState(""); // hooks - const { user } = useUser(); + const { setToastAlert } = useToast(); + + const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => { + if (!workspaceSlug) return; + + return inviteMembersToWorkspace(workspaceSlug.toString(), data) + .then(async () => { + setInviteModal(false); + setToastAlert({ + type: "success", + title: "Success!", + message: "Invitations sent successfully.", + }); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: `${err.error ?? "Something went wrong. Please try again."}`, + }) + ); + }; return ( <> @@ -30,8 +58,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = () => { setInviteModal(false)} - workspaceSlug={workspaceSlug.toString()} - user={user} + onSubmit={handleWorkspaceInvite} /> )}
    @@ -55,7 +82,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = () => {
    ); -}; +}); WorkspaceMembersSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index 7a84cfd9f..425862839 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -12,9 +12,7 @@ import Script from "next/script"; class MyDocument extends Document { render() { - const isSessionRecorderEnabled = parseInt( - process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0" - ); + const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0"); return ( diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 51ae06b9e..7d3a45610 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -18,23 +18,23 @@ import { IWorkspace } from "types"; import { NextPageWithLayout } from "types/app"; const CreateWorkspacePage: NextPageWithLayout = observer(() => { + // router + const router = useRouter(); + // store + const { + user: { currentUser, updateCurrentUser }, + } = useMobxStore(); + // states const [defaultValues, setDefaultValues] = useState({ name: "", slug: "", organization_size: "", }); - - const router = useRouter(); - - const { user: userStore } = useMobxStore(); - const user = userStore.currentUser; - + // hooks const { theme } = useTheme(); const onSubmit = async (workspace: IWorkspace) => { - await userStore - .updateCurrentUser({ last_workspace_id: workspace.id }) - .then(() => router.push(`/${workspace.slug}`)); + await updateCurrentUser({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`)); }; return ( @@ -54,7 +54,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
    - {user?.email} + {currentUser?.email}
    diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index f8c5e3e6f..cd64290c7 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -30,9 +30,12 @@ const workspaceService = new WorkspaceService(); const OnboardingPage: NextPageWithLayout = observer(() => { const [step, setStep] = useState(null); - const { user: userStore, workspace: workspaceStore } = useMobxStore(); + const { + user: { currentUser, updateCurrentUser, updateUserOnBoard }, + workspace: workspaceStore, + } = useMobxStore(); - const user = userStore.currentUser ?? undefined; + const user = currentUser ?? undefined; const workspaces = workspaceStore.workspaces; const userWorkspaces = workspaceStore.workspacesCreateByCurrentUser; @@ -48,7 +51,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { const updateLastWorkspace = async () => { if (!workspaces) return; - await userStore.updateCurrentUser({ + await updateCurrentUser({ last_workspace_id: workspaces[0]?.id, }); }; @@ -64,14 +67,14 @@ const OnboardingPage: NextPageWithLayout = observer(() => { }, }; - await userStore.updateCurrentUser(payload); + await updateCurrentUser(payload); }; // complete onboarding const finishOnboarding = async () => { if (!user) return; - await userStore.updateUserOnBoard(); + await updateUserOnBoard(); }; useEffect(() => { diff --git a/web/services/file.service.ts b/web/services/file.service.ts index 0e3749a4c..84907161e 100644 --- a/web/services/file.service.ts +++ b/web/services/file.service.ts @@ -2,6 +2,7 @@ import { APIService } from "services/api.service"; // helpers import { API_BASE_URL } from "helpers/common.helper"; +import axios from "axios"; export interface UnSplashImage { id: string; @@ -28,25 +29,38 @@ export interface UnSplashImageUrls { } export class FileService extends APIService { + private cancelSource: any; + constructor() { super(API_BASE_URL); this.uploadFile = this.uploadFile.bind(this); this.deleteImage = this.deleteImage.bind(this); + this.cancelUpload = this.cancelUpload.bind(this); } async uploadFile(workspaceSlug: string, file: FormData): Promise { + this.cancelSource = axios.CancelToken.source(); return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { headers: { ...this.getHeaders(), "Content-Type": "multipart/form-data", }, + cancelToken: this.cancelSource.token, }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + if (axios.isCancel(error)) { + console.log(error.message); + } else { + throw error?.response?.data; + } }); } + cancelUpload() { + this.cancelSource.cancel("Upload cancelled"); + } + getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { return async (file: File) => { const formData = new FormData(); diff --git a/web/services/project/project_state.service.ts b/web/services/project/project_state.service.ts index 018df3a20..44b649327 100644 --- a/web/services/project/project_state.service.ts +++ b/web/services/project/project_state.service.ts @@ -4,7 +4,7 @@ import { TrackEventService } from "services/track_event.service"; // helpers import { API_BASE_URL } from "helpers/common.helper"; // types -import type { IUser, IState, IStateResponse } from "types"; +import type { IUser, IState } from "types"; const trackEventService = new TrackEventService(); @@ -13,7 +13,7 @@ export class ProjectStateService extends APIService { super(API_BASE_URL); } - async createState(workspaceSlug: string, projectId: string, data: any, user: IUser | undefined): Promise { + async createState(workspaceSlug: string, projectId: string, data: any, user: IUser | undefined): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data) .then((response) => { trackEventService.trackStateEvent(response?.data, "STATE_CREATE", user as IUser); @@ -24,17 +24,16 @@ export class ProjectStateService extends APIService { }); } - async getStates(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`) + async markDefault(workspaceSlug: string, projectId: string, stateId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/mark-default/`, {}) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + throw error?.response; }); } - async getIssuesByState(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/?group_by=state`) - + async getStates(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/store/cycle/cycles.store.ts b/web/store/cycle/cycles.store.ts index 06ac61854..15cf4a28e 100644 --- a/web/store/cycle/cycles.store.ts +++ b/web/store/cycle/cycles.store.ts @@ -32,7 +32,7 @@ export interface ICycleStore { // actions setCycleView: (_cycleView: TCycleView) => void; setCycleLayout: (_cycleLayout: TCycleLayout) => void; - setCycleId: (cycleId: string) => void; + setCycleId: (cycleId: string | null) => void; validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; @@ -131,7 +131,7 @@ export class CycleStore implements ICycleStore { // actions setCycleView = (_cycleView: TCycleView) => (this.cycleView = _cycleView); setCycleLayout = (_cycleLayout: TCycleLayout) => (this.cycleLayout = _cycleLayout); - setCycleId = (cycleId: string) => (this.cycleId = cycleId); + setCycleId = (cycleId: string | null) => (this.cycleId = cycleId); validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { try { diff --git a/web/store/editor/index.ts b/web/store/editor/index.ts index ff3ce7a33..f1e23e813 100644 --- a/web/store/editor/index.ts +++ b/web/store/editor/index.ts @@ -1 +1 @@ -export * from "./mentions.store" \ No newline at end of file +export * from "./mentions.store"; diff --git a/web/store/editor/mentions.store.ts b/web/store/editor/mentions.store.ts index 4bf1f45c3..11296450f 100644 --- a/web/store/editor/mentions.store.ts +++ b/web/store/editor/mentions.store.ts @@ -3,43 +3,44 @@ import { RootStore } from "../root"; import { computed, makeObservable } from "mobx"; export interface IMentionsStore { - mentionSuggestions: IMentionSuggestion[]; - mentionHighlights: IMentionHighlight[]; + mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: IMentionHighlight[]; } -export class MentionsStore implements IMentionsStore{ +export class MentionsStore implements IMentionsStore { + // root store + rootStore; - // root store - rootStore; + constructor(_rootStore: RootStore) { + // rootStore + this.rootStore = _rootStore; - constructor(_rootStore: RootStore ){ + makeObservable(this, { + mentionHighlights: computed, + mentionSuggestions: computed, + }); + } - // rootStore - this.rootStore = _rootStore; + get mentionSuggestions() { + const projectMembers = this.rootStore.project.projectMembers; - makeObservable(this, { - mentionHighlights: computed, - mentionSuggestions: computed - }) - } - - get mentionSuggestions() { - const projectMembers = this.rootStore.project.projectMembers - - const suggestions = projectMembers === null ? [] : projectMembers.map((member) => ({ + const suggestions = + projectMembers === null + ? [] + : projectMembers.map((member) => ({ id: member.member.id, type: "User", title: member.member.display_name, subtitle: member.member.email ?? "", avatar: member.member.avatar, redirect_uri: `/${member.workspace.slug}/profile/${member.member.id}`, - })) + })); - return suggestions - } + return suggestions; + } - get mentionHighlights() { - const user = this.rootStore.user.currentUser; - return user ? [user.id] : [] - } -} \ No newline at end of file + get mentionHighlights() { + const user = this.rootStore.user.currentUser; + return user ? [user.id] : []; + } +} diff --git a/web/store/global-view/global_views.store.ts b/web/store/global-view/global_views.store.ts index c9915b8d8..588f0a4d2 100644 --- a/web/store/global-view/global_views.store.ts +++ b/web/store/global-view/global_views.store.ts @@ -19,7 +19,7 @@ export interface IGlobalViewsStore { }; // actions - setGlobalViewId: (viewId: string) => void; + setGlobalViewId: (viewId: string | null) => void; fetchAllGlobalViews: (workspaceSlug: string) => Promise; fetchGlobalViewDetails: (workspaceSlug: string, viewId: string) => Promise; @@ -72,7 +72,7 @@ export class GlobalViewsStore implements IGlobalViewsStore { this.workspaceService = new WorkspaceService(); } - setGlobalViewId = (viewId: string) => { + setGlobalViewId = (viewId: string | null) => { this.globalViewId = viewId; }; diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts index c1ca08609..b29d36855 100644 --- a/web/store/inbox/inbox.store.ts +++ b/web/store/inbox/inbox.store.ts @@ -22,7 +22,7 @@ export interface IInboxStore { }; // actions - setInboxId: (inboxId: string) => void; + setInboxId: (inboxId: string | null) => void; getInboxId: (projectId: string) => string | null; @@ -100,7 +100,7 @@ export class InboxStore implements IInboxStore { return this.inboxesList[projectId]?.[0]?.id ?? null; }; - setInboxId = (inboxId: string) => { + setInboxId = (inboxId: string | null) => { runInAction(() => { this.inboxId = inboxId; }); diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts index 91a11cd76..0dc122438 100644 --- a/web/store/module/modules.store.ts +++ b/web/store/module/modules.store.ts @@ -34,7 +34,7 @@ export interface IModuleStore { }; // actions - setModuleId: (moduleSlug: string) => void; + setModuleId: (moduleId: string | null) => void; getModuleById: (moduleId: string) => IModule | null; @@ -144,8 +144,8 @@ export class ModuleStore implements IModuleStore { getModuleById = (moduleId: string) => this.moduleDetails[moduleId] || null; // actions - setModuleId = (moduleSlug: string) => { - this.moduleId = moduleSlug ?? null; + setModuleId = (moduleId: string | null) => { + this.moduleId = moduleId; }; fetchModules = async (workspaceSlug: string, projectId: string) => { diff --git a/web/store/project-view/project_views.store.ts b/web/store/project-view/project_views.store.ts index 4c4baf487..76c58002d 100644 --- a/web/store/project-view/project_views.store.ts +++ b/web/store/project-view/project_views.store.ts @@ -20,7 +20,7 @@ export interface IProjectViewsStore { }; // actions - setViewId: (viewId: string) => void; + setViewId: (viewId: string | null) => void; fetchAllViews: (workspaceSlug: string, projectId: string) => Promise; fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; @@ -82,7 +82,7 @@ export class ProjectViewsStore implements IProjectViewsStore { this.viewService = new ViewService(); } - setViewId = (viewId: string) => { + setViewId = (viewId: string | null) => { this.viewId = viewId; }; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 976654ee9..e2ab7f004 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -1,7 +1,7 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // types import { RootStore } from "../root"; -import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState, IEstimate } from "types"; +import { IProject, IIssueLabels, IProjectMember, IEstimate } from "types"; // services import { ProjectService, ProjectStateService, ProjectEstimateService } from "services/project"; import { IssueService, IssueLabelService } from "services/issue"; @@ -16,9 +16,6 @@ export interface IProjectStore { project_details: { [projectId: string]: IProject; // projectId: project Info }; - states: { - [projectId: string]: IStateResponse; // project_id: states - } | null; labels: { [projectId: string]: IIssueLabels[] | null; // project_id: labels } | null; @@ -31,9 +28,7 @@ export interface IProjectStore { // computed searchedProjects: IProject[]; - workspaceProjects: IProject[]; - projectStatesByGroups: IStateResponse | null; - projectStates: IState[] | null; + workspaceProjects: IProject[] | null; projectLabels: IIssueLabels[] | null; projectMembers: IProjectMember[] | null; projectEstimates: IEstimate[] | null; @@ -44,11 +39,10 @@ export interface IProjectStore { currentProjectDetails: IProject | undefined; // actions - setProjectId: (projectId: string) => void; + setProjectId: (projectId: string | null) => void; setSearchQuery: (query: string) => void; getProjectById: (workspaceSlug: string, projectId: string) => IProject | null; - getProjectStateById: (stateId: string) => IState | null; getProjectLabelById: (labelId: string) => IIssueLabels | null; getProjectMemberById: (memberId: string) => IProjectMember | null; getProjectMemberByUserId: (memberId: string) => IProjectMember | null; @@ -56,7 +50,6 @@ export interface IProjectStore { fetchProjects: (workspaceSlug: string) => Promise; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise; - fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise; fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise; fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise; @@ -93,9 +86,6 @@ export class ProjectStore implements IProjectStore { project_details: { [projectId: string]: IProject; // projectId: project } = {}; - states: { - [projectId: string]: IStateResponse; // projectId: states - } | null = {}; labels: { [projectId: string]: IIssueLabels[]; // projectId: labels } | null = {}; @@ -125,7 +115,6 @@ export class ProjectStore implements IProjectStore { projectId: observable.ref, projects: observable.ref, project_details: observable.ref, - states: observable.ref, labels: observable.ref, members: observable.ref, estimates: observable.ref, @@ -133,8 +122,6 @@ export class ProjectStore implements IProjectStore { // computed searchedProjects: computed, workspaceProjects: computed, - projectStatesByGroups: computed, - projectStates: computed, projectLabels: computed, projectMembers: computed, projectEstimates: computed, @@ -151,12 +138,10 @@ export class ProjectStore implements IProjectStore { fetchProjectDetails: action, getProjectById: action, - getProjectStateById: action, getProjectLabelById: action, getProjectMemberById: action, getProjectEstimateById: action, - fetchProjectStates: action, fetchProjectLabels: action, fetchProjectMembers: action, fetchProjectEstimates: action, @@ -198,8 +183,10 @@ export class ProjectStore implements IProjectStore { } get workspaceProjects() { - if (!this.rootStore.workspace.workspaceSlug) return []; - return this.projects?.[this.rootStore.workspace.workspaceSlug]; + if (!this.rootStore.workspace.workspaceSlug) return null; + const projects = this.projects[this.rootStore.workspace.workspaceSlug]; + if (!projects) return null; + return projects; } get currentProjectDetails() { @@ -217,24 +204,6 @@ export class ProjectStore implements IProjectStore { return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_favorite); } - get projectStatesByGroups() { - if (!this.projectId) return null; - return this.states?.[this.projectId] || null; - } - - get projectStates() { - if (!this.projectId) return null; - const stateByGroups: IStateResponse | null = this.projectStatesByGroups; - if (!stateByGroups) return null; - const _states: IState[] = []; - Object.keys(stateByGroups).forEach((_stateGroup: string) => { - stateByGroups[_stateGroup].map((state) => { - _states.push(state); - }); - }); - return _states.length > 0 ? _states : null; - } - get projectLabels() { if (!this.projectId) return null; return this.labels?.[this.projectId] || null; @@ -251,8 +220,8 @@ export class ProjectStore implements IProjectStore { } // actions - setProjectId = (projectId: string) => { - this.projectId = projectId ?? null; + setProjectId = (projectId: string | null) => { + this.projectId = projectId; }; setSearchQuery = (query: string) => { @@ -304,14 +273,6 @@ export class ProjectStore implements IProjectStore { return projectInfo; }; - getProjectStateById = (stateId: string) => { - if (!this.projectId) return null; - const states = this.projectStates; - if (!states) return null; - const stateInfo: IState | null = states.find((state) => state.id === stateId) || null; - return stateInfo; - }; - getProjectLabelById = (labelId: string) => { if (!this.projectId) return null; const labels = this.projectLabels; @@ -344,29 +305,6 @@ export class ProjectStore implements IProjectStore { return estimateInfo; }; - fetchProjectStates = async (workspaceSlug: string, projectId: string) => { - try { - this.loader = true; - this.error = null; - - const stateResponse = await this.stateService.getStates(workspaceSlug, projectId); - const _states = { - ...this.states, - [projectId]: stateResponse, - }; - - runInAction(() => { - this.states = _states; - this.loader = false; - this.error = null; - }); - } catch (error) { - console.error(error); - this.loader = false; - this.error = error; - } - }; - fetchProjectLabels = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; diff --git a/web/store/project/project_state.store.ts b/web/store/project/project_state.store.ts index 56fc0c203..b0b136cb2 100644 --- a/web/store/project/project_state.store.ts +++ b/web/store/project/project_state.store.ts @@ -1,17 +1,23 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; +import { observable, action, makeObservable, runInAction, computed } from "mobx"; // types import { RootStore } from "../root"; import { IState } from "types"; // services import { ProjectService, ProjectStateService } from "services/project"; -import { groupBy, orderArrayBy } from "helpers/array.helper"; +import { groupBy, orderArrayBy, groupByField } from "helpers/array.helper"; import { orderStateGroups } from "helpers/state.helper"; export interface IProjectStateStore { loader: boolean; error: any | null; - // states + states: { + [projectId: string]: IState[]; // projectId: states + }; + groupedProjectStates: { [groupId: string]: IState[] } | null; + projectStates: IState[] | null; + + fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; createState: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateState: (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => Promise; deleteState: (workspaceSlug: string, projectId: string, stateId: string) => Promise; @@ -28,7 +34,9 @@ export interface IProjectStateStore { export class ProjectStateStore implements IProjectStateStore { loader: boolean = false; error: any | null = null; - + states: { + [projectId: string]: IState[]; // projectId: states + } = {}; // root store rootStore; // service @@ -38,10 +46,13 @@ export class ProjectStateStore implements IProjectStateStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observable - loader: observable, - error: observable, - - // states + loader: observable.ref, + error: observable.ref, + states: observable.ref, + // computed + projectStates: computed, + groupedProjectStates: computed, + // actions createState: action, updateState: action, deleteState: action, @@ -54,6 +65,43 @@ export class ProjectStateStore implements IProjectStateStore { this.stateService = new ProjectStateService(); } + get groupedProjectStates() { + if (!this.rootStore.project.projectId) return null; + const states = this.states[this.rootStore.project.projectId]; + if (!states) return null; + return groupByField(states, "group"); + } + + get projectStates() { + if (!this.rootStore.project.projectId) return null; + const states = this.states[this.rootStore.project.projectId]; + if (!states) return null; + return states; + } + + fetchProjectStates = async (workspaceSlug: string, projectId: string) => { + try { + const states = await this.stateService.getStates(workspaceSlug, projectId); + runInAction(() => { + this.states = { + ...this.states, + [projectId]: states, + }; + }); + return states; + } catch (error) { + throw error; + } + }; + + getProjectStateById = (stateId: string) => { + if (!this.rootStore.project.projectId) return null; + const states = this.states[this.rootStore.project.projectId]; + if (!states) return null; + const stateInfo: IState | null = states.find((state) => state.id === stateId) || null; + return stateInfo; + }; + createState = async (workspaceSlug: string, projectId: string, data: Partial) => { try { const response = await this.stateService.createState( @@ -64,12 +112,9 @@ export class ProjectStateStore implements IProjectStateStore { ); runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [response.group]: [...(this.rootStore.project.states?.[projectId]?.[response.group] || []), response], - }, + this.states = { + ...this.states, + [projectId]: [...this.states?.[projectId], response], }; }); @@ -81,21 +126,23 @@ export class ProjectStateStore implements IProjectStateStore { }; updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => { - const originalStates = this.rootStore.project.states || {}; - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [data.group as string]: (this.rootStore.project.states?.[projectId]?.[data.group as string] || []).map( - (state) => (state.id === stateId ? { ...state, ...data } : state) - ), - }, - }; - }); + const originalStates = this.states; try { + runInAction(() => { + this.states = { + ...this.states, + [projectId]: [ + ...this.states?.[projectId].map((state) => { + if (state.id === stateId) { + return { ...state, ...data }; + } + return state; + }), + ], + }; + }); + const response = await this.stateService.patchState( workspaceSlug, projectId, @@ -104,41 +151,30 @@ export class ProjectStateStore implements IProjectStateStore { this.rootStore.user.currentUser! ); - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [response.group]: (this.rootStore.project.states?.[projectId]?.[response.group] || []).map((state) => - state.id === stateId ? { ...state, ...response } : state - ), - }, - }; - }); - return response; } catch (error) { console.log("Failed to update state from project store"); runInAction(() => { - this.rootStore.project.states = originalStates; + this.states = originalStates; }); throw error; } }; deleteState = async (workspaceSlug: string, projectId: string, stateId: string) => { - const originalStates = this.rootStore.project.projectStates; + const originalStates = this.states; try { runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [originalStates?.[0]?.group || ""]: ( - this.rootStore.project.states?.[projectId]?.[originalStates?.[0]?.group || ""] || [] - ).filter((state) => state.id !== stateId), - }, + this.states = { + ...this.states, + [projectId]: [ + ...this.states?.[projectId].filter((state) => { + if (state.id !== stateId) { + return stateId; + } + }), + ], }; }); @@ -148,65 +184,41 @@ export class ProjectStateStore implements IProjectStateStore { console.log("Failed to delete state from project store"); // reverting back to original label list runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [originalStates?.[0]?.group || ""]: originalStates || [], - }, - }; + this.states = originalStates; }); + throw error; } }; markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => { - const states = this.rootStore.project.projectStates; - const currentDefaultState = states?.find((state) => state.default); - - let newStateList = - states?.map((state) => { - if (state.id === stateId) return { ...state, default: true }; - if (state.id === currentDefaultState?.id) return { ...state, default: false }; - return state; - }) ?? []; - newStateList = orderArrayBy(newStateList, "sequence", "ascending"); - - const newOrderedStateGroups = orderStateGroups(groupBy(newStateList, "group")); - const oldOrderedStateGroup = this.rootStore.project.states?.[projectId] || {}; // for reverting back to old state group if api fails - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: newOrderedStateGroups || {}, - }; - }); - - // updating using api + const originalStates = this.states; try { - this.stateService.patchState( - workspaceSlug, - projectId, - stateId, - { default: true }, - this.rootStore.user.currentUser! - ); + const currentDefaultStateIds = this.projectStates?.filter((s) => s.default).map((state) => state.id); - if (currentDefaultState) - this.stateService.patchState( - workspaceSlug, - projectId, - currentDefaultState.id, - { default: false }, - this.rootStore.user.currentUser! - ); - } catch (err) { - console.log("Failed to mark state as default"); runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: oldOrderedStateGroup, + this.states = { + ...this.states, + [projectId]: [ + ...this.states[projectId].map((state) => { + if (currentDefaultStateIds?.includes(state.id)) { + return { ...state, default: false }; + } else if (state.id === stateId) { + return { ...state, default: true }; + } + return state; + }), + ], }; }); + + // updating using api + await this.stateService.markDefault(workspaceSlug, projectId, stateId); + } catch (error) { + console.log("Failed to mark state as default"); + runInAction(() => { + this.states = originalStates; + }); + throw error; } }; @@ -218,40 +230,35 @@ export class ProjectStateStore implements IProjectStateStore { groupIndex: number ) => { const SEQUENCE_GAP = 15000; - let newSequence = SEQUENCE_GAP; + const originalStates = this.states; - const states = this.rootStore.project.projectStates || []; - const groupedStates = groupBy(states || [], "group"); - - const selectedState = states?.find((state) => state.id === stateId); - const groupStates = states?.filter((state) => state.group === selectedState?.group); - const groupLength = groupStates.length; - - if (direction === "up") { - if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; - else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; - } else { - if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; - else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; - } - - const newStateList = states?.map((state) => { - if (state.id === stateId) return { ...state, sequence: newSequence }; - return state; - }); - const newOrderedStateGroups = orderStateGroups( - groupBy(orderArrayBy(newStateList, "sequence", "ascending"), "group") - ); - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: newOrderedStateGroups || {}, - }; - }); - - // updating using api try { + let newSequence = SEQUENCE_GAP; + const states = this.projectStates || []; + const selectedState = states?.find((state) => state.id === stateId); + const groupStates = states?.filter((state) => state.group === selectedState?.group); + const groupLength = groupStates.length; + if (direction === "up") { + if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; + } else { + if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; + } + + const newStateList = states?.map((state) => { + if (state.id === stateId) return { ...state, sequence: newSequence }; + return state; + }); + + // updating using api + runInAction(() => { + this.states = { + ...this.states, + [projectId]: newStateList, + }; + }); + await this.stateService.patchState( workspaceSlug, projectId, @@ -263,10 +270,7 @@ export class ProjectStateStore implements IProjectStateStore { console.log("Failed to move state position"); // reverting back to old state group if api fails runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: groupedStates, - }; + this.states = originalStates; }); } }; diff --git a/web/store/root.ts b/web/store/root.ts index c6d781b28..655549025 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -19,7 +19,14 @@ import { IIssueQuickAddStore, IssueQuickAddStore, } from "store/issue"; -import { IWorkspaceFilterStore, IWorkspaceStore, WorkspaceFilterStore, WorkspaceStore } from "store/workspace"; +import { + IWorkspaceFilterStore, + IWorkspaceStore, + WorkspaceFilterStore, + WorkspaceStore, + WorkspaceMemberStore, + IWorkspaceMemberStore, +} from "store/workspace"; import { IProjectPublishStore, IProjectStore, @@ -113,6 +120,7 @@ export class RootStore { commandPalette: ICommandPaletteStore; workspace: IWorkspaceStore; workspaceFilter: IWorkspaceFilterStore; + workspaceMember: IWorkspaceMemberStore; projectPublish: IProjectPublishStore; project: IProjectStore; @@ -176,6 +184,7 @@ export class RootStore { this.workspace = new WorkspaceStore(this); this.workspaceFilter = new WorkspaceFilterStore(this); + this.workspaceMember = new WorkspaceMemberStore(this); this.project = new ProjectStore(this); this.projectState = new ProjectStateStore(this); diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 887bd0b0d..e9f90d979 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -1,2 +1,3 @@ export * from "./workspace_filters.store"; export * from "./workspace.store"; +export * from "./workspace-member.store"; diff --git a/web/store/workspace/workspace-member.store.ts b/web/store/workspace/workspace-member.store.ts new file mode 100644 index 000000000..010023736 --- /dev/null +++ b/web/store/workspace/workspace-member.store.ts @@ -0,0 +1,274 @@ +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { RootStore } from "../root"; +// types +import { IUser, IWorkspaceMember, IWorkspaceMemberInvitation, IWorkspaceBulkInviteFormData } from "types"; +// services +import { WorkspaceService } from "services/workspace.service"; + +export interface IWorkspaceMemberStore { + // states + loader: boolean; + error: any | null; + + // observables + members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[] + memberInvitations: { [workspaceSlug: string]: IWorkspaceMemberInvitation[] }; + // actions + fetchWorkspaceMembers: (workspaceSlug: string) => Promise; + fetchWorkspaceMemberInvitations: (workspaceSlug: string) => Promise; + updateMember: (workspaceSlug: string, memberId: string, data: Partial) => Promise; + removeMember: (workspaceSlug: string, memberId: string) => Promise; + inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise; + deleteWorkspaceInvitation: (workspaceSlug: string, memberId: string) => Promise; + // computed + workspaceMembers: IWorkspaceMember[] | null; + workspaceMemberInvitations: IWorkspaceMemberInvitation[] | null; + workspaceMembersWithInvitations: any[] | null; +} + +export class WorkspaceMemberStore implements IWorkspaceMemberStore { + // states + loader: boolean = false; + error: any | null = null; + // observables + members: { [workspaceSlug: string]: IWorkspaceMember[] } = {}; + memberInvitations: { [workspaceSlug: string]: IWorkspaceMemberInvitation[] } = {}; + // services + workspaceService; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + members: observable.ref, + memberInvitations: observable.ref, + // actions + fetchWorkspaceMembers: action, + fetchWorkspaceMemberInvitations: action, + updateMember: action, + removeMember: action, + inviteMembersToWorkspace: action, + deleteWorkspaceInvitation: action, + // computed + workspaceMembers: computed, + workspaceMemberInvitations: computed, + workspaceMembersWithInvitations: computed, + }); + + this.rootStore = _rootStore; + this.workspaceService = new WorkspaceService(); + } + + /** + * computed value of workspace members using the workspace slug from the store + */ + get workspaceMembers() { + if (!this.rootStore.workspace.workspaceSlug) return null; + const members = this.members?.[this.rootStore.workspace.workspaceSlug]; + if (!members) return null; + return members; + } + + /** + * Computed value of workspace member invitations using workspace slug from store + */ + get workspaceMemberInvitations() { + if (!this.rootStore.workspace.workspaceSlug) return null; + const invitations = this.memberInvitations?.[this.rootStore.workspace.workspaceSlug]; + if (!invitations) return null; + return invitations; + } + + /** + * computed value provides the members information including the invitations. + */ + get workspaceMembersWithInvitations() { + if (!this.workspaceMembers || !this.workspaceMemberInvitations) return null; + return [ + ...(this.workspaceMemberInvitations?.map((item) => ({ + id: item.id, + memberId: item.id, + avatar: "", + first_name: item.email, + last_name: "", + email: item.email, + display_name: item.email, + role: item.role, + status: item.accepted, + member: false, + accountCreated: item.accepted, + })) || []), + ...(this.workspaceMembers?.map((item) => ({ + id: item.id, + memberId: item.member?.id, + avatar: item.member?.avatar, + first_name: item.member?.first_name, + last_name: item.member?.last_name, + email: item.member?.email, + display_name: item.member?.display_name, + role: item.role, + status: true, + member: true, + accountCreated: true, + })) || []), + ]; + } + + /** + * fetch workspace members using workspace slug + * @param workspaceSlug + */ + fetchWorkspaceMembers = async (workspaceSlug: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const membersResponse = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug); + + runInAction(() => { + this.members = { + ...this.members, + [workspaceSlug]: membersResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + /** + * fetching workspace member invitations + * @param workspaceSlug + * @returns + */ + fetchWorkspaceMemberInvitations = async (workspaceSlug: string) => { + try { + const membersInvitations = await this.workspaceService.workspaceInvitations(workspaceSlug); + runInAction(() => { + this.memberInvitations = { + ...this.memberInvitations, + [workspaceSlug]: membersInvitations, + }; + }); + return membersInvitations; + } catch (error) { + throw error; + } + }; + + /** + * invite members to the workspace using emails + * @param workspaceSlug + * @param data + */ + inviteMembersToWorkspace = async (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => { + try { + await this.workspaceService.inviteWorkspace(workspaceSlug, data, this.rootStore.user.currentUser as IUser); + await this.fetchWorkspaceMemberInvitations(workspaceSlug); + } catch (error) { + throw error; + } + }; + + /** + * delete the workspace invitation + * @param workspaceSlug + * @param memberId + */ + deleteWorkspaceInvitation = async (workspaceSlug: string, memberId: string) => { + try { + runInAction(() => { + this.memberInvitations = { + ...this.memberInvitations, + [workspaceSlug]: [...this.memberInvitations[workspaceSlug].filter((inv) => inv.id !== memberId)], + }; + }); + await this.workspaceService.deleteWorkspaceInvitations(workspaceSlug.toString(), memberId); + } catch (error) { + throw error; + } + }; + + /** + * update workspace member using workspace slug and member id and data + * @param workspaceSlug + * @param memberId + * @param data + */ + updateMember = async (workspaceSlug: string, memberId: string, data: Partial) => { + const members = this.members?.[workspaceSlug]; + members?.map((m) => (m.id === memberId ? { ...m, ...data } : m)); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data); + + runInAction(() => { + this.loader = false; + this.error = null; + this.members = { + ...this.members, + [workspaceSlug]: members, + }; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * remove workspace member using workspace slug and member id + * @param workspaceSlug + * @param memberId + */ + removeMember = async (workspaceSlug: string, memberId: string) => { + const members = this.members?.[workspaceSlug]; + members?.filter((m) => m.id !== memberId); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId); + + runInAction(() => { + this.loader = false; + this.error = null; + this.members = { + ...this.members, + [workspaceSlug]: members, + }; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/workspace/workspace.store.ts b/web/store/workspace/workspace.store.ts index 6891da72c..1092ec33b 100644 --- a/web/store/workspace/workspace.store.ts +++ b/web/store/workspace/workspace.store.ts @@ -16,7 +16,6 @@ export interface IWorkspaceStore { workspaceSlug: string | null; workspaces: IWorkspace[] | undefined; labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[] - members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[] // actions setWorkspaceSlug: (workspaceSlug: string) => void; @@ -24,22 +23,16 @@ export interface IWorkspaceStore { getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabels | null; fetchWorkspaces: () => Promise; fetchWorkspaceLabels: (workspaceSlug: string) => Promise; - fetchWorkspaceMembers: (workspaceSlug: string) => Promise; // workspace write operations createWorkspace: (data: Partial) => Promise; updateWorkspace: (workspaceSlug: string, data: Partial) => Promise; deleteWorkspace: (workspaceSlug: string) => Promise; - // members write operations - updateMember: (workspaceSlug: string, memberId: string, data: Partial) => Promise; - removeMember: (workspaceSlug: string, memberId: string) => Promise; - // computed currentWorkspace: IWorkspace | null; workspacesCreateByCurrentUser: IWorkspace[] | null; workspaceLabels: IIssueLabels[] | null; - workspaceMembers: IWorkspaceMember[] | null; } export class WorkspaceStore implements IWorkspaceStore { @@ -72,7 +65,6 @@ export class WorkspaceStore implements IWorkspaceStore { workspaceSlug: observable.ref, workspaces: observable.ref, labels: observable.ref, - members: observable.ref, // actions setWorkspaceSlug: action, @@ -80,21 +72,15 @@ export class WorkspaceStore implements IWorkspaceStore { getWorkspaceLabelById: action, fetchWorkspaces: action, fetchWorkspaceLabels: action, - fetchWorkspaceMembers: action, // workspace write operations createWorkspace: action, updateWorkspace: action, deleteWorkspace: action, - // members write operations - updateMember: action, - removeMember: action, - // computed currentWorkspace: computed, workspaceLabels: computed, - workspaceMembers: computed, }); this.rootStore = _rootStore; @@ -135,15 +121,6 @@ export class WorkspaceStore implements IWorkspaceStore { return _labels && Object.keys(_labels).length > 0 ? _labels : []; } - /** - * computed value of workspace members using the workspace slug from the store - */ - get workspaceMembers() { - if (!this.workspaceSlug) return []; - const _members = this.members?.[this.workspaceSlug]; - return _members && Object.keys(_members).length > 0 ? _members : []; - } - /** * set workspace slug in the store * @param workspaceSlug @@ -224,35 +201,6 @@ export class WorkspaceStore implements IWorkspaceStore { } }; - /** - * fetch workspace members using workspace slug - * @param workspaceSlug - */ - fetchWorkspaceMembers = async (workspaceSlug: string) => { - try { - runInAction(() => { - this.loader = true; - this.error = null; - }); - - const membersResponse = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug); - - runInAction(() => { - this.members = { - ...this.members, - [workspaceSlug]: membersResponse, - }; - this.loader = false; - this.error = null; - }); - } catch (error) { - runInAction(() => { - this.loader = false; - this.error = error; - }); - } - }; - /** * create workspace using the workspace data * @param data @@ -351,75 +299,4 @@ export class WorkspaceStore implements IWorkspaceStore { throw error; } }; - - /** - * update workspace member using workspace slug and member id and data - * @param workspaceSlug - * @param memberId - * @param data - */ - updateMember = async (workspaceSlug: string, memberId: string, data: Partial) => { - const members = this.members?.[workspaceSlug]; - members?.map((m) => (m.id === memberId ? { ...m, ...data } : m)); - - try { - runInAction(() => { - this.loader = true; - this.error = null; - }); - - await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data); - - runInAction(() => { - this.loader = false; - this.error = null; - this.members = { - ...this.members, - [workspaceSlug]: members, - }; - }); - } catch (error) { - runInAction(() => { - this.loader = false; - this.error = error; - }); - - throw error; - } - }; - - /** - * remove workspace member using workspace slug and member id - * @param workspaceSlug - * @param memberId - */ - removeMember = async (workspaceSlug: string, memberId: string) => { - const members = this.members?.[workspaceSlug]; - members?.filter((m) => m.id !== memberId); - - try { - runInAction(() => { - this.loader = true; - this.error = null; - }); - - await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId); - - runInAction(() => { - this.loader = false; - this.error = null; - this.members = { - ...this.members, - [workspaceSlug]: members, - }; - }); - } catch (error) { - runInAction(() => { - this.loader = false; - this.error = error; - }); - - throw error; - } - }; } diff --git a/web/styles/table.css b/web/styles/table.css index ad88fd10e..bce7e4683 100644 --- a/web/styles/table.css +++ b/web/styles/table.css @@ -92,7 +92,7 @@ transform: translateY(-50%); } -.tableWrapper .tableControls .columnsControl > button { +.tableWrapper .tableControls .columnsControl .columnsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); width: 30px; @@ -104,14 +104,14 @@ transform: translateX(-50%); } -.tableWrapper .tableControls .rowsControl > button { +.tableWrapper .tableControls .rowsControl .rowsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); height: 30px; width: 15px; } -.tableWrapper .tableControls button { +.tableWrapper .tableControls .rowsControlDiv { background-color: rgba(var(--color-primary-100)); border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; @@ -124,6 +124,18 @@ cursor: pointer; } +.tableWrapper .tableControls .columnsControlDiv { + background-color: rgba(var(--color-primary-100)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 2px; + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: center; + transition: transform ease-out 100ms, background-color ease-out 100ms; + outline: none; + box-shadow: #000 0px 2px 4px; + cursor: pointer; +} .tableWrapper .tableControls .tableToolbox, .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300));