ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: стабилизация контуров, досок и Voice Tasker

This commit is contained in:
DCCONSTRUCTIONS 2026-04-24 23:34:20 +03:00
parent 597480adb9
commit 7209d2caab
37 changed files with 960 additions and 390 deletions

View File

@ -252,12 +252,15 @@ MVP-правило:
Система: Система:
1. берет последние voice-действия пользователя в текущем workspace; 1. берет последние только реально примененные voice-действия пользователя в текущем workspace;
2. находит последную созданную/обновленную voice-задачу; 2. игнорирует parsed-сессии без `created_task`/`updated_task`, чтобы модель не цеплялась за старые неудачные черновики;
3. показывает preview изменения, если confidence низкий; 3. если transcript явно задает исходный проект ("из Бухгалтерии", "последнюю добавленную в Бухгалтерию"), сначала ищет последнюю voice-задачу в этом проекте;
4. меняет `Issue.target_date`; 4. если исходный проект не назван, сначала ищет последнюю voice-задачу в текущем открытом проекте;
5. сохраняет `due_time` в description note / parsed JSON; 5. затем использует последнюю примененную voice-задачу workspace как общий fallback;
6. пишет новое действие в session-backed memory. 6. показывает preview изменения, если confidence низкий;
7. меняет `Issue.target_date`;
8. сохраняет `due_time` в description note / parsed JSON;
9. пишет новое действие в session-backed memory.
Если пользователь говорит "переложи последнюю задачу в проект X", это остается `update_task`, но backend должен: Если пользователь говорит "переложи последнюю задачу в проект X", это остается `update_task`, но backend должен:
@ -882,6 +885,7 @@ Return JSON only.
6. если confidence низкий - preview с ручным выбором; 6. если confidence низкий - preview с ручным выбором;
7. если transcript содержит явную маршрутизацию проекта, но model `project_hint` указывает на проект, которого нет в transcript, auto-commit блокируется; 7. если transcript содержит явную маршрутизацию проекта, но model `project_hint` указывает на проект, которого нет в transcript, auto-commit блокируется;
8. при наличии source/destination фразы выбирать destination project: "из Бухгалтерии в Менеджмент" -> target `Менеджмент`, "из Менеджмента в Бухгалтерию" -> target `Бухгалтерия`. 8. при наличии source/destination фразы выбирать destination project: "из Бухгалтерии в Менеджмент" -> target `Менеджмент`, "из Менеджмента в Бухгалтерию" -> target `Бухгалтерия`.
9. глагол "перенеси/перенести" рядом со сроком/датой считается date update, а не project routing, если нет project/контур-маркера или явного project destination.
Не зашивать термин "контур" как обязательный. Для NODE DC это важный UX-термин, но технически это обычный `Project`. Не зашивать термин "контур" как обязательный. Для NODE DC это важный UX-термин, но технически это обычный `Project`.
@ -947,11 +951,28 @@ MVP:
- `N` дней/недель/месяцев/лет назад; - `N` дней/недель/месяцев/лет назад;
- сложные интервалы: "два месяца и две недели"; - сложные интервалы: "два месяца и две недели";
- числительные цифрами и частые русские словоформы: "2 недели", "две недели", "пару дней"; - числительные цифрами и частые русские словоформы: "2 недели", "две недели", "пару дней";
- абсолютные русские даты: "1 мая 2026 года", "30 апреля";
- числовые даты: "01.05.2026", "1/05/26";
- защита от ложных матчей внутри слов: "последней" не считается как "дней"; - защита от ложных матчей внутри слов: "последней" не считается как "дней";
- защита от трактовки года абсолютной даты как относительного интервала: "2026 года" не считается как "+2026 лет";
- конкретная дата; - конкретная дата;
- конкретное время как `due_time` note. - конкретное время как `due_time` note.
Date resolver обязан работать после OpenAI parser как deterministic fallback. Если модель уже вернула валидный `due_date`, backend его не переписывает. Date resolver обязан работать после OpenAI parser как deterministic слой. Сначала резолвятся абсолютные даты из transcript; они могут переписать ошибочный `due_date` от модели. Затем обрабатываются относительные сдвиги вида "подвинь на 3 дня вперед" / "передвинь назад на 3 дня": backend может переписать `due_date`, даже если модель уже вернула дату, а база расчета берется из текущего `Issue.target_date`, а не из сегодняшней даты. Для фраз вида "через 3 дня" без маркера сдвига база остается текущей датой.
### 10.4.1. Memory resolver
`recent_voice_memory` для parser содержит только примененные voice-сессии, у которых есть доступная `target_task`.
При backend commit:
1. explicit issue key/issue id остается самым сильным указанием цели;
2. `target_memory_ref` на voice-сессию используется только если эта сессия реально связана с доступной задачей;
3. если transcript содержит общее указание "последняя/предыдущая/эта", backend не доверяет model-selected voice session ref и выбирает цель deterministic fallback-ом;
4. если ref ведет в parsed/no-op сессию, resolver переходит к deterministic fallback;
5. fallback сначала учитывает явно названный source project;
6. затем текущий project из `client_context.current_project_id`;
7. затем последнюю примененную voice-задачу workspace.
### 10.5. Voice task representation in Issue ### 10.5. Voice task representation in Issue
@ -1201,6 +1222,7 @@ voice_task.error
23. Относительная дата "на две недели вперед" меняет `Issue.target_date` без ручного ввода ISO-даты. 23. Относительная дата "на две недели вперед" меняет `Issue.target_date` без ручного ввода ISO-даты.
24. Перенос "из Бухгалтерии в Менеджмент" и "из Менеджмента в Бухгалтерию" реально меняет `Issue.project_id` и `task_key`. 24. Перенос "из Бухгалтерии в Менеджмент" и "из Менеджмента в Бухгалтерию" реально меняет `Issue.project_id` и `task_key`.
25. Voice-created task имеет `external_source=voice_tasker` и хранит исходный transcript в description. 25. Voice-created task имеет `external_source=voice_tasker` и хранит исходный transcript в description.
26. Preview modal показывает transcript/description полностью без внутреннего scroll внутри текстовых блоков.
--- ---

View File

