mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
63 Commits
improve/an
...
fix/table-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ca8448350 | ||
|
|
1e40f75770 | ||
|
|
27fc28b7dc | ||
|
|
0082eff4a0 | ||
|
|
3557bc024b | ||
|
|
7c86fbc554 | ||
|
|
57c25c9a5a | ||
|
|
c593d5df1b | ||
|
|
9065b5d368 | ||
|
|
a9e2e21641 | ||
|
|
e175d50ab7 | ||
|
|
75b8e3350a | ||
|
|
6e1cd4194a | ||
|
|
615ccf9459 | ||
|
|
13362590b6 | ||
|
|
7833ca7bea | ||
|
|
a1d27a1bf0 | ||
|
|
8fbd4a059b | ||
|
|
e751686683 | ||
|
|
8ee5ba96ce | ||
|
|
9e8885df5f | ||
|
|
bc48010377 | ||
|
|
9fde539b1d | ||
|
|
ec26bf6e68 | ||
|
|
e9ef3fb32a | ||
|
|
ee2c7c5fa1 | ||
|
|
d64ae9a2e4 | ||
|
|
f58a00a4ab | ||
|
|
a3e5284f71 | ||
|
|
1c06c3f43e | ||
|
|
da1496fe65 | ||
|
|
3d489e186f | ||
|
|
57d5ff7646 | ||
|
|
3c9926d383 | ||
|
|
ece4d5b1ed | ||
|
|
73eed69aa6 | ||
|
|
09603cf189 | ||
|
|
23e53df3ad | ||
|
|
57594aac4e | ||
|
|
8b884ab681 | ||
|
|
08e5f2b156 | ||
|
|
cb3a73e515 | ||
|
|
eef9edff24 | ||
|
|
cb2a7d0930 | ||
|
|
c38e048ce8 | ||
|
|
94b72effbf | ||
|
|
eccb1f5d10 | ||
|
|
a71491ecb9 | ||
|
|
455c2cc787 | ||
|
|
44dc602ac3 | ||
|
|
81f6557908 | ||
|
|
2f10f35191 | ||
|
|
cf64c7bbc6 | ||
|
|
9dd8c8ba14 | ||
|
|
bb4bee00cb | ||
|
|
d8f1404462 | ||
|
|
927ab50ac6 | ||
|
|
d98b688342 | ||
|
|
ce21630388 | ||
|
|
0927fa150c | ||
|
|
eec411baaf | ||
|
|
ecc8fbd79b | ||
|
|
c9b628e578 |
@@ -8,10 +8,16 @@ from plane.app.views import (
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
ActiveCycleEndpoint
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/active-cycles/",
|
||||
ActiveCycleEndpoint.as_view(),
|
||||
name="workspace-active-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
|
||||
CycleViewSet.as_view(
|
||||
|
||||
@@ -62,6 +62,7 @@ from .cycle import (
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
ActiveCycleEndpoint,
|
||||
)
|
||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||
from .issue import (
|
||||
|
||||
@@ -909,3 +909,235 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
)
|
||||
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ActiveCycleEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceUserPermission,
|
||||
]
|
||||
def get(self, request, slug):
|
||||
subquery = CycleFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
cycle_id=OuterRef("pk"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
active_cycles = (
|
||||
Cycle.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=self.request.user,
|
||||
start_date__lte=timezone.now(),
|
||||
end_date__gte=timezone.now(),
|
||||
)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("owned_by")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cancelled_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
unstarted_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
then=Value("DRAFT"),
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__labels",
|
||||
queryset=Label.objects.only("name", "color", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
cycles = CycleSerializer(active_cycles, many=True).data
|
||||
|
||||
for cycle in cycles:
|
||||
assignee_distribution = (
|
||||
Issue.objects.filter(
|
||||
issue_cycle__cycle_id=cycle["id"],
|
||||
project_id=cycle["project"],
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("display_name")
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
Issue.objects.filter(
|
||||
issue_cycle__cycle_id=cycle["id"],
|
||||
project_id=cycle["project"],
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.annotate(label_name=F("labels__name"))
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("label_name")
|
||||
)
|
||||
cycle["distribution"] = {
|
||||
"assignees": assignee_distribution,
|
||||
"labels": label_distribution,
|
||||
"completion_chart": {},
|
||||
}
|
||||
if cycle["start_date"] and cycle["end_date"]:
|
||||
cycle["distribution"][
|
||||
"completion_chart"
|
||||
] = burndown_plot(
|
||||
queryset=active_cycles.get(pk=cycle["id"]),
|
||||
slug=slug,
|
||||
project_id=cycle["project"],
|
||||
cycle_id=cycle["id"],
|
||||
)
|
||||
|
||||
return Response(cycles, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -73,8 +73,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
||||
else editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
||||
if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3 }).run();
|
||||
else editor.chain().focus().insertTable({ rows: 3, cols: 3 }).run();
|
||||
};
|
||||
|
||||
export const unsetLinkEditor = (editor: Editor) => {
|
||||
|
||||
@@ -170,68 +170,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
}
|
||||
}
|
||||
|
||||
#editor-container {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin: 0.5em 0 0.5em 0;
|
||||
|
||||
border: 1px solid rgb(var(--color-border-200));
|
||||
width: 100%;
|
||||
|
||||
td,
|
||||
th {
|
||||
min-width: 1em;
|
||||
border: 1px solid rgb(var(--color-border-200));
|
||||
padding: 10px 15px;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background-color: rgb(var(--color-primary-100));
|
||||
}
|
||||
|
||||
td:hover {
|
||||
background-color: rgba(var(--color-primary-300), 0.1);
|
||||
}
|
||||
|
||||
.selectedCell:after {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(var(--color-primary-300), 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.column-resize-handle {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 0;
|
||||
bottom: -2px;
|
||||
width: 2px;
|
||||
background-color: rgb(var(--color-primary-400));
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tableWrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin: 0;
|
||||
margin-bottom: 3rem;
|
||||
border: 1px solid rgba(var(--color-border-200));
|
||||
margin-bottom: 1rem;
|
||||
border: 2px solid rgba(var(--color-border-200));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -118,9 +118,7 @@
|
||||
background-size: 1.25rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition:
|
||||
transform ease-out 100ms,
|
||||
background-color ease-out 100ms;
|
||||
transition: transform ease-out 100ms, background-color ease-out 100ms;
|
||||
outline: none;
|
||||
box-shadow: #000 0px 2px 4px;
|
||||
cursor: pointer;
|
||||
@@ -133,13 +131,12 @@
|
||||
background-size: 1.25rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition:
|
||||
transform ease-out 100ms,
|
||||
background-color ease-out 100ms;
|
||||
transition: transform ease-out 100ms, background-color ease-out 100ms;
|
||||
outline: none;
|
||||
box-shadow: #000 0px 2px 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tableWrapper .tableControls .tableToolbox,
|
||||
.tableWrapper .tableControls .tableColorPickerToolbox {
|
||||
border: 1px solid rgba(var(--color-border-300));
|
||||
|
||||
@@ -12,6 +12,7 @@ export const CustomQuoteExtension = Blockquote.extend({
|
||||
if (parent.type.name !== "blockquote") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($from.pos !== $to.pos) return false;
|
||||
// if ($head.parentOffset < $head.parent.content.size) return false;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const TableCell = Node.create<TableCellOptions>({
|
||||
};
|
||||
},
|
||||
|
||||
content: "paragraph+",
|
||||
content: "block+",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,14 @@ export const TableRow = Node.create<TableRowOptions>({
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
backgroundColor: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
content: "(tableCell | tableHeader)*",
|
||||
|
||||
tableRole: "row",
|
||||
@@ -21,7 +29,17 @@ export const TableRow = Node.create<TableRowOptions>({
|
||||
return [{ tag: "tr" }];
|
||||
},
|
||||
|
||||
// renderHTML({ HTMLAttributes }) {
|
||||
// return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
// },
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
// Check if backgroundColor attribute is set and create a style string accordingly
|
||||
const style = HTMLAttributes.backgroundColor ? `background-color: ${HTMLAttributes.backgroundColor};` : "";
|
||||
|
||||
// Merge any existing HTMLAttributes with the style for backgroundColor
|
||||
const attributes = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style });
|
||||
|
||||
return ["tr", attributes, 0];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ export const icons = {
|
||||
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" length="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path fill="rgb(var(--color-text-300))" d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414 16.586 13H5.414L11 7.414z"></path></svg>`,
|
||||
deleteColumn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z"/></svg>`,
|
||||
deleteRow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M20 5c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1 .628.835 1 1.874 1 3 0 2.761-2.239 5-5 5s-5-2.239-5-5c0-1.126.372-2.165 1-3H4c-.552 0-1-.448-1-1V6c0-.552.448-1 1-1h16zm-7 10v2h6v-2h-6zm6-8H5v4h14V7z"/></svg>`,
|
||||
toggleColumnHeader: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgb(var(--color-text-300))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-toggle-right"><rect width="20" height="12" x="2" y="6" rx="6" ry="6"/><circle cx="16" cy="12" r="2"/></svg>`,
|
||||
toggleRowHeader: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgb(var(--color-text-300))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-toggle-right"><rect width="20" height="12" x="2" y="6" rx="6" ry="6"/><circle cx="16" cy="12" r="2"/></svg>`,
|
||||
insertLeftTableIcon: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
length={24}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { h } from "jsx-dom-cjs";
|
||||
import { Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model";
|
||||
// import { setCellAttr } from "prosemirror-tables";
|
||||
import { Decoration, NodeView } from "@tiptap/pm/view";
|
||||
import tippy, { Instance, Props } from "tippy.js";
|
||||
|
||||
@@ -95,6 +96,11 @@ function setCellsBackgroundColor(editor: Editor, backgroundColor: string) {
|
||||
}
|
||||
|
||||
const columnsToolboxItems: ToolboxItem[] = [
|
||||
{
|
||||
label: "Toggle Column Header",
|
||||
icon: icons.toggleColumnHeader,
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().toggleHeaderColumn().run(),
|
||||
},
|
||||
{
|
||||
label: "Add Column Before",
|
||||
icon: icons.insertLeftTableIcon,
|
||||
@@ -133,7 +139,46 @@ const columnsToolboxItems: ToolboxItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function setTableRowBackgroundColor(editor: Editor, backgroundColor: string) {
|
||||
const { state, dispatch } = editor.view;
|
||||
const { selection } = state;
|
||||
if (!(selection instanceof CellSelection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the position of the hovered cell in the selection to determine the row.
|
||||
const hoveredCell = selection.$headCell || selection.$anchorCell;
|
||||
|
||||
// Find the depth of the table row node
|
||||
let rowDepth = hoveredCell.depth;
|
||||
while (rowDepth > 0 && hoveredCell.node(rowDepth).type.name !== "tableRow") {
|
||||
rowDepth--;
|
||||
}
|
||||
|
||||
// If we couldn't find a tableRow node, we can't set the background color
|
||||
if (hoveredCell.node(rowDepth).type.name !== "tableRow") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the position where the table row starts
|
||||
const rowStartPos = hoveredCell.start(rowDepth);
|
||||
|
||||
// Create a transaction that sets the background color on the tableRow node.
|
||||
const tr = state.tr.setNodeMarkup(rowStartPos - 1, null, {
|
||||
...hoveredCell.node(rowDepth).attrs,
|
||||
backgroundColor: backgroundColor,
|
||||
});
|
||||
|
||||
dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
const rowsToolboxItems: ToolboxItem[] = [
|
||||
{
|
||||
label: "Toggle Row Header",
|
||||
icon: icons.toggleRowHeader,
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().toggleHeaderRow().run(),
|
||||
},
|
||||
{
|
||||
label: "Add Row Above",
|
||||
icon: icons.insertTopTableIcon,
|
||||
@@ -161,7 +206,7 @@ const rowsToolboxItems: ToolboxItem[] = [
|
||||
tippyOptions: {
|
||||
appendTo: controlsContainer,
|
||||
},
|
||||
onSelectColor: (color) => setCellsBackgroundColor(editor, color),
|
||||
onSelectColor: (color) => setTableRowBackgroundColor(editor, color),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -107,7 +107,7 @@ export const Table = Node.create({
|
||||
addCommands() {
|
||||
return {
|
||||
insertTable:
|
||||
({ rows = 3, cols = 3, withHeaderRow = true } = {}) =>
|
||||
({ rows = 3, cols = 3, withHeaderRow = false } = {}) =>
|
||||
({ tr, dispatch, editor }) => {
|
||||
const node = createTable(editor.schema, rows, cols, withHeaderRow);
|
||||
|
||||
|
||||
@@ -42,15 +42,6 @@ export function CoreEditorProps(
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view, event, _slice, moved) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const selection: any = window?.getSelection();
|
||||
if (selection.rangeCount !== 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (findTableAncestor(range.startContainer)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
@@ -52,6 +52,23 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
|
||||
const { getFloatingProps } = useInteractions([dismiss]);
|
||||
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [coordinates, setCoordinates] = useState<{ x: number; y: number }>();
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
middleware: [flip(), shift(), hide({ strategy: "referenceHidden" })],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const dismiss = useDismiss(context, {
|
||||
ancestorScroll: true,
|
||||
});
|
||||
|
||||
const { getFloatingProps } = useInteractions([dismiss]);
|
||||
|
||||
const handlePageTitleChange = (title: string) => {
|
||||
setPagetitle(title);
|
||||
updatePageTitle(title);
|
||||
|
||||
@@ -48,34 +48,13 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
function getComplexItems(): BubbleMenuItem[] {
|
||||
const items: BubbleMenuItem[] = [TableItem(editor)];
|
||||
|
||||
if (shouldShowImageItem()) {
|
||||
items.push(ImageItem(editor, uploadFile, setIsSubmitting));
|
||||
}
|
||||
items.push(ImageItem(editor, uploadFile, setIsSubmitting));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const complexItems: BubbleMenuItem[] = getComplexItems();
|
||||
|
||||
function shouldShowImageItem(): boolean {
|
||||
if (typeof window !== "undefined") {
|
||||
const selectionRange: any = window?.getSelection();
|
||||
const { selection } = props.editor.state;
|
||||
|
||||
if (selectionRange.rangeCount !== 0) {
|
||||
const range = selectionRange.getRangeAt(0);
|
||||
if (findTableAncestor(range.startContainer)) {
|
||||
return false;
|
||||
}
|
||||
if (isCellSelection(selection)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center divide-x divide-custom-border-200">
|
||||
<div className="flex items-center gap-0.5 pr-2">
|
||||
|
||||
@@ -59,35 +59,12 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
|
||||
function getComplexItems(): BubbleMenuItem[] {
|
||||
const items: BubbleMenuItem[] = [TableItem(props.editor)];
|
||||
|
||||
if (shouldShowImageItem()) {
|
||||
items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting));
|
||||
}
|
||||
|
||||
items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting));
|
||||
return items;
|
||||
}
|
||||
|
||||
const complexItems: BubbleMenuItem[] = getComplexItems();
|
||||
|
||||
function shouldShowImageItem(): boolean {
|
||||
if (typeof window !== "undefined") {
|
||||
const selectionRange: any = window?.getSelection();
|
||||
const { selection } = props.editor.state;
|
||||
|
||||
if (selectionRange.rangeCount !== 0) {
|
||||
const range = selectionRange.getRangeAt(0);
|
||||
if (findTableAncestor(range.startContainer)) {
|
||||
return false;
|
||||
}
|
||||
if (isCellSelection(selection)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const handleAccessChange = (accessKey: string) => {
|
||||
props.commentAccessSpecifier?.onAccessChange(accessKey);
|
||||
};
|
||||
|
||||
18
packages/types/src/cycles.d.ts
vendored
18
packages/types/src/cycles.d.ts
vendored
@@ -1,4 +1,11 @@
|
||||
import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types";
|
||||
import type {
|
||||
IUser,
|
||||
TIssue,
|
||||
IProjectLite,
|
||||
IWorkspaceLite,
|
||||
IIssueFilterOptions,
|
||||
IUserLite,
|
||||
} from "@plane/types";
|
||||
|
||||
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
|
||||
|
||||
@@ -40,6 +47,7 @@ export interface ICycle {
|
||||
};
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspaceLite;
|
||||
issues?: TIssue[];
|
||||
}
|
||||
|
||||
export type TAssigneesDistribution = {
|
||||
@@ -80,9 +88,13 @@ export interface CycleIssueResponse {
|
||||
sub_issues_count: number;
|
||||
}
|
||||
|
||||
export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
|
||||
export type SelectCycleType =
|
||||
| (ICycle & { actionType: "edit" | "delete" | "create-issue" })
|
||||
| undefined;
|
||||
|
||||
export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null;
|
||||
export type SelectIssue =
|
||||
| (TIssue & { actionType: "edit" | "delete" | "create" })
|
||||
| null;
|
||||
|
||||
export type CycleDateCheckData = {
|
||||
start_date: string;
|
||||
|
||||
13
packages/types/src/projects.d.ts
vendored
13
packages/types/src/projects.d.ts
vendored
@@ -1,5 +1,11 @@
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from ".";
|
||||
import type {
|
||||
IUser,
|
||||
IUserLite,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
TStateGroups,
|
||||
} from ".";
|
||||
|
||||
export interface IProject {
|
||||
archive_in: number;
|
||||
@@ -52,6 +58,11 @@ export interface IProjectLite {
|
||||
id: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
emoji: string | null;
|
||||
icon_prop: {
|
||||
name: string;
|
||||
color: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
type ProjectPreferences = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useApplication } from "hooks/store";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
|
||||
@@ -22,11 +22,6 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
|
||||
// derived values
|
||||
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
||||
|
||||
const redirect = (path: string) => {
|
||||
closePalette();
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
|
||||
254
web/components/cycles/active-cycle-info.tsx
Normal file
254
web/components/cycles/active-cycle-info.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import { FC, MouseEvent, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
// ui
|
||||
import {
|
||||
AvatarGroup,
|
||||
Tooltip,
|
||||
LinearProgressIndicator,
|
||||
ContrastIcon,
|
||||
RunningIcon,
|
||||
LayersIcon,
|
||||
StateGroupIcon,
|
||||
Avatar,
|
||||
} from "@plane/ui";
|
||||
// components
|
||||
import { SingleProgressStats } from "components/core";
|
||||
import { ActiveCycleProgressStats } from "./active-cycle-stats";
|
||||
// hooks
|
||||
import { useCycle } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// icons
|
||||
import { ArrowRight, CalendarDays, Star, Target } from "lucide-react";
|
||||
// types
|
||||
import { ICycle, TCycleLayout, TCycleView } from "@plane/types";
|
||||
// helpers
|
||||
import { renderFormattedDate, findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// constants
|
||||
import { STATE_GROUPS_DETAILS } from "constants/cycle";
|
||||
|
||||
export type ActiveCycleInfoProps = {
|
||||
cycle: ICycle;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ActiveCycleInfo: FC<ActiveCycleInfoProps> = (props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
|
||||
// store
|
||||
const { addCycleToFavorites, removeCycleFromFavorites } = useCycle();
|
||||
// local storage
|
||||
const { setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
|
||||
const { setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const groupedIssues: any = {
|
||||
backlog: cycle.backlog_issues,
|
||||
unstarted: cycle.unstarted_issues,
|
||||
started: cycle.started_issues,
|
||||
completed: cycle.completed_issues,
|
||||
cancelled: cycle.cancelled_issues,
|
||||
};
|
||||
|
||||
const progressIndicatorData = STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||
id: index,
|
||||
name: group.title,
|
||||
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const handleCurrentLayout = useCallback(
|
||||
(_layout: TCycleLayout) => {
|
||||
setCycleLayout(_layout);
|
||||
},
|
||||
[setCycleLayout]
|
||||
);
|
||||
|
||||
const handleCurrentView = useCallback(
|
||||
(_view: TCycleView) => {
|
||||
setCycleTab(_view);
|
||||
if (_view === "draft") handleCurrentLayout("list");
|
||||
},
|
||||
[handleCurrentLayout, setCycleTab]
|
||||
);
|
||||
|
||||
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow">
|
||||
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:grid-cols-3 lg:divide-x lg:divide-y-0">
|
||||
<div className="flex flex-col text-xs">
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-60 flex-col justify-between gap-5 rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-5 w-5">
|
||||
<ContrastIcon className="h-5 w-5" color="#09A953" />
|
||||
</span>
|
||||
<Tooltip tooltipContent={cycle.name} position="top-left">
|
||||
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 70)}</h3>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span className="flex items-center gap-1 capitalize">
|
||||
<span className="rounded-full px-1.5 py-0.5 bg-green-600/5 text-green-600">
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<RunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
||||
</span>
|
||||
</span>
|
||||
{cycle.is_favorite ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleRemoveFromFavorites(e);
|
||||
}}
|
||||
>
|
||||
<Star className="h-4 w-4 fill-orange-400 text-orange-400" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleAddToFavorites(e);
|
||||
}}
|
||||
>
|
||||
<Star className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start gap-5 text-custom-text-200">
|
||||
<div className="flex items-start gap-1">
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
{cycle?.start_date && <span>{renderFormattedDate(cycle?.start_date)}</span>}
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-custom-text-200" />
|
||||
<div className="flex items-start gap-1">
|
||||
<Target className="h-4 w-4" />
|
||||
{cycle?.end_date && <span>{renderFormattedDate(cycle?.end_date)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2.5 text-custom-text-200">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<img
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
|
||||
{cycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
|
||||
</div>
|
||||
|
||||
{cycle.assignees.length > 0 && (
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<AvatarGroup>
|
||||
{cycle.assignees.map((assignee: any) => (
|
||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-custom-text-200">
|
||||
<div className="flex gap-2">
|
||||
<LayersIcon className="h-4 w-4 flex-shrink-0" />
|
||||
{cycle.total_issues} issues
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
|
||||
{cycle.completed_issues} issues
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex item-center gap-2">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/cycles`}
|
||||
onClick={() => {
|
||||
handleCurrentView("active");
|
||||
}}
|
||||
>
|
||||
<span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90">
|
||||
View Cycle
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90">
|
||||
View Cycle Issues
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 grid grid-cols-1 divide-y border-custom-border-200 md:grid-cols-2 md:divide-x md:divide-y-0">
|
||||
<div className="flex h-60 flex-col border-custom-border-200">
|
||||
<div className="flex h-full w-full flex-col p-4 text-custom-text-200">
|
||||
<div className="flex w-full items-center gap-2 py-1">
|
||||
<span>Progress</span>
|
||||
<LinearProgressIndicator data={progressIndicatorData} />
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col items-center gap-1">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full "
|
||||
style={{
|
||||
backgroundColor: STATE_GROUPS_DETAILS[index].color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
</div>
|
||||
}
|
||||
completed={groupedIssues[group]}
|
||||
total={cycle.total_issues}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-60 overflow-y-scroll border-custom-border-200">
|
||||
<ActiveCycleProgressStats cycle={cycle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,3 +15,4 @@ export * from "./cycles-board-card";
|
||||
export * from "./delete-modal";
|
||||
export * from "./cycle-peek-overview";
|
||||
export * from "./cycles-list-item";
|
||||
export * from "./active-cycle-info";
|
||||
|
||||
@@ -20,3 +20,4 @@ export * from "./project-archived-issue-details";
|
||||
export * from "./project-archived-issues";
|
||||
export * from "./project-issue-details";
|
||||
export * from "./user-profile";
|
||||
export * from "./workspace-active-cycle";
|
||||
|
||||
46
web/components/headers/workspace-active-cycle.tsx
Normal file
46
web/components/headers/workspace-active-cycle.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Search, SendToBack } from "lucide-react";
|
||||
// hooks
|
||||
import { useWorkspace } from "hooks/store";
|
||||
// ui
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
|
||||
export const WorkspaceActiveCycleHeader = observer(() => {
|
||||
// store hooks
|
||||
const { workspaceActiveCyclesSearchQuery, setWorkspaceActiveCyclesSearchQuery } = useWorkspace();
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
icon={<SendToBack className="h-4 w-4 text-custom-text-300" />}
|
||||
label="Active Cycles"
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<span
|
||||
className="flex items-center justify-center px-3.5 py-0.5 text-xs leading-5 rounded-xl"
|
||||
style={{
|
||||
color: "#F59E0B",
|
||||
backgroundColor: "#F59E0B20",
|
||||
}}
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex w-full items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5 text-custom-text-400">
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
<input
|
||||
className="w-full min-w-[234px] border-none bg-transparent text-sm focus:outline-none"
|
||||
value={workspaceActiveCyclesSearchQuery}
|
||||
onChange={(e) => setWorkspaceActiveCyclesSearchQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
|
||||
export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 }) => (
|
||||
<>
|
||||
{labelDetails.length > 0 ? (
|
||||
{labelDetails?.length > 0 ? (
|
||||
labelDetails.length <= maxRender ? (
|
||||
<>
|
||||
{labelDetails.map((label) => (
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from "./send-workspace-invitation-modal";
|
||||
export * from "./sidebar-dropdown";
|
||||
export * from "./sidebar-menu";
|
||||
export * from "./sidebar-quick-action";
|
||||
export * from "./workspace-active-cycles-list";
|
||||
|
||||
57
web/components/workspace/workspace-active-cycles-list.tsx
Normal file
57
web/components/workspace/workspace-active-cycles-list.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { ActiveCycleInfo } from "components/cycles";
|
||||
// services
|
||||
import { CycleService } from "services/cycle.service";
|
||||
const cycleService = new CycleService();
|
||||
// hooks
|
||||
import { useWorkspace } from "hooks/store";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
export const WorkspaceActiveCyclesList = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// fetching active cycles in workspace
|
||||
const { data } = useSWR("WORKSPACE_ACTIVE_CYCLES", () => cycleService.workspaceActiveCycles(workspaceSlug as string));
|
||||
// store
|
||||
const { workspaceActiveCyclesSearchQuery } = useWorkspace();
|
||||
// filter cycles based on search query
|
||||
const filteredCycles = data?.filter(
|
||||
(cycle) =>
|
||||
cycle.project_detail.name.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase()) ||
|
||||
cycle.project_detail.identifier?.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase()) ||
|
||||
cycle.name.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{workspaceSlug &&
|
||||
filteredCycles &&
|
||||
filteredCycles.map((cycle) => (
|
||||
<div key={cycle.id} className="px-5 py-7">
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5">
|
||||
{cycle.project_detail.emoji ? (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
{renderEmoji(cycle.project_detail.emoji)}
|
||||
</span>
|
||||
) : cycle.project_detail.icon_prop ? (
|
||||
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
|
||||
{renderEmoji(cycle.project_detail.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
{cycle.project_detail?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-xl font-semibold">{cycle.project_detail.name}</h2>
|
||||
</div>
|
||||
<ActiveCycleInfo workspaceSlug={workspaceSlug?.toString()} projectId={cycle.project} cycle={cycle} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -86,3 +86,32 @@ export const CYCLE_STATUS: {
|
||||
bgColor: "bg-custom-background-90",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export const STATE_GROUPS_DETAILS = [
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#dee2e6",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#26b5ce",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#f7ae59",
|
||||
},
|
||||
{
|
||||
key: "cancelled_issues",
|
||||
title: "Cancelled",
|
||||
color: "#d687ff",
|
||||
},
|
||||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#09a953",
|
||||
},
|
||||
];
|
||||
|
||||
16
web/pages/[workspaceSlug]/active-cycles.tsx
Normal file
16
web/pages/[workspaceSlug]/active-cycles.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ReactElement } from "react";
|
||||
// components
|
||||
import { WorkspaceActiveCyclesList } from "components/workspace";
|
||||
import { WorkspaceActiveCycleHeader } from "components/headers";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const WorkspaceActiveCyclesPage: NextPageWithLayout = () => <WorkspaceActiveCyclesList />;
|
||||
|
||||
WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>;
|
||||
};
|
||||
|
||||
export default WorkspaceActiveCyclesPage;
|
||||
@@ -44,7 +44,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
const editorRef = useRef<any>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, pageId } = router.query;
|
||||
// store hooks
|
||||
const {
|
||||
|
||||
@@ -10,6 +10,14 @@ export class CycleService extends APIService {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async workspaceActiveCycles(workspaceSlug: string): Promise<ICycle[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/active-cycles/`)
|
||||
.then((res) => res?.data)
|
||||
.catch((err) => {
|
||||
throw err?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<ICycle> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
||||
.then((response) => response?.data)
|
||||
|
||||
@@ -12,9 +12,12 @@ import { ApiTokenStore, IApiTokenStore } from "./api-token.store";
|
||||
export interface IWorkspaceRootStore {
|
||||
// observables
|
||||
workspaces: Record<string, IWorkspace>;
|
||||
workspaceActiveCyclesSearchQuery: string;
|
||||
// computed
|
||||
currentWorkspace: IWorkspace | null;
|
||||
workspacesCreatedByCurrentUser: IWorkspace[] | null;
|
||||
// actions
|
||||
setWorkspaceActiveCyclesSearchQuery: (query: string) => void;
|
||||
// computed actions
|
||||
getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null;
|
||||
getWorkspaceById: (workspaceId: string) => IWorkspace | null;
|
||||
@@ -31,6 +34,7 @@ export interface IWorkspaceRootStore {
|
||||
|
||||
export class WorkspaceRootStore implements IWorkspaceRootStore {
|
||||
// observables
|
||||
workspaceActiveCyclesSearchQuery: string = "";
|
||||
workspaces: Record<string, IWorkspace> = {};
|
||||
// services
|
||||
workspaceService;
|
||||
@@ -45,6 +49,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
workspaces: observable,
|
||||
workspaceActiveCyclesSearchQuery: observable.ref,
|
||||
// computed
|
||||
currentWorkspace: computed,
|
||||
workspacesCreatedByCurrentUser: computed,
|
||||
@@ -52,6 +57,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
||||
getWorkspaceBySlug: action,
|
||||
getWorkspaceById: action,
|
||||
// actions
|
||||
setWorkspaceActiveCyclesSearchQuery: action,
|
||||
fetchWorkspaces: action,
|
||||
createWorkspace: action,
|
||||
updateWorkspace: action,
|
||||
@@ -102,6 +108,14 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
||||
*/
|
||||
getWorkspaceById = (workspaceId: string) => this.workspaces?.[workspaceId] || null; // TODO: use undefined instead of null
|
||||
|
||||
/**
|
||||
* Sets search query
|
||||
* @param query
|
||||
*/
|
||||
setWorkspaceActiveCyclesSearchQuery = (query: string) => {
|
||||
this.workspaceActiveCyclesSearchQuery = query;
|
||||
};
|
||||
|
||||
/**
|
||||
* fetch user workspaces from API
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user