Merge branch 'develop' of github.com:makeplane/plane into stage/dev-release-0-13

This commit is contained in:
sriramveeraghanta 2023-09-26 19:49:44 +05:30
commit 698b42768e
27 changed files with 407 additions and 240 deletions

View File

@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
- 3rd-party libraries being used and their versions
- a use-case that fails
- 3rd-party libraries being used and their versions
- a use-case that fails
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
### Requirements
- Node.js version v16.18.0
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
- Node.js version v16.18.0
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
### Setup the project
@ -30,6 +30,48 @@ The project is a monorepo, with backend api and frontend in a single repo.
The backend is a django project which is kept inside apiserver
1. Clone the repo
```bash
git clone https://github.com/makeplane/plane
cd plane
chmod +x setup.sh
```
2. Run setup.sh
```bash
./setup.sh
```
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
```
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
```
4. Run Docker compose up
```bash
docker compose up -d
```
5. Install dependencies
```bash
yarn install
```
6. Run the web app in development mode
```bash
yarn dev
```
## Missing a Feature?
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
@ -39,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Need help? Questions and suggestions
@ -48,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in
## Ways to contribute
- Try Plane Cloud and the self hosting platform and give feedback
- Add new integrations
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
- Share your thoughts and suggestions with us
- Help create tutorials and blog posts
- Request a feature by submitting a proposal
- Report a bug
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
- Try Plane Cloud and the self hosting platform and give feedback
- Add new integrations
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
- Share your thoughts and suggestions with us
- Help create tutorials and blog posts
- Request a feature by submitting a proposal
- Report a bug
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.

View File

@ -26,19 +26,19 @@ def workspace_member_props(old_props):
"calendar_date_range": old_props.get("calendarDateRange", ""),
},
"display_properties": {
"assignee": old_props.get("properties", {}).get("assignee",None),
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
"created_on": old_props.get("properties", {}).get("created_on", None),
"due_date": old_props.get("properties", {}).get("due_date", None),
"estimate": old_props.get("properties", {}).get("estimate", None),
"key": old_props.get("properties", {}).get("key", None),
"labels": old_props.get("properties", {}).get("labels", None),
"link": old_props.get("properties", {}).get("link", None),
"priority": old_props.get("properties", {}).get("priority", None),
"start_date": old_props.get("properties", {}).get("start_date", None),
"state": old_props.get("properties", {}).get("state", None),
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
"updated_on": old_props.get("properties", {}).get("updated_on", None),
"assignee": old_props.get("properties", {}).get("assignee", True),
"attachment_count": old_props.get("properties", {}).get("attachment_count", True),
"created_on": old_props.get("properties", {}).get("created_on", True),
"due_date": old_props.get("properties", {}).get("due_date", True),
"estimate": old_props.get("properties", {}).get("estimate", True),
"key": old_props.get("properties", {}).get("key", True),
"labels": old_props.get("properties", {}).get("labels", True),
"link": old_props.get("properties", {}).get("link", True),
"priority": old_props.get("properties", {}).get("priority", True),
"start_date": old_props.get("properties", {}).get("start_date", True),
"state": old_props.get("properties", {}).get("state", True),
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
"updated_on": old_props.get("properties", {}).get("updated_on", True),
},
}
return new_props

View File

@ -1,24 +1,42 @@
# Generated by Django 4.2.3 on 2023-09-15 06:55
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivityModel = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivityModel.objects.all():
if obj.field == "blocks":
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0044_auto_20230913_0709'),
("db", "0044_auto_20230913_0709"),
]
operations = [
migrations.RunPython(update_issue_activity),
migrations.CreateModel(
name="GlobalView",
fields=[
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At"),),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Last Modified At"),),
("id", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True,),),
("name", models.CharField(max_length=255, verbose_name="View Name")),
("description", models.TextField(blank=True, verbose_name="View Description"),),
("query", models.JSONField(verbose_name="View Query")),
("access", models.PositiveSmallIntegerField(choices=[(0, "Private"), (1, "Public")], default=1),),
("query_data", models.JSONField(default=dict)),
("created_by", models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)s_created_by", to=settings.AUTH_USER_MODEL, verbose_name="Created By",),),
("updated_by", models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)s_updated_by", to=settings.AUTH_USER_MODEL, verbose_name="Last Modified By",),),
("workspace", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="global_views", to="db.workspace",),),
],
options={
"verbose_name": "Global View",
"verbose_name_plural": "Global Views",
"db_table": "global_views",
"ordering": ("-created_at",),
},
),
migrations.AddField(
model_name="issueactivity",
name="epoch",
field=models.FloatField(null=True),
),
]

View File

