Early work for saving work_logs.

This commit is contained in:
Yaro Kasear 2025-10-02 11:14:42 -05:00
parent 01c6bb3d09
commit 85b0e576c7
3 changed files with 112 additions and 87 deletions

View file

@ -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")

View file

@ -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 = {};

View file

@ -1,106 +1,125 @@
{% set items = (field.template_ctx.instance.updates or []) %} {% set items = (field.template_ctx.instance.updates or []) %}
<ul class="list-group mt-3"> <ul class="list-group mt-3">
{% for n in items %} {% for n in items %}
<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 }}">
<small class="text-muted text-nowrap"> <small class="text-muted text-nowrap">
{{ n.timestamp.strftime("%Y-%m-%d %H:%M") if n.timestamp else "" }} {{ n.timestamp.strftime("%Y-%m-%d %H:%M") if n.timestamp else "" }}
</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>
</li> </li>
{% else %} {% else %}
<li class="list-group-item text-muted">No updates yet.</li> <li class="list-group-item text-muted">No updates yet.</li>
{% 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 %} document.addEventListener('DOMContentLoaded', () => {
contents[{{ n.id }}] = {{ n.content | tojson }}; const ids = [ {% for n in items %} {{ n.id }}{% if not loop.last %}, {% endif %}{% endfor %} ];
{% endfor %} for (const id of ids) renderView(id, getMarkdown(id));
});
document.addEventListener('DOMContentLoaded', () => { function getMarkdown(id) {
for (const [id, md] of Object.entries(contents)) { const el = document.getElementById(`md-${id}`);
renderView(+id, md); 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]')) {
}); a.setAttribute('target','_blank');
for (const a of container.querySelectorAll('a[href]')) { a.setAttribute('rel','noopener noreferrer nofollow');
a.setAttribute('target', '_blank');
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 container.innerHTML = `
const current = contents[id] ?? ""; <textarea class="form-control w-100 auto-md" id="editor${id}">${escapeForTextarea(current)}</textarea>
container.dataset.prev = container.innerHTML; <div class="mt-2 d-flex gap-2">
<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-outline-secondary btn-sm" onclick="togglePreview(${id})">Preview</button>
</div>
<div class="mt-2 border rounded p-2 d-none" id="preview${id}" aria-live="polite"></div>
`;
const ta = document.getElementById(`editor${id}`);
autoGrow(ta);
ta.addEventListener('input', () => autoGrow(ta));
}
container.innerHTML = ` function saveEdit(id) {
<textarea class="form-control w-100" id="editor${id}" rows="6">${escapeForTextarea(current)}</textarea> const ta = document.getElementById(`editor${id}`);
<div class="mt-2 d-flex gap-2"> const value = ta ? ta.value : "";
<button class="btn btn-primary btn-sm" onclick="saveEdit(${id})">Save</button> setMarkdown(id, value); // persist into the JSON tag
<button class="btn btn-secondary btn-sm" onclick="cancelEdit(${id})">Cancel</button> renderView(id, value); // show it
<button class="btn btn-outline-secondary btn-sm" onclick="togglePreview(${id})">Preview</button> document.getElementById(`editSwitch${id}`).checked = false;
</div> }
<div class="mt-2 border rounded p-2 d-none" id="preview${id}"></div>
`; function cancelEdit(id) {
} else { document.getElementById(`editSwitch${id}`).checked = false;
// Switch to viewer mode renderView(id, getMarkdown(id));
renderView(id, contents[id]); }
}
function togglePreview(id) {
const ta = document.getElementById(`editor${id}`);
const preview = document.getElementById(`preview${id}`);
preview.classList.toggle('d-none');
if (!preview.classList.contains('d-none')) {
const html = marked.parse(ta ? ta.value : "");
preview.innerHTML = DOMPurify.sanitize(html);
} }
}
function saveEdit(id) { function autoGrow(ta) {
const textarea = document.getElementById(`editor${id}`); if (!ta) return;
const value = textarea.value; ta.style.height = 'auto';
contents[id] = value; ta.style.height = ta.scrollHeight + 'px';
renderView(id, value); }
document.getElementById(`editSwitch${id}`).checked = false; function escapeForTextarea(s) {
} return (s ?? "").replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function cancelEdit(id) {
document.getElementById(`editSwitch${id}`).checked = false;
renderView(id, contents[id]);
}
function togglePreview(id) {
const textarea = document.getElementById(`editor${id}`);
const preview = document.getElementById(`preview${id}`);
preview.classList.toggle('d-none');
if (!preview.classList.contains('d-none')) {
const html = marked.parse(textarea.value ?? "");
preview.innerHTML = DOMPurify.sanitize(html);
}
}
function escapeForTextarea(s) {
// Keep control of what goes inside the textarea
return (s ?? "").replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
</script> </script>