Early work for saving work_logs.
This commit is contained in:
parent
01c6bb3d09
commit
85b0e576c7
3 changed files with 112 additions and 87 deletions
|
|
@ -191,11 +191,14 @@ def init_entry_routes(app):
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"complete",
|
"complete",
|
||||||
|
"updates",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise TypeError("Invalid model.")
|
raise TypeError("Invalid model.")
|
||||||
|
|
||||||
|
print(payload)
|
||||||
|
|
||||||
service = crudkit.crud.get_service(cls)
|
service = crudkit.crud.get_service(cls)
|
||||||
service.update(id, data=payload, actor="update_entry")
|
service.update(id, data=payload, actor="update_entry")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<button type="submit" class="btn btn-primary" id="submit">Save</button>
|
<button type="submit" class="btn btn-primary" id="submit">Save</button>
|
||||||
<script>
|
<script>
|
||||||
|
window.newDrafts = window.newDrafts || [];
|
||||||
|
window.deletedIds = window.deletedIds || [];
|
||||||
|
|
||||||
function formToJson(form) {
|
function formToJson(form) {
|
||||||
const fd = new FormData(form);
|
const fd = new FormData(form);
|
||||||
const out = {};
|
const out = {};
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div class="me-3 w-100" id="editContainer{{ n.id }}"></div>
|
<div class="me-3 w-100" id="editContainer{{ n.id }}"></div>
|
||||||
|
<script type="application/json" id="md-{{ n.id }}">{{ n.content | tojson }}</script>
|
||||||
|
|
||||||
<div class="d-flex flex-column align-items-end">
|
<div class="d-flex flex-column align-items-end">
|
||||||
<div id="editView{{ n.id }}">
|
<div id="editView{{ n.id }}">
|
||||||
|
|
@ -13,8 +14,8 @@
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" class="form-check-input" id="editSwitch{{ n.id }}" switch onchange="changeMode({{ n.id }})">
|
<input class="form-check-input" type="checkbox" id="editSwitch{{ n.id }}" onchange="changeMode({{ n.id }})" role="switch" aria-label="Edit mode for update {{ n.id }}">
|
||||||
<label for="editSwitch" class="form-check-label">Edit</label>
|
<label for="editSwitch{{ n.id }}" class="form-check-label">Edit</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -24,83 +25,101 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
textarea.auto-md {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow: hidden;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (field-sizing: content) {
|
||||||
|
text-area.auto-md {
|
||||||
|
field-sizing: content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const contents = {};
|
// Initial render
|
||||||
{% for n in items %}
|
|
||||||
contents[{{ n.id }}] = {{ n.content | tojson }};
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
for (const [id, md] of Object.entries(contents)) {
|
const ids = [ {% for n in items %} {{ n.id }}{% if not loop.last %}, {% endif %}{% endfor %} ];
|
||||||
renderView(+id, md);
|
for (const id of ids) renderView(id, getMarkdown(id));
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getMarkdown(id) {
|
||||||
|
const el = document.getElementById(`md-${id}`);
|
||||||
|
return el ? JSON.parse(el.textContent) : "";
|
||||||
|
}
|
||||||
|
function setMarkdown(id, md) {
|
||||||
|
const el = document.getElementById(`md-${id}`);
|
||||||
|
if (el) el.textContent = JSON.stringify(md ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
function renderView(id, md) {
|
function renderView(id, md) {
|
||||||
const container = document.getElementById(`editContainer${id}`);
|
const container = document.getElementById(`editContainer${id}`);
|
||||||
const html = marked.parse(md ?? "");
|
if (!container) return;
|
||||||
|
const html = marked.parse(md || "");
|
||||||
container.innerHTML = DOMPurify.sanitize(html, {
|
container.innerHTML = DOMPurify.sanitize(html, { ADD_ATTR: ['target','rel'] });
|
||||||
ADD_ATTR: ['target', 'rel'],
|
|
||||||
});
|
|
||||||
for (const a of container.querySelectorAll('a[href]')) {
|
for (const a of container.querySelectorAll('a[href]')) {
|
||||||
a.setAttribute('target', '_blank');
|
a.setAttribute('target','_blank');
|
||||||
a.setAttribute('rel', 'noopener noreferrer nofollow')
|
a.setAttribute('rel','noopener noreferrer nofollow');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeMode(id) {
|
function changeMode(id) {
|
||||||
const container = document.getElementById(`editContainer${id}`);
|
const container = document.getElementById(`editContainer${id}`);
|
||||||
const toggle = document.getElementById(`editSwitch${id}`);
|
const toggle = document.getElementById(`editSwitch${id}`);
|
||||||
|
if (!toggle.checked) return renderView(id, getMarkdown(id));
|
||||||
|
|
||||||
if (toggle.checked) {
|
const current = getMarkdown(id);
|
||||||
// Switch to editor mode
|
|
||||||
const current = contents[id] ?? "";
|
|
||||||
container.dataset.prev = container.innerHTML;
|
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<textarea class="form-control w-100" id="editor${id}" rows="6">${escapeForTextarea(current)}</textarea>
|
<textarea class="form-control w-100 auto-md" id="editor${id}">${escapeForTextarea(current)}</textarea>
|
||||||
<div class="mt-2 d-flex gap-2">
|
<div class="mt-2 d-flex gap-2">
|
||||||
<button class="btn btn-primary btn-sm" onclick="saveEdit(${id})">Save</button>
|
<button class="btn btn-primary btn-sm" onclick="saveEdit(${id})">Save</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="cancelEdit(${id})">Cancel</button>
|
<button class="btn btn-secondary btn-sm" onclick="cancelEdit(${id})">Cancel</button>
|
||||||
<button class="btn btn-outline-secondary btn-sm" onclick="togglePreview(${id})">Preview</button>
|
<button class="btn btn-outline-secondary btn-sm" onclick="togglePreview(${id})">Preview</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 border rounded p-2 d-none" id="preview${id}"></div>
|
<div class="mt-2 border rounded p-2 d-none" id="preview${id}" aria-live="polite"></div>
|
||||||
`;
|
`;
|
||||||
} else {
|
const ta = document.getElementById(`editor${id}`);
|
||||||
// Switch to viewer mode
|
autoGrow(ta);
|
||||||
renderView(id, contents[id]);
|
ta.addEventListener('input', () => autoGrow(ta));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveEdit(id) {
|
function saveEdit(id) {
|
||||||
const textarea = document.getElementById(`editor${id}`);
|
const ta = document.getElementById(`editor${id}`);
|
||||||
const value = textarea.value;
|
const value = ta ? ta.value : "";
|
||||||
contents[id] = value;
|
setMarkdown(id, value); // persist into the JSON tag
|
||||||
renderView(id, value);
|
renderView(id, value); // show it
|
||||||
|
|
||||||
document.getElementById(`editSwitch${id}`).checked = false;
|
document.getElementById(`editSwitch${id}`).checked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelEdit(id) {
|
function cancelEdit(id) {
|
||||||
document.getElementById(`editSwitch${id}`).checked = false;
|
document.getElementById(`editSwitch${id}`).checked = false;
|
||||||
renderView(id, contents[id]);
|
renderView(id, getMarkdown(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePreview(id) {
|
function togglePreview(id) {
|
||||||
const textarea = document.getElementById(`editor${id}`);
|
const ta = document.getElementById(`editor${id}`);
|
||||||
const preview = document.getElementById(`preview${id}`);
|
const preview = document.getElementById(`preview${id}`);
|
||||||
preview.classList.toggle('d-none');
|
preview.classList.toggle('d-none');
|
||||||
if (!preview.classList.contains('d-none')) {
|
if (!preview.classList.contains('d-none')) {
|
||||||
const html = marked.parse(textarea.value ?? "");
|
const html = marked.parse(ta ? ta.value : "");
|
||||||
preview.innerHTML = DOMPurify.sanitize(html);
|
preview.innerHTML = DOMPurify.sanitize(html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function autoGrow(ta) {
|
||||||
|
if (!ta) return;
|
||||||
|
ta.style.height = 'auto';
|
||||||
|
ta.style.height = ta.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
function escapeForTextarea(s) {
|
function escapeForTextarea(s) {
|
||||||
// Keep control of what goes inside the textarea
|
|
||||||
return (s ?? "").replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
return (s ?? "").replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue