Skip to content

Commit 3e4d59d

Browse files
authored
Reimplement user search in enterprise plans CRM and fix plan prefill (#4913)
1 parent 2848ce8 commit 3e4d59d

File tree

4 files changed

+144
-23
lines changed

4 files changed

+144
-23
lines changed

lib/plausible/billing/enterprise_plan.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,5 @@ defmodule Plausible.Billing.EnterprisePlan do
3636
model
3737
|> cast(attrs, @required_fields)
3838
|> validate_required(@required_fields)
39-
|> unique_constraint(:user_id)
4039
end
4140
end

lib/plausible/crm_extensions.ex

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,38 +48,71 @@ defmodule Plausible.CrmExtensions do
4848
]
4949
end
5050

51-
def javascripts(%{assigns: %{context: context}})
52-
when context in ["sites", "billing"] do
51+
def javascripts(%{assigns: %{context: "billing", resource: "enterprise_plan", changeset: %{}}}) do
5352
[
5453
Phoenix.HTML.raw("""
5554
<script type="text/javascript">
56-
(() => {
57-
const publicField = document.querySelector("#kaffy-search-field")
58-
const searchForm = document.querySelector("#kaffy-filters-form")
59-
const searchField = document.querySelector("#kaffy-filter-search")
55+
(async () => {
56+
const CHECK_INTERVAL = 300
57+
const userIdField = document.querySelector("#enterprise_plan_user_id")
58+
const userIdLabel = document.querySelector("label[for=enterprise_plan_user_id]")
59+
const dataList = document.createElement("datalist")
60+
dataList.id = "user-choices"
61+
userIdField.after(dataList)
62+
userIdField.setAttribute("list", "user-choices")
63+
userIdField.setAttribute("type", "text")
64+
const labelSpan = document.createElement("span")
65+
userIdLabel.appendChild(labelSpan)
66+
67+
let updateAction;
68+
69+
const updateLabel = async (id) => {
70+
id = Number(id)
71+
72+
if (!isNaN(id) && id > 0) {
73+
const response = await fetch(`/crm/billing/search/user-by-id/${id}`)
74+
labelSpan.innerHTML = ` <i>(${await response.text()})</i>`
75+
}
76+
}
6077
61-
if (publicField && searchForm && searchField) {
62-
publicField.name = "#{@custom_search}"
63-
searchField.name = "#{@custom_search}"
78+
const updateSearch = async () => {
79+
const search = userIdField.value
6480
65-
const params = new URLSearchParams(window.location.search)
66-
publicField.value = params.get("#{@custom_search}")
81+
updateLabel(search)
6782
68-
const searchInput = document.createElement("input")
69-
searchInput.name = "search"
70-
searchInput.type = "hidden"
71-
searchInput.value = ""
83+
const response = await fetch("/crm/billing/search/user", {
84+
headers: { "Content-Type": "application/json" },
85+
method: "POST",
86+
body: JSON.stringify({ search: search })
87+
})
7288
73-
searchForm.appendChild(searchInput)
89+
const list = await response.json()
90+
91+
const options =
92+
list.map(([label, value]) => {
93+
const option = document.createElement("option")
94+
option.setAttribute("label", label)
95+
option.textContent = value
96+
97+
return option
98+
})
99+
100+
dataList.replaceChildren(...options)
74101
}
102+
103+
updateLabel(userIdField.value)
104+
105+
userIdField.addEventListener("input", async (e) => {
106+
if (updateAction) {
107+
clearTimeout(updateAction)
108+
updateAction = null
109+
}
110+
111+
updateAction = setTimeout(() => updateSearch(), CHECK_INTERVAL)
112+
})
75113
})()
76114
</script>
77-
""")
78-
]
79-
end
80-
81-
def javascripts(%{assigns: %{context: "billing", resource: "enterprise_plan", changeset: %{}}}) do
82-
[
115+
"""),
83116
Phoenix.HTML.raw("""
84117
<script type="text/javascript">
85118
(() => {
@@ -153,6 +186,36 @@ defmodule Plausible.CrmExtensions do
153186
""")
154187
]
155188
end
189+
190+
def javascripts(%{assigns: %{context: context}})
191+
when context in ["sites", "billing"] do
192+
[
193+
Phoenix.HTML.raw("""
194+
<script type="text/javascript">
195+
(() => {
196+
const publicField = document.querySelector("#kaffy-search-field")
197+
const searchForm = document.querySelector("#kaffy-filters-form")
198+
const searchField = document.querySelector("#kaffy-filter-search")
199+
200+
if (publicField && searchForm && searchField) {
201+
publicField.name = "#{@custom_search}"
202+
searchField.name = "#{@custom_search}"
203+
204+
const params = new URLSearchParams(window.location.search)
205+
publicField.value = params.get("#{@custom_search}")
206+
207+
const searchInput = document.createElement("input")
208+
searchInput.name = "search"
209+
searchInput.type = "hidden"
210+
searchInput.value = ""
211+
212+
searchForm.appendChild(searchInput)
213+
}
214+
})()
215+
</script>
216+
""")
217+
]
218+
end
156219
end
157220

158221
def javascripts(_) do

lib/plausible_web/controllers/admin_controller.ex

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ defmodule PlausibleWeb.AdminController do
22
use PlausibleWeb, :controller
33
use Plausible
44

5+
import Ecto.Query
6+
7+
alias Plausible.Repo
58
alias Plausible.Teams
69

710
def usage(conn, params) do
@@ -71,6 +74,60 @@ defmodule PlausibleWeb.AdminController do
7174
|> send_resp(200, json_response)
7275
end
7376

77+
def user_by_id(conn, params) do
78+
id = params["user_id"]
79+
80+
entry =
81+
Repo.one(
82+
from u in Plausible.Auth.User,
83+
where: u.id == ^id,
84+
select: fragment("concat(?, ?, ?, ?)", u.name, " (", u.email, ")")
85+
) || ""
86+
87+
conn
88+
|> send_resp(200, entry)
89+
end
90+
91+
def user_search(conn, params) do
92+
search =
93+
(params["search"] || "")
94+
|> String.trim()
95+
96+
choices =
97+
if search != "" do
98+
term =
99+
search
100+
|> String.replace("%", "\%")
101+
|> String.replace("_", "\_")
102+
103+
term = "%#{term}%"
104+
105+
user_id =
106+
case Integer.parse(search) do
107+
{id, ""} -> id
108+
_ -> 0
109+
end
110+
111+
if user_id != 0 do
112+
[]
113+
else
114+
Repo.all(
115+
from u in Plausible.Auth.User,
116+
where: u.id == ^user_id or ilike(u.name, ^term) or ilike(u.email, ^term),
117+
order_by: [u.name, u.id],
118+
select: [fragment("concat(?, ?, ?, ?)", u.name, " (", u.email, ")"), u.id],
119+
limit: 20
120+
)
121+
end
122+
else
123+
[]
124+
end
125+
126+
conn
127+
|> put_resp_content_type("application/json")
128+
|> send_resp(200, Jason.encode!(choices))
129+
end
130+
74131
defp usage_and_limits_html(team, usage, limits, embed?) do
75132
content = """
76133
<ul>

lib/plausible_web/router.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ defmodule PlausibleWeb.Router do
9292
pipe_through :flags
9393
get "/auth/user/:user_id/usage", AdminController, :usage
9494
get "/billing/user/:user_id/current_plan", AdminController, :current_plan
95+
get "/billing/search/user-by-id/:user_id", AdminController, :user_by_id
96+
post "/billing/search/user", AdminController, :user_search
9597
end
9698
end
9799

0 commit comments

Comments
 (0)