@ -113,6 +113,32 @@ VOICE_TASK_STATE_GROUP_HINTS = {
} }
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$") DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
TIME_PATTERN = re.compile(r"^\d{2}:\d{2}$") TIME_PATTERN = re.compile(r"^\d{2}:\d{2}$")
VOICE_TASK_MONTHS = {
"январь": 1,
"января": 1,
"февраль": 2,
"февраля": 2,
"март": 3,
"марта": 3,
"апрель": 4,
"апреля": 4,
"май": 5,
"мая": 5,
"июнь": 6,
"июня": 6,
"июль": 7,
"июля": 7,
"август": 8,
"августа": 8,
"сентябрь": 9,
"сентября": 9,
"октябрь": 10,
"октября": 10,
"ноябрь": 11,
"ноября": 11,
"декабрь": 12,
"декабря": 12,
}
VOICE_TASK_NUMBER_WORDS = { VOICE_TASK_NUMBER_WORDS = {
"один": 1, "один": 1,
"одна": 1, "одна": 1,
@ -152,6 +178,14 @@ VOICE_TASK_RELATIVE_DATE_PATTERN = re.compile(
r"(?P<unit>день|дня|дней|сутки|суток|неделю|неделя|недели|недель|" r"(?P<unit>день|дня|дней|сутки|суток|неделю|неделя|недели|недель|"
r"месяц|месяца|месяцев|год|года|лет)(?![0-9a-zа-я])" r"месяц|месяца|месяцев|год|года|лет)(?![0-9a-zа-я])"
) )
VOICE_TASK_MONTH_NAME_PATTERN = "|".join(sorted(VOICE_TASK_MONTHS.keys(), key=len, reverse=True))
VOICE_TASK_ABSOLUTE_MONTH_DATE_PATTERN = re.compile(
rf"(?<![0-9a-zа-я])(?P<day>[0-3]?\d)\s+(?P<month>{VOICE_TASK_MONTH_NAME_PATTERN})"
r"(?:\s+(?P<year>\d{4}))?(?:\s+года?)?(?![0-9a-zа-я])"
)
VOICE_TASK_NUMERIC_DATE_PATTERN = re.compile(
r"(?<!\d)(?P<day>[0-3]?\d)[./-](?P<month>[01]?\d)(?:[./-](?P<year>\d{2,4}))?(?!\d)"
)
def normalize_audio_content_type(content_type): def normalize_audio_content_type(content_type):
@ -443,13 +477,15 @@ def transcript_has_project_routing_request(transcript):
if not normalized: if not normalized:
return False return False
normalized = normalized.lower() normalized = normalize_match_value(normalized)
return bool( if re.search(r"(проект|контур|route|move\s+to\s+project|project)", normalized):
re.search( return True
r"(проект|контур|перелож|перенес|перемест|перекин|route|move\s+to\s+project|project)",
normalized, has_transfer_verb = bool(re.search(r"(перелож|перенес|перемест|перекин|move)", normalized))
) has_due_marker = bool(
re.search(r"(срок|дат|дедлайн|deadline|завтра|сегодня|послезавтра|вчера|дн(я|ей|ь)|недел|месяц|год|лет)", normalized)
) )
return has_transfer_verb and not has_due_marker
def transcript_contains_project_hint(project_hint, transcript): def transcript_contains_project_hint(project_hint, transcript):
@ -461,6 +497,19 @@ def transcript_contains_project_hint(project_hint, transcript):
return normalized_hint in normalized_transcript return normalized_hint in normalized_transcript
def transcript_has_generic_memory_reference(transcript):
normalized_transcript = normalize_match_value(transcript)
if not normalized_transcript:
return False
return bool(
re.search(
r"\b(последн\w*|предыдущ\w*|прошл\w*|эту|эта|этой|этот|ее|её|его|ту|той)\b",
normalized_transcript,
)
)
def infer_voice_task_project_from_transcript(projects, transcript): def infer_voice_task_project_from_transcript(projects, transcript):
normalized_transcript = normalize_match_value(transcript) normalized_transcript = normalize_match_value(transcript)
if not normalized_transcript: if not normalized_transcript:
@ -490,7 +539,7 @@ def infer_voice_task_project_from_transcript(projects, transcript):
elif has_transfer_intent and re.search(r"(в|во|на)\s*$", prefix): elif has_transfer_intent and re.search(r"(в|во|на)\s*$", prefix):
score = 0.99 score = 0.99
if score > best_score or (score == best_score and alias_length > best_alias_length): if score > 0 and (score > best_score or (score == best_score and alias_length > best_alias_length)):
best_project = project best_project = project
best_score = score best_score = score
best_alias_length = alias_length best_alias_length = alias_length
@ -501,6 +550,54 @@ def infer_voice_task_project_from_transcript(projects, transcript):
return serialize_resolved_project(best_project, best_score, "transcript_project_hint") return serialize_resolved_project(best_project, best_score, "transcript_project_hint")
def infer_voice_task_source_project_from_transcript(projects, transcript):
normalized_transcript = normalize_match_value(transcript)
if not normalized_transcript:
return None
has_transfer_intent = bool(re.search(r"(перелож|перенес|перемест|перекин|move|route)", normalized_transcript))
best_project = None
best_score = 0.0
best_alias_length = 0
for project in projects:
for candidate in get_project_alias_candidates(project):
normalized_candidate = normalize_match_value(candidate)
if len(normalized_candidate) < 3:
continue
candidate_index = normalized_transcript.find(normalized_candidate)
if candidate_index < 0:
continue
prefix = normalized_transcript[max(0, candidate_index - 56) : candidate_index]
alias_length = len(normalized_candidate)
score = 0.0
if re.search(r"(из|с|со|from|source)\s+(?:проекта\s+|контура\s+)?$", prefix):
score = 1.0
elif re.search(
r"(добав\w*|созда\w*|постав\w*)\s+(?:задач\w+\s+)?(в|во|на)\s+(?:проекте\s+|проект\s+|контуре\s+|контур\s+)?$",
prefix,
):
score = 0.95
elif (
not has_transfer_intent
and re.search(r"(в|во|на)\s+(?:проекте\s+|проект\s+|контуре\s+|контур\s+)?$", prefix)
):
score = 0.9
if score > 0 and (score > best_score or (score == best_score and alias_length > best_alias_length)):
best_project = project
best_score = score
best_alias_length = alias_length
if not best_project:
return None
return best_project
def get_text_match_score(query, candidates): def get_text_match_score(query, candidates):
normalized_query = normalize_match_value(query) normalized_query = normalize_match_value(query)
if not normalized_query: if not normalized_query:
@ -811,6 +908,61 @@ def add_months_to_date(value, months):
return date(year, month, day) return date(year, month, day)
def build_voice_task_date(day, month, year, current_date, year_was_explicit=False):
try:
day = int(day)
month = int(month)
year = int(year) if year else current_date.year
if year < 100:
year += 2000
candidate = date(year, month, day)
except (TypeError, ValueError):
return None
if not year_was_explicit and candidate < current_date:
try:
candidate = date(current_date.year + 1, month, day)
except ValueError:
return None
return candidate.isoformat()
def infer_voice_task_absolute_due_date(transcript, current_date):
normalized = normalize_match_value(transcript)
if normalized:
match = VOICE_TASK_ABSOLUTE_MONTH_DATE_PATTERN.search(normalized)
if match:
month = VOICE_TASK_MONTHS.get(match.group("month"))
result = build_voice_task_date(
day=match.group("day"),
month=month,
year=match.group("year"),
current_date=current_date,
year_was_explicit=bool(match.group("year")),
)
if result:
return result
raw_transcript = normalize_string(transcript)
if not raw_transcript:
return None
match = VOICE_TASK_NUMERIC_DATE_PATTERN.search(raw_transcript.lower().replace("ё", "е"))
if match:
result = build_voice_task_date(
day=match.group("day"),
month=match.group("month"),
year=match.group("year"),
current_date=current_date,
year_was_explicit=bool(match.group("year")),
)
if result:
return result
return None
def infer_voice_task_relative_due_date(transcript, current_date, target_issue=None): def infer_voice_task_relative_due_date(transcript, current_date, target_issue=None):
normalized = normalize_match_value(transcript) normalized = normalize_match_value(transcript)
if not normalized: if not normalized:
@ -868,6 +1020,8 @@ def infer_voice_task_relative_due_date(transcript, current_date, target_issue=No
elif unit.startswith("месяц"): elif unit.startswith("месяц"):
shift_months += quantity shift_months += quantity
elif unit.startswith("год") or unit == "лет": elif unit.startswith("год") or unit == "лет":
if quantity >= 100:
continue
shift_months += quantity * 12 shift_months += quantity * 12
if shift_days == 0 and shift_months == 0: if shift_days == 0 and shift_months == 0:
@ -875,7 +1029,22 @@ def infer_voice_task_relative_due_date(transcript, current_date, target_issue=No
base_date = current_date base_date = current_date
source_date = getattr(target_issue, "target_date", None) source_date = getattr(target_issue, "target_date", None)
if source_date and any(marker in normalized for marker in ["подвин", "сдвин", "смест", "отлож", "раньше", "позже"]): has_existing_due_shift = any(
marker in normalized
for marker in [
"подвин",
"передвин",
"сдвин",
"смест",
"отлож",
"перенес",
"назад",
"вперед",
"раньше",
"позже",
]
)
if source_date and has_existing_due_shift:
base_date = source_date base_date = source_date
result = add_months_to_date(base_date, shift_months * direction) if shift_months else base_date result = add_months_to_date(base_date, shift_months * direction) if shift_months else base_date
@ -884,12 +1053,15 @@ def infer_voice_task_relative_due_date(transcript, current_date, target_issue=No
def hydrate_voice_task_due_date(draft, transcript, client_context, user, workspace, target_issue=None): def hydrate_voice_task_due_date(draft, transcript, client_context, user, workspace, target_issue=None):
if draft.get("due_date"): current_date = get_voice_task_current_date(client_context, user, workspace)
absolute_due_date = infer_voice_task_absolute_due_date(transcript, current_date=current_date)
if absolute_due_date:
draft["due_date"] = absolute_due_date
return return
inferred_due_date = infer_voice_task_relative_due_date( inferred_due_date = infer_voice_task_relative_due_date(
transcript=transcript, transcript=transcript,
current_date=get_voice_task_current_date(client_context, user, workspace), current_date=current_date,
target_issue=target_issue, target_issue=target_issue,
) )
if inferred_due_date: if inferred_due_date:
@ -972,8 +1144,39 @@ def is_voice_task_issue_available(issue):
return bool(issue and not issue.deleted_at and not issue.archived_at) return bool(issue and not issue.deleted_at and not issue.archived_at)
def resolve_voice_task_memory_target(workspace, user, draft, current_session=None): def get_committed_voice_task_memory_sessions(workspace, user, current_session=None):
memory_sessions = (
VoiceTaskSession.objects.filter(
workspace=workspace,
user=user,
status=VoiceTaskSession.Status.PARSED,
)
.filter(Q(created_task__isnull=False) | Q(updated_task__isnull=False))
.select_related("created_task", "created_task__project", "updated_task", "updated_task__project")
.order_by("-updated_at", "-created_at")
)
if current_session:
memory_sessions = memory_sessions.exclude(id=current_session.id)
return list(memory_sessions[: VOICE_TASK_MEMORY_LIMIT * 3])
def find_latest_voice_task_issue(memory_sessions, project_id=None):
for memory_session in memory_sessions:
target_issue = get_voice_session_target_issue(memory_session)
if not is_voice_task_issue_available(target_issue):
continue
if project_id and str(target_issue.project_id) != str(project_id):
continue
return target_issue, memory_session
return None, None
def resolve_voice_task_memory_target(workspace, user, draft, current_session=None, client_context=None, transcript=None):
target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80) target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80)
memory_sessions = get_committed_voice_task_memory_sessions(workspace, user, current_session=current_session)
generic_memory_reference = transcript_has_generic_memory_reference(transcript)
if target_memory_ref: if target_memory_ref:
target_uuid = None target_uuid = None
@ -989,7 +1192,7 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
.first() .first()
) )
target_issue = get_voice_session_target_issue(memory_session) target_issue = get_voice_session_target_issue(memory_session)
if target_issue: if is_voice_task_issue_available(target_issue) and not generic_memory_reference:
return target_issue, "target_memory_ref", memory_session return target_issue, "target_memory_ref", memory_session
target_issue = ( target_issue = (
@ -997,7 +1200,7 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
.select_related("project") .select_related("project")
.first() .first()
) )
if target_issue: if is_voice_task_issue_available(target_issue):
return target_issue, "target_issue_id", None return target_issue, "target_issue_id", None
issue_key_reference = parse_issue_key_reference(target_memory_ref) issue_key_reference = parse_issue_key_reference(target_memory_ref)
@ -1012,26 +1215,25 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
.select_related("project") .select_related("project")
.first() .first()
) )
if target_issue: if is_voice_task_issue_available(target_issue):
return target_issue, "target_issue_key", None return target_issue, "target_issue_key", None
memory_sessions = ( projects = list(get_accessible_projects(workspace, user).order_by("name"))
VoiceTaskSession.objects.filter( source_project = infer_voice_task_source_project_from_transcript(projects, transcript)
workspace=workspace, if source_project:
user=user, target_issue, memory_session = find_latest_voice_task_issue(memory_sessions, source_project.id)
status=VoiceTaskSession.Status.PARSED, if target_issue:
) return target_issue, "latest_voice_task_source_project", memory_session
.filter(Q(created_task__isnull=False) | Q(updated_task__isnull=False))
.select_related("created_task", "created_task__project", "updated_task", "updated_task__project")
.order_by("-updated_at", "-created_at")
)
if current_session:
memory_sessions = memory_sessions.exclude(id=current_session.id)
for memory_session in memory_sessions[:VOICE_TASK_MEMORY_LIMIT * 3]: current_project_id = normalize_string((client_context or {}).get("current_project_id"))
target_issue = get_voice_session_target_issue(memory_session) if current_project_id:
if is_voice_task_issue_available(target_issue): target_issue, memory_session = find_latest_voice_task_issue(memory_sessions, current_project_id)
return target_issue, "latest_voice_task", memory_session if target_issue:
return target_issue, "latest_voice_task_current_project", memory_session
target_issue, memory_session = find_latest_voice_task_issue(memory_sessions)
if target_issue:
return target_issue, "latest_voice_task", memory_session
return None, None, None return None, None, None
@ -1066,6 +1268,8 @@ def build_voice_task_resolution(workspace, user, ai_settings, draft, client_cont
user=user, user=user,
draft=draft, draft=draft,
current_session=voice_session, current_session=voice_session,
client_context=client_context,
transcript=transcript,
) )
hydrate_voice_task_due_date( hydrate_voice_task_due_date(
@ -1493,32 +1697,20 @@ def serialize_workspace_members(workspace):
def serialize_recent_voice_memory(workspace, user): def serialize_recent_voice_memory(workspace, user):
sessions = ( sessions = get_committed_voice_task_memory_sessions(workspace, user)[:VOICE_TASK_MEMORY_LIMIT]
VoiceTaskSession.objects.filter(
workspace=workspace,
user=user,
status=VoiceTaskSession.Status.PARSED,
)
.exclude(parsed_json={})
.select_related("created_task", "created_task__project", "updated_task", "updated_task__project")
.order_by("-updated_at", "-created_at")[:VOICE_TASK_MEMORY_LIMIT]
)
memory = [] memory = []
for session in sessions: for session in sessions:
target_issue = get_voice_session_target_issue(session) target_issue = get_voice_session_target_issue(session)
target_task = ( if not is_voice_task_issue_available(target_issue):
serialize_voice_task_target(target_issue, "recent_voice_memory", session) continue
if is_voice_task_issue_available(target_issue)
else None
)
memory.append( memory.append(
{ {
"voice_session_id": str(session.id), "voice_session_id": str(session.id),
"intent": session.intent, "intent": session.intent,
"title": session.parsed_json.get("title"), "title": session.parsed_json.get("title"),
"project_hint": session.parsed_json.get("project_hint"), "project_hint": session.parsed_json.get("project_hint"),
"target_task": target_task, "target_task": serialize_voice_task_target(target_issue, "recent_voice_memory", session),
"created_at": session.created_at.isoformat(), "created_at": session.created_at.isoformat(),
} }
) )

View File

@ -10,14 +10,9 @@ import { CalendarDays } from "lucide-react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EUserPermissions } from "@plane/constants"; import { EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { PriorityIcon, StateGroupIcon } from "@plane/propel/icons"; import { PriorityIcon, StateGroupIcon, getStateGroupColor } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { import type { IState, TExternalContourBoardDirection, TExternalContourRequest, TIssue } from "@plane/types";
IState,
TExternalContourBoardDirection,
TExternalContourRequest,
TIssue,
} from "@plane/types";
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils"; import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
import { DateDropdown } from "@/components/dropdowns/date"; import { DateDropdown } from "@/components/dropdowns/date";
@ -56,7 +51,7 @@ const buildSourceStateMap = (
state.id, state.id,
{ {
id: state.id, id: state.id,
color: state.color, color: getStateGroupColor(state.group, state.color),
default: false, default: false,
description: "", description: "",
group: state.group, group: state.group,
@ -69,7 +64,10 @@ const buildSourceStateMap = (
]) ])
); );
const resolveRequestStatus = (issue: TExternalContourRequest["issue"], fallbackStatus: TExternalContourRequest["status"]) => { const resolveRequestStatus = (
issue: TExternalContourRequest["issue"],
fallbackStatus: TExternalContourRequest["status"]
) => {
const stateGroup = issue.state_detail?.group; const stateGroup = issue.state_detail?.group;
if (!stateGroup) return fallbackStatus; if (!stateGroup) return fallbackStatus;
return stateGroup === "completed" || stateGroup === "cancelled" ? "closed" : "open"; return stateGroup === "completed" || stateGroup === "cancelled" ? "closed" : "open";
@ -83,23 +81,20 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
const { getUserDetails, workspace } = useMember(); const { getUserDetails, workspace } = useMember();
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions(); const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
const { getStateById, getProjectStateIds } = useProjectState(); const { getStateById, getProjectStateIds } = useProjectState();
const { const { fetchBoard, upsertBoardItems } = useProjectExternalContoursBoard();
fetchBoard, const { fetchTargetOptions, getTargetOptionsByProjectId, updateRequest, updateRequestIssue } =
upsertBoardItems, useProjectExternalContours();
} = useProjectExternalContoursBoard();
const {
fetchTargetOptions,
getTargetOptionsByProjectId,
updateRequest,
updateRequestIssue,
} = useProjectExternalContours();
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [isSourceOptionsLoading, setIsSourceOptionsLoading] = useState(false); const [isSourceOptionsLoading, setIsSourceOptionsLoading] = useState(false);
const issue = request.issue; const issue = request.issue;
const selectedInboxIssueId = searchParams.get("inboxIssueId"); const selectedInboxIssueId = searchParams.get("inboxIssueId");
const isActive = selectedInboxIssueId === request.id; const isActive = selectedInboxIssueId === request.id;
const requester = request.requested_by?.display_name || request.requested_by_name || issue.created_by_detail?.display_name || "NODE.DC"; const requester =
request.requested_by?.display_name ||
request.requested_by_name ||
issue.created_by_detail?.display_name ||
"NODE.DC";
const requesterAvatar = issue.created_by_detail?.avatar_url || ""; const requesterAvatar = issue.created_by_detail?.avatar_url || "";
const counterpartContourName = const counterpartContourName =
direction === "outgoing" direction === "outgoing"
@ -110,8 +105,12 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
? getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId) ? getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId)
: undefined; : undefined;
const canEditTargetIssue = const canEditTargetIssue =
direction === "incoming" && !!targetProjectId && projectRole !== undefined && projectRole !== EUserPermissions.GUEST; direction === "incoming" &&
const canEditSourceRequest = direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId; !!targetProjectId &&
projectRole !== undefined &&
projectRole !== EUserPermissions.GUEST;
const canEditSourceRequest =
direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId;
const canEditCard = canEditTargetIssue || canEditSourceRequest; const canEditCard = canEditTargetIssue || canEditSourceRequest;
const requestLink = `/${workspaceSlug}/projects/${projectId}/external-contours?inboxIssueId=${request.id}`; const requestLink = `/${workspaceSlug}/projects/${projectId}/external-contours?inboxIssueId=${request.id}`;
const targetOptions = getTargetOptionsByProjectId(targetProjectId); const targetOptions = getTargetOptionsByProjectId(targetProjectId);
@ -124,12 +123,11 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : []; const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white"; const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white";
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]"; const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
const pillBackgroundClasses = const pillBackgroundClasses = isActive
isActive ? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]" : "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
const iconBubbleClasses = isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white"; const iconBubbleClasses = isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white";
const statusIconColor = selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)"); const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none"); const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
if (!issue) return null; if (!issue) return null;
@ -314,13 +312,13 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
</div> </div>
</div> </div>
<div className={cn("truncate -mt-0.5 pl-8 text-[11px] font-medium leading-4", subtleTextClasses)}> <div className={cn("-mt-0.5 truncate pl-8 text-[11px] leading-4 font-medium", subtleTextClasses)}>
{counterpartContourName || t("common.none")} {counterpartContourName || t("common.none")}
</div> </div>
</div> </div>
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center"> <div className="flex flex-1 items-center justify-center px-5 py-4 text-center">
<div className="line-clamp-4 max-w-full text-lg font-semibold leading-6">{issue.name}</div> <div className="text-lg line-clamp-4 max-w-full leading-6 font-semibold">{issue.name}</div>
</div> </div>
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}> <div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
@ -333,7 +331,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
disabled={!canEditCard || isUpdating} disabled={!canEditCard || isUpdating}
buttonVariant="transparent-without-text" buttonVariant="transparent-without-text"
button={ button={
<div className={cn(basePillClasses, pillBackgroundClasses, "pl-1 pr-2")}> <div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" /> <ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
</div> </div>
} }
@ -351,7 +349,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
}} }}
buttonVariant="transparent-without-text" buttonVariant="transparent-without-text"
button={ button={
<div className={cn(basePillClasses, pillBackgroundClasses, "pl-1 pr-2")}> <div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" /> <ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
</div> </div>
} }