@ -1,53 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-19 14:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
def update_epoch(apps, schema_editor):
IssueActivity = apps.get_model('db', 'IssueActivity')
updated_issue_activity = []
for obj in IssueActivity.objects.all():
obj.epoch = int(obj.created_at.timestamp())
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0045_auto_20230915_0655'),
]
operations = [
migrations.CreateModel(
name='GlobalView',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255, verbose_name='View Name')),
('description', models.TextField(blank=True, verbose_name='View Description')),
('query', models.JSONField(verbose_name='View Query')),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
('query_data', models.JSONField(default=dict)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
],
options={
'verbose_name': 'Global View',
'verbose_name_plural': 'Global Views',
'db_table': 'global_views',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='issueactivity',
name='epoch',
field=models.FloatField(null=True),
),
migrations.RunPython(update_epoch),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.5 on 2023-09-26 10:15
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.all():
obj.epoch = int(obj.created_at.timestamp())
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity,
["epoch"],
batch_size=5000,
)
class Migration(migrations.Migration):
dependencies = [
('db', '0045_auto_20230915_0655'),
]
operations = [
migrations.RunPython(update_issue_activity),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-21 07:58
from django.db import migrations
def update_priority_history(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.all():
if obj.field == "priority":
obj.new_value = obj.new_value or "none"
obj.old_value = obj.old_value or "none"
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity, ["new_value", "old_value"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
("db", "0046_auto_20230919_1421"),
]
operations = [
migrations.RunPython(update_priority_history),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 4.2.5 on 2023-09-26 10:29
from django.db import migrations
def update_issue_activity_priority(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.filter(field="priority"):
# Set the old and new value to none if it is empty for Priority
obj.new_value = obj.new_value or "none"
obj.old_value = obj.old_value or "none"
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity,
["new_value", "old_value"],
batch_size=1000,
)
def update_issue_activity_blocked(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.filter(field="blocks"):
# Set the field to blocked_by
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity,
["field"],
batch_size=1000,
)
class Migration(migrations.Migration):
dependencies = [
('db', '0046_auto_20230926_1015'),
]
operations = [
migrations.RunPython(update_issue_activity_priority),
migrations.RunPython(update_issue_activity_blocked),
]

View File

@ -11,6 +11,11 @@ http {
client_max_body_size ${FILE_SIZE_LIMIT};
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://web:3000/;
}
@ -20,6 +25,7 @@ http {
}
location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_pass http://space:3000/spaces/;
}
@ -27,4 +33,4 @@ http {
proxy_pass http://plane-minio:9000/uploads/;
}
}
}
}

View File

