Skip to content

Commit

Permalink
feat(condo): DOMA-6736 schema MobileFeatureConfig with tests (#3662)
Browse files Browse the repository at this point in the history
* feat(condo): DOMA-6736 schema MobileFeatureConfig with tests

* fix(condo): DOMA-6736 change field name from ticketSubmittingIsEnabled to ticketSubmittingIsDisabled. Rm emergencyPhone field

* fix(condo): DOMA-6736 fix imports

* feat(condo): DOMA-6753 frontend of MobileFeatureConfig

* fix(condo): DOMA-6736 schemaDoc of some field of MobileFeatureConfig

* fix(condo): DOMA-6736 review fixes

* feat(condo): DOMA-6736 added permission canManageMobileFeatureConfigs for manage access

* fix(condo): DOMA-6736 rename onlyProgressionMeterReadingsIsEnabled to onlyGreaterThanPreviousMeterReadingIsEnabled

* fix(condo): DOMA-6736 replace catchErrorFrom to expectToThrowGQLError

* fix(condo): DOMA-6736 use valuePropName

* fix(condo): DOMA-6736 add deletedAt filter

* fix(condo): DOMA-6736 review fixes

some optimization on front, rm redundant react state. add tests for support. move FF constants to common/constants/featureFlags. fix schemaDoc for meta

* fix(condo): DOMA-6736 some code improvements

* chore(condo): DOMA-6736 fix imports
Co-authored-by: Alllex202 <[email protected]>
--------
  • Loading branch information
VKislov authored Aug 12, 2023
1 parent 200d493 commit c9eadcc
Show file tree
Hide file tree
Showing 31 changed files with 2,402 additions and 5 deletions.
1 change: 0 additions & 1 deletion apps/condo/domains/common/components/PhoneInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ export const PhoneInput: React.FC<IPhoneInputProps> = forwardRef((props, ref) =>
value: formattedValue,
},
}
// @ts-ignore
props.onChange(event)
} else {
props.onChange(formattedValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Col, Row } from 'antd'
import { Gutter } from 'antd/es/grid/row'
import get from 'lodash/get'
import React from 'react'

import { useFeatureFlags } from '@open-condo/featureflags/FeatureFlagsContext'
import { useOrganization } from '@open-condo/next/organization'

import { CardsContainer } from '@condo/domains/common/components/Card/CardsContainer'
import { SettingCardSkeleton } from '@condo/domains/common/components/settings/SettingCard'
import { MOBILE_FEATURE_CONFIGURATION, TICKET_SUBMITTING_FORM_RESIDENT_MOBILE_APP, SUBMIT_ONLY_PROGRESSION_METER_READINGS } from '@condo/domains/common/constants/featureflags'
import {
OnlyProgressionMeterReadingsSettingCard,
} from '@condo/domains/settings/components/ticketSubmitting/OnlyProgressionMeterReadingsSettingCard'
import { TicketSubmittingSettingCard } from '@condo/domains/settings/components/ticketSubmitting/TicketSubmittingSettingCard'
import { MobileFeatureConfig as MobileFeatureConfigAPI } from '@condo/domains/settings/utils/clientSchema'

const CONTENT_GUTTER: Gutter | [Gutter, Gutter] = [0, 40]

export const MobileFeatureConfigContent: React.FC = () => {
const userOrganization = useOrganization()
const userOrganizationId = get(userOrganization, ['organization', 'id'], null)
const { obj: mobileConfig, loading } = MobileFeatureConfigAPI.useObject({
where: {
organization: { id: userOrganizationId },
},
})
const { useFlag } = useFeatureFlags()
const hasMobileFeatureConfigurationFeature = useFlag(MOBILE_FEATURE_CONFIGURATION)
const hasTicketSubmittingSettingFeature = useFlag(TICKET_SUBMITTING_FORM_RESIDENT_MOBILE_APP)
const hasOnlyProgressionMeterReadingsSettingFeature = useFlag(SUBMIT_ONLY_PROGRESSION_METER_READINGS)

if (!hasMobileFeatureConfigurationFeature) {
return null
}

return (
<Row gutter={CONTENT_GUTTER}>
<Col span={24}>
<CardsContainer cardsPerRow={2}>
{
!hasTicketSubmittingSettingFeature ? null : (loading
? <SettingCardSkeleton/>
: <TicketSubmittingSettingCard mobileConfig={mobileConfig}/>)
}
{
!hasOnlyProgressionMeterReadingsSettingFeature ? null : (loading
? <SettingCardSkeleton />
: <OnlyProgressionMeterReadingsSettingCard mobileConfig={mobileConfig}/>)
}
</CardsContainer>
</Col>
</Row>
)
}
6 changes: 6 additions & 0 deletions apps/condo/domains/common/constants/featureflags.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const PROPERTY_REPORT_DELETE_ENTITIES = 'property-report-delete-entities'
const SERVICE_PROVIDER_PROFILE = 'service-provider-profile'
const SHOW_ORGANIZATION_TYPES = 'show-organization-types'
const ENABLE_DISCOVER_SERVICE_CONSUMERS = 'enable-discover-service-consumers'
const MOBILE_FEATURE_CONFIGURATION = 'mobile-feature-configuration'
const TICKET_SUBMITTING_FORM_RESIDENT_MOBILE_APP = 'ticket-submitting-from-resident-mobile-app'
const SUBMIT_ONLY_PROGRESSION_METER_READINGS = 'submit-only-progression-meter-readings'

module.exports = {
SMS_AFTER_TICKET_CREATION,
Expand All @@ -28,4 +31,7 @@ module.exports = {
SERVICE_PROVIDER_PROFILE,
SHOW_ORGANIZATION_TYPES,
ENABLE_DISCOVER_SERVICE_CONSUMERS,
MOBILE_FEATURE_CONFIGURATION,
TICKET_SUBMITTING_FORM_RESIDENT_MOBILE_APP,
SUBMIT_ONLY_PROGRESSION_METER_READINGS,
}
2 changes: 2 additions & 0 deletions apps/condo/domains/common/constants/settingsTabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const SETTINGS_TAB_PAYMENT_DETAILS = 'paymentDetails'
const SETTINGS_TAB_CONTACT_ROLES = 'contactRoles'
const SETTINGS_TAB_TICKET_ORGANIZATION = 'ticketOrganization'
const SETTINGS_TAB_CONTROL_ROOM = 'controlRoom'
const SETTINGS_TAB_MOBILE_FEATURE_CONFIG = 'mobileFeatureConfig'

module.exports = {
SETTINGS_TAB_SUBSCRIPTION,
Expand All @@ -16,4 +17,5 @@ module.exports = {
SETTINGS_TAB_TICKET_ORGANIZATION,
SETTINGS_TAB_CONTROL_ROOM,
SETTINGS_TAB_EMPLOYEE_ROLES,
SETTINGS_TAB_MOBILE_FEATURE_CONFIG,
}
1 change: 1 addition & 0 deletions apps/condo/domains/organization/constants/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const DEFAULT_ROLES = {
'ticketVisibilityType': ORGANIZATION_TICKET_VISIBILITY,
'canManageNewsItems': true,
'canManageNewsItemTemplates': true,
'canManageMobileFeatureConfigs': true,
},
'Dispatcher': {
'name': 'employee.role.Dispatcher.name',
Expand Down
2 changes: 1 addition & 1 deletion apps/condo/domains/organization/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const COMMON_FIELDS = 'id dv sender { dv fingerprint } v deletedAt newId created
const ORGANIZATION_FIELDS = `{ country name type description avatar { publicUrl } meta tin features statusTransitions defaultEmployeeRoleStatusTransitions importId importRemoteSystem phone phoneNumberPrefix ${COMMON_FIELDS} }`
const Organization = generateGqlQueries('Organization', ORGANIZATION_FIELDS)

const ORGANIZATION_EMPLOYEE_ROLE_FIELDS = '{ organization { id } name nameNonLocalized description descriptionNonLocalized statusTransitions canManageOrganization canManageCallRecords canDownloadCallRecords canManageEmployees canInviteNewOrganizationEmployees canManageRoles canManageTicketPropertyHints canManageIntegrations canReadBillingReceipts canReadPayments canManageProperties canManageTickets canManageContacts canManageContactRoles canManageTicketComments canManagePropertyScopes canShareTickets canBeAssignedAsResponsible canBeAssignedAsExecutor canManageMeters canManageMeterReadings ticketVisibilityType canManageBankAccounts canManageBankContractorAccounts canManageBankIntegrationAccountContexts canManageBankIntegrationOrganizationContexts canManageBankTransactions canManageBankAccountReports canManageBankAccountReportTasks canManageBankAccountReports canManageIncidents canManageNewsItems canManageNewsItemTemplates id dv sender { dv fingerprint } v createdBy { id name } updatedBy { id name } createdAt updatedAt }'
const ORGANIZATION_EMPLOYEE_ROLE_FIELDS = '{ organization { id } name nameNonLocalized description descriptionNonLocalized statusTransitions canManageOrganization canManageCallRecords canDownloadCallRecords canManageEmployees canInviteNewOrganizationEmployees canManageRoles canManageTicketPropertyHints canManageIntegrations canReadBillingReceipts canReadPayments canManageProperties canManageTickets canManageContacts canManageContactRoles canManageTicketComments canManagePropertyScopes canShareTickets canBeAssignedAsResponsible canBeAssignedAsExecutor canManageMeters canManageMeterReadings ticketVisibilityType canManageBankAccounts canManageBankContractorAccounts canManageBankIntegrationAccountContexts canManageBankIntegrationOrganizationContexts canManageBankTransactions canManageBankAccountReports canManageBankAccountReportTasks canManageBankAccountReports canManageIncidents canManageNewsItems canManageNewsItemTemplates canManageMobileFeatureConfigs id dv sender { dv fingerprint } v createdBy { id name } updatedBy { id name } createdAt updatedAt }'
const OrganizationEmployeeRole = generateGqlQueries('OrganizationEmployeeRole', ORGANIZATION_EMPLOYEE_ROLE_FIELDS)

const ORGANIZATION_EMPLOYEE_FIELDS = `{ organization ${ORGANIZATION_FIELDS} user { id name } name email phone role ${ORGANIZATION_EMPLOYEE_ROLE_FIELDS} hasAllSpecializations isRejected isAccepted isBlocked id dv sender { dv fingerprint } v createdBy { id name } updatedBy { id name } position createdAt deletedAt updatedAt }`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const OrganizationEmployeeRole = new GQLListSchema('OrganizationEmployeeRole', {
canManageNewsItemTemplates: { type: Checkbox, defaultValue: false },
canManageCallRecords: { type: Checkbox, defaultValue: false },
canDownloadCallRecords: { type: Checkbox, defaultValue: false },
canManageMobileFeatureConfigs: { type: Checkbox, defaultValue: false },
},
plugins: [uuided(), versioned(), tracked(), dvAndSender(), historical()],
access: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('OrganizationEmployeeRole', () => {
expect(obj.canManageIncidents).toBeFalsy()
expect(obj.canManageNewsItems).toBeFalsy()
expect(obj.canManageNewsItemTemplates).toBeFalsy()
expect(obj.canManageMobileFeatureConfigs).toBeFalsy()
expect(obj.nameNonLocalized).toEqual(obj.name)
expect(obj.descriptionNonLocalized).toEqual(obj.description)
expect(obj.ticketVisibilityType).toEqual(ORGANIZATION_TICKET_VISIBILITY)
Expand Down
77 changes: 77 additions & 0 deletions apps/condo/domains/settings/access/MobileFeatureConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Generated by `createschema settings.MobileFeatureConfig 'organization:Relationship:Organization:CASCADE; emergencyPhone:Text; commonPhone:Text; onlyGreaterThanPreviousMeterReadingIsEnabled:Checkbox; meta:Json; ticketSubmittingIsEnabled:Checkbox'`
*/

const { uniq, map, get } = require('lodash')

const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter')
const { find, getById } = require('@open-condo/keystone/schema')

const { checkPermissionInUserOrganizationOrRelatedOrganization } = require('@condo/domains/organization/utils/accessSchema')
const { queryOrganizationEmployeeFor, queryOrganizationEmployeeFromRelatedOrganizationFor } = require('@condo/domains/organization/utils/accessSchema')
const { RESIDENT } = require('@condo/domains/user/constants/common')

async function canReadMobileFeatureConfigs ({ authentication: { item: user } }) {
if (!user) return throwAuthenticationError()
if (user.deletedAt) return false

if (user.isAdmin || user.isSupport) return {}

if (user.type === RESIDENT) {
const residents = await find('Resident', { user: { id: user.id }, deletedAt: null })
const organizations = uniq(map(residents, 'organization'))

if (residents.length > 0) {
return {
organization: {
id_in: organizations,
deletedAt: null,
},
deletedAt: null,
}
}
return false
}

return {
organization: {
OR: [
queryOrganizationEmployeeFor(user.id),
queryOrganizationEmployeeFromRelatedOrganizationFor(user.id),
],
},
}
}

async function canManageMobileFeatureConfigs (attrs) {
const { authentication: { item: user }, originalInput, operation, itemId } = attrs
if (!user) return throwAuthenticationError()
if (user.deletedAt) return false
if (user.type === RESIDENT) return false
if (user.isAdmin || user.isSupport) return true

let organizationId
if (operation === 'create') {
organizationId = get(originalInput, 'organization.connect.id')
}
if ( operation === 'update') {
if (!itemId) return false

const foundConfig = await getById('MobileFeatureConfig', itemId)
if (!foundConfig) return false

organizationId = get(foundConfig, 'organization')
}

return await checkPermissionInUserOrganizationOrRelatedOrganization(user.id, organizationId, 'canManageMobileFeatureConfigs')
}

/*
Rules are logical functions that used for list access, and may return a boolean (meaning
all or no items are available) or a set of filters that limit the available items.
*/
module.exports = {
canReadMobileFeatureConfigs,
canManageMobileFeatureConfigs,
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { MobileFeatureConfig as MobileFeatureConfigType } from '@app/condo/schema'
import { Col, Form, Row } from 'antd'
import { Gutter } from 'antd/es/grid/row'
import get from 'lodash/get'
import { useRouter } from 'next/router'
import React, { useMemo, useState } from 'react'

import { useIntl } from '@open-condo/next/intl'
import { ActionBar, Button, Checkbox, Typography } from '@open-condo/ui'

import { FormWithAction } from '@condo/domains/common/components/containers/FormList'
import { MobileFeatureConfig } from '@condo/domains/settings/utils/clientSchema'

const INPUT_LAYOUT_PROPS = {
labelCol: {
span: 24,
md: 5,
},
wrapperCol: {
span: 24,
md: 6,
},
styled: {
paddingBottom: '12px',
},
colon: false,
}
const BIG_ROW_GUTTERS: [Gutter, Gutter] = [0, 60]
const MIDDLE_ROW_GUTTERS: [Gutter, Gutter] = [0, 40]
const SMALL_ROW_GUTTERS: [Gutter, Gutter] = [0, 20]

interface ITicketSubmittingSettingsForm {
mobileConfig?: MobileFeatureConfigType,
userOrganizationId: string,
}

export const OnlyProgressionMeterReadingsForm: React.FC<ITicketSubmittingSettingsForm> = ({ mobileConfig, userOrganizationId }) => {
const intl = useIntl()
const SaveMessage = intl.formatMessage({ id: 'Save' })
const MessageAboutFeat = intl.formatMessage({ id: 'pages.condo.settings.mobileFeatureConfig.OnlyProgressionMeterReadings.messageAboutFeat' })
const EnableMessage = intl.formatMessage({ id: 'pages.condo.settings.mobileFeatureConfig.OnlyProgressionMeterReadings.isEnabled' })

const router = useRouter()

const initialValues = {
onlyGreaterThanPreviousMeterReadingIsEnabled: get(mobileConfig, 'onlyGreaterThanPreviousMeterReadingIsEnabled'),
}

const updateHook = MobileFeatureConfig.useUpdate({}, () => router.push('/settings?tab=mobileFeatureConfig'))
const updateAction = async (data) => {
await updateHook(data, mobileConfig)
}
const createAction = MobileFeatureConfig.useCreate({}, () => router.push('/settings?tab=mobileFeatureConfig'))
const action = mobileConfig ? updateAction : createAction

return useMemo(() => (
<FormWithAction
initialValues={initialValues}
action={action}
colon={false}
layout='horizontal'
formValuesToMutationDataPreprocessor={(values) => {
if (!mobileConfig) {
values.organization = { connect: { id: userOrganizationId } }
}
return values
}}
>
{({ handleSave, isLoading }) => (
<Row gutter={BIG_ROW_GUTTERS}>
<Col span={24}>
<Row gutter={MIDDLE_ROW_GUTTERS}>
<Col span={24}>
<Row gutter={SMALL_ROW_GUTTERS}>
<Col span={24}>
<Typography.Text >{MessageAboutFeat}</Typography.Text>
</Col>
<Col span={24}>
<Form.Item
name='onlyGreaterThanPreviousMeterReadingIsEnabled'
label={EnableMessage}
labelAlign='left'
{...INPUT_LAYOUT_PROPS}
valuePropName='checked'
>
<Checkbox/>
</Form.Item>
</Col>

</Row>
</Col>

</Row>
</Col>
<Col span={24}>
<ActionBar
actions={[
<Button
key='submit'
onClick={handleSave}
type='primary'
loading={isLoading}
>
{SaveMessage}
</Button>,
]}
/>
</Col>
</Row>
)}
</FormWithAction>
), [action, mobileConfig])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MobileFeatureConfig as MobileFeatureConfigType } from '@app/condo/schema'
import get from 'lodash/get'
import { useRouter } from 'next/router'
import React, { useCallback } from 'react'

import { useIntl } from '@open-condo/next/intl'
import { Typography } from '@open-condo/ui'

import { SettingCard } from '@condo/domains/common/components/settings/SettingCard'

interface OnlyProgressionMeterReadingsSettingCardProps {
mobileConfig?: MobileFeatureConfigType
}

const TICKET_DISABLING_SETTINGS_URL = '/settings/mobileFeatureConfig/onlyProgressionMeterReadings'

export const OnlyProgressionMeterReadingsSettingCard: React.FC<OnlyProgressionMeterReadingsSettingCardProps> = ({ mobileConfig }) => {
const intl = useIntl()
const OnlyProgressionMeterReadingsTitle = intl.formatMessage({ id: 'pages.condo.settings.barItem.MobileFeatureConfig.OnlyProgressionMeterReadings.title' })
const OnlyProgressionMeterReadingsIsDisabledLabel = intl.formatMessage({ id: 'pages.condo.settings.barItem.MobileFeatureConfig.OnlyProgressionMeterReadings.isDisabled' })
const onlyGreaterThanPreviousMeterReadingIsEnabledLabel = intl.formatMessage({ id: 'pages.condo.settings.barItem.MobileFeatureConfig.OnlyProgressionMeterReadings.isEnabled' })

const router = useRouter()

const handleClickCard = useCallback(() => {
router.push(TICKET_DISABLING_SETTINGS_URL)
}, [router])

const isEnabled = get(mobileConfig, 'onlyGreaterThanPreviousMeterReadingIsEnabled')

return (
<SettingCard title={OnlyProgressionMeterReadingsTitle} onClick={handleClickCard}>
<Typography.Text type='secondary' >
{isEnabled ? onlyGreaterThanPreviousMeterReadingIsEnabledLabel : OnlyProgressionMeterReadingsIsDisabledLabel}
</Typography.Text>
</SettingCard>
)
}
Loading

0 comments on commit c9eadcc

Please sign in to comment.