Skip to content

Commit

Permalink
feat(organizations): mutation hook for updating organization settings…
Browse files Browse the repository at this point in the history
… TASK-1219 (#5318)

### 💭 Notes
Cleanup imports. Cleanup organization types typings (preparation for
usage in other PR). Add `usePatchOrganization` mutation hook. Update
comments.

### 👀 Preview steps
Not testable by itself - see #5318.
  • Loading branch information
magicznyleszek authored Dec 5, 2024
1 parent 6cac659 commit cab0d2c
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 29 deletions.
17 changes: 9 additions & 8 deletions jsapp/js/account/accountFieldsEditor.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import type {
AccountFieldsValues,
AccountFieldsErrors,
} from './account.constants';
import {ORGANIZATION_TYPES, type OrganizationTypeName} from 'jsapp/js/account/organization/organizationQuery';

const ORGANIZATION_TYPE_SELECT_OPTIONS = Object.keys(ORGANIZATION_TYPES)
.map((typeName) => {
return {
value: typeName,
label: ORGANIZATION_TYPES[typeName as OrganizationTypeName].label,
};
});

// See: kobo/apps/accounts/forms.py (KoboSignupMixin)
const ORGANIZATION_TYPE_SELECT_OPTIONS = [
{value: 'non-profit', label: t('Non-profit organization')},
{value: 'government', label: t('Government institution')},
{value: 'educational', label: t('Educational organization')},
{value: 'commercial', label: t('A commercial/for-profit company')},
{value: 'none', label: t('I am not associated with any organization')},
];
const GENDER_SELECT_OPTIONS = [
{value: 'male', label: t('Male')},
{value: 'female', label: t('Female')},
Expand Down
86 changes: 65 additions & 21 deletions jsapp/js/account/organization/organizationQuery.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
import type {FailResponse} from 'js/dataInterface';
import {fetchGetUrl} from 'jsapp/js/api';
import type {UndefinedInitialDataOptions} from '@tanstack/react-query';
import {useQuery} from '@tanstack/react-query';
import {QueryKeys} from 'js/query/queryKeys';
// Libraries
import {useMutation, useQuery, useQueryClient, type UndefinedInitialDataOptions} from '@tanstack/react-query';
import {useEffect} from 'react';

// Stores, hooks and utilities
import {fetchGetUrl, fetchPatch} from 'jsapp/js/api';
import {FeatureFlag, useFeatureFlag} from 'js/featureFlags';
import sessionStore from 'js/stores/session';
import {useEffect} from 'react';
import {useSession} from 'jsapp/js/stores/useSession';

// Constants and types
import type {FailResponse} from 'js/dataInterface';
import {QueryKeys} from 'js/query/queryKeys';

// Comes from `kobo/apps/accounts/forms.py`
export type OrganizationTypeName = 'non-profit' | 'government' | 'educational' | 'commercial' | 'none';

export const ORGANIZATION_TYPES: {
[P in OrganizationTypeName]: {name: OrganizationTypeName; label: string}
} = {
'non-profit': {name: 'non-profit', label: t('Non-profit organization')},
government: {name: 'government', label: t('Government institution')},
educational: {name: 'educational', label: t('Educational organization')},
commercial: {name: 'commercial', label: t('A commercial/for-profit company')},
none: {name: 'none', label: t('I am not associated with any organization')},
};

export interface Organization {
id: string;
name: string;
is_active: boolean;
website: string;
organization_type: OrganizationTypeName;
created: string;
modified: string;
slug: string;
is_owner: boolean;
is_mmo: boolean;
request_user_role: OrganizationUserRole;
Expand All @@ -25,6 +43,29 @@ export enum OrganizationUserRole {
owner = 'owner',
}

/**
* Mutation hook for updating organization. It ensures that all related queries
* refetch data (are invalidated).
*/
export function usePatchOrganization() {
const queryClient = useQueryClient();
const session = useSession();
const organizationUrl = session.currentLoggedAccount?.organization?.url;

return useMutation({
mutationFn: async (data: Partial<Organization>) => (
// We're asserting the `organizationUrl` is not `undefined` here, because
// the parent query (`useOrganizationQuery`) wouldn't be enabled without
// it. Plus all the organization-related UI is accessible only to
// logged in users.
fetchPatch<Organization>(organizationUrl!, data, {prependRootUrl: false})
),
onSettled: () => {
queryClient.invalidateQueries({queryKey: [QueryKeys.organization]});
},
});
}

/**
* Organization object is used globally.
* For convenience, errors are handled once at the top, see `RequireOrg`.
Expand All @@ -33,32 +74,34 @@ export enum OrganizationUserRole {
export const useOrganizationQuery = (options?: Omit<UndefinedInitialDataOptions<Organization, FailResponse, Organization, QueryKeys[]>, 'queryFn' | 'queryKey'>) => {
const isMmosEnabled = useFeatureFlag(FeatureFlag.mmosEnabled);

const currentAccount = sessionStore.currentAccount;

const organizationUrl =
'organization' in currentAccount ? currentAccount.organization?.url : null;
const session = useSession();
const organizationUrl = session.currentLoggedAccount?.organization?.url;

// Using a separated function to fetch the organization data to prevent
// feature flag dependencies from being added to the hook
const fetchOrganization = async (): Promise<Organization> => {
// organizationUrl is a full url with protocol and domain name, so we're using fetchGetUrl
// We're asserting the organizationUrl is not null here because the query is disabled if it is
// `organizationUrl` is a full url with protocol and domain name, so we're
// using fetchGetUrl.
// We're asserting the `organizationUrl` is not `undefined` here because
// the query is disabled without it.
const organization = await fetchGetUrl<Organization>(organizationUrl!);

if (isMmosEnabled) {
return organization;
}

// While the project is in development we will force a false return for the is_mmo
// to make sure we don't have any implementations appearing for users
// While the project is in development we will force a `false` return for
// the `is_mmo` to make sure we don't have any implementations appearing
// for users.
return {
...organization,
is_mmo: false,
};
};

// Setting the 'enabled' property so the query won't run until we have the session data
// loaded. Account data is needed to fetch the organization data.
// Setting the 'enabled' property so the query won't run until we have
// the session data loaded. Account data is needed to fetch the organization
// data.
const isQueryEnabled =
!sessionStore.isPending &&
sessionStore.isInitialLoadComplete &&
Expand All @@ -71,9 +114,10 @@ export const useOrganizationQuery = (options?: Omit<UndefinedInitialDataOptions<
enabled: isQueryEnabled && options?.enabled !== false,
});

// `organizationUrl` must exist, unless it's changed (e.g. user added/removed from organization).
// In such case, refetch organizationUrl to fetch the new `organizationUrl`.
// DEBT: don't throw toast within fetchGetUrl.
// `organizationUrl` must exist, unless it's changed (e.g. user added/removed
// from organization).
// In such case, refetch `organizationUrl` to fetch the new `organizationUrl`.
// DEBT: don't throw toast within `fetchGetUrl`.
// DEBT: don't retry the failing url 3-4 times before switching to the new url.
useEffect(() => {
if (query.error?.status === 404) {
Expand Down

0 comments on commit cab0d2c

Please sign in to comment.