Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(organizations): add Organization Settings route TASK-981 #5299

Merged
merged 31 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c6fda11
refactor(router): rename RequireOrgPermissions
Akuukis Nov 20, 2024
5db756a
wip: dump skeleton of organizations settings page
Akuukis Nov 20, 2024
15c6d81
Merge branch 'main' into kalvis/organizations-settings
magicznyleszek Nov 21, 2024
cde2230
use mmo label
magicznyleszek Nov 21, 2024
b5e980f
Merge branch 'main' into kalvis/organizations-settings
magicznyleszek Nov 21, 2024
cb07f7f
organization settings more work
magicznyleszek Nov 21, 2024
bf09e84
Merge branch 'main' into kalvis/organizations-settings
magicznyleszek Nov 25, 2024
35f3f6d
disable changing settings fields for non admin/non owner
magicznyleszek Nov 25, 2024
7b4c42a
add save button, handle permissions to edit things
magicznyleszek Nov 26, 2024
6d9ec12
move planName to subscriptionStore
magicznyleszek Nov 26, 2024
902a982
use plan name in OrganizationSettingsRoute
magicznyleszek Nov 26, 2024
54714c9
remove comment
magicznyleszek Nov 26, 2024
8789179
improve TODO comments
magicznyleszek Nov 26, 2024
b3a2443
improve comments, linter fixes
magicznyleszek Nov 26, 2024
14c9f06
Merge branch 'main' into kalvis/organizations-settings
magicznyleszek Nov 26, 2024
f88d64f
Merge branch 'main' into kalvis/organizations-settings
magicznyleszek Nov 28, 2024
67e6d0a
use better placeholders
magicznyleszek Nov 28, 2024
be201d6
Merge branch 'main' into kalvis/organizations-settings
magicznyleszek Dec 3, 2024
c11cfde
cleanup Organization types, add usePatchOrganization mutation hook an…
magicznyleszek Dec 3, 2024
e5b3059
Merge branch 'main' into kalvis/organizations-settings
magicznyleszek Dec 3, 2024
83b74a0
Merge branch 'main' into leszek/task-1219-org-settings-mutating
magicznyleszek Dec 3, 2024
9edc623
Merge branch 'leszek/task-1219-org-settings-mutating' into kalvis/org…
magicznyleszek Dec 3, 2024
984f205
use single source of truth for organization types
magicznyleszek Dec 4, 2024
147365a
some code review fixes
magicznyleszek Dec 4, 2024
8ffd210
finish hooking up, split out OrganizationSettingsForm
magicznyleszek Dec 4, 2024
b0faa7a
don't require passing orgUrl in usePatchOrganization
magicznyleszek Dec 5, 2024
931a2fa
add export
magicznyleszek Dec 5, 2024
7c92e19
add prepend url false
magicznyleszek Dec 5, 2024
cd52acf
deduplicate organization type const
magicznyleszek Dec 5, 2024
2b23d07
Merge branch 'leszek/task-1219-org-settings-mutating' into kalvis/org…
magicznyleszek Dec 5, 2024
a23c908
use simpler usePatchOrganization, merge files
magicznyleszek Dec 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jsapp/js/account/accountFieldsEditor.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
} from './account.constants';

