mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 21:40:18 +02:00
fix: sticky build (#2219)
This commit is contained in:
@@ -1,2 +1 @@
|
||||
export * from "./sticky-editor";
|
||||
export * from "ce/components/editor";
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { TSticky } from "@plane/types";
|
||||
|
||||
export const STICKY_COLORS = [
|
||||
"#D4DEF7", // light periwinkle
|
||||
"#B4E4FF", // light blue
|
||||
"#FFF2B4", // light yellow
|
||||
"#E3E3E3", // light gray
|
||||
"#FFE2DD", // light pink
|
||||
"#F5D1A5", // light orange
|
||||
"#D1F7C4", // light green
|
||||
"#E5D4FF", // light purple
|
||||
];
|
||||
|
||||
type TProps = {
|
||||
handleUpdate: (data: Partial<TSticky>) => Promise<void>;
|
||||
};
|
||||
|
||||
export const ColorPalette = (props: TProps) => {
|
||||
const { handleUpdate } = props;
|
||||
return (
|
||||
<div className="absolute z-10 bottom-5 left-0 w-56 shadow p-2 rounded-md bg-custom-background-100 mb-2">
|
||||
<div className="text-sm font-semibold text-custom-text-400 mb-2">Background colors</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{STICKY_COLORS.map((color, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
onClick={() => handleUpdate({ color })}
|
||||
className="h-6 w-6 rounded-md hover:ring-2 hover:ring-custom-primary focus:outline-none focus:ring-2 focus:ring-custom-primary transition-all"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
// plane constants
|
||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
// components
|
||||
import { TSticky } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { Toolbar } from "./toolbar";
|
||||
|
||||
interface StickyEditorWrapperProps
|
||||
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
projectId?: string;
|
||||
accessSpecifier?: EIssueCommentAccessSpecifier;
|
||||
handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void;
|
||||
showAccessSpecifier?: boolean;
|
||||
showSubmitButton?: boolean;
|
||||
isSubmitting?: boolean;
|
||||
showToolbarInitially?: boolean;
|
||||
showToolbar?: boolean;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
parentClassName?: string;
|
||||
handleColorChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
handleDelete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperProps>((props, ref) => {
|
||||
const {
|
||||
containerClassName,
|
||||
workspaceSlug,
|
||||
workspaceId,
|
||||
projectId,
|
||||
handleDelete,
|
||||
handleColorChange,
|
||||
showToolbarInitially = true,
|
||||
showToolbar = true,
|
||||
parentClassName = "",
|
||||
placeholder = "Add comment...",
|
||||
uploadFile,
|
||||
...rest
|
||||
} = props;
|
||||
// states
|
||||
const [isFocused, setIsFocused] = useState(showToolbarInitially);
|
||||
// editor flaggings
|
||||
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||
return !!ref && typeof ref === "object" && "current" in ref;
|
||||
}
|
||||
// derived values
|
||||
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative border border-custom-border-200 rounded p-3", parentClassName)}
|
||||
onFocus={() => !showToolbarInitially && setIsFocused(true)}
|
||||
onBlur={() => !showToolbarInitially && setIsFocused(false)}
|
||||
>
|
||||
<LiteTextEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[...disabledExtensions, "enter-key"]}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
renderComponent: () => <></>,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
containerClassName={cn(containerClassName, "relative")}
|
||||
{...rest}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-300 ease-out origin-top",
|
||||
isFocused ? "max-h-[200px] opacity-100 scale-y-100 mt-3" : "max-h-0 opacity-0 scale-y-0 invisible"
|
||||
)}
|
||||
>
|
||||
<Toolbar
|
||||
executeCommand={(item) => {
|
||||
// TODO: update this while toolbar homogenization
|
||||
// @ts-expect-error type mismatch here
|
||||
editorRef?.executeMenuItemCommand({
|
||||
itemKey: item.itemKey,
|
||||
...item.extraProps,
|
||||
});
|
||||
}}
|
||||
handleDelete={handleDelete}
|
||||
handleColorChange={handleColorChange}
|
||||
editorRef={editorRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
StickyEditor.displayName = "StickyEditor";
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./editor";
|
||||
export * from "./toolbar";
|
||||
@@ -1,115 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { Palette, Trash2 } from "lucide-react";
|
||||
// editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// ui
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { TSticky } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { ColorPalette } from "./color-pallete";
|
||||
|
||||
type Props = {
|
||||
executeCommand: (item: ToolbarMenuItem) => void;
|
||||
editorRef: EditorRefApi | null;
|
||||
handleColorChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
handleDelete: () => void;
|
||||
};
|
||||
|
||||
const toolbarItems = TOOLBAR_ITEMS.sticky;
|
||||
|
||||
export const Toolbar: React.FC<Props> = (props) => {
|
||||
const { executeCommand, editorRef, handleColorChange, handleDelete } = props;
|
||||
|
||||
// State to manage active states of toolbar items
|
||||
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
|
||||
const [showColorPalette, setShowColorPalette] = useState(false);
|
||||
const colorPaletteRef = React.useRef<HTMLDivElement>(null);
|
||||
// Function to update active states
|
||||
const updateActiveStates = useCallback(() => {
|
||||
if (!editorRef) return;
|
||||
const newActiveStates: Record<string, boolean> = {};
|
||||
Object.values(toolbarItems)
|
||||
.flat()
|
||||
.forEach((item) => {
|
||||
// TODO: update this while toolbar homogenization
|
||||
// @ts-expect-error type mismatch here
|
||||
newActiveStates[item.renderKey] = editorRef.isMenuItemActive({
|
||||
itemKey: item.itemKey,
|
||||
...item.extraProps,
|
||||
});
|
||||
});
|
||||
setActiveStates(newActiveStates);
|
||||
}, [editorRef]);
|
||||
|
||||
// useEffect to call updateActiveStates when isActive prop changes
|
||||
useEffect(() => {
|
||||
if (!editorRef) return;
|
||||
const unsubscribe = editorRef.onStateChange(updateActiveStates);
|
||||
updateActiveStates();
|
||||
return () => unsubscribe();
|
||||
}, [editorRef, updateActiveStates]);
|
||||
|
||||
useOutsideClickDetector(colorPaletteRef, () => setShowColorPalette(false));
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-between mt-2 h-full">
|
||||
<div className="flex my-auto gap-4" ref={colorPaletteRef}>
|
||||
{/* color palette */}
|
||||
{showColorPalette && <ColorPalette handleUpdate={handleColorChange} />}
|
||||
<button onClick={() => setShowColorPalette(!showColorPalette)} className="flex text-custom-text-300">
|
||||
<Palette className="size-4 my-auto" />
|
||||
</button>
|
||||
|
||||
<div className="flex w-fit items-stretch justify-between gap-4 rounded p-1 my-auto">
|
||||
<div className="flex items-stretch my-auto gap-4">
|
||||
{Object.keys(toolbarItems).map((key) => (
|
||||
<div key={key} className={cn("flex items-stretch gap-4", {})}>
|
||||
{toolbarItems[key].map((item) => {
|
||||
const isItemActive = activeStates[item.renderKey];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={item.renderKey}
|
||||
tooltipContent={
|
||||
<p className="flex flex-col gap-1 text-center text-xs">
|
||||
<span className="font-medium">{item.name}</span>
|
||||
{item.shortcut && <kbd className="text-custom-text-400">{item.shortcut.join(" + ")}</kbd>}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => executeCommand(item)}
|
||||
className={cn(
|
||||
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-300",
|
||||
{}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={cn("h-3.5 w-3.5", {
|
||||
"font-extrabold": isItemActive,
|
||||
})}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* delete action */}
|
||||
<button onClick={handleDelete} className="my-auto text-custom-text-300">
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
// context
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { IStickyStore } from "@/plane-web/store/sticky/sticky.store";
|
||||
// plane web stores
|
||||
|
||||
export const useSticky = (): IStickyStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useSticky must be used within StoreProvider");
|
||||
return context.stickyStore;
|
||||
};
|
||||
@@ -67,7 +67,6 @@ import {
|
||||
import { IPiChatStore, PiChatStore } from "./pi-chat/pi-chat";
|
||||
// timeline
|
||||
import { IProjectStore, ProjectStore } from "./projects/projects";
|
||||
import { IStickyStore, StickyStore } from "./sticky/sticky.store";
|
||||
import { ITimelineStore } from "./timeline";
|
||||
|
||||
export class RootStore extends CoreRootStore {
|
||||
@@ -101,7 +100,6 @@ export class RootStore extends CoreRootStore {
|
||||
gitlabIntegration: IGitlabStore;
|
||||
initiativeFilterStore: IInitiativeFilterStore;
|
||||
initiativeStore: IInitiativeStore;
|
||||
stickyStore: IStickyStore;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -123,7 +121,6 @@ export class RootStore extends CoreRootStore {
|
||||
this.projectDetails = new ProjectStore(this);
|
||||
this.teamRoot = new TeamRootStore(this);
|
||||
this.workspaceNotification = new WorkspaceNotificationStore(this);
|
||||
this.stickyStore = new StickyStore();
|
||||
// importers
|
||||
this.jiraImporter = new JiraStore(this);
|
||||
this.jiraServerImporter = new JiraServerStore(this);
|
||||
@@ -157,7 +154,6 @@ export class RootStore extends CoreRootStore {
|
||||
this.timelineStore = new TimeLineStore(this);
|
||||
this.projectDetails = new ProjectStore(this);
|
||||
this.teamRoot = new TeamRootStore(this);
|
||||
this.stickyStore = new StickyStore();
|
||||
// importers
|
||||
this.jiraImporter = new JiraStore(this);
|
||||
this.jiraServerImporter = new JiraServerStore(this);
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { v4 } from "uuid";
|
||||
import { TSticky } from "@plane/types";
|
||||
import { StickyService } from "@/plane-web/services/sticky.service";
|
||||
export interface IStickyStore {
|
||||
creatingSticky: boolean;
|
||||
fetchingWorkspaceStickies: boolean;
|
||||
workspaceStickies: Record<string, string[]>; // workspaceId -> stickyIds
|
||||
stickies: Record<string, TSticky>; // stickyId -> sticky
|
||||
searchQuery: string;
|
||||
activeStickyId: string | undefined;
|
||||
recentStickyId: string | undefined;
|
||||
showAddNewSticky: boolean;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
|
||||
// computed
|
||||
getWorkspaceStickies: (workspaceSlug: string) => string[];
|
||||
|
||||
// actions
|
||||
toggleShowNewSticky: (value: boolean) => void;
|
||||
updateSearchQuery: (query: string) => void;
|
||||
fetchWorkspaceStickies: (workspaceSlug: string, cursor?: string, per_page?: number) => void;
|
||||
createSticky: (workspaceSlug: string, sticky: Partial<TSticky>) => void;
|
||||
updateSticky: (workspaceSlug: string, id: string, updates: Partial<TSticky>) => void;
|
||||
deleteSticky: (workspaceSlug: string, id: string) => void;
|
||||
updateActiveStickyId: (id: string | undefined) => void;
|
||||
fetchRecentSticky: (workspaceSlug: string) => void;
|
||||
incrementPage: () => void;
|
||||
}
|
||||
|
||||
export class StickyStore implements IStickyStore {
|
||||
creatingSticky = false;
|
||||
fetchingWorkspaceStickies = true;
|
||||
workspaceStickies: Record<string, string[]> = {};
|
||||
stickies: Record<string, TSticky> = {};
|
||||
recentStickyId: string | undefined = undefined;
|
||||
searchQuery = "";
|
||||
activeStickyId: string | undefined = undefined;
|
||||
showAddNewSticky = false;
|
||||
currentPage = 0;
|
||||
totalPages = 0;
|
||||
|
||||
// services
|
||||
stickyService;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
creatingSticky: observable,
|
||||
fetchingWorkspaceStickies: observable,
|
||||
activeStickyId: observable,
|
||||
showAddNewSticky: observable,
|
||||
recentStickyId: observable,
|
||||
workspaceStickies: observable,
|
||||
stickies: observable,
|
||||
searchQuery: observable,
|
||||
currentPage: observable,
|
||||
totalPages: observable,
|
||||
// actions
|
||||
updateSearchQuery: action,
|
||||
updateSticky: action,
|
||||
deleteSticky: action,
|
||||
incrementPage: action,
|
||||
});
|
||||
this.stickyService = new StickyService();
|
||||
}
|
||||
|
||||
getWorkspaceStickies = computedFn((workspaceSlug: string) => {
|
||||
let filteredStickies = (this.workspaceStickies[workspaceSlug] || []).map((stickyId) => this.stickies[stickyId]);
|
||||
if (this.searchQuery) {
|
||||
filteredStickies = filteredStickies.filter(
|
||||
(sticky) => sticky.name && sticky.name.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
return filteredStickies.map((sticky) => sticky.id);
|
||||
});
|
||||
|
||||
toggleShowNewSticky = (value: boolean) => {
|
||||
this.showAddNewSticky = value;
|
||||
};
|
||||
|
||||
updateSearchQuery = (query: string) => {
|
||||
this.searchQuery = query;
|
||||
};
|
||||
|
||||
updateActiveStickyId = (id: string | undefined) => {
|
||||
this.activeStickyId = id;
|
||||
};
|
||||
|
||||
incrementPage = () => {
|
||||
this.currentPage += 1;
|
||||
};
|
||||
|
||||
fetchRecentSticky = async (workspaceSlug: string) => {
|
||||
const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0", 1);
|
||||
runInAction(() => {
|
||||
this.recentStickyId = response.results[0]?.id;
|
||||
this.stickies[response.results[0]?.id] = response.results[0];
|
||||
});
|
||||
};
|
||||
fetchWorkspaceStickies = async (workspaceSlug: string, cursor?: string, per_page?: number) => {
|
||||
this.fetchingWorkspaceStickies = true;
|
||||
const response = await this.stickyService.getStickies(workspaceSlug, cursor, per_page);
|
||||
|
||||
runInAction(() => {
|
||||
response.results.forEach((sticky) => {
|
||||
if (!this.workspaceStickies[workspaceSlug]?.includes(sticky.id)) {
|
||||
this.workspaceStickies[workspaceSlug] = [...(this.workspaceStickies[workspaceSlug] || []), sticky.id];
|
||||
}
|
||||
this.stickies[sticky.id] = sticky;
|
||||
});
|
||||
this.totalPages = response.total_pages;
|
||||
this.fetchingWorkspaceStickies = false;
|
||||
});
|
||||
};
|
||||
|
||||
createSticky = async (workspaceSlug: string, sticky: Partial<TSticky>) => {
|
||||
if (!this.showAddNewSticky) return;
|
||||
this.showAddNewSticky = false;
|
||||
this.creatingSticky = true;
|
||||
const workspaceStickies = this.workspaceStickies[workspaceSlug] || [];
|
||||
const response = await this.stickyService.createSticky(workspaceSlug, sticky);
|
||||
runInAction(() => {
|
||||
this.stickies[response.id] = response;
|
||||
this.workspaceStickies[workspaceSlug] = [response.id, ...workspaceStickies];
|
||||
this.activeStickyId = response.id;
|
||||
this.recentStickyId = response.id;
|
||||
this.creatingSticky = false;
|
||||
});
|
||||
};
|
||||
|
||||
updateSticky = async (workspaceSlug: string, id: string, updates: Partial<TSticky>) => {
|
||||
const sticky = this.stickies[id];
|
||||
if (!sticky) return;
|
||||
try {
|
||||
this.stickies[id] = {
|
||||
...sticky,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.recentStickyId = id;
|
||||
await this.stickyService.updateSticky(workspaceSlug, id, updates);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this.stickies[id] = sticky;
|
||||
}
|
||||
};
|
||||
|
||||
deleteSticky = async (workspaceSlug: string, id: string) => {
|
||||
const sticky = this.stickies[id];
|
||||
if (!sticky) return;
|
||||
try {
|
||||
this.workspaceStickies[workspaceSlug] = this.workspaceStickies[workspaceSlug].filter(
|
||||
(stickyId) => stickyId !== id
|
||||
);
|
||||
if (this.activeStickyId === id) this.activeStickyId = undefined;
|
||||
delete this.stickies[id];
|
||||
this.recentStickyId = this.workspaceStickies[workspaceSlug][0];
|
||||
await this.stickyService.deleteSticky(workspaceSlug, id);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this.stickies[id] = sticky;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user