feat: Introduce Feature Preview page (#29698)
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>pull/29749/head^2
parent
0113c62a5c
commit
e846d873b7
@ -0,0 +1,6 @@ |
||||
--- |
||||
'@rocket.chat/rest-typings': minor |
||||
'@rocket.chat/meteor': minor |
||||
--- |
||||
|
||||
feat: Introduce Feature Preview page |
||||
@ -0,0 +1,21 @@ |
||||
import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; |
||||
|
||||
import type { FeaturesAvailable, FeaturePreviewProps } from './useFeaturePreviewList'; |
||||
|
||||
export const useFeaturePreview = (featureName: FeaturesAvailable) => { |
||||
const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); |
||||
const features = useUserPreference<FeaturePreviewProps[]>('featuresPreview'); |
||||
|
||||
const currentFeature = features?.find((feature) => feature.name === featureName); |
||||
|
||||
if (!featurePreviewEnabled) { |
||||
return false; |
||||
} |
||||
|
||||
if (!currentFeature) { |
||||
console.error(`Feature ${featureName} not found`); |
||||
return false; |
||||
} |
||||
|
||||
return currentFeature.value; |
||||
}; |
||||
@ -0,0 +1,44 @@ |
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts'; |
||||
import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; |
||||
|
||||
export type FeaturesAvailable = 'quickReactions'; |
||||
|
||||
export type FeaturePreviewProps = { |
||||
name: FeaturesAvailable; |
||||
i18n: TranslationKey; |
||||
description: TranslationKey; |
||||
group: 'Message' | 'Navigation'; |
||||
imageUrl?: string; |
||||
value: boolean; |
||||
}; |
||||
|
||||
export const defaultFeaturesPreview: FeaturePreviewProps[] = [ |
||||
{ |
||||
name: 'quickReactions', |
||||
i18n: 'Quick_reactions', |
||||
description: 'Quick_reactions_description', |
||||
group: 'Message', |
||||
imageUrl: 'images/featurePreview/quick-reactions.png', |
||||
value: false, |
||||
}, |
||||
]; |
||||
|
||||
export const useFeaturePreviewList = () => { |
||||
const featurePreviewEnabled = useSetting<boolean>('Accounts_AllowFeaturePreview'); |
||||
const userFeaturesPreview = useUserPreference<FeaturePreviewProps[]>('featuresPreview'); |
||||
|
||||
if (!featurePreviewEnabled) { |
||||
return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; |
||||
} |
||||
|
||||
const unseenFeatures = defaultFeaturesPreview.filter( |
||||
(feature) => !userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name), |
||||
).length; |
||||
|
||||
const mergedFeatures = defaultFeaturesPreview.map((feature) => { |
||||
const userFeature = userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name); |
||||
return { ...feature, ...userFeature }; |
||||
}); |
||||
|
||||
return { unseenFeatures, features: mergedFeatures, featurePreviewEnabled }; |
||||
}; |
||||
@ -0,0 +1,22 @@ |
||||
import { Badge } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
|
||||
import { useFeaturePreviewList } from '../../../hooks/useFeaturePreviewList'; |
||||
|
||||
const AccountFeaturePreviewBadge = () => { |
||||
const t = useTranslation(); |
||||
const { unseenFeatures } = useFeaturePreviewList(); |
||||
|
||||
if (!unseenFeatures) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<Badge variant='primary' aria-label={t('Unseen_features')}> |
||||
{unseenFeatures} |
||||
</Badge> |
||||
); |
||||
}; |
||||
|
||||
export default AccountFeaturePreviewBadge; |
||||
@ -0,0 +1,136 @@ |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
import { |
||||
ButtonGroup, |
||||
Button, |
||||
Box, |
||||
Field, |
||||
ToggleSwitch, |
||||
FieldGroup, |
||||
States, |
||||
StatesIcon, |
||||
StatesTitle, |
||||
Accordion, |
||||
} from '@rocket.chat/fuselage'; |
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts'; |
||||
import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; |
||||
import type { ChangeEvent } from 'react'; |
||||
import React, { useEffect, Fragment } from 'react'; |
||||
import { useForm } from 'react-hook-form'; |
||||
|
||||
import Page from '../../../components/Page'; |
||||
import type { FeaturePreviewProps } from '../../../hooks/useFeaturePreviewList'; |
||||
import { useFeaturePreviewList } from '../../../hooks/useFeaturePreviewList'; |
||||
|
||||
const AccountFeaturePreviewPage = () => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const { features, unseenFeatures } = useFeaturePreviewList(); |
||||
|
||||
const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); |
||||
|
||||
useEffect(() => { |
||||
if (unseenFeatures) { |
||||
const featuresPreview = features.map((feature) => ({ |
||||
name: feature.name, |
||||
value: feature.value, |
||||
})); |
||||
|
||||
void setUserPreferences({ data: { featuresPreview } }); |
||||
} |
||||
}, [setUserPreferences, features, unseenFeatures]); |
||||
|
||||
const { |
||||
watch, |
||||
formState: { isDirty }, |
||||
setValue, |
||||
handleSubmit, |
||||
reset, |
||||
} = useForm({ |
||||
defaultValues: { featuresPreview: features }, |
||||
}); |
||||
|
||||
const { featuresPreview } = watch(); |
||||
|
||||
const handleSave = async () => { |
||||
try { |
||||
await setUserPreferences({ data: { featuresPreview } }); |
||||
dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
reset({ featuresPreview }); |
||||
} |
||||
}; |
||||
|
||||
const handleFeatures = (e: ChangeEvent<HTMLInputElement>) => { |
||||
const updated = featuresPreview.map((item) => (item.name === e.target.name ? { ...item, value: e.target.checked } : item)); |
||||
setValue('featuresPreview', updated, { shouldDirty: true }); |
||||
}; |
||||
|
||||
const grouppedFeaturesPreview = Object.entries( |
||||
featuresPreview.reduce((result, currentValue) => { |
||||
(result[currentValue.group] = result[currentValue.group] || []).push(currentValue); |
||||
return result; |
||||
}, {} as Record<FeaturePreviewProps['group'], FeaturePreviewProps[]>), |
||||
); |
||||
|
||||
return ( |
||||
<Page> |
||||
<Page.Header title={t('Feature_preview')}> |
||||
<ButtonGroup> |
||||
<Button primary disabled={!isDirty} onClick={handleSubmit(handleSave)}> |
||||
{t('Save_changes')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Box maxWidth='x600' w='full' alignSelf='center'> |
||||
{featuresPreview.length === 0 && ( |
||||
<States> |
||||
<StatesIcon name='magnifier' /> |
||||
<StatesTitle>{t('No_feature_to_preview')}</StatesTitle> |
||||
</States> |
||||
)} |
||||
{featuresPreview.length > 0 && ( |
||||
<> |
||||
<Box |
||||
className={css` |
||||
white-space: break-spaces; |
||||
`}
|
||||
pbe='x24' |
||||
fontScale='p1' |
||||
> |
||||
{t('Feature_preview_page_description')} |
||||
</Box> |
||||
<Accordion> |
||||
{grouppedFeaturesPreview?.map(([group, features], index) => ( |
||||
<Accordion.Item defaultExpanded={index === 0} key={group} title={t(group as TranslationKey)}> |
||||
<FieldGroup> |
||||
{features.map((feature) => ( |
||||
<Fragment key={feature.name}> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}> |
||||
<Field.Label>{t(feature.i18n)}</Field.Label> |
||||
<Field.Row> |
||||
<Box mie='x12'>{t('Enabled')}</Box> |
||||
<ToggleSwitch checked={feature.value} name={feature.name} onChange={handleFeatures} /> |
||||
</Field.Row> |
||||
</Box> |
||||
{feature.description && <Field.Hint mbs='x12'>{t(feature.description)}</Field.Hint>} |
||||
</Field> |
||||
{feature.imageUrl && <Box is='img' width='100%' height='auto' mbs='x16' src={feature.imageUrl} />} |
||||
</Fragment> |
||||
))} |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
))} |
||||
</Accordion> |
||||
</> |
||||
)} |
||||
</Box> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
); |
||||
}; |
||||
|
||||
export default AccountFeaturePreviewPage; |
||||
|
After Width: | Height: | Size: 5.5 KiB |
Loading…
Reference in new issue