ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: стабилизация контуров, досок и Voice Tasker
This commit is contained in:
parent
597480adb9
commit
7209d2caab
|
|
@ -252,12 +252,15 @@ MVP-правило:
|
||||||
|
|
||||||
Система:
|
Система:
|
||||||
|
|
||||||
1. берет последние voice-действия пользователя в текущем workspace;
|
1. берет последние только реально примененные voice-действия пользователя в текущем workspace;
|
||||||
2. находит последную созданную/обновленную voice-задачу;
|
2. игнорирует parsed-сессии без `created_task`/`updated_task`, чтобы модель не цеплялась за старые неудачные черновики;
|
||||||
3. показывает preview изменения, если confidence низкий;
|
3. если transcript явно задает исходный проект ("из Бухгалтерии", "последнюю добавленную в Бухгалтерию"), сначала ищет последнюю voice-задачу в этом проекте;
|
||||||
4. меняет `Issue.target_date`;
|
4. если исходный проект не назван, сначала ищет последнюю voice-задачу в текущем открытом проекте;
|
||||||
5. сохраняет `due_time` в description note / parsed JSON;
|
5. затем использует последнюю примененную voice-задачу workspace как общий fallback;
|
||||||
6. пишет новое действие в session-backed memory.
|
6. показывает preview изменения, если confidence низкий;
|
||||||
|
7. меняет `Issue.target_date`;
|
||||||
|
8. сохраняет `due_time` в description note / parsed JSON;
|
||||||
|
9. пишет новое действие в session-backed memory.
|
||||||
|
|
||||||
Если пользователь говорит "переложи последнюю задачу в проект X", это остается `update_task`, но backend должен:
|
Если пользователь говорит "переложи последнюю задачу в проект X", это остается `update_task`, но backend должен:
|
||||||
|
|
||||||
|
|
@ -882,6 +885,7 @@ Return JSON only.
|
||||||
6. если confidence низкий - preview с ручным выбором;
|
6. если confidence низкий - preview с ручным выбором;
|
||||||
7. если transcript содержит явную маршрутизацию проекта, но model `project_hint` указывает на проект, которого нет в transcript, auto-commit блокируется;
|
7. если transcript содержит явную маршрутизацию проекта, но model `project_hint` указывает на проект, которого нет в transcript, auto-commit блокируется;
|
||||||
8. при наличии source/destination фразы выбирать destination project: "из Бухгалтерии в Менеджмент" -> target `Менеджмент`, "из Менеджмента в Бухгалтерию" -> target `Бухгалтерия`.
|
8. при наличии source/destination фразы выбирать destination project: "из Бухгалтерии в Менеджмент" -> target `Менеджмент`, "из Менеджмента в Бухгалтерию" -> target `Бухгалтерия`.
|
||||||
|
9. глагол "перенеси/перенести" рядом со сроком/датой считается date update, а не project routing, если нет project/контур-маркера или явного project destination.
|
||||||
|
|
||||||
Не зашивать термин "контур" как обязательный. Для NODE DC это важный UX-термин, но технически это обычный `Project`.
|
Не зашивать термин "контур" как обязательный. Для NODE DC это важный UX-термин, но технически это обычный `Project`.
|
||||||
|
|
||||||
|
|
@ -947,11 +951,28 @@ MVP:
|
||||||
- `N` дней/недель/месяцев/лет назад;
|
- `N` дней/недель/месяцев/лет назад;
|
||||||
- сложные интервалы: "два месяца и две недели";
|
- сложные интервалы: "два месяца и две недели";
|
||||||
- числительные цифрами и частые русские словоформы: "2 недели", "две недели", "пару дней";
|
- числительные цифрами и частые русские словоформы: "2 недели", "две недели", "пару дней";
|
||||||
|
- абсолютные русские даты: "1 мая 2026 года", "30 апреля";
|
||||||
|
- числовые даты: "01.05.2026", "1/05/26";
|
||||||
- защита от ложных матчей внутри слов: "последней" не считается как "дней";
|
- защита от ложных матчей внутри слов: "последней" не считается как "дней";
|
||||||
|
- защита от трактовки года абсолютной даты как относительного интервала: "2026 года" не считается как "+2026 лет";
|
||||||
- конкретная дата;
|
- конкретная дата;
|
||||||
- конкретное время как `due_time` note.
|
- конкретное время как `due_time` note.
|
||||||
|
|
||||||
Date resolver обязан работать после OpenAI parser как deterministic fallback. Если модель уже вернула валидный `due_date`, backend его не переписывает.
|
Date resolver обязан работать после OpenAI parser как deterministic слой. Сначала резолвятся абсолютные даты из transcript; они могут переписать ошибочный `due_date` от модели. Затем обрабатываются относительные сдвиги вида "подвинь на 3 дня вперед" / "передвинь назад на 3 дня": backend может переписать `due_date`, даже если модель уже вернула дату, а база расчета берется из текущего `Issue.target_date`, а не из сегодняшней даты. Для фраз вида "через 3 дня" без маркера сдвига база остается текущей датой.
|
||||||
|
|
||||||
|
### 10.4.1. Memory resolver
|
||||||
|
|
||||||
|
`recent_voice_memory` для parser содержит только примененные voice-сессии, у которых есть доступная `target_task`.
|
||||||
|
|
||||||
|
При backend commit:
|
||||||
|
|
||||||
|
1. explicit issue key/issue id остается самым сильным указанием цели;
|
||||||
|
2. `target_memory_ref` на voice-сессию используется только если эта сессия реально связана с доступной задачей;
|
||||||
|
3. если transcript содержит общее указание "последняя/предыдущая/эта", backend не доверяет model-selected voice session ref и выбирает цель deterministic fallback-ом;
|
||||||
|
4. если ref ведет в parsed/no-op сессию, resolver переходит к deterministic fallback;
|
||||||
|
5. fallback сначала учитывает явно названный source project;
|
||||||
|
6. затем текущий project из `client_context.current_project_id`;
|
||||||
|
7. затем последнюю примененную voice-задачу workspace.
|
||||||
|
|
||||||
### 10.5. Voice task representation in Issue
|
### 10.5. Voice task representation in Issue
|
||||||
|
|
||||||
|
|
@ -1201,6 +1222,7 @@ voice_task.error
|
||||||
23. Относительная дата "на две недели вперед" меняет `Issue.target_date` без ручного ввода ISO-даты.
|
23. Относительная дата "на две недели вперед" меняет `Issue.target_date` без ручного ввода ISO-даты.
|
||||||
24. Перенос "из Бухгалтерии в Менеджмент" и "из Менеджмента в Бухгалтерию" реально меняет `Issue.project_id` и `task_key`.
|
24. Перенос "из Бухгалтерии в Менеджмент" и "из Менеджмента в Бухгалтерию" реально меняет `Issue.project_id` и `task_key`.
|
||||||
25. Voice-created task имеет `external_source=voice_tasker` и хранит исходный transcript в description.
|
25. Voice-created task имеет `external_source=voice_tasker` и хранит исходный transcript в description.
|
||||||
|
26. Preview modal показывает transcript/description полностью без внутреннего scroll внутри текстовых блоков.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,32 @@ VOICE_TASK_STATE_GROUP_HINTS = {
|
||||||
}
|
}
|
||||||
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
TIME_PATTERN = re.compile(r"^\d{2}:\d{2}$")
|
TIME_PATTERN = re.compile(r"^\d{2}:\d{2}$")
|
||||||
|
VOICE_TASK_MONTHS = {
|
||||||
|
"январь": 1,
|
||||||
|
"января": 1,
|
||||||
|
"февраль": 2,
|
||||||
|
"февраля": 2,
|
||||||
|
"март": 3,
|
||||||
|
"марта": 3,
|
||||||
|
"апрель": 4,
|
||||||
|
"апреля": 4,
|
||||||
|
"май": 5,
|
||||||
|
"мая": 5,
|
||||||
|
"июнь": 6,
|
||||||
|
"июня": 6,
|
||||||
|
"июль": 7,
|
||||||
|
"июля": 7,
|
||||||
|
"август": 8,
|
||||||
|
"августа": 8,
|
||||||
|
"сентябрь": 9,
|
||||||
|
"сентября": 9,
|
||||||
|
"октябрь": 10,
|
||||||
|
"октября": 10,
|
||||||
|
"ноябрь": 11,
|
||||||
|
"ноября": 11,
|
||||||
|
"декабрь": 12,
|
||||||
|
"декабря": 12,
|
||||||
|
}
|
||||||
VOICE_TASK_NUMBER_WORDS = {
|
VOICE_TASK_NUMBER_WORDS = {
|
||||||
"один": 1,
|
"один": 1,
|
||||||
"одна": 1,
|
"одна": 1,
|
||||||
|
|
@ -152,6 +178,14 @@ VOICE_TASK_RELATIVE_DATE_PATTERN = re.compile(
|
||||||
r"(?P<unit>день|дня|дней|сутки|суток|неделю|неделя|недели|недель|"
|
r"(?P<unit>день|дня|дней|сутки|суток|неделю|неделя|недели|недель|"
|
||||||
r"месяц|месяца|месяцев|год|года|лет)(?![0-9a-zа-я])"
|
r"месяц|месяца|месяцев|год|года|лет)(?![0-9a-zа-я])"
|
||||||
)
|
)
|
||||||
|
VOICE_TASK_MONTH_NAME_PATTERN = "|".join(sorted(VOICE_TASK_MONTHS.keys(), key=len, reverse=True))
|
||||||
|
VOICE_TASK_ABSOLUTE_MONTH_DATE_PATTERN = re.compile(
|
||||||
|
rf"(?<![0-9a-zа-я])(?P<day>[0-3]?\d)\s+(?P<month>{VOICE_TASK_MONTH_NAME_PATTERN})"
|
||||||
|
r"(?:\s+(?P<year>\d{4}))?(?:\s+года?)?(?![0-9a-zа-я])"
|
||||||
|
)
|
||||||
|
VOICE_TASK_NUMERIC_DATE_PATTERN = re.compile(
|
||||||
|
r"(?<!\d)(?P<day>[0-3]?\d)[./-](?P<month>[01]?\d)(?:[./-](?P<year>\d{2,4}))?(?!\d)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def normalize_audio_content_type(content_type):
|
def normalize_audio_content_type(content_type):
|
||||||
|
|
@ -443,13 +477,15 @@ def transcript_has_project_routing_request(transcript):
|
||||||
if not normalized:
|
if not normalized:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
normalized = normalized.lower()
|
normalized = normalize_match_value(normalized)
|
||||||
return bool(
|
if re.search(r"(проект|контур|route|move\s+to\s+project|project)", normalized):
|
||||||
re.search(
|
return True
|
||||||
r"(проект|контур|перелож|перенес|перемест|перекин|route|move\s+to\s+project|project)",
|
|
||||||
normalized,
|
has_transfer_verb = bool(re.search(r"(перелож|перенес|перемест|перекин|move)", normalized))
|
||||||
)
|
has_due_marker = bool(
|
||||||
|
re.search(r"(срок|дат|дедлайн|deadline|завтра|сегодня|послезавтра|вчера|дн(я|ей|ь)|недел|месяц|год|лет)", normalized)
|
||||||
)
|
)
|
||||||
|
return has_transfer_verb and not has_due_marker
|
||||||
|
|
||||||
|
|
||||||
def transcript_contains_project_hint(project_hint, transcript):
|
def transcript_contains_project_hint(project_hint, transcript):
|
||||||
|
|
@ -461,6 +497,19 @@ def transcript_contains_project_hint(project_hint, transcript):
|
||||||
return normalized_hint in normalized_transcript
|
return normalized_hint in normalized_transcript
|
||||||
|
|
||||||
|
|
||||||
|
def transcript_has_generic_memory_reference(transcript):
|
||||||
|
normalized_transcript = normalize_match_value(transcript)
|
||||||
|
if not normalized_transcript:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return bool(
|
||||||
|
re.search(
|
||||||
|
r"\b(последн\w*|предыдущ\w*|прошл\w*|эту|эта|этой|этот|ее|её|его|ту|той)\b",
|
||||||
|
normalized_transcript,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def infer_voice_task_project_from_transcript(projects, transcript):
|
def infer_voice_task_project_from_transcript(projects, transcript):
|
||||||
normalized_transcript = normalize_match_value(transcript)
|
normalized_transcript = normalize_match_value(transcript)
|
||||||
if not normalized_transcript:
|
if not normalized_transcript:
|
||||||
|
|
@ -490,7 +539,7 @@ def infer_voice_task_project_from_transcript(projects, transcript):
|
||||||
elif has_transfer_intent and re.search(r"(в|во|на)\s*$", prefix):
|
elif has_transfer_intent and re.search(r"(в|во|на)\s*$", prefix):
|
||||||
score = 0.99
|
score = 0.99
|
||||||
|
|
||||||
if score > best_score or (score == best_score and alias_length > best_alias_length):
|
if score > 0 and (score > best_score or (score == best_score and alias_length > best_alias_length)):
|
||||||
best_project = project
|
best_project = project
|
||||||
best_score = score
|
best_score = score
|
||||||
best_alias_length = alias_length
|
best_alias_length = alias_length
|
||||||
|
|
@ -501,6 +550,54 @@ def infer_voice_task_project_from_transcript(projects, transcript):
|
||||||
return serialize_resolved_project(best_project, best_score, "transcript_project_hint")
|
return serialize_resolved_project(best_project, best_score, "transcript_project_hint")
|
||||||
|
|
||||||
|
|
||||||
|
def infer_voice_task_source_project_from_transcript(projects, transcript):
|
||||||
|
normalized_transcript = normalize_match_value(transcript)
|
||||||
|
if not normalized_transcript:
|
||||||
|
return None
|
||||||
|
|
||||||
|
has_transfer_intent = bool(re.search(r"(перелож|перенес|перемест|перекин|move|route)", normalized_transcript))
|
||||||
|
best_project = None
|
||||||
|
best_score = 0.0
|
||||||
|
best_alias_length = 0
|
||||||
|
|
||||||
|
for project in projects:
|
||||||
|
for candidate in get_project_alias_candidates(project):
|
||||||
|
normalized_candidate = normalize_match_value(candidate)
|
||||||
|
if len(normalized_candidate) < 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidate_index = normalized_transcript.find(normalized_candidate)
|
||||||
|
if candidate_index < 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
prefix = normalized_transcript[max(0, candidate_index - 56) : candidate_index]
|
||||||
|
alias_length = len(normalized_candidate)
|
||||||
|
score = 0.0
|
||||||
|
|
||||||
|
if re.search(r"(из|с|со|from|source)\s+(?:проекта\s+|контура\s+)?$", prefix):
|
||||||
|
score = 1.0
|
||||||
|
elif re.search(
|
||||||
|
r"(добав\w*|созда\w*|постав\w*)\s+(?:задач\w+\s+)?(в|во|на)\s+(?:проекте\s+|проект\s+|контуре\s+|контур\s+)?$",
|
||||||
|
prefix,
|
||||||
|
):
|
||||||
|
score = 0.95
|
||||||
|
elif (
|
||||||
|
not has_transfer_intent
|
||||||
|
and re.search(r"(в|во|на)\s+(?:проекте\s+|проект\s+|контуре\s+|контур\s+)?$", prefix)
|
||||||
|
):
|
||||||
|
score = 0.9
|
||||||
|
|
||||||
|
if score > 0 and (score > best_score or (score == best_score and alias_length > best_alias_length)):
|
||||||
|
best_project = project
|
||||||
|
best_score = score
|
||||||
|
best_alias_length = alias_length
|
||||||
|
|
||||||
|
if not best_project:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return best_project
|
||||||
|
|
||||||
|
|
||||||
def get_text_match_score(query, candidates):
|
def get_text_match_score(query, candidates):
|
||||||
normalized_query = normalize_match_value(query)
|
normalized_query = normalize_match_value(query)
|
||||||
if not normalized_query:
|
if not normalized_query:
|
||||||
|
|
@ -811,6 +908,61 @@ def add_months_to_date(value, months):
|
||||||
return date(year, month, day)
|
return date(year, month, day)
|
||||||
|
|
||||||
|
|
||||||
|
def build_voice_task_date(day, month, year, current_date, year_was_explicit=False):
|
||||||
|
try:
|
||||||
|
day = int(day)
|
||||||
|
month = int(month)
|
||||||
|
year = int(year) if year else current_date.year
|
||||||
|
if year < 100:
|
||||||
|
year += 2000
|
||||||
|
candidate = date(year, month, day)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not year_was_explicit and candidate < current_date:
|
||||||
|
try:
|
||||||
|
candidate = date(current_date.year + 1, month, day)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return candidate.isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def infer_voice_task_absolute_due_date(transcript, current_date):
|
||||||
|
normalized = normalize_match_value(transcript)
|
||||||
|
if normalized:
|
||||||
|
match = VOICE_TASK_ABSOLUTE_MONTH_DATE_PATTERN.search(normalized)
|
||||||
|
if match:
|
||||||
|
month = VOICE_TASK_MONTHS.get(match.group("month"))
|
||||||
|
result = build_voice_task_date(
|
||||||
|
day=match.group("day"),
|
||||||
|
month=month,
|
||||||
|
year=match.group("year"),
|
||||||
|
current_date=current_date,
|
||||||
|
year_was_explicit=bool(match.group("year")),
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
raw_transcript = normalize_string(transcript)
|
||||||
|
if not raw_transcript:
|
||||||
|
return None
|
||||||
|
|
||||||
|
match = VOICE_TASK_NUMERIC_DATE_PATTERN.search(raw_transcript.lower().replace("ё", "е"))
|
||||||
|
if match:
|
||||||
|
result = build_voice_task_date(
|
||||||
|
day=match.group("day"),
|
||||||
|
month=match.group("month"),
|
||||||
|
year=match.group("year"),
|
||||||
|
current_date=current_date,
|
||||||
|
year_was_explicit=bool(match.group("year")),
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def infer_voice_task_relative_due_date(transcript, current_date, target_issue=None):
|
def infer_voice_task_relative_due_date(transcript, current_date, target_issue=None):
|
||||||
normalized = normalize_match_value(transcript)
|
normalized = normalize_match_value(transcript)
|
||||||
if not normalized:
|
if not normalized:
|
||||||
|
|
@ -868,6 +1020,8 @@ def infer_voice_task_relative_due_date(transcript, current_date, target_issue=No
|
||||||
elif unit.startswith("месяц"):
|
elif unit.startswith("месяц"):
|
||||||
shift_months += quantity
|
shift_months += quantity
|
||||||
elif unit.startswith("год") or unit == "лет":
|
elif unit.startswith("год") or unit == "лет":
|
||||||
|
if quantity >= 100:
|
||||||
|
continue
|
||||||
shift_months += quantity * 12
|
shift_months += quantity * 12
|
||||||
|
|
||||||
if shift_days == 0 and shift_months == 0:
|
if shift_days == 0 and shift_months == 0:
|
||||||
|
|
@ -875,7 +1029,22 @@ def infer_voice_task_relative_due_date(transcript, current_date, target_issue=No
|
||||||
|
|
||||||
base_date = current_date
|
base_date = current_date
|
||||||
source_date = getattr(target_issue, "target_date", None)
|
source_date = getattr(target_issue, "target_date", None)
|
||||||
if source_date and any(marker in normalized for marker in ["подвин", "сдвин", "смест", "отлож", "раньше", "позже"]):
|
has_existing_due_shift = any(
|
||||||
|
marker in normalized
|
||||||
|
for marker in [
|
||||||
|
"подвин",
|
||||||
|
"передвин",
|
||||||
|
"сдвин",
|
||||||
|
"смест",
|
||||||
|
"отлож",
|
||||||
|
"перенес",
|
||||||
|
"назад",
|
||||||
|
"вперед",
|
||||||
|
"раньше",
|
||||||
|
"позже",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if source_date and has_existing_due_shift:
|
||||||
base_date = source_date
|
base_date = source_date
|
||||||
|
|
||||||
result = add_months_to_date(base_date, shift_months * direction) if shift_months else base_date
|
result = add_months_to_date(base_date, shift_months * direction) if shift_months else base_date
|
||||||
|
|
@ -884,12 +1053,15 @@ def infer_voice_task_relative_due_date(transcript, current_date, target_issue=No
|
||||||
|
|
||||||
|
|
||||||
def hydrate_voice_task_due_date(draft, transcript, client_context, user, workspace, target_issue=None):
|
def hydrate_voice_task_due_date(draft, transcript, client_context, user, workspace, target_issue=None):
|
||||||
if draft.get("due_date"):
|
current_date = get_voice_task_current_date(client_context, user, workspace)
|
||||||
|
absolute_due_date = infer_voice_task_absolute_due_date(transcript, current_date=current_date)
|
||||||
|
if absolute_due_date:
|
||||||
|
draft["due_date"] = absolute_due_date
|
||||||
return
|
return
|
||||||
|
|
||||||
inferred_due_date = infer_voice_task_relative_due_date(
|
inferred_due_date = infer_voice_task_relative_due_date(
|
||||||
transcript=transcript,
|
transcript=transcript,
|
||||||
current_date=get_voice_task_current_date(client_context, user, workspace),
|
current_date=current_date,
|
||||||
target_issue=target_issue,
|
target_issue=target_issue,
|
||||||
)
|
)
|
||||||
if inferred_due_date:
|
if inferred_due_date:
|
||||||
|
|
@ -972,8 +1144,39 @@ def is_voice_task_issue_available(issue):
|
||||||
return bool(issue and not issue.deleted_at and not issue.archived_at)
|
return bool(issue and not issue.deleted_at and not issue.archived_at)
|
||||||
|
|
||||||
|
|
||||||
def resolve_voice_task_memory_target(workspace, user, draft, current_session=None):
|
def get_committed_voice_task_memory_sessions(workspace, user, current_session=None):
|
||||||
|
memory_sessions = (
|
||||||
|
VoiceTaskSession.objects.filter(
|
||||||
|
workspace=workspace,
|
||||||
|
user=user,
|
||||||
|
status=VoiceTaskSession.Status.PARSED,
|
||||||
|
)
|
||||||
|
.filter(Q(created_task__isnull=False) | Q(updated_task__isnull=False))
|
||||||
|
.select_related("created_task", "created_task__project", "updated_task", "updated_task__project")
|
||||||
|
.order_by("-updated_at", "-created_at")
|
||||||
|
)
|
||||||
|
if current_session:
|
||||||
|
memory_sessions = memory_sessions.exclude(id=current_session.id)
|
||||||
|
|
||||||
|
return list(memory_sessions[: VOICE_TASK_MEMORY_LIMIT * 3])
|
||||||
|
|
||||||
|
|
||||||
|
def find_latest_voice_task_issue(memory_sessions, project_id=None):
|
||||||
|
for memory_session in memory_sessions:
|
||||||
|
target_issue = get_voice_session_target_issue(memory_session)
|
||||||
|
if not is_voice_task_issue_available(target_issue):
|
||||||
|
continue
|
||||||
|
if project_id and str(target_issue.project_id) != str(project_id):
|
||||||
|
continue
|
||||||
|
return target_issue, memory_session
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_voice_task_memory_target(workspace, user, draft, current_session=None, client_context=None, transcript=None):
|
||||||
target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80)
|
target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80)
|
||||||
|
memory_sessions = get_committed_voice_task_memory_sessions(workspace, user, current_session=current_session)
|
||||||
|
generic_memory_reference = transcript_has_generic_memory_reference(transcript)
|
||||||
|
|
||||||
if target_memory_ref:
|
if target_memory_ref:
|
||||||
target_uuid = None
|
target_uuid = None
|
||||||
|
|
@ -989,7 +1192,7 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
target_issue = get_voice_session_target_issue(memory_session)
|
target_issue = get_voice_session_target_issue(memory_session)
|
||||||
if target_issue:
|
if is_voice_task_issue_available(target_issue) and not generic_memory_reference:
|
||||||
return target_issue, "target_memory_ref", memory_session
|
return target_issue, "target_memory_ref", memory_session
|
||||||
|
|
||||||
target_issue = (
|
target_issue = (
|
||||||
|
|
@ -997,7 +1200,7 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if target_issue:
|
if is_voice_task_issue_available(target_issue):
|
||||||
return target_issue, "target_issue_id", None
|
return target_issue, "target_issue_id", None
|
||||||
|
|
||||||
issue_key_reference = parse_issue_key_reference(target_memory_ref)
|
issue_key_reference = parse_issue_key_reference(target_memory_ref)
|
||||||
|
|
@ -1012,26 +1215,25 @@ def resolve_voice_task_memory_target(workspace, user, draft, current_session=Non
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if target_issue:
|
if is_voice_task_issue_available(target_issue):
|
||||||
return target_issue, "target_issue_key", None
|
return target_issue, "target_issue_key", None
|
||||||
|
|
||||||
memory_sessions = (
|
projects = list(get_accessible_projects(workspace, user).order_by("name"))
|
||||||
VoiceTaskSession.objects.filter(
|
source_project = infer_voice_task_source_project_from_transcript(projects, transcript)
|
||||||
workspace=workspace,
|
if source_project:
|
||||||
user=user,
|
target_issue, memory_session = find_latest_voice_task_issue(memory_sessions, source_project.id)
|
||||||
status=VoiceTaskSession.Status.PARSED,
|
if target_issue:
|
||||||
)
|
return target_issue, "latest_voice_task_source_project", memory_session
|
||||||
.filter(Q(created_task__isnull=False) | Q(updated_task__isnull=False))
|
|
||||||
.select_related("created_task", "created_task__project", "updated_task", "updated_task__project")
|
|
||||||
.order_by("-updated_at", "-created_at")
|
|
||||||
)
|
|
||||||
if current_session:
|
|
||||||
memory_sessions = memory_sessions.exclude(id=current_session.id)
|
|
||||||
|
|
||||||
for memory_session in memory_sessions[:VOICE_TASK_MEMORY_LIMIT * 3]:
|
current_project_id = normalize_string((client_context or {}).get("current_project_id"))
|
||||||
target_issue = get_voice_session_target_issue(memory_session)
|
if current_project_id:
|
||||||
if is_voice_task_issue_available(target_issue):
|
target_issue, memory_session = find_latest_voice_task_issue(memory_sessions, current_project_id)
|
||||||
return target_issue, "latest_voice_task", memory_session
|
if target_issue:
|
||||||
|
return target_issue, "latest_voice_task_current_project", memory_session
|
||||||
|
|
||||||
|
target_issue, memory_session = find_latest_voice_task_issue(memory_sessions)
|
||||||
|
if target_issue:
|
||||||
|
return target_issue, "latest_voice_task", memory_session
|
||||||
|
|
||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
||||||
|
|
@ -1066,6 +1268,8 @@ def build_voice_task_resolution(workspace, user, ai_settings, draft, client_cont
|
||||||
user=user,
|
user=user,
|
||||||
draft=draft,
|
draft=draft,
|
||||||
current_session=voice_session,
|
current_session=voice_session,
|
||||||
|
client_context=client_context,
|
||||||
|
transcript=transcript,
|
||||||
)
|
)
|
||||||
|
|
||||||
hydrate_voice_task_due_date(
|
hydrate_voice_task_due_date(
|
||||||
|
|
@ -1493,32 +1697,20 @@ def serialize_workspace_members(workspace):
|
||||||
|
|
||||||
|
|
||||||
def serialize_recent_voice_memory(workspace, user):
|
def serialize_recent_voice_memory(workspace, user):
|
||||||
sessions = (
|
sessions = get_committed_voice_task_memory_sessions(workspace, user)[:VOICE_TASK_MEMORY_LIMIT]
|
||||||
VoiceTaskSession.objects.filter(
|
|
||||||
workspace=workspace,
|
|
||||||
user=user,
|
|
||||||
status=VoiceTaskSession.Status.PARSED,
|
|
||||||
)
|
|
||||||
.exclude(parsed_json={})
|
|
||||||
.select_related("created_task", "created_task__project", "updated_task", "updated_task__project")
|
|
||||||
.order_by("-updated_at", "-created_at")[:VOICE_TASK_MEMORY_LIMIT]
|
|
||||||
)
|
|
||||||
|
|
||||||
memory = []
|
memory = []
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
target_issue = get_voice_session_target_issue(session)
|
target_issue = get_voice_session_target_issue(session)
|
||||||
target_task = (
|
if not is_voice_task_issue_available(target_issue):
|
||||||
serialize_voice_task_target(target_issue, "recent_voice_memory", session)
|
continue
|
||||||
if is_voice_task_issue_available(target_issue)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
memory.append(
|
memory.append(
|
||||||
{
|
{
|
||||||
"voice_session_id": str(session.id),
|
"voice_session_id": str(session.id),
|
||||||
"intent": session.intent,
|
"intent": session.intent,
|
||||||
"title": session.parsed_json.get("title"),
|
"title": session.parsed_json.get("title"),
|
||||||
"project_hint": session.parsed_json.get("project_hint"),
|
"project_hint": session.parsed_json.get("project_hint"),
|
||||||
"target_task": target_task,
|
"target_task": serialize_voice_task_target(target_issue, "recent_voice_memory", session),
|
||||||
"created_at": session.created_at.isoformat(),
|
"created_at": session.created_at.isoformat(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,9 @@ import { CalendarDays } from "lucide-react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { EUserPermissions } from "@plane/constants";
|
import { EUserPermissions } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { PriorityIcon, StateGroupIcon } from "@plane/propel/icons";
|
import { PriorityIcon, StateGroupIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type {
|
import type { IState, TExternalContourBoardDirection, TExternalContourRequest, TIssue } from "@plane/types";
|
||||||
IState,
|
|
||||||
TExternalContourBoardDirection,
|
|
||||||
TExternalContourRequest,
|
|
||||||
TIssue,
|
|
||||||
} from "@plane/types";
|
|
||||||
import { Avatar } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
||||||
import { DateDropdown } from "@/components/dropdowns/date";
|
import { DateDropdown } from "@/components/dropdowns/date";
|
||||||
|
|
@ -56,7 +51,7 @@ const buildSourceStateMap = (
|
||||||
state.id,
|
state.id,
|
||||||
{
|
{
|
||||||
id: state.id,
|
id: state.id,
|
||||||
color: state.color,
|
color: getStateGroupColor(state.group, state.color),
|
||||||
default: false,
|
default: false,
|
||||||
description: "",
|
description: "",
|
||||||
group: state.group,
|
group: state.group,
|
||||||
|
|
@ -69,7 +64,10 @@ const buildSourceStateMap = (
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolveRequestStatus = (issue: TExternalContourRequest["issue"], fallbackStatus: TExternalContourRequest["status"]) => {
|
const resolveRequestStatus = (
|
||||||
|
issue: TExternalContourRequest["issue"],
|
||||||
|
fallbackStatus: TExternalContourRequest["status"]
|
||||||
|
) => {
|
||||||
const stateGroup = issue.state_detail?.group;
|
const stateGroup = issue.state_detail?.group;
|
||||||
if (!stateGroup) return fallbackStatus;
|
if (!stateGroup) return fallbackStatus;
|
||||||
return stateGroup === "completed" || stateGroup === "cancelled" ? "closed" : "open";
|
return stateGroup === "completed" || stateGroup === "cancelled" ? "closed" : "open";
|
||||||
|
|
@ -83,23 +81,20 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
const { getUserDetails, workspace } = useMember();
|
const { getUserDetails, workspace } = useMember();
|
||||||
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
const { getStateById, getProjectStateIds } = useProjectState();
|
const { getStateById, getProjectStateIds } = useProjectState();
|
||||||
const {
|
const { fetchBoard, upsertBoardItems } = useProjectExternalContoursBoard();
|
||||||
fetchBoard,
|
const { fetchTargetOptions, getTargetOptionsByProjectId, updateRequest, updateRequestIssue } =
|
||||||
upsertBoardItems,
|
useProjectExternalContours();
|
||||||
} = useProjectExternalContoursBoard();
|
|
||||||
const {
|
|
||||||
fetchTargetOptions,
|
|
||||||
getTargetOptionsByProjectId,
|
|
||||||
updateRequest,
|
|
||||||
updateRequestIssue,
|
|
||||||
} = useProjectExternalContours();
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [isSourceOptionsLoading, setIsSourceOptionsLoading] = useState(false);
|
const [isSourceOptionsLoading, setIsSourceOptionsLoading] = useState(false);
|
||||||
|
|
||||||
const issue = request.issue;
|
const issue = request.issue;
|
||||||
const selectedInboxIssueId = searchParams.get("inboxIssueId");
|
const selectedInboxIssueId = searchParams.get("inboxIssueId");
|
||||||
const isActive = selectedInboxIssueId === request.id;
|
const isActive = selectedInboxIssueId === request.id;
|
||||||
const requester = request.requested_by?.display_name || request.requested_by_name || issue.created_by_detail?.display_name || "NODE.DC";
|
const requester =
|
||||||
|
request.requested_by?.display_name ||
|
||||||
|
request.requested_by_name ||
|
||||||
|
issue.created_by_detail?.display_name ||
|
||||||
|
"NODE.DC";
|
||||||
const requesterAvatar = issue.created_by_detail?.avatar_url || "";
|
const requesterAvatar = issue.created_by_detail?.avatar_url || "";
|
||||||
const counterpartContourName =
|
const counterpartContourName =
|
||||||
direction === "outgoing"
|
direction === "outgoing"
|
||||||
|
|
@ -110,8 +105,12 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
? getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId)
|
? getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const canEditTargetIssue =
|
const canEditTargetIssue =
|
||||||
direction === "incoming" && !!targetProjectId && projectRole !== undefined && projectRole !== EUserPermissions.GUEST;
|
direction === "incoming" &&
|
||||||
const canEditSourceRequest = direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId;
|
!!targetProjectId &&
|
||||||
|
projectRole !== undefined &&
|
||||||
|
projectRole !== EUserPermissions.GUEST;
|
||||||
|
const canEditSourceRequest =
|
||||||
|
direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId;
|
||||||
const canEditCard = canEditTargetIssue || canEditSourceRequest;
|
const canEditCard = canEditTargetIssue || canEditSourceRequest;
|
||||||
const requestLink = `/${workspaceSlug}/projects/${projectId}/external-contours?inboxIssueId=${request.id}`;
|
const requestLink = `/${workspaceSlug}/projects/${projectId}/external-contours?inboxIssueId=${request.id}`;
|
||||||
const targetOptions = getTargetOptionsByProjectId(targetProjectId);
|
const targetOptions = getTargetOptionsByProjectId(targetProjectId);
|
||||||
|
|
@ -124,12 +123,11 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||||
const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white";
|
const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white";
|
||||||
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
|
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
|
||||||
const pillBackgroundClasses =
|
const pillBackgroundClasses = isActive
|
||||||
isActive
|
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
||||||
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
|
||||||
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
|
|
||||||
const iconBubbleClasses = isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white";
|
const iconBubbleClasses = isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white";
|
||||||
const statusIconColor = selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)");
|
const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
|
||||||
if (!issue) return null;
|
if (!issue) return null;
|
||||||
|
|
@ -314,13 +312,13 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("truncate -mt-0.5 pl-8 text-[11px] font-medium leading-4", subtleTextClasses)}>
|
<div className={cn("-mt-0.5 truncate pl-8 text-[11px] leading-4 font-medium", subtleTextClasses)}>
|
||||||
{counterpartContourName || t("common.none")}
|
{counterpartContourName || t("common.none")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center">
|
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center">
|
||||||
<div className="line-clamp-4 max-w-full text-lg font-semibold leading-6">{issue.name}</div>
|
<div className="text-lg line-clamp-4 max-w-full leading-6 font-semibold">{issue.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
||||||
|
|
@ -333,7 +331,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
disabled={!canEditCard || isUpdating}
|
disabled={!canEditCard || isUpdating}
|
||||||
buttonVariant="transparent-without-text"
|
buttonVariant="transparent-without-text"
|
||||||
button={
|
button={
|
||||||
<div className={cn(basePillClasses, pillBackgroundClasses, "pl-1 pr-2")}>
|
<div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -351,7 +349,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
}}
|
}}
|
||||||
buttonVariant="transparent-without-text"
|
buttonVariant="transparent-without-text"
|
||||||
button={
|
button={
|
||||||
<div className={cn(basePillClasses, pillBackgroundClasses, "pl-1 pr-2")}>
|
<div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
StateGroupIcon,
|
StateGroupIcon,
|
||||||
StatePropertyIcon,
|
StatePropertyIcon,
|
||||||
UserCirclePropertyIcon,
|
UserCirclePropertyIcon,
|
||||||
|
getStateGroupColor,
|
||||||
} from "@plane/propel/icons";
|
} from "@plane/propel/icons";
|
||||||
import type {
|
import type {
|
||||||
IProject,
|
IProject,
|
||||||
|
|
@ -57,14 +58,11 @@ const buildCounterpartyProjects = (requests: TExternalContourRequest[], projectI
|
||||||
|
|
||||||
if (!project?.id || project.id === projectId || projectMap.has(project.id)) return;
|
if (!project?.id || project.id === projectId || projectMap.has(project.id)) return;
|
||||||
|
|
||||||
projectMap.set(
|
projectMap.set(project.id, {
|
||||||
project.id,
|
id: project.id,
|
||||||
{
|
name: project.name,
|
||||||
id: project.id,
|
logo_props: project.logo_props,
|
||||||
name: project.name,
|
} as IProject);
|
||||||
logo_props: project.logo_props,
|
|
||||||
} as IProject
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return sortByName(Array.from(projectMap.values()));
|
return sortByName(Array.from(projectMap.values()));
|
||||||
|
|
@ -77,21 +75,18 @@ const buildStates = (requests: TExternalContourRequest[]): IState[] => {
|
||||||
const state = request.issue.state_detail;
|
const state = request.issue.state_detail;
|
||||||
if (!state?.id || stateMap.has(state.id)) return;
|
if (!state?.id || stateMap.has(state.id)) return;
|
||||||
|
|
||||||
stateMap.set(
|
stateMap.set(state.id, {
|
||||||
state.id,
|
id: state.id,
|
||||||
{
|
color: getStateGroupColor(state.group, state.color),
|
||||||
id: state.id,
|
default: false,
|
||||||
color: state.color,
|
description: "",
|
||||||
default: false,
|
group: state.group,
|
||||||
description: "",
|
name: state.name,
|
||||||
group: state.group,
|
order: index + 1,
|
||||||
name: state.name,
|
project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
|
||||||
order: index + 1,
|
sequence: index + 1,
|
||||||
project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
|
workspace_id: "",
|
||||||
sequence: index + 1,
|
} as IState);
|
||||||
workspace_id: "",
|
|
||||||
} as IState
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return sortByName(Array.from(stateMap.values()));
|
return sortByName(Array.from(stateMap.values()));
|
||||||
|
|
@ -103,18 +98,15 @@ const buildLabels = (requests: TExternalContourRequest[]): IIssueLabel[] => {
|
||||||
requests.forEach((request) => {
|
requests.forEach((request) => {
|
||||||
request.issue.label_details?.forEach((label) => {
|
request.issue.label_details?.forEach((label) => {
|
||||||
if (!label.id || labelMap.has(label.id)) return;
|
if (!label.id || labelMap.has(label.id)) return;
|
||||||
labelMap.set(
|
labelMap.set(label.id, {
|
||||||
label.id,
|
id: label.id,
|
||||||
{
|
color: label.color,
|
||||||
id: label.id,
|
name: label.name,
|
||||||
color: label.color,
|
parent: null,
|
||||||
name: label.name,
|
project_id: request.issue.project_id || "",
|
||||||
parent: null,
|
sort_order: 0,
|
||||||
project_id: request.issue.project_id || "",
|
workspace_id: "",
|
||||||
sort_order: 0,
|
} as IIssueLabel);
|
||||||
workspace_id: "",
|
|
||||||
} as IIssueLabel
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -127,14 +119,11 @@ const buildAssignees = (requests: TExternalContourRequest[]): IUserLite[] => {
|
||||||
requests.forEach((request) => {
|
requests.forEach((request) => {
|
||||||
request.issue.assignee_details?.forEach((assignee) => {
|
request.issue.assignee_details?.forEach((assignee) => {
|
||||||
if (!assignee.id || memberMap.has(assignee.id)) return;
|
if (!assignee.id || memberMap.has(assignee.id)) return;
|
||||||
memberMap.set(
|
memberMap.set(assignee.id, {
|
||||||
assignee.id,
|
id: assignee.id,
|
||||||
{
|
avatar_url: assignee.avatar_url,
|
||||||
id: assignee.id,
|
display_name: assignee.display_name,
|
||||||
avatar_url: assignee.avatar_url,
|
} as IUserLite);
|
||||||
display_name: assignee.display_name,
|
|
||||||
} as IUserLite
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -151,14 +140,11 @@ const buildRequesters = (requests: TExternalContourRequest[]): IUserLite[] => {
|
||||||
|
|
||||||
if (!requesterId || !requesterName || memberMap.has(requesterId)) return;
|
if (!requesterId || !requesterName || memberMap.has(requesterId)) return;
|
||||||
|
|
||||||
memberMap.set(
|
memberMap.set(requesterId, {
|
||||||
requesterId,
|
id: requesterId,
|
||||||
{
|
avatar_url: request.issue.created_by_detail?.avatar_url,
|
||||||
id: requesterId,
|
display_name: requesterName,
|
||||||
avatar_url: request.issue.created_by_detail?.avatar_url,
|
} as IUserLite);
|
||||||
display_name: requesterName,
|
|
||||||
} as IUserLite
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return sortByName(Array.from(memberMap.values()));
|
return sortByName(Array.from(memberMap.values()));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { getStateGroupColor } from "@plane/propel/icons";
|
||||||
import type { ISearchIssueResponse } from "@plane/types";
|
import type { ISearchIssueResponse } from "@plane/types";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
||||||
|
|
@ -19,7 +20,7 @@ interface Props {
|
||||||
export const BulkDeleteIssuesModalItem = observer(function BulkDeleteIssuesModalItem(props: Props) {
|
export const BulkDeleteIssuesModalItem = observer(function BulkDeleteIssuesModalItem(props: Props) {
|
||||||
const { issue, canDeleteIssueIds } = props;
|
const { issue, canDeleteIssueIds } = props;
|
||||||
|
|
||||||
const color = issue.state__color;
|
const color = getStateGroupColor(issue.state__group, issue.state__color);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Combobox } from "@headlessui/react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// types
|
// types
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { SearchIcon, CloseIcon } from "@plane/propel/icons";
|
import { SearchIcon, CloseIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
||||||
|
|
@ -263,7 +263,7 @@ export function ExistingIssuesListModal(props: Props) {
|
||||||
<span
|
<span
|
||||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: issue.state__color,
|
backgroundColor: getStateGroupColor(issue.state__group, issue.state__color),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export const BlockRow = observer(function BlockRow(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative w-max min-w-full"
|
className="nodedc-project-gantt-row relative w-max min-w-full"
|
||||||
onMouseEnter={() => updateActiveBlockId(blockId)}
|
onMouseEnter={() => updateActiveBlockId(blockId)}
|
||||||
onMouseLeave={() => updateActiveBlockId(null)}
|
onMouseLeave={() => updateActiveBlockId(null)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -89,19 +89,18 @@ export const BlockRow = observer(function BlockRow(props: Props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn("relative h-full bg-layer-transparent hover:bg-layer-transparent-hover", {
|
className={cn("nodedc-project-gantt-row-bg relative h-full", {
|
||||||
"rounded-l-sm border border-r-0 border-accent-strong": getIsIssuePeeked(block.data.id),
|
"nodedc-project-gantt-row-peeked": getIsIssuePeeked(block.data.id),
|
||||||
"bg-layer-transparent-hover": isBlockHoveredOn,
|
"nodedc-project-gantt-row-hovered": isBlockHoveredOn,
|
||||||
"bg-accent-primary/5 hover:bg-accent-primary/10": isBlockSelected,
|
"nodedc-project-gantt-row-selected": isBlockSelected,
|
||||||
"bg-accent-primary/10": isBlockSelected && isBlockHoveredOn,
|
"nodedc-project-gantt-row-focused": isBlockFocused,
|
||||||
"border border-r-0 border-strong-1": isBlockFocused,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isBlockVisibleOnChart
|
{isBlockVisibleOnChart
|
||||||
? isHidden && (
|
? isHidden && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="sticky z-[5] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center rounded-sm border border-strong bg-layer-1 text-secondary hover:text-primary"
|
className="nodedc-project-gantt-jump-button sticky z-[5] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center text-secondary hover:text-primary"
|
||||||
style={{
|
style={{
|
||||||
left: `${SIDEBAR_WIDTH + 4}px`,
|
left: `${SIDEBAR_WIDTH + 4}px`,
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export const GanttChartBlock = observer(function GanttChartBlock(props: Props) {
|
||||||
horizontalOffset={100}
|
horizontalOffset={100}
|
||||||
verticalOffset={200}
|
verticalOffset={200}
|
||||||
classNames="flex h-full w-full items-center"
|
classNames="flex h-full w-full items-center"
|
||||||
placeholderChildren={<div className="h-8 w-full rounded-sm bg-layer-1" />}
|
placeholderChildren={<div className="nodedc-project-gantt-block-placeholder h-8 w-full" />}
|
||||||
shouldRecordHeights={false}
|
shouldRecordHeights={false}
|
||||||
forceRender={isCurrentDependencyDragging}
|
forceRender={isCurrentDependencyDragging}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Expand, Shrink } from "lucide-react";
|
import { CalendarDays, Expand, Shrink } from "lucide-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// plane
|
// plane
|
||||||
import type { TGanttViews } from "@plane/types";
|
import type { TGanttViews } from "@plane/types";
|
||||||
|
|
@ -25,62 +25,81 @@ type Props = {
|
||||||
handleChartView: (view: TGanttViews) => void;
|
handleChartView: (view: TGanttViews) => void;
|
||||||
handleToday: () => void;
|
handleToday: () => void;
|
||||||
loaderTitle: string;
|
loaderTitle: string;
|
||||||
|
title: string;
|
||||||
toggleFullScreenMode: () => void;
|
toggleFullScreenMode: () => void;
|
||||||
showToday: boolean;
|
showToday: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GANTT_VIEW_SHORT_LABELS: Record<TGanttViews, string> = {
|
||||||
|
week: "1W",
|
||||||
|
month: "1M",
|
||||||
|
quarter: "3M",
|
||||||
|
};
|
||||||
|
|
||||||
export const GanttChartHeader = observer(function GanttChartHeader(props: Props) {
|
export const GanttChartHeader = observer(function GanttChartHeader(props: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } =
|
const {
|
||||||
props;
|
blockIds,
|
||||||
|
fullScreenMode,
|
||||||
|
handleChartView,
|
||||||
|
handleToday,
|
||||||
|
loaderTitle,
|
||||||
|
title,
|
||||||
|
toggleFullScreenMode,
|
||||||
|
showToday,
|
||||||
|
} = props;
|
||||||
// chart hook
|
// chart hook
|
||||||
const { currentView } = useTimeLineChartStore();
|
const { currentView } = useTimeLineChartStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 bg-surface-1 py-2 whitespace-nowrap"
|
className="nodedc-project-gantt-toolbar relative flex w-full flex-shrink-0 flex-wrap items-center justify-between gap-3 whitespace-nowrap"
|
||||||
style={{ height: `${GANTT_BREADCRUMBS_HEIGHT}px` }}
|
style={{ height: `${GANTT_BREADCRUMBS_HEIGHT}px` }}
|
||||||
>
|
>
|
||||||
<div className="ml-auto">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<div className="ml-auto text-11 font-medium text-tertiary">
|
<div className="nodedc-project-gantt-toolbar-icon">
|
||||||
{blockIds ? `${blockIds.length} ${loaderTitle}` : t("common.loading")}
|
<CalendarDays className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-14 font-semibold text-primary">{title}</div>
|
||||||
|
<div className="mt-0.5 truncate text-11 font-medium text-tertiary">
|
||||||
|
{currentView ? GANTT_VIEW_SHORT_LABELS[currentView] : null}
|
||||||
|
{currentView ? " / " : null}
|
||||||
|
{blockIds ? `${blockIds.length} ${loaderTitle}` : t("common.loading")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
{showToday && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="nodedc-project-gantt-chip nodedc-project-gantt-chip-live"
|
||||||
|
onClick={handleToday}
|
||||||
|
>
|
||||||
|
Live
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{VIEWS_LIST.map((chartView: any) => (
|
{VIEWS_LIST.map((chartView: any) => (
|
||||||
<div
|
<button
|
||||||
key={chartView?.key}
|
key={chartView?.key}
|
||||||
className={cn(
|
type="button"
|
||||||
"cursor-pointer rounded-md bg-layer-transparent p-1 px-2 text-11 hover:bg-layer-transparent-hover",
|
aria-label={t(chartView?.i18n_title)}
|
||||||
{
|
aria-pressed={currentView === chartView?.key}
|
||||||
"bg-layer-transparent-selected": currentView === chartView?.key,
|
className={cn("nodedc-project-gantt-chip", {
|
||||||
}
|
"nodedc-project-gantt-chip-active": currentView === chartView?.key,
|
||||||
)}
|
})}
|
||||||
onClick={() => handleChartView(chartView?.key)}
|
onClick={() => handleChartView(chartView?.key)}
|
||||||
>
|
>
|
||||||
{t(chartView?.i18n_title)}
|
{GANTT_VIEW_SHORT_LABELS[chartView?.key as TGanttViews]}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
|
|
||||||
{showToday && (
|
<button type="button" className="nodedc-project-gantt-round-button" onClick={toggleFullScreenMode}>
|
||||||
<button
|
{fullScreenMode ? <Shrink className="size-4" /> : <Expand className="size-4" />}
|
||||||
type="button"
|
|
||||||
className="rounded-md bg-layer-transparent p-1 px-2 text-11 hover:bg-layer-transparent-hover"
|
|
||||||
onClick={handleToday}
|
|
||||||
>
|
|
||||||
{t("common.today")}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center justify-center rounded-md border border-subtle bg-layer-transparent p-1 transition-all hover:bg-layer-transparent-hover"
|
|
||||||
onClick={toggleFullScreenMode}
|
|
||||||
>
|
|
||||||
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
|
|
||||||
</button>
|
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ export const GanttChartMainContent = observer(function GanttChartMainContent(pro
|
||||||
// DO NOT REMOVE THE ID
|
// DO NOT REMOVE THE ID
|
||||||
id="gantt-container"
|
id="gantt-container"
|
||||||
className={cn(
|
className={cn(
|
||||||
"vertical-scrollbar horizontal-scrollbar flex scrollbar-lg h-full w-full overflow-auto border-t-[0.5px] border-subtle",
|
"nodedc-project-gantt-scroll vertical-scrollbar horizontal-scrollbar flex scrollbar-lg h-full w-full overflow-auto",
|
||||||
{
|
{
|
||||||
"mb-8": bottomSpacing,
|
"mb-8": bottomSpacing,
|
||||||
}
|
}
|
||||||
|
|
@ -199,11 +199,11 @@ export const GanttChartMainContent = observer(function GanttChartMainContent(pro
|
||||||
showAllBlocks={showAllBlocks}
|
showAllBlocks={showAllBlocks}
|
||||||
isEpic={isEpic}
|
isEpic={isEpic}
|
||||||
/>
|
/>
|
||||||
<div className="relative h-max min-h-full flex-shrink-0 flex-grow">
|
<div className="nodedc-project-gantt-stage relative h-max min-h-full flex-shrink-0 flex-grow">
|
||||||
<ActiveChartView />
|
<ActiveChartView />
|
||||||
{currentViewData && (
|
{currentViewData && (
|
||||||
<div
|
<div
|
||||||
className="relative h-full"
|
className="nodedc-project-gantt-layer relative h-full"
|
||||||
style={{
|
style={{
|
||||||
width: `${itemsContainerWidth}px`,
|
width: `${itemsContainerWidth}px`,
|
||||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||||
|
|
|
||||||
|
|
@ -180,8 +180,8 @@ export const ChartViewRoot = observer(function ChartViewRoot(props: ChartViewRoo
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
className={cn("shadow relative flex h-full flex-col rounded-xs bg-surface-1 select-none", {
|
className={cn("nodedc-project-gantt-card shadow relative flex h-full flex-col select-none", {
|
||||||
"inset-0 z-[25] bg-surface-1": fullScreenMode,
|
"fixed inset-0 z-[25] rounded-none": fullScreenMode,
|
||||||
"border-[0.5px] border-subtle": border,
|
"border-[0.5px] border-subtle": border,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
@ -192,6 +192,7 @@ export const ChartViewRoot = observer(function ChartViewRoot(props: ChartViewRoo
|
||||||
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||||
handleToday={handleToday}
|
handleToday={handleToday}
|
||||||
loaderTitle={loaderTitle}
|
loaderTitle={loaderTitle}
|
||||||
|
title={title}
|
||||||
showToday={showToday}
|
showToday={showToday}
|
||||||
/>
|
/>
|
||||||
<GanttChartMainContent
|
<GanttChartMainContent
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,12 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
||||||
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
|
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 left-0 flex h-max min-h-full w-max">
|
<div className="nodedc-project-gantt-calendar absolute top-0 left-0 flex h-max min-h-full w-max">
|
||||||
{currentViewData && (
|
{currentViewData && (
|
||||||
<div className="relative flex flex-col outline-[0.25px] outline-subtle-1">
|
<div className="nodedc-project-gantt-calendar-group relative flex flex-col">
|
||||||
{/** Header Div */}
|
{/** Header Div */}
|
||||||
<div
|
<div
|
||||||
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1"
|
className="nodedc-project-gantt-calendar-header sticky top-0 z-[5] w-full flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
height: `${HEADER_HEIGHT}px`,
|
height: `${HEADER_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
|
|
@ -45,18 +45,22 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
||||||
{months?.map((monthBlock) => (
|
{months?.map((monthBlock) => (
|
||||||
<div
|
<div
|
||||||
key={`month-${monthBlock?.month}-${monthBlock?.year}`}
|
key={`month-${monthBlock?.month}-${monthBlock?.year}`}
|
||||||
className="flex outline-[0.5px] outline-subtle-1"
|
className="flex"
|
||||||
style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }}
|
style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="sticky z-[1] m-1 flex items-center bg-surface-1 px-3 py-1 text-14 font-regular whitespace-nowrap text-secondary capitalize"
|
className="nodedc-project-gantt-period-label sticky z-[1] m-1 flex items-center px-3 py-1 text-12 font-semibold whitespace-nowrap text-secondary capitalize"
|
||||||
style={{
|
style={{
|
||||||
left: `${SIDEBAR_WIDTH}px`,
|
left: `${SIDEBAR_WIDTH}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{monthBlock?.title}
|
{monthBlock?.title}
|
||||||
{monthBlock.today && (
|
{monthBlock.today && (
|
||||||
<span className={cn("ml-2 rounded-sm bg-accent-primary px-1 text-9 font-medium text-on-color")}>
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-2 rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-2 py-0.5 text-9 font-semibold text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
Current
|
Current
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -70,9 +74,9 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
||||||
<div
|
<div
|
||||||
key={`sub-title-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
key={`sub-title-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-shrink-0 justify-between px-2 py-1 text-center capitalize outline-[0.25px] outline-subtle-1",
|
"nodedc-project-gantt-subcell flex flex-shrink-0 justify-between px-2 py-1 text-center capitalize",
|
||||||
{
|
{
|
||||||
"bg-accent-primary/20": weekBlock.today,
|
"nodedc-project-gantt-subcell-today": weekBlock.today,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||||
|
|
@ -80,7 +84,8 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
||||||
<div className="space-x-1 text-11 font-medium text-placeholder">
|
<div className="space-x-1 text-11 font-medium text-placeholder">
|
||||||
<span
|
<span
|
||||||
className={cn({
|
className={cn({
|
||||||
"rounded-sm bg-accent-primary px-1 text-on-color": weekBlock.today,
|
"rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-1.5 text-[rgb(var(--nodedc-on-card-active-rgb))]":
|
||||||
|
weekBlock.today,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{weekBlock.startDate.getDate()}-{weekBlock.endDate.getDate()}
|
{weekBlock.startDate.getDate()}-{weekBlock.endDate.getDate()}
|
||||||
|
|
@ -96,8 +101,8 @@ export const MonthChartView = observer(function MonthChartView(_props: any) {
|
||||||
{weeks?.map((weekBlock) => (
|
{weeks?.map((weekBlock) => (
|
||||||
<div
|
<div
|
||||||
key={`column-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
key={`column-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
||||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
|
||||||
"bg-accent-primary/20": weekBlock.today,
|
"nodedc-project-gantt-column-today": weekBlock.today,
|
||||||
})}
|
})}
|
||||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,16 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
||||||
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`absolute top-0 left-0 flex h-max min-h-full w-max`}>
|
<div className="nodedc-project-gantt-calendar absolute top-0 left-0 flex h-max min-h-full w-max">
|
||||||
{currentViewData &&
|
{currentViewData &&
|
||||||
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
||||||
<div
|
<div
|
||||||
key={`month-${quarterBlock.quarterNumber}-${quarterBlock.year}`}
|
key={`month-${quarterBlock.quarterNumber}-${quarterBlock.year}`}
|
||||||
className="relative flex flex-col outline-[0.25px] outline-subtle-1"
|
className="nodedc-project-gantt-calendar-group relative flex flex-col"
|
||||||
>
|
>
|
||||||
{/** Header Div */}
|
{/** Header Div */}
|
||||||
<div
|
<div
|
||||||
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1 outline-[1px] outline-subtle-1"
|
className="nodedc-project-gantt-calendar-header sticky top-0 z-[5] w-full flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
height: `${HEADER_HEIGHT}px`,
|
height: `${HEADER_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
|
|
@ -38,19 +38,23 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
||||||
{/** Main Quarter Title */}
|
{/** Main Quarter Title */}
|
||||||
<div className="inline-flex h-7 w-full justify-between">
|
<div className="inline-flex h-7 w-full justify-between">
|
||||||
<div
|
<div
|
||||||
className="sticky z-[1] my-1 flex items-center bg-surface-1 px-3 py-1 text-14 font-regular whitespace-nowrap text-secondary capitalize"
|
className="nodedc-project-gantt-period-label sticky z-[1] my-1 flex items-center px-3 py-1 text-12 font-semibold whitespace-nowrap text-secondary capitalize"
|
||||||
style={{
|
style={{
|
||||||
left: `${SIDEBAR_WIDTH}px`,
|
left: `${SIDEBAR_WIDTH}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{quarterBlock?.title}
|
{quarterBlock?.title}
|
||||||
{quarterBlock.today && (
|
{quarterBlock.today && (
|
||||||
<span className={cn("ml-2 rounded-sm bg-accent-primary px-1 text-9 font-medium text-on-color")}>
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-2 rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-2 py-0.5 text-9 font-semibold text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
Current
|
Current
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
|
<div className="nodedc-project-gantt-period-meta sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
|
||||||
{quarterBlock.shortTitle}
|
{quarterBlock.shortTitle}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -60,9 +64,9 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
||||||
<div
|
<div
|
||||||
key={`sub-title-${rootIndex}-${index}`}
|
key={`sub-title-${rootIndex}-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-shrink-0 justify-center text-center capitalize outline-[0.25px] outline-subtle-1",
|
"nodedc-project-gantt-subcell flex flex-shrink-0 justify-center text-center capitalize",
|
||||||
{
|
{
|
||||||
"bg-accent-primary/20": monthBlock.today,
|
"nodedc-project-gantt-subcell-today": monthBlock.today,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||||
|
|
@ -70,7 +74,8 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
||||||
<div className="flex h-full items-center justify-center space-x-1 text-11 font-medium">
|
<div className="flex h-full items-center justify-center space-x-1 text-11 font-medium">
|
||||||
<span
|
<span
|
||||||
className={cn({
|
className={cn({
|
||||||
"rounded-lg bg-accent-primary px-2 text-on-color": monthBlock.today,
|
"rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-2 text-[rgb(var(--nodedc-on-card-active-rgb))]":
|
||||||
|
monthBlock.today,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{monthBlock.monthData.shortTitle}
|
{monthBlock.monthData.shortTitle}
|
||||||
|
|
@ -85,8 +90,8 @@ export const QuarterChartView = observer(function QuarterChartView(_props: any)
|
||||||
{quarterBlock?.children?.map((monthBlock, index) => (
|
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||||
<div
|
<div
|
||||||
key={`column-${rootIndex}-${index}`}
|
key={`column-${rootIndex}-${index}`}
|
||||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
|
||||||
"bg-accent-primary/20": monthBlock.today,
|
"nodedc-project-gantt-column-today": monthBlock.today,
|
||||||
})}
|
})}
|
||||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,16 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
||||||
const weekBlocks: IWeekBlock[] = renderView;
|
const weekBlocks: IWeekBlock[] = renderView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`absolute top-0 left-0 flex h-max min-h-full w-max`}>
|
<div className="nodedc-project-gantt-calendar absolute top-0 left-0 flex h-max min-h-full w-max">
|
||||||
{currentViewData &&
|
{currentViewData &&
|
||||||
weekBlocks?.map((block, rootIndex) => (
|
weekBlocks?.map((block, rootIndex) => (
|
||||||
<div
|
<div
|
||||||
key={`month-${block?.startDate.toString()}-${block?.endDate.toString()}`}
|
key={`month-${block?.startDate.toString()}-${block?.endDate.toString()}`}
|
||||||
className="relative flex flex-col outline-[0.25px] outline-subtle-1"
|
className="nodedc-project-gantt-calendar-group relative flex flex-col"
|
||||||
>
|
>
|
||||||
{/** Header Div */}
|
{/** Header Div */}
|
||||||
<div
|
<div
|
||||||
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1 outline-[1px] outline-subtle-1"
|
className="nodedc-project-gantt-calendar-header sticky top-0 z-[5] w-full flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
height: `${HEADER_HEIGHT}px`,
|
height: `${HEADER_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
|
|
@ -35,14 +35,14 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
||||||
{/** Main Months Title */}
|
{/** Main Months Title */}
|
||||||
<div className="inline-flex h-7 w-full justify-between">
|
<div className="inline-flex h-7 w-full justify-between">
|
||||||
<div
|
<div
|
||||||
className="sticky z-[1] m-1 flex items-center bg-surface-1 px-3 py-1 text-13 font-regular whitespace-nowrap text-secondary capitalize"
|
className="nodedc-project-gantt-period-label sticky z-[1] m-1 flex items-center px-3 py-1 text-12 font-semibold whitespace-nowrap text-secondary capitalize"
|
||||||
style={{
|
style={{
|
||||||
left: `${SIDEBAR_WIDTH}px`,
|
left: `${SIDEBAR_WIDTH}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{block?.title}
|
{block?.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
|
<div className="nodedc-project-gantt-period-meta sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
|
||||||
{block?.weekData?.title}
|
{block?.weekData?.title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -52,9 +52,9 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
||||||
<div
|
<div
|
||||||
key={`sub-title-${rootIndex}-${index}`}
|
key={`sub-title-${rootIndex}-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-shrink-0 justify-between p-1 text-center capitalize outline-[0.25px] outline-subtle-1",
|
"nodedc-project-gantt-subcell flex flex-shrink-0 justify-between p-1 text-center capitalize",
|
||||||
{
|
{
|
||||||
"bg-accent-primary/20": weekDay.today,
|
"nodedc-project-gantt-subcell-today": weekDay.today,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||||
|
|
@ -63,7 +63,8 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
||||||
<div className="space-x-1 text-11 font-medium">
|
<div className="space-x-1 text-11 font-medium">
|
||||||
<span
|
<span
|
||||||
className={cn({
|
className={cn({
|
||||||
"rounded-sm bg-accent-primary px-1 text-on-color": weekDay.today,
|
"rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] px-1.5 text-[rgb(var(--nodedc-on-card-active-rgb))]":
|
||||||
|
weekDay.today,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{weekDay.date.getDate()}
|
{weekDay.date.getDate()}
|
||||||
|
|
@ -74,17 +75,17 @@ export const WeekChartView = observer(function WeekChartView(_props: any) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/** Day Columns */}
|
{/** Day Columns */}
|
||||||
<div className="flex h-full w-full flex-grow bg-surface-1">
|
<div className="flex h-full w-full flex-grow">
|
||||||
{block?.children?.map((weekDay, index) => (
|
{block?.children?.map((weekDay, index) => (
|
||||||
<div
|
<div
|
||||||
key={`column-${rootIndex}-${index}`}
|
key={`column-${rootIndex}-${index}`}
|
||||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
className={cn("nodedc-project-gantt-column h-full overflow-hidden", {
|
||||||
"bg-accent-primary/20": weekDay.today,
|
"nodedc-project-gantt-column-today": weekDay.today,
|
||||||
})}
|
})}
|
||||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||||
>
|
>
|
||||||
{["sat", "sun"].includes(weekDay?.dayData?.shortTitle) && (
|
{["sat", "sun"].includes(weekDay?.dayData?.shortTitle) && (
|
||||||
<div className="h-full bg-surface-2 outline-[0.25px] outline-strong" />
|
<div className="nodedc-project-gantt-column-weekend h-full" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const BLOCK_HEIGHT = 44;
|
export const BLOCK_HEIGHT = 46;
|
||||||
|
|
||||||
export const HEADER_HEIGHT = 48;
|
export const HEADER_HEIGHT = 56;
|
||||||
|
|
||||||
export const GANTT_BREADCRUMBS_HEIGHT = 40;
|
export const GANTT_BREADCRUMBS_HEIGHT = 60;
|
||||||
|
|
||||||
export const SIDEBAR_WIDTH = 360;
|
export const SIDEBAR_WIDTH = 360;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const LeftResizable = observer(function LeftResizable(props: LeftResizabl
|
||||||
<>
|
<>
|
||||||
{(isHovering || isLeftResizing) && dateString && (
|
{(isHovering || isLeftResizing) && dateString && (
|
||||||
<div className="absolute -left-36 flex h-full w-32 items-center justify-end text-11 font-regular text-tertiary">
|
<div className="absolute -left-36 flex h-full w-32 items-center justify-end text-11 font-regular text-tertiary">
|
||||||
<div className="rounded-sm bg-accent-subtle px-2 py-1">{dateString}</div>
|
<div className="nodedc-project-gantt-resize-tooltip px-2 py-1">{dateString}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|
@ -56,7 +56,7 @@ export const LeftResizable = observer(function LeftResizable(props: LeftResizabl
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-1/2 left-1 z-[5] h-7 w-1 -translate-y-1/2 rounded-xs bg-surface-1 opacity-0 transition-all duration-300 group-hover:opacity-100",
|
"nodedc-project-gantt-resize-handle absolute top-1/2 left-1 z-[5] h-7 w-1 -translate-y-1/2 opacity-0 transition-all duration-300 group-hover:opacity-100",
|
||||||
{
|
{
|
||||||
"-left-1.5 opacity-100": isLeftResizing,
|
"-left-1.5 opacity-100": isLeftResizing,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const RightResizable = observer(function RightResizable(props: RightResiz
|
||||||
<>
|
<>
|
||||||
{(isHovering || isRightResizing) && dateString && (
|
{(isHovering || isRightResizing) && dateString && (
|
||||||
<div className="absolute -right-36 z-[10] flex h-full w-32 items-center justify-start text-11 font-regular text-tertiary">
|
<div className="absolute -right-36 z-[10] flex h-full w-32 items-center justify-start text-11 font-regular text-tertiary">
|
||||||
<div className="rounded-sm bg-accent-subtle px-2 py-1">{dateString}</div>
|
<div className="nodedc-project-gantt-resize-tooltip px-2 py-1">{dateString}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|
@ -54,7 +54,7 @@ export const RightResizable = observer(function RightResizable(props: RightResiz
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-1/2 right-1 z-[5] h-7 w-1 -translate-y-1/2 rounded-xs bg-surface-1 opacity-0 transition-all duration-300 group-hover:opacity-100",
|
"nodedc-project-gantt-resize-handle absolute top-1/2 right-1 z-[5] h-7 w-1 -translate-y-1/2 opacity-0 transition-all duration-300 group-hover:opacity-100",
|
||||||
{
|
{
|
||||||
"-right-1.5 opacity-100": isRightResizing,
|
"-right-1.5 opacity-100": isRightResizing,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const ChartDraggable = observer(function ChartDraggable(props: Props) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative z-[5] inline-flex h-full w-full cursor-pointer items-center font-medium transition-all">
|
<div className="nodedc-project-gantt-draggable group relative z-[5] inline-flex h-full w-full cursor-pointer items-center font-medium transition-all">
|
||||||
{/* left resize drag handle */}
|
{/* left resize drag handle */}
|
||||||
{(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && (
|
{(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && (
|
||||||
<LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
|
<LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
|
||||||
|
|
@ -55,7 +55,7 @@ export const ChartDraggable = observer(function ChartDraggable(props: Props) {
|
||||||
position={block.position}
|
position={block.position}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn("relative z-[6] flex h-8 w-full items-center rounded-sm", {
|
className={cn("nodedc-project-gantt-draggable-shell relative z-[6] flex h-8 w-full items-center", {
|
||||||
"pointer-events-none": isMoving,
|
"pointer-events-none": isMoving,
|
||||||
})}
|
})}
|
||||||
onMouseDown={(e) => enableBlockMove && handleBlockDrag(e, "move")}
|
onMouseDown={(e) => enableBlockMove && handleBlockDrag(e, "move")}
|
||||||
|
|
|
||||||
|
|
@ -44,23 +44,19 @@ export const IssuesSidebarBlock = observer(function IssuesSidebarBlock(props: Pr
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("group/list-block", {
|
className={cn("nodedc-project-gantt-sidebar-block group/list-block", {
|
||||||
"rounded-sm bg-layer-1": isDragging,
|
"nodedc-project-gantt-sidebar-block-dragging": isDragging,
|
||||||
"rounded-l-sm border border-r-0 border-accent-strong": getIsIssuePeeked(block.data.id),
|
"nodedc-project-gantt-sidebar-block-peeked": getIsIssuePeeked(block.data.id),
|
||||||
"border border-r-0 border-strong-1": isIssueFocused,
|
"nodedc-project-gantt-sidebar-block-focused": isIssueFocused,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||||
onMouseLeave={() => updateActiveBlockId(null)}
|
onMouseLeave={() => updateActiveBlockId(null)}
|
||||||
>
|
>
|
||||||
<Row
|
<Row
|
||||||
className={cn(
|
className={cn("nodedc-project-gantt-sidebar-row group flex w-full items-center gap-2 pr-4", {
|
||||||
"group flex w-full items-center gap-2 bg-layer-transparent pr-4 hover:bg-layer-transparent-hover",
|
"nodedc-project-gantt-sidebar-row-hovered": isBlockHoveredOn,
|
||||||
{
|
"nodedc-project-gantt-sidebar-row-selected": isIssueSelected,
|
||||||
"bg-layer-transparent-hover": isBlockHoveredOn,
|
})}
|
||||||
"bg-accent-primary/5 hover:bg-accent-primary/10": isIssueSelected,
|
|
||||||
"bg-accent-primary/10": isIssueSelected && isBlockHoveredOn,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
height: `${BLOCK_HEIGHT}px`,
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Prop
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="nodedc-project-gantt-sidebar-list">
|
||||||
{blockIds ? (
|
{blockIds ? (
|
||||||
<>
|
<>
|
||||||
{blockIds.map((blockId, index) => {
|
{blockIds.map((blockId, index) => {
|
||||||
|
|
@ -117,7 +117,7 @@ export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Prop
|
||||||
})}
|
})}
|
||||||
{canLoadMoreBlocks && (
|
{canLoadMoreBlocks && (
|
||||||
<div ref={setIntersectionElement} className="p-2">
|
<div ref={setIntersectionElement} className="p-2">
|
||||||
<div className="flex h-10 w-full animate-pulse items-center justify-between gap-1.5 rounded-sm bg-layer-1 px-4 py-1.5 md:h-8 md:px-1" />
|
<div className="nodedc-project-gantt-sidebar-loader flex h-10 w-full animate-pulse items-center justify-between gap-1.5 px-4 py-1.5 md:h-8 md:px-1" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -56,14 +56,14 @@ export const GanttChartSidebar = observer(function GanttChartSidebar(props: Prop
|
||||||
<Row
|
<Row
|
||||||
// DO NOT REMOVE THE ID
|
// DO NOT REMOVE THE ID
|
||||||
id="gantt-sidebar"
|
id="gantt-sidebar"
|
||||||
className="sticky left-0 z-10 h-max min-h-full flex-shrink-0 border-r-[0.5px] border-subtle-1 bg-surface-1"
|
className="nodedc-project-gantt-sidebar sticky left-0 z-10 h-max min-h-full flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
width: `${SIDEBAR_WIDTH}px`,
|
width: `${SIDEBAR_WIDTH}px`,
|
||||||
}}
|
}}
|
||||||
variant={ERowVariant.HUGGING}
|
variant={ERowVariant.HUGGING}
|
||||||
>
|
>
|
||||||
<Row
|
<Row
|
||||||
className="group/list-header sticky top-0 z-10 box-border flex flex-shrink-0 items-end justify-between gap-2 border-b-[0.5px] border-subtle-1 bg-surface-1 pr-4 pb-2 text-13 font-medium text-tertiary"
|
className="nodedc-project-gantt-sidebar-header group/list-header sticky top-0 z-10 box-border flex flex-shrink-0 items-end justify-between gap-2 pr-4 pb-3 text-12 font-semibold text-tertiary"
|
||||||
style={{
|
style={{
|
||||||
height: `${HEADER_HEIGHT}px`,
|
height: `${HEADER_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
|
|
@ -88,7 +88,7 @@ export const GanttChartSidebar = observer(function GanttChartSidebar(props: Prop
|
||||||
<h6>{t("common.duration")}</h6>
|
<h6>{t("common.duration")}</h6>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row variant={ERowVariant.HUGGING} className="h-max min-h-full bg-surface-1">
|
<Row variant={ERowVariant.HUGGING} className="nodedc-project-gantt-sidebar-body h-max min-h-full">
|
||||||
{sidebarToRender &&
|
{sidebarToRender &&
|
||||||
sidebarToRender({
|
sidebarToRender({
|
||||||
title,
|
title,
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,13 @@ const EXTERNAL_DECK_LIMIT = 10;
|
||||||
const INTERNAL_CURSOR = `${INTERNAL_DECK_LIMIT}:0:0`;
|
const INTERNAL_CURSOR = `${INTERNAL_DECK_LIMIT}:0:0`;
|
||||||
|
|
||||||
type HomeRecentIssueDecksProps = {
|
type HomeRecentIssueDecksProps = {
|
||||||
|
compact?: boolean;
|
||||||
project?: THomeProjectData;
|
project?: THomeProjectData;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeckSectionProps = {
|
type DeckSectionProps = {
|
||||||
|
compact?: boolean;
|
||||||
count: number;
|
count: number;
|
||||||
description: string;
|
description: string;
|
||||||
emptyDescription: string;
|
emptyDescription: string;
|
||||||
|
|
@ -49,6 +51,7 @@ type DeckSectionProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type InternalIssueCardProps = {
|
type InternalIssueCardProps = {
|
||||||
|
compact?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
|
|
@ -56,6 +59,7 @@ type InternalIssueCardProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExternalIssueCardProps = {
|
type ExternalIssueCardProps = {
|
||||||
|
compact?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
project: THomeProjectData;
|
project: THomeProjectData;
|
||||||
|
|
@ -76,14 +80,14 @@ const sortByRecentCreatedDate = <
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeckSection = (props: DeckSectionProps) => {
|
const DeckSection = (props: DeckSectionProps) => {
|
||||||
const { count, description, emptyDescription, emptyTitle, isLoading, items, title } = props;
|
const { compact = false, count, description, emptyDescription, emptyTitle, isLoading, items, title } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-14 font-semibold text-primary">{title}</div>
|
<div className="text-14 font-semibold text-primary">{title}</div>
|
||||||
<div className="text-12 text-secondary">{description}</div>
|
{!compact && <div className="text-12 text-secondary">{description}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="nodedc-toolbar-pill inline-flex items-center gap-2">
|
<div className="nodedc-toolbar-pill inline-flex items-center gap-2">
|
||||||
<Sparkles className="size-3.5" />
|
<Sparkles className="size-3.5" />
|
||||||
|
|
@ -91,16 +95,24 @@ const DeckSection = (props: DeckSectionProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-task-deck-scroller">
|
<div
|
||||||
<div className="flex min-h-[236px] items-end px-1 py-4">
|
className={cn("nodedc-home-task-deck-scroller", {
|
||||||
|
"nodedc-home-task-deck-scroller-compact": compact,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn("flex items-start px-1 py-2", {
|
||||||
|
"min-h-[236px] gap-4": !compact,
|
||||||
|
"nodedc-home-task-deck-row-compact min-h-[172px]": compact,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{isLoading
|
{isLoading
|
||||||
? Array.from({ length: 4 }, (_, index) => (
|
? Array.from({ length: 4 }, (_, index) => (
|
||||||
<div
|
<div
|
||||||
key={`skeleton-${title}-${index}`}
|
key={`skeleton-${title}-${index}`}
|
||||||
className={cn("nodedc-home-task-card nodedc-home-task-card-skeleton animate-pulse", {
|
className={cn("nodedc-home-task-card nodedc-home-task-card-skeleton animate-pulse", {
|
||||||
"-ml-16": index > 0,
|
"nodedc-home-task-card-compact": compact,
|
||||||
})}
|
})}
|
||||||
style={{ zIndex: 5 - index }}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: items}
|
: items}
|
||||||
|
|
@ -118,7 +130,7 @@ const DeckSection = (props: DeckSectionProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCard(props: InternalIssueCardProps) {
|
const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCard(props: InternalIssueCardProps) {
|
||||||
const { isActive, issue, onSelect, project } = props;
|
const { compact = false, isActive, issue, onSelect, project } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
|
|
@ -183,7 +195,7 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
||||||
|
|
||||||
const footer = (
|
const footer = (
|
||||||
<>
|
<>
|
||||||
<div className={cn("inline-flex min-h-9 items-center rounded-full pl-1 pr-2", pillBackgroundClasses)}>
|
<div className={cn("inline-flex min-h-9 items-center rounded-full pr-2 pl-1", pillBackgroundClasses)}>
|
||||||
{(issue.assignee_ids?.length ?? 0) > 0 ? (
|
{(issue.assignee_ids?.length ?? 0) > 0 ? (
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -191,7 +203,12 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium", pillBackgroundClasses)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium",
|
||||||
|
pillBackgroundClasses
|
||||||
|
)}
|
||||||
|
>
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
<CalendarDays className="h-3.5 w-3.5" />
|
||||||
<span className="truncate">{dueDateLabel}</span>
|
<span className="truncate">{dueDateLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,14 +216,24 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" className="nodedc-home-task-card" data-active={isActive} onClick={onSelect} title={issue.name}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("nodedc-home-task-card", { "nodedc-home-task-card-compact": compact })}
|
||||||
|
data-active={isActive}
|
||||||
|
onClick={onSelect}
|
||||||
|
title={issue.name}
|
||||||
|
>
|
||||||
<NodedcWorkItemCard
|
<NodedcWorkItemCard
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
surfaceClassName={cn(
|
surfaceClassName={cn(
|
||||||
"nodedc-home-task-card-surface px-0",
|
"nodedc-home-task-card-surface px-0",
|
||||||
|
compact && "!rounded-[24px]",
|
||||||
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
|
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
|
||||||
)}
|
)}
|
||||||
contentClassName="px-1"
|
contentClassName={cn("px-1", compact && "min-h-[168px]")}
|
||||||
|
titleContainerClassName={cn(compact && "px-3 py-3")}
|
||||||
|
titleClassName={cn(compact && "text-base leading-5")}
|
||||||
|
footerClassName={cn(compact && "gap-2")}
|
||||||
header={header}
|
header={header}
|
||||||
subtitle={sourceContourName}
|
subtitle={sourceContourName}
|
||||||
title={issue.name}
|
title={issue.name}
|
||||||
|
|
@ -217,15 +244,13 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
||||||
});
|
});
|
||||||
|
|
||||||
const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCard(props: ExternalIssueCardProps) {
|
const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCard(props: ExternalIssueCardProps) {
|
||||||
const { isActive, onSelect, project, request } = props;
|
const { compact = false, isActive, onSelect, project, request } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getStateById } = useProjectState();
|
const { getStateById } = useProjectState();
|
||||||
const { iconBubbleClasses, pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
const { iconBubbleClasses, pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
||||||
|
|
||||||
const issue = request.issue;
|
const issue = request.issue;
|
||||||
const isOutgoing = request.direction
|
const isOutgoing = request.direction ? request.direction === "outgoing" : request.source_project_id === project.id;
|
||||||
? request.direction === "outgoing"
|
|
||||||
: request.source_project_id === project.id;
|
|
||||||
const requester =
|
const requester =
|
||||||
request.requested_by?.display_name ||
|
request.requested_by?.display_name ||
|
||||||
request.requested_by_name ||
|
request.requested_by_name ||
|
||||||
|
|
@ -242,15 +267,28 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" className="nodedc-home-task-card" data-active={isActive} onClick={onSelect} title={issue.name}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("nodedc-home-task-card", { "nodedc-home-task-card-compact": compact })}
|
||||||
|
data-active={isActive}
|
||||||
|
onClick={onSelect}
|
||||||
|
title={issue.name}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
"nodedc-external-card nodedc-home-task-card-surface relative flex min-h-[220px] w-full flex-col p-4",
|
"nodedc-external-card nodedc-home-task-card-surface relative flex w-full flex-col",
|
||||||
|
compact ? "min-h-[168px] rounded-[24px] p-3" : "min-h-[220px] p-4",
|
||||||
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
|
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn("relative flex min-h-[220px] flex-col px-1", isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white")}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex flex-col px-1",
|
||||||
|
compact ? "min-h-[168px]" : "min-h-[220px]",
|
||||||
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white/70"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
|
|
@ -283,7 +321,7 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"truncate -mt-0.5 pl-8 text-[11px] font-medium leading-4",
|
"-mt-0.5 truncate pl-8 text-[11px] leading-4 font-medium",
|
||||||
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/72" : "text-[#B3B3B8]"
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/72" : "text-[#B3B3B8]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -291,12 +329,21 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center">
|
<div
|
||||||
<div className="line-clamp-4 max-w-full text-lg font-semibold leading-6">{issue.name}</div>
|
className={cn("flex flex-1 items-center justify-center text-center", compact ? "px-3 py-3" : "px-5 py-4")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"line-clamp-4 max-w-full font-semibold",
|
||||||
|
compact ? "text-base leading-5" : "text-lg leading-6"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{issue.name}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className={cn("flex items-center justify-between", compact ? "gap-2" : "gap-3")}>
|
||||||
<div className={cn("inline-flex min-h-9 items-center rounded-full pl-1 pr-2", pillBackgroundClasses)}>
|
<div className={cn("inline-flex min-h-9 items-center rounded-full pr-2 pl-1", pillBackgroundClasses)}>
|
||||||
{(issue.assignee_ids?.length ?? 0) > 0 ? (
|
{(issue.assignee_ids?.length ?? 0) > 0 ? (
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -304,7 +351,12 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium", pillBackgroundClasses)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium",
|
||||||
|
pillBackgroundClasses
|
||||||
|
)}
|
||||||
|
>
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
<CalendarDays className="h-3.5 w-3.5" />
|
||||||
<span className="truncate">{dueDateLabel}</span>
|
<span className="truncate">{dueDateLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -316,7 +368,7 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
||||||
});
|
});
|
||||||
|
|
||||||
export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props: HomeRecentIssueDecksProps) {
|
export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props: HomeRecentIssueDecksProps) {
|
||||||
const { project, workspaceSlug } = props;
|
const { compact = false, project, workspaceSlug } = props;
|
||||||
const [selectedInternalIssueId, setSelectedInternalIssueId] = useState<string | null>(null);
|
const [selectedInternalIssueId, setSelectedInternalIssueId] = useState<string | null>(null);
|
||||||
const [selectedExternalRequestId, setSelectedExternalRequestId] = useState<string | null>(null);
|
const [selectedExternalRequestId, setSelectedExternalRequestId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -382,9 +434,13 @@ export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return (
|
return (
|
||||||
<HomeCardShell
|
<HomeCardShell
|
||||||
eyebrow="Task Decks"
|
eyebrow={compact ? "Последние задачи" : "Task Decks"}
|
||||||
title="Последние задачи по проекту"
|
title={compact ? "Последние задачи проекта" : "Последние задачи по проекту"}
|
||||||
description="Выберите проект слева, и здесь появятся колоды последних задач внешнего и внутреннего контуров."
|
description={
|
||||||
|
compact
|
||||||
|
? undefined
|
||||||
|
: "Выберите проект слева, и здесь появятся колоды последних задач внешнего и внутреннего контуров."
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="rounded-[24px] border border-white/6 bg-black/10 px-5 py-6 text-13 text-secondary">
|
<div className="rounded-[24px] border border-white/6 bg-black/10 px-5 py-6 text-13 text-secondary">
|
||||||
Фокус проекта пока не выбран.
|
Фокус проекта пока не выбран.
|
||||||
|
|
@ -393,61 +449,63 @@ export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalIssueCards = internalIssues.map((issue, index) => (
|
const internalIssueCards = internalIssues.map((issue) => (
|
||||||
<div
|
<HomeInternalContourDeckCard
|
||||||
|
compact={compact}
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className={cn({ "-ml-16": index > 0 })}
|
issue={issue}
|
||||||
style={{ zIndex: issue.id === selectedInternalIssueId ? internalIssues.length + 6 : index + 1 }}
|
isActive={issue.id === selectedInternalIssueId}
|
||||||
>
|
onSelect={() => setSelectedInternalIssueId(issue.id)}
|
||||||
<HomeInternalContourDeckCard
|
project={project}
|
||||||
issue={issue}
|
/>
|
||||||
isActive={issue.id === selectedInternalIssueId}
|
|
||||||
onSelect={() => setSelectedInternalIssueId(issue.id)}
|
|
||||||
project={project}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
));
|
||||||
|
|
||||||
const externalIssueCards = externalRequests.map((request, index) => (
|
const externalIssueCards = externalRequests.map((request) => (
|
||||||
<div
|
<HomeExternalContourDeckCard
|
||||||
|
compact={compact}
|
||||||
key={request.id}
|
key={request.id}
|
||||||
className={cn({ "-ml-16": index > 0 })}
|
isActive={request.id === selectedExternalRequestId}
|
||||||
style={{ zIndex: request.id === selectedExternalRequestId ? externalRequests.length + 6 : index + 1 }}
|
onSelect={() => setSelectedExternalRequestId(request.id)}
|
||||||
>
|
project={project}
|
||||||
<HomeExternalContourDeckCard
|
request={request}
|
||||||
isActive={request.id === selectedExternalRequestId}
|
/>
|
||||||
onSelect={() => setSelectedExternalRequestId(request.id)}
|
|
||||||
project={project}
|
|
||||||
request={request}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomeCardShell
|
<HomeCardShell
|
||||||
eyebrow={`${project.identifier} • последние задачи`}
|
eyebrow={compact ? `${project.identifier} • последние задачи` : `${project.identifier} • последние задачи`}
|
||||||
title="Последние задачи проекта"
|
title={compact ? "Последние задачи проекта" : "Последние задачи проекта"}
|
||||||
description="Ниже собраны две колоды для быстрого просмотра новых карточек во внешнем и внутреннем контурах."
|
description={
|
||||||
contentClassName="space-y-5 p-5"
|
compact
|
||||||
|
? undefined
|
||||||
|
: "Ниже собраны две колоды для быстрого просмотра новых карточек во внешнем и внутреннем контурах."
|
||||||
|
}
|
||||||
|
contentClassName={compact ? "space-y-3 p-4" : "grid gap-5 p-5 xl:grid-cols-2"}
|
||||||
>
|
>
|
||||||
<DeckSection
|
<DeckSection
|
||||||
|
compact={compact}
|
||||||
count={externalRequests.length}
|
count={externalRequests.length}
|
||||||
description="Последние запросы и задачи внешнего контура по текущему проекту."
|
description={
|
||||||
|
compact ? "Последние внешние карточки" : "Последние запросы и задачи внешнего контура по текущему проекту."
|
||||||
|
}
|
||||||
emptyDescription="У проекта пока нет внешних контурных задач. Когда появится первый обмен с контрагентом, он сразу попадет в эту колоду."
|
emptyDescription="У проекта пока нет внешних контурных задач. Когда появится первый обмен с контрагентом, он сразу попадет в эту колоду."
|
||||||
emptyTitle="Внешний контур пока пуст"
|
emptyTitle="Внешний контур пока пуст"
|
||||||
isLoading={isExternalRequestsLoading}
|
isLoading={isExternalRequestsLoading}
|
||||||
items={externalIssueCards}
|
items={externalIssueCards}
|
||||||
title="Последние задачи внешнего контура"
|
title={compact ? "Внешний контур" : "Внешний контур"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeckSection
|
<DeckSection
|
||||||
|
compact={compact}
|
||||||
count={internalIssues.length}
|
count={internalIssues.length}
|
||||||
description="Последние добавленные внутренние задачи выбранного проекта."
|
description={
|
||||||
|
compact ? "Последние внутренние карточки" : "Последние добавленные внутренние задачи выбранного проекта."
|
||||||
|
}
|
||||||
emptyDescription="Во внутреннем контуре пока нет задач. Как только в проекте появится новая карточка, она ляжет сюда первой."
|
emptyDescription="Во внутреннем контуре пока нет задач. Как только в проекте появится новая карточка, она ляжет сюда первой."
|
||||||
emptyTitle="Внутренний контур пока пуст"
|
emptyTitle="Внутренний контур пока пуст"
|
||||||
isLoading={isInternalIssuesLoading}
|
isLoading={isInternalIssuesLoading}
|
||||||
items={internalIssueCards}
|
items={internalIssueCards}
|
||||||
title="Последние задачи внутреннего контура"
|
title={compact ? "Внутренний контур" : "Внутренний контур"}
|
||||||
/>
|
/>
|
||||||
</HomeCardShell>
|
</HomeCardShell>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,20 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// plane types
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import type { IUser, TProjectAnalyticsCount } from "@plane/types";
|
import type { IUser } from "@plane/types";
|
||||||
|
import { Avatar } from "@plane/ui";
|
||||||
|
import { getFileURL } from "@plane/utils";
|
||||||
import { useCurrentTime } from "@/hooks/use-current-time";
|
import { useCurrentTime } from "@/hooks/use-current-time";
|
||||||
import { getCompletionRate, type THomeProjectData } from "./home.utils";
|
|
||||||
|
|
||||||
export interface IUserGreetingsView {
|
export interface IUserGreetingsView {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
workspaceName?: string | null;
|
workspaceName?: string | null;
|
||||||
selectedProject?: THomeProjectData;
|
|
||||||
selectedProjectAnalytics?: TProjectAnalyticsCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserGreetingsView(props: IUserGreetingsView) {
|
export function UserGreetingsView(props: IUserGreetingsView) {
|
||||||
const { user, workspaceName, selectedProject, selectedProjectAnalytics } = props;
|
const { user, workspaceName } = props;
|
||||||
// current time hook
|
|
||||||
const { currentTime } = useCurrentTime();
|
const { currentTime } = useCurrentTime();
|
||||||
// store hooks
|
|
||||||
const { t, currentLocale } = useTranslation();
|
const { t, currentLocale } = useTranslation();
|
||||||
|
|
||||||
const hour = new Intl.DateTimeFormat(currentLocale, {
|
const hour = new Intl.DateTimeFormat(currentLocale, {
|
||||||
|
|
@ -46,39 +42,37 @@ export function UserGreetingsView(props: IUserGreetingsView) {
|
||||||
}).format(currentTime);
|
}).format(currentTime);
|
||||||
|
|
||||||
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
|
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
|
||||||
const completionRate = getCompletionRate(selectedProjectAnalytics);
|
const userName = `${user?.first_name ?? ""} ${user?.last_name ?? ""}`.trim() || user?.email || "Workspace admin";
|
||||||
|
const userEmail = user?.email || workspaceName || "NODE DC";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="nodedc-home-card px-5 py-4">
|
<section className="nodedc-home-user-card">
|
||||||
<div className="grid items-stretch gap-3 xl:grid-cols-[minmax(0,1fr)_280px_220px]">
|
<div className="nodedc-home-user-card-orb" />
|
||||||
<div className="flex min-w-0 items-center">
|
<div className="relative z-[1] p-5">
|
||||||
|
<div className="text-[11px] font-semibold tracking-[0.22em] text-white/55 uppercase">
|
||||||
|
{workspaceName ?? "NODE DC"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 flex items-start gap-4">
|
||||||
|
<div className="shrink-0 rounded-[26px] bg-white/14 p-1 backdrop-blur-xl">
|
||||||
|
<Avatar src={getFileURL(user?.avatar_url ?? "")} name={userName} size="lg" />
|
||||||
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">
|
<div className="text-12 text-white/58">{`${weekDay}, ${date} ${timeString}`}</div>
|
||||||
{workspaceName ?? "Workspace Home"}
|
<h2 className="mt-1 line-clamp-2 text-24 font-semibold leading-tight text-white">
|
||||||
</div>
|
{`${t("good")} ${t(greeting)}, ${user?.first_name || "DC"}`}
|
||||||
<h2 className="mt-1 truncate text-24 font-semibold text-primary">
|
|
||||||
{`${t("good")} ${t(greeting)}, ${user?.first_name} ${user?.last_name}`}
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-1 text-13 text-secondary">{`${weekDay}, ${date} ${timeString}`}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3">
|
<div className="mt-8 space-y-1">
|
||||||
<div className="text-12 font-medium text-secondary">Текущий фокус</div>
|
<div className="text-18 font-semibold text-white">{userName}</div>
|
||||||
<div className="mt-2 line-clamp-2 text-18 font-semibold text-primary">
|
<div className="truncate text-13 text-white/62">{userEmail}</div>
|
||||||
{selectedProject ? selectedProject.name : "Выберите проект слева"}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-12 text-secondary">
|
|
||||||
{selectedProject ? selectedProject.identifier : "Домашняя сводка перестроится под выбранную карточку."}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3">
|
<div className="mt-5 flex flex-wrap items-center gap-2 text-[11px] font-medium text-white/56">
|
||||||
<div className="text-12 font-medium text-secondary">Прогресс фокуса</div>
|
<div className="rounded-full bg-black/24 px-3 py-1.5 backdrop-blur-md">home admin</div>
|
||||||
<div className="mt-2 text-18 font-semibold text-primary">{selectedProject ? `${completionRate}%` : "—"}</div>
|
<div className="rounded-full bg-black/24 px-3 py-1.5 backdrop-blur-md">{workspaceName ?? "workspace"}</div>
|
||||||
<div className="mt-1 text-12 text-secondary">
|
|
||||||
{selectedProject ? "Закрытые задачи относительно общего объёма." : "Станет доступен после выбора проекта."}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@ import { useTranslation } from "@plane/i18n";
|
||||||
// plane types
|
// plane types
|
||||||
import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons";
|
import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons";
|
||||||
import type { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types";
|
import type { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types";
|
||||||
// plane ui
|
|
||||||
// components
|
|
||||||
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
|
|
||||||
// plane web services
|
// plane web services
|
||||||
import { WorkspaceService } from "@/services/workspace.service";
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
import { getActivityProjectId } from "../../home.utils";
|
import { getActivityProjectId } from "../../home.utils";
|
||||||
|
|
@ -105,20 +102,15 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentOverflowWrapper
|
<div className="box-border min-h-[250px]">
|
||||||
maxHeight={415}
|
|
||||||
containerClassName="box-border min-h-[250px]"
|
|
||||||
fallback={<></>}
|
|
||||||
buttonClassName="nodedc-toolbar-pill justify-center"
|
|
||||||
>
|
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<div className="text-14 font-semibold text-tertiary">{t("home.recents.title")}</div>
|
<div className="text-14 font-semibold text-tertiary">{t("home.recents.title")}</div>
|
||||||
{showFilterSelect && <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />}
|
{showFilterSelect && <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-[250px] flex-col">
|
<div className="flex max-h-[415px] min-h-[250px] flex-col overflow-y-auto">
|
||||||
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
|
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
|
||||||
{!isLoading && recents.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
|
{!isLoading && recents.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
|
||||||
</div>
|
</div>
|
||||||
</ContentOverflowWrapper>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { useTheme } from "next-themes";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { SearchIcon } from "@plane/propel/icons";
|
import { SearchIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { ISearchIssueResponse } from "@plane/types";
|
import type { ISearchIssueResponse } from "@plane/types";
|
||||||
import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||||
|
|
@ -90,7 +90,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
|
||||||
{query === "" && <h2 className="mt-4 mb-2 px-3 text-11 font-semibold text-primary">Select work item</h2>}
|
{query === "" && <h2 className="mt-4 mb-2 px-3 text-11 font-semibold text-primary">Select work item</h2>}
|
||||||
<ul className="text-13 text-primary">
|
<ul className="text-13 text-primary">
|
||||||
{filteredIssues.map((issue) => {
|
{filteredIssues.map((issue) => {
|
||||||
const stateColor = issue.state__color || "";
|
const stateColor = getStateGroupColor(issue.state__group, issue.state__color);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { useRouter } from "next/navigation";
|
||||||
import { MinusCircle } from "lucide-react";
|
import { MinusCircle } from "lucide-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
|
import { getStateGroupColor } from "@plane/propel/icons";
|
||||||
import type { TIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
// component
|
// component
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -55,7 +56,7 @@ export const IssueParentDetail = observer(function IssueParentDetail(props: TIss
|
||||||
const issueParentState = getProjectStates(parentIssue?.project_id)?.find(
|
const issueParentState = getProjectStates(parentIssue?.project_id)?.find(
|
||||||
(state) => state?.id === parentIssue?.state_id
|
(state) => state?.id === parentIssue?.state_id
|
||||||
);
|
);
|
||||||
const stateColor = issueParentState?.color || undefined;
|
const stateColor = getStateGroupColor(issueParentState?.group, issueParentState?.color);
|
||||||
|
|
||||||
if (!parentIssue) return <></>;
|
if (!parentIssue) return <></>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { useParams } from "next/navigation";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
import { MoreHorizontal } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { Popover } from "@plane/propel/popover";
|
import { Popover } from "@plane/propel/popover";
|
||||||
|
import { getStateGroupColor } from "@plane/propel/icons";
|
||||||
import type { TIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
import { ControlLink } from "@plane/ui";
|
import { ControlLink } from "@plane/ui";
|
||||||
import { cn, generateWorkItemLink } from "@plane/utils";
|
import { cn, generateWorkItemLink } from "@plane/utils";
|
||||||
|
|
@ -50,16 +51,15 @@ export const CalendarIssueBlock = observer(
|
||||||
const { issuesFilter } = useIssues(storeType);
|
const { issuesFilter } = useIssues(storeType);
|
||||||
const { getProjectIdentifierById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
|
|
||||||
const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || "";
|
const stateDetails = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id);
|
||||||
|
const stateColor = getStateGroupColor(stateDetails?.group, stateDetails?.color);
|
||||||
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
|
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug.toString(), issue, isMobile);
|
const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug.toString(), issue, isMobile);
|
||||||
|
|
||||||
const customActionButton = (
|
const customActionButton = (
|
||||||
<div
|
<div className="w-full cursor-pointer rounded-sm p-1 text-secondary hover:bg-layer-1 hover:text-primary">
|
||||||
className="w-full cursor-pointer rounded-sm p-1 text-secondary hover:bg-layer-1 hover:text-primary"
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// plane imports
|
// plane imports
|
||||||
|
import { getStateGroupColor } from "@plane/propel/icons";
|
||||||
import { Popover } from "@plane/propel/popover";
|
import { Popover } from "@plane/propel/popover";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import { ControlLink } from "@plane/ui";
|
import { ControlLink } from "@plane/ui";
|
||||||
|
|
@ -53,7 +54,15 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) {
|
||||||
const stateDetails =
|
const stateDetails =
|
||||||
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
|
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
|
||||||
|
|
||||||
const { blockStyle } = getBlockViewDetails(issueDetails, stateDetails?.color ?? "");
|
const stateGroupColor = getStateGroupColor(stateDetails?.group, stateDetails?.color);
|
||||||
|
const { blockStyle } = getBlockViewDetails(issueDetails, stateGroupColor);
|
||||||
|
const ganttBlockStyle = {
|
||||||
|
...blockStyle,
|
||||||
|
backgroundColor: stateGroupColor,
|
||||||
|
maskImage: "none",
|
||||||
|
WebkitMaskImage: "none",
|
||||||
|
};
|
||||||
|
const ganttBlockTextColor = ["completed", "started"].includes(stateDetails?.group ?? "") ? "#0B1117" : "#FFFFFF";
|
||||||
|
|
||||||
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
|
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||||
|
|
||||||
|
|
@ -66,14 +75,14 @@ export const IssueGanttBlock = observer(function IssueGanttBlock(props: Props) {
|
||||||
render={
|
render={
|
||||||
<div
|
<div
|
||||||
id={`issue-${issueId}`}
|
id={`issue-${issueId}`}
|
||||||
className="space-between relative flex h-full w-full cursor-pointer items-center rounded-sm"
|
className="nodedc-project-gantt-issue-bar space-between relative flex h-full w-full cursor-pointer items-center"
|
||||||
style={blockStyle}
|
style={ganttBlockStyle}
|
||||||
onClick={handleIssuePeekOverview}
|
onClick={handleIssuePeekOverview}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 left-0 h-full w-full bg-surface-1/50" />
|
<div className="nodedc-project-gantt-issue-bar-shade absolute top-0 left-0 h-full w-full" />
|
||||||
<div
|
<div
|
||||||
className="sticky w-auto flex-1 truncate overflow-hidden px-2.5 py-1 text-13 text-primary"
|
className="nodedc-project-gantt-issue-bar-title sticky w-auto flex-1 truncate overflow-hidden px-3 py-1 text-12 font-semibold text-primary"
|
||||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
style={{ left: `${SIDEBAR_WIDTH}px`, color: ganttBlockTextColor }}
|
||||||
>
|
>
|
||||||
{issueDetails?.name}
|
{issueDetails?.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -73,11 +73,12 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||||
const { pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
const { pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
||||||
const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
|
const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
|
||||||
|
const controlStatusIconColor = selectedState?.group === "started" ? "#F5F7FB" : statusIconColor;
|
||||||
|
|
||||||
const creatorName = creatorDetails?.display_name ?? t("common.none");
|
const creatorName = creatorDetails?.display_name ?? t("common.none");
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
const cornerControlClasses =
|
const cornerControlClasses =
|
||||||
"flex h-12 w-12 items-center justify-center rounded-full border-0 bg-white text-[#0B1117] shadow-none outline-none ring-0 transition-transform hover:scale-[1.03]";
|
"flex h-12 w-12 items-center justify-center rounded-full border-0 bg-[#17181B] text-white shadow-none outline-none ring-0 transition-transform hover:scale-[1.03] hover:bg-[#0F1012]";
|
||||||
const dateButton = (
|
const dateButton = (
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
className="h-auto self-start"
|
className="h-auto self-start"
|
||||||
|
|
@ -112,7 +113,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
<div
|
<div
|
||||||
data-control-link-ignore="true"
|
data-control-link-ignore="true"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-black text-white shadow-none ring-0 transition-transform outline-none hover:scale-[1.03]",
|
"flex h-12 w-12 -translate-x-0.5 -translate-y-0.5 cursor-pointer items-center justify-center rounded-full bg-black text-white shadow-none ring-0 transition-transform outline-none hover:scale-[1.03]",
|
||||||
isActive
|
isActive
|
||||||
? "text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90"
|
? "text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90"
|
||||||
: "bg-[#111214] text-white hover:bg-[#0A0B0C]"
|
: "bg-[#111214] text-white hover:bg-[#0A0B0C]"
|
||||||
|
|
@ -131,6 +132,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
name={creatorName}
|
name={creatorName}
|
||||||
size="md"
|
size="md"
|
||||||
showTooltip={!isMobile}
|
showTooltip={!isMobile}
|
||||||
|
className="border-0 shadow-none ring-0 outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
|
|
@ -139,7 +141,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute top-0 right-0 flex shrink-0 items-center gap-0">
|
<div className="absolute top-0.5 right-0.5 flex shrink-0 items-center gap-0">
|
||||||
<PriorityDropdown
|
<PriorityDropdown
|
||||||
value={issue.priority}
|
value={issue.priority}
|
||||||
onChange={(priority) => updateIssue?.(issue.project_id ?? null, issue.id, { priority })}
|
onChange={(priority) => updateIssue?.(issue.project_id ?? null, issue.id, { priority })}
|
||||||
|
|
@ -160,7 +162,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
<div data-control-link-ignore="true" className={cornerControlClasses}>
|
<div data-control-link-ignore="true" className={cornerControlClasses}>
|
||||||
<StateGroupIcon
|
<StateGroupIcon
|
||||||
stateGroup={selectedState?.group ?? "backlog"}
|
stateGroup={selectedState?.group ?? "backlog"}
|
||||||
color={statusIconColor}
|
color={controlStatusIconColor}
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
percentage={selectedState?.order}
|
percentage={selectedState?.order}
|
||||||
/>
|
/>
|
||||||
|
|
@ -190,7 +192,10 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
button={
|
button={
|
||||||
<div
|
<div
|
||||||
data-control-link-ignore="true"
|
data-control-link-ignore="true"
|
||||||
className={cn(basePillClasses, "h-11 min-w-11 justify-center rounded-full bg-transparent px-0 py-0")}
|
className={cn(
|
||||||
|
basePillClasses,
|
||||||
|
"h-11 min-w-11 justify-center rounded-full bg-transparent px-0 py-0 shadow-none ring-0 outline-none [&_.bg-accent-subtle]:!bg-transparent [&_.border-subtle-1]:!border-0"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids} size="sm" />
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { observer } from "mobx-react";
|
||||||
import type { Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { ETabIndices } from "@plane/constants";
|
import { ETabIndices } from "@plane/constants";
|
||||||
import { CloseIcon } from "@plane/propel/icons";
|
import { CloseIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||||
// plane imports
|
// plane imports
|
||||||
// types
|
// types
|
||||||
import type { ISearchIssueResponse, TIssue } from "@plane/types";
|
import type { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||||
|
|
@ -44,7 +44,7 @@ export const IssueParentTag = observer(function IssueParentTag(props: TIssuePare
|
||||||
<span
|
<span
|
||||||
className="block h-1.5 w-1.5 rounded-full"
|
className="block h-1.5 w-1.5 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedParentIssue.state__color,
|
backgroundColor: getStateGroupColor(selectedParentIssue.state__group, selectedParentIssue.state__color),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0 text-secondary">
|
<span className="flex-shrink-0 text-secondary">
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { Rocket } from "lucide-react";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
// i18n
|
// i18n
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { SearchIcon } from "@plane/propel/icons";
|
import { SearchIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||||
// types
|
// types
|
||||||
import type { ISearchIssueResponse } from "@plane/types";
|
import type { ISearchIssueResponse } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -159,7 +159,7 @@ export function ParentIssuesListModal({
|
||||||
<span
|
<span
|
||||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: issue.state__color,
|
backgroundColor: getStateGroupColor(issue.state__group, issue.state__color),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ export const ModuleGanttBlock = observer(function ModuleGanttBlock(props: Props)
|
||||||
moduleDetails,
|
moduleDetails,
|
||||||
MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? ""
|
MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? ""
|
||||||
);
|
);
|
||||||
|
const ganttBlockStyle = {
|
||||||
|
...blockStyle,
|
||||||
|
maskImage: "none",
|
||||||
|
WebkitMaskImage: "none",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|
@ -53,15 +58,15 @@ export const ModuleGanttBlock = observer(function ModuleGanttBlock(props: Props)
|
||||||
position="top-start"
|
position="top-start"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full cursor-pointer items-center rounded-sm"
|
className="nodedc-project-gantt-issue-bar relative flex h-full w-full cursor-pointer items-center"
|
||||||
style={blockStyle}
|
style={ganttBlockStyle}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`
|
`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 left-0 h-full w-full bg-surface-1/50" />
|
<div className="nodedc-project-gantt-issue-bar-shade absolute top-0 left-0 h-full w-full" />
|
||||||
<div
|
<div
|
||||||
className="sticky w-auto truncate overflow-hidden px-2.5 py-1 text-13 text-primary"
|
className="sticky w-auto truncate overflow-hidden px-2.5 py-1 text-13 text-primary"
|
||||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||||
|
|
|
||||||
|
|
@ -311,10 +311,10 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshVisibleIssueStores = useCallback(
|
const refreshVisibleIssueStores = useCallback(
|
||||||
async (createdProjectId?: string) => {
|
async () => {
|
||||||
const refreshes: Promise<unknown>[] = [];
|
const refreshes: Promise<unknown>[] = [];
|
||||||
|
|
||||||
if (createdProjectId && activeProjectId === createdProjectId) {
|
if (activeProjectId) {
|
||||||
refreshes.push(refreshProjectIssues(workspaceSlug, activeProjectId, "mutation"));
|
refreshes.push(refreshProjectIssues(workspaceSlug, activeProjectId, "mutation"));
|
||||||
if (activeProjectViewId) {
|
if (activeProjectViewId) {
|
||||||
refreshes.push(refreshProjectViewIssues(workspaceSlug, activeProjectId, activeProjectViewId, "mutation"));
|
refreshes.push(refreshProjectViewIssues(workspaceSlug, activeProjectId, activeProjectViewId, "mutation"));
|
||||||
|
|
@ -361,7 +361,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||||
action,
|
action,
|
||||||
draft: parseResult.draft,
|
draft: parseResult.draft,
|
||||||
});
|
});
|
||||||
await refreshVisibleIssueStores(result.project_id);
|
await refreshVisibleIssueStores();
|
||||||
setCommitResult(result);
|
setCommitResult(result);
|
||||||
setStatus("committed");
|
setStatus("committed");
|
||||||
setToast({
|
setToast({
|
||||||
|
|
@ -456,7 +456,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||||
{parseResult.transcript && (
|
{parseResult.transcript && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-11 font-medium text-tertiary uppercase">Транскрипт</div>
|
<div className="text-11 font-medium text-tertiary uppercase">Транскрипт</div>
|
||||||
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary">
|
<p className="mt-1 whitespace-pre-wrap break-words text-12 leading-5 text-secondary">
|
||||||
{parseResult.transcript}
|
{parseResult.transcript}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -524,7 +524,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||||
{parseResult.draft.description && (
|
{parseResult.draft.description && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-11 font-medium text-tertiary uppercase">Описание</div>
|
<div className="text-11 font-medium text-tertiary uppercase">Описание</div>
|
||||||
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary">
|
<p className="mt-1 whitespace-pre-wrap break-words text-12 leading-5 text-secondary">
|
||||||
{parseResult.draft.description}
|
{parseResult.draft.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2108,6 +2108,286 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-card {
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.5rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.006) 100%), #08080a !important;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.035),
|
||||||
|
0 20px 60px rgba(0, 0, 0, 0.22) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-toolbar {
|
||||||
|
padding: 0.7rem 0.95rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.045);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 0%, rgba(var(--nodedc-card-active-rgb), 0.08), transparent 24rem),
|
||||||
|
rgba(8, 8, 10, 0.96) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-toolbar-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 2.45rem;
|
||||||
|
min-width: 2.45rem;
|
||||||
|
height: 2.45rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 0, 0, 0.58);
|
||||||
|
color: rgb(var(--nodedc-card-active-rgb));
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 2.15rem;
|
||||||
|
min-width: 2.55rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.07) !important;
|
||||||
|
padding-inline: 0.82rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 750;
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
color 160ms ease,
|
||||||
|
transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-chip:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-chip-active,
|
||||||
|
.nodedc-project-gantt-chip-live {
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-round-button {
|
||||||
|
display: inline-grid !important;
|
||||||
|
width: 2.25rem;
|
||||||
|
min-width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
place-items: center;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(0, 0, 0, 0.56) !important;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-round-button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.76) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-scroll {
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px) 0 0 / 5.5rem 100%,
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px) 0 0 / 100% 3.85rem,
|
||||||
|
rgba(3, 3, 5, 0.96) !important;
|
||||||
|
scrollbar-color: rgba(var(--nodedc-card-active-rgb), 0.66) rgba(255, 255, 255, 0.04);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-scroll::-webkit-scrollbar {
|
||||||
|
width: 0.65rem;
|
||||||
|
height: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-scroll::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(var(--nodedc-card-active-rgb), 0.62);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-stage {
|
||||||
|
background: rgba(4, 4, 6, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-layer {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar {
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.055);
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(11, 11, 13, 0.98) 0%,
|
||||||
|
rgba(11, 11, 13, 0.9) 78%,
|
||||||
|
rgba(11, 11, 13, 0.7) 100%
|
||||||
|
) !important;
|
||||||
|
box-shadow: 14px 0 32px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-header {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.055);
|
||||||
|
background: rgba(10, 10, 12, 0.96) !important;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-body,
|
||||||
|
.nodedc-project-gantt-sidebar-list {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-loader {
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.055) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-calendar-group {
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.045);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-calendar-header {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.055);
|
||||||
|
background: rgba(8, 8, 10, 0.95) !important;
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-period-label {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.055) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-period-meta {
|
||||||
|
color: var(--text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-subcell {
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.045);
|
||||||
|
color: var(--text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-subcell-today {
|
||||||
|
background: rgba(var(--nodedc-card-active-rgb), 0.13) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-column {
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.035);
|
||||||
|
background: rgba(255, 255, 255, 0.004);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-column-today {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(var(--nodedc-card-active-rgb), 0.13),
|
||||||
|
rgba(var(--nodedc-card-active-rgb), 0.035)
|
||||||
|
) !important;
|
||||||
|
box-shadow: inset 1px 0 0 rgba(var(--nodedc-card-active-rgb), 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-column-weekend {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-row-bg {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.026);
|
||||||
|
transition:
|
||||||
|
background 140ms ease,
|
||||||
|
box-shadow 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-row-bg:hover,
|
||||||
|
.nodedc-project-gantt-row-hovered {
|
||||||
|
background: rgba(255, 255, 255, 0.035) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-row-selected {
|
||||||
|
background: rgba(var(--nodedc-card-active-rgb), 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-row-focused,
|
||||||
|
.nodedc-project-gantt-row-peeked {
|
||||||
|
box-shadow: inset 3px 0 0 rgba(var(--nodedc-card-active-rgb), 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-jump-button,
|
||||||
|
.nodedc-project-gantt-block-placeholder {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(15, 15, 18, 0.92) !important;
|
||||||
|
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-draggable-shell {
|
||||||
|
border-radius: 999px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-issue-bar {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-issue-bar-shade {
|
||||||
|
border-radius: inherit;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-issue-bar-title {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-resize-tooltip {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(18, 18, 22, 0.96);
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-resize-handle {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-block {
|
||||||
|
transition:
|
||||||
|
background 140ms ease,
|
||||||
|
box-shadow 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-block-dragging {
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-block-peeked,
|
||||||
|
.nodedc-project-gantt-sidebar-block-focused {
|
||||||
|
box-shadow: inset 3px 0 0 rgba(var(--nodedc-card-active-rgb), 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-row {
|
||||||
|
background: transparent !important;
|
||||||
|
transition: background 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-row:hover,
|
||||||
|
.nodedc-project-gantt-sidebar-row-hovered {
|
||||||
|
background: rgba(255, 255, 255, 0.035) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-project-gantt-sidebar-row-selected {
|
||||||
|
background: rgba(var(--nodedc-card-active-rgb), 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-card {
|
.nodedc-home-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
||||||
|
|
@ -23,31 +23,31 @@ export const STATE_GROUPS: {
|
||||||
key: "backlog",
|
key: "backlog",
|
||||||
label: "Backlog",
|
label: "Backlog",
|
||||||
defaultStateName: "Backlog",
|
defaultStateName: "Backlog",
|
||||||
color: "#d9d9d9",
|
color: "#050505",
|
||||||
},
|
},
|
||||||
unstarted: {
|
unstarted: {
|
||||||
key: "unstarted",
|
key: "unstarted",
|
||||||
label: "Unstarted",
|
label: "Unstarted",
|
||||||
defaultStateName: "Todo",
|
defaultStateName: "Todo",
|
||||||
color: "#3f76ff",
|
color: "#7C7F85",
|
||||||
},
|
},
|
||||||
started: {
|
started: {
|
||||||
key: "started",
|
key: "started",
|
||||||
label: "Started",
|
label: "Started",
|
||||||
defaultStateName: "In Progress",
|
defaultStateName: "In Progress",
|
||||||
color: "#f59e0b",
|
color: "#FFFFFF",
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
key: "completed",
|
key: "completed",
|
||||||
label: "Completed",
|
label: "Completed",
|
||||||
defaultStateName: "Done",
|
defaultStateName: "Done",
|
||||||
color: "#16a34a",
|
color: "#C3FF66",
|
||||||
},
|
},
|
||||||
cancelled: {
|
cancelled: {
|
||||||
key: "cancelled",
|
key: "cancelled",
|
||||||
label: "Canceled",
|
label: "Canceled",
|
||||||
defaultStateName: "Cancelled",
|
defaultStateName: "Cancelled",
|
||||||
color: "#dc2626",
|
color: "#050505",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -92,22 +92,22 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [
|
||||||
{
|
{
|
||||||
key: "completed_issues",
|
key: "completed_issues",
|
||||||
title: "Completed",
|
title: "Completed",
|
||||||
color: "#16A34A",
|
color: "#C3FF66",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "started_issues",
|
key: "started_issues",
|
||||||
title: "Started",
|
title: "Started",
|
||||||
color: "#F59E0B",
|
color: "#FFFFFF",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "unstarted_issues",
|
key: "unstarted_issues",
|
||||||
title: "Unstarted",
|
title: "Unstarted",
|
||||||
color: "#3A3A3A",
|
color: "#7C7F85",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "backlog_issues",
|
key: "backlog_issues",
|
||||||
title: "Backlog",
|
title: "Backlog",
|
||||||
color: "#A3A3A3",
|
color: "#050505",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
export * from "./backlog-group-icon";
|
export * from "./backlog-group-icon";
|
||||||
export * from "./cancelled-group-icon";
|
export * from "./cancelled-group-icon";
|
||||||
export * from "./completed-group-icon";
|
export * from "./completed-group-icon";
|
||||||
|
export * from "./helper";
|
||||||
export * from "./started-group-icon";
|
export * from "./started-group-icon";
|
||||||
export * from "./state-group-icon";
|
export * from "./state-group-icon";
|
||||||
export * from "./unstarted-group-icon";
|
export * from "./unstarted-group-icon";
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { BacklogGroupIcon } from "./backlog-group-icon";
|
||||||
import { CancelledGroupIcon } from "./cancelled-group-icon";
|
import { CancelledGroupIcon } from "./cancelled-group-icon";
|
||||||
import { CompletedGroupIcon } from "./completed-group-icon";
|
import { CompletedGroupIcon } from "./completed-group-icon";
|
||||||
import type { IStateGroupIcon } from "./helper";
|
import type { IStateGroupIcon } from "./helper";
|
||||||
import { STATE_GROUP_COLORS, STATE_GROUP_SIZES } from "./helper";
|
import { getStateGroupColor, STATE_GROUP_SIZES } from "./helper";
|
||||||
import { StartedGroupIcon } from "./started-group-icon";
|
import { StartedGroupIcon } from "./started-group-icon";
|
||||||
import { UnstartedGroupIcon } from "./unstarted-group-icon";
|
import { UnstartedGroupIcon } from "./unstarted-group-icon";
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function StateGroupIcon({
|
||||||
<StateIconComponent
|
<StateIconComponent
|
||||||
height={STATE_GROUP_SIZES[size]}
|
height={STATE_GROUP_SIZES[size]}
|
||||||
width={STATE_GROUP_SIZES[size]}
|
width={STATE_GROUP_SIZES[size]}
|
||||||
color={color ?? STATE_GROUP_COLORS[stateGroup]}
|
color={getStateGroupColor(stateGroup, color)}
|
||||||
className={`flex-shrink-0 ${className}`}
|
className={`flex-shrink-0 ${className}`}
|
||||||
percentage={percentage}
|
percentage={percentage}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue