Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f397710ce | ||
|
|
e6bd6b6a8c | ||
|
|
2d1406953e | ||
|
|
a8fdd42cb9 | ||
|
|
9d3952006b | ||
|
|
b75473a684 | ||
|
|
561fb9815b | ||
|
|
2cc67f6498 | ||
|
|
2f5bd58c61 | ||
|
|
e833fccf61 | ||
|
|
eee6658cc2 | ||
|
|
62ba9abdb4 | ||
|
|
46b138eb0b | ||
|
|
68b438ab1a | ||
|
|
59bdf222f5 | ||
|
|
eb50ade5e3 | ||
|
|
b406a70e72 | ||
|
|
85a08e4abd | ||
|
|
aa2e1697b0 | ||
|
|
b02417120b | ||
|
|
d040394826 | ||
|
|
f7682c57ba | ||
|
|
3beab9de6f | ||
|
|
9bb6254515 | ||
|
|
ae052f1890 | ||
|
|
cfc7049343 | ||
|
|
41e55dff85 | ||
|
|
0bccb63a9f | ||
|
|
2eb956e97e | ||
|
|
d470adf262 | ||
|
|
cebc8bdc8d | ||
|
|
64b5ba196f | ||
|
|
13d21e752d | ||
|
|
8d5018318d | ||
|
|
0fbdc0b157 | ||
|
|
2f39181eb7 | ||
|
|
d825dc5579 | ||
|
|
1f8117c987 | ||
|
|
9ff8994c0e | ||
|
|
3488001197 | ||
|
|
2ced7e4911 | ||
|
|
e5a3bec28c | ||
|
|
e930f8cc7b |
+10
-3
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
*[
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,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,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,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> {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# cp ./.env.example ./.env
|
||||
cp ./.env.example ./.env
|
||||
|
||||
# Export for tr error in mac
|
||||
export LC_ALL=C
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user