Order membership by organization name

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-19 17:37:15 +04:00
parent 63390db416
commit 887d39144b
10 changed files with 144 additions and 93 deletions

View File

@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- Membership Profile authz done from membership in membershipResolver (it's a 1:1 association)
- Match keycloak URL ports with default base URL one for local dev
- Drop the authentication dialog in favor of a dedicated auth page for compliance page
- Order memberships by organization name on console / and memberships dropdown
### Fixed

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<7759f335d6dd4b9ca6afbb601aa7794c>>
* @generated SignedSource<<ccb5c8952aaa41ddee0df7e7e3abb2b0>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,7 +10,7 @@
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MembershipOrderField = "CREATED_AT" | "EMAIL_ADDRESS" | "FULL_NAME" | "ROLE";
export type MembershipOrderField = "CREATED_AT" | "EMAIL_ADDRESS" | "FULL_NAME" | "ORGANIZATION_NAME" | "ROLE";
export type OrderDirection = "ASC" | "DESC";
export type MembershipOrder = {
direction: OrderDirection;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b51fba8f2d50c85b2eeb51c658d82e17>>
* @generated SignedSource<<a7fb3832b9c9d3830ceff389237da093>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -42,8 +42,8 @@ var v0 = [
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
"direction": "ASC",
"field": "ORGANIZATION_NAME"
}
}
],
@@ -141,7 +141,7 @@ return {
"action": "THROW"
}
],
"storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
"storageKey": "memberships(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"ORGANIZATION_NAME\"})"
},
"action": "THROW"
}
@@ -239,7 +239,7 @@ return {
"storageKey": null
}
],
"storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
"storageKey": "memberships(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"ORGANIZATION_NAME\"})"
},
(v1/*: any*/)
],
@@ -248,16 +248,16 @@ return {
]
},
"params": {
"cacheID": "edb71632c35f1f8aefa1bb426c81a985",
"cacheID": "9295fb9d95666f4b397645e143b7e774",
"id": null,
"metadata": {},
"name": "MembershipsDropdownMenuQuery",
"operationKind": "query",
"text": "query MembershipsDropdownMenuQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n organization {\n name\n id\n }\n ...MembershipsDropdownMenuItemFragment\n }\n }\n }\n id\n }\n}\n\nfragment MembershipsDropdownMenuItemFragment on Membership {\n id\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n logoUrl\n name\n }\n}\n"
"text": "query MembershipsDropdownMenuQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: ASC, field: ORGANIZATION_NAME}) {\n edges {\n node {\n id\n organization {\n name\n id\n }\n ...MembershipsDropdownMenuItemFragment\n }\n }\n }\n id\n }\n}\n\nfragment MembershipsDropdownMenuItemFragment on Membership {\n id\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n logoUrl\n name\n }\n}\n"
}
};
})();
(node as any).hash = "aa787b33a7cb3046d30fd638f4850621";
(node as any).hash = "cd47570a10588c0e1c68240817141fd8";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<1d247c2fede04c2292000421a9e49fa8>>
* @generated SignedSource<<3a268aba389df25968eb44b454150288>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -46,42 +46,39 @@ var v0 = {
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
"direction": "ASC",
"field": "ORGANIZATION_NAME"
}
},
v1 = [
(v0/*: any*/)
],
v2 = {
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v4 = {
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v5 = {
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v6 = {
v5 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
@@ -106,7 +103,7 @@ v6 = {
],
"storageKey": null
},
v7 = {
v6 = {
"kind": "ClientExtension",
"selections": [
{
@@ -118,16 +115,29 @@ v7 = {
}
]
},
v8 = [
{
"kind": "Literal",
"name": "first",
"value": 1000
},
v7 = {
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
}
},
v8 = {
"kind": "Literal",
"name": "first",
"value": 1000
},
v9 = [
(v8/*: any*/),
(v0/*: any*/)
],
v9 = [
v10 = [
"orderBy"
],
v11 = [
(v8/*: any*/),
(v7/*: any*/)
];
return {
"fragment": {
@@ -150,7 +160,9 @@ return {
"kind": "RequiredField",
"field": {
"alias": "memberships",
"args": (v1/*: any*/),
"args": [
(v0/*: any*/)
],
"concreteType": "MembershipConnection",
"kind": "LinkedField",
"name": "__MembershipsPage_memberships_connection",
@@ -174,7 +186,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v1/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
@@ -190,26 +202,26 @@ return {
"name": "organization",
"plural": false,
"selections": [
(v3/*: any*/)
(v2/*: any*/)
],
"storageKey": null
},
"action": "THROW"
},
(v4/*: any*/)
(v3/*: any*/)
],
"storageKey": null
},
(v5/*: any*/)
(v4/*: any*/)
],
"storageKey": null
},
"action": "THROW"
},
(v6/*: any*/),
(v7/*: any*/)
(v5/*: any*/),
(v6/*: any*/)
],
"storageKey": "__MembershipsPage_memberships_connection(orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
"storageKey": "__MembershipsPage_memberships_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"ORGANIZATION_NAME\"})"
},
"action": "THROW"
},
@@ -217,7 +229,9 @@ return {
"kind": "RequiredField",
"field": {
"alias": "pendingInvitations",
"args": (v1/*: any*/),
"args": [
(v7/*: any*/)
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "__MembershipsPage_pendingInvitations_connection",
@@ -241,24 +255,24 @@ return {
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v1/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "InvitationCardFragment"
},
(v4/*: any*/)
(v3/*: any*/)
],
"storageKey": null
},
(v5/*: any*/)
(v4/*: any*/)
],
"storageKey": null
},
"action": "THROW"
},
(v6/*: any*/),
(v7/*: any*/)
(v5/*: any*/),
(v6/*: any*/)
],
"storageKey": "__MembershipsPage_pendingInvitations_connection(orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
},
@@ -289,7 +303,7 @@ return {
"selections": [
{
"alias": null,
"args": (v8/*: any*/),
"args": (v9/*: any*/),
"concreteType": "MembershipConnection",
"kind": "LinkedField",
"name": "memberships",
@@ -311,7 +325,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
@@ -320,7 +334,7 @@ return {
"name": "lastSession",
"plural": false,
"selections": [
(v2/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
@@ -339,8 +353,8 @@ return {
"name": "organization",
"plural": false,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
@@ -351,23 +365,23 @@ return {
],
"storageKey": null
},
(v4/*: any*/)
(v3/*: any*/)
],
"storageKey": null
},
(v5/*: any*/)
(v4/*: any*/)
],
"storageKey": null
},
(v6/*: any*/),
(v7/*: any*/)
(v5/*: any*/),
(v6/*: any*/)
],
"storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
"storageKey": "memberships(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"ORGANIZATION_NAME\"})"
},
{
"alias": null,
"args": (v8/*: any*/),
"filters": (v9/*: any*/),
"args": (v9/*: any*/),
"filters": (v10/*: any*/),
"handle": "connection",
"key": "MembershipsPage_memberships",
"kind": "LinkedHandle",
@@ -375,7 +389,7 @@ return {
},
{
"alias": null,
"args": (v8/*: any*/),
"args": (v11/*: any*/),
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "pendingInvitations",
@@ -397,7 +411,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
@@ -420,41 +434,41 @@ return {
"name": "organization",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
(v1/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
(v4/*: any*/)
(v3/*: any*/)
],
"storageKey": null
},
(v5/*: any*/)
(v4/*: any*/)
],
"storageKey": null
},
(v6/*: any*/),
(v7/*: any*/)
(v5/*: any*/),
(v6/*: any*/)
],
"storageKey": "pendingInvitations(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
},
{
"alias": null,
"args": (v8/*: any*/),
"filters": (v9/*: any*/),
"args": (v11/*: any*/),
"filters": (v10/*: any*/),
"handle": "connection",
"key": "MembershipsPage_pendingInvitations",
"kind": "LinkedHandle",
"name": "pendingInvitations"
},
(v2/*: any*/)
(v1/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "a3dc3f3e775fa02b7d181fa57ade06b9",
"cacheID": "9807cdf4aa713babb1639e4effd96e54",
"id": null,
"metadata": {
"connection": [
@@ -480,11 +494,11 @@ return {
},
"name": "MembershipsPageQuery",
"operationKind": "query",
"text": "query MembershipsPageQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...MembershipCardFragment\n organization {\n name\n id\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n pendingInvitations(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n\nfragment InvitationCardFragment on Invitation {\n id\n role\n createdAt\n organization {\n id\n name\n }\n}\n\nfragment MembershipCardFragment on Membership {\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n name\n logoUrl\n }\n}\n"
"text": "query MembershipsPageQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: ASC, field: ORGANIZATION_NAME}) {\n edges {\n node {\n id\n ...MembershipCardFragment\n organization {\n name\n id\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n pendingInvitations(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n\nfragment InvitationCardFragment on Invitation {\n id\n role\n createdAt\n organization {\n id\n name\n }\n}\n\nfragment MembershipCardFragment on Membership {\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n name\n logoUrl\n }\n}\n"
}
};
})();
(node as any).hash = "b21b9cae0ab7a4afdf12fb6ae516b64e";
(node as any).hash = "7956b20535004550c271e1a520c95fe4";
export default node;

