forked from retoor/devplacepy
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { Component } from "./Component.js";
|
||||
|
||||
export class AppDialog extends Component {
|
||||
connectedCallback() {
|
||||
if (this._built) {
|
||||
return;
|
||||
}
|
||||
this._built = true;
|
||||
this.resolver = null;
|
||||
this.mode = "confirm";
|
||||
this.lastFocus = null;
|
||||
this.build();
|
||||
}
|
||||
|
||||
build() {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "dialog-overlay";
|
||||
overlay.setAttribute("role", "dialog");
|
||||
overlay.setAttribute("aria-modal", "true");
|
||||
overlay.innerHTML =
|
||||
'<div class="card dialog-card">' +
|
||||
'<div class="modal-header"><h3 class="dialog-title"></h3>' +
|
||||
'<button type="button" class="modal-close btn-ghost btn-icon dialog-close">×</button></div>' +
|
||||
'<p class="dialog-message"></p>' +
|
||||
'<div class="dialog-field" hidden><label class="dialog-field-label"></label>' +
|
||||
'<input type="text" class="dialog-input" autocomplete="off"></div>' +
|
||||
'<div class="modal-footer">' +
|
||||
'<button type="button" class="btn btn-secondary dialog-cancel">Cancel</button>' +
|
||||
'<button type="button" class="btn btn-primary dialog-confirm">OK</button>' +
|
||||
"</div></div>";
|
||||
this.appendChild(overlay);
|
||||
|
||||
this.overlay = overlay;
|
||||
this.titleEl = overlay.querySelector(".dialog-title");
|
||||
this.messageEl = overlay.querySelector(".dialog-message");
|
||||
this.field = overlay.querySelector(".dialog-field");
|
||||
this.fieldLabel = overlay.querySelector(".dialog-field-label");
|
||||
this.input = overlay.querySelector(".dialog-input");
|
||||
this.cancelBtn = overlay.querySelector(".dialog-cancel");
|
||||
this.confirmBtn = overlay.querySelector(".dialog-confirm");
|
||||
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (e.target === overlay) this.dismiss();
|
||||
});
|
||||
overlay.querySelector(".dialog-close").addEventListener("click", () => this.dismiss());
|
||||
this.cancelBtn.addEventListener("click", () => this.dismiss());
|
||||
this.confirmBtn.addEventListener("click", () => this.accept());
|
||||
this.input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.accept();
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && this.overlay.classList.contains("visible")) {
|
||||
e.preventDefault();
|
||||
this.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
open(mode, options) {
|
||||
const opts = options || {};
|
||||
this.mode = mode;
|
||||
this.titleEl.textContent = opts.title || (mode === "prompt" ? "Rename" : mode === "alert" ? "Notice" : "Confirm");
|
||||
this.messageEl.textContent = opts.message || "";
|
||||
this.messageEl.hidden = !opts.message;
|
||||
this.confirmBtn.textContent = opts.confirmLabel || (mode === "alert" ? "OK" : mode === "prompt" ? "Save" : "Confirm");
|
||||
this.cancelBtn.textContent = opts.cancelLabel || "Cancel";
|
||||
this.cancelBtn.hidden = mode === "alert";
|
||||
this.confirmBtn.classList.toggle("dialog-danger", !!opts.danger);
|
||||
|
||||
if (mode === "prompt") {
|
||||
this.field.hidden = false;
|
||||
this.fieldLabel.textContent = opts.label || "";
|
||||
this.fieldLabel.hidden = !opts.label;
|
||||
this.input.value = opts.value || "";
|
||||
this.input.placeholder = opts.placeholder || "";
|
||||
} else {
|
||||
this.field.hidden = true;
|
||||
}
|
||||
|
||||
this.lastFocus = document.activeElement;
|
||||
this.overlay.classList.add("visible");
|
||||
const focusTarget = mode === "prompt" ? this.input : this.confirmBtn;
|
||||
setTimeout(() => {
|
||||
focusTarget.focus();
|
||||
if (mode === "prompt") this.input.select();
|
||||
}, 20);
|
||||
|
||||
return new Promise((resolve) => { this.resolver = resolve; });
|
||||
}
|
||||
|
||||
accept() {
|
||||
const value = this.mode === "prompt" ? this.input.value : true;
|
||||
this.close(value);
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.close(this.mode === "prompt" ? null : this.mode === "alert" ? undefined : false);
|
||||
}
|
||||
|
||||
close(value) {
|
||||
this.overlay.classList.remove("visible");
|
||||
if (this.lastFocus && this.lastFocus.focus) this.lastFocus.focus();
|
||||
const resolve = this.resolver;
|
||||
this.resolver = null;
|
||||
if (resolve) resolve(value);
|
||||
}
|
||||
|
||||
confirm(options) {
|
||||
return this.open("confirm", options);
|
||||
}
|
||||
|
||||
prompt(options) {
|
||||
return this.open("prompt", options);
|
||||
}
|
||||
|
||||
alert(options) {
|
||||
return this.open("alert", options);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("dp-dialog", AppDialog);
|
||||
Reference in New Issue
Block a user