@ -33,7 +33,7 @@ export const SignInView = observer(() => {
const onSignInSuccess = (response: any) => {
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/";
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login";
userStore.setCurrentUser(response?.user);
@ -41,7 +41,7 @@ export const SignInView = observer(() => {
router.push(`/onboarding?next_path=${nextPath}`);
return;
}
router.push((nextPath ?? "/").toString());
router.push((nextPath ?? "/login").toString());
};
const handleGoogleSignIn = async ({ clientId, credential }: any) => {

View File

@ -1 +1 @@
export * from "./home";
export * from "./login";

View File

@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components
import { SignInView, UserLoggedIn } from "components/accounts";
export const HomeView = observer(() => {
export const LoginView = observer(() => {
const { user: userStore } = useMobxStore();
if (!userStore.currentUser) return <SignInView />;

View File

@ -1,8 +0,0 @@
import React from "react";
// components
import { HomeView } from "components/views";
const HomePage = () => <HomeView />;
export default HomePage;

View File

@ -0,0 +1,8 @@
import React from "react";
// components
import { LoginView } from "components/views";
const LoginPage = () => <LoginView />;
export default LoginPage;

View File

@ -93,7 +93,9 @@ export const IssuesFilterView: React.FC = () => {
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
<span className="capitalize">
{replaceUnderscoreIfSnakeCase(option.type)} Layout
</span>
}
position="bottom"
>
@ -318,7 +320,7 @@ export const IssuesFilterView: React.FC = () => {
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<h4 className="text-custom-text-200">Show empty groups</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters.show_empty_groups ?? true}

View File

@ -20,7 +20,7 @@ import { renderEmoji } from "helpers/emoji.helper";
// types
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@ -59,6 +59,15 @@ export const BoardHeader: React.FC<Props> = ({
: null
);
const { data: workspaceLabels } = useSWR(
workspaceSlug && displayFilters?.group_by === "labels"
? WORKSPACE_LABELS(workspaceSlug.toString())
: null,
workspaceSlug && displayFilters?.group_by === "labels"
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
: null
);
const { data: members } = useSWR(
workspaceSlug &&
projectId &&
@ -82,7 +91,10 @@ export const BoardHeader: React.FC<Props> = ({
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
case "labels":
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
title =
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
(label) => label.id === groupTitle
)?.name ?? "None";
break;
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
@ -137,7 +149,9 @@ export const BoardHeader: React.FC<Props> = ({
break;
case "labels":
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
(label) => label.id === groupTitle
)?.color ?? "#000000";
icon = (
<span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"

View File

@ -183,7 +183,10 @@ export const CalendarView: React.FC<Props> = ({
{calendarIssues ? (
<div className="h-full overflow-y-auto">
<DragDropContext onDragEnd={onDragEnd}>
<div className="h-full rounded-lg p-8 text-custom-text-200">
<div
id={`calendar-view-${cycleId ?? moduleId ?? viewId}`}
className="h-full rounded-lg p-8 text-custom-text-200"
>
<CalendarHeader
isMonthlyView={isMonthlyView}
setIsMonthlyView={setIsMonthlyView}

View File

@ -1,5 +1,8 @@
import { useEffect, useRef, useState } from "react";
// next
import { useRouter } from "next/router";
// react hook form
import { useFormContext } from "react-hook-form";
@ -16,21 +19,29 @@ type Props = {
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
dependencies: any[];
};
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>) => {
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>, deps: any[]) => {
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
const router = useRouter();
const { moduleId, cycleId, viewId } = router.query;
const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`);
useEffect(() => {
if (!ref.current) return;
const { right } = ref.current.getBoundingClientRect();
const width = right + 250;
const width = right;
if (width > window.innerWidth) setIsThereSpaceOnRight(false);
const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth;
if (width > innerWidth) setIsThereSpaceOnRight(false);
else setIsThereSpaceOnRight(true);
}, [ref]);
}, [ref, deps, container]);
return isThereSpaceOnRight;
};
@ -63,11 +74,11 @@ const InlineInput = () => {
};
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
const { isOpen } = props;
const { isOpen, dependencies } = props;
const ref = useRef<HTMLDivElement>(null);
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref);
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies);
return (
<>

View File

@ -83,6 +83,7 @@ export const SingleCalendarDate: React.FC<Props> = (props) => {
<CalendarInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
dependencies={[showWeekEnds]}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
target_date: date.date,

View File

@ -218,11 +218,11 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
);
if (isDraftIssues)
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params));
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
await mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params));
if (displayFilters.layout === "calendar") await mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart") await mutate(ganttFetchKey);
if (displayFilters.layout === "spreadsheet") await mutate(spreadsheetFetchKey);
if (groupedIssues) await mutateMyIssues();
setToastAlert({
type: "success",

View File

@ -34,7 +34,7 @@ import {
UserAuth,
} from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@ -86,16 +86,33 @@ export const SingleList: React.FC<Props> = (props) => {
const { displayFilters, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
const { data: issueLabels } = useSWR(
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: workspaceLabels } = useSWR(
workspaceSlug && displayFilters?.group_by === "labels"
? WORKSPACE_LABELS(workspaceSlug.toString())
: null,
workspaceSlug && displayFilters?.group_by === "labels"
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
: null
);
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? PROJECT_MEMBERS(projectId as string)
: null,
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
@ -110,7 +127,10 @@ export const SingleList: React.FC<Props> = (props) => {
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
case "labels":
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
title =
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
(label) => label.id === groupTitle
)?.name ?? "None";
break;
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
@ -164,7 +184,9 @@ export const SingleList: React.FC<Props> = (props) => {
break;
case "labels":
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
(label) => label.id === groupTitle
)?.color ?? "#000000";
icon = (
<span
className="h-3 w-3 flex-shrink-0 rounded-full"

View File

@ -68,7 +68,11 @@ export const SpreadsheetView: React.FC<Props> = ({
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
<div
className={`h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100 ${
isInlineCreateIssueFormOpen ? "mb-24" : "mb-12"
}`}
>
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
</div>
@ -89,62 +93,66 @@ export const SpreadsheetView: React.FC<Props> = ({
userAuth={userAuth}
/>
))}
<div className="absolute bottom-0 left-0 z-10 group pb-2 hover:rounded-sm bg-custom-background-100 hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max">
<ListInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>
{type === "issue"
? !disableUserActions &&
!isInlineCreateIssueFormOpen && (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 border-custom-border-200 w-full"
onClick={() => setIsInlineCreateIssueFormOpen(true)}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
)
: !disableUserActions &&
!isInlineCreateIssueFormOpen && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
verticalPosition="top"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</div>
) : (
<Spinner />
)}
</div>
<div
className={`absolute bottom-0 left-0 z-10 group hover:rounded-sm bg-custom-background-100 hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max ${
isInlineCreateIssueFormOpen ? "pb-2" : ""
}`}
>
<ListInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>
{type === "issue"
? !disableUserActions &&
!isInlineCreateIssueFormOpen && (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 border-custom-border-200 w-full"
onClick={() => setIsInlineCreateIssueFormOpen(true)}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
)
: !disableUserActions &&
!isInlineCreateIssueFormOpen && (
<CustomMenu
className="sticky left-0 z-[1] !w-full"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
verticalPosition="top"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</>
);
};

View File

@ -92,6 +92,7 @@ export const GanttSidebar: React.FC<Props> = (props) => {
<StrictModeDroppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
@ -162,6 +163,19 @@ export const GanttSidebar: React.FC<Props> = (props) => {
<GanttInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
handleClose={() => setIsCreateIssueFormOpen(false)}
onSuccess={() => {
const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`);
const timeoutId = setTimeout(() => {
if (ganttSidebar)
ganttSidebar.scrollBy({
top: ganttSidebar.scrollHeight,
left: 0,
behavior: "smooth",
});
clearTimeout(timeoutId);
}, 10);
}}
prePopulatedData={{
start_date: new Date(Date.now()).toISOString().split("T")[0],
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],

View File

@ -234,7 +234,7 @@ export const MyIssuesViewOptions: React.FC = () => {
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<h4 className="text-custom-text-200">Show empty groups</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}

View File

@ -266,7 +266,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<h4 className="text-custom-text-200">Show empty groups</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}

View File

@ -44,7 +44,9 @@ export const WorkspaceSidebarQuickAction = () => {
>
<button
type="button"
className="relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
className={`relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 ${
store?.theme?.sidebarCollapsed ? "justify-center" : ""
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
@ -61,11 +63,17 @@ export const WorkspaceSidebarQuickAction = () => {
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
<>
<div className="h-8 w-0.5 bg-custom-sidebar-background-80" />
<div
className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${
store?.theme?.sidebarCollapsed ? "hidden" : "block"
}`}
/>
<button
type="button"
className="flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5"
className={`flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5 ${
store?.theme?.sidebarCollapsed ? "hidden" : "block"
}`}
>
<ChevronDown
size={16}
@ -73,7 +81,11 @@ export const WorkspaceSidebarQuickAction = () => {
/>
</button>
<div className="absolute w-full h-10 pt-2 top-full left-0 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto">
<div
className={`fixed h-10 pt-2 w-[203px] left-4 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto ${
store?.theme?.sidebarCollapsed ? "top-[5.5rem]" : "top-24"
}`}
>
<div className="w-full h-full">
<button
onClick={() => setIsDraftIssueModalOpen(true)}

View File

@ -31,15 +31,53 @@ import { BoltOutlined, GridViewOutlined } from "@mui/icons-material";
import emptyDashboard from "public/empty-state/dashboard.svg";
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// helpers
import { render12HourFormatTime, renderShortDate } from "helpers/date-time.helper";
// types
import { ICurrentUserResponse } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys";
// constants
import { DAYS } from "constants/project";
const Greeting = ({ user }: { user: ICurrentUserResponse | undefined }) => {
const currentTime = new Date();
const hour = new Intl.DateTimeFormat("en-US", {
hour12: false,
hour: "numeric",
}).format(currentTime);
const date = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
}).format(currentTime);
const weekDay = new Intl.DateTimeFormat("en-US", {
weekday: "long",
}).format(currentTime);
const timeString = new Intl.DateTimeFormat("en-US", {
timeZone: user?.user_timezone,
hour12: false, // Use 24-hour format
hour: "2-digit",
minute: "2-digit",
}).format(currentTime);
const greeting =
parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
return (
<div>
<h3 className="text-2xl font-semibold">
Good {greeting}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="text-custom-text-400 font-medium flex items-center gap-2">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{weekDay}, {date} {timeString}
</div>
</h6>
</div>
);
};
const WorkspacePage: NextPage = () => {
const [month, setMonth] = useState(new Date().getMonth() + 1);
@ -58,10 +96,6 @@ const WorkspacePage: NextPage = () => {
workspaceSlug ? () => userService.userWorkspaceDashboard(workspaceSlug as string, month) : null
);
const today = new Date();
const greeting =
today.getHours() < 12 ? "morning" : today.getHours() < 18 ? "afternoon" : "evening";
useEffect(() => {
if (!workspaceSlug) return;
@ -128,17 +162,7 @@ const WorkspacePage: NextPage = () => {
</div>
)}
<div className="p-8 space-y-8">
<div>
<h3 className="text-2xl font-semibold">
Good {greeting}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="text-custom-text-400 font-medium flex items-center gap-2">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)}
</div>
</h6>
</div>
<Greeting user={user} />
{projects ? (
projects.length > 0 ? (

View File

@ -189,7 +189,7 @@ const SingleCycle: React.FC = () => {
{cycleStatus === "completed" && (
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
)}
<div className="relative overflow-y-auto">
<div className="relative overflow-y-auto w-full h-full">
<IssuesView
openIssuesListModal={openIssuesListModal}
disableUserActions={cycleStatus === "completed" ?? false}