View File

@ -17,6 +17,7 @@ import {
StateGroupIcon, StateGroupIcon,
StatePropertyIcon, StatePropertyIcon,
UserCirclePropertyIcon, UserCirclePropertyIcon,
getStateGroupColor,
} from "@plane/propel/icons"; } from "@plane/propel/icons";
import type { import type {
IProject, IProject,
@ -57,14 +58,11 @@ const buildCounterpartyProjects = (requests: TExternalContourRequest[], projectI
if (!project?.id || project.id === projectId || projectMap.has(project.id)) return; if (!project?.id || project.id === projectId || projectMap.has(project.id)) return;
projectMap.set( projectMap.set(project.id, {
project.id, id: project.id,
{ name: project.name,
id: project.id, logo_props: project.logo_props,
name: project.name, } as IProject);
logo_props: project.logo_props,
} as IProject
);
}); });
return sortByName(Array.from(projectMap.values())); return sortByName(Array.from(projectMap.values()));
@ -77,21 +75,18 @@ const buildStates = (requests: TExternalContourRequest[]): IState[] => {
const state = request.issue.state_detail; const state = request.issue.state_detail;
if (!state?.id || stateMap.has(state.id)) return; if (!state?.id || stateMap.has(state.id)) return;
stateMap.set( stateMap.set(state.id, {
state.id, id: state.id,
{ color: getStateGroupColor(state.group, state.color),
id: state.id, default: false,
color: state.color, description: "",
default: false, group: state.group,
description: "", name: state.name,
group: state.group, order: index + 1,
name: state.name, project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
order: index + 1, sequence: index + 1,
project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "", workspace_id: "",
sequence: index + 1, } as IState);
workspace_id: "",
} as IState
);
}); });
return sortByName(Array.from(stateMap.values())); return sortByName(Array.from(stateMap.values()));
@ -103,18 +98,15 @@ const buildLabels = (requests: TExternalContourRequest[]): IIssueLabel[] => {
requests.forEach((request) => { requests.forEach((request) => {
request.issue.label_details?.forEach((label) => { request.issue.label_details?.forEach((label) => {
if (!label.id || labelMap.has(label.id)) return; if (!label.id || labelMap.has(label.id)) return;
labelMap.set( labelMap.set(label.id, {
label.id, id: label.id,
{ color: label.color,
id: label.id, name: label.name,
color: label.color, parent: null,
name: label.name, project_id: request.issue.project_id || "",
parent: null, sort_order: 0,
project_id: request.issue.project_id || "", workspace_id: "",
sort_order: 0, } as IIssueLabel);
workspace_id: "",
} as IIssueLabel
);
}); });
}); });
@ -127,14 +119,11 @@ const buildAssignees = (requests: TExternalContourRequest[]): IUserLite[] => {
requests.forEach((request) => { requests.forEach((request) => {
request.issue.assignee_details?.forEach((assignee) => { request.issue.assignee_details?.forEach((assignee) => {
if (!assignee.id || memberMap.has(assignee.id)) return; if (!assignee.id || memberMap.has(assignee.id)) return;
memberMap.set( memberMap.set(assignee.id, {
assignee.id, id: assignee.id,
{ avatar_url: assignee.avatar_url,
id: assignee.id, display_name: assignee.display_name,
avatar_url: assignee.avatar_url, } as IUserLite);
display_name: assignee.display_name,
} as IUserLite
);
}); });
}); });
@ -151,14 +140,11 @@ const buildRequesters = (requests: TExternalContourRequest[]): IUserLite[] => {
if (!requesterId || !requesterName || memberMap.has(requesterId)) return; if (!requesterId || !requesterName || memberMap.has(requesterId)) return;
memberMap.set( memberMap.set(requesterId, {
requesterId, id: requesterId,
{ avatar_url: request.issue.created_by_detail?.avatar_url,
id: requesterId, display_name: requesterName,
avatar_url: request.issue.created_by_detail?.avatar_url, } as IUserLite);
display_name: requesterName,
} as IUserLite
);
}); });
return sortByName(Array.from(memberMap.values())); return sortByName(Array.from(memberMap.values()));

View File