// See: kobo/apps/accounts/forms.py (KoboSignupMixin)
const ORGANIZATION_TYPE_SELECT_OPTIONS = [
export const ORGANIZATION_TYPE_SELECT_OPTIONS = [
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
{value: 'non-profit', label: t('Non-profit organization')},
{value: 'government', label: t('Government institution')},
{value: 'educational', label: t('Educational organization')},
Expand Down
36 changes: 36 additions & 0 deletions jsapp/js/account/organization/OrganizationSettingsField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import TextBox from 'jsapp/js/components/common/textBox';
import organizationSettingsStyles from 'js/account/organization/organizationSettingsRoute.module.scss';

interface Props {
label: string;
value: string;
isDisabled?: boolean;
/** If `onChange` is not provided, we make the field disabled for safety. */
onChange?: (newValue: string) => void;
/**
* Function that ensures that field value is valid. If invalid will cause
* an error to be displayed.
*/
validateValue?: (currentValue: string) => string | boolean | string[] | undefined;
}

/**
* A `TextBox` wrapper componet for `OrganizationSettingsRoute` that makes code
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
* a bit more DRY.
*/
export default function OrganizationSettingsField(
{label, value, isDisabled, onChange, validateValue}: Props
) {
return (
<div className={organizationSettingsStyles.field}>
<TextBox
label={label}
value={value}
required
onChange={onChange}
disabled={!onChange || isDisabled}
errors={validateValue ? validateValue(value) : undefined}
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
/>
</div>
);
}
153 changes: 151 additions & 2 deletions jsapp/js/account/organization/OrganizationSettingsRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,156 @@
import React from 'react';
// Libraries
import {useState, useEffect} from 'react';

// Partial components
import OrganizationSettingsField from './OrganizationSettingsField';
import LoadingSpinner from 'jsapp/js/components/common/loadingSpinner';
import InlineMessage from 'jsapp/js/components/common/inlineMessage';
import Button from 'jsapp/js/components/common/button';

// Stores, hooks and utilities
import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
import {OrganizationUserRole, useOrganizationQuery} from 'js/account/organization/organizationQuery';
import subscriptionStore from 'js/account/subscriptionStore';
import envStore from 'js/envStore';
import {getSimpleMMOLabel} from './organization.utils';

// Constants and types
import {ORGANIZATION_TYPE_SELECT_OPTIONS} from 'js/account/accountFieldsEditor.component';

// Styles
import styles from 'js/account/organization/organizationSettingsRoute.module.scss';

interface State {
name: string;
website?: string;
type?: string;
}

/**
* Renders few fields with organization related settings, like name or website
* (with some logic in regards to their visibility). If user has necessary role,
* they can edit available fields.
*/
export default function OrganizationSettingsRoute() {
const orgQuery = useOrganizationQuery();
const [subscriptions] = useState(() => subscriptionStore);
const [state, setState] = useState<State>({name: ''});
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
const [isStripeEnabled, setIsStripeEnabled] = useState(false);

useEffect(() => {
if (orgQuery.data) {
setState({name: orgQuery.data.name});
}
}, [orgQuery.data]);

useWhenStripeIsEnabled(() => {
setIsStripeEnabled(true);
}, []);

const isUserAdminOrOwner = (
orgQuery.data?.request_user_role &&
[OrganizationUserRole.admin, OrganizationUserRole.owner]
.includes(orgQuery.data?.request_user_role)
);

const isPendingOrgPatch = orgQuery.data && orgQuery.isPending;

function handleSave() {
// TODO: call the API endpoint
// to be done while doing: https://www.notion.so/kobotoolbox/Add-react-query-mutation-hook-for-org-changes-1307e515f6548010b5d3c087b634f01a
console.log('save');
}

function handleChangeName(name: string) {
setState((prevState) => {return {...prevState, name};});
}

function handleChangeWebsite(website: string) {
setState((prevState) => {return {...prevState, website};});
}

function isNameValueValid(currentName: string) {
return !currentName;
}

function isWebsiteValueValid(currentWebsite: string) {
return !currentWebsite;
}

function getTypeLabel(typeName: string) {
// TODO: see if this would be an actual source of the organization type label
// to be done while doing: https://www.notion.so/kobotoolbox/Add-react-query-mutation-hook-for-org-changes-1307e515f6548010b5d3c087b634f01a
const foundLabel = ORGANIZATION_TYPE_SELECT_OPTIONS.find((item) => item.value === typeName)?.label;
return foundLabel || typeName;
}

const mmoLabel = getSimpleMMOLabel(
envStore.data,
subscriptionStore.activeSubscriptions[0],
false,
true
);
const mmoLabelLowercase = mmoLabel.toLowerCase();

if (!orgQuery.data) {
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
return <LoadingSpinner />;
}

return (
<div>Organization settings view to be implemented</div>
<div className={styles.orgSettingsRoot}>
<header className={styles.orgSettingsHeader}>
<h2 className={styles.orgSettingsHeaderText}>
{t('##team/org## details').replace('##team/org##', mmoLabel)}
</h2>
</header>

<section className={styles.fieldsRow}>
<OrganizationSettingsField
label={t('##team/org## name').replace('##team/org##', mmoLabel)}
onChange={handleChangeName}
value={state.name}
validateValue={isNameValueValid}
isDisabled={!isUserAdminOrOwner || isPendingOrgPatch}
/>

{isStripeEnabled && state.website && (
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
<OrganizationSettingsField
label={t('##team/org## website').replace('##team/org##', mmoLabel)}
onChange={handleChangeWebsite}
value={state.website}
validateValue={isWebsiteValueValid}
isDisabled={!isUserAdminOrOwner || isPendingOrgPatch}
/>
)}
</section>

<section className={styles.fieldsRow}>
{isStripeEnabled && state.type && (
<OrganizationSettingsField
label={t('##team/org## type').replace('##team/org##', mmoLabel)}
Akuukis marked this conversation as resolved.
Show resolved Hide resolved
value={getTypeLabel(state.type)}
isDisabled
/>
)}
</section>

<Button
type='primary'
size='m'
onClick={handleSave}
label={t('Save')}
isDisabled={!isUserAdminOrOwner}
isPending={isPendingOrgPatch}
/>

<InlineMessage
type='default'
message={
t("To delete this ##team/org##, you need to cancel your current ##plan name## plan. At the end of the plan period your ##team/org##'s projects will be converted to projects owned by your personal account.")
.replaceAll('##team/org##', mmoLabelLowercase)
.replace('##plan name##', subscriptions.planName)
}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@use 'scss/mixins';
@use 'scss/colors';
@use 'scss/breakpoints';
@use 'js/components/common/textBox.module';

.orgSettingsRoot {
padding: 20px;
overflow-y: auto;
height: 100%;
}

header.orgSettingsHeader {
@include mixins.centerRowFlex;
margin: 24px 0;

&:not(:first-child) {
margin-top: 44px;
}
}

h2.orgSettingsHeaderText {
color: colors.$kobo-storm;
text-transform: uppercase;
font-size: 18px;
font-weight: 700;
flex: 1;
margin: 0;
}

.fieldsRow {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 16px;

&:not(:first-child) {
margin-top: 16px;
}
}

.field {
max-width: 285px;
width: 100%;
}

@include breakpoints.breakpoint(mediumAndUp) {
.orgSettingsRoot {
padding: 50px;
}
}
2 changes: 1 addition & 1 deletion jsapp/js/components/common/textBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface TextBoxProps {
endIcon?: IconName;
value: string;
/** Not needed if `readOnly` */
onChange?: Function;
onChange?: (newValue: string) => void;
onBlur?: Function;
onKeyPress?: Function;
/**
Expand Down