projects: деревовидный выбор отделов вместо плоского списка.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+28
-8
@@ -45,15 +45,35 @@ def _departments():
|
||||
return list(
|
||||
Directory.objects.filter(
|
||||
directory_type__code='department', is_active=True,
|
||||
).order_by('name')
|
||||
).order_by('_path', 'name')
|
||||
)
|
||||
|
||||
|
||||
def _departments_json():
|
||||
return [
|
||||
{'id': d.id, 'name': d.name, 'code': d.code or ''}
|
||||
for d in _departments()
|
||||
]
|
||||
def _departments_tree():
|
||||
"""Дерево отделов {id, name, code, children} для DeptTreePicker."""
|
||||
depts = list(
|
||||
Directory.objects.filter(
|
||||
directory_type__code='department', is_active=True,
|
||||
).order_by('_path', 'name').only('pk', 'name', 'code', 'parent_id')
|
||||
)
|
||||
nodes = {
|
||||
d.pk: {
|
||||
'id': d.pk,
|
||||
'name': d.name,
|
||||
'code': d.code or '',
|
||||
'children': [],
|
||||
}
|
||||
for d in depts
|
||||
}
|
||||
roots = []
|
||||
for d in depts:
|
||||
node = nodes[d.pk]
|
||||
parent = nodes.get(d.parent_id)
|
||||
if parent is not None:
|
||||
parent['children'].append(node)
|
||||
else:
|
||||
roots.append(node)
|
||||
return roots
|
||||
|
||||
|
||||
def _project_qs():
|
||||
@@ -102,7 +122,7 @@ def projects_list_view(request):
|
||||
'page_tab': (request.GET.get('tab') or 'projects').strip(),
|
||||
'templates': templates,
|
||||
'departments': _departments(),
|
||||
'departments_json': _departments_json(),
|
||||
'departments_tree': _departments_tree(),
|
||||
'ug_message': request.session.pop('projects_message', ''),
|
||||
'ug_error': request.session.pop('projects_error', ''),
|
||||
})
|
||||
@@ -122,7 +142,7 @@ def project_detail_view(request, pk: int):
|
||||
'pct': project.progress_percent,
|
||||
'can_manage': _can_manage_project(request.user, project),
|
||||
'departments': _departments(),
|
||||
'departments_json': _departments_json(),
|
||||
'departments_tree': _departments_tree(),
|
||||
'ug_message': request.session.pop('projects_message', ''),
|
||||
'ug_error': request.session.pop('projects_error', ''),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Деревовидный выпадающий выбор отдела (Directory type=department).
|
||||
*
|
||||
* Данные: {{ tree|json_script:"…-tree" }}
|
||||
* [{id, name, children: [...]}, ...]
|
||||
*
|
||||
* Использование:
|
||||
* var el = DeptTreePicker.create({
|
||||
* name: 'department_id',
|
||||
* value: '',
|
||||
* emptyLabel: 'Отдел…',
|
||||
* treeDataId: 'proj-departments-tree',
|
||||
* });
|
||||
* container.appendChild(el);
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var treeCache = {};
|
||||
|
||||
function attachParentRefs(nodes, parent) {
|
||||
(nodes || []).forEach(function (node) {
|
||||
node.parentRef = parent || null;
|
||||
if (node.children && node.children.length) {
|
||||
attachParentRefs(node.children, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function readTree(treeDataId) {
|
||||
if (treeCache[treeDataId]) return treeCache[treeDataId];
|
||||
var el = document.getElementById(treeDataId);
|
||||
var tree = [];
|
||||
if (el) {
|
||||
try { tree = JSON.parse(el.textContent) || []; } catch (e) { tree = []; }
|
||||
}
|
||||
attachParentRefs(tree, null);
|
||||
treeCache[treeDataId] = tree;
|
||||
return tree;
|
||||
}
|
||||
|
||||
function buildPathLabel(node) {
|
||||
var parts = [];
|
||||
var cur = node;
|
||||
while (cur) {
|
||||
parts.unshift(cur.name);
|
||||
cur = cur.parentRef;
|
||||
}
|
||||
return parts.join(' → ');
|
||||
}
|
||||
|
||||
function findNode(nodes, id) {
|
||||
if (!id) return null;
|
||||
var sid = String(id);
|
||||
for (var i = 0; i < (nodes || []).length; i++) {
|
||||
var n = nodes[i];
|
||||
if (String(n.id) === sid) return n;
|
||||
var found = findNode(n.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function nodeMatches(node, q) {
|
||||
if (!q) return true;
|
||||
if ((node.name || '').toLowerCase().indexOf(q) !== -1) return true;
|
||||
return (node.children || []).some(function (c) { return nodeMatches(c, q); });
|
||||
}
|
||||
|
||||
function create(options) {
|
||||
options = options || {};
|
||||
var name = options.name || 'department_id';
|
||||
var emptyLabel = options.emptyLabel || '—';
|
||||
var treeDataId = options.treeDataId || 'dept-tree-data';
|
||||
var initial = options.value != null ? String(options.value) : '';
|
||||
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'dept-tree-picker' + (options.className ? ' ' + options.className : '');
|
||||
wrap.style.position = 'relative';
|
||||
wrap.style.width = '100%';
|
||||
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = initial;
|
||||
wrap.appendChild(input);
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'input';
|
||||
btn.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:0.5rem;width:100%;height:auto;padding:0.3rem 0.65rem;cursor:pointer;text-align:left;font-size:0.8125rem;';
|
||||
var label = document.createElement('span');
|
||||
label.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;';
|
||||
label.setAttribute('data-empty-label', emptyLabel);
|
||||
label.textContent = emptyLabel;
|
||||
var chevron = document.createElement('span');
|
||||
chevron.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0;opacity:0.6"><path d="m6 9 6 6 6-6"/></svg>';
|
||||
btn.appendChild(label);
|
||||
btn.appendChild(chevron);
|
||||
wrap.appendChild(btn);
|
||||
|
||||
var panel = document.createElement('div');
|
||||
panel.style.cssText = 'display:none;position:absolute;top:calc(100% + 0.25rem);left:0;right:0;z-index:80;background:var(--color-card);border:1px solid var(--color-border);border-radius:0.625rem;box-shadow:0 4px 16px rgb(0 0 0/0.12);max-height:16rem;overflow:hidden;';
|
||||
wrap.appendChild(panel);
|
||||
|
||||
function setValue(node) {
|
||||
if (node) {
|
||||
input.value = String(node.id);
|
||||
label.textContent = buildPathLabel(node);
|
||||
label.title = label.textContent;
|
||||
} else {
|
||||
input.value = '';
|
||||
label.textContent = emptyLabel;
|
||||
label.title = '';
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
function close() {
|
||||
panel.style.display = 'none';
|
||||
}
|
||||
|
||||
function renderNode(node, container, depth, query) {
|
||||
if (query && !nodeMatches(node, query)) return;
|
||||
var row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;align-items:center;gap:0.25rem;padding:0.35rem 0.5rem;padding-left:' +
|
||||
(0.5 + depth) + 'rem;border-radius:calc(var(--radius) - 2px);cursor:pointer;font-size:0.8125rem;';
|
||||
row.onmouseover = function () { row.style.background = 'var(--color-muted)'; };
|
||||
row.onmouseout = function () { row.style.background = ''; };
|
||||
|
||||
var hasChildren = node.children && node.children.length > 0;
|
||||
var toggle = document.createElement('span');
|
||||
toggle.textContent = hasChildren ? '▸' : '';
|
||||
toggle.style.cssText = 'width:0.875rem;flex-shrink:0;color:var(--color-muted-foreground);user-select:none;';
|
||||
row.appendChild(toggle);
|
||||
|
||||
var nameEl = document.createElement('span');
|
||||
nameEl.textContent = node.name;
|
||||
nameEl.style.cssText = 'flex:1;color:var(--color-foreground);';
|
||||
row.appendChild(nameEl);
|
||||
container.appendChild(row);
|
||||
|
||||
var childrenWrap = null;
|
||||
if (hasChildren) {
|
||||
childrenWrap = document.createElement('div');
|
||||
// при поиске раскрываем совпадения
|
||||
childrenWrap.style.display = query ? 'block' : 'none';
|
||||
if (query) toggle.textContent = '▾';
|
||||
container.appendChild(childrenWrap);
|
||||
node.children.forEach(function (child) {
|
||||
renderNode(child, childrenWrap, depth + 1, query);
|
||||
});
|
||||
toggle.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
var open = childrenWrap.style.display !== 'none';
|
||||
childrenWrap.style.display = open ? 'none' : 'block';
|
||||
toggle.textContent = open ? '▸' : '▾';
|
||||
});
|
||||
}
|
||||
|
||||
nameEl.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
setValue(node);
|
||||
});
|
||||
}
|
||||
|
||||
function rebuild(query) {
|
||||
panel.innerHTML = '';
|
||||
var searchWrap = document.createElement('div');
|
||||
searchWrap.style.cssText = 'padding:0.4rem 0.5rem;border-bottom:1px solid var(--color-border);position:sticky;top:0;background:var(--color-card);';
|
||||
var search = document.createElement('input');
|
||||
search.type = 'search';
|
||||
search.className = 'input';
|
||||
search.placeholder = 'Поиск…';
|
||||
search.style.cssText = 'width:100%;font-size:0.75rem;padding:0.25rem 0.5rem;height:auto;box-sizing:border-box;';
|
||||
search.value = query || '';
|
||||
searchWrap.appendChild(search);
|
||||
panel.appendChild(searchWrap);
|
||||
|
||||
var list = document.createElement('div');
|
||||
list.style.cssText = 'max-height:12.5rem;overflow-y:auto;padding:0.35rem;';
|
||||
panel.appendChild(list);
|
||||
|
||||
var clearRow = document.createElement('div');
|
||||
clearRow.textContent = emptyLabel;
|
||||
clearRow.style.cssText = 'padding:0.35rem 0.5rem;border-radius:calc(var(--radius) - 2px);cursor:pointer;font-size:0.8125rem;font-weight:500;color:var(--color-muted-foreground);margin-bottom:0.15rem;';
|
||||
clearRow.onmouseover = function () { clearRow.style.background = 'var(--color-muted)'; };
|
||||
clearRow.onmouseout = function () { clearRow.style.background = ''; };
|
||||
clearRow.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
setValue(null);
|
||||
});
|
||||
list.appendChild(clearRow);
|
||||
|
||||
var q = (query || '').trim().toLowerCase();
|
||||
readTree(treeDataId).forEach(function (node) {
|
||||
renderNode(node, list, 0, q);
|
||||
});
|
||||
|
||||
search.addEventListener('input', function () {
|
||||
var pos = search.selectionStart;
|
||||
rebuild(search.value);
|
||||
var again = panel.querySelector('input[type="search"]');
|
||||
if (again) {
|
||||
again.focus();
|
||||
try { again.setSelectionRange(pos, pos); } catch (e) {}
|
||||
}
|
||||
});
|
||||
search.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||
}
|
||||
|
||||
function open(e) {
|
||||
if (e) e.stopPropagation();
|
||||
rebuild('');
|
||||
panel.style.display = 'block';
|
||||
var search = panel.querySelector('input[type="search"]');
|
||||
if (search) search.focus();
|
||||
document.addEventListener('click', function onDoc(ev) {
|
||||
if (!wrap.contains(ev.target)) {
|
||||
close();
|
||||
document.removeEventListener('click', onDoc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function (e) {
|
||||
if (panel.style.display === 'none') open(e);
|
||||
else close();
|
||||
});
|
||||
|
||||
if (initial) {
|
||||
var node = findNode(readTree(treeDataId), initial);
|
||||
if (node) {
|
||||
label.textContent = buildPathLabel(node);
|
||||
label.title = label.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
wrap._deptTreeSetValue = setValue;
|
||||
wrap._deptTreeInput = input;
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** Invalidate cache after HTMX swaps new json_script. */
|
||||
function clearCache() {
|
||||
treeCache = {};
|
||||
}
|
||||
|
||||
global.DeptTreePicker = {
|
||||
create: create,
|
||||
clearCache: clearCache,
|
||||
readTree: readTree,
|
||||
};
|
||||
})(window);
|
||||
@@ -40,6 +40,7 @@
|
||||
<script src="{% static 'assets/js/timetrack.js' %}"></script>
|
||||
<script src="{% static 'assets/js/utilities.js' %}"></script>
|
||||
<script src="{% static 'assets/js/people-picker.js' %}"></script>
|
||||
<script src="{% static 'assets/js/dept-tree-picker.js' %}"></script>
|
||||
<script src="{% static 'assets/js/searchable-select.js' %}"></script>
|
||||
<script src="{% static 'assets/js/hr-portal.js' %}"></script>
|
||||
<script src="{% static 'assets/js/tasks.js' %}"></script>
|
||||
|
||||
@@ -102,12 +102,7 @@
|
||||
<form method="post" action="{% url 'project_add_step' project.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="title" required class="input" style="width:100%; margin-bottom:0.5rem;" placeholder="{% trans 'Название шага' %}">
|
||||
<select name="department_id" class="input" style="width:100%; margin-bottom:0.5rem;">
|
||||
<option value="">{% trans "Отдел шага по умолчанию (необяз.)…" %}</option>
|
||||
{% for d in departments %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div id="project-add-step-dept" style="margin-bottom:0.5rem;"></div>
|
||||
<textarea name="items" class="input" rows="4" style="width:100%; margin-bottom:0.75rem;"
|
||||
placeholder="{% trans 'IT | Настроить сеть HR | Нанять персонал или просто название (возьмёт отдел шага)' %}"></textarea>
|
||||
<p style="font-size:0.75rem; color:var(--color-muted-foreground); margin:0 0 0.75rem;">
|
||||
@@ -118,3 +113,17 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ departments_tree|json_script:"proj-departments-tree" }}
|
||||
<script>
|
||||
(function () {
|
||||
var mount = document.getElementById('project-add-step-dept');
|
||||
if (!mount || !window.DeptTreePicker) return;
|
||||
DeptTreePicker.clearCache();
|
||||
mount.appendChild(DeptTreePicker.create({
|
||||
name: 'department_id',
|
||||
emptyLabel: '{% trans "Отдел шага по умолчанию (необяз.)…" %}',
|
||||
treeDataId: 'proj-departments-tree',
|
||||
}));
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ departments_json|json_script:"proj-departments-data" }}
|
||||
{{ departments_tree|json_script:"proj-departments-tree" }}
|
||||
|
||||
<style>
|
||||
dialog.proj-dialog {
|
||||
@@ -150,6 +150,8 @@
|
||||
padding: 0;
|
||||
width: min(40rem, calc(100vw - 1.5rem));
|
||||
max-height: min(90vh, 52rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: visible;
|
||||
background: var(--color-card);
|
||||
color: var(--color-foreground);
|
||||
margin: auto;
|
||||
@@ -158,8 +160,7 @@
|
||||
dialog.proj-dialog::backdrop { background: rgb(0 0 0 / 0.45); }
|
||||
.proj-dialog-body {
|
||||
padding: 1.25rem 1.25rem 0.5rem;
|
||||
overflow-y: auto;
|
||||
max-height: calc(min(90vh, 52rem) - 4.5rem);
|
||||
overflow: visible;
|
||||
}
|
||||
.proj-dialog-foot {
|
||||
display: flex;
|
||||
@@ -196,6 +197,7 @@
|
||||
padding: 0.85rem;
|
||||
margin-bottom: 0.65rem;
|
||||
background: var(--color-muted);
|
||||
overflow: visible;
|
||||
}
|
||||
.proj-step-head {
|
||||
display: flex;
|
||||
@@ -206,11 +208,12 @@
|
||||
.proj-step-head .input { flex: 1; }
|
||||
.proj-item-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7rem, 10rem) 1fr auto;
|
||||
grid-template-columns: minmax(10rem, 15rem) 1fr auto;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
.proj-item-row .dept-tree-picker { min-width: 0; }
|
||||
@media (max-width: 520px) {
|
||||
.proj-item-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -277,18 +280,22 @@
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function depts() {
|
||||
var el = document.getElementById('proj-departments-data');
|
||||
try { return el ? JSON.parse(el.textContent) : []; } catch (e) { return []; }
|
||||
}
|
||||
var TREE_ID = 'proj-departments-tree';
|
||||
var EMPTY = '{% trans "Отдел…" %}';
|
||||
|
||||
function deptOptionsHtml(selected) {
|
||||
var opts = '<option value="">' + '{% trans "Отдел…" %}' + '</option>';
|
||||
depts().forEach(function (d) {
|
||||
var sel = String(selected || '') === String(d.id) ? ' selected' : '';
|
||||
opts += '<option value="' + d.id + '"' + sel + '>' + (d.name || '') + '</option>';
|
||||
});
|
||||
return opts;
|
||||
function makeDeptPicker(name) {
|
||||
if (window.DeptTreePicker) {
|
||||
return DeptTreePicker.create({
|
||||
name: name,
|
||||
emptyLabel: EMPTY,
|
||||
treeDataId: TREE_ID,
|
||||
});
|
||||
}
|
||||
var sel = document.createElement('select');
|
||||
sel.name = name;
|
||||
sel.className = 'input';
|
||||
sel.innerHTML = '<option value="">' + EMPTY + '</option>';
|
||||
return sel;
|
||||
}
|
||||
|
||||
function reindexSteps(builderId) {
|
||||
@@ -310,11 +317,27 @@
|
||||
var list = block.querySelector('[data-items]');
|
||||
var row = document.createElement('div');
|
||||
row.className = 'proj-item-row';
|
||||
row.innerHTML =
|
||||
'<select name="item_department_id" class="input">' + deptOptionsHtml() + '</select>' +
|
||||
'<input type="text" name="item_title" class="input" placeholder="{% trans "Название задачи" %}" required>' +
|
||||
'<input type="hidden" name="item_step" value="' + stepIndex + '">' +
|
||||
'<button type="button" class="btn btn-secondary" style="font-size:0.7rem;" onclick="this.parentElement.remove()">×</button>';
|
||||
var dept = makeDeptPicker('item_department_id');
|
||||
var hid = document.createElement('input');
|
||||
hid.type = 'hidden';
|
||||
hid.name = 'item_step';
|
||||
hid.value = stepIndex;
|
||||
dept.appendChild(hid);
|
||||
row.appendChild(dept);
|
||||
var title = document.createElement('input');
|
||||
title.type = 'text';
|
||||
title.name = 'item_title';
|
||||
title.className = 'input';
|
||||
title.placeholder = '{% trans "Название задачи" %}';
|
||||
title.required = true;
|
||||
row.appendChild(title);
|
||||
var rm = document.createElement('button');
|
||||
rm.type = 'button';
|
||||
rm.className = 'btn btn-secondary';
|
||||
rm.style.fontSize = '0.7rem';
|
||||
rm.textContent = '×';
|
||||
rm.onclick = function () { row.remove(); };
|
||||
row.appendChild(rm);
|
||||
list.appendChild(row);
|
||||
};
|
||||
|
||||
@@ -326,33 +349,62 @@
|
||||
block.className = 'proj-step-block';
|
||||
block.setAttribute('data-step-block', '1');
|
||||
block.setAttribute('data-step-index', String(idx));
|
||||
var timing = '';
|
||||
|
||||
var head = document.createElement('div');
|
||||
head.className = 'proj-step-head';
|
||||
head.innerHTML =
|
||||
'<div style="flex:1;">' +
|
||||
'<div data-step-label style="font-size:0.7rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Шаг" %} ' + (idx + 1) + '</div>' +
|
||||
'<input type="text" name="step_title" class="input" placeholder="{% trans "Название шага" %}" required>' +
|
||||
'</div>';
|
||||
var rmStep = document.createElement('button');
|
||||
rmStep.type = 'button';
|
||||
rmStep.className = 'btn btn-secondary';
|
||||
rmStep.style.cssText = 'font-size:0.7rem;margin-top:1.1rem;';
|
||||
rmStep.textContent = '×';
|
||||
rmStep.onclick = function () {
|
||||
block.remove();
|
||||
reindexSteps(builderId);
|
||||
};
|
||||
head.appendChild(rmStep);
|
||||
block.appendChild(head);
|
||||
|
||||
var deptField = document.createElement('div');
|
||||
deptField.className = 'proj-field';
|
||||
deptField.style.marginBottom = '0.5rem';
|
||||
var deptLabel = document.createElement('label');
|
||||
deptLabel.textContent = '{% trans "Отдел шага (по умолчанию)" %}';
|
||||
deptField.appendChild(deptLabel);
|
||||
deptField.appendChild(makeDeptPicker('step_department_id'));
|
||||
block.appendChild(deptField);
|
||||
|
||||
if (withTiming) {
|
||||
timing =
|
||||
'<div class="proj-row-2" style="margin-bottom:0.5rem;">' +
|
||||
'<div><label style="font-size:0.7rem;">{% trans "Смещение, дн." %}</label>' +
|
||||
'<input type="number" name="step_offset_days" class="input" value="0" min="0"></div>' +
|
||||
'<div><label style="font-size:0.7rem;">{% trans "Длительность, дн." %}</label>' +
|
||||
'<input type="number" name="step_duration_days" class="input" value="14" min="1"></div>' +
|
||||
'</div>';
|
||||
var timing = document.createElement('div');
|
||||
timing.className = 'proj-row-2';
|
||||
timing.style.marginBottom = '0.5rem';
|
||||
timing.innerHTML =
|
||||
'<div><label style="font-size:0.7rem;">{% trans "Смещение, дн." %}</label>' +
|
||||
'<input type="number" name="step_offset_days" class="input" value="0" min="0"></div>' +
|
||||
'<div><label style="font-size:0.7rem;">{% trans "Длительность, дн." %}</label>' +
|
||||
'<input type="number" name="step_duration_days" class="input" value="14" min="1"></div>';
|
||||
block.appendChild(timing);
|
||||
}
|
||||
block.innerHTML =
|
||||
'<div class="proj-step-head">' +
|
||||
'<div style="flex:1;">' +
|
||||
'<div data-step-label style="font-size:0.7rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Шаг" %} ' + (idx + 1) + '</div>' +
|
||||
'<input type="text" name="step_title" class="input" placeholder="{% trans "Название шага" %}" required>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="btn btn-secondary" style="font-size:0.7rem; margin-top:1.1rem;" onclick="this.closest(\'[data-step-block]\').remove(); reindexSteps(\'' + builderId + '\')">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="proj-field" style="margin-bottom:0.5rem;">' +
|
||||
'<label>{% trans "Отдел шага (по умолчанию)" %}</label>' +
|
||||
'<select name="step_department_id" class="input">' + deptOptionsHtml() + '</select>' +
|
||||
'</div>' +
|
||||
timing +
|
||||
'<div data-items></div>' +
|
||||
'<button type="button" class="btn btn-secondary" style="font-size:0.7rem;" data-add-item onclick="addItemRow(this)">{% trans "+ Задача" %}</button>';
|
||||
|
||||
var items = document.createElement('div');
|
||||
items.setAttribute('data-items', '');
|
||||
block.appendChild(items);
|
||||
|
||||
var addItem = document.createElement('button');
|
||||
addItem.type = 'button';
|
||||
addItem.className = 'btn btn-secondary';
|
||||
addItem.style.fontSize = '0.7rem';
|
||||
addItem.setAttribute('data-add-item', '');
|
||||
addItem.textContent = '{% trans "+ Задача" %}';
|
||||
addItem.onclick = function () { addItemRow(addItem); };
|
||||
block.appendChild(addItem);
|
||||
|
||||
root.appendChild(block);
|
||||
addItemRow(block.querySelector('[data-add-item]'));
|
||||
addItemRow(addItem);
|
||||
reindexSteps(builderId);
|
||||
};
|
||||
|
||||
@@ -365,6 +417,7 @@
|
||||
};
|
||||
|
||||
window.openProjectDialog = function (templateId) {
|
||||
if (window.DeptTreePicker) DeptTreePicker.clearCache();
|
||||
var dlg = document.getElementById('project-create');
|
||||
var mode = document.getElementById('project-mode');
|
||||
if (templateId) {
|
||||
@@ -379,7 +432,7 @@
|
||||
dlg.showModal();
|
||||
};
|
||||
|
||||
// Первый шаг шаблона сразу на вкладке
|
||||
if (window.DeptTreePicker) DeptTreePicker.clearCache();
|
||||
var tplBuilder = document.getElementById('template-steps-builder');
|
||||
if (tplBuilder && !tplBuilder.children.length) {
|
||||
addStepBlock('template-steps-builder', true);
|
||||
|
||||
Reference in New Issue
Block a user