Skip to content

Commit

Permalink
Implement admin pages for roles update
Browse files Browse the repository at this point in the history
Refactor how 'enrollee' are exposed and listed.
Ensure that roles are reflected in programs and
enrollments CRUD pages.
  • Loading branch information
DeeTheDev authored and rgalanakis committed Dec 3, 2024
1 parent 1709b4d commit e1ca075
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 36 deletions.
1 change: 1 addition & 0 deletions adminapp/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export default {
searchVendors: (data) => post(`/adminapi/v1/search/vendors`, data),
searchMembers: (data) => post(`/adminapi/v1/search/members`, data),
searchOrganizations: (data) => post(`/adminapi/v1/search/organizations`, data),
searchRoles: (data) => post(`/adminapi/v1/search/roles`, data),
searchVendorServices: (data) => post(`/adminapi/v1/search/vendor_services`, data),
searchCommerceOffering: (data) => post(`/adminapi/v1/search/commerce_offerings`, data),
searchPrograms: (data) => post(`/adminapi/v1/search/programs`, data),
Expand Down
8 changes: 4 additions & 4 deletions adminapp/src/pages/ProgramDetailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ export default function ProgramDetailPage() {
/>
<RelatedList
title="Program Enrollments"
headers={["Id", "Member", "Organization", "Approved At", "Unenrolled At"]}
headers={["Id", "Enrollee", "Enrollee Type", "Approved At", "Unenrolled At"]}
rows={model.enrollments}
addNewLabel="Enroll member or organization"
addNewLabel="Enroll member, organization or role"
addNewLink={createRelativeUrl(`/program-enrollment/new`, {
programId: model.id,
programLabel: `(${model.id}) ${model.name.en}`,
Expand All @@ -124,8 +124,8 @@ export default function ProgramDetailPage() {
keyRowAttr="id"
toCells={(row) => [
<AdminLink key="id" model={row} />,
<AdminLink model={row.member}>{row.member?.name}</AdminLink>,
<AdminLink model={row.organization}>{row.organization?.name}</AdminLink>,
<AdminLink model={row.enrollee}>{row.enrollee?.name}</AdminLink>,
row.enrolleeType,
dayjsOrNull(row.approvedAt)?.format("lll"),
dayjsOrNull(row.unenrolledAt)?.format("lll"),
]}
Expand Down
1 change: 1 addition & 0 deletions adminapp/src/pages/ProgramEnrollmentCreatePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default function ProgramEnrollmentCreatePage() {
program: {},
member: {},
organization: {},
role: {},
};
return (
<ResourceCreate
Expand Down
17 changes: 4 additions & 13 deletions adminapp/src/pages/ProgramEnrollmentDetailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,10 @@ export default function ProgramEnrollmentDetailPage() {
label: "Program Name ES",
value: <AdminLink model={model.program}>{model.program.name.es}</AdminLink>,
},
model.member
? {
label: "Enrolled Member",
value: <AdminLink model={model.member}>{model.member?.name}</AdminLink>,
}
: {
label: "Enrolled Organization",
value: (
<AdminLink model={model.organization}>
{model.organization?.name}
</AdminLink>
),
},
{
label: "Enrolled " + model.enrolleeType,
value: <AdminLink model={model.enrollee}>{model.enrollee?.name}</AdminLink>,
},
{
label: "Approved",
children: (
Expand Down
30 changes: 24 additions & 6 deletions adminapp/src/pages/ProgramEnrollmentForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export default function ProgramEnrollmentForm({
return (
<FormLayout
title={isCreate ? "Create a Program Enrollment" : "Update a Program Enrollment"}
subtitle="Program enrollment that are approved gives access to a member
or members in an organization to resources connected with an active program.
After creation, you can approve the enrollment and/or unenroll."
subtitle="Program enrollment that are approved gives access to a member,
members in an organization, or members with a role to resources connected
with an active program. After creation, you can approve the enrollment and/or unenroll."
onSubmit={onSubmit}
isBusy={isBusy}
>
Expand All @@ -82,23 +82,25 @@ export default function ProgramEnrollmentForm({
control={<Radio />}
label="Organization"
/>
<FormControlLabel value="role" control={<Radio />} label="Role" />
</RadioGroup>
</FormControl>
{enrolleeType === "member" ? (
{enrolleeType === "member" && (
<AutocompleteSearch
key="member"
{...register("member")}
label="Member"
helperText="Who can access this program?"
value={resource.member?.label || ""}
value={resource.member.label || ""}
fullWidth
search={api.searchMembers}
disabled={fixedEnrollee}
style={{ flex: 1 }}
onValueSelect={(mem) => setField("member", mem)}
onTextChange={() => setField("member", {})}
/>
) : (
)}
{enrolleeType === "organization" && (
<AutocompleteSearch
key="org"
{...register("organization")}
Expand All @@ -113,6 +115,22 @@ export default function ProgramEnrollmentForm({
onTextChange={() => setField("organization", {})}
/>
)}
{enrolleeType === "role" && (
<AutocompleteSearch
key="role"
{...register("role")}
label="Role"
helperText="What members with this role can access this program?"
value={resource.role.label || ""}
fullWidth
search={api.searchRoles}
searchEmpty={true}
disabled={fixedEnrollee}
style={{ flex: 1 }}
onValueSelect={(role) => setField("role", role)}
onTextChange={() => setField("role", {})}
/>
)}
</Stack>
</FormLayout>
);
Expand Down
14 changes: 6 additions & 8 deletions adminapp/src/pages/ProgramEnrollmentListPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,17 @@ export default function ProgramEnrollmentListPage() {
render: (c) => <AdminLink model={c.program}>{c.program.name.en}</AdminLink>,
},
{
id: "member",
label: "Member",
id: "enrollee",
label: "Enrollee",
align: "left",
render: (c) => <AdminLink model={c.member}>{c.member?.name}</AdminLink>,
render: (c) => <AdminLink model={c.enrollee}>{c.enrollee?.name}</AdminLink>,
hideEmpty: true,
},
{
id: "organization",
label: "Organization",
id: "enrollee_type",
label: "Enrollee Type",
align: "left",
render: (c) => (
<AdminLink model={c.organization}>{c.organization?.name}</AdminLink>
),
render: (c) => c.enrolleeType,
hideEmpty: true,
},
{
Expand Down
12 changes: 9 additions & 3 deletions lib/suma/admin_api/entities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,19 @@ class ProgramEntity < BaseEntity
expose :app_link_text, with: TranslatedTextEntity
end

class ProgramEnrolleeEntity < BaseEntity
include AutoExposeBase
expose :name do |inst|
inst.is_a?(Suma::Role) ? inst.name.titleize : inst.name
end
end

class ProgramEnrollmentEntity < BaseEntity
include AutoExposeBase
expose :admin_link
expose :program, with: ProgramEntity
expose :member, with: MemberEntity
expose :organization, with: OrganizationEntity
expose :role, with: RoleEntity
expose :enrollee, with: ProgramEnrolleeEntity
expose :enrollee_type
expose :approved_at
expose :unenrolled_at
expose :program_active do |pe|
Expand Down
22 changes: 22 additions & 0 deletions lib/suma/admin_api/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,20 @@ def ds_search_or_order_by(column_sym, ds, params)
present_collection ds, with: SearchOrganizationEntity
end

params do
optional :q, type: String
end
post :roles do
check_role_access!(admin_member, :read, :admin_members)
# role names are sluggified by default.
params[:q] = Suma.to_slug(params[:q]) if params[:q].present?
ds = Suma::Role.dataset
ds = ds_search_or_order_by(:name, ds, params)
ds = ds.limit(15)
status 200
present_collection ds, with: SearchRoleEntity
end

params do
optional :q, type: String
end
Expand Down Expand Up @@ -335,6 +349,14 @@ class SearchOrganizationEntity < BaseEntity
expose :name, as: :label
end

class SearchRoleEntity < BaseEntity
expose :key, &self.delegate_to(:id, :to_s)
expose :id
expose :admin_link
expose :name
expose :label
end

class SearchVendorServiceEntity < BaseEntity
expose :key, &self.delegate_to(:id, :to_s)
expose :id
Expand Down
6 changes: 4 additions & 2 deletions lib/suma/program/enrollment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ def unenrolled=(v)
self.unenrolled_at = v ? Time.now : nil
end

# @return [Suma::Member,Suma::Organization]
def enrollee = self.member || self.organization
# @return [Suma::Member,Suma::Organization,Suma::Role]
def enrollee = self.member || self.organization || self.role

def enrollee_type = self.enrollee.class.name.demodulize

def rel_admin_link = "/program-enrollment/#{self.id}"
end
41 changes: 41 additions & 0 deletions spec/suma/admin_api/search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,47 @@
end
end

describe "POST /v1/search/roles" do
it "errors without role access" do
replace_roles(admin, Suma::Role.cache.noop_admin)

post "/v1/search/roles"

expect(last_response).to have_status(403)
expect(last_response).to have_json_body.that_includes(error: include(code: "role_check"))
end

it "returns matching roles" do
r1 = Suma::Role.create(name: "spongebob")
r2 = Suma::Role.create(name: "patrick")

post "/v1/search/roles", q: "bob"

expect(last_response).to have_status(200)
expect(last_response).to have_json_body.that_includes(items: have_same_ids_as(r1))
end

it "returns matching roles label" do
Suma::Role.create(name: "hard worker")

post "/v1/search/roles", q: "hard"

expect(last_response).to have_status(200)
expect(last_response).to have_json_body.that_includes(items: [include(label: "Hard Worker")])
end

it "returns all results in descending order if no query" do
r1 = Suma::Role.create(name: "x role")
r2 = Suma::Role.create(name: "addmin")

post "/v1/search/roles"

expect(last_response).to have_status(200)
expect(last_response_json_body[:items].first).to include(name: r2.name)
expect(last_response_json_body[:items].last).to include(name: r1.name)
end
end

describe "POST /v1/search/vendor_services" do
it "errors without role access" do
replace_roles(admin, Suma::Role.cache.noop_admin)
Expand Down

0 comments on commit e1ca075

Please sign in to comment.