mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into stage/dev-release-0-13
This commit is contained in:
commit
698b42768e
@ -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
|
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?
|
## 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.
|
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.
|
||||||
|
@ -26,19 +26,19 @@ def workspace_member_props(old_props):
|
|||||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||||
},
|
},
|
||||||
"display_properties": {
|
"display_properties": {
|
||||||
"assignee": old_props.get("properties", {}).get("assignee",None),
|
"assignee": old_props.get("properties", {}).get("assignee", True),
|
||||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
"attachment_count": old_props.get("properties", {}).get("attachment_count", True),
|
||||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
"created_on": old_props.get("properties", {}).get("created_on", True),
|
||||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
"due_date": old_props.get("properties", {}).get("due_date", True),
|
||||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
"estimate": old_props.get("properties", {}).get("estimate", True),
|
||||||
"key": old_props.get("properties", {}).get("key", None),
|
"key": old_props.get("properties", {}).get("key", True),
|
||||||
"labels": old_props.get("properties", {}).get("labels", None),
|
"labels": old_props.get("properties", {}).get("labels", True),
|
||||||
"link": old_props.get("properties", {}).get("link", None),
|
"link": old_props.get("properties", {}).get("link", True),
|
||||||
"priority": old_props.get("properties", {}).get("priority", None),
|
"priority": old_props.get("properties", {}).get("priority", True),
|
||||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
"start_date": old_props.get("properties", {}).get("start_date", True),
|
||||||
"state": old_props.get("properties", {}).get("state", None),
|
"state": old_props.get("properties", {}).get("state", True),
|
||||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
|
||||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
"updated_on": old_props.get("properties", {}).get("updated_on", True),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return new_props
|
return new_props
|
||||||
|
@ -1,24 +1,42 @@
|
|||||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
def update_issue_activity(apps, schema_editor):
|
import uuid
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('db', '0044_auto_20230913_0709'),
|
("db", "0044_auto_20230913_0709"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -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),
|
|
||||||
]
|
|
26
apiserver/plane/db/migrations/0046_auto_20230926_1015.py
Normal file
26
apiserver/plane/db/migrations/0046_auto_20230926_1015.py
Normal 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),
|
||||||
|
]
|
@ -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),
|
|
||||||
]
|
|
44
apiserver/plane/db/migrations/0047_auto_20230926_1029.py
Normal file
44
apiserver/plane/db/migrations/0047_auto_20230926_1029.py
Normal 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),
|
||||||
|
]
|
@ -11,6 +11,11 @@ http {
|
|||||||
|
|
||||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
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 / {
|
location / {
|
||||||
proxy_pass http://web:3000/;
|
proxy_pass http://web:3000/;
|
||||||
}
|
}
|
||||||
@ -20,6 +25,7 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /spaces/ {
|
location /spaces/ {
|
||||||
|
rewrite ^/spaces/?$ /spaces/login break;
|
||||||
proxy_pass http://space:3000/spaces/;
|
proxy_pass http://space:3000/spaces/;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export const SignInView = observer(() => {
|
|||||||
const onSignInSuccess = (response: any) => {
|
const onSignInSuccess = (response: any) => {
|
||||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
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);
|
userStore.setCurrentUser(response?.user);
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export const SignInView = observer(() => {
|
|||||||
router.push(`/onboarding?next_path=${nextPath}`);
|
router.push(`/onboarding?next_path=${nextPath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push((nextPath ?? "/").toString());
|
router.push((nextPath ?? "/login").toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||||
|
@ -1 +1 @@
|
|||||||
export * from "./home";
|
export * from "./login";
|
||||||
|
@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
// components
|
// components
|
||||||
import { SignInView, UserLoggedIn } from "components/accounts";
|
import { SignInView, UserLoggedIn } from "components/accounts";
|
||||||
|
|
||||||
export const HomeView = observer(() => {
|
export const LoginView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
if (!userStore.currentUser) return <SignInView />;
|
if (!userStore.currentUser) return <SignInView />;
|
@ -1,8 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { HomeView } from "components/views";
|
|
||||||
|
|
||||||
const HomePage = () => <HomeView />;
|
|
||||||
|
|
||||||
export default HomePage;
|
|
8
space/pages/login/index.tsx
Normal file
8
space/pages/login/index.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { LoginView } from "components/views";
|
||||||
|
|
||||||
|
const LoginPage = () => <LoginView />;
|
||||||
|
|
||||||
|
export default LoginPage;
|
@ -93,7 +93,9 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
|
<span className="capitalize">
|
||||||
|
{replaceUnderscoreIfSnakeCase(option.type)} Layout
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
@ -318,7 +320,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
displayFilters.layout !== "spreadsheet" &&
|
displayFilters.layout !== "spreadsheet" &&
|
||||||
displayFilters.layout !== "gantt_chart" && (
|
displayFilters.layout !== "gantt_chart" && (
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<div className="w-28">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={displayFilters.show_empty_groups ?? true}
|
value={displayFilters.show_empty_groups ?? true}
|
||||||
|
@ -20,7 +20,7 @@ import { renderEmoji } from "helpers/emoji.helper";
|
|||||||
// types
|
// types
|
||||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
||||||
// fetch-keys
|
// 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
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -59,6 +59,15 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
: null
|
: 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(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug &&
|
workspaceSlug &&
|
||||||
projectId &&
|
projectId &&
|
||||||
@ -82,7 +91,10 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
title =
|
||||||
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
@ -137,7 +149,9 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.color ?? "#000000";
|
||||||
icon = (
|
icon = (
|
||||||
<span
|
<span
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
|
@ -183,7 +183,10 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
{calendarIssues ? (
|
{calendarIssues ? (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<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
|
<CalendarHeader
|
||||||
isMonthlyView={isMonthlyView}
|
isMonthlyView={isMonthlyView}
|
||||||
setIsMonthlyView={setIsMonthlyView}
|
setIsMonthlyView={setIsMonthlyView}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
@ -16,21 +19,29 @@ type Props = {
|
|||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||||
prePopulatedData?: Partial<IIssue>;
|
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 [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { moduleId, cycleId, viewId } = router.query;
|
||||||
|
|
||||||
|
const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
const { right } = ref.current.getBoundingClientRect();
|
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);
|
else setIsThereSpaceOnRight(true);
|
||||||
}, [ref]);
|
}, [ref, deps, container]);
|
||||||
|
|
||||||
return isThereSpaceOnRight;
|
return isThereSpaceOnRight;
|
||||||
};
|
};
|
||||||
@ -63,11 +74,11 @@ const InlineInput = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
|
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
|
||||||
const { isOpen } = props;
|
const { isOpen, dependencies } = props;
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref);
|
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -83,6 +83,7 @@ export const SingleCalendarDate: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
<CalendarInlineCreateIssueForm
|
<CalendarInlineCreateIssueForm
|
||||||
isOpen={isCreateIssueFormOpen}
|
isOpen={isCreateIssueFormOpen}
|
||||||
|
dependencies={[showWeekEnds]}
|
||||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||||
prePopulatedData={{
|
prePopulatedData={{
|
||||||
target_date: date.date,
|
target_date: date.date,
|
||||||
|
@ -218,11 +218,11 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isDraftIssues)
|
if (isDraftIssues)
|
||||||
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params));
|
await mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params));
|
||||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
if (displayFilters.layout === "calendar") await mutate(calendarFetchKey);
|
||||||
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
if (displayFilters.layout === "gantt_chart") await mutate(ganttFetchKey);
|
||||||
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
if (displayFilters.layout === "spreadsheet") await mutate(spreadsheetFetchKey);
|
||||||
if (groupedIssues) mutateMyIssues();
|
if (groupedIssues) await mutateMyIssues();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
@ -34,7 +34,7 @@ import {
|
|||||||
UserAuth,
|
UserAuth,
|
||||||
} from "types";
|
} from "types";
|
||||||
// fetch-keys
|
// 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
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -86,16 +86,33 @@ export const SingleList: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||||
workspaceSlug && projectId
|
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
: 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
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
workspaceSlug &&
|
||||||
workspaceSlug && projectId
|
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)
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -110,7 +127,10 @@ export const SingleList: React.FC<Props> = (props) => {
|
|||||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
title =
|
||||||
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
@ -164,7 +184,9 @@ export const SingleList: React.FC<Props> = (props) => {
|
|||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.color ?? "#000000";
|
||||||
icon = (
|
icon = (
|
||||||
<span
|
<span
|
||||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||||
|
@ -68,7 +68,11 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
readOnly={disableUserActions}
|
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">
|
<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} />
|
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
||||||
</div>
|
</div>
|
||||||
@ -89,8 +93,17 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
<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
|
<ListInlineCreateIssueForm
|
||||||
isOpen={isInlineCreateIssueFormOpen}
|
isOpen={isInlineCreateIssueFormOpen}
|
||||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||||
@ -114,7 +127,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
: !disableUserActions &&
|
: !disableUserActions &&
|
||||||
!isInlineCreateIssueFormOpen && (
|
!isInlineCreateIssueFormOpen && (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
className="sticky left-0 z-[1]"
|
className="sticky left-0 z-[1] !w-full"
|
||||||
customButton={
|
customButton={
|
||||||
<button
|
<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"
|
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"
|
||||||
@ -140,11 +153,6 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -92,6 +92,7 @@ export const GanttSidebar: React.FC<Props> = (props) => {
|
|||||||
<StrictModeDroppable droppableId="gantt-sidebar">
|
<StrictModeDroppable droppableId="gantt-sidebar">
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<div
|
<div
|
||||||
|
id={`gantt-sidebar-${cycleId}`}
|
||||||
className="h-full overflow-y-auto pl-2.5"
|
className="h-full overflow-y-auto pl-2.5"
|
||||||
ref={droppableProvided.innerRef}
|
ref={droppableProvided.innerRef}
|
||||||
{...droppableProvided.droppableProps}
|
{...droppableProvided.droppableProps}
|
||||||
@ -162,6 +163,19 @@ export const GanttSidebar: React.FC<Props> = (props) => {
|
|||||||
<GanttInlineCreateIssueForm
|
<GanttInlineCreateIssueForm
|
||||||
isOpen={isCreateIssueFormOpen}
|
isOpen={isCreateIssueFormOpen}
|
||||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
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={{
|
prePopulatedData={{
|
||||||
start_date: new Date(Date.now()).toISOString().split("T")[0],
|
start_date: new Date(Date.now()).toISOString().split("T")[0],
|
||||||
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
||||||
|
@ -234,7 +234,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
displayFilters?.layout !== "spreadsheet" && (
|
displayFilters?.layout !== "spreadsheet" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<div className="w-28">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={displayFilters?.show_empty_groups ?? true}
|
value={displayFilters?.show_empty_groups ?? true}
|
||||||
|
@ -266,7 +266,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
displayFilters?.layout !== "spreadsheet" && (
|
displayFilters?.layout !== "spreadsheet" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<div className="w-28">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={displayFilters?.show_empty_groups ?? true}
|
value={displayFilters?.show_empty_groups ?? true}
|
||||||
|
@ -44,7 +44,9 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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={() => {
|
onClick={() => {
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
document.dispatchEvent(e);
|
document.dispatchEvent(e);
|
||||||
@ -61,11 +63,17 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
|
|
||||||
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
|
{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
|
<button
|
||||||
type="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
|
<ChevronDown
|
||||||
size={16}
|
size={16}
|
||||||
@ -73,7 +81,11 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</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">
|
<div className="w-full h-full">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsDraftIssueModalOpen(true)}
|
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||||
|
@ -31,15 +31,53 @@ import { BoltOutlined, GridViewOutlined } from "@mui/icons-material";
|
|||||||
import emptyDashboard from "public/empty-state/dashboard.svg";
|
import emptyDashboard from "public/empty-state/dashboard.svg";
|
||||||
import githubBlackImage from "/public/logos/github-black.png";
|
import githubBlackImage from "/public/logos/github-black.png";
|
||||||
import githubWhiteImage from "/public/logos/github-white.png";
|
import githubWhiteImage from "/public/logos/github-white.png";
|
||||||
// helpers
|
|
||||||
import { render12HourFormatTime, renderShortDate } from "helpers/date-time.helper";
|
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CURRENT_USER, USER_WORKSPACE_DASHBOARD } from "constants/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 WorkspacePage: NextPage = () => {
|
||||||
const [month, setMonth] = useState(new Date().getMonth() + 1);
|
const [month, setMonth] = useState(new Date().getMonth() + 1);
|
||||||
@ -58,10 +96,6 @@ const WorkspacePage: NextPage = () => {
|
|||||||
workspaceSlug ? () => userService.userWorkspaceDashboard(workspaceSlug as string, month) : null
|
workspaceSlug ? () => userService.userWorkspaceDashboard(workspaceSlug as string, month) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
const greeting =
|
|
||||||
today.getHours() < 12 ? "morning" : today.getHours() < 18 ? "afternoon" : "evening";
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
@ -128,17 +162,7 @@ const WorkspacePage: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="p-8 space-y-8">
|
<div className="p-8 space-y-8">
|
||||||
<div>
|
<Greeting user={user} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{projects ? (
|
{projects ? (
|
||||||
projects.length > 0 ? (
|
projects.length > 0 ? (
|
||||||
|
@ -189,7 +189,7 @@ const SingleCycle: React.FC = () => {
|
|||||||
{cycleStatus === "completed" && (
|
{cycleStatus === "completed" && (
|
||||||
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
|
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
|
||||||
)}
|
)}
|
||||||
<div className="relative overflow-y-auto">
|
<div className="relative overflow-y-auto w-full h-full">
|
||||||
<IssuesView
|
<IssuesView
|
||||||
openIssuesListModal={openIssuesListModal}
|
openIssuesListModal={openIssuesListModal}
|
||||||
disableUserActions={cycleStatus === "completed" ?? false}
|
disableUserActions={cycleStatus === "completed" ?? false}
|
||||||
|
Loading…
Reference in New Issue
Block a user