View File

@@ -16,7 +16,10 @@ import { usePageTitle } from "@probo/hooks";
export const membershipsPageQuery = graphql`
query MembershipsPageQuery {
viewer @required(action: THROW) {
memberships(first: 1000, orderBy: { direction: DESC, field: CREATED_AT })
memberships(
first: 1000
orderBy: { direction: ASC, field: ORGANIZATION_NAME }
)
@connection(key: "MembershipsPage_memberships")
@required(action: THROW) {
__id

View File

@@ -6,8 +6,10 @@ import { MembershipsDropdownMenuItem } from "./MembershipsDropdownMenuItem";
export const membershipsDropdownMenuQuery = graphql`
query MembershipsDropdownMenuQuery {
viewer @required(action: THROW) {
memberships(first: 1000, orderBy: { direction: DESC, field: CREATED_AT })
@required(action: THROW) {
memberships(
first: 1000
orderBy: { direction: ASC, field: ORGANIZATION_NAME }
) @required(action: THROW) {
edges @required(action: THROW) {
node @required(action: THROW) {
id

View File

@@ -31,16 +31,17 @@ import (
type (
Membership struct {
ID gid.GID `db:"id"`
IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"`
Role MembershipRole `db:"role"`
Source MembershipSource `db:"source"`
State MembershipState `db:"state"`
FullName string `db:"full_name"`
EmailAddress mail.Addr `db:"email_address"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"`
Role MembershipRole `db:"role"`
Source MembershipSource `db:"source"`
State MembershipState `db:"state"`
FullName string `db:"full_name"`
EmailAddress mail.Addr `db:"email_address"`
OrganizationName string `db:"organization_name"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Memberships []*Membership
@@ -48,6 +49,8 @@ type (
func (m Membership) CursorKey(orderBy MembershipOrderField) page.CursorKey {
switch orderBy {
case MembershipOrderFieldOrganizationName:
return page.NewCursorKey(m.ID, m.OrganizationName)
case MembershipOrderFieldFullName:
return page.NewCursorKey(m.ID, m.FullName)
case MembershipOrderFieldEmailAddress:
@@ -88,11 +91,13 @@ SELECT
mbr.state,
COALESCE(mp.full_name, i.full_name, '') as full_name,
i.email_address,
o.name AS organization_name,
mbr.created_at,
mbr.updated_at
FROM
mbr
JOIN identities i ON mbr.identity_id = i.id
JOIN organizations o ON mbr.organization_id = o.id
LEFT JOIN iam_membership_profiles mp ON mp.membership_id = mbr.id
`
@@ -207,12 +212,15 @@ SELECT
mbr.state,
COALESCE(mp.full_name, i.full_name, '') as full_name,
i.email_address,
o.name as organization_name,
mbr.created_at,
mbr.updated_at
FROM
mbr
JOIN
identities i ON mbr.identity_id = i.id
JOIN
organizations o ON mbr.organization_id = o.id
LEFT JOIN
iam_membership_profiles mp ON mp.membership_id = mbr.id
`
@@ -314,12 +322,15 @@ SELECT
mbr.state,
COALESCE(mp.full_name, i.full_name, '') as full_name,
i.email_address,
o.name as organization_name,
mbr.created_at,
mbr.updated_at
FROM
mbr
JOIN
identities i ON mbr.identity_id = i.id
JOIN
organizations o ON mbr.organization_id = o.id
LEFT JOIN
iam_membership_profiles mp ON mp.membership_id = mbr.id
`
@@ -451,12 +462,15 @@ SELECT
mbr.state,
COALESCE(mp.full_name, i.full_name, '') as full_name,
i.email_address,
o.name as organization_name,
mbr.created_at,
mbr.updated_at
FROM
mbr
JOIN
identities i ON mbr.identity_id = i.id
JOIN
organizations o ON mbr.organization_id = o.id
LEFT JOIN
iam_membership_profiles mp ON mp.membership_id = mbr.id
WHERE
@@ -504,12 +518,15 @@ WITH membership_with_profile AS (
m.state,
COALESCE(mp.full_name, i.full_name, '') AS full_name,
i.email_address,
o.name as organization_name,
m.created_at,
m.updated_at
FROM
iam_memberships m
JOIN
identities i ON m.identity_id = i.id
JOIN
organizations o ON mbr.organization_id = o.id
LEFT JOIN
iam_membership_profiles mp ON mp.membership_id = m.id
WHERE
@@ -632,6 +649,7 @@ SELECT
state,
'' as full_name,
NULL as email_address,
'' as organization_name,
created_at,
updated_at
FROM

View File

@@ -19,14 +19,17 @@ type (
)
const (
MembershipOrderFieldFullName MembershipOrderField = "FULL_NAME"
MembershipOrderFieldEmailAddress MembershipOrderField = "EMAIL_ADDRESS"
MembershipOrderFieldRole MembershipOrderField = "ROLE"
MembershipOrderFieldCreatedAt MembershipOrderField = "CREATED_AT"
MembershipOrderFieldOrganizationName MembershipOrderField = "ORGANIZATION_NAME"
MembershipOrderFieldFullName MembershipOrderField = "FULL_NAME"
MembershipOrderFieldEmailAddress MembershipOrderField = "EMAIL_ADDRESS"
MembershipOrderFieldRole MembershipOrderField = "ROLE"
MembershipOrderFieldCreatedAt MembershipOrderField = "CREATED_AT"
)
func (p MembershipOrderField) Column() string {
switch p {
case MembershipOrderFieldOrganizationName:
return "organization_name"
case MembershipOrderFieldFullName:
return "full_name"
case MembershipOrderFieldEmailAddress:

View File

@@ -412,6 +412,10 @@ enum MembershipOrderField
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName"
)
ORGANIZATION_NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldOrganizationName"
)
EMAIL_ADDRESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress"

View File

@@ -2687,6 +2687,10 @@ enum MembershipOrderField
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName"
)
ORGANIZATION_NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldOrganizationName"
)
EMAIL_ADDRESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress"
@@ -20088,16 +20092,18 @@ func (ec *executionContext) marshalNMembershipOrderField2goᚗproboᚗincᚋprob
var (
unmarshalNMembershipOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipOrderField = map[string]coredata.MembershipOrderField{
"FULL_NAME": coredata.MembershipOrderFieldFullName,
"EMAIL_ADDRESS": coredata.MembershipOrderFieldEmailAddress,
"ROLE": coredata.MembershipOrderFieldRole,
"CREATED_AT": coredata.MembershipOrderFieldCreatedAt,
"FULL_NAME": coredata.MembershipOrderFieldFullName,
"ORGANIZATION_NAME": coredata.MembershipOrderFieldOrganizationName,
"EMAIL_ADDRESS": coredata.MembershipOrderFieldEmailAddress,
"ROLE": coredata.MembershipOrderFieldRole,
"CREATED_AT": coredata.MembershipOrderFieldCreatedAt,
}
marshalNMembershipOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipOrderField = map[coredata.MembershipOrderField]string{
coredata.MembershipOrderFieldFullName: "FULL_NAME",
coredata.MembershipOrderFieldEmailAddress: "EMAIL_ADDRESS",
coredata.MembershipOrderFieldRole: "ROLE",
coredata.MembershipOrderFieldCreatedAt: "CREATED_AT",
coredata.MembershipOrderFieldFullName: "FULL_NAME",
coredata.MembershipOrderFieldOrganizationName: "ORGANIZATION_NAME",
coredata.MembershipOrderFieldEmailAddress: "EMAIL_ADDRESS",
coredata.MembershipOrderFieldRole: "ROLE",
coredata.MembershipOrderFieldCreatedAt: "CREATED_AT",
}
)