feat(project): 프로젝트 페이지의 내 업무 패널에도 우클릭 메뉴 추가
지난번엔 홈 화면 내 업무 패널만 처리했는데, 프로젝트 상세 페이지 우측 내 업무 패널(.pj-project-mytasks)에도 동일하게 열기/업무 추가/수정/ 완료토글/삭제 메뉴를 추가. "업무 추가"는 openTaskModal(null) 이 항상 현재 열려 있는 프로젝트에만 새 업무를 만들 수 있는 제약이 있어, 업무가 다른 프로젝트 소속이면 그 프로젝트 페이지로 이동시켜 거기서 추가하도록 처리(실제 프로젝트 A/B 두 개로 헤드리스 크롬에서 검증).
This commit is contained in:
@@ -45,5 +45,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/project.js?v=20260917j" defer></script>
|
<script src="/static/project.js?v=20260917k" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -393,5 +393,5 @@
|
|||||||
<script>
|
<script>
|
||||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/project.js?v=20260917j" defer></script>
|
<script src="/static/project.js?v=20260917k" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -461,5 +461,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||||
<script src="/static/project.js?v=20260917j" defer></script>
|
<script src="/static/project.js?v=20260917k" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+88
-3
@@ -1081,15 +1081,100 @@
|
|||||||
const avatars = data.avatars || {};
|
const avatars = data.avatars || {};
|
||||||
const myTasksById = {};
|
const myTasksById = {};
|
||||||
(data.myTasks || []).forEach(function (t) { myTasksById[t.id] = t; });
|
(data.myTasks || []).forEach(function (t) { myTasksById[t.id] = t; });
|
||||||
// 우측 "내 업무" 패널 — openTaskModal 은 이 함수 뒤쪽에서 선언되지만
|
// 우측 "내 업무" 패널 — openTaskModal/mergeTask/refreshActiveView 는 이
|
||||||
// function 선언은 호이스팅되어 여기서 참조해도 문제없다.
|
// 함수 뒤쪽에서 선언되지만 function 선언은 호이스팅되어 여기서 참조해도
|
||||||
document.querySelectorAll(".pj-project-mytasks .pj-task-popup-link").forEach(function (a) {
|
// 문제없다. 이 패널의 업무는 전부 "내가 담당자"인 게 보장돼 있어(서버가
|
||||||
|
// assignee_email 로 조회) 홈 페이지 syncMyTaskPanel 과 달리 재배정 시
|
||||||
|
// 사라지는 경우를 신경 쓸 필요 없다.
|
||||||
|
function myTaskRowHtml(t) {
|
||||||
|
const dates = t.start_date || t.due_date
|
||||||
|
? '<span class="pj-mytree-dates">' +
|
||||||
|
(t.start_date && t.due_date ? fmtDateKr(t.start_date) + " ~ " + fmtDateKr(t.due_date)
|
||||||
|
: t.due_date ? "~ " + fmtDateKr(t.due_date) : fmtDateKr(t.start_date) + " ~") +
|
||||||
|
"</span>"
|
||||||
|
: "";
|
||||||
|
return '<li class="pj-mytree-task' + (t.completed_at ? " is-done" : "") + '" data-task-id="' + t.id + '">' +
|
||||||
|
'<a href="/project/p/' + t.project_id + '?task=' + t.id + '" data-task-id="' + t.id + '" class="pj-task-popup-link">' +
|
||||||
|
'<span class="pj-mytree-lead">' +
|
||||||
|
(t.comment_count
|
||||||
|
? '<span class="pj-cmt-badge" data-task-id="' + t.id + '" data-count="' + t.comment_count + '" title="댓글 ' + t.comment_count + '개">' +
|
||||||
|
'<span class="pj-cmt-ic">💬</span><span class="pj-cmt-n">' + t.comment_count + "</span></span>"
|
||||||
|
: "") +
|
||||||
|
"</span>" +
|
||||||
|
'<span class="pj-mytree-body"><span class="pj-mytree-title">' + escapeHtml(t.title || "") + "</span>" + dates + "</span>" +
|
||||||
|
"</a></li>";
|
||||||
|
}
|
||||||
|
function syncProjectMyTaskPanel(t, removed) {
|
||||||
|
const group = document.querySelector('.pj-project-mytasks .pj-mytree-group[data-group-id="' + t.project_id + '"] .pj-mytree-tasks');
|
||||||
|
if (!group) return; // 이 프로젝트에 내 업무가 하나도 없었으면 다음 새로고침까지 생략.
|
||||||
|
const existingLink = group.querySelector('.pj-task-popup-link[data-task-id="' + t.id + '"]');
|
||||||
|
const existingLi = existingLink ? existingLink.closest("li") : null;
|
||||||
|
if (removed) { if (existingLi) existingLi.remove(); return; }
|
||||||
|
const wrap = document.createElement("div");
|
||||||
|
wrap.innerHTML = myTaskRowHtml(t);
|
||||||
|
const newLi = wrap.firstElementChild;
|
||||||
|
wireMyTaskPopupLink(newLi.querySelector(".pj-task-popup-link"));
|
||||||
|
if (existingLi) existingLi.replaceWith(newLi); else group.appendChild(newLi);
|
||||||
|
}
|
||||||
|
// 내 업무 항목 우클릭 메뉴 — 열기(프로젝트 페이지로 이동)/업무 추가/수정
|
||||||
|
// (팝업 편집)/완료토글/삭제. "업무 추가"는 현재 열려 있는 프로젝트로만
|
||||||
|
// 새 업무를 만들 수 있는 openTaskModal(null) 의 제약 때문에, 이 업무가
|
||||||
|
// 다른 프로젝트 소속이면 그 프로젝트 페이지로 이동시켜 거기서 추가하게 한다.
|
||||||
|
function myTaskContextItems(t) {
|
||||||
|
return [
|
||||||
|
{ label: "열기", onClick: function () { location.href = "/project/p/" + t.project_id + "?task=" + t.id; } },
|
||||||
|
{
|
||||||
|
label: "업무 추가",
|
||||||
|
onClick: function () {
|
||||||
|
if (t.project_id === projectId) openTaskModal(null);
|
||||||
|
else location.href = "/project/p/" + t.project_id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: "수정", onClick: function () { openTaskModal(t); } },
|
||||||
|
{
|
||||||
|
label: t.completed_at ? "완료 취소" : "완료로 표시",
|
||||||
|
onClick: async function () {
|
||||||
|
try {
|
||||||
|
const res = await api("PUT", "/project/api/tasks/" + t.id, { completed: !t.completed_at });
|
||||||
|
mergeTask(res.task);
|
||||||
|
myTasksById[t.id] = Object.assign({}, t, res.task);
|
||||||
|
syncProjectMyTaskPanel(myTasksById[t.id]);
|
||||||
|
refreshActiveView();
|
||||||
|
} catch (e) { showToast("처리 실패: " + e.message, "error"); }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
label: "삭제", danger: true,
|
||||||
|
onClick: async function () {
|
||||||
|
if (!canDelete(t.created_by)) { alert(DELETE_DENIED); return; }
|
||||||
|
if (!confirm("이 업무를 삭제할까요?")) return;
|
||||||
|
try {
|
||||||
|
await api("DELETE", "/project/api/tasks/" + t.id);
|
||||||
|
tasks = tasks.filter(function (x) { return x.id !== t.id; });
|
||||||
|
delete myTasksById[t.id];
|
||||||
|
syncProjectMyTaskPanel(t, true);
|
||||||
|
refreshActiveView();
|
||||||
|
} catch (e) { showToast("삭제 실패: " + e.message, "error"); }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function wireMyTaskPopupLink(a) {
|
||||||
a.addEventListener("click", function (e) {
|
a.addEventListener("click", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const t = myTasksById[a.dataset.taskId];
|
const t = myTasksById[a.dataset.taskId];
|
||||||
if (t) openTaskModal(t);
|
if (t) openTaskModal(t);
|
||||||
});
|
});
|
||||||
|
a.addEventListener("contextmenu", function (e) {
|
||||||
|
const t = myTasksById[a.dataset.taskId];
|
||||||
|
if (!t) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
showContextMenu(e.clientX, e.clientY, myTaskContextItems(t));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
document.querySelectorAll(".pj-project-mytasks .pj-task-popup-link").forEach(function (a) { wireMyTaskPopupLink(a); });
|
||||||
// 담당자/작성자 아이콘 = 구글 프로필 이미지(원형). 없으면 기본 아이콘.
|
// 담당자/작성자 아이콘 = 구글 프로필 이미지(원형). 없으면 기본 아이콘.
|
||||||
function avatarHtml(email, cls) {
|
function avatarHtml(email, cls) {
|
||||||
const url = email ? avatars[String(email).toLowerCase()] : "";
|
const url = email ? avatars[String(email).toLowerCase()] : "";
|
||||||
|
|||||||
Reference in New Issue
Block a user