last ...
This commit is contained in:
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"environments": [
|
||||
{
|
||||
"id": "env-1789757606914",
|
||||
"name": "کاوش - توسعه",
|
||||
"description": "این محیط فقط برای تست عملیاتی توسعه دهندگان است",
|
||||
"backgroundColor": "#3f94ee",
|
||||
"textColor": "#ffffff"
|
||||
},
|
||||
{
|
||||
"id": "env-1789757658261",
|
||||
"name": "کاوش تست",
|
||||
"description": "این محیط برای تست محصول توسط تیم های تضمین کیفیت و تحلیل است",
|
||||
"backgroundColor": "#df3416",
|
||||
"textColor": "#ffffff"
|
||||
},
|
||||
{
|
||||
"id": "env-1789757730121",
|
||||
"name": "کانون - قرمز",
|
||||
"description": "محیطی برای تست محصولات در فضای کانون",
|
||||
"backgroundColor": "#ff0000",
|
||||
"textColor": "#ffffff"
|
||||
},
|
||||
{
|
||||
"id": "env-1789757791901",
|
||||
"name": "کانون - آبی",
|
||||
"description": "محیط انتشار عمومی محصولات در کانون",
|
||||
"backgroundColor": "#0000ff",
|
||||
"textColor": "#ffffff"
|
||||
},
|
||||
{
|
||||
"id": "env-1789757853333",
|
||||
"name": "سازمان - قرمز",
|
||||
"description": "محیطی برای تست محصولات در سازمان",
|
||||
"backgroundColor": "#ff0000",
|
||||
"textColor": "#ffffff"
|
||||
},
|
||||
{
|
||||
"id": "env-1789757900213",
|
||||
"name": "سازمان - آبی",
|
||||
"description": "محیط انتشار عمومی محصولات در سازمان",
|
||||
"backgroundColor": "#0000ff",
|
||||
"textColor": "#ffffff"
|
||||
}
|
||||
],
|
||||
"products": [
|
||||
{
|
||||
"id": "prod-1789906178306",
|
||||
"title": "زیرساخت",
|
||||
"description": "کلیه پروژه هایی که بعنوان زیرساخت برای سایر محصولات شرکت مورد استفاده قرار می گیرند ...",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:09:38.306Z",
|
||||
"owner": "تیم آتنا",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789906450942",
|
||||
"title": "کنترل سنتر",
|
||||
"description": "مدیریت تنظیمات و ارائه اطلاعات پایه برای پروژه های مختلف ...",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:14:10.942Z",
|
||||
"owner": "تیم آتنا",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789906580440",
|
||||
"title": "مانا",
|
||||
"description": "مدیریت ارجاع نقشه برداران ایران",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:16:20.440Z",
|
||||
"owner": "تیم کاداستر",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789906612889",
|
||||
"title": "مدام",
|
||||
"description": "مدیریت داده های مکانی",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:16:52.889Z",
|
||||
"owner": "تیم کاداستر",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789906649432",
|
||||
"title": "مدیریت قرارداد ها",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:17:29.432Z",
|
||||
"owner": "تیم کاداستر",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907070679",
|
||||
"title": "پیام رسان",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:24:30.679Z",
|
||||
"owner": "تیم کاداستر",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907660064",
|
||||
"title": "تفکیک آپارتمانی - کاداستر",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:34:20.064Z",
|
||||
"owner": "تیم کاداستر",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907676416",
|
||||
"title": "ابزار WebGIS",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:34:36.416Z",
|
||||
"owner": "تیم کاداستر",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907707481",
|
||||
"title": "سرویس های املاک",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:35:07.481Z",
|
||||
"owner": "تیم کاداستر",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907737585",
|
||||
"title": "ساماندهی",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:35:37.585Z",
|
||||
"owner": "تیم املاک",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907760114",
|
||||
"title": "تعیین تکلیف",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:36:00.114Z",
|
||||
"owner": "تیم املاک",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907772450",
|
||||
"title": "دفترچه ای",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:36:12.450Z",
|
||||
"owner": "تیم املاک",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907792986",
|
||||
"title": "تفکیک آپارتمانی",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:36:32.986Z",
|
||||
"owner": "تیم املاک",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907850075",
|
||||
"title": "ثبت اطلاعات سامانه املاک",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:37:30.075Z",
|
||||
"owner": "تیم املاک",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907867139",
|
||||
"title": "املاک من",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:37:47.139Z",
|
||||
"owner": "تیم املاک",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
},
|
||||
{
|
||||
"id": "prod-1789907887203",
|
||||
"title": "شروع از کارگزاری",
|
||||
"description": "",
|
||||
"metadata": {
|
||||
"createdAt": "2026-09-20T12:38:07.203Z",
|
||||
"owner": "تیم املاک",
|
||||
"customFields": {}
|
||||
},
|
||||
"projects": []
|
||||
}
|
||||
],
|
||||
"boards": [],
|
||||
"boardTags": []
|
||||
}
|
||||
+279
-85
@@ -3017,6 +3017,12 @@
|
||||
<option value="">همه محیطها</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-dropdown">
|
||||
<span>👤</span>
|
||||
<select class="filter-select" id="filterOwner">
|
||||
<option value="">همه مالکان</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-stats">
|
||||
<div class="stat-item">
|
||||
@@ -3296,32 +3302,38 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">عنوان <span class="required">*</span></label><input type="text" class="form-input"
|
||||
id="newProjectTitle" />
|
||||
<label class="form-label">عنوان <span class="required">*</span></label>
|
||||
<input type="text" class="form-input" id="newProjectTitle" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">نوع <span class="required">*</span></label><select class="form-select"
|
||||
id="newProjectType">
|
||||
<label class="form-label">نوع <span class="required">*</span></label>
|
||||
<select class="form-select" id="newProjectType">
|
||||
<option value="">انتخاب کنید...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">فریمورک</label><input type="text" class="form-input" id="newProjectFramework" />
|
||||
<label class="form-label">فریمورک</label>
|
||||
<input type="text" class="form-input" id="newProjectFramework" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">زبان</label><input type="text" class="form-input" id="newProjectLanguage" />
|
||||
<label class="form-label">مالک پروژه</label>
|
||||
<input type="text" class="form-input" id="newProjectOwner" placeholder="نام مسئول پروژه..." />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="newProjectRepo"
|
||||
dir="ltr" />
|
||||
<label class="form-label">زبان</label>
|
||||
<input type="text" class="form-input" id="newProjectLanguage" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label><textarea class="form-textarea"
|
||||
id="newProjectDescription"></textarea>
|
||||
<label class="form-label">آدرس مخزن</label>
|
||||
<input type="text" class="form-input" id="newProjectRepo" dir="ltr" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label>
|
||||
<textarea class="form-textarea" id="newProjectDescription"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -3338,35 +3350,42 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">شناسه <span class="readonly-badge">فقط خواندنی</span></label><input type="text"
|
||||
class="form-input" id="editProjId" readonly />
|
||||
<label class="form-label">شناسه <span class="readonly-badge">فقط خواندنی</span></label>
|
||||
<input type="text" class="form-input" id="editProjId" readonly />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">عنوان <span class="required">*</span></label><input type="text" class="form-input"
|
||||
id="editProjTitle" />
|
||||
<label class="form-label">عنوان <span class="required">*</span></label>
|
||||
<input type="text" class="form-input" id="editProjTitle" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">نوع <span class="required">*</span></label><select class="form-select"
|
||||
id="editProjType">
|
||||
<label class="form-label">نوع <span class="required">*</span></label>
|
||||
<select class="form-select" id="editProjType">
|
||||
<option value="">انتخاب کنید...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">فریمورک</label><input type="text" class="form-input" id="editProjFramework" />
|
||||
<label class="form-label">فریمورک</label>
|
||||
<input type="text" class="form-input" id="editProjFramework" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">زبان</label><input type="text" class="form-input" id="editProjLanguage" />
|
||||
<label class="form-label">مالک پروژه</label>
|
||||
<input type="text" class="form-input" id="editProjOwner" placeholder="نام مسئول پروژه..." />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="editProjRepo"
|
||||
dir="ltr" />
|
||||
<label class="form-label">زبان</label>
|
||||
<input type="text" class="form-input" id="editProjLanguage" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label><textarea class="form-textarea" id="editProjDescription"></textarea>
|
||||
<label class="form-label">آدرس مخزن</label>
|
||||
<input type="text" class="form-input" id="editProjRepo" dir="ltr" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label>
|
||||
<textarea class="form-textarea" id="editProjDescription"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -3486,8 +3505,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="galaxy-stats" id="galaxyStats"></div>
|
||||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||||
<div class="galaxy-stats" id="galaxyStats"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3505,8 +3524,8 @@
|
||||
<div id="dashboardContainer" class="dashboard-container"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="dashboard-footer-stats" id="dashboardFooterStats"></div>
|
||||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||||
<div class="dashboard-footer-stats" id="dashboardFooterStats"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3524,8 +3543,8 @@
|
||||
<div id="dependencyAnalysisContainer" class="dashboard-container"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="dashboard-footer-stats" id="depAnalysisFooterStats"></div>
|
||||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||||
<div class="dashboard-footer-stats" id="depAnalysisFooterStats"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3543,8 +3562,8 @@
|
||||
<div id="deploymentAnalysisContainer" class="dashboard-container"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="dashboard-footer-stats" id="deployAnalysisFooterStats"></div>
|
||||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||||
<div class="dashboard-footer-stats" id="deployAnalysisFooterStats"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3581,8 +3600,8 @@
|
||||
<div id="riskAnalysisContainer" class="dashboard-container"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="dashboard-footer-stats" id="riskAnalysisFooterStats"></div>
|
||||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||||
<div class="dashboard-footer-stats" id="riskAnalysisFooterStats"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4283,9 +4302,10 @@
|
||||
};
|
||||
/* ==================== TOOLBAR MANAGER ==================== */
|
||||
const ToolbarManager = {
|
||||
currentFilters: { query: "", typeFilter: "", envFilter: "" },
|
||||
currentFilters: { query: "", typeFilter: "", envFilter: "", ownerFilter: "" },
|
||||
init() {
|
||||
this.populateEnvironmentFilter();
|
||||
this.populateOwnerFilter();
|
||||
this.bindEvents();
|
||||
this.updateStats();
|
||||
},
|
||||
@@ -4298,10 +4318,28 @@
|
||||
.map((e) => `<option value="${e.id}">${e.name}</option>`)
|
||||
.join("");
|
||||
},
|
||||
populateOwnerFilter() {
|
||||
const s = document.getElementById("filterOwner");
|
||||
if (!s) return;
|
||||
const owners = new Set();
|
||||
(projectData.appData.products || []).forEach((p) => {
|
||||
const owner = p.metadata?.owner?.trim();
|
||||
if (owner) owners.add(owner);
|
||||
});
|
||||
const sortedOwners = Array.from(owners).sort((a, b) =>
|
||||
a.localeCompare(b, "fa"),
|
||||
);
|
||||
s.innerHTML =
|
||||
'<option value="">همه مالکان</option>' +
|
||||
sortedOwners
|
||||
.map((o) => `<option value="${o}">${o}</option>`)
|
||||
.join("");
|
||||
},
|
||||
bindEvents() {
|
||||
const si = document.getElementById("toolbarSearch"),
|
||||
ft = document.getElementById("filterType"),
|
||||
fe = document.getElementById("filterEnvironment");
|
||||
fe = document.getElementById("filterEnvironment"),
|
||||
fo = document.getElementById("filterOwner");
|
||||
if (si)
|
||||
si.addEventListener("input", () => {
|
||||
this.applyFilters();
|
||||
@@ -4317,6 +4355,11 @@
|
||||
this.applyFilters();
|
||||
refreshUI();
|
||||
});
|
||||
if (fo)
|
||||
fo.addEventListener("change", () => {
|
||||
this.applyFilters();
|
||||
refreshUI();
|
||||
});
|
||||
},
|
||||
applyFilters() {
|
||||
this.currentFilters = {
|
||||
@@ -4324,8 +4367,8 @@
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
typeFilter: document.getElementById("filterType")?.value || "",
|
||||
envFilter:
|
||||
document.getElementById("filterEnvironment")?.value || "",
|
||||
envFilter: document.getElementById("filterEnvironment")?.value || "",
|
||||
ownerFilter: document.getElementById("filterOwner")?.value || "",
|
||||
};
|
||||
this.updateStats();
|
||||
},
|
||||
@@ -4350,7 +4393,15 @@
|
||||
},
|
||||
shouldShowProduct(product) {
|
||||
const f = this.currentFilters;
|
||||
if (!f.query && !f.typeFilter && !f.envFilter && !f.ownerFilter) return true;
|
||||
|
||||
if (f.ownerFilter) {
|
||||
const prodOwner = product.metadata?.owner?.trim();
|
||||
if (prodOwner !== f.ownerFilter) return false;
|
||||
}
|
||||
|
||||
if (!f.query && !f.typeFilter && !f.envFilter) return true;
|
||||
|
||||
return (product.projects || []).some((pr) => {
|
||||
const mq =
|
||||
!f.query ||
|
||||
@@ -4362,6 +4413,7 @@
|
||||
(pr.deployments || [])
|
||||
.filter((d) => d.status === "active")
|
||||
.some((d) => d.environmentId === f.envFilter);
|
||||
|
||||
return mq && mt && me;
|
||||
});
|
||||
},
|
||||
@@ -4891,6 +4943,7 @@
|
||||
updateProjectUI();
|
||||
renderToolGroups();
|
||||
ToolbarManager.populateEnvironmentFilter();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
refreshUI();
|
||||
showToast("📂 بارگذاری شد", "success");
|
||||
} catch (err) {
|
||||
@@ -4913,6 +4966,7 @@
|
||||
};
|
||||
AutosaveManager.clear();
|
||||
ToolbarManager.populateEnvironmentFilter();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
refreshUI();
|
||||
showToast("🗑️ پاکسازی شد", "success");
|
||||
},
|
||||
@@ -5132,7 +5186,13 @@
|
||||
return null;
|
||||
},
|
||||
getActiveDeployments(proj) {
|
||||
return (proj.deployments || []).filter((d) => d.status === "active");
|
||||
return (proj.deployments || [])
|
||||
.filter((d) => d.status === "active")
|
||||
.sort((a, b) => {
|
||||
const dateA = a.deploymentDate ? new Date(a.deploymentDate).getTime() : 0;
|
||||
const dateB = b.deploymentDate ? new Date(b.deploymentDate).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
},
|
||||
getEnvironmentsWithoutDeployment(proj) {
|
||||
const allEnvIds = projectData.appData.environments.map((e) => e.id);
|
||||
@@ -5154,7 +5214,9 @@
|
||||
(s, pr) => s + this.getActiveDeployments(pr).length,
|
||||
0,
|
||||
);
|
||||
|
||||
let html = `<div class="detail-section"><div class="detail-section-header"><span>ℹ️ اطلاعات محصول</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openProductEditModal()">✏️ ویرایش</button></div></div><div class="detail-section-body"><div class="detail-info-grid"><div class="detail-info-item"><span class="detail-info-label">عنوان</span><span class="detail-info-value">${prod.title}</span></div><div class="detail-info-item"><span class="detail-info-label">توضیحات</span><span class="detail-info-value">${prod.description || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تاریخ ایجاد</span><span class="detail-info-value">${prod.metadata?.createdAt ? new Date(prod.metadata.createdAt).toLocaleDateString("fa-IR") : "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">مالک</span><span class="detail-info-value">${prod.metadata?.owner || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تعداد پروژه</span><span class="detail-info-value">${projects.length}</span></div><div class="detail-info-item"><span class="detail-info-label">استقرارهای فعال</span><span class="detail-info-value">${totalDeploys}</span></div></div></div></div><div class="detail-section"><div class="detail-section-header"><span>🌿 پروژهها (${projects.length})</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openAddProjectModal()">➕ افزودن</button></div></div><div class="detail-section-body">`;
|
||||
|
||||
if (projects.length === 0) {
|
||||
html += '<div class="empty-subprojects">🌿 هیچ پروژهای نیست</div>';
|
||||
} else {
|
||||
@@ -5163,21 +5225,43 @@
|
||||
const ti = this.getTypeInfo(pr.type);
|
||||
const deps = pr.dependencies || [];
|
||||
const deploys = this.getActiveDeployments(pr);
|
||||
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
|
||||
const canAdd = emptyEnvs.length > 0;
|
||||
html += `<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${ti.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${ti.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openDependencyManager('${prod.id}','${pr.id}')">🔗 وابستگیها</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}','${pr.id}')">✏️</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}','${pr.id}')">🗑️</button></div></div><div class="subproject-meta">${pr.description || ""}</div><div class="detail-info-grid" style="margin-bottom:8px">${pr.metadata?.framework ? `<div class="detail-info-item"><span class="detail-info-label">فریمورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>` : ""}${pr.metadata?.language ? `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>` : ""}${deps.length > 0
|
||||
? `<div class="detail-info-item"><span class="detail-info-label">وابستگیها (${deps.length})</span><span class="detail-info-value">${deps
|
||||
const archivedDeploys = (pr.deployments || [])
|
||||
.filter((d) => d.status === "archived")
|
||||
.sort((a, b) => {
|
||||
const dateA = a.archivedAt ? new Date(a.archivedAt).getTime() : 0;
|
||||
const dateB = b.archivedAt ? new Date(b.archivedAt).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
const hasEnvs = projectData.appData.environments.length > 0;
|
||||
|
||||
html += `<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${ti.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${ti.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openDependencyManager('${prod.id}','${pr.id}')">🔗 وابستگیها</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}','${pr.id}')">✏️</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}','${pr.id}')">🗑️</button></div></div><div class="subproject-meta">${pr.description || ""}</div><div class="detail-info-grid" style="margin-bottom:8px">`;
|
||||
|
||||
if (pr.metadata?.owner) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">👤 مالک</span><span class="detail-info-value">${pr.metadata.owner}</span></div>`;
|
||||
}
|
||||
|
||||
if (pr.metadata?.framework) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">فریمورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>`;
|
||||
}
|
||||
|
||||
if (pr.metadata?.language) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>`;
|
||||
}
|
||||
|
||||
if (deps.length > 0) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">وابستگیها (${deps.length})</span><span class="detail-info-value">${deps
|
||||
.map((depId) => {
|
||||
const dp = this.findProjectById(depId);
|
||||
return dp ? dp.title : depId;
|
||||
})
|
||||
.join("، ")}</span></div>`
|
||||
: ""
|
||||
}</div>`;
|
||||
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${canAdd ? `<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}','${pr.id}')">➕ افزودن</button>` : '<span style="font-size:10px;color:var(--text-muted)">✓ کامل</span>'}</div>`;
|
||||
.join("، ")}</span></div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${hasEnvs ? `<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}','${pr.id}')">➕ افزودن</button>` : '<span style="font-size:10px;color:var(--text-muted)">⚠️ محیطی تعریف نشده</span>'}</div>`;
|
||||
|
||||
if (deploys.length === 0) {
|
||||
html +=
|
||||
'<div class="empty-deployments">هنوز استقرار ندارد</div>';
|
||||
html += '<div class="empty-deployments">هنوز استقرار ندارد</div>';
|
||||
} else {
|
||||
html += '<div class="deployment-list">';
|
||||
deploys.forEach((d) => {
|
||||
@@ -5186,13 +5270,22 @@
|
||||
const fd = d.deploymentDate
|
||||
? new Date(d.deploymentDate).toLocaleString("fa-IR")
|
||||
: "—";
|
||||
html += `<div class="deployment-item"><div class="deployment-item-info"><span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${en}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${fd}</span></div><div class="deployment-actions"><button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')">✏️</button><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')">🗑️</button></div>`;
|
||||
if (d.releaseNotes)
|
||||
|
||||
html += `<div class="deployment-item"><div class="deployment-item-info"><span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${en}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${fd}</span></div><div class="deployment-actions">`;
|
||||
|
||||
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')" title="ویرایش">✏️</button>`;
|
||||
|
||||
html += `<button class="btn btn-xs" style="background:#dd6b20;color:#fff" onclick="ProductManager.archiveDeployment('${prod.id}','${pr.id}','${d.id}')" title="انتقال به تاریخچه">📦 آرشیو</button>`;
|
||||
|
||||
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
if (d.releaseNotes) {
|
||||
html += `<div class="deployment-notes">📝 ${d.releaseNotes}</div>`;
|
||||
if (
|
||||
d.requiredDeployments &&
|
||||
d.requiredDeployments.length > 0
|
||||
) {
|
||||
}
|
||||
|
||||
if (d.requiredDeployments && d.requiredDeployments.length > 0) {
|
||||
html += `<div class="deployment-req">🔗 نیازمندیها: ${d.requiredDeployments
|
||||
.map((rd) => {
|
||||
const rp = this.findProjectById(rd.projectId);
|
||||
@@ -5207,14 +5300,62 @@
|
||||
})
|
||||
.join("، ")}</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
html += `<div class="deployment-section" style="margin-top:12px;border-top:2px dashed var(--border)">`;
|
||||
html += `<div class="deployment-header">`;
|
||||
html += `<span class="deployment-header-title">📜 تاریخچه استقرارها (${archivedDeploys.length})</span>`;
|
||||
if (archivedDeploys.length > 0) {
|
||||
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.toggleArchiveVisibility('${pr.id}')" id="archiveToggle-${pr.id}">👁️ نمایش/مخفی</button>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
if (archivedDeploys.length === 0) {
|
||||
html += `<div class="empty-deployments" style="opacity:0.6">هنوز استقراری آرشیو نشده است</div>`;
|
||||
} else {
|
||||
html += `<div class="deployment-list" id="archiveList-${pr.id}" style="display:none">`;
|
||||
archivedDeploys.forEach((d) => {
|
||||
const en = this.getEnvironmentName(d.environmentId);
|
||||
const cl = this.getEnvironmentColors(d.environmentId);
|
||||
const fd = d.deploymentDate
|
||||
? new Date(d.deploymentDate).toLocaleString("fa-IR")
|
||||
: "—";
|
||||
const archivedDate = d.archivedAt
|
||||
? new Date(d.archivedAt).toLocaleString("fa-IR")
|
||||
: "—";
|
||||
|
||||
html += `<div class="deployment-item" style="opacity:0.75;background:var(--border-light)">`;
|
||||
html += `<div class="deployment-item-info">`;
|
||||
html += `<span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor};opacity:0.7">${en}</span>`;
|
||||
html += `<span class="deployment-version" style="opacity:0.7">v${d.version}</span>`;
|
||||
html += `<span class="deployment-date">📅 ${fd}</span>`;
|
||||
html += `<span class="deployment-date" style="color:var(--warning)">📦 آرشیو: ${archivedDate}</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="deployment-actions">`;
|
||||
html += `<button class="btn btn-xs" style="background:#3182ce;color:#fff" onclick="ProductManager.restoreDeployment('${prod.id}','${pr.id}','${d.id}')" title="بازگرداندن به لیست فعال">↩️ بازگردانی</button>`;
|
||||
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
|
||||
html += `</div>`;
|
||||
|
||||
if (d.releaseNotes) {
|
||||
html += `<div class="deployment-notes" style="opacity:0.7">📝 ${d.releaseNotes}</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
html += `</div></div>`;
|
||||
});
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
html += `</div></div>`;
|
||||
document.getElementById("projectDetailBody").innerHTML = html;
|
||||
ModalManager.open("projectDetailModal");
|
||||
@@ -5259,6 +5400,7 @@
|
||||
});
|
||||
ModalManager.close("addProductModal");
|
||||
refreshUI();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
showToast(`"${title}" ایجاد شد`, "success");
|
||||
},
|
||||
openDependencyManager(productId, projId) {
|
||||
@@ -5449,6 +5591,7 @@
|
||||
prod.metadata.updatedAt = new Date().toISOString();
|
||||
ModalManager.close("productEditModal");
|
||||
refreshUI();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
this.showDetail(pid);
|
||||
showToast(`"${prod.title}" ویرایش شد`, "success");
|
||||
},
|
||||
@@ -5460,6 +5603,7 @@
|
||||
document.getElementById("newProjectTitle").value = "";
|
||||
document.getElementById("newProjectType").value = "";
|
||||
document.getElementById("newProjectFramework").value = "";
|
||||
document.getElementById("newProjectOwner").value = ""; // 🆕
|
||||
document.getElementById("newProjectLanguage").value = "";
|
||||
document.getElementById("newProjectRepo").value = "";
|
||||
document.getElementById("newProjectDescription").value = "";
|
||||
@@ -5482,20 +5626,18 @@
|
||||
const np = {
|
||||
id: `proj-${Date.now()}`,
|
||||
title,
|
||||
description:
|
||||
document.getElementById("newProjectDescription").value.trim() ||
|
||||
"",
|
||||
description: document.getElementById("newProjectDescription").value.trim() || "",
|
||||
type,
|
||||
repositoryUrl: document
|
||||
.getElementById("newProjectRepo")
|
||||
.value.trim(),
|
||||
metadata: { createdAt: new Date().toISOString(), customFields: {} },
|
||||
repositoryUrl: document.getElementById("newProjectRepo").value.trim(),
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
owner: document.getElementById("newProjectOwner").value.trim() || "", // 🆕
|
||||
customFields: {}
|
||||
},
|
||||
dependencies: [],
|
||||
deployments: [],
|
||||
};
|
||||
const fw = document
|
||||
.getElementById("newProjectFramework")
|
||||
.value.trim();
|
||||
const fw = document.getElementById("newProjectFramework").value.trim();
|
||||
if (fw) np.metadata.framework = fw;
|
||||
const lg = document.getElementById("newProjectLanguage").value.trim();
|
||||
if (lg) np.metadata.language = lg;
|
||||
@@ -5506,9 +5648,7 @@
|
||||
showToast(`"${title}" ایجاد شد`, "success");
|
||||
},
|
||||
openProjectEditModal(productId, projId) {
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
const prod = projectData.appData.products.find((p) => p.id === productId);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
@@ -5517,14 +5657,11 @@
|
||||
document.getElementById("editProjId").value = pr.id;
|
||||
document.getElementById("editProjTitle").value = pr.title;
|
||||
document.getElementById("editProjType").value = pr.type;
|
||||
document.getElementById("editProjFramework").value =
|
||||
pr.metadata?.framework || "";
|
||||
document.getElementById("editProjLanguage").value =
|
||||
pr.metadata?.language || "";
|
||||
document.getElementById("editProjRepo").value =
|
||||
pr.repositoryUrl || "";
|
||||
document.getElementById("editProjDescription").value =
|
||||
pr.description || "";
|
||||
document.getElementById("editProjFramework").value = pr.metadata?.framework || "";
|
||||
document.getElementById("editProjOwner").value = pr.metadata?.owner || ""; // 🆕
|
||||
document.getElementById("editProjLanguage").value = pr.metadata?.language || "";
|
||||
document.getElementById("editProjRepo").value = pr.repositoryUrl || "";
|
||||
document.getElementById("editProjDescription").value = pr.description || "";
|
||||
ModalManager.open("editProjectModal");
|
||||
},
|
||||
saveProjectEdit() {
|
||||
@@ -5545,15 +5682,15 @@
|
||||
AutosaveManager.scheduleSave();
|
||||
pr.title = nt;
|
||||
pr.type = nty;
|
||||
pr.description =
|
||||
document.getElementById("editProjDescription").value.trim() || "";
|
||||
pr.repositoryUrl = document
|
||||
.getElementById("editProjRepo")
|
||||
.value.trim();
|
||||
pr.description = document.getElementById("editProjDescription").value.trim() || "";
|
||||
pr.repositoryUrl = document.getElementById("editProjRepo").value.trim();
|
||||
if (!pr.metadata) pr.metadata = {};
|
||||
const fw = document.getElementById("editProjFramework").value.trim();
|
||||
if (fw) pr.metadata.framework = fw;
|
||||
else delete pr.metadata.framework;
|
||||
const owner = document.getElementById("editProjOwner").value.trim(); // 🆕
|
||||
if (owner) pr.metadata.owner = owner;
|
||||
else delete pr.metadata.owner;
|
||||
const lg = document.getElementById("editProjLanguage").value.trim();
|
||||
if (lg) pr.metadata.language = lg;
|
||||
else delete pr.metadata.language;
|
||||
@@ -5565,36 +5702,39 @@
|
||||
},
|
||||
openAddDeploymentModal(productId, projId) {
|
||||
if (!productId || !projId) return;
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
const prod = projectData.appData.products.find((p) => p.id === productId);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
|
||||
if (emptyEnvs.length === 0) {
|
||||
showToast("همه محیطها استقرار دارند", "warning");
|
||||
|
||||
const allEnvs = projectData.appData.environments;
|
||||
if (allEnvs.length === 0) {
|
||||
showToast("ابتدا یک محیط عملیاتی ایجاد کنید", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentDeploymentProjId = projId;
|
||||
this.currentDeploymentMode = "add";
|
||||
this.currentDeploymentEnvId = null;
|
||||
this.currentEditDeploymentId = null;
|
||||
|
||||
document.getElementById("deploymentModalTitle").innerHTML =
|
||||
"<span>🚀</span><span>افزودن نسخه استقرار</span>";
|
||||
|
||||
const envGroup = document.getElementById("deploymentEnvGroup");
|
||||
const envs = projectData.appData.environments.filter((e) =>
|
||||
emptyEnvs.includes(e.id),
|
||||
);
|
||||
const envs = allEnvs;
|
||||
|
||||
envGroup.innerHTML = `<label class="form-label">محیط استقرار <span class="required">*</span></label><select class="form-select" id="depEnvironment" onchange="ProductManager.onEnvChange()">${envs
|
||||
.map((e) => {
|
||||
const cl = this.getEnvironmentColors(e.id);
|
||||
return `<option value="${e.id}" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${e.name} - ${e.description}</option>`;
|
||||
})
|
||||
.join("")}</select>`;
|
||||
|
||||
document.getElementById("depVersion").value = "";
|
||||
document.getElementById("depDate").value = "";
|
||||
document.getElementById("depReleaseNotes").value = "";
|
||||
|
||||
this.renderRequiredDeploymentsSection();
|
||||
ModalManager.open("deploymentModal");
|
||||
},
|
||||
@@ -5762,7 +5902,26 @@
|
||||
this.showDetail(pid);
|
||||
},
|
||||
removeDeployment(productId, projId, depId) {
|
||||
if (!confirm("حذف این استقرار؟")) return;
|
||||
if (!confirm("⚠️ حذف کامل این استقرار؟\n\nاین عمل غیرقابل بازگشت است.\nاگر میخواهید فقط از لیست فعال خارج شود، از دکمه «آرشیو» استفاده کنید.")) return;
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
|
||||
HistoryManager.pushState();
|
||||
AutosaveManager.scheduleSave();
|
||||
|
||||
pr.deployments = (pr.deployments || []).filter((d) => d.id !== depId);
|
||||
|
||||
refreshUI();
|
||||
this.showDetail(productId);
|
||||
showToast("🗑️ استقرار بهطور کامل حذف شد", "warning");
|
||||
},
|
||||
archiveDeployment(productId, projId, depId) {
|
||||
if (!confirm("📦 انتقال این استقرار به تاریخچه؟\n\nاستقرار از لیست فعال خارج شده و در بخش تاریخچه نگهداری میشود.")) return;
|
||||
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
@@ -5771,12 +5930,16 @@
|
||||
if (!pr) return;
|
||||
const dep = (pr.deployments || []).find((d) => d.id === depId);
|
||||
if (!dep) return;
|
||||
|
||||
HistoryManager.pushState();
|
||||
AutosaveManager.scheduleSave();
|
||||
dep.status = "removed";
|
||||
|
||||
dep.status = "archived";
|
||||
dep.archivedAt = new Date().toISOString();
|
||||
|
||||
refreshUI();
|
||||
this.showDetail(productId);
|
||||
showToast("استقرار حذف شد", "warning");
|
||||
showToast("📦 استقرار به تاریخچه منتقل شد", "success");
|
||||
},
|
||||
removeProduct(productId) {
|
||||
const prod = projectData.appData.products.find(
|
||||
@@ -5790,6 +5953,7 @@
|
||||
(p) => p.id !== productId,
|
||||
);
|
||||
refreshUI();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
ModalManager.close("projectDetailModal");
|
||||
showToast(`"${prod.title}" حذف شد`, "warning");
|
||||
},
|
||||
@@ -5809,6 +5973,35 @@
|
||||
this.showDetail(productId);
|
||||
showToast(`"${pr.title}" حذف شد`, "warning");
|
||||
},
|
||||
toggleArchiveVisibility(projId) {
|
||||
const list = document.getElementById(`archiveList-${projId}`);
|
||||
if (!list) return;
|
||||
const isHidden = list.style.display === "none";
|
||||
list.style.display = isHidden ? "flex" : "none";
|
||||
},
|
||||
restoreDeployment(productId, projId, depId) {
|
||||
if (!confirm("↩️ بازگرداندن این استقرار به لیست فعال؟")) return;
|
||||
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
const dep = (pr.deployments || []).find((d) => d.id === depId);
|
||||
if (!dep) return;
|
||||
|
||||
HistoryManager.pushState();
|
||||
AutosaveManager.scheduleSave();
|
||||
|
||||
// ✅ بازگرداندن به وضعیت active
|
||||
dep.status = "active";
|
||||
delete dep.archivedAt;
|
||||
|
||||
refreshUI();
|
||||
this.showDetail(productId);
|
||||
showToast("↩️ استقرار به لیست فعال بازگردانده شد", "success");
|
||||
},
|
||||
};
|
||||
/* ==================== DASHBOARD MANAGER ==================== */
|
||||
const DashboardManager = {
|
||||
@@ -6710,6 +6903,7 @@
|
||||
updateProjectUI();
|
||||
renderToolGroups();
|
||||
ToolbarManager.populateEnvironmentFilter();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
refreshUI();
|
||||
if (ReleaseBoardsManager) {
|
||||
ReleaseBoardsManager.render(true);
|
||||
|
||||
+368
-85
@@ -3017,6 +3017,12 @@
|
||||
<option value="">همه محیطها</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-dropdown">
|
||||
<span>👤</span>
|
||||
<select class="filter-select" id="filterOwner">
|
||||
<option value="">همه مالکان</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-stats">
|
||||
<div class="stat-item">
|
||||
@@ -3296,32 +3302,38 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">عنوان <span class="required">*</span></label><input type="text" class="form-input"
|
||||
id="newProjectTitle" />
|
||||
<label class="form-label">عنوان <span class="required">*</span></label>
|
||||
<input type="text" class="form-input" id="newProjectTitle" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">نوع <span class="required">*</span></label><select class="form-select"
|
||||
id="newProjectType">
|
||||
<label class="form-label">نوع <span class="required">*</span></label>
|
||||
<select class="form-select" id="newProjectType">
|
||||
<option value="">انتخاب کنید...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">فریمورک</label><input type="text" class="form-input" id="newProjectFramework" />
|
||||
<label class="form-label">فریمورک</label>
|
||||
<input type="text" class="form-input" id="newProjectFramework" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">زبان</label><input type="text" class="form-input" id="newProjectLanguage" />
|
||||
<label class="form-label">مالک پروژه</label>
|
||||
<input type="text" class="form-input" id="newProjectOwner" placeholder="نام مسئول پروژه..." />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="newProjectRepo"
|
||||
dir="ltr" />
|
||||
<label class="form-label">زبان</label>
|
||||
<input type="text" class="form-input" id="newProjectLanguage" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label><textarea class="form-textarea"
|
||||
id="newProjectDescription"></textarea>
|
||||
<label class="form-label">آدرس مخزن</label>
|
||||
<input type="text" class="form-input" id="newProjectRepo" dir="ltr" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label>
|
||||
<textarea class="form-textarea" id="newProjectDescription"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -3338,35 +3350,42 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">شناسه <span class="readonly-badge">فقط خواندنی</span></label><input type="text"
|
||||
class="form-input" id="editProjId" readonly />
|
||||
<label class="form-label">شناسه <span class="readonly-badge">فقط خواندنی</span></label>
|
||||
<input type="text" class="form-input" id="editProjId" readonly />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">عنوان <span class="required">*</span></label><input type="text" class="form-input"
|
||||
id="editProjTitle" />
|
||||
<label class="form-label">عنوان <span class="required">*</span></label>
|
||||
<input type="text" class="form-input" id="editProjTitle" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">نوع <span class="required">*</span></label><select class="form-select"
|
||||
id="editProjType">
|
||||
<label class="form-label">نوع <span class="required">*</span></label>
|
||||
<select class="form-select" id="editProjType">
|
||||
<option value="">انتخاب کنید...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">فریمورک</label><input type="text" class="form-input" id="editProjFramework" />
|
||||
<label class="form-label">فریمورک</label>
|
||||
<input type="text" class="form-input" id="editProjFramework" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">زبان</label><input type="text" class="form-input" id="editProjLanguage" />
|
||||
<label class="form-label">مالک پروژه</label>
|
||||
<input type="text" class="form-input" id="editProjOwner" placeholder="نام مسئول پروژه..." />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="editProjRepo"
|
||||
dir="ltr" />
|
||||
<label class="form-label">زبان</label>
|
||||
<input type="text" class="form-input" id="editProjLanguage" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label><textarea class="form-textarea" id="editProjDescription"></textarea>
|
||||
<label class="form-label">آدرس مخزن</label>
|
||||
<input type="text" class="form-input" id="editProjRepo" dir="ltr" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">توضیحات</label>
|
||||
<textarea class="form-textarea" id="editProjDescription"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -4283,9 +4302,10 @@
|
||||
};
|
||||
/* ==================== TOOLBAR MANAGER ==================== */
|
||||
const ToolbarManager = {
|
||||
currentFilters: { query: "", typeFilter: "", envFilter: "" },
|
||||
currentFilters: { query: "", typeFilter: "", envFilter: "", ownerFilter: "" },
|
||||
init() {
|
||||
this.populateEnvironmentFilter();
|
||||
this.populateOwnerFilter();
|
||||
this.bindEvents();
|
||||
this.updateStats();
|
||||
},
|
||||
@@ -4298,10 +4318,28 @@
|
||||
.map((e) => `<option value="${e.id}">${e.name}</option>`)
|
||||
.join("");
|
||||
},
|
||||
populateOwnerFilter() {
|
||||
const s = document.getElementById("filterOwner");
|
||||
if (!s) return;
|
||||
const owners = new Set();
|
||||
(projectData.appData.products || []).forEach((p) => {
|
||||
const owner = p.metadata?.owner?.trim();
|
||||
if (owner) owners.add(owner);
|
||||
});
|
||||
const sortedOwners = Array.from(owners).sort((a, b) =>
|
||||
a.localeCompare(b, "fa"),
|
||||
);
|
||||
s.innerHTML =
|
||||
'<option value="">همه مالکان</option>' +
|
||||
sortedOwners
|
||||
.map((o) => `<option value="${o}">${o}</option>`)
|
||||
.join("");
|
||||
},
|
||||
bindEvents() {
|
||||
const si = document.getElementById("toolbarSearch"),
|
||||
ft = document.getElementById("filterType"),
|
||||
fe = document.getElementById("filterEnvironment");
|
||||
fe = document.getElementById("filterEnvironment"),
|
||||
fo = document.getElementById("filterOwner");
|
||||
if (si)
|
||||
si.addEventListener("input", () => {
|
||||
this.applyFilters();
|
||||
@@ -4317,6 +4355,11 @@
|
||||
this.applyFilters();
|
||||
refreshUI();
|
||||
});
|
||||
if (fo)
|
||||
fo.addEventListener("change", () => {
|
||||
this.applyFilters();
|
||||
refreshUI();
|
||||
});
|
||||
},
|
||||
applyFilters() {
|
||||
this.currentFilters = {
|
||||
@@ -4324,8 +4367,8 @@
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
typeFilter: document.getElementById("filterType")?.value || "",
|
||||
envFilter:
|
||||
document.getElementById("filterEnvironment")?.value || "",
|
||||
envFilter: document.getElementById("filterEnvironment")?.value || "",
|
||||
ownerFilter: document.getElementById("filterOwner")?.value || "",
|
||||
};
|
||||
this.updateStats();
|
||||
},
|
||||
@@ -4350,7 +4393,15 @@
|
||||
},
|
||||
shouldShowProduct(product) {
|
||||
const f = this.currentFilters;
|
||||
if (!f.query && !f.typeFilter && !f.envFilter && !f.ownerFilter) return true;
|
||||
|
||||
if (f.ownerFilter) {
|
||||
const prodOwner = product.metadata?.owner?.trim();
|
||||
if (prodOwner !== f.ownerFilter) return false;
|
||||
}
|
||||
|
||||
if (!f.query && !f.typeFilter && !f.envFilter) return true;
|
||||
|
||||
return (product.projects || []).some((pr) => {
|
||||
const mq =
|
||||
!f.query ||
|
||||
@@ -4362,6 +4413,7 @@
|
||||
(pr.deployments || [])
|
||||
.filter((d) => d.status === "active")
|
||||
.some((d) => d.environmentId === f.envFilter);
|
||||
|
||||
return mq && mt && me;
|
||||
});
|
||||
},
|
||||
@@ -4891,6 +4943,7 @@
|
||||
updateProjectUI();
|
||||
renderToolGroups();
|
||||
ToolbarManager.populateEnvironmentFilter();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
refreshUI();
|
||||
showToast("📂 بارگذاری شد", "success");
|
||||
} catch (err) {
|
||||
@@ -4913,6 +4966,7 @@
|
||||
};
|
||||
AutosaveManager.clear();
|
||||
ToolbarManager.populateEnvironmentFilter();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
refreshUI();
|
||||
showToast("🗑️ پاکسازی شد", "success");
|
||||
},
|
||||
@@ -5132,7 +5186,13 @@
|
||||
return null;
|
||||
},
|
||||
getActiveDeployments(proj) {
|
||||
return (proj.deployments || []).filter((d) => d.status === "active");
|
||||
return (proj.deployments || [])
|
||||
.filter((d) => d.status === "active")
|
||||
.sort((a, b) => {
|
||||
const dateA = a.deploymentDate ? new Date(a.deploymentDate).getTime() : 0;
|
||||
const dateB = b.deploymentDate ? new Date(b.deploymentDate).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
},
|
||||
getEnvironmentsWithoutDeployment(proj) {
|
||||
const allEnvIds = projectData.appData.environments.map((e) => e.id);
|
||||
@@ -5154,7 +5214,9 @@
|
||||
(s, pr) => s + this.getActiveDeployments(pr).length,
|
||||
0,
|
||||
);
|
||||
|
||||
let html = `<div class="detail-section"><div class="detail-section-header"><span>ℹ️ اطلاعات محصول</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openProductEditModal()">✏️ ویرایش</button></div></div><div class="detail-section-body"><div class="detail-info-grid"><div class="detail-info-item"><span class="detail-info-label">عنوان</span><span class="detail-info-value">${prod.title}</span></div><div class="detail-info-item"><span class="detail-info-label">توضیحات</span><span class="detail-info-value">${prod.description || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تاریخ ایجاد</span><span class="detail-info-value">${prod.metadata?.createdAt ? new Date(prod.metadata.createdAt).toLocaleDateString("fa-IR") : "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">مالک</span><span class="detail-info-value">${prod.metadata?.owner || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تعداد پروژه</span><span class="detail-info-value">${projects.length}</span></div><div class="detail-info-item"><span class="detail-info-label">استقرارهای فعال</span><span class="detail-info-value">${totalDeploys}</span></div></div></div></div><div class="detail-section"><div class="detail-section-header"><span>🌿 پروژهها (${projects.length})</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openAddProjectModal()">➕ افزودن</button></div></div><div class="detail-section-body">`;
|
||||
|
||||
if (projects.length === 0) {
|
||||
html += '<div class="empty-subprojects">🌿 هیچ پروژهای نیست</div>';
|
||||
} else {
|
||||
@@ -5163,21 +5225,43 @@
|
||||
const ti = this.getTypeInfo(pr.type);
|
||||
const deps = pr.dependencies || [];
|
||||
const deploys = this.getActiveDeployments(pr);
|
||||
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
|
||||
const canAdd = emptyEnvs.length > 0;
|
||||
html += `<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${ti.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${ti.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openDependencyManager('${prod.id}','${pr.id}')">🔗 وابستگیها</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}','${pr.id}')">✏️</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}','${pr.id}')">🗑️</button></div></div><div class="subproject-meta">${pr.description || ""}</div><div class="detail-info-grid" style="margin-bottom:8px">${pr.metadata?.framework ? `<div class="detail-info-item"><span class="detail-info-label">فریمورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>` : ""}${pr.metadata?.language ? `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>` : ""}${deps.length > 0
|
||||
? `<div class="detail-info-item"><span class="detail-info-label">وابستگیها (${deps.length})</span><span class="detail-info-value">${deps
|
||||
const archivedDeploys = (pr.deployments || [])
|
||||
.filter((d) => d.status === "archived")
|
||||
.sort((a, b) => {
|
||||
const dateA = a.archivedAt ? new Date(a.archivedAt).getTime() : 0;
|
||||
const dateB = b.archivedAt ? new Date(b.archivedAt).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
const hasEnvs = projectData.appData.environments.length > 0;
|
||||
|
||||
html += `<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${ti.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${ti.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openDependencyManager('${prod.id}','${pr.id}')">🔗 وابستگیها</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}','${pr.id}')">✏️</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}','${pr.id}')">🗑️</button></div></div><div class="subproject-meta">${pr.description || ""}</div><div class="detail-info-grid" style="margin-bottom:8px">`;
|
||||
|
||||
if (pr.metadata?.owner) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">👤 مالک</span><span class="detail-info-value">${pr.metadata.owner}</span></div>`;
|
||||
}
|
||||
|
||||
if (pr.metadata?.framework) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">فریمورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>`;
|
||||
}
|
||||
|
||||
if (pr.metadata?.language) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>`;
|
||||
}
|
||||
|
||||
if (deps.length > 0) {
|
||||
html += `<div class="detail-info-item"><span class="detail-info-label">وابستگیها (${deps.length})</span><span class="detail-info-value">${deps
|
||||
.map((depId) => {
|
||||
const dp = this.findProjectById(depId);
|
||||
return dp ? dp.title : depId;
|
||||
})
|
||||
.join("، ")}</span></div>`
|
||||
: ""
|
||||
}</div>`;
|
||||
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${canAdd ? `<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}','${pr.id}')">➕ افزودن</button>` : '<span style="font-size:10px;color:var(--text-muted)">✓ کامل</span>'}</div>`;
|
||||
.join("، ")}</span></div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${hasEnvs ? `<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}','${pr.id}')">➕ افزودن</button>` : '<span style="font-size:10px;color:var(--text-muted)">⚠️ محیطی تعریف نشده</span>'}</div>`;
|
||||
|
||||
if (deploys.length === 0) {
|
||||
html +=
|
||||
'<div class="empty-deployments">هنوز استقرار ندارد</div>';
|
||||
html += '<div class="empty-deployments">هنوز استقرار ندارد</div>';
|
||||
} else {
|
||||
html += '<div class="deployment-list">';
|
||||
deploys.forEach((d) => {
|
||||
@@ -5186,13 +5270,22 @@
|
||||
const fd = d.deploymentDate
|
||||
? new Date(d.deploymentDate).toLocaleString("fa-IR")
|
||||
: "—";
|
||||
html += `<div class="deployment-item"><div class="deployment-item-info"><span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${en}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${fd}</span></div><div class="deployment-actions"><button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')">✏️</button><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')">🗑️</button></div>`;
|
||||
if (d.releaseNotes)
|
||||
|
||||
html += `<div class="deployment-item"><div class="deployment-item-info"><span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${en}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${fd}</span></div><div class="deployment-actions">`;
|
||||
|
||||
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')" title="ویرایش">✏️</button>`;
|
||||
|
||||
html += `<button class="btn btn-xs" style="background:#dd6b20;color:#fff" onclick="ProductManager.archiveDeployment('${prod.id}','${pr.id}','${d.id}')" title="انتقال به تاریخچه">📦 آرشیو</button>`;
|
||||
|
||||
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
if (d.releaseNotes) {
|
||||
html += `<div class="deployment-notes">📝 ${d.releaseNotes}</div>`;
|
||||
if (
|
||||
d.requiredDeployments &&
|
||||
d.requiredDeployments.length > 0
|
||||
) {
|
||||
}
|
||||
|
||||
if (d.requiredDeployments && d.requiredDeployments.length > 0) {
|
||||
html += `<div class="deployment-req">🔗 نیازمندیها: ${d.requiredDeployments
|
||||
.map((rd) => {
|
||||
const rp = this.findProjectById(rd.projectId);
|
||||
@@ -5207,14 +5300,62 @@
|
||||
})
|
||||
.join("، ")}</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
html += `<div class="deployment-section" style="margin-top:12px;border-top:2px dashed var(--border)">`;
|
||||
html += `<div class="deployment-header">`;
|
||||
html += `<span class="deployment-header-title">📜 تاریخچه استقرارها (${archivedDeploys.length})</span>`;
|
||||
if (archivedDeploys.length > 0) {
|
||||
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.toggleArchiveVisibility('${pr.id}')" id="archiveToggle-${pr.id}">👁️ نمایش/مخفی</button>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
if (archivedDeploys.length === 0) {
|
||||
html += `<div class="empty-deployments" style="opacity:0.6">هنوز استقراری آرشیو نشده است</div>`;
|
||||
} else {
|
||||
html += `<div class="deployment-list" id="archiveList-${pr.id}" style="display:none">`;
|
||||
archivedDeploys.forEach((d) => {
|
||||
const en = this.getEnvironmentName(d.environmentId);
|
||||
const cl = this.getEnvironmentColors(d.environmentId);
|
||||
const fd = d.deploymentDate
|
||||
? new Date(d.deploymentDate).toLocaleString("fa-IR")
|
||||
: "—";
|
||||
const archivedDate = d.archivedAt
|
||||
? new Date(d.archivedAt).toLocaleString("fa-IR")
|
||||
: "—";
|
||||
|
||||
html += `<div class="deployment-item" style="opacity:0.75;background:var(--border-light)">`;
|
||||
html += `<div class="deployment-item-info">`;
|
||||
html += `<span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor};opacity:0.7">${en}</span>`;
|
||||
html += `<span class="deployment-version" style="opacity:0.7">v${d.version}</span>`;
|
||||
html += `<span class="deployment-date">📅 ${fd}</span>`;
|
||||
html += `<span class="deployment-date" style="color:var(--warning)">📦 آرشیو: ${archivedDate}</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="deployment-actions">`;
|
||||
html += `<button class="btn btn-xs" style="background:#3182ce;color:#fff" onclick="ProductManager.restoreDeployment('${prod.id}','${pr.id}','${d.id}')" title="بازگرداندن به لیست فعال">↩️ بازگردانی</button>`;
|
||||
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
|
||||
html += `</div>`;
|
||||
|
||||
if (d.releaseNotes) {
|
||||
html += `<div class="deployment-notes" style="opacity:0.7">📝 ${d.releaseNotes}</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
html += `</div></div>`;
|
||||
});
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
html += `</div></div>`;
|
||||
document.getElementById("projectDetailBody").innerHTML = html;
|
||||
ModalManager.open("projectDetailModal");
|
||||
@@ -5259,6 +5400,7 @@
|
||||
});
|
||||
ModalManager.close("addProductModal");
|
||||
refreshUI();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
showToast(`"${title}" ایجاد شد`, "success");
|
||||
},
|
||||
openDependencyManager(productId, projId) {
|
||||
@@ -5449,6 +5591,7 @@
|
||||
prod.metadata.updatedAt = new Date().toISOString();
|
||||
ModalManager.close("productEditModal");
|
||||
refreshUI();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
this.showDetail(pid);
|
||||
showToast(`"${prod.title}" ویرایش شد`, "success");
|
||||
},
|
||||
@@ -5460,6 +5603,7 @@
|
||||
document.getElementById("newProjectTitle").value = "";
|
||||
document.getElementById("newProjectType").value = "";
|
||||
document.getElementById("newProjectFramework").value = "";
|
||||
document.getElementById("newProjectOwner").value = ""; // 🆕
|
||||
document.getElementById("newProjectLanguage").value = "";
|
||||
document.getElementById("newProjectRepo").value = "";
|
||||
document.getElementById("newProjectDescription").value = "";
|
||||
@@ -5482,20 +5626,18 @@
|
||||
const np = {
|
||||
id: `proj-${Date.now()}`,
|
||||
title,
|
||||
description:
|
||||
document.getElementById("newProjectDescription").value.trim() ||
|
||||
"",
|
||||
description: document.getElementById("newProjectDescription").value.trim() || "",
|
||||
type,
|
||||
repositoryUrl: document
|
||||
.getElementById("newProjectRepo")
|
||||
.value.trim(),
|
||||
metadata: { createdAt: new Date().toISOString(), customFields: {} },
|
||||
repositoryUrl: document.getElementById("newProjectRepo").value.trim(),
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
owner: document.getElementById("newProjectOwner").value.trim() || "", // 🆕
|
||||
customFields: {}
|
||||
},
|
||||
dependencies: [],
|
||||
deployments: [],
|
||||
};
|
||||
const fw = document
|
||||
.getElementById("newProjectFramework")
|
||||
.value.trim();
|
||||
const fw = document.getElementById("newProjectFramework").value.trim();
|
||||
if (fw) np.metadata.framework = fw;
|
||||
const lg = document.getElementById("newProjectLanguage").value.trim();
|
||||
if (lg) np.metadata.language = lg;
|
||||
@@ -5506,9 +5648,7 @@
|
||||
showToast(`"${title}" ایجاد شد`, "success");
|
||||
},
|
||||
openProjectEditModal(productId, projId) {
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
const prod = projectData.appData.products.find((p) => p.id === productId);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
@@ -5517,14 +5657,11 @@
|
||||
document.getElementById("editProjId").value = pr.id;
|
||||
document.getElementById("editProjTitle").value = pr.title;
|
||||
document.getElementById("editProjType").value = pr.type;
|
||||
document.getElementById("editProjFramework").value =
|
||||
pr.metadata?.framework || "";
|
||||
document.getElementById("editProjLanguage").value =
|
||||
pr.metadata?.language || "";
|
||||
document.getElementById("editProjRepo").value =
|
||||
pr.repositoryUrl || "";
|
||||
document.getElementById("editProjDescription").value =
|
||||
pr.description || "";
|
||||
document.getElementById("editProjFramework").value = pr.metadata?.framework || "";
|
||||
document.getElementById("editProjOwner").value = pr.metadata?.owner || ""; // 🆕
|
||||
document.getElementById("editProjLanguage").value = pr.metadata?.language || "";
|
||||
document.getElementById("editProjRepo").value = pr.repositoryUrl || "";
|
||||
document.getElementById("editProjDescription").value = pr.description || "";
|
||||
ModalManager.open("editProjectModal");
|
||||
},
|
||||
saveProjectEdit() {
|
||||
@@ -5545,15 +5682,15 @@
|
||||
AutosaveManager.scheduleSave();
|
||||
pr.title = nt;
|
||||
pr.type = nty;
|
||||
pr.description =
|
||||
document.getElementById("editProjDescription").value.trim() || "";
|
||||
pr.repositoryUrl = document
|
||||
.getElementById("editProjRepo")
|
||||
.value.trim();
|
||||
pr.description = document.getElementById("editProjDescription").value.trim() || "";
|
||||
pr.repositoryUrl = document.getElementById("editProjRepo").value.trim();
|
||||
if (!pr.metadata) pr.metadata = {};
|
||||
const fw = document.getElementById("editProjFramework").value.trim();
|
||||
if (fw) pr.metadata.framework = fw;
|
||||
else delete pr.metadata.framework;
|
||||
const owner = document.getElementById("editProjOwner").value.trim(); // 🆕
|
||||
if (owner) pr.metadata.owner = owner;
|
||||
else delete pr.metadata.owner;
|
||||
const lg = document.getElementById("editProjLanguage").value.trim();
|
||||
if (lg) pr.metadata.language = lg;
|
||||
else delete pr.metadata.language;
|
||||
@@ -5565,36 +5702,39 @@
|
||||
},
|
||||
openAddDeploymentModal(productId, projId) {
|
||||
if (!productId || !projId) return;
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
const prod = projectData.appData.products.find((p) => p.id === productId);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
|
||||
if (emptyEnvs.length === 0) {
|
||||
showToast("همه محیطها استقرار دارند", "warning");
|
||||
|
||||
const allEnvs = projectData.appData.environments;
|
||||
if (allEnvs.length === 0) {
|
||||
showToast("ابتدا یک محیط عملیاتی ایجاد کنید", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentDeploymentProjId = projId;
|
||||
this.currentDeploymentMode = "add";
|
||||
this.currentDeploymentEnvId = null;
|
||||
this.currentEditDeploymentId = null;
|
||||
|
||||
document.getElementById("deploymentModalTitle").innerHTML =
|
||||
"<span>🚀</span><span>افزودن نسخه استقرار</span>";
|
||||
|
||||
const envGroup = document.getElementById("deploymentEnvGroup");
|
||||
const envs = projectData.appData.environments.filter((e) =>
|
||||
emptyEnvs.includes(e.id),
|
||||
);
|
||||
const envs = allEnvs;
|
||||
|
||||
envGroup.innerHTML = `<label class="form-label">محیط استقرار <span class="required">*</span></label><select class="form-select" id="depEnvironment" onchange="ProductManager.onEnvChange()">${envs
|
||||
.map((e) => {
|
||||
const cl = this.getEnvironmentColors(e.id);
|
||||
return `<option value="${e.id}" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${e.name} - ${e.description}</option>`;
|
||||
})
|
||||
.join("")}</select>`;
|
||||
|
||||
document.getElementById("depVersion").value = "";
|
||||
document.getElementById("depDate").value = "";
|
||||
document.getElementById("depReleaseNotes").value = "";
|
||||
|
||||
this.renderRequiredDeploymentsSection();
|
||||
ModalManager.open("deploymentModal");
|
||||
},
|
||||
@@ -5762,7 +5902,26 @@
|
||||
this.showDetail(pid);
|
||||
},
|
||||
removeDeployment(productId, projId, depId) {
|
||||
if (!confirm("حذف این استقرار؟")) return;
|
||||
if (!confirm("⚠️ حذف کامل این استقرار؟\n\nاین عمل غیرقابل بازگشت است.\nاگر میخواهید فقط از لیست فعال خارج شود، از دکمه «آرشیو» استفاده کنید.")) return;
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
|
||||
HistoryManager.pushState();
|
||||
AutosaveManager.scheduleSave();
|
||||
|
||||
pr.deployments = (pr.deployments || []).filter((d) => d.id !== depId);
|
||||
|
||||
refreshUI();
|
||||
this.showDetail(productId);
|
||||
showToast("🗑️ استقرار بهطور کامل حذف شد", "warning");
|
||||
},
|
||||
archiveDeployment(productId, projId, depId) {
|
||||
if (!confirm("📦 انتقال این استقرار به تاریخچه؟\n\nاستقرار از لیست فعال خارج شده و در بخش تاریخچه نگهداری میشود.")) return;
|
||||
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
@@ -5771,12 +5930,16 @@
|
||||
if (!pr) return;
|
||||
const dep = (pr.deployments || []).find((d) => d.id === depId);
|
||||
if (!dep) return;
|
||||
|
||||
HistoryManager.pushState();
|
||||
AutosaveManager.scheduleSave();
|
||||
dep.status = "removed";
|
||||
|
||||
dep.status = "archived";
|
||||
dep.archivedAt = new Date().toISOString();
|
||||
|
||||
refreshUI();
|
||||
this.showDetail(productId);
|
||||
showToast("استقرار حذف شد", "warning");
|
||||
showToast("📦 استقرار به تاریخچه منتقل شد", "success");
|
||||
},
|
||||
removeProduct(productId) {
|
||||
const prod = projectData.appData.products.find(
|
||||
@@ -5790,6 +5953,7 @@
|
||||
(p) => p.id !== productId,
|
||||
);
|
||||
refreshUI();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
ModalManager.close("projectDetailModal");
|
||||
showToast(`"${prod.title}" حذف شد`, "warning");
|
||||
},
|
||||
@@ -5809,6 +5973,35 @@
|
||||
this.showDetail(productId);
|
||||
showToast(`"${pr.title}" حذف شد`, "warning");
|
||||
},
|
||||
toggleArchiveVisibility(projId) {
|
||||
const list = document.getElementById(`archiveList-${projId}`);
|
||||
if (!list) return;
|
||||
const isHidden = list.style.display === "none";
|
||||
list.style.display = isHidden ? "flex" : "none";
|
||||
},
|
||||
restoreDeployment(productId, projId, depId) {
|
||||
if (!confirm("↩️ بازگرداندن این استقرار به لیست فعال؟")) return;
|
||||
|
||||
const prod = projectData.appData.products.find(
|
||||
(p) => p.id === productId,
|
||||
);
|
||||
if (!prod) return;
|
||||
const pr = prod.projects.find((p) => p.id === projId);
|
||||
if (!pr) return;
|
||||
const dep = (pr.deployments || []).find((d) => d.id === depId);
|
||||
if (!dep) return;
|
||||
|
||||
HistoryManager.pushState();
|
||||
AutosaveManager.scheduleSave();
|
||||
|
||||
// ✅ بازگرداندن به وضعیت active
|
||||
dep.status = "active";
|
||||
delete dep.archivedAt;
|
||||
|
||||
refreshUI();
|
||||
this.showDetail(productId);
|
||||
showToast("↩️ استقرار به لیست فعال بازگردانده شد", "success");
|
||||
},
|
||||
};
|
||||
/* ==================== DASHBOARD MANAGER ==================== */
|
||||
const DashboardManager = {
|
||||
@@ -6340,6 +6533,35 @@
|
||||
const fm = gf.append("feMerge");
|
||||
fm.append("feMergeNode").attr("in", "blur");
|
||||
fm.append("feMergeNode").attr("in", "SourceGraphic");
|
||||
|
||||
defs.append("marker")
|
||||
.attr("id", "arrowDependency")
|
||||
.attr("viewBox", "0 -5 10 10")
|
||||
.attr("refX", 10) // نقطه مرجع در نوک فلش
|
||||
.attr("refY", 0)
|
||||
.attr("markerWidth", 8) // عرض فلش
|
||||
.attr("markerHeight", 8) // ارتفاع فلش
|
||||
.attr("orient", "auto")
|
||||
.attr("markerUnits", "userSpaceOnUse")
|
||||
.append("path")
|
||||
.attr("d", "M0,-5L10,0L0,5") // شکل مثلثی فلش
|
||||
.attr("fill", "#e0c789"); // رنگ طلایی هماهنگ با لینک وابستگی
|
||||
|
||||
// 🆕 Marker برای لینکهای ownership (اختیاری - بدون فلش یا فلش کوچکتر)
|
||||
defs.append("marker")
|
||||
.attr("id", "arrowOwnership")
|
||||
.attr("viewBox", "0 -5 10 10")
|
||||
.attr("refX", 10)
|
||||
.attr("refY", 0)
|
||||
.attr("markerWidth", 6)
|
||||
.attr("markerHeight", 6)
|
||||
.attr("orient", "auto")
|
||||
.attr("markerUnits", "userSpaceOnUse")
|
||||
.append("path")
|
||||
.attr("d", "M0,-4L10,0L0,4")
|
||||
.attr("fill", "#4a5568")
|
||||
.attr("opacity", 0.7);
|
||||
|
||||
this.zoomGroup = svg.append("g");
|
||||
this.zoomBehavior = d3
|
||||
.zoom()
|
||||
@@ -6370,10 +6592,38 @@
|
||||
d.type === "dependency" ? "#e0c789" : "#4a5568",
|
||||
)
|
||||
.attr("stroke-width", (d) => (d.type === "dependency" ? 2 : 1))
|
||||
.attr("stroke-opacity", 0.6)
|
||||
.attr("stroke-opacity", (d) => (d.type === "dependency" ? 0.85 : 0.4))
|
||||
.attr("stroke-dasharray", (d) =>
|
||||
d.type === "dependency" ? "5,3" : "none",
|
||||
)
|
||||
.attr("marker-end", (d) =>
|
||||
d.type === "dependency" ? "url(#arrowDependency)" : "url(#arrowOwnership)"
|
||||
);
|
||||
this.linkElements
|
||||
.style("cursor", "pointer")
|
||||
.on("mouseover", function (e, d) {
|
||||
d3.select(this)
|
||||
.attr("stroke-opacity", 1)
|
||||
.attr("stroke-width", d.type === "dependency" ? 3 : 2);
|
||||
|
||||
const sourceNode = this.__data__.source;
|
||||
const targetNode = this.__data__.target;
|
||||
const sourceLabel = sourceNode.label || sourceNode.id;
|
||||
const targetLabel = targetNode.label || targetNode.id;
|
||||
|
||||
const tooltipText = d.type === "dependency"
|
||||
? `🔗 ${sourceLabel} → ${targetLabel}`
|
||||
: `📦 ${targetLabel} شامل ${sourceLabel}`;
|
||||
|
||||
showD3Tooltip(e, tooltipText);
|
||||
})
|
||||
.on("mousemove", (e) => showD3Tooltip(e, ""))
|
||||
.on("mouseout", function (e, d) {
|
||||
d3.select(this)
|
||||
.attr("stroke-opacity", d.type === "dependency" ? 0.85 : 0.4)
|
||||
.attr("stroke-width", d.type === "dependency" ? 2 : 1);
|
||||
hideD3Tooltip();
|
||||
});
|
||||
const node = this.zoomGroup
|
||||
.append("g")
|
||||
.selectAll("g")
|
||||
@@ -6437,14 +6687,46 @@
|
||||
this.toggleCollapseProduct(d.id);
|
||||
}
|
||||
});
|
||||
|
||||
const getEdgePoint = (source, target, targetRadius) => {
|
||||
const dx = target.x - source.x;
|
||||
const dy = target.y - source.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const offset = targetRadius + 10;
|
||||
|
||||
return {
|
||||
x: target.x - (dx / distance) * offset,
|
||||
y: target.y - (dy / distance) * offset,
|
||||
};
|
||||
};
|
||||
|
||||
sim.on("tick", () => {
|
||||
this.linkElements
|
||||
.attr("x1", (d) => d.source.x)
|
||||
.attr("y1", (d) => d.source.y)
|
||||
.attr("x2", (d) => d.target.x)
|
||||
.attr("y2", (d) => d.target.y);
|
||||
.attr("x1", (d) => {
|
||||
// نقطه شروع: روی مرز گره منبع
|
||||
const dx = d.target.x - d.source.x;
|
||||
const dy = d.target.y - d.source.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
return d.source.x + (dx / distance) * d.source.radius;
|
||||
})
|
||||
.attr("y1", (d) => {
|
||||
const dx = d.target.x - d.source.x;
|
||||
const dy = d.target.y - d.source.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
return d.source.y + (dy / distance) * d.source.radius;
|
||||
})
|
||||
.attr("x2", (d) => {
|
||||
// نقطه پایان: روی مرز گره هدف (با offset برای فلش)
|
||||
const edge = getEdgePoint(d.source, d.target, d.target.radius);
|
||||
return edge.x;
|
||||
})
|
||||
.attr("y2", (d) => {
|
||||
const edge = getEdgePoint(d.source, d.target, d.target.radius);
|
||||
return edge.y;
|
||||
});
|
||||
node.attr("transform", (d) => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
this.renderLegend();
|
||||
this.updateStats(nodes, links);
|
||||
this.setupSearch();
|
||||
@@ -6710,6 +6992,7 @@
|
||||
updateProjectUI();
|
||||
renderToolGroups();
|
||||
ToolbarManager.populateEnvironmentFilter();
|
||||
ToolbarManager.populateOwnerFilter();
|
||||
refreshUI();
|
||||
if (ReleaseBoardsManager) {
|
||||
ReleaseBoardsManager.render(true);
|
||||
|
||||
Reference in New Issue
Block a user