From 9980794f0cbddaafd2eff1cac7b82dd6f1d9fba8 Mon Sep 17 00:00:00 2001 From: Johanna Lee <123130134+johyunjihyunji@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:40:46 -0500 Subject: [PATCH 01/16] add input text autocomplete for lead bind and pscheduler address --- .../components/input-text-autocomplete.js | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 pscompose/frontend/components/input-text-autocomplete.js diff --git a/pscompose/frontend/components/input-text-autocomplete.js b/pscompose/frontend/components/input-text-autocomplete.js new file mode 100644 index 0000000..6a904a9 --- /dev/null +++ b/pscompose/frontend/components/input-text-autocomplete.js @@ -0,0 +1,105 @@ +import { InputText } from "./input-text.js"; + +class InputTextAutocomplete extends InputText { + constructor() { + super(); + this.slotEl = ` + + `; + this.inputEl = null; + this.wrapperEl = null; + this.optionsEl = null; + this.actionBtn = null; + this.autocompleteOptions = [ + "{% address[0] %}", + "{% address[1] %}", + "{% pscheduler_address[0] %}", + "{% pscheduler_address[1] %}", + "{% flip %}", + "{% localhost %}", + "{% scheduled_by_address %}", + "{% lead_bind_address[0] %}", + "{% lead_bind_address[1] %}", + ]; + } + + attachAutocompleteEventListener() { + this.inputEl.addEventListener("input", (e) => this.handleInput(e)); + document.addEventListener("click", (e) => this.closeDropdownOutside(e)); + } + + handleInput(e) { + const value = e.target.value; + const cursorPos = e.target.selectionStart; + + const beforeCursor = value.slice(0, cursorPos); + if (!beforeCursor.trim().startsWith("{%")) { + this.closeDropdown(); + return; + } + + const typed = beforeCursor.replace("{%", "").trim().toLowerCase(); + this.showAutocompleteDropdown(typed); + } + + showAutocompleteDropdown(filterText = "") { + this.optionsEl.innerHTML = ""; + this.optionsEl.classList.add("open"); + + const filtered = this.autocompleteOptions.filter((opt) => + opt.toLowerCase().includes(filterText), + ); + + if (filtered.length === 0) { + this.closeDropdown(); + return; + } + + filtered.forEach((option) => { + const li = document.createElement("li"); + li.textContent = option; + li.dataset.value = option; + + li.addEventListener("mousedown", (e) => { + e.preventDefault(); // prevents blur + this.insertAutocompleteValue(option); + }); + + this.optionsEl.appendChild(li); + }); + } + + insertAutocompleteValue(value) { + this.inputEl.value = value; + this.inputEl.dispatchEvent(new Event("change", { bubbles: true })); + this.closeDropdown(); + } + + closeDropdownOutside(e) { + if (!this.contains(e.target)) { + this.closeDropdown(); + } + } + + closeDropdown() { + this.optionsEl.classList.remove("open"); + this.optionsEl.innerHTML = ""; + } + + render() { + super.render(); + + this.inputEl = this.querySelector("input"); + this.wrapperEl = this.querySelector(".wrapper"); + this.optionsEl = this.querySelector(".options"); + + this.attachAutocompleteEventListener(); + } +} + +customElements.define("input-text-autocomplete", InputTextAutocomplete); From 5904aadc35f2402613d83ad40c42cceed38a5ecd Mon Sep 17 00:00:00 2001 From: Johanna Lee <123130134+johyunjihyunji@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:42:33 -0500 Subject: [PATCH 02/16] add checkbox star for favorited --- .../components/input-checkbox-star.js | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 pscompose/frontend/components/input-checkbox-star.js diff --git a/pscompose/frontend/components/input-checkbox-star.js b/pscompose/frontend/components/input-checkbox-star.js new file mode 100644 index 0000000..483a5a2 --- /dev/null +++ b/pscompose/frontend/components/input-checkbox-star.js @@ -0,0 +1,62 @@ +class InputCheckboxStar extends HTMLElement { + static get observedAttributes() { + return ["value", "disabled"]; + } + + get value() { + return this._value === true; + } + + set value(v) { + this._value = Boolean(v); + this.setAttribute("value", this._value); + } + + get disabled() { + return this.hasAttribute("disabled"); + } + + set disabled(v) { + v ? this.setAttribute("disabled", "") : this.removeAttribute("disabled"); + } + + constructor() { + super(); + } + + connectedCallback() { + this.render(); + lucide.createIcons(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue == newValue) return; + this.render(); + lucide.createIcons(); + } + + toggle() { + if (this.hasAttribute("disabled")) return; + + this.value = !this.value; + this.dispatchEvent(new Event("change", { bubbles: true })); + } + + attachEventListener() { + this.starEl.addEventListener("click", () => this.toggle()); + } + + render() { + const disabled = this.hasAttribute("disabled"); + + this.innerHTML = ``; + this.starEl = this.querySelector("button"); + this.attachEventListener(); + } +} + +customElements.define("input-checkbox-star", InputCheckboxStar); From b84b3a82c64c2018b355a6d9b73de10e8fbe1fdf Mon Sep 17 00:00:00 2001 From: Johanna Lee <123130134+johyunjihyunji@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:50:43 -0500 Subject: [PATCH 03/16] adding favorites backend in datatype --- pscompose/api/api.py | 14 +++++++++++++- pscompose/api/routers/addresses.py | 1 + pscompose/api/routers/archives.py | 1 + pscompose/api/routers/contexts.py | 1 + pscompose/api/routers/groups.py | 1 + pscompose/api/routers/home.py | 23 +++++++++++++++++++++++ pscompose/api/routers/schedules.py | 1 + pscompose/api/routers/tasks.py | 1 + pscompose/api/routers/templates.py | 1 + pscompose/api/routers/tests.py | 1 + pscompose/backends/postgres.py | 21 ++++++++++++++++++++- pscompose/models.py | 2 ++ pscompose/schemas.py | 2 ++ pscompose/utils.py | 1 + 14 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 pscompose/api/routers/home.py diff --git a/pscompose/api/api.py b/pscompose/api/api.py index 236f997..40101f7 100644 --- a/pscompose/api/api.py +++ b/pscompose/api/api.py @@ -11,6 +11,7 @@ tasks, templates, tests, + home, ) # initialize FastAPI application @@ -25,7 +26,18 @@ ) # include submodule routers -for lib in [addresses, archives, basic_auth, contexts, groups, schedules, tasks, templates, tests]: +for lib in [ + addresses, + archives, + basic_auth, + contexts, + groups, + schedules, + tasks, + templates, + tests, + home, +]: app.include_router(lib.router) diff --git a/pscompose/api/routers/addresses.py b/pscompose/api/routers/addresses.py index 6ba0fd9..663a7b7 100644 --- a/pscompose/api/routers/addresses.py +++ b/pscompose/api/routers/addresses.py @@ -62,6 +62,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/archives.py b/pscompose/api/routers/archives.py index ecf01b3..291b7e6 100644 --- a/pscompose/api/routers/archives.py +++ b/pscompose/api/routers/archives.py @@ -55,6 +55,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/contexts.py b/pscompose/api/routers/contexts.py index 75b76f7..1165698 100644 --- a/pscompose/api/routers/contexts.py +++ b/pscompose/api/routers/contexts.py @@ -53,6 +53,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/groups.py b/pscompose/api/routers/groups.py index 5371788..fca6a98 100644 --- a/pscompose/api/routers/groups.py +++ b/pscompose/api/routers/groups.py @@ -99,6 +99,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Group with id: {item_id} not found") diff --git a/pscompose/api/routers/home.py b/pscompose/api/routers/home.py new file mode 100644 index 0000000..7943ad2 --- /dev/null +++ b/pscompose/api/routers/home.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter +from pscompose.backends.postgres import backend + +# Setup CRUD endpoints +router = APIRouter(tags=["home"]) + + +@router.get( + "/api/recently_edited", + summary="Return first 3 items of most recently edited", +) +def recently_edited(): + rows = backend.get_recently_edited(3) + return rows + + +@router.get( + "/api/favorites", + summary="Return first 3 items of favorites", +) +def favorites(): + rows = backend.get_favorites(3) + return rows diff --git a/pscompose/api/routers/schedules.py b/pscompose/api/routers/schedules.py index 7da043e..8180c0b 100644 --- a/pscompose/api/routers/schedules.py +++ b/pscompose/api/routers/schedules.py @@ -34,6 +34,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/tasks.py b/pscompose/api/routers/tasks.py index fd43ec3..e18dee6 100644 --- a/pscompose/api/routers/tasks.py +++ b/pscompose/api/routers/tasks.py @@ -55,6 +55,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Task with id: {item_id} not found") diff --git a/pscompose/api/routers/templates.py b/pscompose/api/routers/templates.py index 20ad91d..aa303e4 100644 --- a/pscompose/api/routers/templates.py +++ b/pscompose/api/routers/templates.py @@ -36,6 +36,7 @@ def get_existing_form(item_id: str): response = backend.get_datatype(datatype=DataTypes.TEMPLATE, item_id=item_id) response_json = response.json response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Address with id: {item_id} not found") diff --git a/pscompose/api/routers/tests.py b/pscompose/api/routers/tests.py index 76e3ac8..6350ee2 100644 --- a/pscompose/api/routers/tests.py +++ b/pscompose/api/routers/tests.py @@ -252,6 +252,7 @@ def get_existing_form(item_id: str): } # Need to remove null fields response_json["name"] = response.name # Adding "name" since it's not present in the json + response_json["favorited"] = response.favorited except HTTPException: raise HTTPException(status_code=404, detail=f"Test with id: {item_id} not found") diff --git a/pscompose/backends/postgres.py b/pscompose/backends/postgres.py index 8de5ab9..7284a3d 100644 --- a/pscompose/backends/postgres.py +++ b/pscompose/backends/postgres.py @@ -15,7 +15,9 @@ def __init__(self): DataTable.__table__.create(bind=engine, checkfirst=True) self.session = sessionmaker(bind=engine)() - def create_datatype(self, ref_set, datatype, json, name, created_by, last_edited_by): + def create_datatype( + self, ref_set, datatype, json, name, created_by, last_edited_by, favorited + ): try: new_type = DataTable( ref_set=ref_set, @@ -24,6 +26,7 @@ def create_datatype(self, ref_set, datatype, json, name, created_by, last_edited name=name, created_by=created_by, last_edited_by=last_edited_by, + favorited=favorited, # created_at = created_at, # last_edited_at = last_edited_at, # url = url @@ -61,6 +64,7 @@ def update_datatype(self, existing_result, updated_data): "ref_set": existing_result.ref_set, "last_edited_by": existing_result.last_edited_by, "last_edited_at": existing_result.last_edited_at, + "favorited": existing_result.favorited, } except Exception as e: self.session.rollback() @@ -179,5 +183,20 @@ def get_datatype(self, datatype, item_id): else: return result + def get_recently_edited(self, limit: int = 5): + query = self.session.query(DataTable).order_by(DataTable.created_at.desc()).limit(limit) + rows = query.all() + return [row for row in rows] + + def get_favorites(self, limit: int = 5): + query = ( + self.session.query(DataTable) + .filter_by(favorited=True) + .order_by(DataTable.created_at.desc()) + .limit(limit) + ) + rows = query.all() + return [row for row in rows] + backend = PostgresBackend() diff --git a/pscompose/models.py b/pscompose/models.py index 5172f72..72c990d 100644 --- a/pscompose/models.py +++ b/pscompose/models.py @@ -13,6 +13,7 @@ LargeBinary, Integer, Text, + Boolean, String, DateTime, func, @@ -115,3 +116,4 @@ class DataTable(SQLAlchemyStorage): onupdate=func.now(), nullable=False, # enforces that every record has a timestamp ) + favorited = Column(Boolean, default=False) diff --git a/pscompose/schemas.py b/pscompose/schemas.py index 46e3218..4f065de 100644 --- a/pscompose/schemas.py +++ b/pscompose/schemas.py @@ -345,6 +345,7 @@ class DataTableBase(BaseModel): last_edited_by: str last_edited_at: Optional[datetime] = None url: Optional[str] = None + favorited: bool = Field(default=False) class Config: orm_mode = True @@ -371,6 +372,7 @@ class DataTableUpdate(BaseModel): last_edited_by: Optional[str] = None last_edited_at: Optional[datetime] = None url: Optional[str] = None + favorited: bool = Field(default=False) pSConfigSchema.update_forward_refs() diff --git a/pscompose/utils.py b/pscompose/utils.py index 40d867d..92c7c23 100644 --- a/pscompose/utils.py +++ b/pscompose/utils.py @@ -60,6 +60,7 @@ def create_item( created_by=data.created_by, # created_by=user.name, last_edited_by=data.last_edited_by, + favorited=data.favorited, # last_edited_by=user.name, ) return {"message": f"{datatype} created successfully", "id": response.id} From f7b9390d45577975ce7c4c2fcf95dea532f562c5 Mon Sep 17 00:00:00 2001 From: Johanna Lee <123130134+johyunjihyunji@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:51:35 -0500 Subject: [PATCH 04/16] add the new componenets to custom renderer --- pscompose/frontend/partials/customRenderer.js | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pscompose/frontend/partials/customRenderer.js b/pscompose/frontend/partials/customRenderer.js index 32e975f..e5b010e 100644 --- a/pscompose/frontend/partials/customRenderer.js +++ b/pscompose/frontend/partials/customRenderer.js @@ -8,8 +8,10 @@ function toAllCaps(str) { const webComponents = [ "input-text", "input-text-area", + "input-text-autocomplete", "input-number", "input-checkbox", + "input-checkbox-star", "dropdown-single-select", "dropdown-multi-select", "dropdown-excludes", @@ -85,27 +87,21 @@ document.body.addEventListener("json-form:beforeMount", (event) => { document.body.addEventListener("json-form:mounted", (event) => { if (event.detail[0].target.readonly == "true") { webComponents.forEach((component) => { - document - .querySelector("form") - .querySelectorAll(component) - .forEach((comp) => { - comp.disabled = true; - }); + document.querySelectorAll(component).forEach((comp) => { + comp.disabled = true; + }); }); } }); document.body.addEventListener("json-form:updated", (event) => { webComponents.forEach((component) => { - document - .querySelector("form") - .querySelectorAll(component) - .forEach((comp) => { - if (event.detail[0].target.readonly == "true") { - comp.disabled = true; - } else { - comp.disabled = false; - } - }); + document.querySelectorAll(component).forEach((comp) => { + if (event.detail[0].target.readonly == "true") { + comp.disabled = true; + } else { + comp.disabled = false; + } + }); }); }); From c63d0727ae158f6f491a2e9fdbb4187bad5f519c Mon Sep 17 00:00:00 2001 From: Johanna Lee <123130134+johyunjihyunji@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:52:04 -0500 Subject: [PATCH 05/16] remove comments from input text --- pscompose/frontend/components/input-text.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/pscompose/frontend/components/input-text.js b/pscompose/frontend/components/input-text.js index 61385cb..4bb7db6 100644 --- a/pscompose/frontend/components/input-text.js +++ b/pscompose/frontend/components/input-text.js @@ -28,8 +28,6 @@ export class InputText extends FormControl { } this.dispatchEvent(new Event("change", { bubbles: true })); }); - // this.inputEl?.addEventListener("input", () => this.markDirty(), { once: true }); - // this.inputEl?.addEventListener("blur", () => this.markDirty(), { once: true }); } render() { From ecd0953185cb454776fe20ce0d4a278b08d01ef7 Mon Sep 17 00:00:00 2001 From: Johanna Lee <123130134+johyunjihyunji@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:52:51 -0500 Subject: [PATCH 06/16] change submenu names to singular version --- pscompose/frontend/partials/nav_left.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pscompose/frontend/partials/nav_left.html b/pscompose/frontend/partials/nav_left.html index a842364..7aa75da 100644 --- a/pscompose/frontend/partials/nav_left.html +++ b/pscompose/frontend/partials/nav_left.html @@ -1,42 +1,42 @@