@ -7,6 +7,7 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// hooks // hooks
import { getStateGroupColor } from "@plane/propel/icons";
import type { ISearchIssueResponse } from "@plane/types"; import type { ISearchIssueResponse } from "@plane/types";
// plane web hooks // plane web hooks
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier"; import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
@ -19,7 +20,7 @@ interface Props {
export const BulkDeleteIssuesModalItem = observer(function BulkDeleteIssuesModalItem(props: Props) { export const BulkDeleteIssuesModalItem = observer(function BulkDeleteIssuesModalItem(props: Props) {
const { issue, canDeleteIssueIds } = props; const { issue, canDeleteIssueIds } = props;
const color = issue.state__color; const color = getStateGroupColor(issue.state__group, issue.state__color);
return ( return (
<Combobox.Option <Combobox.Option

View File

@ -11,7 +11,7 @@ import { Combobox } from "@headlessui/react";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { SearchIcon, CloseIcon } from "@plane/propel/icons"; import { SearchIcon, CloseIcon, getStateGroupColor } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
@ -263,7 +263,7 @@ export function ExistingIssuesListModal(props: Props) {
<span <span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full" className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: issue.state__color, backgroundColor: getStateGroupColor(issue.state__group, issue.state__color),
}} }}
/> />
<span className="flex-shrink-0"> <span className="flex-shrink-0">

View File

@ -81,7 +81,7 @@ export const BlockRow = observer(function BlockRow(props: Props) {
return ( return (
<div <div
className="relative w-max min-w-full" className="nodedc-project-gantt-row relative w-max min-w-full"
onMouseEnter={() => updateActiveBlockId(blockId)} onMouseEnter={() => updateActiveBlockId(blockId)}
onMouseLeave={() => updateActiveBlockId(null)} onMouseLeave={() => updateActiveBlockId(null)}
style={{ style={{
@ -89,19 +89,18 @@ export const BlockRow = observer(function BlockRow(props: Props) {
}} }}
> >
<div <div
className={cn("relative h-full bg-layer-transparent hover:bg-layer-transparent-hover", { className={cn("nodedc-project-gantt-row-bg relative h-full", {
"rounded-l-sm border border-r-0 border-accent-strong": getIsIssuePeeked(block.data.id), "nodedc-project-gantt-row-peeked": getIsIssuePeeked(block.data.id),
"bg-layer-transparent-hover": isBlockHoveredOn, "nodedc-project-gantt-row-hovered": isBlockHoveredOn,
"bg-accent-primary/5 hover:bg-accent-primary/10": isBlockSelected, "nodedc-project-gantt-row-selected": isBlockSelected,
"bg-accent-primary/10": isBlockSelected && isBlockHoveredOn, "nodedc-project-gantt-row-focused": isBlockFocused,
"border border-r-0 border-strong-1": isBlockFocused,
})} })}
> >
{isBlockVisibleOnChart {isBlockVisibleOnChart
? isHidden && ( ? isHidden && (
<button <button
type="button" type="button"
className="sticky z-[5] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center rounded-sm border border-strong bg-layer-1 text-secondary hover:text-primary" className="nodedc-project-gantt-jump-button sticky z-[5] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center text-secondary hover:text-primary"
style={{ style={{
left: `${SIDEBAR_WIDTH + 4}px`, left: `${SIDEBAR_WIDTH + 4}px`,
}} }}

View File

@ -83,7 +83,7 @@ export const GanttChartBlock = observer(function GanttChartBlock(props: Props) {
horizontalOffset={100} horizontalOffset={100}
verticalOffset={200} verticalOffset={200}
classNames="flex h-full w-full items-center" classNames="flex h-full w-full items-center"
placeholderChildren={<div className="h-8 w-full rounded-sm bg-layer-1" />} placeholderChildren={<div className="nodedc-project-gantt-block-placeholder h-8 w-full" />}
shouldRecordHeights={false} shouldRecordHeights={false}
forceRender={isCurrentDependencyDragging} forceRender={isCurrentDependencyDragging}
> >

View File

@ -5,7 +5,7 @@
*/ */
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Expand, Shrink } from "lucide-react"; import { CalendarDays, Expand, Shrink } from "lucide-react";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// plane // plane
import type { TGanttViews } from "@plane/types"; import type { TGanttViews } from "@plane/types";
@ -25,62 +25,81 @@ type Props = {
handleChartView: (view: TGanttViews) => void; handleChartView: (view: TGanttViews) => void;
handleToday: () => void; handleToday: () => void;
loaderTitle: string; loaderTitle: string;
title: string;
toggleFullScreenMode: () => void; toggleFullScreenMode: () => void;
showToday: boolean; showToday: boolean;
}; };
const GANTT_VIEW_SHORT_LABELS: Record<TGanttViews, string> = {
week: "1W",
month: "1M",
quarter: "3M",
};
export const GanttChartHeader = observer(function GanttChartHeader(props: Props) { export const GanttChartHeader = observer(function GanttChartHeader(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } = const {
props; blockIds,
fullScreenMode,
handleChartView,
handleToday,
loaderTitle,
title,
toggleFullScreenMode,
showToday,
} = props;
// chart hook // chart hook
const { currentView } = useTimeLineChartStore(); const { currentView } = useTimeLineChartStore();
return ( return (
<Row <Row
className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 bg-surface-1 py-2 whitespace-nowrap" className="nodedc-project-gantt-toolbar relative flex w-full flex-shrink-0 flex-wrap items-center justify-between gap-3 whitespace-nowrap"
style={{ height: `${GANTT_BREADCRUMBS_HEIGHT}px` }} style={{ height: `${GANTT_BREADCRUMBS_HEIGHT}px` }}
> >
<div className="ml-auto"> <div className="flex min-w-0 items-center gap-3">
<div className="ml-auto text-11 font-medium text-tertiary"> <div className="nodedc-project-gantt-toolbar-icon">
{blockIds ? `${blockIds.length} ${loaderTitle}` : t("common.loading")} <CalendarDays className="size-4" />
</div>
<div className="min-w-0">
<div className="truncate text-14 font-semibold text-primary">{title}</div>
<div className="mt-0.5 truncate text-11 font-medium text-tertiary">
{currentView ? GANTT_VIEW_SHORT_LABELS[currentView] : null}
{currentView ? " / " : null}
{blockIds ? `${blockIds.length} ${loaderTitle}` : t("common.loading")}
</div>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center justify-end gap-2">
{showToday && (
<button
type="button"
className="nodedc-project-gantt-chip nodedc-project-gantt-chip-live"
onClick={handleToday}
>
Live
</button>
)}
{VIEWS_LIST.map((chartView: any) => ( {VIEWS_LIST.map((chartView: any) => (
<div <button
key={chartView?.key} key={chartView?.key}
className={cn( type="button"
"cursor-pointer rounded-md bg-layer-transparent p-1 px-2 text-11 hover:bg-layer-transparent-hover", aria-label={t(chartView?.i18n_title)}
{ aria-pressed={currentView === chartView?.key}
"bg-layer-transparent-selected": currentView === chartView?.key, className={cn("nodedc-project-gantt-chip", {
} "nodedc-project-gantt-chip-active": currentView === chartView?.key,
)} })}
onClick={() => handleChartView(chartView?.key)} onClick={() => handleChartView(chartView?.key)}
> >
{t(chartView?.i18n_title)} {GANTT_VIEW_SHORT_LABELS[chartView?.key as TGanttViews]}
</div> </button>
))} ))}
</div>
{showToday && ( <button type="button" className="nodedc-project-gantt-round-button" onClick={toggleFullScreenMode}>
<button {fullScreenMode ? <Shrink className="size-4" /> : <Expand className="size-4" />}
type="button"
className="rounded-md bg-layer-transparent p-1 px-2 text-11 hover:bg-layer-transparent-hover"
onClick={handleToday}
>
{t("common.today")}
</button> </button>
)} </div>
<button
type="button"
className="flex items-center justify-center rounded-md border border-subtle bg-layer-transparent p-1 transition-all hover:bg-layer-transparent-hover"
onClick={toggleFullScreenMode}
>
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
</button>
</Row> </Row>
); );
}); });

View File

@ -177,7 +177,7 @@ export const GanttChartMainContent = observer(function GanttChartMainContent(pro
// DO NOT REMOVE THE ID // DO NOT REMOVE THE ID
id="gantt-container" id="gantt-container"
className={cn( className={cn(
"vertical-scrollbar horizontal-scrollbar flex scrollbar-lg h-full w-full overflow-auto border-t-[0.5px] border-subtle", "nodedc-project-gantt-scroll vertical-scrollbar horizontal-scrollbar flex scrollbar-lg h-full w-full overflow-auto",
{ {
"mb-8": bottomSpacing, "mb-8": bottomSpacing,
} }
@ -199,11 +199,11 @@ export const GanttChartMainContent = observer(function GanttChartMainContent(pro
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
isEpic={isEpic} isEpic={isEpic}
/> />
<div className="relative h-max min-h-full flex-shrink-0 flex-grow"> <div className="nodedc-project-gantt-stage relative h-max min-h-full flex-shrink-0 flex-grow">
<ActiveChartView /> <ActiveChartView />
{currentViewData && ( {currentViewData && (
<div <div
className="relative h-full" className="nodedc-project-gantt-layer relative h-full"
style={{ style={{
width: `${itemsContainerWidth}px`, width: `${itemsContainerWidth}px`,
transform: `translateY(${HEADER_HEIGHT}px)`, transform: `translateY(${HEADER_HEIGHT}px)`,

View File

@ -180,8 +180,8 @@ export const ChartViewRoot = observer(function ChartViewRoot(props: ChartViewRoo
const content = ( const content = (
<div <div
className={cn("shadow relative flex h-full flex-col rounded-xs bg-surface-1 select-none", { className={cn("nodedc-project-gantt-card shadow relative flex h-full flex-col select-none", {
"inset-0 z-[25] bg-surface-1": fullScreenMode, "fixed inset-0 z-[25] rounded-none": fullScreenMode,
"border-[0.5px] border-subtle": border, "border-[0.5px] border-subtle": border,
})} })}
> >
@ -192,6 +192,7 @@ export const ChartViewRoot = observer(function ChartViewRoot(props: ChartViewRoo
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)} handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
handleToday={handleToday} handleToday={handleToday}
loaderTitle={loaderTitle} loaderTitle={loaderTitle}
title={title}
showToday={showToday} showToday={showToday}
/> />
<GanttChartMainContent <GanttChartMainContent

View File

@ -30,12 +30,12 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate); const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
return ( return (
<div className="absolute top-0 left-0 flex h-max min-h-full w-max"> <div className="nodedc-project-gantt-calendar absolute top-0 left-0 flex h-max min-h-full w-max">
{currentViewData && ( {currentViewData && (
<div className="relative flex flex-col outline-[0.25px] outline-subtle-1"> <div className="nodedc-project-gantt-calendar-group relative flex flex-col">
{/** Header Div */} {/** Header Div */}
<div <div
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1" className="nodedc-project-gantt-calendar-header sticky top-0 z-[5] w-full flex-shrink-0"
style={{ style={{
height: `${HEADER_HEIGHT}px`, height: `${HEADER_HEIGHT}px`,
}} }}
@ -45,18 +45,22 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
{months?.map((monthBlock) => ( {months?.map((monthBlock) => (
<div <div
key={`month-${monthBlock?.month}-${monthBlock?.year}`} key={`month-${monthBlock?.month}-${monthBlock?.year}`}
className="flex outline-[0.5px] outline-subtle-1" className="flex"
style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }} style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }}
> >
<div <div
className="sticky z-[1] m-1 flex items-center bg-surface-1 px-3 py-1 text-14 font-regular whitespace-nowrap text-secondary capitalize" className="nodedc-project-gantt-period-label sticky z-[1] m-1 flex items-center px-3 py-1 text-12 font-semibold whitespace-nowrap text-secondary capitalize"
style={{ style={{
left: `${SIDEBAR_WIDTH}px`, left: `${SIDEBAR_WIDTH}px`,
}} }}
> >
{monthBlock?.title} {monthBlock?.title}
{monthBlock.today && ( {monthBlock.today && (
<span className={cn("ml-2 rounded-sm bg-accent-primary px-1 text-9 font-medium text-on-color")}> <span
className={cn(
"ml-2 rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-2 py-0.5 text-9 font-semibold text-[rgb(var(--nodedc-on-card-active-rgb))]"
)}
>
Current Current
</span> </span>
)} )}
@ -70,9 +74,9 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
<div <div
key={`sub-title-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`} key={`sub-title-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
className={cn( className={cn(
"flex flex-shrink-0 justify-between px-2 py-1 text-center capitalize outline-[0.25px] outline-subtle-1", "nodedc-project-gantt-subcell flex flex-shrink-0 justify-between px-2 py-1 text-center capitalize",
{ {
"bg-accent-primary/20": weekBlock.today, "nodedc-project-gantt-subcell-today": weekBlock.today,
} }
)} )}
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }} style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
@ -80,7 +84,8 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
<div className="space-x-1 text-11 font-medium text-placeholder"> <div className="space-x-1 text-11 font-medium text-placeholder">
<span <span
className={cn({ className={cn({
"rounded-sm bg-accent-primary px-1 text-on-color": weekBlock.today, "rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-1.5 text-[rgb(var(--nodedc-on-card-active-rgb))]":
weekBlock.today,
})} })}
> >
{weekBlock.startDate.getDate()}-{weekBlock.endDate.getDate()} {weekBlock.startDate.getDate()}-{weekBlock.endDate.getDate()}
@ -96,8 +101,8 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
{weeks?.map((weekBlock) => ( {weeks?.map((weekBlock) => (
<div <div
key={`column-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`} key={`column-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", { className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
"bg-accent-primary/20": weekBlock.today, "nodedc-project-gantt-column-today": weekBlock.today,
})} })}
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }} style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
/> />

View File

@ -21,16 +21,16 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks); const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
return ( return (
<div className={`absolute top-0 left-0 flex h-max min-h-full w-max`}> <div className="nodedc-project-gantt-calendar absolute top-0 left-0 flex h-max min-h-full w-max">
{currentViewData && {currentViewData &&
quarterBlocks?.map((quarterBlock, rootIndex) => ( quarterBlocks?.map((quarterBlock, rootIndex) => (
<div <div
key={`month-${quarterBlock.quarterNumber}-${quarterBlock.year}`} key={`month-${quarterBlock.quarterNumber}-${quarterBlock.year}`}
className="relative flex flex-col outline-[0.25px] outline-subtle-1" className="nodedc-project-gantt-calendar-group relative flex flex-col"
> >
{/** Header Div */} {/** Header Div */}
<div <div
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1 outline-[1px] outline-subtle-1" className="nodedc-project-gantt-calendar-header sticky top-0 z-[5] w-full flex-shrink-0"
style={{ style={{
height: `${HEADER_HEIGHT}px`, height: `${HEADER_HEIGHT}px`,
}} }}
@ -38,19 +38,23 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
{/** Main Quarter Title */} {/** Main Quarter Title */}
<div className="inline-flex h-7 w-full justify-between"> <div className="inline-flex h-7 w-full justify-between">
<div <div
className="sticky z-[1] my-1 flex items-center bg-surface-1 px-3 py-1 text-14 font-regular whitespace-nowrap text-secondary capitalize" className="nodedc-project-gantt-period-label sticky z-[1] my-1 flex items-center px-3 py-1 text-12 font-semibold whitespace-nowrap text-secondary capitalize"
style={{ style={{
left: `${SIDEBAR_WIDTH}px`, left: `${SIDEBAR_WIDTH}px`,
}} }}
> >
{quarterBlock?.title} {quarterBlock?.title}
{quarterBlock.today && ( {quarterBlock.today && (
<span className={cn("ml-2 rounded-sm bg-accent-primary px-1 text-9 font-medium text-on-color")}> <span
className={cn(
"ml-2 rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-2 py-0.5 text-9 font-semibold text-[rgb(var(--nodedc-on-card-active-rgb))]"
)}
>
Current Current
</span> </span>
)} )}
</div> </div>
<div className="sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize"> <div className="nodedc-project-gantt-period-meta sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
{quarterBlock.shortTitle} {quarterBlock.shortTitle}
</div> </div>
</div> </div>
@ -60,9 +64,9 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
<div <div
key={`sub-title-${rootIndex}-${index}`} key={`sub-title-${rootIndex}-${index}`}
className={cn( className={cn(
"flex flex-shrink-0 justify-center text-center capitalize outline-[0.25px] outline-subtle-1", "nodedc-project-gantt-subcell flex flex-shrink-0 justify-center text-center capitalize",
{ {
"bg-accent-primary/20": monthBlock.today, "nodedc-project-gantt-subcell-today": monthBlock.today,
} }
)} )}
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }} style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
@ -70,7 +74,8 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
<div className="flex h-full items-center justify-center space-x-1 text-11 font-medium"> <div className="flex h-full items-center justify-center space-x-1 text-11 font-medium">
<span <span
className={cn({ className={cn({
"rounded-lg bg-accent-primary px-2 text-on-color": monthBlock.today, "rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-2 text-[rgb(var(--nodedc-on-card-active-rgb))]":
monthBlock.today,
})} })}
> >
{monthBlock.monthData.shortTitle} {monthBlock.monthData.shortTitle}
@ -85,8 +90,8 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
{quarterBlock?.children?.map((monthBlock, index) => ( {quarterBlock?.children?.map((monthBlock, index) => (
<div <div
key={`column-${rootIndex}-${index}`} key={`column-${rootIndex}-${index}`}
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", { className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
"bg-accent-primary/20": monthBlock.today, "nodedc-project-gantt-column-today": monthBlock.today,
})} })}
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }} style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
/> />

View File

@ -18,16 +18,16 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
const weekBlocks: IWeekBlock[] = renderView; const weekBlocks: IWeekBlock[] = renderView;
return ( return (
<div className={`absolute top-0 left-0 flex h-max min-h-full w-max`}> <div className="nodedc-project-gantt-calendar absolute top-0 left-0 flex h-max min-h-full w-max">
{currentViewData && {currentViewData &&
weekBlocks?.map((block, rootIndex) => ( weekBlocks?.map((block, rootIndex) => (
<div <div
key={`month-${block?.startDate.toString()}-${block?.endDate.toString()}`} key={`month-${block?.startDate.toString()}-${block?.endDate.toString()}`}
className="relative flex flex-col outline-[0.25px] outline-subtle-1" className="nodedc-project-gantt-calendar-group relative flex flex-col"
> >
{/** Header Div */} {/** Header Div */}
<div <div
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1 outline-[1px] outline-subtle-1" className="nodedc-project-gantt-calendar-header sticky top-0 z-[5] w-full flex-shrink-0"
style={{ style={{
height: `${HEADER_HEIGHT}px`, height: `${HEADER_HEIGHT}px`,
}} }}
@ -35,14 +35,14 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
{/** Main Months Title */} {/** Main Months Title */}
<div className="inline-flex h-7 w-full justify-between"> <div className="inline-flex h-7 w-full justify-between">
<div <div
className="sticky z-[1] m-1 flex items-center bg-surface-1 px-3 py-1 text-13 font-regular whitespace-nowrap text-secondary capitalize" className="nodedc-project-gantt-period-label sticky z-[1] m-1 flex items-center px-3 py-1 text-12 font-semibold whitespace-nowrap text-secondary capitalize"
style={{ style={{
left: `${SIDEBAR_WIDTH}px`, left: `${SIDEBAR_WIDTH}px`,
}} }}
> >
{block?.title} {block?.title}
</div> </div>
<div className="sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize"> <div className="nodedc-project-gantt-period-meta sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
{block?.weekData?.title} {block?.weekData?.title}
</div> </div>
</div> </div>
@ -52,9 +52,9 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
<div <div
key={`sub-title-${rootIndex}-${index}`} key={`sub-title-${rootIndex}-${index}`}
className={cn( className={cn(
"flex flex-shrink-0 justify-between p-1 text-center capitalize outline-[0.25px] outline-subtle-1", "nodedc-project-gantt-subcell flex flex-shrink-0 justify-between p-1 text-center capitalize",
{ {
"bg-accent-primary/20": weekDay.today, "nodedc-project-gantt-subcell-today": weekDay.today,
} }
)} )}
style={{ width: `${currentViewData?.data.dayWidth}px` }} style={{ width: `${currentViewData?.data.dayWidth}px` }}
@ -63,7 +63,8 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
<div className="space-x-1 text-11 font-medium"> <div className="space-x-1 text-11 font-medium">
<span <span
className={cn({ className={cn({
"rounded-sm bg-accent-primary px-1 text-on-color": weekDay.today, "rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-1.5 text-[rgb(var(--nodedc-on-card-active-rgb))]":
weekDay.today,
})} })}
> >
{weekDay.date.getDate()} {weekDay.date.getDate()}
@ -74,17 +75,17 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
</div> </div>
</div> </div>
{/** Day Columns */} {/** Day Columns */}
<div className="flex h-full w-full flex-grow bg-surface-1"> <div className="flex h-full w-full flex-grow">
{block?.children?.map((weekDay, index) => ( {block?.children?.map((weekDay, index) => (
<div <div
key={`column-${rootIndex}-${index}`} key={`column-${rootIndex}-${index}`}
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", { className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
"bg-accent-primary/20": weekDay.today, "nodedc-project-gantt-column-today": weekDay.today,
})} })}
style={{ width: `${currentViewData?.data.dayWidth}px` }} style={{ width: `${currentViewData?.data.dayWidth}px` }}
> >
{["sat", "sun"].includes(weekDay?.dayData?.shortTitle) && ( {["sat", "sun"].includes(weekDay?.dayData?.shortTitle) && (
<div className="h-full bg-surface-2 outline-[0.25px] outline-strong" /> <div className="nodedc-project-gantt-column-weekend h-full" />
)} )}
</div> </div>
))} ))}

View File

@ -4,11 +4,11 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
export const BLOCK_HEIGHT = 44; export const BLOCK_HEIGHT = 46;
export const HEADER_HEIGHT = 48; export const HEADER_HEIGHT = 56;
export const GANTT_BREADCRUMBS_HEIGHT = 40; export const GANTT_BREADCRUMBS_HEIGHT = 60;
export const SIDEBAR_WIDTH = 360; export const SIDEBAR_WIDTH = 360;

View File

@ -39,7 +39,7 @@ export const LeftResizable = observer(function LeftResizable(props: LeftResizabl
<> <>
{(isHovering || isLeftResizing) && dateString && ( {(isHovering || isLeftResizing) && dateString && (
<div className="absolute -left-36 flex h-full w-32 items-center justify-end text-11 font-regular text-tertiary"> <div className="absolute -left-36 flex h-full w-32 items-center justify-end text-11 font-regular text-tertiary">
<div className="rounded-sm bg-accent-subtle px-2 py-1">{dateString}</div> <div className="nodedc-project-gantt-resize-tooltip px-2 py-1">{dateString}</div>
</div> </div>
)} )}
<div <div
@ -56,7 +56,7 @@ export const LeftResizable = observer(function LeftResizable(props: LeftResizabl
/> />
<div <div
className={cn( className={cn(
"absolute top-1/2 left-1 z-[5] h-7 w-1 -translate-y-1/2 rounded-xs bg-surface-1 opacity-0 transition-all duration-300 group-hover:opacity-100", "nodedc-project-gantt-resize-handle absolute top-1/2 left-1 z-[5] h-7 w-1 -translate-y-1/2 opacity-0 transition-all duration-300 group-hover:opacity-100",
{ {
"-left-1.5 opacity-100": isLeftResizing, "-left-1.5 opacity-100": isLeftResizing,
} }

View File

@ -39,7 +39,7 @@ export const RightResizable = observer(function RightResizable(props: RightResiz
<> <>
{(isHovering || isRightResizing) && dateString && ( {(isHovering || isRightResizing) && dateString && (
<div className="absolute -right-36 z-[10] flex h-full w-32 items-center justify-start text-11 font-regular text-tertiary"> <div className="absolute -right-36 z-[10] flex h-full w-32 items-center justify-start text-11 font-regular text-tertiary">
<div className="rounded-sm bg-accent-subtle px-2 py-1">{dateString}</div> <div className="nodedc-project-gantt-resize-tooltip px-2 py-1">{dateString}</div>
</div> </div>
)} )}
<div <div
@ -54,7 +54,7 @@ export const RightResizable = observer(function RightResizable(props: RightResiz
/> />
<div <div
className={cn( className={cn(
"absolute top-1/2 right-1 z-[5] h-7 w-1 -translate-y-1/2 rounded-xs bg-surface-1 opacity-0 transition-all duration-300 group-hover:opacity-100", "nodedc-project-gantt-resize-handle absolute top-1/2 right-1 z-[5] h-7 w-1 -translate-y-1/2 opacity-0 transition-all duration-300 group-hover:opacity-100",
{ {
"-right-1.5 opacity-100": isRightResizing, "-right-1.5 opacity-100": isRightResizing,
} }

View File

@ -43,7 +43,7 @@ export const ChartDraggable = observer(function ChartDraggable(props: Props) {
} = props; } = props;
return ( return (
<div className="group relative z-[5] inline-flex h-full w-full cursor-pointer items-center font-medium transition-all"> <div className="nodedc-project-gantt-draggable group relative z-[5] inline-flex h-full w-full cursor-pointer items-center font-medium transition-all">
{/* left resize drag handle */} {/* left resize drag handle */}
{(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && ( {(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && (
<LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} /> <LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
@ -55,7 +55,7 @@ export const ChartDraggable = observer(function ChartDraggable(props: Props) {
position={block.position} position={block.position}
/> />
<div <div
className={cn("relative z-[6] flex h-8 w-full items-center rounded-sm", { className={cn("nodedc-project-gantt-draggable-shell relative z-[6] flex h-8 w-full items-center", {
"pointer-events-none": isMoving, "pointer-events-none": isMoving,
})} })}
onMouseDown={(e) => enableBlockMove && handleBlockDrag(e, "move")} onMouseDown={(e) => enableBlockMove && handleBlockDrag(e, "move")}

View File

@ -44,23 +44,19 @@ export const IssuesSidebarBlock = observer(function IssuesSidebarBlock(props: Pr
return ( return (
<div <div
className={cn("group/list-block", { className={cn("nodedc-project-gantt-sidebar-block group/list-block", {
"rounded-sm bg-layer-1": isDragging, "nodedc-project-gantt-sidebar-block-dragging": isDragging,
"rounded-l-sm border border-r-0 border-accent-strong": getIsIssuePeeked(block.data.id), "nodedc-project-gantt-sidebar-block-peeked": getIsIssuePeeked(block.data.id),
"border border-r-0 border-strong-1": isIssueFocused, "nodedc-project-gantt-sidebar-block-focused": isIssueFocused,
})} })}
onMouseEnter={() => updateActiveBlockId(block.id)} onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)} onMouseLeave={() => updateActiveBlockId(null)}
> >
<Row <Row
className={cn( className={cn("nodedc-project-gantt-sidebar-row group flex w-full items-center gap-2 pr-4", {
"group flex w-full items-center gap-2 bg-layer-transparent pr-4 hover:bg-layer-transparent-hover", "nodedc-project-gantt-sidebar-row-hovered": isBlockHoveredOn,
{ "nodedc-project-gantt-sidebar-row-selected": isIssueSelected,
"bg-layer-transparent-hover": isBlockHoveredOn, })}
"bg-accent-primary/5 hover:bg-accent-primary/10": isIssueSelected,
"bg-accent-primary/10": isIssueSelected && isBlockHoveredOn,
}
)}
style={{ style={{
height: `${BLOCK_HEIGHT}px`, height: `${BLOCK_HEIGHT}px`,
}} }}

View File

@ -77,7 +77,7 @@ export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Prop
}; };
return ( return (
<div> <div className="nodedc-project-gantt-sidebar-list">
{blockIds ? ( {blockIds ? (
<> <>
{blockIds.map((blockId, index) => { {blockIds.map((blockId, index) => {
@ -117,7 +117,7 @@ export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Prop
})} })}
{canLoadMoreBlocks && ( {canLoadMoreBlocks && (
<div ref={setIntersectionElement} className="p-2"> <div ref={setIntersectionElement} className="p-2">
<div className="flex h-10 w-full animate-pulse items-center justify-between gap-1.5 rounded-sm bg-layer-1 px-4 py-1.5 md:h-8 md:px-1" /> <div className="nodedc-project-gantt-sidebar-loader flex h-10 w-full animate-pulse items-center justify-between gap-1.5 px-4 py-1.5 md:h-8 md:px-1" />
</div> </div>
)} )}
</> </>

View File

@ -56,14 +56,14 @@ export const GanttChartSidebar = observer(function GanttChartSidebar(props: Prop
<Row <Row
// DO NOT REMOVE THE ID // DO NOT REMOVE THE ID
id="gantt-sidebar" id="gantt-sidebar"
className="sticky left-0 z-10 h-max min-h-full flex-shrink-0 border-r-[0.5px] border-subtle-1 bg-surface-1" className="nodedc-project-gantt-sidebar sticky left-0 z-10 h-max min-h-full flex-shrink-0"
style={{ style={{
width: `${SIDEBAR_WIDTH}px`, width: `${SIDEBAR_WIDTH}px`,
}} }}
variant={ERowVariant.HUGGING} variant={ERowVariant.HUGGING}
> >
<Row <Row
className="group/list-header sticky top-0 z-10 box-border flex flex-shrink-0 items-end justify-between gap-2 border-b-[0.5px] border-subtle-1 bg-surface-1 pr-4 pb-2 text-13 font-medium text-tertiary" className="nodedc-project-gantt-sidebar-header group/list-header sticky top-0 z-10 box-border flex flex-shrink-0 items-end justify-between gap-2 pr-4 pb-3 text-12 font-semibold text-tertiary"
style={{ style={{
height: `${HEADER_HEIGHT}px`, height: `${HEADER_HEIGHT}px`,
}} }}
@ -88,7 +88,7 @@ export const GanttChartSidebar = observer(function GanttChartSidebar(props: Prop
<h6>{t("common.duration")}</h6> <h6>{t("common.duration")}</h6>
</Row> </Row>
<Row variant={ERowVariant.HUGGING} className="h-max min-h-full bg-surface-1"> <Row variant={ERowVariant.HUGGING} className="nodedc-project-gantt-sidebar-body h-max min-h-full">
{sidebarToRender && {sidebarToRender &&
sidebarToRender({ sidebarToRender({
title, title,

View File

@ -34,11 +34,13 @@ const EXTERNAL_DECK_LIMIT = 10;
const INTERNAL_CURSOR = `${INTERNAL_DECK_LIMIT}:0:0`; const INTERNAL_CURSOR = `${INTERNAL_DECK_LIMIT}:0:0`;
type HomeRecentIssueDecksProps = { type HomeRecentIssueDecksProps = {
compact?: boolean;
project?: THomeProjectData; project?: THomeProjectData;
workspaceSlug: string; workspaceSlug: string;
}; };
type DeckSectionProps = { type DeckSectionProps = {
compact?: boolean;
count: number; count: number;
description: string; description: string;
emptyDescription: string; emptyDescription: string;
@ -49,6 +51,7 @@ type DeckSectionProps = {
}; };
type InternalIssueCardProps = { type InternalIssueCardProps = {
compact?: boolean;
isActive: boolean; isActive: boolean;
issue: TIssue; issue: TIssue;
onSelect: () => void; onSelect: () => void;
@ -56,6 +59,7 @@ type InternalIssueCardProps = {
}; };
type ExternalIssueCardProps = { type ExternalIssueCardProps = {
compact?: boolean;
isActive: boolean; isActive: boolean;
onSelect: () => void; onSelect: () => void;
project: THomeProjectData; project: THomeProjectData;
@ -76,14 +80,14 @@ const sortByRecentCreatedDate = <
}; };
const DeckSection = (props: DeckSectionProps) => { const DeckSection = (props: DeckSectionProps) => {
const { count, description, emptyDescription, emptyTitle, isLoading, items, title } = props; const { compact = false, count, description, emptyDescription, emptyTitle, isLoading, items, title } = props;
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<div className="text-14 font-semibold text-primary">{title}</div> <div className="text-14 font-semibold text-primary">{title}</div>
<div className="text-12 text-secondary">{description}</div> {!compact && <div className="text-12 text-secondary">{description}</div>}
</div> </div>
<div className="nodedc-toolbar-pill inline-flex items-center gap-2"> <div className="nodedc-toolbar-pill inline-flex items-center gap-2">
<Sparkles className="size-3.5" /> <Sparkles className="size-3.5" />
@ -91,16 +95,24 @@ const DeckSection = (props: DeckSectionProps) => {
</div> </div>
</div> </div>
<div className="nodedc-home-task-deck-scroller"> <div
<div className="flex min-h-[236px] items-end px-1 py-4"> className={cn("nodedc-home-task-deck-scroller", {
"nodedc-home-task-deck-scroller-compact": compact,
})}
>
<div
className={cn("flex items-start px-1 py-2", {
"min-h-[236px] gap-4": !compact,
"nodedc-home-task-deck-row-compact min-h-[172px]": compact,
})}
>
{isLoading {isLoading
? Array.from({ length: 4 }, (_, index) => ( ? Array.from({ length: 4 }, (_, index) => (
<div <div
key={`skeleton-${title}-${index}`} key={`skeleton-${title}-${index}`}
className={cn("nodedc-home-task-card nodedc-home-task-card-skeleton animate-pulse", { className={cn("nodedc-home-task-card nodedc-home-task-card-skeleton animate-pulse", {
"-ml-16": index > 0, "nodedc-home-task-card-compact": compact,
})} })}
style={{ zIndex: 5 - index }}
/> />
)) ))
: items} : items}
@ -118,7 +130,7 @@ const DeckSection = (props: DeckSectionProps) => {
}; };
const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCard(props: InternalIssueCardProps) { const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCard(props: InternalIssueCardProps) {
const { isActive, issue, onSelect, project } = props; const { compact = false, isActive, issue, onSelect, project } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
@ -183,7 +195,7 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
const footer = ( const footer = (
<> <>
<div className={cn("inline-flex min-h-9 items-center rounded-full pl-1 pr-2", pillBackgroundClasses)}> <div className={cn("inline-flex min-h-9 items-center rounded-full pr-2 pl-1", pillBackgroundClasses)}>
{(issue.assignee_ids?.length ?? 0) > 0 ? ( {(issue.assignee_ids?.length ?? 0) > 0 ? (
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" /> <ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
) : ( ) : (
@ -191,7 +203,12 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
)} )}
</div> </div>
<div className={cn("inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium", pillBackgroundClasses)}> <div
className={cn(
"inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium",
pillBackgroundClasses
)}
>
<CalendarDays className="h-3.5 w-3.5" /> <CalendarDays className="h-3.5 w-3.5" />
<span className="truncate">{dueDateLabel}</span> <span className="truncate">{dueDateLabel}</span>
</div> </div>
@ -199,14 +216,24 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
); );
return ( return (
<button type="button" className="nodedc-home-task-card" data-active={isActive} onClick={onSelect} title={issue.name}> <button
type="button"
className={cn("nodedc-home-task-card", { "nodedc-home-task-card-compact": compact })}
data-active={isActive}
onClick={onSelect}
title={issue.name}
>
<NodedcWorkItemCard <NodedcWorkItemCard
isActive={isActive} isActive={isActive}
surfaceClassName={cn( surfaceClassName={cn(
"nodedc-home-task-card-surface px-0", "nodedc-home-task-card-surface px-0",
compact && "!rounded-[24px]",
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive" isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
)} )}
contentClassName="px-1" contentClassName={cn("px-1", compact && "min-h-[168px]")}
titleContainerClassName={cn(compact && "px-3 py-3")}
titleClassName={cn(compact && "text-base leading-5")}
footerClassName={cn(compact && "gap-2")}
header={header} header={header}
subtitle={sourceContourName} subtitle={sourceContourName}
title={issue.name} title={issue.name}
@ -217,15 +244,13 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
}); });
const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCard(props: ExternalIssueCardProps) { const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCard(props: ExternalIssueCardProps) {
const { isActive, onSelect, project, request } = props; const { compact = false, isActive, onSelect, project, request } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { getStateById } = useProjectState(); const { getStateById } = useProjectState();
const { iconBubbleClasses, pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive); const { iconBubbleClasses, pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
const issue = request.issue; const issue = request.issue;
const isOutgoing = request.direction const isOutgoing = request.direction ? request.direction === "outgoing" : request.source_project_id === project.id;
? request.direction === "outgoing"
: request.source_project_id === project.id;
const requester = const requester =
request.requested_by?.display_name || request.requested_by?.display_name ||
request.requested_by_name || request.requested_by_name ||
@ -242,15 +267,28 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none"); const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
return ( return (
<button type="button" className="nodedc-home-task-card" data-active={isActive} onClick={onSelect} title={issue.name}> <button
type="button"
className={cn("nodedc-home-task-card", { "nodedc-home-task-card-compact": compact })}
data-active={isActive}
onClick={onSelect}
title={issue.name}
>
<div <div
data-active={isActive} data-active={isActive}
className={cn( className={cn(
"nodedc-external-card nodedc-home-task-card-surface relative flex min-h-[220px] w-full flex-col p-4", "nodedc-external-card nodedc-home-task-card-surface relative flex w-full flex-col",
compact ? "min-h-[168px] rounded-[24px] p-3" : "min-h-[220px] p-4",
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive" isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
)} )}
> >
<div className={cn("relative flex min-h-[220px] flex-col px-1", isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white")}> <div
className={cn(
"relative flex flex-col px-1",
compact ? "min-h-[168px]" : "min-h-[220px]",
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white/70"
)}
>
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 flex-1 items-center gap-3"> <div className="flex min-w-0 flex-1 items-center gap-3">
@ -283,7 +321,7 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
<div <div
className={cn( className={cn(
"truncate -mt-0.5 pl-8 text-[11px] font-medium leading-4", "-mt-0.5 truncate pl-8 text-[11px] leading-4 font-medium",
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/72" : "text-[#B3B3B8]" isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/72" : "text-[#B3B3B8]"
)} )}
> >
@ -291,12 +329,21 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
</div> </div>
</div> </div>
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center"> <div
<div className="line-clamp-4 max-w-full text-lg font-semibold leading-6">{issue.name}</div> className={cn("flex flex-1 items-center justify-center text-center", compact ? "px-3 py-3" : "px-5 py-4")}
>
<div
className={cn(
"line-clamp-4 max-w-full font-semibold",
compact ? "text-base leading-5" : "text-lg leading-6"
)}
>
{issue.name}
</div>
</div> </div>
<div className="flex items-center justify-between gap-3"> <div className={cn("flex items-center justify-between", compact ? "gap-2" : "gap-3")}>
<div className={cn("inline-flex min-h-9 items-center rounded-full pl-1 pr-2", pillBackgroundClasses)}> <div className={cn("inline-flex min-h-9 items-center rounded-full pr-2 pl-1", pillBackgroundClasses)}>
{(issue.assignee_ids?.length ?? 0) > 0 ? ( {(issue.assignee_ids?.length ?? 0) > 0 ? (
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" /> <ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
) : ( ) : (
@ -304,7 +351,12 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
)} )}
</div> </div>
<div className={cn("inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium", pillBackgroundClasses)}> <div
className={cn(
"inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium",
pillBackgroundClasses
)}
>
<CalendarDays className="h-3.5 w-3.5" /> <CalendarDays className="h-3.5 w-3.5" />
<span className="truncate">{dueDateLabel}</span> <span className="truncate">{dueDateLabel}</span>
</div> </div>
@ -316,7 +368,7 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
}); });
export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props: HomeRecentIssueDecksProps) { export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props: HomeRecentIssueDecksProps) {
const { project, workspaceSlug } = props; const { compact = false, project, workspaceSlug } = props;
const [selectedInternalIssueId, setSelectedInternalIssueId] = useState<string | null>(null); const [selectedInternalIssueId, setSelectedInternalIssueId] = useState<string | null>(null);
const [selectedExternalRequestId, setSelectedExternalRequestId] = useState<string | null>(null); const [selectedExternalRequestId, setSelectedExternalRequestId] = useState<string | null>(null);
@ -382,9 +434,13 @@ export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props
if (!project) { if (!project) {
return ( return (
<HomeCardShell <HomeCardShell
eyebrow="Task Decks" eyebrow={compact ? "Последние задачи" : "Task Decks"}
title="Последние задачи по проекту" title={compact ? "Последние задачи проекта" : "Последние задачи по проекту"}
description="Выберите проект слева, и здесь появятся колоды последних задач внешнего и внутреннего контуров." description={
compact
? undefined
: "Выберите проект слева, и здесь появятся колоды последних задач внешнего и внутреннего контуров."
}
> >
<div className="rounded-[24px] border border-white/6 bg-black/10 px-5 py-6 text-13 text-secondary"> <div className="rounded-[24px] border border-white/6 bg-black/10 px-5 py-6 text-13 text-secondary">
Фокус проекта пока не выбран. Фокус проекта пока не выбран.
@ -393,61 +449,63 @@ export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props
); );
} }
const internalIssueCards = internalIssues.map((issue, index) => ( const internalIssueCards = internalIssues.map((issue) => (
<div <HomeInternalContourDeckCard
compact={compact}
key={issue.id} key={issue.id}
className={cn({ "-ml-16": index > 0 })} issue={issue}
style={{ zIndex: issue.id === selectedInternalIssueId ? internalIssues.length + 6 : index + 1 }} isActive={issue.id === selectedInternalIssueId}
> onSelect={() => setSelectedInternalIssueId(issue.id)}
<HomeInternalContourDeckCard project={project}
issue={issue} />
isActive={issue.id === selectedInternalIssueId}
onSelect={() => setSelectedInternalIssueId(issue.id)}
project={project}
/>
</div>
)); ));
const externalIssueCards = externalRequests.map((request, index) => ( const externalIssueCards = externalRequests.map((request) => (
<div <HomeExternalContourDeckCard
compact={compact}
key={request.id} key={request.id}
className={cn({ "-ml-16": index > 0 })} isActive={request.id === selectedExternalRequestId}
style={{ zIndex: request.id === selectedExternalRequestId ? externalRequests.length + 6 : index + 1 }} onSelect={() => setSelectedExternalRequestId(request.id)}
> project={project}
<HomeExternalContourDeckCard request={request}
isActive={request.id === selectedExternalRequestId} />
onSelect={() => setSelectedExternalRequestId(request.id)}
project={project}
request={request}
/>
</div>
)); ));
return ( return (
<HomeCardShell <HomeCardShell
eyebrow={`${project.identifier} • последние задачи`} eyebrow={compact ? `${project.identifier} • последние задачи` : `${project.identifier} • последние задачи`}
title="Последние задачи проекта" title={compact ? "Последние задачи проекта" : "Последние задачи проекта"}
description="Ниже собраны две колоды для быстрого просмотра новых карточек во внешнем и внутреннем контурах." description={
contentClassName="space-y-5 p-5" compact
? undefined
: "Ниже собраны две колоды для быстрого просмотра новых карточек во внешнем и внутреннем контурах."
}
contentClassName={compact ? "space-y-3 p-4" : "grid gap-5 p-5 xl:grid-cols-2"}
> >
<DeckSection <DeckSection
compact={compact}
count={externalRequests.length} count={externalRequests.length}
description="Последние запросы и задачи внешнего контура по текущему проекту." description={
compact ? "Последние внешние карточки" : "Последние запросы и задачи внешнего контура по текущему проекту."
}
emptyDescription="У проекта пока нет внешних контурных задач. Когда появится первый обмен с контрагентом, он сразу попадет в эту колоду." emptyDescription="У проекта пока нет внешних контурных задач. Когда появится первый обмен с контрагентом, он сразу попадет в эту колоду."
emptyTitle="Внешний контур пока пуст" emptyTitle="Внешний контур пока пуст"
isLoading={isExternalRequestsLoading} isLoading={isExternalRequestsLoading}
items={externalIssueCards} items={externalIssueCards}
title="Последние задачи внешнего контура" title={compact ? "Внешний контур" : "Внешний контур"}
/> />
<DeckSection <DeckSection
compact={compact}
count={internalIssues.length} count={internalIssues.length}
description="Последние добавленные внутренние задачи выбранного проекта." description={
compact ? "Последние внутренние карточки" : "Последние добавленные внутренние задачи выбранного проекта."
}
emptyDescription="Во внутреннем контуре пока нет задач. Как только в проекте появится новая карточка, она ляжет сюда первой." emptyDescription="Во внутреннем контуре пока нет задач. Как только в проекте появится новая карточка, она ляжет сюда первой."
emptyTitle="Внутренний контур пока пуст" emptyTitle="Внутренний контур пока пуст"
isLoading={isInternalIssuesLoading} isLoading={isInternalIssuesLoading}
items={internalIssueCards} items={internalIssueCards}
title="Последние задачи внутреннего контура" title={compact ? "Внутренний контур" : "Внутренний контур"}
/> />
</HomeCardShell> </HomeCardShell>
); );

View File

@ -4,24 +4,20 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
// plane types
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import type { IUser, TProjectAnalyticsCount } from "@plane/types"; import type { IUser } from "@plane/types";
import { Avatar } from "@plane/ui";
import { getFileURL } from "@plane/utils";
import { useCurrentTime } from "@/hooks/use-current-time"; import { useCurrentTime } from "@/hooks/use-current-time";
import { getCompletionRate, type THomeProjectData } from "./home.utils";
export interface IUserGreetingsView { export interface IUserGreetingsView {
user: IUser; user: IUser;
workspaceName?: string | null; workspaceName?: string | null;
selectedProject?: THomeProjectData;
selectedProjectAnalytics?: TProjectAnalyticsCount;
} }
export function UserGreetingsView(props: IUserGreetingsView) { export function UserGreetingsView(props: IUserGreetingsView) {
const { user, workspaceName, selectedProject, selectedProjectAnalytics } = props; const { user, workspaceName } = props;
// current time hook
const { currentTime } = useCurrentTime(); const { currentTime } = useCurrentTime();
// store hooks
const { t, currentLocale } = useTranslation(); const { t, currentLocale } = useTranslation();
const hour = new Intl.DateTimeFormat(currentLocale, { const hour = new Intl.DateTimeFormat(currentLocale, {
@ -46,39 +42,37 @@ export function UserGreetingsView(props: IUserGreetingsView) {
}).format(currentTime); }).format(currentTime);
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening"; const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
const completionRate = getCompletionRate(selectedProjectAnalytics); const userName = `${user?.first_name ?? ""} ${user?.last_name ?? ""}`.trim() || user?.email || "Workspace admin";
const userEmail = user?.email || workspaceName || "NODE DC";
return ( return (
<section className="nodedc-home-card px-5 py-4"> <section className="nodedc-home-user-card">
<div className="grid items-stretch gap-3 xl:grid-cols-[minmax(0,1fr)_280px_220px]"> <div className="nodedc-home-user-card-orb" />
<div className="flex min-w-0 items-center"> <div className="relative z-[1] p-5">
<div className="text-[11px] font-semibold tracking-[0.22em] text-white/55 uppercase">
{workspaceName ?? "NODE DC"}
</div>
<div className="mt-5 flex items-start gap-4">
<div className="shrink-0 rounded-[26px] bg-white/14 p-1 backdrop-blur-xl">
<Avatar src={getFileURL(user?.avatar_url ?? "")} name={userName} size="lg" />
</div>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase"> <div className="text-12 text-white/58">{`${weekDay}, ${date} ${timeString}`}</div>
{workspaceName ?? "Workspace Home"} <h2 className="mt-1 line-clamp-2 text-24 font-semibold leading-tight text-white">
</div> {`${t("good")} ${t(greeting)}, ${user?.first_name || "DC"}`}
<h2 className="mt-1 truncate text-24 font-semibold text-primary">
{`${t("good")} ${t(greeting)}, ${user?.first_name} ${user?.last_name}`}
</h2> </h2>
<div className="mt-1 text-13 text-secondary">{`${weekDay}, ${date} ${timeString}`}</div>
</div> </div>
</div> </div>
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3"> <div className="mt-8 space-y-1">
<div className="text-12 font-medium text-secondary">Текущий фокус</div> <div className="text-18 font-semibold text-white">{userName}</div>
<div className="mt-2 line-clamp-2 text-18 font-semibold text-primary"> <div className="truncate text-13 text-white/62">{userEmail}</div>
{selectedProject ? selectedProject.name : "Выберите проект слева"}
</div>
<div className="mt-1 text-12 text-secondary">
{selectedProject ? selectedProject.identifier : "Домашняя сводка перестроится под выбранную карточку."}
</div>
</div> </div>
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3"> <div className="mt-5 flex flex-wrap items-center gap-2 text-[11px] font-medium text-white/56">
<div className="text-12 font-medium text-secondary">Прогресс фокуса</div> <div className="rounded-full bg-black/24 px-3 py-1.5 backdrop-blur-md">home admin</div>
<div className="mt-2 text-18 font-semibold text-primary">{selectedProject ? `${completionRate}%` : "—"}</div> <div className="rounded-full bg-black/24 px-3 py-1.5 backdrop-blur-md">{workspaceName ?? "workspace"}</div>
<div className="mt-1 text-12 text-secondary">
{selectedProject ? "Закрытые задачи относительно общего объёма." : "Станет доступен после выбора проекта."}
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -11,9 +11,6 @@ import { useTranslation } from "@plane/i18n";
// plane types // plane types
import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons"; import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons";
import type { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types"; import type { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types";
// plane ui
// components
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
// plane web services // plane web services
import { WorkspaceService } from "@/services/workspace.service"; import { WorkspaceService } from "@/services/workspace.service";
import { getActivityProjectId } from "../../home.utils"; import { getActivityProjectId } from "../../home.utils";
@ -105,20 +102,15 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
); );
return ( return (
<ContentOverflowWrapper <div className="box-border min-h-[250px]">
maxHeight={415}
containerClassName="box-border min-h-[250px]"
fallback={<></>}
buttonClassName="nodedc-toolbar-pill justify-center"
>
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<div className="text-14 font-semibold text-tertiary">{t("home.recents.title")}</div> <div className="text-14 font-semibold text-tertiary">{t("home.recents.title")}</div>
{showFilterSelect && <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />} {showFilterSelect && <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />}
</div> </div>
<div className="flex min-h-[250px] flex-col"> <div className="flex max-h-[415px] min-h-[250px] flex-col overflow-y-auto">
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />} {isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
{!isLoading && recents.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)} {!isLoading && recents.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
</div> </div>
</ContentOverflowWrapper> </div>
); );
}); });

View File

@ -10,7 +10,7 @@ import { useTheme } from "next-themes";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { SearchIcon } from "@plane/propel/icons"; import { SearchIcon, getStateGroupColor } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse } from "@plane/types"; import type { ISearchIssueResponse } from "@plane/types";
import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
@ -90,7 +90,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
{query === "" && <h2 className="mt-4 mb-2 px-3 text-11 font-semibold text-primary">Select work item</h2>} {query === "" && <h2 className="mt-4 mb-2 px-3 text-11 font-semibold text-primary">Select work item</h2>}
<ul className="text-13 text-primary"> <ul className="text-13 text-primary">
{filteredIssues.map((issue) => { {filteredIssues.map((issue) => {
const stateColor = issue.state__color || ""; const stateColor = getStateGroupColor(issue.state__group, issue.state__color);
return ( return (
<Combobox.Option <Combobox.Option

View File

@ -9,6 +9,7 @@ import { useRouter } from "next/navigation";
import { MinusCircle } from "lucide-react"; import { MinusCircle } from "lucide-react";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { getIconButtonStyling } from "@plane/propel/icon-button"; import { getIconButtonStyling } from "@plane/propel/icon-button";
import { getStateGroupColor } from "@plane/propel/icons";
import type { TIssue } from "@plane/types"; import type { TIssue } from "@plane/types";
// component // component
// ui // ui
@ -55,7 +56,7 @@ export const IssueParentDetail = observer(function IssueParentDetail(props: TIss
const issueParentState = getProjectStates(parentIssue?.project_id)?.find( const issueParentState = getProjectStates(parentIssue?.project_id)?.find(
(state) => state?.id === parentIssue?.state_id (state) => state?.id === parentIssue?.state_id
); );
const stateColor = issueParentState?.color || undefined; const stateColor = getStateGroupColor(issueParentState?.group, issueParentState?.color);
if (!parentIssue) return <></>; if (!parentIssue) return <></>;

View File

@ -10,6 +10,7 @@ import { useParams } from "next/navigation";
import { MoreHorizontal } from "lucide-react"; import { MoreHorizontal } from "lucide-react";
// plane imports // plane imports
import { Popover } from "@plane/propel/popover"; import { Popover } from "@plane/propel/popover";
import { getStateGroupColor } from "@plane/propel/icons";
import type { TIssue } from "@plane/types"; import type { TIssue } from "@plane/types";
import { ControlLink } from "@plane/ui"; import { ControlLink } from "@plane/ui";
import { cn, generateWorkItemLink } from "@plane/utils"; import { cn, generateWorkItemLink } from "@plane/utils";
@ -50,16 +51,15 @@ export const CalendarIssueBlock = observer(
const { issuesFilter } = useIssues(storeType); const { issuesFilter } = useIssues(storeType);
const { getProjectIdentifierById } = useProject(); const { getProjectIdentifierById } = useProject();
const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; const stateDetails = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id);
const stateColor = getStateGroupColor(stateDetails?.group, stateDetails?.color);
const projectIdentifier = getProjectIdentifierById(issue?.project_id); const projectIdentifier = getProjectIdentifierById(issue?.project_id);
// handlers // handlers
const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug.toString(), issue, isMobile); const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug.toString(), issue, isMobile);
const customActionButton = ( const customActionButton = (
<div <div className="w-full cursor-pointer rounded-sm p-1 text-secondary hover:bg-layer-1 hover:text-primary">
className="w-full cursor-pointer rounded-sm p-1 text-secondary hover:bg-layer-1 hover:text-primary"
>
<MoreHorizontal className="h-3.5 w-3.5" /> <MoreHorizontal className="h-3.5 w-3.5" />
</div> </div>
); );

View File

@ -7,6 +7,7 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// plane imports // plane imports
import { getStateGroupColor } from "@plane/propel/icons";
import { Popover } from "@plane/propel/popover"; import { Popover } from "@plane/propel/popover";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import { ControlLink } from "@plane/ui"; import { ControlLink } from "@plane/ui";
@ -53,7 +54,15 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) {
const stateDetails = const stateDetails =
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id); issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
const { blockStyle } = getBlockViewDetails(issueDetails, stateDetails?.color ?? ""); const stateGroupColor = getStateGroupColor(stateDetails?.group, stateDetails?.color);
const { blockStyle } = getBlockViewDetails(issueDetails, stateGroupColor);
const ganttBlockStyle = {
...blockStyle,
backgroundColor: stateGroupColor,
maskImage: "none",
WebkitMaskImage: "none",
};
const ganttBlockTextColor = ["completed", "started"].includes(stateDetails?.group ?? "") ? "#0B1117" : "#FFFFFF";
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile); const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
@ -66,14 +75,14 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) {
render={ render={
<div <div
id={`issue-${issueId}`} id={`issue-${issueId}`}
className="space-between relative flex h-full w-full cursor-pointer items-center rounded-sm" className="nodedc-project-gantt-issue-bar space-between relative flex h-full w-full cursor-pointer items-center"
style={blockStyle} style={ganttBlockStyle}
onClick={handleIssuePeekOverview} onClick={handleIssuePeekOverview}
> >
<div className="absolute top-0 left-0 h-full w-full bg-surface-1/50" /> <div className="nodedc-project-gantt-issue-bar-shade absolute top-0 left-0 h-full w-full" />
<div <div
className="sticky w-auto flex-1 truncate overflow-hidden px-2.5 py-1 text-13 text-primary" className="nodedc-project-gantt-issue-bar-title sticky w-auto flex-1 truncate overflow-hidden px-3 py-1 text-12 font-semibold text-primary"
style={{ left: `${SIDEBAR_WIDTH}px` }} style={{ left: `${SIDEBAR_WIDTH}px`, color: ganttBlockTextColor }}
> >
{issueDetails?.name} {issueDetails?.name}
</div> </div>

View File

@ -73,11 +73,12 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : []; const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
const { pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive); const { pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color); const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
const controlStatusIconColor = selectedState?.group === "started" ? "#F5F7FB" : statusIconColor;
const creatorName = creatorDetails?.display_name ?? t("common.none"); const creatorName = creatorDetails?.display_name ?? t("common.none");
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none"); const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
const cornerControlClasses = const cornerControlClasses =
"flex h-12 w-12 items-center justify-center rounded-full border-0 bg-white text-[#0B1117] shadow-none outline-none ring-0 transition-transform hover:scale-[1.03]"; "flex h-12 w-12 items-center justify-center rounded-full border-0 bg-[#17181B] text-white shadow-none outline-none ring-0 transition-transform hover:scale-[1.03] hover:bg-[#0F1012]";
const dateButton = ( const dateButton = (
<DateDropdown <DateDropdown
className="h-auto self-start" className="h-auto self-start"
@ -112,7 +113,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
<div <div
data-control-link-ignore="true" data-control-link-ignore="true"
className={cn( className={cn(
"flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-black text-white shadow-none ring-0 transition-transform outline-none hover:scale-[1.03]", "flex h-12 w-12 -translate-x-0.5 -translate-y-0.5 cursor-pointer items-center justify-center rounded-full bg-black text-white shadow-none ring-0 transition-transform outline-none hover:scale-[1.03]",
isActive isActive
? "text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90" ? "text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90"
: "bg-[#111214] text-white hover:bg-[#0A0B0C]" : "bg-[#111214] text-white hover:bg-[#0A0B0C]"
@ -131,6 +132,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
name={creatorName} name={creatorName}
size="md" size="md"
showTooltip={!isMobile} showTooltip={!isMobile}
className="border-0 shadow-none ring-0 outline-none"
/> />
</div> </div>
<div className="flex min-w-0 flex-col gap-1"> <div className="flex min-w-0 flex-col gap-1">
@ -139,7 +141,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
</div> </div>
</div> </div>
<div className="absolute top-0 right-0 flex shrink-0 items-center gap-0"> <div className="absolute top-0.5 right-0.5 flex shrink-0 items-center gap-0">
<PriorityDropdown <PriorityDropdown
value={issue.priority} value={issue.priority}
onChange={(priority) => updateIssue?.(issue.project_id ?? null, issue.id, { priority })} onChange={(priority) => updateIssue?.(issue.project_id ?? null, issue.id, { priority })}
@ -160,7 +162,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
<div data-control-link-ignore="true" className={cornerControlClasses}> <div data-control-link-ignore="true" className={cornerControlClasses}>
<StateGroupIcon <StateGroupIcon
stateGroup={selectedState?.group ?? "backlog"} stateGroup={selectedState?.group ?? "backlog"}
color={statusIconColor} color={controlStatusIconColor}
className="h-4 w-4" className="h-4 w-4"
percentage={selectedState?.order} percentage={selectedState?.order}
/> />
@ -190,7 +192,10 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
button={ button={
<div <div
data-control-link-ignore="true" data-control-link-ignore="true"
className={cn(basePillClasses, "h-11 min-w-11 justify-center rounded-full bg-transparent px-0 py-0")} className={cn(
basePillClasses,
"h-11 min-w-11 justify-center rounded-full bg-transparent px-0 py-0 shadow-none ring-0 outline-none [&_.bg-accent-subtle]:!bg-transparent [&_.border-subtle-1]:!border-0"
)}
> >
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids} size="sm" /> <ButtonAvatars showTooltip={false} userIds={issue.assignee_ids} size="sm" />
</div> </div>

View File

@ -9,7 +9,7 @@ import { observer } from "mobx-react";
import type { Control } from "react-hook-form"; import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { ETabIndices } from "@plane/constants"; import { ETabIndices } from "@plane/constants";
import { CloseIcon } from "@plane/propel/icons"; import { CloseIcon, getStateGroupColor } from "@plane/propel/icons";
// plane imports // plane imports
// types // types
import type { ISearchIssueResponse, TIssue } from "@plane/types"; import type { ISearchIssueResponse, TIssue } from "@plane/types";
@ -44,7 +44,7 @@ export const IssueParentTag = observer(function IssueParentTag(props: TIssuePare
<span <span
className="block h-1.5 w-1.5 rounded-full" className="block h-1.5 w-1.5 rounded-full"
style={{ style={{
backgroundColor: selectedParentIssue.state__color, backgroundColor: getStateGroupColor(selectedParentIssue.state__group, selectedParentIssue.state__color),
}} }}
/> />
<span className="flex-shrink-0 text-secondary"> <span className="flex-shrink-0 text-secondary">

View File

@ -12,7 +12,7 @@ import { Rocket } from "lucide-react";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// i18n // i18n
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { SearchIcon } from "@plane/propel/icons"; import { SearchIcon, getStateGroupColor } from "@plane/propel/icons";
// types // types
import type { ISearchIssueResponse } from "@plane/types"; import type { ISearchIssueResponse } from "@plane/types";
// ui // ui
@ -159,7 +159,7 @@ export function ParentIssuesListModal({
<span <span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full" className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: issue.state__color, backgroundColor: getStateGroupColor(issue.state__group, issue.state__color),
}} }}
/> />
<span className="flex-shrink-0"> <span className="flex-shrink-0">

View File

@ -40,6 +40,11 @@ export const ModuleGanttBlock = observer(function ModuleGanttBlock(props: Props)
moduleDetails, moduleDetails,
MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? "" MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? ""
); );
const ganttBlockStyle = {
...blockStyle,
maskImage: "none",
WebkitMaskImage: "none",
};
return ( return (
<Tooltip <Tooltip
@ -53,15 +58,15 @@ export const ModuleGanttBlock = observer(function ModuleGanttBlock(props: Props)
position="top-start" position="top-start"
> >
<div <div
className="relative flex h-full w-full cursor-pointer items-center rounded-sm" className="nodedc-project-gantt-issue-bar relative flex h-full w-full cursor-pointer items-center"
style={blockStyle} style={ganttBlockStyle}
onClick={() => onClick={() =>
router.push( router.push(
`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}` `/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`
) )
} }
> >
<div className="absolute top-0 left-0 h-full w-full bg-surface-1/50" /> <div className="nodedc-project-gantt-issue-bar-shade absolute top-0 left-0 h-full w-full" />
<div <div
className="sticky w-auto truncate overflow-hidden px-2.5 py-1 text-13 text-primary" className="sticky w-auto truncate overflow-hidden px-2.5 py-1 text-13 text-primary"
style={{ left: `${SIDEBAR_WIDTH}px` }} style={{ left: `${SIDEBAR_WIDTH}px` }}

View File

@ -311,10 +311,10 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
}; };
const refreshVisibleIssueStores = useCallback( const refreshVisibleIssueStores = useCallback(
async (createdProjectId?: string) => { async () => {
const refreshes: Promise<unknown>[] = []; const refreshes: Promise<unknown>[] = [];
if (createdProjectId && activeProjectId === createdProjectId) { if (activeProjectId) {
refreshes.push(refreshProjectIssues(workspaceSlug, activeProjectId, "mutation")); refreshes.push(refreshProjectIssues(workspaceSlug, activeProjectId, "mutation"));
if (activeProjectViewId) { if (activeProjectViewId) {
refreshes.push(refreshProjectViewIssues(workspaceSlug, activeProjectId, activeProjectViewId, "mutation")); refreshes.push(refreshProjectViewIssues(workspaceSlug, activeProjectId, activeProjectViewId, "mutation"));
@ -361,7 +361,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
action, action,
draft: parseResult.draft, draft: parseResult.draft,
}); });
await refreshVisibleIssueStores(result.project_id); await refreshVisibleIssueStores();
setCommitResult(result); setCommitResult(result);
setStatus("committed"); setStatus("committed");
setToast({ setToast({
@ -456,7 +456,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
{parseResult.transcript && ( {parseResult.transcript && (
<div> <div>
<div className="text-11 font-medium text-tertiary uppercase">Транскрипт</div> <div className="text-11 font-medium text-tertiary uppercase">Транскрипт</div>
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary"> <p className="mt-1 whitespace-pre-wrap break-words text-12 leading-5 text-secondary">
{parseResult.transcript} {parseResult.transcript}
</p> </p>
</div> </div>
@ -524,7 +524,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
{parseResult.draft.description && ( {parseResult.draft.description && (
<div> <div>
<div className="text-11 font-medium text-tertiary uppercase">Описание</div> <div className="text-11 font-medium text-tertiary uppercase">Описание</div>
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary"> <p className="mt-1 whitespace-pre-wrap break-words text-12 leading-5 text-secondary">
{parseResult.draft.description} {parseResult.draft.description}
</p> </p>
</div> </div>

View File

@ -2108,6 +2108,286 @@
} }
} }
.nodedc-project-gantt-card {
overflow: hidden;
isolation: isolate;
border: 0 !important;
border-radius: 1.5rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.006) 100%), #08080a !important;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.035),
0 20px 60px rgba(0, 0, 0, 0.22) !important;
}
.nodedc-project-gantt-toolbar {
padding: 0.7rem 0.95rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.045);
background:
radial-gradient(circle at 18% 0%, rgba(var(--nodedc-card-active-rgb), 0.08), transparent 24rem),
rgba(8, 8, 10, 0.96) !important;
}
.nodedc-project-gantt-toolbar-icon {
display: grid;
width: 2.45rem;
min-width: 2.45rem;
height: 2.45rem;
place-items: center;
border-radius: 999px;
background: rgba(0, 0, 0, 0.58);
color: rgb(var(--nodedc-card-active-rgb));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.nodedc-project-gantt-chip {
display: inline-flex;
height: 2.15rem;
min-width: 2.55rem;
align-items: center;
justify-content: center;
border: 0 !important;
outline: 0 !important;
border-radius: 999px !important;
background: rgba(255, 255, 255, 0.07) !important;
padding-inline: 0.82rem;
color: var(--text-color-secondary);
font-size: 0.72rem;
font-weight: 750;
transition:
background 160ms ease,
color 160ms ease,
transform 160ms ease;
}
.nodedc-project-gantt-chip:hover {
background: rgba(255, 255, 255, 0.1) !important;
color: var(--text-color-primary);
}
.nodedc-project-gantt-chip-active,
.nodedc-project-gantt-chip-live {
background: rgb(var(--nodedc-card-active-rgb)) !important;
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
}
.nodedc-project-gantt-round-button {
display: inline-grid !important;
width: 2.25rem;
min-width: 2.25rem;
height: 2.25rem;
place-items: center;
border: 0 !important;
outline: 0 !important;
border-radius: 999px !important;
background: rgba(0, 0, 0, 0.56) !important;
color: var(--text-color-primary);
}
.nodedc-project-gantt-round-button:hover {
background: rgba(0, 0, 0, 0.76) !important;
}
.nodedc-project-gantt-scroll {
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px) 0 0 / 5.5rem 100%,
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px) 0 0 / 100% 3.85rem,
rgba(3, 3, 5, 0.96) !important;
scrollbar-color: rgba(var(--nodedc-card-active-rgb), 0.66) rgba(255, 255, 255, 0.04);
scrollbar-width: thin;
}
.nodedc-project-gantt-scroll::-webkit-scrollbar {
width: 0.65rem;
height: 0.65rem;
}
.nodedc-project-gantt-scroll::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.04);
border-radius: 999px;
}
.nodedc-project-gantt-scroll::-webkit-scrollbar-thumb {
background: rgba(var(--nodedc-card-active-rgb), 0.62);
border-radius: 999px;
}
.nodedc-project-gantt-stage {
background: rgba(4, 4, 6, 0.72);
}
.nodedc-project-gantt-layer {
background: transparent;
}
.nodedc-project-gantt-sidebar {
border-right: 1px solid rgba(255, 255, 255, 0.055);
background: linear-gradient(
90deg,
rgba(11, 11, 13, 0.98) 0%,
rgba(11, 11, 13, 0.9) 78%,
rgba(11, 11, 13, 0.7) 100%
) !important;
box-shadow: 14px 0 32px rgba(0, 0, 0, 0.16);
}
.nodedc-project-gantt-sidebar-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.055);
background: rgba(10, 10, 12, 0.96) !important;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.nodedc-project-gantt-sidebar-body,
.nodedc-project-gantt-sidebar-list {
background: transparent !important;
}
.nodedc-project-gantt-sidebar-loader {
border-radius: 1rem;
background: rgba(255, 255, 255, 0.055) !important;
}
.nodedc-project-gantt-calendar-group {
border-left: 1px solid rgba(255, 255, 255, 0.045);
}
.nodedc-project-gantt-calendar-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.055);
background: rgba(8, 8, 10, 0.95) !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
}
.nodedc-project-gantt-period-label {
border-radius: 999px;
background: rgba(255, 255, 255, 0.055) !important;
color: var(--text-color-primary) !important;
letter-spacing: 0;
}
.nodedc-project-gantt-period-meta {
color: var(--text-color-placeholder);
}
.nodedc-project-gantt-subcell {
border-left: 1px solid rgba(255, 255, 255, 0.045);
color: var(--text-color-placeholder);
}
.nodedc-project-gantt-subcell-today {
background: rgba(var(--nodedc-card-active-rgb), 0.13) !important;
}
.nodedc-project-gantt-column {
border-left: 1px solid rgba(255, 255, 255, 0.035);
background: rgba(255, 255, 255, 0.004);
}
.nodedc-project-gantt-column-today {
background: linear-gradient(
180deg,
rgba(var(--nodedc-card-active-rgb), 0.13),
rgba(var(--nodedc-card-active-rgb), 0.035)
) !important;
box-shadow: inset 1px 0 0 rgba(var(--nodedc-card-active-rgb), 0.18);
}
.nodedc-project-gantt-column-weekend {
background: rgba(255, 255, 255, 0.03);
}
.nodedc-project-gantt-row-bg {
border-bottom: 1px solid rgba(255, 255, 255, 0.026);
transition:
background 140ms ease,
box-shadow 140ms ease;
}
.nodedc-project-gantt-row-bg:hover,
.nodedc-project-gantt-row-hovered {
background: rgba(255, 255, 255, 0.035) !important;
}
.nodedc-project-gantt-row-selected {
background: rgba(var(--nodedc-card-active-rgb), 0.08) !important;
}
.nodedc-project-gantt-row-focused,
.nodedc-project-gantt-row-peeked {
box-shadow: inset 3px 0 0 rgba(var(--nodedc-card-active-rgb), 0.82);
}
.nodedc-project-gantt-jump-button,
.nodedc-project-gantt-block-placeholder {
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 999px !important;
background: rgba(15, 15, 18, 0.92) !important;
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.2);
}
.nodedc-project-gantt-draggable-shell {
border-radius: 999px !important;
}
.nodedc-project-gantt-issue-bar {
overflow: hidden;
border: 0 !important;
border-radius: 999px !important;
box-shadow: none !important;
}
.nodedc-project-gantt-issue-bar-shade {
border-radius: inherit;
background: transparent;
}
.nodedc-project-gantt-issue-bar-title {
text-shadow: none;
}
.nodedc-project-gantt-resize-tooltip {
border-radius: 999px;
background: rgba(18, 18, 22, 0.96);
color: var(--text-color-primary);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.24);
}
.nodedc-project-gantt-resize-handle {
border-radius: 999px;
background: rgba(255, 255, 255, 0.8) !important;
}
.nodedc-project-gantt-sidebar-block {
transition:
background 140ms ease,
box-shadow 140ms ease;
}
.nodedc-project-gantt-sidebar-block-dragging {
border-radius: 1rem;
background: rgba(255, 255, 255, 0.06) !important;
}
.nodedc-project-gantt-sidebar-block-peeked,
.nodedc-project-gantt-sidebar-block-focused {
box-shadow: inset 3px 0 0 rgba(var(--nodedc-card-active-rgb), 0.82);
}
.nodedc-project-gantt-sidebar-row {
background: transparent !important;
transition: background 140ms ease;
}
.nodedc-project-gantt-sidebar-row:hover,
.nodedc-project-gantt-sidebar-row-hovered {
background: rgba(255, 255, 255, 0.035) !important;
}
.nodedc-project-gantt-sidebar-row-selected {
background: rgba(var(--nodedc-card-active-rgb), 0.08) !important;
}
.nodedc-home-card { .nodedc-home-card {
position: relative; position: relative;
overflow: hidden; overflow: hidden;

View File

@ -23,31 +23,31 @@ export const STATE_GROUPS: {
key: "backlog", key: "backlog",
label: "Backlog", label: "Backlog",
defaultStateName: "Backlog", defaultStateName: "Backlog",
color: "#d9d9d9", color: "#050505",
}, },
unstarted: { unstarted: {
key: "unstarted", key: "unstarted",
label: "Unstarted", label: "Unstarted",
defaultStateName: "Todo", defaultStateName: "Todo",
color: "#3f76ff", color: "#7C7F85",
}, },
started: { started: {
key: "started", key: "started",
label: "Started", label: "Started",
defaultStateName: "In Progress", defaultStateName: "In Progress",
color: "#f59e0b", color: "#FFFFFF",
}, },
completed: { completed: {
key: "completed", key: "completed",
label: "Completed", label: "Completed",
defaultStateName: "Done", defaultStateName: "Done",
color: "#16a34a", color: "#C3FF66",
}, },
cancelled: { cancelled: {
key: "cancelled", key: "cancelled",
label: "Canceled", label: "Canceled",
defaultStateName: "Cancelled", defaultStateName: "Cancelled",
color: "#dc2626", color: "#050505",
}, },
}; };
@ -92,22 +92,22 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [
{ {
key: "completed_issues", key: "completed_issues",
title: "Completed", title: "Completed",
color: "#16A34A", color: "#C3FF66",
}, },
{ {
key: "started_issues", key: "started_issues",
title: "Started", title: "Started",
color: "#F59E0B", color: "#FFFFFF",
}, },
{ {
key: "unstarted_issues", key: "unstarted_issues",
title: "Unstarted", title: "Unstarted",
color: "#3A3A3A", color: "#7C7F85",
}, },
{ {
key: "backlog_issues", key: "backlog_issues",
title: "Backlog", title: "Backlog",
color: "#A3A3A3", color: "#050505",
}, },
]; ];

View File

@ -7,6 +7,7 @@
export * from "./backlog-group-icon"; export * from "./backlog-group-icon";
export * from "./cancelled-group-icon"; export * from "./cancelled-group-icon";
export * from "./completed-group-icon"; export * from "./completed-group-icon";
export * from "./helper";
export * from "./started-group-icon"; export * from "./started-group-icon";
export * from "./state-group-icon"; export * from "./state-group-icon";
export * from "./unstarted-group-icon"; export * from "./unstarted-group-icon";

View File

@ -11,7 +11,7 @@ import { BacklogGroupIcon } from "./backlog-group-icon";
import { CancelledGroupIcon } from "./cancelled-group-icon"; import { CancelledGroupIcon } from "./cancelled-group-icon";
import { CompletedGroupIcon } from "./completed-group-icon"; import { CompletedGroupIcon } from "./completed-group-icon";
import type { IStateGroupIcon } from "./helper"; import type { IStateGroupIcon } from "./helper";
import { STATE_GROUP_COLORS, STATE_GROUP_SIZES } from "./helper"; import { getStateGroupColor, STATE_GROUP_SIZES } from "./helper";
import { StartedGroupIcon } from "./started-group-icon"; import { StartedGroupIcon } from "./started-group-icon";
import { UnstartedGroupIcon } from "./unstarted-group-icon"; import { UnstartedGroupIcon } from "./unstarted-group-icon";
@ -36,7 +36,7 @@ export function StateGroupIcon({
<StateIconComponent <StateIconComponent
height={STATE_GROUP_SIZES[size]} height={STATE_GROUP_SIZES[size]}
width={STATE_GROUP_SIZES[size]} width={STATE_GROUP_SIZES[size]}
color={color ?? STATE_GROUP_COLORS[stateGroup]} color={getStateGroupColor(stateGroup, color)}
className={`flex-shrink-0 ${className}`} className={`flex-shrink-0 ${className}`}
percentage={percentage} percentage={percentage}
/> />