snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { Skeleton } from '@rocket.chat/fuselage';
|
||||
|
||||
const SKELETON_ITEMS = 3;
|
||||
|
||||
const AccordionLoading = () => (
|
||||
<>
|
||||
{Array.from({ length: SKELETON_ITEMS }, (_v, k) => (
|
||||
<Skeleton key={k} variant='rect' height='80px' margin='2px' />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
export default AccordionLoading;
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import AppInstallModal from './AppInstallModal';
|
||||
|
||||
export default {
|
||||
component: AppInstallModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof AppInstallModal>;
|
||||
|
||||
export const Default = {
|
||||
name: 'AppInstallModal',
|
||||
|
||||
args: {
|
||||
enabled: 1,
|
||||
limit: 3,
|
||||
appName: 'Example-app-name',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalFooterControllers,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MarkdownText from '../../../../components/MarkdownText';
|
||||
|
||||
type AppsInstallationModalProps = {
|
||||
enabled: number;
|
||||
limit: number;
|
||||
appName: string;
|
||||
handleClose: () => void;
|
||||
handleConfirm: () => void;
|
||||
handleEnableUnlimitedApps: () => void;
|
||||
};
|
||||
|
||||
const AppInstallationModal = ({
|
||||
enabled,
|
||||
limit,
|
||||
appName,
|
||||
handleClose,
|
||||
handleConfirm,
|
||||
handleEnableUnlimitedApps,
|
||||
}: AppsInstallationModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getTitle = () => {
|
||||
if (enabled === limit) {
|
||||
return t('App_limit_reached');
|
||||
}
|
||||
|
||||
if (enabled > limit) {
|
||||
return t('App_limit_exceeded');
|
||||
}
|
||||
|
||||
return t('Apps_Currently_Enabled', { context: '', enabled, limit });
|
||||
};
|
||||
|
||||
const getContent = () => {
|
||||
if (enabled === limit) {
|
||||
return t('Enable_of_limit_apps_currently_enabled', { context: '', enabled, limit, appName });
|
||||
}
|
||||
|
||||
if (enabled > limit) {
|
||||
return t('Enable_of_limit_apps_currently_enabled_exceeded', {
|
||||
enabled,
|
||||
limit,
|
||||
exceed: enabled - limit + 1,
|
||||
appName,
|
||||
});
|
||||
}
|
||||
|
||||
return t('Workspaces_on_Community_edition_install_app', { context: '', enabled, limit });
|
||||
};
|
||||
|
||||
const confirmButtonOverLimitLabel = t('Install_anyway');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{getTitle()}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={handleClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<MarkdownText content={getContent()} />
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<ModalFooterControllers>
|
||||
<Button onClick={handleEnableUnlimitedApps}>{t('Enable_unlimited_apps')}</Button>
|
||||
<Button {...(enabled < limit && { primary: true })} onClick={handleConfirm}>
|
||||
{enabled < limit ? t('Next') : confirmButtonOverLimitLabel}
|
||||
</Button>
|
||||
</ModalFooterControllers>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppInstallationModal;
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { AppPermission } from '@rocket.chat/core-typings';
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const defaultPermissions = [
|
||||
'user.read',
|
||||
'user.write',
|
||||
'upload.read',
|
||||
'upload.write',
|
||||
'ui.interact',
|
||||
'server-setting.read',
|
||||
'server-setting.write',
|
||||
'room.read',
|
||||
'room.write',
|
||||
'message.read',
|
||||
'message.write',
|
||||
'livechat-department.read',
|
||||
'livechat-department.write',
|
||||
'livechat-room.read',
|
||||
'livechat-room.write',
|
||||
'livechat-message.read',
|
||||
'livechat-message.write',
|
||||
'livechat-visitor.read',
|
||||
'livechat-visitor.write',
|
||||
'livechat-status.read',
|
||||
'livechat-custom-fields.write',
|
||||
'scheduler',
|
||||
'networking',
|
||||
'persistence',
|
||||
'env.read',
|
||||
'slashcommand',
|
||||
'api',
|
||||
];
|
||||
|
||||
export type AppPermissionsListProps = { appPermissions: AppPermission[] | undefined };
|
||||
|
||||
const AppPermissionsList = ({ appPermissions }: AppPermissionsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const handleAppPermissions = (permission: string): string => t(`Apps_Permissions_${permission.replace('.', '_')}` as TranslationKey);
|
||||
|
||||
if (appPermissions?.length) {
|
||||
return (
|
||||
<>
|
||||
{appPermissions.map((permission) => (
|
||||
<Fragment key={permission.name}>
|
||||
<li>{handleAppPermissions(permission.name)}</li>
|
||||
{permission.required && (
|
||||
<Box is='span' color='status-font-on-danger'>
|
||||
({t('required')})
|
||||
</Box>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{defaultPermissions.map((permission) => (
|
||||
<Fragment key={permission}>
|
||||
<li>{handleAppPermissions(permission)}</li>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppPermissionsList;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Banner, Icon } from '@rocket.chat/fuselage';
|
||||
import { useLocalStorage } from '@rocket.chat/fuselage-hooks';
|
||||
import { usePermission, useSetting } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BannerEnterpriseTrialEnded = () => {
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = usePermission('manage-apps');
|
||||
|
||||
const bannerLink = {
|
||||
link: '/links/manage-subscription',
|
||||
linkText: t('Upgrade_tab_upgrade_your_plan'),
|
||||
linkTarget: '_blank',
|
||||
};
|
||||
|
||||
const cloudWorkspaceHadTrial = useSetting('Cloud_Workspace_Had_Trial', false);
|
||||
|
||||
const [showTrialBanner, setShowTrialBanner] = useLocalStorage('showAppsTrialEndBanner', cloudWorkspaceHadTrial);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTrialBanner && (
|
||||
<Banner
|
||||
closeable
|
||||
icon={<Icon name='store' size='x24' />}
|
||||
variant='warning'
|
||||
title={t('Apps_disabled_when_Premium_trial_ended')}
|
||||
onClose={() => setShowTrialBanner(false)}
|
||||
{...(isAdmin && bannerLink)}
|
||||
>
|
||||
{isAdmin
|
||||
? t('Apps_disabled_when_Premium_trial_ended_description_admin')
|
||||
: t('Apps_disabled_when_Premium_trial_ended_description')}
|
||||
</Banner>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BannerEnterpriseTrialEnded;
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Box, ButtonGroup } from '@rocket.chat/fuselage';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import CategoryDropDown from './CategoryDropDown';
|
||||
import CategoryDropDownAnchor from './CategoryDropDownAnchor';
|
||||
import CategoryDropDownList from './CategoryDropDownList';
|
||||
import TagList from './TagList';
|
||||
import type { CategoryDropDownListProps } from '../../definitions/CategoryDropdownDefinitions';
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useCategoryToggle } from '../../hooks/useCategoryToggle';
|
||||
|
||||
export default {
|
||||
component: CategoryDropDownAnchor,
|
||||
};
|
||||
|
||||
const testCategories: CategoryDropDownListProps['categories'] = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: 'all',
|
||||
label: 'All categories',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Filter by Category',
|
||||
items: [
|
||||
{ id: '0', label: 'Analytics', checked: false },
|
||||
{ id: '1', label: 'Bots', checked: false },
|
||||
{ id: '2', label: 'Communication', checked: false },
|
||||
{ id: '3', label: 'Content Management', checked: false },
|
||||
{ id: '4', label: 'Customer Support', checked: false },
|
||||
{ id: '5', label: 'Design', checked: false },
|
||||
{ id: '6', label: 'Developer Tools', checked: false },
|
||||
{ id: '7', label: 'File Management', checked: false },
|
||||
{ id: '8', label: 'Finance', checked: false },
|
||||
{ id: '9', label: 'Health & Wellness', checked: false },
|
||||
{ id: '10', label: 'Human Resources', checked: false },
|
||||
{ id: '11', label: 'Marketing', checked: false },
|
||||
{ id: '12', label: 'Media & News', checked: false },
|
||||
{ id: '13', label: 'Office Management', checked: false },
|
||||
{ id: '14', label: 'Omnichannel', checked: false },
|
||||
{ id: '15', label: 'Other', checked: false },
|
||||
{ id: '16', label: 'Productivity', checked: false },
|
||||
{ id: '17', label: 'Project Management', checked: false },
|
||||
{ id: '18', label: 'Sales', checked: false },
|
||||
{ id: '19', label: 'Security & Compliance', checked: false },
|
||||
{ id: '20', label: 'Social & Fun', checked: false },
|
||||
{ id: '21', label: 'Team Culture', checked: false },
|
||||
{ id: '22', label: 'Travel', checked: false },
|
||||
{ id: '23', label: 'Voice & Video', checked: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Anchor: StoryFn = () => (
|
||||
<CategoryDropDownAnchor selectedCategoriesCount={testCategories.flatMap((group) => group.items).length} />
|
||||
);
|
||||
export const List: StoryFn = () => {
|
||||
const [data, setData] = useState(() => testCategories);
|
||||
|
||||
const onSelected = useCategoryToggle(setData);
|
||||
|
||||
return <CategoryDropDownList categories={data} onSelected={onSelected} />;
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
const [, selectedCategories, categoryTagList] = useCategories();
|
||||
|
||||
const [data, setData] = useState(testCategories);
|
||||
|
||||
const onSelected = useCategoryToggle(setData);
|
||||
|
||||
return (
|
||||
<Box width='x144'>
|
||||
<ButtonGroup>
|
||||
<CategoryDropDown categories={data} selectedCategories={selectedCategories} onSelected={onSelected} />
|
||||
</ButtonGroup>
|
||||
<TagList categories={categoryTagList} onClick={onSelected} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Button } from '@rocket.chat/fuselage';
|
||||
import { useToggle } from '@rocket.chat/fuselage-hooks';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import CategoryDropDownAnchor from './CategoryDropDownAnchor';
|
||||
import CategoryDropDownList from './CategoryDropDownList';
|
||||
import type { CategoryDropdownItem, CategoryDropDownListProps } from '../../definitions/CategoryDropdownDefinitions';
|
||||
import { isValidReference } from '../../helpers/isValidReference';
|
||||
import { onMouseEventPreventSideEffects } from '../../helpers/onMouseEventPreventSideEffects';
|
||||
import DropDownListWrapper from '../DropDownListWrapper';
|
||||
|
||||
export type CategoryDropDownProps = {
|
||||
categories: CategoryDropDownListProps['categories'];
|
||||
onSelected: CategoryDropDownListProps['onSelected'];
|
||||
selectedCategories: (CategoryDropdownItem & { checked: true })[];
|
||||
} & ComponentProps<typeof Button>;
|
||||
|
||||
const CategoryDropDown = ({ categories, onSelected, selectedCategories, ...props }: CategoryDropDownProps) => {
|
||||
const reference = useRef<HTMLInputElement>(null);
|
||||
const [collapsed, toggleCollapsed] = useToggle(false);
|
||||
|
||||
const onClose = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (isValidReference(reference, e as { target: Node | null })) {
|
||||
toggleCollapsed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
onMouseEventPreventSideEffects(e);
|
||||
},
|
||||
[toggleCollapsed],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CategoryDropDownAnchor
|
||||
ref={reference}
|
||||
onClick={() => toggleCollapsed()}
|
||||
selectedCategoriesCount={selectedCategories.length}
|
||||
{...props}
|
||||
/>
|
||||
{collapsed && (
|
||||
<DropDownListWrapper ref={reference} onClose={onClose}>
|
||||
<CategoryDropDownList categories={categories} onSelected={onSelected} />
|
||||
</DropDownListWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryDropDown;
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { Button } from '@rocket.chat/fuselage';
|
||||
import { Box, Icon } from '@rocket.chat/fuselage';
|
||||
import colorTokens from '@rocket.chat/fuselage-tokens/dist/colors.json';
|
||||
import type { ComponentProps, MouseEventHandler } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type CategoryDropDownAnchorProps = { selectedCategoriesCount: number; onClick?: MouseEventHandler<HTMLElement> } & ComponentProps<
|
||||
typeof Button
|
||||
>;
|
||||
|
||||
const CategoryDropDownAnchor = forwardRef<HTMLElement, CategoryDropDownAnchorProps>(function CategoryDropDownAnchor(
|
||||
{ onClick, selectedCategoriesCount, ...props },
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
is='button'
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
alignItems='center'
|
||||
backgroundColor={selectedCategoriesCount ? colorTokens.b500 : 'light'}
|
||||
borderColor={selectedCategoriesCount ? 'none' : 'light'}
|
||||
borderRadius='x4'
|
||||
borderWidth={selectedCategoriesCount ? 'none' : 'x1'}
|
||||
display='flex'
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
justifyContent='space-between'
|
||||
minWidth='x144'
|
||||
height='x40'
|
||||
paddingInlineEnd={7}
|
||||
paddingInlineStart={14}
|
||||
lineHeight='unset'
|
||||
rcx-input-box
|
||||
{...props}
|
||||
>
|
||||
{selectedCategoriesCount > 0 && (
|
||||
<Box
|
||||
is='span'
|
||||
alignItems='center'
|
||||
backgroundColor='light'
|
||||
borderRadius='x32'
|
||||
color='info'
|
||||
display='flex'
|
||||
fontSize='micro'
|
||||
fontWeight={700}
|
||||
height='fit-content'
|
||||
justifyContent='center'
|
||||
marginInlineEnd={6}
|
||||
minWidth={22}
|
||||
paddingBlock={4}
|
||||
paddingInline={0}
|
||||
>
|
||||
{selectedCategoriesCount}
|
||||
</Box>
|
||||
)}
|
||||
<Box is='span' display='flex' flexGrow={1} fontScale='p2' color={selectedCategoriesCount ? 'white' : 'hint'}>
|
||||
{selectedCategoriesCount > 0 ? t('Categories') : t('All_categories')}
|
||||
</Box>
|
||||
<Box marginInline={4} display='flex' alignItems='center' justifyContent='center'>
|
||||
<Icon name='chevron-down' size='x20' color={selectedCategoriesCount ? 'white' : 'hint'} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default CategoryDropDownAnchor;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Box, CheckBox, Option, Tile } from '@rocket.chat/fuselage';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import type { CategoryDropDownListProps } from '../../definitions/CategoryDropdownDefinitions';
|
||||
|
||||
const CategoryDropDownList = ({ categories, onSelected }: CategoryDropDownListProps) => {
|
||||
return (
|
||||
<Tile overflow='auto' paddingBlock={12} paddingInline={0} elevation='2' width='full' backgroundColor='light' borderRadius={2}>
|
||||
{categories.map((category, index) => (
|
||||
<Fragment key={index}>
|
||||
{category.label && (
|
||||
<Box paddingInline={16} paddingBlockStart={8} paddingBlockEnd={4} fontScale='micro' textTransform='uppercase' color='default'>
|
||||
{category.label}
|
||||
</Box>
|
||||
)}
|
||||
{category.items.map((item) => (
|
||||
<Option key={item.id} {...({ label: item.label } as any)} onClick={(): void => onSelected(item)}>
|
||||
<CheckBox checked={item.checked} onChange={(): void => onSelected(item)} />
|
||||
</Option>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryDropDownList;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Chip, ButtonGroup } from '@rocket.chat/fuselage';
|
||||
|
||||
import type { CategoryDropdownItem, CategoryDropDownListProps } from '../../definitions/CategoryDropdownDefinitions';
|
||||
|
||||
export type TagListProps = {
|
||||
categories: (CategoryDropdownItem & { checked: true })[];
|
||||
onClick: CategoryDropDownListProps['onSelected'];
|
||||
};
|
||||
|
||||
const TagList = ({ categories, onClick }: TagListProps) => {
|
||||
if (!categories.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonGroup wrap small>
|
||||
{categories.map((category) => (
|
||||
<Chip key={category.id} onClick={() => onClick(category)}>
|
||||
{category.label}
|
||||
</Chip>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagList;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { usePosition, useOutsideClick } from '@rocket.chat/fuselage-hooks';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { forwardRef, useRef } from 'react';
|
||||
|
||||
const options = {
|
||||
margin: 8,
|
||||
placement: 'bottom-end',
|
||||
} as const;
|
||||
|
||||
const hidden = {
|
||||
visibility: 'hidden',
|
||||
opacity: 0,
|
||||
position: 'fixed',
|
||||
} as const;
|
||||
|
||||
const DropDownListWrapper = forwardRef<Element, ComponentProps<typeof Box> & { onClose: (e: MouseEvent) => void }>(
|
||||
function CategoryDropDownListWrapper({ children, onClose }, ref) {
|
||||
const target = useRef<HTMLElement>(null);
|
||||
useOutsideClick([target], onClose);
|
||||
const { style = hidden } = usePosition(ref as Parameters<typeof usePosition>[0], target, options);
|
||||
return (
|
||||
<Box ref={target} style={style} minWidth={224} zIndex='99999'>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default DropDownListWrapper;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import EnabledAppsCount from './EnabledAppsCount';
|
||||
|
||||
describe('in private context', () => {
|
||||
const context = 'private';
|
||||
|
||||
it('should work under the limit', async () => {
|
||||
render(<EnabledAppsCount enabled={1} limit={2} context={context} />, {
|
||||
wrapper: mockAppRoot()
|
||||
.withTranslations('en', 'core', {
|
||||
Private_Apps_Count_Enabled_one: '{{count}} private app enabled',
|
||||
Private_Apps_Count_Enabled_other: '{{count}} private apps enabled',
|
||||
})
|
||||
.build(),
|
||||
});
|
||||
|
||||
expect(screen.getByText('1 private app enabled')).toBeInTheDocument();
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemin', '0');
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50');
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemax', '100');
|
||||
});
|
||||
|
||||
it('should work with private apps disabled', async () => {
|
||||
render(<EnabledAppsCount enabled={0} limit={0} context={context} />, {
|
||||
wrapper: mockAppRoot()
|
||||
.withTranslations('en', 'core', {
|
||||
Private_Apps_Count_Enabled_one: '{{count}} private app enabled',
|
||||
Private_Apps_Count_Enabled_other: '{{count}} private apps enabled',
|
||||
})
|
||||
.build(),
|
||||
});
|
||||
|
||||
expect(screen.getByText('0 private apps enabled')).toBeInTheDocument();
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemin', '0');
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '100');
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemax', '100');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(['explore', 'installed', 'premium', 'requested'] as const)('in %s context', (context) => {
|
||||
it('should work', async () => {
|
||||
render(<EnabledAppsCount enabled={1} limit={2} context={context} />, {
|
||||
wrapper: mockAppRoot()
|
||||
.withTranslations('en', 'core', {
|
||||
Apps_Count_Enabled_one: '{{count}} app enabled',
|
||||
Apps_Count_Enabled_other: '{{count}} apps enabled',
|
||||
})
|
||||
.build(),
|
||||
});
|
||||
|
||||
expect(screen.getByText('1 app enabled')).toBeInTheDocument();
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemin', '0');
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50');
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemax', '100');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { GenericResourceUsage } from '../../../components/GenericResourceUsage';
|
||||
|
||||
const EnabledAppsCount = ({
|
||||
limit,
|
||||
enabled,
|
||||
context,
|
||||
tooltip,
|
||||
}: {
|
||||
limit: number;
|
||||
enabled: number;
|
||||
context: 'private' | 'explore' | 'installed' | 'premium' | 'requested';
|
||||
tooltip?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const variant = useMemo(() => {
|
||||
if (enabled + 1 === limit) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
if (limit === 0 || enabled >= limit) {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
return 'success';
|
||||
}, [enabled, limit]);
|
||||
|
||||
const percentage = limit === 0 ? 100 : Math.round((enabled * 100) / limit);
|
||||
|
||||
return (
|
||||
<GenericResourceUsage
|
||||
title={context === 'private' ? t('Private_Apps_Count_Enabled', { count: enabled }) : t('Apps_Count_Enabled', { count: enabled })}
|
||||
value={enabled}
|
||||
max={limit}
|
||||
percentage={percentage}
|
||||
threshold={80}
|
||||
variant={variant}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnabledAppsCount;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Button, ButtonGroup, Margins } from '@rocket.chat/fuselage';
|
||||
import { PageHeader } from '@rocket.chat/ui-client';
|
||||
import { usePermission, useRoute, useRouteParameter, useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { GenericResourceUsageSkeleton } from '../../../components/GenericResourceUsage';
|
||||
import UpgradeButton from '../../admin/subscription/components/UpgradeButton';
|
||||
import UnlimitedAppsUpsellModal from '../UnlimitedAppsUpsellModal';
|
||||
import EnabledAppsCount from './EnabledAppsCount';
|
||||
import { useAppsCountQuery } from '../hooks/useAppsCountQuery';
|
||||
import { usePrivateAppsEnabled } from '../hooks/usePrivateAppsEnabled';
|
||||
import PrivateAppInstallModal from './PrivateAppInstallModal/PrivateAppInstallModal';
|
||||
import UpdateRocketChatButton from './UpdateRocketChatButton';
|
||||
|
||||
export type MarketplaceHeaderProps = { title: string; unsupportedVersion: boolean };
|
||||
|
||||
const MarketplaceHeader = ({ title, unsupportedVersion }: MarketplaceHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = usePermission('manage-apps');
|
||||
const context = (useRouteParameter('context') || 'explore') as 'private' | 'explore' | 'installed' | 'premium' | 'requested';
|
||||
const route = useRoute('marketplace');
|
||||
const setModal = useSetModal();
|
||||
const result = useAppsCountQuery(context);
|
||||
|
||||
const privateAppsEnabled = usePrivateAppsEnabled();
|
||||
|
||||
const handleProceed = (): void => {
|
||||
setModal(null);
|
||||
route.push({ context, page: 'install' });
|
||||
};
|
||||
|
||||
const handleClickPrivate = () => {
|
||||
if (!privateAppsEnabled) {
|
||||
setModal(<PrivateAppInstallModal onClose={() => setModal(null)} onProceed={handleProceed} />);
|
||||
return;
|
||||
}
|
||||
|
||||
route.push({ context, page: 'install' });
|
||||
};
|
||||
|
||||
if (result.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageHeader title={title}>
|
||||
{result.isLoading && <GenericResourceUsageSkeleton marginInline={16} />}
|
||||
|
||||
{!unsupportedVersion && result.isSuccess && !result.data.hasUnlimitedApps && (
|
||||
<Margins inline={16}>
|
||||
<EnabledAppsCount
|
||||
{...result.data}
|
||||
tooltip={context === 'private' && !privateAppsEnabled ? t('Private_apps_premium_message') : undefined}
|
||||
context={context}
|
||||
/>
|
||||
</Margins>
|
||||
)}
|
||||
|
||||
<ButtonGroup wrap align='end'>
|
||||
{!unsupportedVersion && isAdmin && result.isSuccess && !result.data.hasUnlimitedApps && context !== 'private' && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setModal(<UnlimitedAppsUpsellModal onClose={() => setModal(null)} />);
|
||||
}}
|
||||
>
|
||||
{t('Enable_unlimited_apps')}
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && context === 'private' && <Button onClick={handleClickPrivate}>{t('Upload_private_app')}</Button>}
|
||||
|
||||
{isAdmin && result.isSuccess && !privateAppsEnabled && context === 'private' && (
|
||||
<UpgradeButton primary target='private-apps-header' action='upgrade'>
|
||||
{t('Upgrade')}
|
||||
</UpgradeButton>
|
||||
)}
|
||||
{unsupportedVersion && isAdmin && context !== 'private' && <UpdateRocketChatButton />}
|
||||
</ButtonGroup>
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplaceHeader;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Badge, Skeleton } from '@rocket.chat/fuselage';
|
||||
|
||||
import { useAppRequestStats } from '../hooks/useAppRequestStats';
|
||||
|
||||
const MarketplaceRequestBadge = () => {
|
||||
const requestStatsResult = useAppRequestStats();
|
||||
|
||||
if (requestStatsResult.isPending)
|
||||
return requestStatsResult.fetchStatus !== 'idle' ? <Skeleton variant='circle' height='x16' width='x16' /> : null;
|
||||
|
||||
if (requestStatsResult.isError) return null;
|
||||
|
||||
if (!requestStatsResult.data.totalUnseen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Badge variant='primary'>{requestStatsResult.data.totalUnseen}</Badge>;
|
||||
};
|
||||
|
||||
export default MarketplaceRequestBadge;
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalFooterAnnotation,
|
||||
ModalFooterControllers,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useExternalLink } from '../../../../hooks/useExternalLink';
|
||||
import { useCheckoutUrl } from '../../../admin/subscription/hooks/useCheckoutUrl';
|
||||
import { PRICING_LINK } from '../../../admin/subscription/utils/links';
|
||||
|
||||
export type PrivateAppInstallModalProps = {
|
||||
onClose: () => void;
|
||||
onProceed: () => void;
|
||||
};
|
||||
|
||||
const PrivateAppInstallModal = ({ onClose, onProceed }: PrivateAppInstallModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const openExternalLink = useExternalLink();
|
||||
const manageSubscriptionUrl = useCheckoutUrl()({ target: 'private-apps-page', action: 'upgrade' });
|
||||
|
||||
const goToManageSubscriptionPage = (): void => {
|
||||
openExternalLink(manageSubscriptionUrl);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('Private_app_install_modal_title')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose aria-label={t('Close')} onClick={onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Box marginBlockEnd={28}>{t('Private_app_install_modal_content')}</Box>
|
||||
{t('Upgrade_subscription_to_enable_private_apps')}
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter justifyContent='space-between'>
|
||||
<ModalFooterAnnotation>
|
||||
<a target='_blank' rel='noopener noreferrer' href={PRICING_LINK}>
|
||||
{t('Compare_plans')}
|
||||
</a>
|
||||
</ModalFooterAnnotation>
|
||||
<ModalFooterControllers>
|
||||
<Button onClick={onProceed}>{t('Upload_anyway')}</Button>
|
||||
<Button onClick={goToManageSubscriptionPage} primary>
|
||||
{t('Upgrade')}
|
||||
</Button>
|
||||
</ModalFooterControllers>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivateAppInstallModal;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Box, Option, RadioButton, Tile } from '@rocket.chat/fuselage';
|
||||
|
||||
import type { RadioDropDownProps } from '../definitions/RadioDropDownDefinitions';
|
||||
|
||||
const RadioButtonList = ({ group, onSelected }: RadioDropDownProps) => (
|
||||
<Tile overflow='auto' paddingBlock={12} paddingInline={0} elevation='2' width='full' backgroundColor='light' borderRadius='x2'>
|
||||
{group.label && (
|
||||
<Box paddingInline={16} paddingBlockStart={8} paddingBlockEnd={4} fontScale='micro' textTransform='uppercase' color='default'>
|
||||
{group.label}
|
||||
</Box>
|
||||
)}
|
||||
{group.items.map((item) => (
|
||||
<Option key={item.id} label={item.label} onClick={(): void => onSelected(item)}>
|
||||
<RadioButton checked={item.checked} onChange={(): void => onSelected(item)} />
|
||||
</Option>
|
||||
))}
|
||||
</Tile>
|
||||
);
|
||||
|
||||
export default RadioButtonList;
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Button } from '@rocket.chat/fuselage';
|
||||
import { Box, Icon } from '@rocket.chat/fuselage';
|
||||
import type { ComponentProps, MouseEvent } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { RadioDropDownGroup } from '../../definitions/RadioDropDownDefinitions';
|
||||
|
||||
type RadioDropdownAnchorProps = {
|
||||
onClick: (event: MouseEvent<HTMLElement>) => void;
|
||||
group: RadioDropDownGroup;
|
||||
} & Omit<ComponentProps<typeof Button>, 'onClick'>;
|
||||
|
||||
const RadioDownAnchor = forwardRef<HTMLElement, RadioDropdownAnchorProps>(function SortDropDownAnchor({ onClick, group, ...props }, ref) {
|
||||
const selected = group?.items.find((item) => item.checked)?.label;
|
||||
|
||||
return (
|
||||
<Box
|
||||
is='button'
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
alignItems='center'
|
||||
backgroundColor='light'
|
||||
borderColor='light'
|
||||
borderRadius='x4'
|
||||
borderWidth='x1'
|
||||
color='secondary-info'
|
||||
display='flex'
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
fontScale='p2'
|
||||
height='x40'
|
||||
justifyContent='space-between'
|
||||
minWidth='x144'
|
||||
paddingInlineEnd={10}
|
||||
paddingInlineStart={14}
|
||||
rcx-input-box
|
||||
{...props}
|
||||
>
|
||||
{selected}
|
||||
<Icon name='chevron-down' size='x20' />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default RadioDownAnchor;
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRadioToggle } from '../../hooks/useRadioToggle';
|
||||
import RadioButtonList from '../RadioButtonList';
|
||||
import RadioDropDown from './RadioDropDown';
|
||||
|
||||
export default {
|
||||
component: RadioDropDown,
|
||||
};
|
||||
|
||||
const testGroup = {
|
||||
label: 'Sort by',
|
||||
items: [
|
||||
{ id: 'az', label: 'A-Z', checked: true },
|
||||
{ id: 'za', label: 'Z-A', checked: false },
|
||||
{ id: 'MRU', label: 'Most recent updated', checked: false },
|
||||
{ id: 'LRU', label: 'Least recent updated', checked: false },
|
||||
],
|
||||
};
|
||||
|
||||
export const List: StoryFn = () => {
|
||||
const [data, setData] = useState(() => testGroup);
|
||||
|
||||
const onSelected = useRadioToggle(setData);
|
||||
|
||||
return <RadioButtonList group={data} onSelected={onSelected} />;
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
const [data, setData] = useState(() => testGroup);
|
||||
|
||||
const onSelected = useRadioToggle(setData);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RadioDropDown group={data} onSelected={onSelected} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Button } from '@rocket.chat/fuselage';
|
||||
import { useToggle } from '@rocket.chat/fuselage-hooks';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import RadioDropDownAnchor from './RadioDownAnchor';
|
||||
import type { RadioDropDownProps } from '../../definitions/RadioDropDownDefinitions';
|
||||
import { isValidReference } from '../../helpers/isValidReference';
|
||||
import { onMouseEventPreventSideEffects } from '../../helpers/onMouseEventPreventSideEffects';
|
||||
import DropDownListWrapper from '../DropDownListWrapper';
|
||||
import RadioButtonList from '../RadioButtonList';
|
||||
|
||||
const RadioDropDown = ({ group, onSelected, ...props }: RadioDropDownProps & ComponentProps<typeof Button>) => {
|
||||
const reference = useRef<HTMLElement>(null);
|
||||
const [collapsed, toggleCollapsed] = useToggle(false);
|
||||
|
||||
const onClose = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (isValidReference(reference, e as { target: Node | null })) {
|
||||
toggleCollapsed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
onMouseEventPreventSideEffects(e);
|
||||
|
||||
return false;
|
||||
},
|
||||
[toggleCollapsed],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RadioDropDownAnchor ref={reference} group={group} onClick={() => toggleCollapsed()} {...props} />
|
||||
{collapsed && (
|
||||
<DropDownListWrapper ref={reference} onClose={onClose}>
|
||||
<RadioButtonList group={group} onSelected={onSelected} />
|
||||
</DropDownListWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioDropDown;
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { AppScreenshot } from '@rocket.chat/core-typings';
|
||||
import { Box, IconButton } from '@rocket.chat/fuselage';
|
||||
|
||||
export type ScreenshotCarouselProps = {
|
||||
AppScreenshots: Array<AppScreenshot>;
|
||||
setViewCarousel: (state: boolean) => void;
|
||||
handleNextSlide: () => void;
|
||||
handlePrevSlide: () => void;
|
||||
isFirstSlide: boolean;
|
||||
isLastSlide: boolean;
|
||||
currentSlideIndex: number;
|
||||
};
|
||||
|
||||
const ScreenshotCarousel = ({
|
||||
AppScreenshots,
|
||||
setViewCarousel,
|
||||
handleNextSlide,
|
||||
handlePrevSlide,
|
||||
isFirstSlide,
|
||||
isLastSlide,
|
||||
currentSlideIndex,
|
||||
}: ScreenshotCarouselProps) => {
|
||||
const handleScreenshotRender = () =>
|
||||
AppScreenshots.map((currentScreenshot, index) => {
|
||||
const isCurrentImageOnScreen = index === currentSlideIndex;
|
||||
const screenshotWrapperStyle = isCurrentImageOnScreen
|
||||
? {
|
||||
opacity: '1',
|
||||
transitionDuration: '1s',
|
||||
transform: 'scale(1.08)',
|
||||
}
|
||||
: {
|
||||
opacity: '0',
|
||||
transitionDuration: '1s ease',
|
||||
};
|
||||
|
||||
return (
|
||||
<Box style={screenshotWrapperStyle} key={currentScreenshot.id}>
|
||||
{isCurrentImageOnScreen && (
|
||||
<Box
|
||||
is='img'
|
||||
src={currentScreenshot.accessUrl}
|
||||
alt='Carousel image'
|
||||
maxWidth='x1200'
|
||||
maxHeight='x600'
|
||||
width='100%'
|
||||
height='100%'
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position='fixed' width='100%' height='100vh' backgroundColor='font-pure-black' opacity='0.7' marginBlock='-0.75px' zIndex='2' />
|
||||
|
||||
{!isFirstSlide && (
|
||||
<IconButton
|
||||
secondary
|
||||
icon='chevron-right'
|
||||
onClick={handlePrevSlide}
|
||||
style={{ top: '50%', left: '10px', cursor: 'pointer', transform: 'translateY(-50%)' }}
|
||||
position='absolute'
|
||||
zIndex={3}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLastSlide && (
|
||||
<IconButton
|
||||
secondary
|
||||
icon='chevron-left'
|
||||
onClick={handleNextSlide}
|
||||
style={{ top: '50%', right: '10px', cursor: 'pointer', transform: 'translateY(-50%)' }}
|
||||
position='absolute'
|
||||
zIndex={3}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box
|
||||
onClick={(): void => setViewCarousel(false)}
|
||||
position='fixed'
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='flex'
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
zIndex='2'
|
||||
marginInline={38}
|
||||
>
|
||||
{handleScreenshotRender()}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScreenshotCarousel;
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { AppScreenshot } from '@rocket.chat/core-typings';
|
||||
import { css } from '@rocket.chat/css-in-js';
|
||||
import { Box, Icon } from '@rocket.chat/fuselage';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import ScreenshotCarousel from './ScreenshotCarousel';
|
||||
|
||||
export type ScreenshotCarouselAnchorProps = {
|
||||
screenshots: AppScreenshot[];
|
||||
};
|
||||
|
||||
type voidFunction = () => void;
|
||||
|
||||
const ScreenshotCarouselAnchor = ({ screenshots }: ScreenshotCarouselAnchorProps) => {
|
||||
const [viewCarousel, setViewCarousel] = useState(false);
|
||||
|
||||
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
|
||||
|
||||
const [currentPreviewIndex, setCurrentPreviewIndex] = useState(0);
|
||||
|
||||
const { length } = screenshots;
|
||||
|
||||
const isFirstSlide = currentSlideIndex === 0;
|
||||
const isLastSlide = currentSlideIndex === length - 1;
|
||||
|
||||
const isCarouselVisible = viewCarousel && screenshots?.length;
|
||||
|
||||
const handleNextSlide = (): void => {
|
||||
setCurrentSlideIndex(currentSlideIndex + 1);
|
||||
};
|
||||
|
||||
const handlePrevSlide = (): void => {
|
||||
setCurrentSlideIndex(currentSlideIndex - 1);
|
||||
};
|
||||
|
||||
const handleKeyboardKey = useCallback(
|
||||
(onKeyDownEvent: KeyboardEvent): void => {
|
||||
const keysObject: Record<string, voidFunction> = {
|
||||
ArrowLeft: () => setCurrentSlideIndex((prevSlideIndex) => (prevSlideIndex !== 0 ? prevSlideIndex - 1 : 0)),
|
||||
ArrowRight: () => setCurrentSlideIndex((prevSlideIndex) => (prevSlideIndex !== length - 1 ? prevSlideIndex + 1 : length - 1)),
|
||||
Escape: () => setViewCarousel(false),
|
||||
};
|
||||
|
||||
keysObject[onKeyDownEvent.key]();
|
||||
},
|
||||
[length],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentPreviewIndex((prevIndex) => {
|
||||
if (prevIndex === length - 1) return 0;
|
||||
|
||||
return prevIndex + 1;
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
document.addEventListener('keydown', handleKeyboardKey);
|
||||
|
||||
return (): void => {
|
||||
clearInterval(intervalId);
|
||||
document.removeEventListener('keydown', handleKeyboardKey);
|
||||
};
|
||||
}, [handleKeyboardKey, length]);
|
||||
|
||||
const carouselPortal = createPortal(
|
||||
<ScreenshotCarousel
|
||||
AppScreenshots={screenshots}
|
||||
setViewCarousel={setViewCarousel}
|
||||
handleNextSlide={handleNextSlide}
|
||||
handlePrevSlide={handlePrevSlide}
|
||||
isFirstSlide={isFirstSlide}
|
||||
isLastSlide={isLastSlide}
|
||||
currentSlideIndex={currentSlideIndex}
|
||||
/>,
|
||||
document.body,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
onClick={(): void => setViewCarousel(true)}
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
maxWidth='x640'
|
||||
width='100%'
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Box
|
||||
is='img'
|
||||
src={screenshots[currentPreviewIndex]?.accessUrl}
|
||||
alt='App preview image'
|
||||
className={[
|
||||
css`
|
||||
transition: filter 0.2s ease;
|
||||
&:hover {
|
||||
filter: brightness(90%);
|
||||
}
|
||||
`,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Box display='flex' flexDirection='row' backgroundColor='tint' paddingInline={16} paddingBlock={10} alignItems='center'>
|
||||
<Icon name='image' size='x24' marginInlineEnd={8} />{' '}
|
||||
<Box is='span' fontScale='p2m' color='default'>
|
||||
{currentPreviewIndex + 1} of {screenshots.length}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{isCarouselVisible && carouselPortal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScreenshotCarouselAnchor;
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import UninstallGrandfatheredAppModal from './UninstallGrandfatheredAppModal';
|
||||
|
||||
export default {
|
||||
component: UninstallGrandfatheredAppModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof UninstallGrandfatheredAppModal>;
|
||||
|
||||
export const Default = {
|
||||
name: 'UninstallGrandfatheredAppModal',
|
||||
|
||||
args: {
|
||||
appName: 'Example-App-Name',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalFooterAnnotation,
|
||||
ModalFooterControllers,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MarkdownText from '../../../../components/MarkdownText';
|
||||
import { links } from '../../../../lib/links';
|
||||
import type { MarketplaceRouteContext } from '../../hooks/useAppsCountQuery';
|
||||
import { usePrivateAppsEnabled } from '../../hooks/usePrivateAppsEnabled';
|
||||
|
||||
export type UninstallGrandfatheredAppModalProps = {
|
||||
context: MarketplaceRouteContext;
|
||||
limit: number;
|
||||
appName: string;
|
||||
handleUninstall: () => void;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const UninstallGrandfatheredAppModal = ({ context, limit, appName, handleUninstall, handleClose }: UninstallGrandfatheredAppModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const privateAppsEnabled = usePrivateAppsEnabled();
|
||||
|
||||
const modalContent =
|
||||
context === 'private' && !privateAppsEnabled
|
||||
? t('App_will_lose_grandfathered_status_private')
|
||||
: t('App_will_lose_grandfathered_status', { limit });
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('Uninstall_grandfathered_app', { appName })}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={handleClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<MarkdownText content={modalContent} />
|
||||
</ModalContent>
|
||||
<ModalFooter justifyContent='space-between'>
|
||||
<ModalFooterAnnotation>
|
||||
<a target='_blank' rel='noopener noreferrer' href={links.go.appsDocumentation}>
|
||||
{t('Learn_more')}
|
||||
</a>
|
||||
</ModalFooterAnnotation>
|
||||
<ModalFooterControllers>
|
||||
<Button onClick={handleClose}>{t('Cancel')}</Button>
|
||||
<Button danger onClick={handleUninstall}>
|
||||
{t('Uninstall')}
|
||||
</Button>
|
||||
</ModalFooterControllers>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UninstallGrandfatheredAppModal;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Button } from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { links } from '../../../lib/links';
|
||||
|
||||
const UpdateRocketChatButton = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Button icon='new-window' primary is='a' href={links.updatingRocketChat} external>
|
||||
{t('Update')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateRocketChatButton;
|
||||
Reference in New Issue
Block a user