ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: стабилизация контуров, досок и Voice Tasker
This commit is contained in:
parent
597480adb9
commit
7209d2caab
|
|
@ -252,12 +252,15 @@ MVP-правило:
|
|||
|
||||
Система:
|
||||
|
||||
1. берет последние voice-действия пользователя в текущем workspace;
|
||||
2. находит последную созданную/обновленную voice-задачу;
|
||||
3. показывает preview изменения, если confidence низкий;
|
||||
4. меняет `Issue.target_date`;
|
||||
5. сохраняет `due_time` в description note / parsed JSON;
|
||||
6. пишет новое действие в session-backed memory.
|
||||
1. берет последние только реально примененные voice-действия пользователя в текущем workspace;
|
||||
2. игнорирует parsed-сессии без `created_task`/`updated_task`, чтобы модель не цеплялась за старые неудачные черновики;
|
||||
3. если transcript явно задает исходный проект ("из Бухгалтерии", "последнюю добавленную в Бухгалтерию"), сначала ищет последнюю voice-задачу в этом проекте;
|
||||
4. если исходный проект не назван, сначала ищет последнюю voice-задачу в текущем открытом проекте;
|
||||
5. затем использует последнюю примененную voice-задачу workspace как общий fallback;
|
||||
6. показывает preview изменения, если confidence низкий;
|
||||
7. меняет `Issue.target_date`;
|
||||
8. сохраняет `due_time` в description note / parsed JSON;
|
||||
9. пишет новое действие в session-backed memory.
|
||||
|
||||
Если пользователь говорит "переложи последнюю задачу в проект X", это остается `update_task`, но backend должен:
|
||||
|
||||
|
|
@ -882,6 +885,7 @@ Return JSON only.
|
|||
6. если confidence низкий - preview с ручным выбором;
|
||||
7. если transcript содержит явную маршрутизацию проекта, но model `project_hint` указывает на проект, которого нет в transcript, auto-commit блокируется;
|
||||
8. при наличии source/destination фразы выбирать destination project: "из Бухгалтерии в Менеджмент" -> target `Менеджмент`, "из Менеджмента в Бухгалтерию" -> target `Бухгалтерия`.
|
||||
9. глагол "перенеси/перенести" рядом со сроком/датой считается date update, а не project routing, если нет project/контур-маркера или явного project destination.
|
||||
|
||||
Не зашивать термин "контур" как обязательный. Для NODE DC это важный UX-термин, но технически это обычный `Project`.
|
||||
|
||||
|
|
@ -947,11 +951,28 @@ MVP:
|
|||
- `N` дней/недель/месяцев/лет назад;
|
||||
- сложные интервалы: "два месяца и две недели";
|
||||
- числительные цифрами и частые русские словоформы: "2 недели", "две недели", "пару дней";
|
||||
- абсолютные русские даты: "1 мая 2026 года", "30 апреля";
|
||||
- числовые даты: "01.05.2026", "1/05/26";
|
||||
- защита от ложных матчей внутри слов: "последней" не считается как "дней";
|
||||
- защита от трактовки года абсолютной даты как относительного интервала: "2026 года" не считается как "+2026 лет";
|
||||
- конкретная дата;
|
||||
- конкретное время как `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
|
||||
|
||||
|
|
@ -1201,6 +1222,7 @@ voice_task.error
|
|||
23. Относительная дата "на две недели вперед" меняет `Issue.target_date` без ручного ввода ISO-даты.
|
||||
24. Перенос "из Бухгалтерии в Менеджмент" и "из Менеджмента в Бухгалтерию" реально меняет `Issue.project_id` и `task_key`.
|
||||
25. Voice-created task имеет `external_source=voice_tasker` и хранит исходный transcript в description.
|
||||
26. Preview modal показывает transcript/description полностью без внутреннего scroll внутри текстовых блоков.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,32 @@ VOICE_TASK_STATE_GROUP_HINTS = {
|
|||
}
|
||||
DATE_PATTERN = re.compile(r"^\d{4}-\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 = {
|
||||
"один": 1,
|
||||
"одна": 1,
|
||||
|
|
@ -152,6 +178,14 @@ VOICE_TASK_RELATIVE_DATE_PATTERN = re.compile(
|
|||
r"(?P<unit>день|дня|дней|сутки|суток|неделю|неделя|недели|недель|"
|
||||
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):
|
||||
|
|
@ -443,13 +477,15 @@ def transcript_has_project_routing_request(transcript):
|
|||
if not normalized:
|
||||
return False
|
||||
|
||||
normalized = normalized.lower()
|
||||
return bool(
|
||||
re.search(
|
||||
r"(проект|контур|перелож|перенес|перемест|перекин|route|move\s+to\s+project|project)",
|
||||
normalized,
|
||||
)
|
||||
normalized = normalize_match_value(normalized)
|
||||
if re.search(r"(проект|контур|route|move\s+to\s+project|project)", normalized):
|
||||
return True
|
||||
|
||||
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):
|
||||
|
|
@ -461,6 +497,19 @@ def transcript_contains_project_hint(project_hint, 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):
|
||||
normalized_transcript = normalize_match_value(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):
|
||||
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_score = score
|
||||
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")
|
||||
|
||||
|
||||
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):
|
||||
normalized_query = normalize_match_value(query)
|
||||
if not normalized_query:
|
||||
|
|
@ -811,6 +908,61 @@ def add_months_to_date(value, months):
|
|||
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):
|
||||
normalized = normalize_match_value(transcript)
|
||||
if not normalized:
|
||||
|
|
@ -868,6 +1020,8 @@ def infer_voice_task_relative_due_date(transcript, current_date, target_issue=No
|
|||
elif unit.startswith("месяц"):
|
||||
shift_months += quantity
|
||||
elif unit.startswith("год") or unit == "лет":
|
||||
if quantity >= 100:
|
||||
continue
|
||||
shift_months += quantity * 12
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
inferred_due_date = infer_voice_task_relative_due_date(
|
||||
transcript=transcript,
|
||||
current_date=get_voice_task_current_date(client_context, user, workspace),
|
||||
current_date=current_date,
|
||||
target_issue=target_issue,
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
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:
|
||||
target_uuid = None
|
||||
|
|
@ -989,7 +1192,7 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
|
|||
.first()
|
||||
)
|
||||
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
|
||||
|
||||
target_issue = (
|
||||
|
|
@ -997,7 +1200,7 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
|
|||
.select_related("project")
|
||||
.first()
|
||||
)
|
||||
if target_issue:
|
||||
if is_voice_task_issue_available(target_issue):
|
||||
return target_issue, "target_issue_id", None
|
||||
|
||||
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")
|
||||
.first()
|
||||
)
|
||||
if target_issue:
|
||||
if is_voice_task_issue_available(target_issue):
|
||||
return target_issue, "target_issue_key", 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)
|
||||
projects = list(get_accessible_projects(workspace, user).order_by("name"))
|
||||
source_project = infer_voice_task_source_project_from_transcript(projects, transcript)
|
||||
if source_project:
|
||||
target_issue, memory_session = find_latest_voice_task_issue(memory_sessions, source_project.id)
|
||||
if target_issue:
|
||||
return target_issue, "latest_voice_task_source_project", memory_session
|
||||
|
||||
for memory_session in memory_sessions[:VOICE_TASK_MEMORY_LIMIT * 3]:
|
||||
target_issue = get_voice_session_target_issue(memory_session)
|
||||
if is_voice_task_issue_available(target_issue):
|
||||
return target_issue, "latest_voice_task", memory_session
|
||||
current_project_id = normalize_string((client_context or {}).get("current_project_id"))
|
||||
if current_project_id:
|
||||
target_issue, memory_session = find_latest_voice_task_issue(memory_sessions, current_project_id)
|
||||
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
|
||||
|
||||
|
|
@ -1066,6 +1268,8 @@ def build_voice_task_resolution(workspace, user, ai_settings, draft, client_cont
|
|||
user=user,
|
||||
draft=draft,
|
||||
current_session=voice_session,
|
||||
client_context=client_context,
|
||||
transcript=transcript,
|
||||
)
|
||||
|
||||
hydrate_voice_task_due_date(
|
||||
|
|
@ -1493,32 +1697,20 @@ def serialize_workspace_members(workspace):
|
|||
|
||||
|
||||
def serialize_recent_voice_memory(workspace, user):
|
||||
sessions = (
|
||||
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]
|
||||
)
|
||||
sessions = get_committed_voice_task_memory_sessions(workspace, user)[:VOICE_TASK_MEMORY_LIMIT]
|
||||
|
||||
memory = []
|
||||
for session in sessions:
|
||||
target_issue = get_voice_session_target_issue(session)
|
||||
target_task = (
|
||||
serialize_voice_task_target(target_issue, "recent_voice_memory", session)
|
||||
if is_voice_task_issue_available(target_issue)
|
||||
else None
|
||||
)
|
||||
if not is_voice_task_issue_available(target_issue):
|
||||
continue
|
||||
memory.append(
|
||||
{
|
||||
"voice_session_id": str(session.id),
|
||||
"intent": session.intent,
|
||||
"title": session.parsed_json.get("title"),
|
||||
"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(),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,14 +10,9 @@ import { CalendarDays } from "lucide-react";
|
|||
import { observer } from "mobx-react";
|
||||
import { EUserPermissions } from "@plane/constants";
|
||||
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 type {
|
||||
IState,
|
||||
TExternalContourBoardDirection,
|
||||
TExternalContourRequest,
|
||||
TIssue,
|
||||
} from "@plane/types";
|
||||
import type { IState, TExternalContourBoardDirection, TExternalContourRequest, TIssue } from "@plane/types";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
||||
import { DateDropdown } from "@/components/dropdowns/date";
|
||||
|
|
@ -56,7 +51,7 @@ const buildSourceStateMap = (
|
|||
state.id,
|
||||
{
|
||||
id: state.id,
|
||||
color: state.color,
|
||||
color: getStateGroupColor(state.group, state.color),
|
||||
default: false,
|
||||
description: "",
|
||||
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;
|
||||
if (!stateGroup) return fallbackStatus;
|
||||
return stateGroup === "completed" || stateGroup === "cancelled" ? "closed" : "open";
|
||||
|
|
@ -83,23 +81,20 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
|||
const { getUserDetails, workspace } = useMember();
|
||||
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||
const { getStateById, getProjectStateIds } = useProjectState();
|
||||
const {
|
||||
fetchBoard,
|
||||
upsertBoardItems,
|
||||
} = useProjectExternalContoursBoard();
|
||||
const {
|
||||
fetchTargetOptions,
|
||||
getTargetOptionsByProjectId,
|
||||
updateRequest,
|
||||
updateRequestIssue,
|
||||
} = useProjectExternalContours();
|
||||
const { fetchBoard, upsertBoardItems } = useProjectExternalContoursBoard();
|
||||
const { fetchTargetOptions, getTargetOptionsByProjectId, updateRequest, updateRequestIssue } =
|
||||
useProjectExternalContours();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isSourceOptionsLoading, setIsSourceOptionsLoading] = useState(false);
|
||||
|
||||
const issue = request.issue;
|
||||
const selectedInboxIssueId = searchParams.get("inboxIssueId");
|
||||
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 counterpartContourName =
|
||||
direction === "outgoing"
|
||||
|
|
@ -110,8 +105,12 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
|||
? getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId)
|
||||
: undefined;
|
||||
const canEditTargetIssue =
|
||||
direction === "incoming" && !!targetProjectId && projectRole !== undefined && projectRole !== EUserPermissions.GUEST;
|
||||
const canEditSourceRequest = direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId;
|
||||
direction === "incoming" &&
|
||||
!!targetProjectId &&
|
||||
projectRole !== undefined &&
|
||||
projectRole !== EUserPermissions.GUEST;
|
||||
const canEditSourceRequest =
|
||||
direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId;
|
||||
const canEditCard = canEditTargetIssue || canEditSourceRequest;
|
||||
const requestLink = `/${workspaceSlug}/projects/${projectId}/external-contours?inboxIssueId=${request.id}`;
|
||||
const targetOptions = getTargetOptionsByProjectId(targetProjectId);
|
||||
|
|
@ -124,12 +123,11 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
|||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||
const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white";
|
||||
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
|
||||
const pillBackgroundClasses =
|
||||
isActive
|
||||
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
||||
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
|
||||
const pillBackgroundClasses = isActive
|
||||
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
||||
: "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 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");
|
||||
|
||||
if (!issue) return null;
|
||||
|
|
@ -314,13 +312,13 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
|||
</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")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
||||
|
|
@ -333,7 +331,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
|||
disabled={!canEditCard || isUpdating}
|
||||
buttonVariant="transparent-without-text"
|
||||
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" />
|
||||
</div>
|
||||
}
|
||||
|
|
@ -351,7 +349,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
|||
}}
|
||||
buttonVariant="transparent-without-text"
|
||||
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" />
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
StateGroupIcon,
|
||||
StatePropertyIcon,
|
||||
UserCirclePropertyIcon,
|
||||
getStateGroupColor,
|
||||
} from "@plane/propel/icons";
|
||||
import type {
|
||||
IProject,
|
||||
|
|
@ -57,14 +58,11 @@ const buildCounterpartyProjects = (requests: TExternalContourRequest[], projectI
|
|||
|
||||
if (!project?.id || project.id === projectId || projectMap.has(project.id)) return;
|
||||
|
||||
projectMap.set(
|
||||
project.id,
|
||||
{
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
logo_props: project.logo_props,
|
||||
} as IProject
|
||||
);
|
||||
projectMap.set(project.id, {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
logo_props: project.logo_props,
|
||||
} as IProject);
|
||||
});
|
||||
|
||||
return sortByName(Array.from(projectMap.values()));
|
||||
|
|
@ -77,21 +75,18 @@ const buildStates = (requests: TExternalContourRequest[]): IState[] => {
|
|||
const state = request.issue.state_detail;
|
||||
if (!state?.id || stateMap.has(state.id)) return;
|
||||
|
||||
stateMap.set(
|
||||
state.id,
|
||||
{
|
||||
id: state.id,
|
||||
color: state.color,
|
||||
default: false,
|
||||
description: "",
|
||||
group: state.group,
|
||||
name: state.name,
|
||||
order: index + 1,
|
||||
project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
|
||||
sequence: index + 1,
|
||||
workspace_id: "",
|
||||
} as IState
|
||||
);
|
||||
stateMap.set(state.id, {
|
||||
id: state.id,
|
||||
color: getStateGroupColor(state.group, state.color),
|
||||
default: false,
|
||||
description: "",
|
||||
group: state.group,
|
||||
name: state.name,
|
||||
order: index + 1,
|
||||
project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
|
||||
sequence: index + 1,
|
||||
workspace_id: "",
|
||||
} as IState);
|
||||
});
|
||||
|
||||
return sortByName(Array.from(stateMap.values()));
|
||||
|
|
@ -103,18 +98,15 @@ const buildLabels = (requests: TExternalContourRequest[]): IIssueLabel[] => {
|
|||
requests.forEach((request) => {
|
||||
request.issue.label_details?.forEach((label) => {
|
||||
if (!label.id || labelMap.has(label.id)) return;
|
||||
labelMap.set(
|
||||
label.id,
|
||||
{
|
||||
id: label.id,
|
||||
color: label.color,
|
||||
name: label.name,
|
||||
parent: null,
|
||||
project_id: request.issue.project_id || "",
|
||||
sort_order: 0,
|
||||
workspace_id: "",
|
||||
} as IIssueLabel
|
||||
);
|
||||
labelMap.set(label.id, {
|
||||
id: label.id,
|
||||
color: label.color,
|
||||
name: label.name,
|
||||
parent: null,
|
||||
project_id: request.issue.project_id || "",
|
||||
sort_order: 0,
|
||||
workspace_id: "",
|
||||
} as IIssueLabel);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -127,14 +119,11 @@ const buildAssignees = (requests: TExternalContourRequest[]): IUserLite[] => {
|
|||
requests.forEach((request) => {
|
||||
request.issue.assignee_details?.forEach((assignee) => {
|
||||
if (!assignee.id || memberMap.has(assignee.id)) return;
|
||||
memberMap.set(
|
||||
assignee.id,
|
||||
{
|
||||
id: assignee.id,
|
||||
avatar_url: assignee.avatar_url,
|
||||
display_name: assignee.display_name,
|
||||
} as IUserLite
|
||||
);
|
||||
memberMap.set(assignee.id, {
|
||||
id: assignee.id,
|
||||
avatar_url: assignee.avatar_url,
|
||||
display_name: assignee.display_name,
|
||||
} as IUserLite);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -151,14 +140,11 @@ const buildRequesters = (requests: TExternalContourRequest[]): IUserLite[] => {
|
|||
|
||||
if (!requesterId || !requesterName || memberMap.has(requesterId)) return;
|
||||
|
||||
memberMap.set(
|
||||
requesterId,
|
||||
{
|
||||
id: requesterId,
|
||||
avatar_url: request.issue.created_by_detail?.avatar_url,
|
||||
display_name: requesterName,
|
||||
} as IUserLite
|
||||
);
|
||||
memberMap.set(requesterId, {
|
||||
id: requesterId,
|
||||
avatar_url: request.issue.created_by_detail?.avatar_url,
|
||||
display_name: requesterName,
|
||||
} as IUserLite);
|
||||
});
|
||||
|
||||
return sortByName(Array.from(memberMap.values()));
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// hooks
|
||||
import { getStateGroupColor } from "@plane/propel/icons";
|
||||
import type { ISearchIssueResponse } from "@plane/types";
|
||||
// plane web hooks
|
||||
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) {
|
||||
const { issue, canDeleteIssueIds } = props;
|
||||
|
||||
const color = issue.state__color;
|
||||
const color = getStateGroupColor(issue.state__group, issue.state__color);
|
||||
|
||||
return (
|
||||
<Combobox.Option
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Combobox } from "@headlessui/react";
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
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 { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
||||
|
|
@ -263,7 +263,7 @@ export function ExistingIssuesListModal(props: Props) {
|
|||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state__color,
|
||||
backgroundColor: getStateGroupColor(issue.state__group, issue.state__color),
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0">
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const BlockRow = observer(function BlockRow(props: Props) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="relative w-max min-w-full"
|
||||
className="nodedc-project-gantt-row relative w-max min-w-full"
|
||||
onMouseEnter={() => updateActiveBlockId(blockId)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
style={{
|
||||
|
|
@ -89,19 +89,18 @@ export const BlockRow = observer(function BlockRow(props: Props) {
|
|||
}}
|
||||
>
|
||||
<div
|
||||
className={cn("relative h-full bg-layer-transparent hover:bg-layer-transparent-hover", {
|
||||
"rounded-l-sm border border-r-0 border-accent-strong": getIsIssuePeeked(block.data.id),
|
||||
"bg-layer-transparent-hover": isBlockHoveredOn,
|
||||
"bg-accent-primary/5 hover:bg-accent-primary/10": isBlockSelected,
|
||||
"bg-accent-primary/10": isBlockSelected && isBlockHoveredOn,
|
||||
"border border-r-0 border-strong-1": isBlockFocused,
|
||||
className={cn("nodedc-project-gantt-row-bg relative h-full", {
|
||||
"nodedc-project-gantt-row-peeked": getIsIssuePeeked(block.data.id),
|
||||
"nodedc-project-gantt-row-hovered": isBlockHoveredOn,
|
||||
"nodedc-project-gantt-row-selected": isBlockSelected,
|
||||
"nodedc-project-gantt-row-focused": isBlockFocused,
|
||||
})}
|
||||
>
|
||||
{isBlockVisibleOnChart
|
||||
? isHidden && (
|
||||
<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={{
|
||||
left: `${SIDEBAR_WIDTH + 4}px`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export const GanttChartBlock = observer(function GanttChartBlock(props: Props) {
|
|||
horizontalOffset={100}
|
||||
verticalOffset={200}
|
||||
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}
|
||||
forceRender={isCurrentDependencyDragging}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
import { CalendarDays, Expand, Shrink } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane
|
||||
import type { TGanttViews } from "@plane/types";
|
||||
|
|
@ -25,62 +25,81 @@ type Props = {
|
|||
handleChartView: (view: TGanttViews) => void;
|
||||
handleToday: () => void;
|
||||
loaderTitle: string;
|
||||
title: string;
|
||||
toggleFullScreenMode: () => void;
|
||||
showToday: boolean;
|
||||
};
|
||||
|
||||
const GANTT_VIEW_SHORT_LABELS: Record<TGanttViews, string> = {
|
||||
week: "1W",
|
||||
month: "1M",
|
||||
quarter: "3M",
|
||||
};
|
||||
|
||||
export const GanttChartHeader = observer(function GanttChartHeader(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } =
|
||||
props;
|
||||
const {
|
||||
blockIds,
|
||||
fullScreenMode,
|
||||
handleChartView,
|
||||
handleToday,
|
||||
loaderTitle,
|
||||
title,
|
||||
toggleFullScreenMode,
|
||||
showToday,
|
||||
} = props;
|
||||
// chart hook
|
||||
const { currentView } = useTimeLineChartStore();
|
||||
|
||||
return (
|
||||
<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` }}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto text-11 font-medium text-tertiary">
|
||||
{blockIds ? `${blockIds.length} ${loaderTitle}` : t("common.loading")}
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="nodedc-project-gantt-toolbar-icon">
|
||||
<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 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) => (
|
||||
<div
|
||||
<button
|
||||
key={chartView?.key}
|
||||
className={cn(
|
||||
"cursor-pointer rounded-md bg-layer-transparent p-1 px-2 text-11 hover:bg-layer-transparent-hover",
|
||||
{
|
||||
"bg-layer-transparent-selected": currentView === chartView?.key,
|
||||
}
|
||||
)}
|
||||
type="button"
|
||||
aria-label={t(chartView?.i18n_title)}
|
||||
aria-pressed={currentView === chartView?.key}
|
||||
className={cn("nodedc-project-gantt-chip", {
|
||||
"nodedc-project-gantt-chip-active": currentView === chartView?.key,
|
||||
})}
|
||||
onClick={() => handleChartView(chartView?.key)}
|
||||
>
|
||||
{t(chartView?.i18n_title)}
|
||||
</div>
|
||||
{GANTT_VIEW_SHORT_LABELS[chartView?.key as TGanttViews]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showToday && (
|
||||
<button
|
||||
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 type="button" className="nodedc-project-gantt-round-button" onClick={toggleFullScreenMode}>
|
||||
{fullScreenMode ? <Shrink className="size-4" /> : <Expand className="size-4" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export const GanttChartMainContent = observer(function GanttChartMainContent(pro
|
|||
// DO NOT REMOVE THE ID
|
||||
id="gantt-container"
|
||||
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,
|
||||
}
|
||||
|
|
@ -199,11 +199,11 @@ export const GanttChartMainContent = observer(function GanttChartMainContent(pro
|
|||
showAllBlocks={showAllBlocks}
|
||||
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 />
|
||||
{currentViewData && (
|
||||
<div
|
||||
className="relative h-full"
|
||||
className="nodedc-project-gantt-layer relative h-full"
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
|
|
|
|||
|
|
@ -180,8 +180,8 @@ export const ChartViewRoot = observer(function ChartViewRoot(props: ChartViewRoo
|
|||
|
||||
const content = (
|
||||
<div
|
||||
className={cn("shadow relative flex h-full flex-col rounded-xs bg-surface-1 select-none", {
|
||||
"inset-0 z-[25] bg-surface-1": fullScreenMode,
|
||||
className={cn("nodedc-project-gantt-card shadow relative flex h-full flex-col select-none", {
|
||||
"fixed inset-0 z-[25] rounded-none": fullScreenMode,
|
||||
"border-[0.5px] border-subtle": border,
|
||||
})}
|
||||
>
|
||||
|
|
@ -192,6 +192,7 @@ export const ChartViewRoot = observer(function ChartViewRoot(props: ChartViewRoo
|
|||
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||
handleToday={handleToday}
|
||||
loaderTitle={loaderTitle}
|
||||
title={title}
|
||||
showToday={showToday}
|
||||
/>
|
||||
<GanttChartMainContent
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
|||
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
|
||||
|
||||
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 && (
|
||||
<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 */}
|
||||
<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={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
|
|
@ -45,18 +45,22 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
|||
{months?.map((monthBlock) => (
|
||||
<div
|
||||
key={`month-${monthBlock?.month}-${monthBlock?.year}`}
|
||||
className="flex outline-[0.5px] outline-subtle-1"
|
||||
className="flex"
|
||||
style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
<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={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{monthBlock?.title}
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -70,9 +74,9 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
|||
<div
|
||||
key={`sub-title-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
||||
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` }}
|
||||
|
|
@ -80,7 +84,8 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
|||
<div className="space-x-1 text-11 font-medium text-placeholder">
|
||||
<span
|
||||
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()}
|
||||
|
|
@ -96,8 +101,8 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
|||
{weeks?.map((weekBlock) => (
|
||||
<div
|
||||
key={`column-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
||||
"bg-accent-primary/20": weekBlock.today,
|
||||
className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
|
||||
"nodedc-project-gantt-column-today": weekBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -21,16 +21,16 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
|||
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
||||
|
||||
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 &&
|
||||
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
||||
<div
|
||||
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 */}
|
||||
<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={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
|
|
@ -38,19 +38,23 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
|||
{/** Main Quarter Title */}
|
||||
<div className="inline-flex h-7 w-full justify-between">
|
||||
<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={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{quarterBlock?.title}
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -60,9 +64,9 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
|||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
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` }}
|
||||
|
|
@ -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">
|
||||
<span
|
||||
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}
|
||||
|
|
@ -85,8 +90,8 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
|||
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
||||
"bg-accent-primary/20": monthBlock.today,
|
||||
className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
|
||||
"nodedc-project-gantt-column-today": monthBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,16 +18,16 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
|||
const weekBlocks: IWeekBlock[] = renderView;
|
||||
|
||||
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 &&
|
||||
weekBlocks?.map((block, rootIndex) => (
|
||||
<div
|
||||
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 */}
|
||||
<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={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
|
|
@ -35,14 +35,14 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
|||
{/** Main Months Title */}
|
||||
<div className="inline-flex h-7 w-full justify-between">
|
||||
<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={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{block?.title}
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -52,9 +52,9 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
|||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
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` }}
|
||||
|
|
@ -63,7 +63,8 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
|||
<div className="space-x-1 text-11 font-medium">
|
||||
<span
|
||||
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()}
|
||||
|
|
@ -74,17 +75,17 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
|||
</div>
|
||||
</div>
|
||||
{/** 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) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
||||
"bg-accent-primary/20": weekDay.today,
|
||||
className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
|
||||
"nodedc-project-gantt-column-today": weekDay.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
{["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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
* 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const LeftResizable = observer(function LeftResizable(props: LeftResizabl
|
|||
<>
|
||||
{(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="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
|
||||
|
|
@ -56,7 +56,7 @@ export const LeftResizable = observer(function LeftResizable(props: LeftResizabl
|
|||
/>
|
||||
<div
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const RightResizable = observer(function RightResizable(props: RightResiz
|
|||
<>
|
||||
{(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="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
|
||||
|
|
@ -54,7 +54,7 @@ export const RightResizable = observer(function RightResizable(props: RightResiz
|
|||
/>
|
||||
<div
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const ChartDraggable = observer(function ChartDraggable(props: Props) {
|
|||
} = props;
|
||||
|
||||
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 */}
|
||||
{(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && (
|
||||
<LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
|
||||
|
|
@ -55,7 +55,7 @@ export const ChartDraggable = observer(function ChartDraggable(props: Props) {
|
|||
position={block.position}
|
||||
/>
|
||||
<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,
|
||||
})}
|
||||
onMouseDown={(e) => enableBlockMove && handleBlockDrag(e, "move")}
|
||||
|
|
|
|||
|
|
@ -44,23 +44,19 @@ export const IssuesSidebarBlock = observer(function IssuesSidebarBlock(props: Pr
|
|||
|
||||
return (
|
||||
<div
|
||||
className={cn("group/list-block", {
|
||||
"rounded-sm bg-layer-1": isDragging,
|
||||
"rounded-l-sm border border-r-0 border-accent-strong": getIsIssuePeeked(block.data.id),
|
||||
"border border-r-0 border-strong-1": isIssueFocused,
|
||||
className={cn("nodedc-project-gantt-sidebar-block group/list-block", {
|
||||
"nodedc-project-gantt-sidebar-block-dragging": isDragging,
|
||||
"nodedc-project-gantt-sidebar-block-peeked": getIsIssuePeeked(block.data.id),
|
||||
"nodedc-project-gantt-sidebar-block-focused": isIssueFocused,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
>
|
||||
<Row
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-2 bg-layer-transparent pr-4 hover:bg-layer-transparent-hover",
|
||||
{
|
||||
"bg-layer-transparent-hover": isBlockHoveredOn,
|
||||
"bg-accent-primary/5 hover:bg-accent-primary/10": isIssueSelected,
|
||||
"bg-accent-primary/10": isIssueSelected && isBlockHoveredOn,
|
||||
}
|
||||
)}
|
||||
className={cn("nodedc-project-gantt-sidebar-row group flex w-full items-center gap-2 pr-4", {
|
||||
"nodedc-project-gantt-sidebar-row-hovered": isBlockHoveredOn,
|
||||
"nodedc-project-gantt-sidebar-row-selected": isIssueSelected,
|
||||
})}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Prop
|
|||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="nodedc-project-gantt-sidebar-list">
|
||||
{blockIds ? (
|
||||
<>
|
||||
{blockIds.map((blockId, index) => {
|
||||
|
|
@ -117,7 +117,7 @@ export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Prop
|
|||
})}
|
||||
{canLoadMoreBlocks && (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -56,14 +56,14 @@ export const GanttChartSidebar = observer(function GanttChartSidebar(props: Prop
|
|||
<Row
|
||||
// DO NOT REMOVE THE ID
|
||||
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={{
|
||||
width: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
variant={ERowVariant.HUGGING}
|
||||
>
|
||||
<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={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
|
|
@ -88,7 +88,7 @@ export const GanttChartSidebar = observer(function GanttChartSidebar(props: Prop
|
|||
<h6>{t("common.duration")}</h6>
|
||||
</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({
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ const EXTERNAL_DECK_LIMIT = 10;
|
|||
const INTERNAL_CURSOR = `${INTERNAL_DECK_LIMIT}:0:0`;
|
||||
|
||||
type HomeRecentIssueDecksProps = {
|
||||
compact?: boolean;
|
||||
project?: THomeProjectData;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
type DeckSectionProps = {
|
||||
compact?: boolean;
|
||||
count: number;
|
||||
description: string;
|
||||
emptyDescription: string;
|
||||
|
|
@ -49,6 +51,7 @@ type DeckSectionProps = {
|
|||
};
|
||||
|
||||
type InternalIssueCardProps = {
|
||||
compact?: boolean;
|
||||
isActive: boolean;
|
||||
issue: TIssue;
|
||||
onSelect: () => void;
|
||||
|
|
@ -56,6 +59,7 @@ type InternalIssueCardProps = {
|
|||
};
|
||||
|
||||
type ExternalIssueCardProps = {
|
||||
compact?: boolean;
|
||||
isActive: boolean;
|
||||
onSelect: () => void;
|
||||
project: THomeProjectData;
|
||||
|
|
@ -76,14 +80,14 @@ const sortByRecentCreatedDate = <
|
|||
};
|
||||
|
||||
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 (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<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 className="nodedc-toolbar-pill inline-flex items-center gap-2">
|
||||
<Sparkles className="size-3.5" />
|
||||
|
|
@ -91,16 +95,24 @@ const DeckSection = (props: DeckSectionProps) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-task-deck-scroller">
|
||||
<div className="flex min-h-[236px] items-end px-1 py-4">
|
||||
<div
|
||||
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
|
||||
? Array.from({ length: 4 }, (_, index) => (
|
||||
<div
|
||||
key={`skeleton-${title}-${index}`}
|
||||
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}
|
||||
|
|
@ -118,7 +130,7 @@ const DeckSection = (props: DeckSectionProps) => {
|
|||
};
|
||||
|
||||
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 { getProjectById } = useProject();
|
||||
const { getUserDetails } = useMember();
|
||||
|
|
@ -183,7 +195,7 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
|||
|
||||
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 ? (
|
||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
||||
) : (
|
||||
|
|
@ -191,7 +203,12 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
|||
)}
|
||||
</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" />
|
||||
<span className="truncate">{dueDateLabel}</span>
|
||||
</div>
|
||||
|
|
@ -199,14 +216,24 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
|||
);
|
||||
|
||||
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
|
||||
isActive={isActive}
|
||||
surfaceClassName={cn(
|
||||
"nodedc-home-task-card-surface px-0",
|
||||
compact && "!rounded-[24px]",
|
||||
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}
|
||||
subtitle={sourceContourName}
|
||||
title={issue.name}
|
||||
|
|
@ -217,15 +244,13 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
|||
});
|
||||
|
||||
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 { getStateById } = useProjectState();
|
||||
const { iconBubbleClasses, pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
||||
|
||||
const issue = request.issue;
|
||||
const isOutgoing = request.direction
|
||||
? request.direction === "outgoing"
|
||||
: request.source_project_id === project.id;
|
||||
const isOutgoing = request.direction ? request.direction === "outgoing" : request.source_project_id === project.id;
|
||||
const requester =
|
||||
request.requested_by?.display_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");
|
||||
|
||||
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
|
||||
data-active={isActive}
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<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="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
|
|
@ -283,7 +321,7 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
|||
|
||||
<div
|
||||
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]"
|
||||
)}
|
||||
>
|
||||
|
|
@ -291,12 +329,21 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<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={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 className="flex items-center justify-between gap-3">
|
||||
<div className={cn("inline-flex min-h-9 items-center rounded-full pl-1 pr-2", pillBackgroundClasses)}>
|
||||
<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 pr-2 pl-1", pillBackgroundClasses)}>
|
||||
{(issue.assignee_ids?.length ?? 0) > 0 ? (
|
||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
||||
) : (
|
||||
|
|
@ -304,7 +351,12 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
|||
)}
|
||||
</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" />
|
||||
<span className="truncate">{dueDateLabel}</span>
|
||||
</div>
|
||||
|
|
@ -316,7 +368,7 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
|||
});
|
||||
|
||||
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 [selectedExternalRequestId, setSelectedExternalRequestId] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -382,9 +434,13 @@ export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props
|
|||
if (!project) {
|
||||
return (
|
||||
<HomeCardShell
|
||||
eyebrow="Task Decks"
|
||||
title="Последние задачи по проекту"
|
||||
description="Выберите проект слева, и здесь появятся колоды последних задач внешнего и внутреннего контуров."
|
||||
eyebrow={compact ? "Последние задачи" : "Task Decks"}
|
||||
title={compact ? "Последние задачи проекта" : "Последние задачи по проекту"}
|
||||
description={
|
||||
compact
|
||||
? undefined
|
||||
: "Выберите проект слева, и здесь появятся колоды последних задач внешнего и внутреннего контуров."
|
||||
}
|
||||
>
|
||||
<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) => (
|
||||
<div
|
||||
const internalIssueCards = internalIssues.map((issue) => (
|
||||
<HomeInternalContourDeckCard
|
||||
compact={compact}
|
||||
key={issue.id}
|
||||
className={cn({ "-ml-16": index > 0 })}
|
||||
style={{ zIndex: issue.id === selectedInternalIssueId ? internalIssues.length + 6 : index + 1 }}
|
||||
>
|
||||
<HomeInternalContourDeckCard
|
||||
issue={issue}
|
||||
isActive={issue.id === selectedInternalIssueId}
|
||||
onSelect={() => setSelectedInternalIssueId(issue.id)}
|
||||
project={project}
|
||||
/>
|
||||
</div>
|
||||
issue={issue}
|
||||
isActive={issue.id === selectedInternalIssueId}
|
||||
onSelect={() => setSelectedInternalIssueId(issue.id)}
|
||||
project={project}
|
||||
/>
|
||||
));
|
||||
|
||||
const externalIssueCards = externalRequests.map((request, index) => (
|
||||
<div
|
||||
const externalIssueCards = externalRequests.map((request) => (
|
||||
<HomeExternalContourDeckCard
|
||||
compact={compact}
|
||||
key={request.id}
|
||||
className={cn({ "-ml-16": index > 0 })}
|
||||
style={{ zIndex: request.id === selectedExternalRequestId ? externalRequests.length + 6 : index + 1 }}
|
||||
>
|
||||
<HomeExternalContourDeckCard
|
||||
isActive={request.id === selectedExternalRequestId}
|
||||
onSelect={() => setSelectedExternalRequestId(request.id)}
|
||||
project={project}
|
||||
request={request}
|
||||
/>
|
||||
</div>
|
||||
isActive={request.id === selectedExternalRequestId}
|
||||
onSelect={() => setSelectedExternalRequestId(request.id)}
|
||||
project={project}
|
||||
request={request}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<HomeCardShell
|
||||
eyebrow={`${project.identifier} • последние задачи`}
|
||||
title="Последние задачи проекта"
|
||||
description="Ниже собраны две колоды для быстрого просмотра новых карточек во внешнем и внутреннем контурах."
|
||||
contentClassName="space-y-5 p-5"
|
||||
eyebrow={compact ? `${project.identifier} • последние задачи` : `${project.identifier} • последние задачи`}
|
||||
title={compact ? "Последние задачи проекта" : "Последние задачи проекта"}
|
||||
description={
|
||||
compact
|
||||
? undefined
|
||||
: "Ниже собраны две колоды для быстрого просмотра новых карточек во внешнем и внутреннем контурах."
|
||||
}
|
||||
contentClassName={compact ? "space-y-3 p-4" : "grid gap-5 p-5 xl:grid-cols-2"}
|
||||
>
|
||||
<DeckSection
|
||||
compact={compact}
|
||||
count={externalRequests.length}
|
||||
description="Последние запросы и задачи внешнего контура по текущему проекту."
|
||||
description={
|
||||
compact ? "Последние внешние карточки" : "Последние запросы и задачи внешнего контура по текущему проекту."
|
||||
}
|
||||
emptyDescription="У проекта пока нет внешних контурных задач. Когда появится первый обмен с контрагентом, он сразу попадет в эту колоду."
|
||||
emptyTitle="Внешний контур пока пуст"
|
||||
isLoading={isExternalRequestsLoading}
|
||||
items={externalIssueCards}
|
||||
title="Последние задачи внешнего контура"
|
||||
title={compact ? "Внешний контур" : "Внешний контур"}
|
||||
/>
|
||||
|
||||
<DeckSection
|
||||
compact={compact}
|
||||
count={internalIssues.length}
|
||||
description="Последние добавленные внутренние задачи выбранного проекта."
|
||||
description={
|
||||
compact ? "Последние внутренние карточки" : "Последние добавленные внутренние задачи выбранного проекта."
|
||||
}
|
||||
emptyDescription="Во внутреннем контуре пока нет задач. Как только в проекте появится новая карточка, она ляжет сюда первой."
|
||||
emptyTitle="Внутренний контур пока пуст"
|
||||
isLoading={isInternalIssuesLoading}
|
||||
items={internalIssueCards}
|
||||
title="Последние задачи внутреннего контура"
|
||||
title={compact ? "Внутренний контур" : "Внутренний контур"}
|
||||
/>
|
||||
</HomeCardShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@
|
|||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// plane types
|
||||
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 { getCompletionRate, type THomeProjectData } from "./home.utils";
|
||||
|
||||
export interface IUserGreetingsView {
|
||||
user: IUser;
|
||||
workspaceName?: string | null;
|
||||
selectedProject?: THomeProjectData;
|
||||
selectedProjectAnalytics?: TProjectAnalyticsCount;
|
||||
}
|
||||
|
||||
export function UserGreetingsView(props: IUserGreetingsView) {
|
||||
const { user, workspaceName, selectedProject, selectedProjectAnalytics } = props;
|
||||
// current time hook
|
||||
const { user, workspaceName } = props;
|
||||
const { currentTime } = useCurrentTime();
|
||||
// store hooks
|
||||
const { t, currentLocale } = useTranslation();
|
||||
|
||||
const hour = new Intl.DateTimeFormat(currentLocale, {
|
||||
|
|
@ -46,39 +42,37 @@ export function UserGreetingsView(props: IUserGreetingsView) {
|
|||
}).format(currentTime);
|
||||
|
||||
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 (
|
||||
<section className="nodedc-home-card px-5 py-4">
|
||||
<div className="grid items-stretch gap-3 xl:grid-cols-[minmax(0,1fr)_280px_220px]">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<section className="nodedc-home-user-card">
|
||||
<div className="nodedc-home-user-card-orb" />
|
||||
<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="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">
|
||||
{workspaceName ?? "Workspace Home"}
|
||||
</div>
|
||||
<h2 className="mt-1 truncate text-24 font-semibold text-primary">
|
||||
{`${t("good")} ${t(greeting)}, ${user?.first_name} ${user?.last_name}`}
|
||||
<div className="text-12 text-white/58">{`${weekDay}, ${date} ${timeString}`}</div>
|
||||
<h2 className="mt-1 line-clamp-2 text-24 font-semibold leading-tight text-white">
|
||||
{`${t("good")} ${t(greeting)}, ${user?.first_name || "DC"}`}
|
||||
</h2>
|
||||
<div className="mt-1 text-13 text-secondary">{`${weekDay}, ${date} ${timeString}`}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3">
|
||||
<div className="text-12 font-medium text-secondary">Текущий фокус</div>
|
||||
<div className="mt-2 line-clamp-2 text-18 font-semibold text-primary">
|
||||
{selectedProject ? selectedProject.name : "Выберите проект слева"}
|
||||
</div>
|
||||
<div className="mt-1 text-12 text-secondary">
|
||||
{selectedProject ? selectedProject.identifier : "Домашняя сводка перестроится под выбранную карточку."}
|
||||
</div>
|
||||
<div className="mt-8 space-y-1">
|
||||
<div className="text-18 font-semibold text-white">{userName}</div>
|
||||
<div className="truncate text-13 text-white/62">{userEmail}</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3">
|
||||
<div className="text-12 font-medium text-secondary">Прогресс фокуса</div>
|
||||
<div className="mt-2 text-18 font-semibold text-primary">{selectedProject ? `${completionRate}%` : "—"}</div>
|
||||
<div className="mt-1 text-12 text-secondary">
|
||||
{selectedProject ? "Закрытые задачи относительно общего объёма." : "Станет доступен после выбора проекта."}
|
||||
</div>
|
||||
<div className="mt-5 flex flex-wrap items-center gap-2 text-[11px] font-medium text-white/56">
|
||||
<div className="rounded-full bg-black/24 px-3 py-1.5 backdrop-blur-md">home admin</div>
|
||||
<div className="rounded-full bg-black/24 px-3 py-1.5 backdrop-blur-md">{workspaceName ?? "workspace"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ import { useTranslation } from "@plane/i18n";
|
|||
// plane types
|
||||
import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons";
|
||||
import type { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types";
|
||||
// plane ui
|
||||
// components
|
||||
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { getActivityProjectId } from "../../home.utils";
|
||||
|
|
@ -105,20 +102,15 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
|
|||
);
|
||||
|
||||
return (
|
||||
<ContentOverflowWrapper
|
||||
maxHeight={415}
|
||||
containerClassName="box-border min-h-[250px]"
|
||||
fallback={<></>}
|
||||
buttonClassName="nodedc-toolbar-pill justify-center"
|
||||
>
|
||||
<div className="box-border min-h-[250px]">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="text-14 font-semibold text-tertiary">{t("home.recents.title")}</div>
|
||||
{showFilterSelect && <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />}
|
||||
</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 && recents.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
|
||||
</div>
|
||||
</ContentOverflowWrapper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useTheme } from "next-themes";
|
|||
import { Combobox } from "@headlessui/react";
|
||||
// plane imports
|
||||
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 type { ISearchIssueResponse } from "@plane/types";
|
||||
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>}
|
||||
<ul className="text-13 text-primary">
|
||||
{filteredIssues.map((issue) => {
|
||||
const stateColor = issue.state__color || "";
|
||||
const stateColor = getStateGroupColor(issue.state__group, issue.state__color);
|
||||
|
||||
return (
|
||||
<Combobox.Option
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useRouter } from "next/navigation";
|
|||
import { MinusCircle } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||
import { getStateGroupColor } from "@plane/propel/icons";
|
||||
import type { TIssue } from "@plane/types";
|
||||
// component
|
||||
// ui
|
||||
|
|
@ -55,7 +56,7 @@ export const IssueParentDetail = observer(function IssueParentDetail(props: TIss
|
|||
const issueParentState = getProjectStates(parentIssue?.project_id)?.find(
|
||||
(state) => state?.id === parentIssue?.state_id
|
||||
);
|
||||
const stateColor = issueParentState?.color || undefined;
|
||||
const stateColor = getStateGroupColor(issueParentState?.group, issueParentState?.color);
|
||||
|
||||
if (!parentIssue) return <></>;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useParams } from "next/navigation";
|
|||
import { MoreHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import { Popover } from "@plane/propel/popover";
|
||||
import { getStateGroupColor } from "@plane/propel/icons";
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { ControlLink } from "@plane/ui";
|
||||
import { cn, generateWorkItemLink } from "@plane/utils";
|
||||
|
|
@ -50,16 +51,15 @@ export const CalendarIssueBlock = observer(
|
|||
const { issuesFilter } = useIssues(storeType);
|
||||
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);
|
||||
|
||||
// handlers
|
||||
const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug.toString(), issue, isMobile);
|
||||
|
||||
const customActionButton = (
|
||||
<div
|
||||
className="w-full cursor-pointer rounded-sm p-1 text-secondary hover:bg-layer-1 hover:text-primary"
|
||||
>
|
||||
<div 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" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { getStateGroupColor } from "@plane/propel/icons";
|
||||
import { Popover } from "@plane/propel/popover";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { ControlLink } from "@plane/ui";
|
||||
|
|
@ -53,7 +54,15 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) {
|
|||
const stateDetails =
|
||||
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);
|
||||
|
||||
|
|
@ -66,14 +75,14 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) {
|
|||
render={
|
||||
<div
|
||||
id={`issue-${issueId}`}
|
||||
className="space-between relative flex h-full w-full cursor-pointer items-center rounded-sm"
|
||||
style={blockStyle}
|
||||
className="nodedc-project-gantt-issue-bar space-between relative flex h-full w-full cursor-pointer items-center"
|
||||
style={ganttBlockStyle}
|
||||
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
|
||||
className="sticky w-auto flex-1 truncate overflow-hidden px-2.5 py-1 text-13 text-primary"
|
||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||
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`, color: ganttBlockTextColor }}
|
||||
>
|
||||
{issueDetails?.name}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -73,11 +73,12 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
|||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||
const { pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
||||
const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
|
||||
const controlStatusIconColor = selectedState?.group === "started" ? "#F5F7FB" : statusIconColor;
|
||||
|
||||
const creatorName = creatorDetails?.display_name ?? t("common.none");
|
||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||
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 = (
|
||||
<DateDropdown
|
||||
className="h-auto self-start"
|
||||
|
|
@ -112,7 +113,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
|||
<div
|
||||
data-control-link-ignore="true"
|
||||
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
|
||||
? "text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90"
|
||||
: "bg-[#111214] text-white hover:bg-[#0A0B0C]"
|
||||
|
|
@ -131,6 +132,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
|||
name={creatorName}
|
||||
size="md"
|
||||
showTooltip={!isMobile}
|
||||
className="border-0 shadow-none ring-0 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
|
|
@ -139,7 +141,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
|||
</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
|
||||
value={issue.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}>
|
||||
<StateGroupIcon
|
||||
stateGroup={selectedState?.group ?? "backlog"}
|
||||
color={statusIconColor}
|
||||
color={controlStatusIconColor}
|
||||
className="h-4 w-4"
|
||||
percentage={selectedState?.order}
|
||||
/>
|
||||
|
|
@ -190,7 +192,10 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
|||
button={
|
||||
<div
|
||||
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" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { observer } from "mobx-react";
|
|||
import type { Control } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { ETabIndices } from "@plane/constants";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import { CloseIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||
// plane imports
|
||||
// types
|
||||
import type { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
|
|
@ -44,7 +44,7 @@ export const IssueParentTag = observer(function IssueParentTag(props: TIssuePare
|
|||
<span
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: selectedParentIssue.state__color,
|
||||
backgroundColor: getStateGroupColor(selectedParentIssue.state__group, selectedParentIssue.state__color),
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-secondary">
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Rocket } from "lucide-react";
|
|||
import { Combobox } from "@headlessui/react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { SearchIcon } from "@plane/propel/icons";
|
||||
import { SearchIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { ISearchIssueResponse } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -159,7 +159,7 @@ export function ParentIssuesListModal({
|
|||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state__color,
|
||||
backgroundColor: getStateGroupColor(issue.state__group, issue.state__color),
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0">
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ export const ModuleGanttBlock = observer(function ModuleGanttBlock(props: Props)
|
|||
moduleDetails,
|
||||
MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? ""
|
||||
);
|
||||
const ganttBlockStyle = {
|
||||
...blockStyle,
|
||||
maskImage: "none",
|
||||
WebkitMaskImage: "none",
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
|
@ -53,15 +58,15 @@ export const ModuleGanttBlock = observer(function ModuleGanttBlock(props: Props)
|
|||
position="top-start"
|
||||
>
|
||||
<div
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded-sm"
|
||||
style={blockStyle}
|
||||
className="nodedc-project-gantt-issue-bar relative flex h-full w-full cursor-pointer items-center"
|
||||
style={ganttBlockStyle}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/${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
|
||||
className="sticky w-auto truncate overflow-hidden px-2.5 py-1 text-13 text-primary"
|
||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||
|
|
|
|||
|
|
@ -311,10 +311,10 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
};
|
||||
|
||||
const refreshVisibleIssueStores = useCallback(
|
||||
async (createdProjectId?: string) => {
|
||||
async () => {
|
||||
const refreshes: Promise<unknown>[] = [];
|
||||
|
||||
if (createdProjectId && activeProjectId === createdProjectId) {
|
||||
if (activeProjectId) {
|
||||
refreshes.push(refreshProjectIssues(workspaceSlug, activeProjectId, "mutation"));
|
||||
if (activeProjectViewId) {
|
||||
refreshes.push(refreshProjectViewIssues(workspaceSlug, activeProjectId, activeProjectViewId, "mutation"));
|
||||
|
|
@ -361,7 +361,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
action,
|
||||
draft: parseResult.draft,
|
||||
});
|
||||
await refreshVisibleIssueStores(result.project_id);
|
||||
await refreshVisibleIssueStores();
|
||||
setCommitResult(result);
|
||||
setStatus("committed");
|
||||
setToast({
|
||||
|
|
@ -456,7 +456,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
{parseResult.transcript && (
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -524,7 +524,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
{parseResult.draft.description && (
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -23,31 +23,31 @@ export const STATE_GROUPS: {
|
|||
key: "backlog",
|
||||
label: "Backlog",
|
||||
defaultStateName: "Backlog",
|
||||
color: "#d9d9d9",
|
||||
color: "#050505",
|
||||
},
|
||||
unstarted: {
|
||||
key: "unstarted",
|
||||
label: "Unstarted",
|
||||
defaultStateName: "Todo",
|
||||
color: "#3f76ff",
|
||||
color: "#7C7F85",
|
||||
},
|
||||
started: {
|
||||
key: "started",
|
||||
label: "Started",
|
||||
defaultStateName: "In Progress",
|
||||
color: "#f59e0b",
|
||||
color: "#FFFFFF",
|
||||
},
|
||||
completed: {
|
||||
key: "completed",
|
||||
label: "Completed",
|
||||
defaultStateName: "Done",
|
||||
color: "#16a34a",
|
||||
color: "#C3FF66",
|
||||
},
|
||||
cancelled: {
|
||||
key: "cancelled",
|
||||
label: "Canceled",
|
||||
defaultStateName: "Cancelled",
|
||||
color: "#dc2626",
|
||||
color: "#050505",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -92,22 +92,22 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [
|
|||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#16A34A",
|
||||
color: "#C3FF66",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#F59E0B",
|
||||
color: "#FFFFFF",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#3A3A3A",
|
||||
color: "#7C7F85",
|
||||
},
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#A3A3A3",
|
||||
color: "#050505",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
export * from "./backlog-group-icon";
|
||||
export * from "./cancelled-group-icon";
|
||||
export * from "./completed-group-icon";
|
||||
export * from "./helper";
|
||||
export * from "./started-group-icon";
|
||||
export * from "./state-group-icon";
|
||||
export * from "./unstarted-group-icon";
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { BacklogGroupIcon } from "./backlog-group-icon";
|
|||
import { CancelledGroupIcon } from "./cancelled-group-icon";
|
||||
import { CompletedGroupIcon } from "./completed-group-icon";
|
||||
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 { UnstartedGroupIcon } from "./unstarted-group-icon";
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ export function StateGroupIcon({
|
|||
<StateIconComponent
|
||||
height={STATE_GROUP_SIZES[size]}
|
||||
width={STATE_GROUP_SIZES[size]}
|
||||
color={color ?? STATE_GROUP_COLORS[stateGroup]}
|
||||
color={getStateGroupColor(stateGroup, color)}
|
||||
className={`flex-shrink-0 ${className}`}
|
||||
percentage={percentage}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue