Compare commits

...
Author SHA1 Message Date
sriram veeraghantaandGitHub 6f397710ce Merge pull request #1951 from makeplane/stage-release
Promote: Stage release to Production
2023-08-23 16:18:08 +05:30
sriram veeraghantaandGitHub e6bd6b6a8c Merge pull request #1950 from makeplane/develop
Promote: Develop to Stage Release
2023-08-23 15:49:14 +05:30
2d1406953e fix: mutate projects list after joining (#1944)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 15:44:27 +05:30
a8fdd42cb9 fix: label color select popover overflow (#1949)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 15:43:09 +05:30
sriram veeraghantaandGitHub 9d3952006b Merge pull request #1948 from makeplane/stage-release
Promot: Stage Release to Production
2023-08-23 14:51:27 +05:30
sriram veeraghantaandGitHub b75473a684 Merge pull request #1947 from makeplane/develop
Promote: Develop to Stage Release
2023-08-23 12:37:25 +05:30
561fb9815b chore: update export services icons (#1946)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 12:29:59 +05:30
Bavisetti NarayanandGitHub 2cc67f6498 fix: date validation in cycle and module (#1945) 2023-08-23 12:17:20 +05:30
NikhilandGitHub 2f5bd58c61 Merge pull request #1941 from makeplane/stage-release
promote: stage-release to master
2023-08-22 19:37:49 +05:30
NikhilandGitHub e833fccf61 Merge pull request #1940 from makeplane/develop
dev: deploy docker containers (#1939)
2023-08-22 19:34:13 +05:30
NikhilandGitHub eee6658cc2 dev: deploy docker containers (#1939) 2023-08-22 19:33:29 +05:30
NikhilandGitHub 62ba9abdb4 Merge pull request #1936 from makeplane/stage-release
promote: stage-release to master
2023-08-22 13:57:57 +05:30
NikhilandGitHub 46b138eb0b Merge pull request #1934 from makeplane/develop
fix: aws region changed for exporter (#1933)
2023-08-22 13:51:20 +05:30
68b438ab1a fix: aws region changed for exporter (#1933)
Co-authored-by: Bavisetti Narayan <narayan@Bavisettis-MacBook-Pro.local>
2023-08-22 13:18:15 +05:30
NikhilandGitHub 59bdf222f5 Merge pull request #1932 from makeplane/stage-release
promote: stage release to master
2023-08-22 01:34:37 +05:30
NikhilandGitHub eb50ade5e3 Merge pull request #1931 from makeplane/develop
fix: access environment variables is changed in services (#1930)
2023-08-22 01:14:56 +05:30
b406a70e72 fix: access environment variables is changed in services (#1930)
Co-authored-by: Sainath <sainath@Sainaths-MacBook-Pro.local>
2023-08-22 01:13:51 +05:30
Vamsi KuramaandGitHub 85a08e4abd Merge pull request #1929 from makeplane/stage-release 2023-08-22 00:17:29 +05:30
NikhilandGitHub aa2e1697b0 Merge pull request #1928 from makeplane/develop
promote: develop to stage-release
2023-08-21 20:51:46 +05:30
Aaryan KhandelwalandGitHub b02417120b chore: hide new issue button from my subscribed issues page (#1927) 2023-08-21 20:50:17 +05:30
Aaryan KhandelwalandGitHub d040394826 fix: create project button not appearing on the sidebar (#1926) 2023-08-21 20:44:51 +05:30
NikhilandGitHub f7682c57ba fix: plane space start up command (#1925) 2023-08-21 20:25:12 +05:30
NikhilandGitHub 3beab9de6f Merge pull request #1923 from makeplane/develop
promote: develop to stage-release
2023-08-21 18:18:14 +05:30
guru_sainathandGitHub 9bb6254515 chore: updated default api base_url (#1922) 2023-08-21 18:17:32 +05:30
Aaryan KhandelwalandGitHub ae052f1890 chore: update restricted workspace slugs (#1920) 2023-08-21 18:13:08 +05:30
cfc7049343 Dockerrizing space project (#1921)
* chore: Added Dockerfile for Space Project

* fix: next js config to standalone mode

* fix: workedaround build error with rename 404 page

* chore: modified dockerfile with new conventions

* chore: modified dockercompose file for new plane-deploy

* fix: handled ts errors with possibly undefined states

* chore: updated main dockerfile with plane-deploy

* feat: included space project to start.sh

* chore: modified space project port while running in production

* chore: restored changes inside space project

* chore: added ngnix config for space project running :4000

* fix: Updated docker-compose files

* chore: added space url for ngnix config

* chore: Updated ngnix template

* chore: updated space url in compose hub file

* dev: updated dockerfile.space and start and replace script

* dev: equate hub and build docker files

* dev: revert workspace space page

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-08-21 18:12:41 +05:30
guru_sainathandGitHub 41e55dff85 fix: build error for 404 and search params null check (#1919) 2023-08-21 12:43:41 +05:30
Aaryan KhandelwalandGitHub 0bccb63a9f fix: module start and target date validations (#1914) 2023-08-21 11:46:02 +05:30
Aaryan KhandelwalandGitHub 2eb956e97e refactor: project and workspace delete modals (#1915) 2023-08-21 11:44:59 +05:30
M. PalanikannanandGitHub d470adf262 fix: Image resize, Link selector in Modals, Delete/ sync images and so much more (#1896)
* added image-resizing support

* link form removed

* updated image upload logic and 35% default width on upload

* removed shadow, added alert if not saved and ts errors

* prevent enter key on Link selector to trigger modal submit

* added workspace slug to all tiptap instances

* added delete plugin with loading indicator

* added better syncing of "Saved" state of editor and Image uploads

* removed redundant description_html check
2023-08-19 18:58:54 +05:30
Aaryan KhandelwalandGitHub cebc8bdc8d fix: context menu dynamic positioning, multiple context menus opening issue (#1913)
* fix: context menu positioning

* fix: close already opened context menu on outside click
2023-08-19 18:50:12 +05:30
Anmol Singh BhatiaandGitHub 64b5ba196f style: responsive empty state for profile stats (#1911) 2023-08-18 20:18:03 +05:30
NikhilandGitHub 13d21e752d Merge pull request #1910 from makeplane/develop
promote: develop to stage-release
2023-08-18 19:29:11 +05:30
Anmol Singh BhatiaandGitHub 8d5018318d chore: add favorite project from sidebar (#1909) 2023-08-18 18:42:50 +05:30
Anmol Singh BhatiaandGitHub 0fbdc0b157 style: consistent ui for create update issue modal (#1907) 2023-08-18 18:42:04 +05:30
Bavisetti NarayanandGitHub 2f39181eb7 fix: priority ordering (#1908) 2023-08-18 18:27:29 +05:30
Dakshesh JainandGitHub d825dc5579 style: showing first name for bot profile (#1894) 2023-08-18 17:15:20 +05:30
Aaryan KhandelwalandGitHub 1f8117c987 fix: dashboard upcoming issues list (#1904) 2023-08-18 17:10:12 +05:30
Vamsi KuramaandGitHub 9ff8994c0e Merge pull request #1784 from makeplane/stage-release
promote: stage-release to master v0.10.1-patch
2023-08-03 18:55:24 +05:30
Vamsi KuramaandGitHub 3488001197 Merge pull request #1762 from makeplane/stage-release
promote: stage-release to master
2023-08-01 22:38:20 +05:30
Vamsi KuramaandGitHub 2ced7e4911 Merge pull request #1758 from makeplane/stage-release
promote: stage-release to master v0.10
2023-08-01 20:04:40 +05:30
Aaryan KhandelwalandGitHub e5a3bec28c Merge pull request #1709 from makeplane/stage-release
promote: stage-release to master
2023-07-28 19:47:04 +05:30
Vamsi KuramaandGitHub e930f8cc7b Merge pull request #1697 from makeplane/stage-release
promote: stage-release to master
2023-07-28 00:55:32 +05:30
83 changed files with 1049 additions and 494 deletions
+10 -3
View File
@@ -5,9 +5,11 @@ WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
RUN apk add tree
COPY . .
RUN turbo prune --scope=app --docker
RUN turbo prune --scope=app --scope=plane-deploy --docker
CMD tree -I node_modules/
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
@@ -21,14 +23,14 @@ COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
# # Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
RUN yarn turbo run build
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
@@ -96,11 +98,16 @@ RUN adduser --system --uid 1001 captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
COPY --from=installer /app/apps/space/next.config.js .
COPY --from=installer /app/apps/space/package.json .
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf
+10
View File
@@ -14,6 +14,11 @@ from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleWriteSerializer(BaseSerializer):
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
class Meta:
model = Cycle
fields = "__all__"
@@ -35,6 +40,11 @@ class CycleSerializer(BaseSerializer):
started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
def get_assignees(self, obj):
members = [
@@ -40,6 +40,11 @@ class ModuleWriteSerializer(BaseSerializer):
"updated_at",
]
def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
members = validated_data.pop("members_list", None)
+1
View File
@@ -1083,6 +1083,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.filter(**filters)
.values("priority")
.annotate(priority_count=Count("priority"))
.filter(priority_count__gte=1)
.annotate(
priority_order=Case(
*[
+1 -1
View File
@@ -68,7 +68,7 @@ def create_zip_file(files):
def upload_to_s3(zip_file, workspace_id, token_id, slug):
s3 = boto3.client(
"s3",
region_name="ap-south-1",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
+2 -2
View File
@@ -33,8 +33,8 @@ RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL}
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app
FROM node:18-alpine AS runner
WORKDIR /app
@@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC<Props> = ({
return (
<div
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
isOpen ? "block" : "hidden"
}`}
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
}`}
>
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="text-sm">
Content:
<TiptapEditor
workspaceSlug={workspaceSlug as string}
value={htmlContent ?? `<p>${content}</p>`}
customClassName="-m-3"
noBorder
@@ -161,6 +161,7 @@ export const GptAssistantModal: React.FC<Props> = ({
<div className="page-block-section text-sm">
Response:
<Tiptap
workspaceSlug={workspaceSlug as string}
value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3"
noBorder
@@ -179,11 +180,10 @@ export const GptAssistantModal: React.FC<Props> = ({
type="text"
name="task"
register={register}
placeholder={`${
content && content !== ""
placeholder={`${content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
}`}
autoComplete="off"
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
@@ -219,8 +219,8 @@ export const GptAssistantModal: React.FC<Props> = ({
{isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again"}
? "Generate response"
: "Generate again"}
</PrimaryButton>
</div>
</div>
@@ -86,7 +86,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const [isMenuActive, setIsMenuActive] = useState(false);
const [isDropdownActive, setIsDropdownActive] = useState(false);
@@ -201,7 +202,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
return (
<>
<ContextMenu
position={contextMenuPosition}
clickEvent={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
@@ -243,7 +244,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
setContextMenuPosition(e);
}}
>
<div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]">
@@ -33,7 +33,7 @@ import {
} from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
@@ -71,7 +71,7 @@ export const SingleListIssue: React.FC<Props> = ({
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -167,7 +167,7 @@ export const SingleListIssue: React.FC<Props> = ({
return (
<>
<ContextMenu
position={contextMenuPosition}
clickEvent={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
@@ -199,7 +199,7 @@ export const SingleListIssue: React.FC<Props> = ({
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
setContextMenuPosition(e);
}}
>
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
@@ -293,6 +293,7 @@ export const InboxMainContent: React.FC = () => {
</div>
<div>
<IssueDescriptionForm
workspaceSlug={workspaceSlug as string}
issue={{
name: issueDetails.name,
description: issueDetails.description,
+7 -2
View File
@@ -134,7 +134,9 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.display_name.charAt(0)}
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name.charAt(0)
: activityItem.actor_detail.display_name.charAt(0)}
</div>
)}
</div>
@@ -153,7 +155,9 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
) : (
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
<a className="text-gray font-medium">
{activityItem.actor_detail.display_name}
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name
: activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}
@@ -171,6 +175,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
return (
<div key={activityItem.id} className="mt-4">
<CommentCard
workspaceSlug={workspaceSlug as string}
comment={activityItem as IIssueComment}
onSubmit={handleCommentUpdate}
handleCommentDeletion={handleCommentDelete}
@@ -93,11 +93,12 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
control={control}
render={({ field: { value, onChange } }) => (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("comment_html")
: value
}
@@ -22,12 +22,13 @@ const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEdit
TiptapEditor.displayName = "TiptapEditor";
type Props = {
workspaceSlug: string;
comment: IIssueComment;
onSubmit: (comment: IIssueComment) => void;
handleCommentDeletion: (comment: string) => void;
};
export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit, handleCommentDeletion }) => {
const { user } = useUser();
const editorRef = React.useRef<any>(null);
@@ -65,7 +66,11 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
<img
src={comment.actor_detail.avatar}
alt={comment.actor_detail.display_name}
alt={
comment.actor_detail.is_bot
? comment.actor_detail.first_name + " Bot"
: comment.actor_detail.display_name
}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
@@ -74,7 +79,9 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{comment.actor_detail.display_name.charAt(0)}
{comment.actor_detail.is_bot
? comment.actor_detail.first_name.charAt(0)
: comment.actor_detail.display_name.charAt(0)}
</div>
)}
@@ -103,6 +110,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
>
<div>
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={watch("comment_html")}
debouncedUpdatesEnabled={false}
@@ -132,6 +140,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
</form>
<div className={`${isEditing ? "hidden" : ""}`}>
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={showEditorRef}
value={comment.comment_html}
editable={false}
@@ -24,6 +24,7 @@ export interface IssueDetailsProps {
description: string;
description_html: string;
};
workspaceSlug: string;
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
isAllowed: boolean;
}
@@ -31,6 +32,7 @@ export interface IssueDetailsProps {
export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
issue,
handleFormSubmit,
workspaceSlug,
isAllowed,
}) => {
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
@@ -69,11 +71,15 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
useEffect(() => {
if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => {
setIsSubmitting("saved");
}, 2000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
}
}, [isSubmitting]);
}, [isSubmitting, setShowAlert]);
// reset form values
useEffect(() => {
@@ -112,9 +118,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
{characterLimit && (
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
<span
className={`${
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
}`}
className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
}`}
>
{watch("name").length}
</span>
@@ -134,16 +139,19 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
<Tiptap
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
workspaceSlug={workspaceSlug}
debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting}
customClassName="min-h-[150px] shadow-sm"
editorContentCustomClassNames="pb-9"
onChange={(description: Object, description_html: string) => {
setShowAlert(true);
setIsSubmitting("submitting");
onChange(description_html);
setValue("description", description);
@@ -156,9 +164,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
}}
/>
<div
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
}`}
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === "saved" ? "fadeOut" : "fadeIn"
}`}
>
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
</div>
+1
View File
@@ -370,6 +370,7 @@ export const IssueForm: FC<IssueFormProps> = ({
return (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
@@ -124,6 +124,7 @@ export const IssueMainContent: React.FC<Props> = ({
</div>
) : null}
<IssueDescriptionForm
workspaceSlug={workspaceSlug as string}
issue={issueDetails}
handleFormSubmit={submitChanges}
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}
@@ -270,16 +270,18 @@ export const MyIssuesView: React.FC<Props> = ({
? "You have not created any issue yet."
: "You have not subscribed to any issue yet.",
description: "Keep track of your work in a single place.",
primaryButton: {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
},
primaryButton: filters.subscriber
? undefined
: {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
},
}}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
@@ -49,7 +49,7 @@ export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], on
<AssigneesList userIds={value} length={3} showLength={true} />
</div>
) : (
<div className="flex items-center justify-center gap-2 px-1.5 py-1 rounded shadow-sm border border-custom-border-300">
<div className="flex items-center justify-center gap-2 px-1.5 py-1 rounded shadow-sm border border-custom-border-300 hover:bg-custom-background-80">
<Icon iconName="person" className="!text-base !leading-4" />
<span className="text-custom-text-200">Assignee</span>
</div>
+4 -2
View File
@@ -17,10 +17,10 @@ type Props = {
export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onChange, value }) => (
<Popover className="relative flex items-center justify-center rounded-lg">
{({ open }) => (
{({ close }) => (
<>
<Popover.Button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200">
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200">
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80">
{value ? (
<>
<span className="text-custom-text-100">{renderShortDateWithYearFormat(value)}</span>
@@ -52,6 +52,8 @@ export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onCh
onChange={(val) => {
if (!val) onChange("");
else onChange(renderDateFormat(val));
close();
}}
dateFormat="dd-MM-yyyy"
minDate={minDate}
+4 -4
View File
@@ -59,17 +59,17 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
>
{({ open }: any) => (
<>
<Combobox.Button className="flex cursor-pointer items-center text-xs">
<Combobox.Button className="flex items-center gap-2 cursor-pointer text-xs text-custom-text-200">
{value && value.length > 0 ? (
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs">
<span className="flex items-center justify-center gap-2 text-xs">
<IssueLabelsList
labels={value.map((v) => issueLabels?.find((l) => l.id === v)?.color) ?? []}
labels={value.map((v) => issueLabels?.find((l) => l.id === v)) ?? []}
length={3}
showLength={true}
/>
</span>
) : (
<span className="flex items-center justify-center gap-2 px-2.5 py-1 text-xs rounded-md border border-custom-border-200 shadow-sm duration-200 hover:bg-custom-background-80">
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs rounded shadow-sm border border-custom-border-300 hover:bg-custom-background-80">
<TagIcon className="h-3.5 w-3.5 text-custom-text-200" />
<span className=" text-custom-text-200">Label</span>
</span>
@@ -17,13 +17,7 @@ import useUser from "hooks/use-user";
// ui
import { Input, Spinner } from "components/ui";
// icons
import {
ChevronDownIcon,
PlusIcon,
RectangleGroupIcon,
TagIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { PlusIcon, RectangleGroupIcon, TagIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IIssueLabels } from "types";
// fetch-keys
@@ -281,18 +275,15 @@ export const SidebarLabelSelect: React.FC<Props> = ({
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`flex items-center gap-1 rounded-md bg-custom-background-80 p-1 outline-none focus:ring-2 focus:ring-custom-primary`}
>
<Popover.Button className="grid place-items-center outline-none">
{watch("color") && watch("color") !== "" && (
<span
className="h-5 w-5 rounded"
className="h-6 w-6 rounded"
style={{
backgroundColor: watch("color") ?? "black",
}}
/>
)}
<ChevronDownIcon className="h-3 w-3" />
</Popover.Button>
<Transition
@@ -304,7 +295,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 bottom-8 z-10 mt-1 max-w-xs transform px-2 sm:px-0">
<Popover.Panel className="absolute z-10 mt-1.5 max-w-xs px-2 sm:px-0">
<Controller
name="color"
control={labelControl}
+2 -2
View File
@@ -285,7 +285,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
user={user}
/>
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="flex items-center justify-between pb-3">
<div className="flex items-center justify-between px-5 pb-3">
<h4 className="text-sm font-medium">
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
</h4>
@@ -327,7 +327,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
</div>
</div>
<div className="h-full w-full overflow-y-auto">
<div className="h-full w-full px-5 overflow-y-auto">
<div className={`divide-y-2 divide-custom-border-200 ${uneditable ? "opacity-60" : ""}`}>
{showFirstSection && (
<div className="py-1">
+13 -34
View File
@@ -1,15 +1,11 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
// components
import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules";
// ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helper
import { isDateRangeValid } from "helpers/date-time.helper";
// types
import { IModule } from "types";
@@ -29,8 +25,6 @@ const defaultValues: Partial<IModule> = {
};
export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
const [isDateValid, setIsDateValid] = useState(true);
const { setToastAlert } = useToast();
const {
register,
formState: { errors, isSubmitting },
@@ -57,6 +51,15 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
});
}, [data, reset]);
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
return (
<form onSubmit={handleSubmit(handleCreateUpdateModule)}>
<div className="space-y-5">
@@ -103,20 +106,8 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
value={value}
onChange={(val) => {
onChange(val);
if (val && watch("target_date")) {
if (isDateRangeValid(val, `${watch("target_date")}`)) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
}}
maxDate={maxDate ?? undefined}
/>
)}
/>
@@ -129,20 +120,8 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
value={value}
onChange={(val) => {
onChange(val);
if (watch("start_date") && val) {
if (isDateRangeValid(`${watch("start_date")}`, val)) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
}}
minDate={minDate ?? undefined}
/>
)}
/>
@@ -166,7 +145,7 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
</div>
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-200 px-5 pt-5">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting || isDateValid ? false : true}>
<PrimaryButton type="submit" loading={isSubmitting}>
{status
? isSubmitting
? "Updating Module..."
@@ -80,7 +80,9 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
) : (
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
<span className="text-custom-text-100 font-medium text-lg">
{notification.triggered_by_details.display_name?.[0] ? (
{notification.triggered_by_details.is_bot ? (
notification.triggered_by_details.first_name?.[0]?.toUpperCase()
) : notification.triggered_by_details.display_name?.[0] ? (
notification.triggered_by_details.display_name?.[0]?.toUpperCase()
) : (
<Icon iconName="person" className="h-6 w-6" />
@@ -91,7 +93,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
</div>
<div className="space-y-2.5 w-full overflow-hidden">
<div className="text-sm w-full break-words">
<span className="font-semibold">{notification.triggered_by_details.display_name} </span>
<span className="font-semibold">
{notification.triggered_by_details.is_bot
? notification.triggered_by_details.first_name
: notification.triggered_by_details.display_name}{" "}
</span>
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.verb}{" "}
{notification.data.issue_activity.field === "comment"
@@ -231,9 +231,9 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
description:
!data.description || data.description === ""
? {
type: "doc",
content: [{ type: "paragraph" }],
}
type: "doc",
content: [{ type: "paragraph" }],
}
: data.description,
description_html: data.description_html ?? "<p></p>",
});
@@ -292,6 +292,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
if (!data)
return (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={"<p></p>"}
debouncedUpdatesEnabled={false}
@@ -311,12 +312,11 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
return (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={
value && value !== "" && Object.keys(value).length > 0
? value
: watch("description_html") && watch("description_html") !== ""
? watch("description_html")
: { type: "doc", content: [{ type: "paragraph" }] }
}
debouncedUpdatesEnabled={false}
@@ -334,9 +334,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
<div className="m-2 mt-6 flex">
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${
iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
}`}
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
>
@@ -368,8 +367,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
? "Updating..."
: "Update block"
: isSubmitting
? "Adding..."
: "Add block"}
? "Adding..."
: "Add block"}
</PrimaryButton>
</div>
</form>
@@ -456,6 +456,7 @@ export const SinglePageBlock: React.FC<Props> = ({
{showBlockDetails
? block.description_html.length > 7 && (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
value={block.description_html}
customClassName="text-sm min-h-[150px]"
noBorder
@@ -12,10 +12,10 @@ type Props = {
};
export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) => (
<div className="space-y-2">
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">Issues by Priority</h3>
{userProfile ? (
<div className="border border-custom-border-100 rounded">
<div className="flex-grow border border-custom-border-100 rounded">
{userProfile.priority_distribution.length > 0 ? (
<BarGraph
data={userProfile.priority_distribution.map((priority) => ({
@@ -63,7 +63,7 @@ export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) =>
}}
/>
) : (
<div className="p-7">
<div className="flex-grow p-7">
<ProfileEmptyState
title="No Data yet"
description="Create issues to view the them by priority in the graph for better analysis."
@@ -16,9 +16,9 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
if (!userProfile) return null;
return (
<div className="space-y-2">
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">Issues by State</h3>
<div className="border border-custom-border-100 rounded p-7">
<div className="flex-grow border border-custom-border-100 rounded p-7">
{userProfile.state_distribution.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<div>
@@ -42,6 +42,7 @@ import { NETWORK_CHOICES } from "constants/project";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setToFavorite?: boolean;
user: ICurrentUserResponse | undefined;
};
@@ -74,7 +75,12 @@ const IsGuestCondition: React.FC<{
return null;
};
export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user }) => {
export const CreateProjectModal: React.FC<Props> = ({
isOpen,
setIsOpen,
setToFavorite = false,
user,
}) => {
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
const { setToastAlert } = useToast();
@@ -104,6 +110,29 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
reset(defaultValues);
};
const handleAddToFavorites = (projectId: string) => {
if (!workspaceSlug) return;
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) =>
(prevData ?? []).map((p) => (p.id === projectId ? { ...p, is_favorite: true } : p)),
false
);
projectServices
.addProjectToFavorites(workspaceSlug as string, {
project: projectId,
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
})
);
};
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug) return;
@@ -125,6 +154,9 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
title: "Success!",
message: "Project created successfully.",
});
if (setToFavorite) {
handleAddToFavorites(res.id);
}
handleClose();
})
.catch((err) => {
@@ -1,9 +1,11 @@
import React, { useEffect, useState } from "react";
import React from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
@@ -27,6 +29,11 @@ type TConfirmProjectDeletionProps = {
user: ICurrentUserResponse | undefined;
};
const defaultValues = {
projectName: "",
confirmDelete: "",
};
export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
isOpen,
data,
@@ -34,51 +41,41 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
onSuccess,
user,
}) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [confirmProjectName, setConfirmProjectName] = useState("");
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
const [selectedProject, setSelectedProject] = useState<IProject | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject;
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues });
useEffect(() => {
if (data) setSelectedProject(data);
else {
const timer = setTimeout(() => {
setSelectedProject(null);
clearTimeout(timer);
}, 300);
}
}, [data]);
const canDelete =
watch("projectName") === data?.name && watch("confirmDelete") === "delete my project";
const handleClose = () => {
setIsDeleteLoading(false);
const timer = setTimeout(() => {
setConfirmProjectName("");
setConfirmDeleteMyProject(false);
reset(defaultValues);
clearTimeout(timer);
}, 350);
onClose();
};
const handleDeletion = async () => {
const onSubmit = async () => {
if (!data || !workspaceSlug || !canDelete) return;
setIsDeleteLoading(true);
await projectService
.deleteProject(workspaceSlug as string, data.id, user)
.deleteProject(workspaceSlug.toString(), data.id, user)
.then(() => {
handleClose();
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
PROJECTS_LIST(workspaceSlug.toString(), { is_favorite: "all" }),
(prevData) => prevData?.filter((project: IProject) => project.id !== data.id),
false
);
@@ -91,8 +88,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
title: "Error!",
message: "Something went wrong. Please try again later.",
})
)
.finally(() => setIsDeleteLoading(false));
);
};
return (
@@ -122,7 +118,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
@@ -137,28 +133,29 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
<span>
<p className="text-sm leading-7 text-custom-text-200">
Are you sure you want to delete project{" "}
<span className="break-words font-semibold">{selectedProject?.name}</span>?
All of the data related to the project will be permanently removed. This
action cannot be undone
<span className="break-words font-semibold">{data?.name}</span>? All of the
data related to the project will be permanently removed. This action cannot be
undone
</p>
</span>
<div className="text-custom-text-200">
<p className="break-words text-sm ">
Enter the project name{" "}
<span className="font-medium text-custom-text-100">
{selectedProject?.name}
</span>{" "}
to continue:
<span className="font-medium text-custom-text-100">{data?.name}</span> to
continue:
</p>
<Input
type="text"
placeholder="Project name"
className="mt-2"
value={confirmProjectName}
onChange={(e) => {
setConfirmProjectName(e.target.value);
}}
<Controller
control={control}
name="projectName"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Project name"
className="mt-2"
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="text-custom-text-200">
@@ -167,31 +164,27 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
<span className="font-medium text-custom-text-100">delete my project</span>{" "}
below:
</p>
<Input
type="text"
placeholder="Enter 'delete my project'"
className="mt-2"
onChange={(e) => {
if (e.target.value === "delete my project") {
setConfirmDeleteMyProject(true);
} else {
setConfirmDeleteMyProject(false);
}
}}
name="typeDelete"
<Controller
control={control}
name="confirmDelete"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Enter 'delete my project'"
className="mt-2"
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton
onClick={handleDeletion}
disabled={!canDelete}
loading={isDeleteLoading}
>
{isDeleteLoading ? "Deleting..." : "Delete Project"}
<DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
{isSubmitting ? "Deleting..." : "Delete Project"}
</DangerButton>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
@@ -19,6 +19,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, on
const handleJoin = () => {
setIsJoiningLoading(true);
onJoin()
.then(() => {
setIsJoiningLoading(false);
@@ -59,7 +60,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, on
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-80 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-300 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
<div className="space-y-5">
<Dialog.Title
as="h3"
+33 -25
View File
@@ -9,11 +9,10 @@ import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautif
import { Disclosure, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
import useTheme from "hooks/use-theme";
import useUserAuth from "hooks/use-user-auth";
import useProjects from "hooks/use-projects";
// components
import { DeleteProjectModal, SingleSidebarProject } from "components/project";
import { CreateProjectModal, DeleteProjectModal, SingleSidebarProject } from "components/project";
// services
import projectService from "services/project.service";
// icons
@@ -32,6 +31,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
export const ProjectSidebarList: FC = () => {
const store: any = useMobxStore();
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null);
@@ -41,18 +41,15 @@ export const ProjectSidebarList: FC = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug } = router.query;
const { user } = useUserAuth();
const { collapsed: sidebarCollapse } = useTheme();
const { setToastAlert } = useToast();
const { projects: allProjects } = useProjects();
const joinedProjects = allProjects?.filter((p) => p.sort_order);
const joinedProjects = allProjects?.filter((p) => p.is_member);
const favoriteProjects = allProjects?.filter((p) => p.is_favorite);
const otherProjects = allProjects?.filter((p) => p.sort_order === null);
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
? orderArrayBy(joinedProjects, "sort_order", "ascending")
@@ -151,6 +148,12 @@ export const ProjectSidebarList: FC = () => {
return (
<>
<CreateProjectModal
isOpen={isProjectModalOpen}
setIsOpen={setIsProjectModalOpen}
setToFavorite
user={user}
/>
<DeleteProjectModal
isOpen={deleteProjectModal}
onClose={() => setDeleteProjectModal(false)}
@@ -172,17 +175,25 @@ export const ProjectSidebarList: FC = () => {
{({ open }) => (
<>
{!store?.theme?.sidebarCollapsed && (
<Disclosure.Button
as="button"
type="button"
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-400 text-left hover:bg-custom-sidebar-background-80 rounded w-full whitespace-nowrap"
>
Favorites
<Icon
iconName={open ? "arrow_drop_down" : "arrow_right"}
className="group-hover:opacity-100 opacity-0 !text-lg"
/>
</Disclosure.Button>
<div className="group flex justify-between items-center text-xs px-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 w-full">
<Disclosure.Button
as="button"
type="button"
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-400 text-left hover:bg-custom-sidebar-background-80 rounded w-full whitespace-nowrap"
>
Favorites
<Icon
iconName={open ? "arrow_drop_down" : "arrow_right"}
className="group-hover:opacity-100 opacity-0 !text-lg"
/>
</Disclosure.Button>
<button
className="group-hover:opacity-100 opacity-0"
onClick={() => setIsProjectModalOpen(true)}
>
<Icon iconName="add" />
</button>
</div>
)}
<Disclosure.Panel as="div" className="space-y-2">
{orderedFavProjects.map((project, index) => (
@@ -190,7 +201,7 @@ export const ProjectSidebarList: FC = () => {
key={project.id}
draggableId={project.id}
index={index}
isDragDisabled={project.sort_order === null}
isDragDisabled={!project.is_member}
>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
@@ -241,10 +252,7 @@ export const ProjectSidebarList: FC = () => {
</Disclosure.Button>
<button
className="group-hover:opacity-100 opacity-0"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
onClick={() => setIsProjectModalOpen(true)}
>
<Icon iconName="add" />
</button>
@@ -288,10 +296,10 @@ export const ProjectSidebarList: FC = () => {
</Droppable>
</DragDropContext>
{allProjects && allProjects.length === 0 && (
{joinedProjects && joinedProjects.length === 0 && (
<button
type="button"
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-custom-sidebar-text-200 mt-5"
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
@@ -15,36 +15,36 @@ export interface BubbleMenuItem {
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
const items: BubbleMenuItem[] = [
{
name: "bold",
isActive: () => props.editor.isActive("bold"),
command: () => props.editor.chain().focus().toggleBold().run(),
isActive: () => props.editor?.isActive("bold"),
command: () => props.editor?.chain().focus().toggleBold().run(),
icon: BoldIcon,
},
{
name: "italic",
isActive: () => props.editor.isActive("italic"),
command: () => props.editor.chain().focus().toggleItalic().run(),
isActive: () => props.editor?.isActive("italic"),
command: () => props.editor?.chain().focus().toggleItalic().run(),
icon: ItalicIcon,
},
{
name: "underline",
isActive: () => props.editor.isActive("underline"),
command: () => props.editor.chain().focus().toggleUnderline().run(),
isActive: () => props.editor?.isActive("underline"),
command: () => props.editor?.chain().focus().toggleUnderline().run(),
icon: UnderlineIcon,
},
{
name: "strike",
isActive: () => props.editor.isActive("strike"),
command: () => props.editor.chain().focus().toggleStrike().run(),
isActive: () => props.editor?.isActive("strike"),
command: () => props.editor?.chain().focus().toggleStrike().run(),
icon: StrikethroughIcon,
},
{
name: "code",
isActive: () => props.editor.isActive("code"),
command: () => props.editor.chain().focus().toggleCode().run(),
isActive: () => props.editor?.isActive("code"),
command: () => props.editor?.chain().focus().toggleCode().run(),
icon: CodeIcon,
},
];
@@ -78,7 +78,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
>
<NodeSelector
editor={props.editor}
editor={props.editor!}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen);
@@ -86,7 +86,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
}}
/>
<LinkSelector
editor={props.editor}
editor={props.editor!!}
isOpen={isLinkSelectorOpen}
setIsOpen={() => {
setIsLinkSelectorOpen(!isLinkSelectorOpen);
@@ -1,17 +1,27 @@
import { Editor } from "@tiptap/core";
import { Check, Trash } from "lucide-react";
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
import { cn } from "../utils";
import isValidHttpUrl from "./utils/link-validator";
interface LinkSelectorProps {
editor: Editor;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
const inputRef = useRef<HTMLInputElement>(null);
const onLinkSubmit = useCallback(() => {
const input = inputRef.current;
const url = input?.value;
if (url && isValidHttpUrl(url)) {
editor.chain().focus().setLink({ href: url }).run();
setIsOpen(false);
}
}, [editor, inputRef, setIsOpen]);
useEffect(() => {
inputRef.current && inputRef.current?.focus();
});
@@ -38,15 +48,13 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
</p>
</button>
{isOpen && (
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const input = form.elements[0] as HTMLInputElement;
editor.chain().focus().setLink({ href: input.value }).run();
setIsOpen(false);
}}
<div
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); onLinkSubmit();
}
}}
>
<input
ref={inputRef}
@@ -57,6 +65,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
onClick={() => {
editor.chain().focus().unsetLink().run();
@@ -66,11 +75,15 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
<Trash className="h-4 w-4" />
</button>
) : (
<button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90">
<button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90" type="button"
onClick={() => {
onLinkSubmit();
}}
>
<Check className="h-4 w-4" />
</button>
)}
</form>
</div>
)}
</div>
);
@@ -0,0 +1,12 @@
export default function isValidHttpUrl(string: string): boolean {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:";
}
@@ -0,0 +1,57 @@
import { Editor } from "@tiptap/react";
import Moveable from "react-moveable";
export const ImageResizer = ({ editor }: { editor: Editor }) => {
const updateMediaSize = () => {
const imageInfo = document.querySelector(
".ProseMirror-selectednode",
) as HTMLImageElement;
if (imageInfo) {
const selection = editor.state.selection;
editor.commands.setImage({
src: imageInfo.src,
width: Number(imageInfo.style.width.replace("px", "")),
height: Number(imageInfo.style.height.replace("px", "")),
} as any);
editor.commands.setNodeSelection(selection.from);
}
};
return (
<>
<Moveable
target={document.querySelector(".ProseMirror-selectednode") as any}
container={null}
origin={false}
edge={false}
throttleDrag={0}
keepRatio={true}
resizable={true}
throttleResize={0}
onResize={({
target,
width,
height,
delta,
}:
any) => {
delta[0] && (target!.style.width = `${width}px`);
delta[1] && (target!.style.height = `${height}px`);
}}
onResizeEnd={() => {
updateMediaSize();
}}
scalable={true}
renderDirections={["w", "e"]}
onScale={({
target,
transform,
}:
any) => {
target!.style.transform = transform;
}}
/>
</>
);
};
@@ -1,7 +1,6 @@
import StarterKit from "@tiptap/starter-kit";
import HorizontalRule from "@tiptap/extension-horizontal-rule";
import TiptapLink from "@tiptap/extension-link";
import TiptapImage from "@tiptap/extension-image";
import Placeholder from "@tiptap/extension-placeholder";
import TiptapUnderline from "@tiptap/extension-underline";
import TextStyle from "@tiptap/extension-text-style";
@@ -18,18 +17,13 @@ import { InputRule } from "@tiptap/core";
import ts from "highlight.js/lib/languages/typescript";
import "highlight.js/styles/github-dark.css";
import UploadImagesPlugin from "../plugins/upload-image";
import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image";
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
lowlight.registerLanguage("ts", ts);
const CustomImage = TiptapImage.extend({
addProseMirrorPlugins() {
return [UploadImagesPlugin()];
},
});
export const TiptapExtensions = [
export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@@ -93,13 +87,14 @@ export const TiptapExtensions = [
},
}),
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",
},
}),
CustomImage.configure({
allowBase64: true,
UpdatedImage.configure({
HTMLAttributes: {
class: "rounded-lg border border-custom-border-300",
},
@@ -117,7 +112,7 @@ export const TiptapExtensions = [
UniqueID.configure({
types: ["image"],
}),
SlashCommand,
SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline,
TextStyle,
Color,
@@ -0,0 +1,22 @@
import Image from "@tiptap/extension-image";
import TrackImageDeletionPlugin from "../plugins/delete-image";
import UploadImagesPlugin from "../plugins/upload-image";
const UpdatedImage = Image.extend({
addProseMirrorPlugins() {
return [UploadImagesPlugin(), TrackImageDeletionPlugin()];
},
addAttributes() {
return {
...this.parent?.(),
width: {
default: '35%',
},
height: {
default: null,
},
};
},
});
export default UpdatedImage;
+13 -52
View File
@@ -1,14 +1,10 @@
// @ts-nocheck
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import { useDebouncedCallback } from "use-debounce";
import { EditorBubbleMenu } from "./bubble-menu";
import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props";
import { Node } from "@tiptap/pm/model";
import { Editor as CoreEditor } from "@tiptap/core";
import { useCallback, useImperativeHandle, useRef } from "react";
import { EditorState } from "@tiptap/pm/state";
import fileService from "services/file.service";
import { useImperativeHandle, useRef } from "react";
import { ImageResizer } from "./extensions/image-resize";
export interface ITiptapRichTextEditor {
value: string;
@@ -18,6 +14,8 @@ export interface ITiptapRichTextEditor {
editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setShouldShowAlert?: (showAlert: boolean) => void;
workspaceSlug: string;
editable?: boolean;
forwardedRef?: any;
debouncedUpdatesEnabled?: boolean;
@@ -30,22 +28,24 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
forwardedRef,
editable,
setIsSubmitting,
setShouldShowAlert,
editorContentCustomClassNames,
value,
noBorder,
workspaceSlug,
borderOnFocus,
customClassName,
} = props;
const editor = useEditor({
editable: editable ?? true,
editorProps: TiptapEditorProps,
extensions: TiptapExtensions,
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
extensions: TiptapExtensions(workspaceSlug, setIsSubmitting),
content: value,
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting");
checkForNodeDeletions(editor);
setShouldShowAlert?.(true);
if (debouncedUpdatesEnabled) {
debouncedUpdates({ onChange, editor });
} else {
@@ -65,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
},
}));
const previousState = useRef<EditorState>();
const onNodeDeleted = useCallback(async (node: Node) => {
if (node.type.name === "image") {
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("file deleted successfully");
}
}
}, []);
const checkForNodeDeletions = useCallback(
(editor: CoreEditor) => {
const prevNodesById: Record<string, Node> = {};
previousState.current?.doc.forEach((node) => {
if (node.attrs.id) {
prevNodesById[node.attrs.id] = node;
}
});
const nodesById: Record<string, Node> = {};
editor.state?.doc.forEach((node) => {
if (node.attrs.id) {
nodesById[node.attrs.id] = node;
}
});
previousState.current = editor.state;
for (const [id, node] of Object.entries(prevNodesById)) {
if (nodesById[id] === undefined) {
onNodeDeleted(node);
}
}
},
[onNodeDeleted]
);
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
setTimeout(async () => {
if (onChange) {
@@ -112,10 +73,9 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
}, 500);
}, 1000);
const editorClassNames = `relative w-full max-w-screen-lg mt-2 p-3 relative focus:outline-none rounded-lg
${noBorder ? "" : "border border-custom-border-200"} ${
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
} ${customClassName}`;
const editorClassNames = `relative w-full max-w-screen-lg 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}`;
if (!editor) return null;
editorRef.current = editor;
@@ -131,6 +91,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
{editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}>
<EditorContent editor={editor} />
{editor?.isActive("image") && <ImageResizer editor={editor} />}
</div>
</div>
);
@@ -0,0 +1,56 @@
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from '@tiptap/pm/model';
import fileService from "services/file.service";
const deleteKey = new PluginKey("delete-image");
const TrackImageDeletionPlugin = () =>
new Plugin({
key: deleteKey,
appendTransaction: (transactions, oldState, newState) => {
transactions.forEach((transaction) => {
if (!transaction.docChanged) return;
const removedImages: ProseMirrorNode[] = [];
oldState.doc.descendants((oldNode, oldPos) => {
if (oldNode.type.name !== 'image') return;
if (!newState.doc.resolve(oldPos).parent) return;
const newNode = newState.doc.nodeAt(oldPos);
// Check if the node has been deleted or replaced
if (!newNode || newNode.type.name !== 'image') {
// Check if the node still exists elsewhere in the document
let nodeExists = false;
newState.doc.descendants((node) => {
if (node.attrs.id === oldNode.attrs.id) {
nodeExists = true;
}
});
if (!nodeExists) {
removedImages.push(oldNode as ProseMirrorNode);
}
}
});
removedImages.forEach((node) => {
const src = node.attrs.src;
onNodeDeleted(src);
});
});
return null;
},
});
export default TrackImageDeletionPlugin;
async function onNodeDeleted(src: string) {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("Image deleted successfully");
}
}
@@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) {
return found.length ? found[0].from : null;
}
export async function startImageUpload(file: File, view: EditorView, pos: number) {
export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) {
if (!file.type.includes("image/")) {
return;
} else if (file.size / 1024 / 1024 > 20) {
@@ -82,7 +82,11 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
view.dispatch(tr);
};
const src = await UploadImageHandler(file);
if (!workspaceSlug) {
return;
}
setIsSubmitting?.("submitting")
const src = await UploadImageHandler(file, workspaceSlug);
const { schema } = view.state;
pos = findPlaceholder(view.state, id);
@@ -96,7 +100,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
view.dispatch(transaction);
}
const UploadImageHandler = (file: File): Promise<string> => {
const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string> => {
if (!workspaceSlug) {
return Promise.reject("Workspace slug is missing");
}
try {
const formData = new FormData();
formData.append("asset", file);
@@ -104,7 +111,7 @@ const UploadImageHandler = (file: File): Promise<string> => {
return new Promise(async (resolve, reject) => {
const imageUrl = await fileService
.uploadFile("plane", formData)
.uploadFile(workspaceSlug, formData)
.then((response) => response.asset);
const image = new Image();
+51 -51
View File
@@ -1,56 +1,56 @@
import { EditorProps } from "@tiptap/pm/view";
import { startImageUpload } from "./plugins/upload-image";
export const TiptapEditorProps: EditorProps = {
attributes: {
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
},
handleDOMEvents: {
keydown: (_view, event) => {
// prevent default event listeners from firing when slash command is active
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
return true;
}
}
export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
return {
attributes: {
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
},
},
handlePaste: (view, event) => {
if (
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files[0]
) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;
startImageUpload(file, view, pos);
return true;
}
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (
!moved &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files[0]
) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (coordinates) {
startImageUpload(file, view, coordinates.pos - 1);
handleDOMEvents: {
keydown: (_view, event) => {
// prevent default event listeners from firing when slash command is active
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
return true;
}
}
},
},
handlePaste: (view, event) => {
if (
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files[0]
) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;
startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting);
return true;
}
return true;
}
return false;
},
};
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (
!moved &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files[0]
) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (coordinates) {
startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting);
}
return true;
}
return false;
},
};
}
@@ -52,7 +52,7 @@ const Command = Extension.create({
},
});
const getSuggestionItems = ({ query }: { query: string }) =>
const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) =>
[
{
title: "Text",
@@ -163,7 +163,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
startImageUpload(file, editor.view, pos);
startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting);
}
};
input.click();
@@ -328,11 +328,12 @@ const renderItems = () => {
};
};
const SlashCommand = Command.configure({
suggestion: {
items: getSuggestionItems,
render: renderItems,
},
});
export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
Command.configure({
suggestion: {
items: getSuggestionItems(workspaceSlug, setIsSubmitting),
render: renderItems,
},
});
export default SlashCommand;
@@ -1,47 +1,76 @@
import React, { useEffect } from "react";
import React, { useEffect, useRef } from "react";
import Link from "next/link";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
type Props = {
position: {
x: number;
y: number;
};
clickEvent: React.MouseEvent | null;
children: React.ReactNode;
title?: string | JSX.Element;
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
};
const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => {
const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => {
const contextMenuRef = useRef<HTMLDivElement>(null);
// Close the context menu when clicked outside
useOutsideClickDetector(contextMenuRef, () => {
if (isOpen) setIsOpen(false);
});
useEffect(() => {
const hideContextMenu = () => {
if (isOpen) setIsOpen(false);
};
window.addEventListener("click", hideContextMenu);
window.addEventListener("keydown", (e: KeyboardEvent) => {
const escapeKeyEvent = (e: KeyboardEvent) => {
if (e.key === "Escape") hideContextMenu();
});
};
window.addEventListener("click", hideContextMenu);
window.addEventListener("keydown", escapeKeyEvent);
return () => {
window.removeEventListener("click", hideContextMenu);
window.removeEventListener("keydown", hideContextMenu);
window.removeEventListener("keydown", escapeKeyEvent);
};
}, [isOpen, setIsOpen]);
useEffect(() => {
const contextMenu = contextMenuRef.current;
if (contextMenu && isOpen) {
const contextMenuWidth = contextMenu.clientWidth;
const contextMenuHeight = contextMenu.clientHeight;
const clickX = clickEvent?.pageX || 0;
const clickY = clickEvent?.pageY || 0;
let top = clickY;
// check if there's enough space at the bottom, otherwise show at the top
if (clickY + contextMenuHeight > window.innerHeight) top = clickY - contextMenuHeight;
// check if there's enough space on the right, otherwise show on the left
let left = clickX;
if (clickX + contextMenuWidth > window.innerWidth) left = clickX - contextMenuWidth;
contextMenu.style.top = `${top}px`;
contextMenu.style.left = `${left}px`;
}
}, [clickEvent, isOpen]);
return (
<div
className={`fixed z-20 h-full w-full ${
className={`fixed z-50 top-0 left-0 h-full w-full ${
isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
}`}
>
<div
className={`fixed z-20 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
style={{
left: `${position.x}px`,
top: `${position.y}px`,
}}
ref={contextMenuRef}
className={`fixed z-50 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
>
{title && (
<h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium">
+2 -2
View File
@@ -29,9 +29,9 @@ export const Input: React.FC<Props> = ({
type={type}
id={id}
value={value}
{...(register && register(name, validations))}
{...(register && register(name ?? "", validations))}
onChange={(e) => {
register && register(name).onChange(e);
register && register(name ?? "").onChange(e);
onChange && onChange(e);
}}
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
+1 -1
View File
@@ -3,7 +3,7 @@ import type { UseFormRegister, RegisterOptions } from "react-hook-form";
export interface Props extends React.ComponentPropsWithoutRef<"input"> {
label?: string;
name: string;
name?: string;
value?: string | number | readonly string[];
mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled";
register?: UseFormRegister<any>;
+14 -12
View File
@@ -1,7 +1,11 @@
import React from "react";
// ui
import { Tooltip } from "components/ui";
// types
import { IIssueLabels } from "types";
type IssueLabelsListProps = {
labels?: (string | undefined)[];
labels?: (IIssueLabels | undefined)[];
length?: number;
showLength?: boolean;
};
@@ -14,18 +18,16 @@ export const IssueLabelsList: React.FC<IssueLabelsListProps> = ({
<>
{labels && (
<>
{labels.slice(0, length).map((color, index) => (
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}>
<span
className={`h-4 w-4 flex-shrink-0 rounded-full border border-custom-border-200
`}
style={{
backgroundColor: color && color !== "" ? color : "#000000",
}}
/>
<Tooltip
position="top"
tooltipHeading="Labels"
tooltipContent={labels.map((l) => l?.name).join(", ")}
>
<div className="flex items-center gap-1.5 px-2 py-1 text-custom-text-200 rounded shadow-sm border border-custom-border-300">
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
{`${labels.length} Labels`}
</div>
))}
{labels.length > length ? <span>+{labels.length - length}</span> : null}
</Tooltip>
</>
)}
</>
@@ -45,6 +45,7 @@ const restrictedUrls = [
"profile",
"reset-password",
"sign-up",
"spaces",
"workspace-member-invitation",
];
@@ -1,9 +1,11 @@
import React, { useEffect, useState } from "react";
import React from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
@@ -26,57 +28,63 @@ type Props = {
user: ICurrentUserResponse | undefined;
};
const defaultValues = {
workspaceName: "",
confirmDelete: "",
};
export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, user }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [confirmWorkspaceName, setConfirmWorkspaceName] = useState("");
const [confirmDeleteMyWorkspace, setConfirmDeleteMyWorkspace] = useState(false);
const [selectedWorkspace, setSelectedWorkspace] = useState<IWorkspace | null>(null);
const router = useRouter();
const { setToastAlert } = useToast();
useEffect(() => {
if (data) setSelectedWorkspace(data);
else {
const timer = setTimeout(() => {
setSelectedWorkspace(null);
clearTimeout(timer);
}, 350);
}
}, [data]);
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues });
const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace;
const canDelete =
watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace";
const handleClose = () => {
setIsDeleteLoading(false);
setConfirmWorkspaceName("");
setConfirmDeleteMyWorkspace(false);
const timer = setTimeout(() => {
reset(defaultValues);
clearTimeout(timer);
}, 350);
onClose();
};
const handleDeletion = async () => {
setIsDeleteLoading(true);
const onSubmit = async () => {
if (!data || !canDelete) return;
await workspaceService
.deleteWorkspace(data.slug, user)
.then(() => {
handleClose();
router.push("/");
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
prevData?.filter((workspace) => workspace.id !== data.id)
);
setToastAlert({
type: "success",
message: "Workspace deleted successfully",
title: "Success",
title: "Success!",
message: "Workspace deleted successfully.",
});
})
.catch((error) => {
console.log(error);
setIsDeleteLoading(false);
});
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again later.",
})
);
};
return (
@@ -106,7 +114,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
@@ -131,20 +139,21 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
<div className="text-custom-text-200">
<p className="break-words text-sm ">
Enter the workspace name{" "}
<span className="font-medium text-custom-text-100">
{selectedWorkspace?.name}
</span>{" "}
to continue:
<span className="font-medium text-custom-text-100">{data?.name}</span> to
continue:
</p>
<Input
type="text"
placeholder="Workspace name"
className="mt-2"
value={confirmWorkspaceName}
onChange={(e) => {
setConfirmWorkspaceName(e.target.value);
}}
<Controller
control={control}
name="workspaceName"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Workspace name"
className="mt-2"
onChange={onChange}
value={value}
/>
)}
/>
</div>
@@ -154,28 +163,28 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
<span className="font-medium text-custom-text-100">delete my workspace</span>{" "}
below:
</p>
<Input
type="text"
placeholder="Enter 'delete my workspace'"
className="mt-2"
onChange={(e) => {
if (e.target.value === "delete my workspace") {
setConfirmDeleteMyWorkspace(true);
} else {
setConfirmDeleteMyWorkspace(false);
}
}}
name="typeDelete"
<Controller
control={control}
name="confirmDelete"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Enter 'delete my workspace'"
className="mt-2"
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading || !canDelete}>
{isDeleteLoading ? "Deleting..." : "Delete Workspace"}
<DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
{isSubmitting ? "Deleting..." : "Delete Workspace"}
</DangerButton>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
@@ -45,12 +45,14 @@ export const IssuesList: React.FC<Props> = ({ issues, type }) => {
>
<h4 className="capitalize">{type}</h4>
<h4 className="col-span-2">Issue</h4>
<h4>Due Date</h4>
<h4>{type === "overdue" ? "Due" : "Start"} Date</h4>
</div>
<div className="max-h-72 overflow-y-scroll">
{issues.length > 0 ? (
issues.map((issue) => {
const dateDifference = getDateDifference(new Date(issue.target_date as string));
const date = type === "overdue" ? issue.target_date : issue.start_date;
const dateDifference = getDateDifference(new Date(date as string));
return (
<Link
@@ -75,7 +77,7 @@ export const IssuesList: React.FC<Props> = ({ issues, type }) => {
</h5>
<h5 className="col-span-2">{truncateText(issue.name, 30)}</h5>
<h5 className="cursor-default">
{renderShortDateWithYearFormat(new Date(issue.target_date as string))}
{renderShortDateWithYearFormat(new Date(date?.toString() ?? ""))}
</h5>
</div>
</a>
+3 -3
View File
@@ -1,9 +1,9 @@
// services images
import GithubLogo from "public/services/github.png";
import JiraLogo from "public/services/jira.png";
import CSVLogo from "public/services/csv.png";
import ExcelLogo from "public/services/excel.png";
import JSONLogo from "public/services/json.png";
import CSVLogo from "public/services/csv.svg";
import ExcelLogo from "public/services/excel.svg";
import JSONLogo from "public/services/json.svg";
export const ROLE = {
5: "Guest",
@@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: ()
};
useEffect(() => {
document.addEventListener("click", handleClick);
document.addEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("click", handleClick);
document.removeEventListener("mousedown", handleClick);
};
});
};
+3 -2
View File
@@ -52,10 +52,10 @@
"highlight.js": "^11.8.0",
"js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
"mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3",
"lowlight": "^2.9.0",
"lucide-react": "^0.263.1",
"mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3",
"next": "12.3.2",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
@@ -68,6 +68,7 @@
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.38.0",
"react-markdown": "^8.0.7",
"react-moveable": "^0.54.1",
"sharp": "^0.32.1",
"sonner": "^0.6.2",
"swr": "^2.1.3",
@@ -106,6 +106,7 @@ const ProfileActivity = () => {
</div>
<div className="issue-comments-section p-0">
<Tiptap
workspaceSlug={workspaceSlug as string}
value={
activityItem?.new_value !== ""
? activityItem.new_value
@@ -148,7 +148,7 @@ const IssueDetailsPage: NextPage = () => {
<div className="w-2/3 h-full overflow-y-auto space-y-5 divide-y-2 divide-custom-border-300 p-5">
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />
</div>
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-300 p-5 overflow-hidden">
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-300 py-5 overflow-hidden">
<IssueDetailsSidebar
control={control}
issueDetail={issueDetails}
@@ -28,6 +28,8 @@ import type { NextPage } from "next";
import { PROJECT_MEMBERS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// types
import { IProject } from "types";
const ProjectsPage: NextPage = () => {
// router
@@ -39,7 +41,7 @@ const ProjectsPage: NextPage = () => {
const { user } = useUserAuth();
// context data
const { activeWorkspace } = useWorkspaces();
const { projects } = useProjects();
const { projects, mutateProjects } = useProjects();
// states
const [deleteProject, setDeleteProject] = useState<string | null>(null);
const [selectedProjectToJoin, setSelectedProjectToJoin] = useState<string | null>(null);
@@ -101,6 +103,14 @@ const ProjectsPage: NextPage = () => {
})
.then(async () => {
mutate(PROJECT_MEMBERS(project.id));
mutateProjects<IProject[]>(
(prevData) =>
(prevData ?? []).map((p) => ({
...p,
is_member: p.id === project.id ? true : p.is_member,
})),
false
);
setSelectedProjectToJoin(null);
})
.catch(() => {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6990_114407)">
<rect width="24" height="24" rx="12" fill="#FAFAFA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.2667 6.86693C8.14293 6.86693 8.02423 6.91609 7.93672 7.00361C7.8492 7.09113 7.80003 7.20983 7.80003 7.33359V16.6669C7.80003 16.7907 7.8492 16.9094 7.93672 16.9969C8.02423 17.0844 8.14293 17.1336 8.2667 17.1336H15.7334C15.8571 17.1336 15.9758 17.0844 16.0633 16.9969C16.1509 16.9094 16.2 16.7907 16.2 16.6669V10.6003H12.9334C12.8096 10.6003 12.6909 10.5511 12.6034 10.4636C12.5159 10.3761 12.4667 10.2574 12.4667 10.1336V6.86693H8.2667ZM13.4 7.52679L15.5402 9.66693H13.4V7.52679ZM6.8667 7.33359C6.8667 6.96229 7.0142 6.6062 7.27675 6.34365C7.5393 6.08109 7.8954 5.93359 8.2667 5.93359H12.9334C12.9947 5.93348 13.0554 5.94546 13.1121 5.96884C13.1688 5.99223 13.2203 6.02655 13.2638 6.06986L16.9971 9.80319C17.0404 9.84661 17.0747 9.89814 17.0981 9.95483C17.1215 10.0115 17.1335 10.0723 17.1334 10.1336V16.6669C17.1334 17.0382 16.9859 17.3943 16.7233 17.6569C16.4608 17.9194 16.1047 18.0669 15.7334 18.0669H8.2667C7.8954 18.0669 7.5393 17.9194 7.27675 17.6569C7.0142 17.3943 6.8667 17.0382 6.8667 16.6669V7.33359Z" fill="#3F76FF"/>
<path d="M10.876 14.3008H10.4422C10.4298 14.2297 10.407 14.1667 10.3737 14.1117C10.3405 14.0561 10.2991 14.0089 10.2496 13.9702C10.2001 13.9315 10.1437 13.9025 10.0803 13.8832C10.0176 13.8631 9.94995 13.8531 9.87725 13.8531C9.7481 13.8531 9.63365 13.8855 9.53388 13.9505C9.43412 14.0147 9.35601 14.109 9.29955 14.2335C9.2431 14.3573 9.21487 14.5085 9.21487 14.6871C9.21487 14.8689 9.2431 15.022 9.29955 15.1465C9.35678 15.2702 9.43489 15.3638 9.53388 15.4272C9.63365 15.4899 9.74772 15.5212 9.87609 15.5212C9.94724 15.5212 10.0138 15.5119 10.0756 15.4933C10.1383 15.474 10.1943 15.4458 10.2438 15.4087C10.2941 15.3715 10.3362 15.3259 10.3703 15.2718C10.4051 15.2176 10.429 15.1558 10.4422 15.0862L10.876 15.0885C10.8598 15.2014 10.8246 15.3074 10.7705 15.4063C10.7171 15.5053 10.6471 15.5927 10.5605 15.6685C10.4739 15.7435 10.3726 15.8023 10.2566 15.8448C10.1406 15.8866 10.0118 15.9075 9.87029 15.9075C9.66149 15.9075 9.47511 15.8591 9.31115 15.7625C9.1472 15.6658 9.01805 15.5262 8.9237 15.3437C8.82935 15.1612 8.78218 14.9423 8.78218 14.6871C8.78218 14.4311 8.82974 14.2123 8.92486 14.0305C9.01999 13.848 9.14952 13.7084 9.31348 13.6118C9.47743 13.5151 9.66303 13.4668 9.87029 13.4668C10.0025 13.4668 10.1255 13.4853 10.2392 13.5224C10.3529 13.5596 10.4542 13.6141 10.5431 13.686C10.6321 13.7572 10.7051 13.8445 10.7624 13.9482C10.8204 14.051 10.8583 14.1686 10.876 14.3008ZM12.496 14.1523C12.4851 14.051 12.4395 13.9722 12.3591 13.9157C12.2794 13.8592 12.1758 13.831 12.0482 13.831C11.9585 13.831 11.8815 13.8445 11.8173 13.8716C11.7531 13.8987 11.704 13.9354 11.67 13.9818C11.636 14.0282 11.6186 14.0812 11.6178 14.1407C11.6178 14.1902 11.629 14.2332 11.6515 14.2695C11.6747 14.3059 11.706 14.3368 11.7454 14.3623C11.7849 14.3871 11.8285 14.4079 11.8765 14.425C11.9244 14.442 11.9728 14.4563 12.0215 14.4679L12.2442 14.5236C12.3339 14.5444 12.4202 14.5727 12.5029 14.6082C12.5864 14.6438 12.6611 14.6887 12.7268 14.7428C12.7933 14.7969 12.8459 14.8623 12.8846 14.9388C12.9232 15.0154 12.9426 15.1051 12.9426 15.208C12.9426 15.3472 12.907 15.4698 12.8358 15.5757C12.7647 15.6809 12.6618 15.7632 12.5273 15.8228C12.3935 15.8816 12.2315 15.911 12.0412 15.911C11.8564 15.911 11.6959 15.8823 11.5598 15.8251C11.4245 15.7679 11.3185 15.6844 11.242 15.5746C11.1662 15.4647 11.1252 15.3309 11.119 15.1732H11.5424C11.5486 15.2559 11.5741 15.3248 11.619 15.3797C11.6638 15.4346 11.7222 15.4756 11.7941 15.5026C11.8668 15.5297 11.948 15.5432 12.0377 15.5432C12.1313 15.5432 12.2133 15.5293 12.2837 15.5015C12.3548 15.4729 12.4105 15.4334 12.4507 15.3831C12.4909 15.3321 12.5114 15.2726 12.5122 15.2045C12.5114 15.1426 12.4933 15.0916 12.4577 15.0514C12.4221 15.0104 12.3722 14.9764 12.308 14.9493C12.2446 14.9214 12.1704 14.8967 12.0853 14.875L11.815 14.8054C11.6194 14.7552 11.4647 14.679 11.351 14.5769C11.2381 14.4741 11.1816 14.3376 11.1816 14.1674C11.1816 14.0274 11.2195 13.9049 11.2953 13.7997C11.3719 13.6945 11.4759 13.6129 11.6074 13.5549C11.7388 13.4961 11.8877 13.4668 12.054 13.4668C12.2226 13.4668 12.3703 13.4961 12.4971 13.5549C12.6247 13.6129 12.7249 13.6937 12.7976 13.7974C12.8703 13.9002 12.9078 14.0186 12.9101 14.1523H12.496ZM13.5788 13.4992L14.1971 15.3692H14.2214L14.8386 13.4992H15.3119L14.4743 15.875H13.943L13.1066 13.4992H13.5788Z" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_6990_114407">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6990_114419)">
<rect width="24" height="24" rx="12" fill="#FAFAFA"/>
<path d="M15.9375 6.3125H8.9375C8.70544 6.3125 8.48288 6.40469 8.31878 6.56878C8.15469 6.73288 8.0625 6.95544 8.0625 7.1875V8.5H7.1875C6.95544 8.5 6.73288 8.59219 6.56878 8.75628C6.40469 8.92038 6.3125 9.14294 6.3125 9.375V14.625C6.3125 14.8571 6.40469 15.0796 6.56878 15.2437C6.73288 15.4078 6.95544 15.5 7.1875 15.5H8.0625V16.8125C8.0625 17.0446 8.15469 17.2671 8.31878 17.4312C8.48288 17.5953 8.70544 17.6875 8.9375 17.6875H15.9375C16.1696 17.6875 16.3921 17.5953 16.5562 17.4312C16.7203 17.2671 16.8125 17.0446 16.8125 16.8125V7.1875C16.8125 6.95544 16.7203 6.73288 16.5562 6.56878C16.3921 6.40469 16.1696 6.3125 15.9375 6.3125ZM13.75 10.6875H15.9375V13.3125H13.75V10.6875ZM15.9375 9.8125H13.75V9.375C13.75 9.14294 13.6578 8.92038 13.4937 8.75628C13.3296 8.59219 13.1071 8.5 12.875 8.5V7.1875H15.9375V9.8125ZM8.9375 7.1875H12V8.5H8.9375V7.1875ZM7.1875 9.375H12.875V14.625H7.1875V9.375ZM8.9375 15.5H12V16.8125H8.9375V15.5ZM12.875 16.8125V15.5C13.1071 15.5 13.3296 15.4078 13.4937 15.2437C13.6578 15.0796 13.75 14.8571 13.75 14.625V14.1875H15.9375V16.8125H12.875ZM8.60117 13.0325L9.46195 12L8.60117 10.9675C8.52691 10.8783 8.49113 10.7633 8.50169 10.6477C8.51225 10.5321 8.5683 10.4254 8.6575 10.3512C8.7467 10.2769 8.86175 10.2411 8.97733 10.2517C9.09291 10.2623 9.19957 10.3183 9.27383 10.4075L10.0312 11.3164L10.7887 10.4075C10.8254 10.3633 10.8706 10.3268 10.9214 10.3001C10.9723 10.2734 11.0279 10.2569 11.0852 10.2517C11.1424 10.2465 11.2001 10.2526 11.255 10.2696C11.3099 10.2867 11.3608 10.3144 11.405 10.3512C11.4492 10.3879 11.4857 10.4331 11.5124 10.4839C11.5391 10.5348 11.5556 10.5904 11.5608 10.6477C11.566 10.7049 11.5599 10.7626 11.5429 10.8175C11.5258 10.8724 11.4981 10.9233 11.4613 10.9675L10.6005 12L11.4613 13.0325C11.5356 13.1217 11.5714 13.2367 11.5608 13.3523C11.5502 13.4679 11.4942 13.5746 11.405 13.6488C11.3158 13.7231 11.2008 13.7589 11.0852 13.7483C10.9696 13.7377 10.8629 13.6817 10.7887 13.5925L10.0312 12.6836L9.27383 13.5925C9.19957 13.6817 9.09291 13.7377 8.97733 13.7483C8.86175 13.7589 8.7467 13.7231 8.6575 13.6488C8.5683 13.5746 8.51225 13.4679 8.50169 13.3523C8.49113 13.2367 8.52691 13.1217 8.60117 13.0325Z" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_6990_114419">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6990_114394)">
<rect width="24" height="24" rx="12" fill="#FAFAFA"/>
<path d="M13.4583 6.1665H8.49992C8.1905 6.1665 7.89375 6.28942 7.67496 6.50821C7.45617 6.72701 7.33325 7.02375 7.33325 7.33317V16.6665C7.33325 16.9759 7.45617 17.2727 7.67496 17.4915C7.89375 17.7103 8.1905 17.8332 8.49992 17.8332H15.4999C15.8093 17.8332 16.1061 17.7103 16.3249 17.4915C16.5437 17.2727 16.6666 16.9759 16.6666 16.6665V9.37484L13.4583 6.1665Z" stroke="#3F76FF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.1665 6.1665V9.6665H16.6665M10.8332 11.9998C10.6785 11.9998 10.5301 12.0613 10.4207 12.1707C10.3113 12.2801 10.2498 12.4285 10.2498 12.5832V13.1665C10.2498 13.3212 10.1884 13.4696 10.079 13.579C9.96959 13.6884 9.82121 13.7498 9.6665 13.7498C9.82121 13.7498 9.96959 13.8113 10.079 13.9207C10.1884 14.0301 10.2498 14.1785 10.2498 14.3332V14.9165C10.2498 15.0712 10.3113 15.2196 10.4207 15.329C10.5301 15.4384 10.6785 15.4998 10.8332 15.4998M13.1665 15.4998C13.3212 15.4998 13.4696 15.4384 13.579 15.329C13.6884 15.2196 13.7498 15.0712 13.7498 14.9165V14.3332C13.7498 14.1785 13.8113 14.0301 13.9207 13.9207C14.0301 13.8113 14.1785 13.7498 14.3332 13.7498C14.1785 13.7498 14.0301 13.6884 13.9207 13.579C13.8113 13.4696 13.7498 13.3212 13.7498 13.1665V12.5832C13.7498 12.4285 13.6884 12.2801 13.579 12.1707C13.4696 12.0613 13.3212 11.9998 13.1665 11.9998" stroke="#3F76FF" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_6990_114394">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+24
View File
@@ -126,3 +126,27 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
transition: opacity 0.2s ease-out;
}
.img-placeholder {
position: relative;
width: 35%;
&:before {
content: "";
box-sizing: border-box;
position: absolute;
top: 50%;
left: 45%;
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid rgba(var(--color-text-200));
border-top-color: rgba(var(--color-text-800));
animation: spinning 0.6s linear infinite;
}
}
@keyframes spinning {
to {
transform: rotate(360deg);
}
}
+2 -1
View File
@@ -207,7 +207,8 @@ export interface IIssueLite {
id: string;
name: string;
project_id: string;
target_date: string;
start_date?: string | null;
target_date?: string | null;
workspace__slug: string;
}
+70
View File
@@ -0,0 +1,70 @@
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=space --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install --network-timeout 500000
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=space
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} space
FROM node:18-alpine AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 plane
RUN adduser --system --uid 1001 captain
USER captain
COPY --from=installer /app/apps/space/next.config.js .
COPY --from=installer /app/apps/space/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
USER captain
ENV NEXT_TELEMETRY_DISABLED 1
EXPOSE 3000
@@ -25,7 +25,10 @@ const WorkspaceProjectPage = observer(() => {
const routerSearchparams = useSearchParams();
const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string };
const board = routerSearchparams.get("board") as TIssueBoardKeys | "";
const board =
routerSearchparams &&
routerSearchparams.get("board") != null &&
(routerSearchparams.get("board") as TIssueBoardKeys | "");
// updating default board view when we are in the issues page
useEffect(() => {
-9
View File
@@ -1,10 +1,6 @@
"use client";
import { useEffect } from "react";
// next imports
import { useSearchParams } from "next/navigation";
// interface
import { TIssueBoardKeys } from "store/types";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
@@ -12,11 +8,6 @@ import { RootStore } from "store/root";
const MobxStoreInit = () => {
const store: RootStore = useMobxStore();
// search params
const routerSearchparams = useSearchParams();
const board = routerSearchparams.get("board") as TIssueBoardKeys;
useEffect(() => {
// theme
const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light";
+5
View File
@@ -1,9 +1,14 @@
/** @type {import('next').NextConfig} */
const path = require("path");
const nextConfig = {
reactStrictMode: false,
swcMinify: true,
experimental: {
outputFileTracingRoot: path.join(__dirname, "../../"),
appDir: true,
},
output: "standalone",
};
module.exports = nextConfig;
+3 -3
View File
@@ -1,11 +1,11 @@
{
"name": "plane-deploy",
"name": "space",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 4000",
"build": "next build",
"start": "next start",
"start": "next start -p 4000",
"lint": "next lint"
},
"dependencies": {
+10
View File
@@ -0,0 +1,10 @@
// styles
import "styles/globals.css";
// types
import type { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default MyApp;
+17
View File
@@ -0,0 +1,17 @@
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
+2 -4
View File
@@ -3,14 +3,12 @@ import axios from "axios";
// js cookie
import Cookies from "js-cookie";
const base_url: string | null = "https://boarding.plane.so";
abstract class APIService {
protected baseURL: string;
protected headers: any = {};
constructor(baseURL: string) {
this.baseURL = base_url ? base_url : baseURL;
constructor(_baseURL: string) {
this.baseURL = _baseURL;
}
setRefreshToken(token: string) {
+1 -3
View File
@@ -1,11 +1,9 @@
// services
import APIService from "services/api.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
class IssueService extends APIService {
constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async getPublicIssues(workspace_slug: string, project_slug: string): Promise<any> {
+1 -3
View File
@@ -1,11 +1,9 @@
// services
import APIService from "services/api.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
class ProjectService extends APIService {
constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async getProjectSettingsAsync(workspace_slug: string, project_slug: string): Promise<any> {
+1 -3
View File
@@ -1,11 +1,9 @@
// services
import APIService from "services/api.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
class UserService extends APIService {
constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async currentUser(): Promise<any> {
+2 -1
View File
@@ -38,11 +38,12 @@ services:
container_name: planefrontend
image: makeplane/plane-frontend:latest
restart: always
command: /usr/local/bin/start.sh
command: /usr/local/bin/start.sh apps/app/server.js app
env_file:
- .env
environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
NEXT_PUBLIC_GITHUB_ID: "0"
+3 -1
View File
@@ -41,12 +41,14 @@ services:
dockerfile: ./apps/app/Dockerfile.web
args:
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
restart: always
command: /usr/local/bin/start.sh
command: /usr/local/bin/start.sh apps/app/server.js app
env_file:
- .env
environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
NEXT_PUBLIC_GITHUB_ID: "0"
+5
View File
@@ -18,6 +18,11 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /space/ {
proxy_pass http://localhost:4000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
+2 -1
View File
@@ -1,6 +1,7 @@
#!/bin/sh
FROM=$1
TO=$2
DIRECTORY=$3
if [ "${FROM}" = "${TO}" ]; then
echo "Nothing to replace, the value is already set to ${TO}."
@@ -11,4 +12,4 @@ fi
# Only perform action if $FROM and $TO are different.
echo "Replacing all statically built instances of $FROM with this string $TO ."
grep -R -la "${FROM}" apps/app/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
grep -R -la "${FROM}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
+1 -1
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# cp ./.env.example ./.env
cp ./.env.example ./.env
# Export for tr error in mac
export LC_ALL=C
+2 -2
View File
@@ -3,7 +3,7 @@ set -x
# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL
# NOTE: if these values are the same, this will be skipped.
/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL"
/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" $2
echo "Starting Plane Frontend.."
node apps/app/server.js
node $1
+156
View File
@@ -1006,6 +1006,40 @@
react-popper "^2.3.0"
tslib "~2.5.0"
"@cfcs/core@^0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121"
integrity sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==
dependencies:
"@egjs/component" "^3.0.2"
"@daybrush/utils@^1.1.1", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff"
integrity sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==
"@egjs/agent@^2.2.1":
version "2.4.3"
resolved "https://registry.yarnpkg.com/@egjs/agent/-/agent-2.4.3.tgz#6d44e2fb1ff7bab242c07f82732fe60305ac6f06"
integrity sha512-XvksSENe8wPeFlEVouvrOhKdx8HMniJ3by7sro2uPF3M6QqWwjzVcmvwoPtdjiX8O1lfRoLhQMp1a7NGlVTdIA==
"@egjs/children-differ@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@egjs/children-differ/-/children-differ-1.0.1.tgz#5465fa80671d5ca3564ebe912f48b05b3e8a14fd"
integrity sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ==
dependencies:
"@egjs/list-differ" "^1.0.0"
"@egjs/component@^3.0.2":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.4.tgz#ad7b53794b2a612806179a188ad828acb9525f61"
integrity sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g==
"@egjs/list-differ@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.1.tgz#5772b0f8b87973bb67827f6c7d7df8d7f64a22eb"
integrity sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg==
"@emotion/babel-plugin@^11.11.0":
version "11.11.0"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"
@@ -1990,6 +2024,28 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69"
integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==
"@scena/dragscroll@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz#220b2430c16119cd3e70044ee533a5b9a43cffd7"
integrity sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA==
dependencies:
"@daybrush/utils" "^1.6.0"
"@scena/event-emitter" "^1.0.2"
"@scena/event-emitter@^1.0.2", "@scena/event-emitter@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz#047e3acef93cf238d7ce3a8cc5a12ec6bd9c3bb1"
integrity sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg==
dependencies:
"@daybrush/utils" "^1.1.1"
"@scena/matrix@^1.0.0", "@scena/matrix@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scena/matrix/-/matrix-1.1.1.tgz#5297f71825c72e2c2c8f802f924f482ed200c43c"
integrity sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg==
dependencies:
"@daybrush/utils" "^1.4.0"
"@sentry-internal/tracing@7.63.0":
version "7.63.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7"
@@ -3462,6 +3518,21 @@ css-box-model@^1.2.0:
dependencies:
tiny-invariant "^1.0.6"
css-styled@^1.0.8, css-styled@~1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/css-styled/-/css-styled-1.0.8.tgz#c9c05dc4abdef5571033090bfb8cfc5e19429974"
integrity sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g==
dependencies:
"@daybrush/utils" "^1.13.0"
css-to-mat@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/css-to-mat/-/css-to-mat-1.1.1.tgz#0dd10dcf9ec17df15708c8ff07a74fbd0b9a3fe5"
integrity sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==
dependencies:
"@daybrush/utils" "^1.13.0"
"@scena/matrix" "^1.0.0"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -4484,6 +4555,11 @@ fraction.js@^4.2.0:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
framework-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/framework-utils/-/framework-utils-1.1.0.tgz#a3b528bce838dfd623148847dc92371b09d0da2d"
integrity sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg==
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -4539,6 +4615,14 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
gesto@^1.19.0, gesto@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.1.tgz#b2a29730663eecf77b248982bbff929e79d4a461"
integrity sha512-ofWVEdqmnpFm3AFf7aoclhoayseb3OkwSiXbXusKYu/99iN5HgeWP+SWqdghQ5TFlOgP5Zlz+6SY8mP2V0kFaQ==
dependencies:
"@daybrush/utils" "^1.13.0"
"@scena/event-emitter" "^1.0.2"
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
@@ -5244,6 +5328,21 @@ jsonpointer@^5.0.0:
object.assign "^4.1.4"
object.values "^1.1.6"
keycode@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==
keycon@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/keycon/-/keycon-1.4.0.tgz#bf2a633f3c3b659ea564045938cff33e584cebd5"
integrity sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A==
dependencies:
"@cfcs/core" "^0.0.6"
"@daybrush/utils" "^1.7.1"
"@scena/event-emitter" "^1.0.2"
keycode "^2.2.0"
kleur@^4.0.3:
version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
@@ -6081,6 +6180,13 @@ orderedmap@^2.0.0:
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
overlap-area@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2"
integrity sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw==
dependencies:
"@daybrush/utils" "^1.7.1"
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@@ -6603,6 +6709,14 @@ react-color@^2.19.3:
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
react-css-styled@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/react-css-styled/-/react-css-styled-1.1.9.tgz#a7cc948e49f72b2f7fb1393bd85416a8293afab3"
integrity sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw==
dependencies:
css-styled "~1.0.8"
framework-utils "^1.1.0"
react-datepicker@^4.8.0:
version "4.16.0"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f"
@@ -6683,6 +6797,25 @@ react-markdown@^8.0.7:
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-moveable@^0.54.1:
version "0.54.1"
resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.1.tgz#3c69748c444184700e6999501b0da953c934205e"
integrity sha512-Kj2ifw9nk3LZvu7ezhst8Z5WBPRr+yVv9oROwrBirFlHmwGHHZXUGk5Gaezu+JGqqNRsQJncVMW5Uf68KSSOvg==
dependencies:
"@daybrush/utils" "^1.13.0"
"@egjs/agent" "^2.2.1"
"@egjs/children-differ" "^1.0.1"
"@egjs/list-differ" "^1.0.0"
"@scena/dragscroll" "^1.4.0"
"@scena/event-emitter" "^1.0.5"
"@scena/matrix" "^1.1.1"
css-to-mat "^1.1.1"
framework-utils "^1.1.0"
gesto "^1.19.0"
overlap-area "^1.1.0"
react-css-styled "^1.1.9"
react-selecto "^1.25.0"
react-onclickoutside@^6.12.2:
version "6.13.0"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc"
@@ -6740,6 +6873,13 @@ react-remove-scroll@2.5.4:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-selecto@^1.25.0:
version "1.26.0"
resolved "https://registry.yarnpkg.com/react-selecto/-/react-selecto-1.26.0.tgz#9157ff0a732fc426602b30c08ec21b6ca0a9c472"
integrity sha512-aBTZEYA68uE+o8TytNjTb2GpIn4oKEv0U4LIow3cspJQlF/PdAnBwkq9UuiKVuFluu5kfLQ7Keu3S2Tihlmw0g==
dependencies:
selecto "~1.26.0"
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
@@ -7023,6 +7163,22 @@ schema-utils@^3.1.1:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
selecto@~1.26.0:
version "1.26.0"
resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.0.tgz#f3f04fb6409112b198243458f6c9963946d5ba2f"
integrity sha512-cEFKdv5rmkF6pf2OScQJllaNp4UJy/FvviB40ZaMSHrQCxC72X/Q6uhzW1tlb2RE+0danvUNJTs64cI9VXtUyg==
dependencies:
"@daybrush/utils" "^1.13.0"
"@egjs/children-differ" "^1.0.1"
"@scena/dragscroll" "^1.4.0"
"@scena/event-emitter" "^1.0.5"
css-styled "^1.0.8"
css-to-mat "^1.1.1"
framework-utils "^1.1.0"
gesto "^1.19.1"
keycon "^1.2.0"
overlap-area "^1.1.0"
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"