UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: приведение карточек внешнего контура к референсу
This commit is contained in:
parent
248292bd52
commit
d53fa2b38c
|
|
@ -40,7 +40,7 @@ const issueService = new IssueService();
|
||||||
const issueArchiveService = new IssueArchiveService();
|
const issueArchiveService = new IssueArchiveService();
|
||||||
|
|
||||||
const basePillClasses =
|
const basePillClasses =
|
||||||
"inline-flex min-h-9 items-center gap-1.5 rounded-full border-0 px-2.5 py-1 text-[11px] font-medium shadow-none outline-none transition-colors";
|
"inline-flex min-h-8 items-center gap-1.5 rounded-full border-0 px-2.5 py-1 text-[10px] font-medium shadow-none outline-none transition-colors";
|
||||||
|
|
||||||
const buildSourceStateMap = (
|
const buildSourceStateMap = (
|
||||||
states: { id: string; name: string; color: string; group: IState["group"] }[] | undefined,
|
states: { id: string; name: string; color: string; group: IState["group"] }[] | undefined,
|
||||||
|
|
@ -130,7 +130,16 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
const pillBackgroundClasses = isActive
|
const pillBackgroundClasses = 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 cornerActionButtonClasses = cn(
|
||||||
|
"flex h-12 w-12 -translate-x-0.5 -translate-y-0.5 items-center justify-center rounded-full border bg-transparent shadow-none ring-0 transition-colors outline-none",
|
||||||
|
isActive
|
||||||
|
? "border-black/25 text-black hover:bg-black/5"
|
||||||
|
: "border-white/20 text-white hover:border-white/35 hover:bg-white/5"
|
||||||
|
);
|
||||||
|
const assigneeButtonClasses = cn(
|
||||||
|
"flex h-7 min-w-7 items-center justify-center rounded-full border-0 bg-transparent p-0 shadow-none outline-none transition-colors",
|
||||||
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white"
|
||||||
|
);
|
||||||
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 canArchive = canEditTargetIssue && !!selectedState && ARCHIVABLE_STATE_GROUPS.includes(selectedState.group);
|
const canArchive = canEditTargetIssue && !!selectedState && ARCHIVABLE_STATE_GROUPS.includes(selectedState.group);
|
||||||
|
|
||||||
|
|
@ -279,162 +288,207 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
onSubmit={handleDeleteRequest}
|
onSubmit={handleDeleteRequest}
|
||||||
/>
|
/>
|
||||||
<div className="group/kanban-block relative mb-2">
|
<div className="group/kanban-block relative mb-2">
|
||||||
<div
|
<div
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
data-priority={issue.priority ?? "none"}
|
data-priority={issue.priority ?? "none"}
|
||||||
className="nodedc-external-card relative flex min-h-[220px] w-full cursor-pointer flex-col p-4 transition-all hover:bg-white/5"
|
className="nodedc-external-card relative flex min-h-[220px] w-full cursor-pointer flex-col p-4 transition-all hover:bg-white/5"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={openDetail}
|
onClick={openDetail}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openDetail();
|
openDetail();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={cn("relative flex min-h-[220px] flex-col px-1", foregroundClasses)}>
|
<div className={cn("relative flex min-h-[220px] flex-col px-1", foregroundClasses)}>
|
||||||
<div className="space-y-0.5">
|
<div className="absolute top-0.5 left-0.5 z-20">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<Avatar
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
src={requesterAvatar}
|
||||||
<div className="shrink-0">
|
name={requester}
|
||||||
<Avatar src={requesterAvatar} name={requester} size="md" />
|
size={48}
|
||||||
</div>
|
className="border border-white/10 shadow-none ring-0 outline-none"
|
||||||
<div className={cn("truncate text-body-sm-medium leading-5", foregroundClasses)}>{requester}</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2" onClick={stopCardPropagation}>
|
<div className="absolute top-0.5 right-0.5 z-20" onClick={stopCardPropagation}>
|
||||||
{request.has_unread_updates && (
|
<ActionDropdown
|
||||||
<span
|
placement="bottom-end"
|
||||||
className={cn("size-2 rounded-full", isActive ? "bg-black/70" : "bg-accent-primary")}
|
button={
|
||||||
title={t("external_contours_page.list.unread_updates")}
|
<div className={cornerActionButtonClasses}>
|
||||||
/>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
)}
|
</div>
|
||||||
|
}
|
||||||
|
buttonClassName="h-12 w-12"
|
||||||
|
menuClassName="min-w-[18rem]"
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (isOpen) void ensureSourceOptions();
|
||||||
|
}}
|
||||||
|
items={[]}
|
||||||
|
menuContent={({ closeDropdown }) => (
|
||||||
|
<div className="max-h-[calc(100vh-2rem)] space-y-2 overflow-y-auto" onClick={stopCardPropagation}>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
|
||||||
|
Приоритет
|
||||||
|
</div>
|
||||||
|
{priorityOptions.map((priority) => (
|
||||||
|
<button
|
||||||
|
key={priority}
|
||||||
|
type="button"
|
||||||
|
className={cn(menuItemClasses, { "bg-white/7 text-primary": issue.priority === priority })}
|
||||||
|
disabled={!canEditCard || isUpdating}
|
||||||
|
onClick={() => {
|
||||||
|
void handleCardUpdate({ priority });
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PriorityIcon priority={priority} className="h-3.5 w-3.5" />
|
||||||
|
<span>{priorityLabels[priority]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ActionDropdown
|
<div className="space-y-1 border-t border-white/8 pt-2">
|
||||||
placement="bottom-end"
|
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
|
||||||
button={<div className={cn("flex h-8 w-8 items-center justify-center rounded-full", iconBubbleClasses)}><MoreHorizontal className="h-4 w-4" /></div>}
|
Статус
|
||||||
buttonClassName="h-8 w-8"
|
</div>
|
||||||
menuClassName="min-w-[18rem]"
|
{isSourceOptionsLoading && stateOptions.length === 0 ? (
|
||||||
onOpenChange={(isOpen) => {
|
<div className="px-2.5 py-2 text-12 text-tertiary">Загрузка статусов...</div>
|
||||||
if (isOpen) void ensureSourceOptions();
|
) : (
|
||||||
}}
|
stateOptions.map((state) => (
|
||||||
items={[]}
|
|
||||||
menuContent={({ closeDropdown }) => (
|
|
||||||
<div className="max-h-[min(75vh,34rem)] space-y-2 overflow-y-auto" onClick={stopCardPropagation}>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">Приоритет</div>
|
|
||||||
{priorityOptions.map((priority) => (
|
|
||||||
<button
|
<button
|
||||||
key={priority}
|
key={state.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(menuItemClasses, { "bg-white/7 text-primary": issue.priority === priority })}
|
className={cn(menuItemClasses, { "bg-white/7 text-primary": issue.state_id === state.id })}
|
||||||
disabled={!canEditCard || isUpdating}
|
disabled={!canEditCard || isUpdating}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleCardUpdate({ priority });
|
void handleCardUpdate({ state_id: state.id });
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PriorityIcon priority={priority} className="h-3.5 w-3.5" />
|
<StateGroupIcon
|
||||||
<span>{priorityLabels[priority]}</span>
|
stateGroup={state.group}
|
||||||
|
color={getStateGroupColor(state.group, state.color)}
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
percentage={state.order}
|
||||||
|
/>
|
||||||
|
<span>{state.name}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="space-y-1 border-t border-white/8 pt-2">
|
|
||||||
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">Статус</div>
|
|
||||||
{isSourceOptionsLoading && stateOptions.length === 0 ? (
|
|
||||||
<div className="px-2.5 py-2 text-12 text-tertiary">Загрузка статусов...</div>
|
|
||||||
) : (
|
|
||||||
stateOptions.map((state) => (
|
|
||||||
<button
|
|
||||||
key={state.id}
|
|
||||||
type="button"
|
|
||||||
className={cn(menuItemClasses, { "bg-white/7 text-primary": issue.state_id === state.id })}
|
|
||||||
disabled={!canEditCard || isUpdating}
|
|
||||||
onClick={() => {
|
|
||||||
void handleCardUpdate({ state_id: state.id });
|
|
||||||
closeDropdown();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={state.group}
|
|
||||||
color={getStateGroupColor(state.group, state.color)}
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
percentage={state.order}
|
|
||||||
/>
|
|
||||||
<span>{state.name}</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1 border-t border-white/8 pt-2">
|
|
||||||
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">Быстрые действия</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={menuItemClasses}
|
|
||||||
onClick={() => {
|
|
||||||
router.push(requestLink);
|
|
||||||
closeDropdown();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
|
||||||
<span>Редактировать</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={menuItemClasses}
|
|
||||||
onClick={() => {
|
|
||||||
void handleCopyLink();
|
|
||||||
closeDropdown();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-3.5 w-3.5" />
|
|
||||||
<span>Копировать ссылку</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={menuItemClasses}
|
|
||||||
disabled={!canArchive || isUpdating}
|
|
||||||
onClick={() => {
|
|
||||||
void handleArchiveIssue();
|
|
||||||
closeDropdown();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Archive className="h-3.5 w-3.5" />
|
|
||||||
<span>Архивировать</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(menuItemClasses, "text-red-300 hover:bg-red-500/10 disabled:text-placeholder")}
|
|
||||||
disabled={direction !== "outgoing" || isUpdating}
|
|
||||||
onClick={() => {
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
closeDropdown();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
|
||||||
<span>Удалить</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
/>
|
<div className="space-y-1 border-t border-white/8 pt-2">
|
||||||
|
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
|
||||||
|
Быстрые действия
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={menuItemClasses}
|
||||||
|
onClick={() => {
|
||||||
|
router.push(requestLink);
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
<span>Редактировать</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={menuItemClasses}
|
||||||
|
onClick={() => {
|
||||||
|
void handleCopyLink();
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
<span>Копировать ссылку</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={menuItemClasses}
|
||||||
|
disabled={!canArchive || isUpdating}
|
||||||
|
onClick={() => {
|
||||||
|
void handleArchiveIssue();
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Archive className="h-3.5 w-3.5" />
|
||||||
|
<span>Архивировать</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(menuItemClasses, "text-red-300 hover:bg-red-500/10 disabled:text-placeholder")}
|
||||||
|
disabled={direction !== "outgoing" || isUpdating}
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
<span>Удалить</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 pr-[58px] pl-[58px] pt-1">
|
||||||
|
<div className="flex min-w-0 items-center gap-1.5">
|
||||||
|
<div className={cn("truncate text-body-sm-medium leading-5", foregroundClasses)}>{requester}</div>
|
||||||
|
{request.has_unread_updates && (
|
||||||
|
<span
|
||||||
|
className={cn("size-2 shrink-0 rounded-full", isActive ? "bg-black/70" : "bg-accent-primary")}
|
||||||
|
title={t("external_contours_page.list.unread_updates")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={cn("truncate text-[10px] leading-3.5 font-medium", subtleTextClasses)}>
|
||||||
|
{counterpartContourName || t("common.none")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("-mt-0.5 truncate pl-8 text-[11px] leading-4 font-medium", subtleTextClasses)}>
|
<div className="flex flex-1 items-center justify-start px-1 pt-7 pb-4 text-left">
|
||||||
{counterpartContourName || t("common.none")}
|
<div className="line-clamp-5 max-w-full text-[15px] leading-5 font-medium">{issue.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center">
|
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
||||||
<div className="text-lg line-clamp-4 max-w-full leading-6 font-semibold">{issue.name}</div>
|
{canEditTargetIssue ? (
|
||||||
</div>
|
<MemberDropdown
|
||||||
|
multiple
|
||||||
|
projectId={issue.project_id ?? undefined}
|
||||||
|
value={issue.assignee_ids ?? []}
|
||||||
|
onChange={(assigneeIds) => void handleCardUpdate({ assignee_ids: assigneeIds })}
|
||||||
|
disabled={!canEditCard || isUpdating}
|
||||||
|
buttonVariant="transparent-without-text"
|
||||||
|
button={
|
||||||
|
<div className={assigneeButtonClasses}>
|
||||||
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size={26} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MemberDropdownBase
|
||||||
|
multiple
|
||||||
|
getUserDetails={getUserDetails}
|
||||||
|
memberIds={targetOptions?.member_ids ?? []}
|
||||||
|
value={issue.assignee_ids ?? []}
|
||||||
|
onChange={(assigneeIds) => void handleCardUpdate({ assignee_ids: assigneeIds })}
|
||||||
|
disabled={!canEditCard || isUpdating || !targetProjectId}
|
||||||
|
onDropdownOpen={() => {
|
||||||
|
void ensureSourceOptions();
|
||||||
|
}}
|
||||||
|
buttonVariant="transparent-without-text"
|
||||||
|
button={
|
||||||
|
<div className={assigneeButtonClasses}>
|
||||||
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size={26} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
|
||||||
{direction === "outgoing" && (
|
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
value={issue.target_date}
|
value={issue.target_date}
|
||||||
rangePreview={{
|
rangePreview={{
|
||||||
|
|
@ -450,73 +504,15 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
buttonVariant="transparent-without-text"
|
buttonVariant="transparent-without-text"
|
||||||
button={
|
button={
|
||||||
<div className={cn(basePillClasses, pillBackgroundClasses)}>
|
<div className={cn(basePillClasses, pillBackgroundClasses)}>
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
<CalendarDays className="h-3 w-3" />
|
||||||
<span className="truncate">{dueDateLabel}</span>
|
<span className="truncate">{dueDateLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{canEditTargetIssue ? (
|
|
||||||
<MemberDropdown
|
|
||||||
multiple
|
|
||||||
projectId={issue.project_id ?? undefined}
|
|
||||||
value={issue.assignee_ids ?? []}
|
|
||||||
onChange={(assigneeIds) => void handleCardUpdate({ assignee_ids: assigneeIds })}
|
|
||||||
disabled={!canEditCard || isUpdating}
|
|
||||||
buttonVariant="transparent-without-text"
|
|
||||||
button={
|
|
||||||
<div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
|
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<MemberDropdownBase
|
|
||||||
multiple
|
|
||||||
getUserDetails={getUserDetails}
|
|
||||||
memberIds={targetOptions?.member_ids ?? []}
|
|
||||||
value={issue.assignee_ids ?? []}
|
|
||||||
onChange={(assigneeIds) => void handleCardUpdate({ assignee_ids: assigneeIds })}
|
|
||||||
disabled={!canEditCard || isUpdating || !targetProjectId}
|
|
||||||
onDropdownOpen={() => {
|
|
||||||
void ensureSourceOptions();
|
|
||||||
}}
|
|
||||||
buttonVariant="transparent-without-text"
|
|
||||||
button={
|
|
||||||
<div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
|
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{direction !== "outgoing" && (
|
|
||||||
<DateDropdown
|
|
||||||
value={issue.target_date}
|
|
||||||
rangePreview={{
|
|
||||||
from: issue.start_date,
|
|
||||||
to: issue.target_date,
|
|
||||||
}}
|
|
||||||
onChange={(targetDate) =>
|
|
||||||
void handleCardUpdate({
|
|
||||||
target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={!canEditCard || isUpdating}
|
|
||||||
buttonVariant="transparent-without-text"
|
|
||||||
button={
|
|
||||||
<div className={cn(basePillClasses, pillBackgroundClasses)}>
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
<span className="truncate">{dueDateLabel}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue