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