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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import type { IWorkspaceInfo } from '@rocket.chat/core-typings';
|
||||
import { Box, Card, CardBody, CardCol, CardControls, CardHeader, CardTitle, Icon } from '@rocket.chat/fuselage';
|
||||
import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
|
||||
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
|
||||
import { ExternalLink, useLicense, useLicenseName } from '@rocket.chat/ui-client';
|
||||
import type { LocationPathname } from '@rocket.chat/ui-contexts';
|
||||
import { useSetModal, useMediaUrl } from '@rocket.chat/ui-contexts';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import VersionCardActionButton from './components/VersionCardActionButton';
|
||||
import type { VersionActionItem } from './components/VersionCardActionItem';
|
||||
import VersionCardActionItem from './components/VersionCardActionItem';
|
||||
import { VersionCardSkeleton } from './components/VersionCardSkeleton';
|
||||
import { VersionTag } from './components/VersionTag';
|
||||
import { getVersionStatus } from './getVersionStatus';
|
||||
import RegisterWorkspaceModal from './modals/RegisterWorkspaceModal';
|
||||
import { useFormatDate } from '../../../../hooks/useFormatDate';
|
||||
import { useRegistrationStatus } from '../../../../hooks/useRegistrationStatus';
|
||||
import { links } from '../../../../lib/links';
|
||||
import { isOverLicenseLimits } from '../../../../lib/utils/isOverLicenseLimits';
|
||||
|
||||
const SUPPORT_EXTERNAL_LINK = links.go.versionSupport;
|
||||
const RELEASES_EXTERNAL_LINK = links.go.updateProduct;
|
||||
|
||||
export type VersionCardProps = {
|
||||
serverInfo: IWorkspaceInfo;
|
||||
};
|
||||
|
||||
const VersionCard = ({ serverInfo }: VersionCardProps) => {
|
||||
const breakpoints = useBreakpoints();
|
||||
const isExtraLargeOrBigger = breakpoints.includes('xl');
|
||||
|
||||
const getUrl = useMediaUrl();
|
||||
const cardBackground = {
|
||||
backgroundImage: `url(${getUrl('images/globe.png')})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: isExtraLargeOrBigger ? 'right 20px center' : 'left 450px center',
|
||||
backgroundSize: 'auto',
|
||||
};
|
||||
|
||||
const setModal = useSetModal();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const { data: licenseData, isPending, refetch: refetchLicense } = useLicense({ loadValues: true });
|
||||
const { isRegistered, canViewRegistrationStatus } = useRegistrationStatus();
|
||||
|
||||
const { license, limits } = licenseData || {};
|
||||
const isAirgapped = license?.information?.offline;
|
||||
const licenseName = useLicenseName();
|
||||
|
||||
const serverVersion = serverInfo.version;
|
||||
|
||||
const { versionStatus, versions } = useMemo(() => {
|
||||
const supportedVersions = serverInfo?.supportedVersions?.signed ? decodeBase64(serverInfo?.supportedVersions?.signed) : undefined;
|
||||
|
||||
if (!supportedVersions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const versionStatus = getVersionStatus(serverVersion, supportedVersions?.versions);
|
||||
|
||||
return {
|
||||
versionStatus,
|
||||
versions: supportedVersions?.versions,
|
||||
};
|
||||
}, [serverInfo?.supportedVersions?.signed, serverVersion]);
|
||||
|
||||
const isOverLimits = limits && isOverLicenseLimits(limits);
|
||||
|
||||
const actionButton:
|
||||
| undefined
|
||||
| {
|
||||
path: LocationPathname;
|
||||
label: ReactNode;
|
||||
}
|
||||
| {
|
||||
action: () => void;
|
||||
label: ReactNode;
|
||||
} = useMemo(() => {
|
||||
if (canViewRegistrationStatus && !isRegistered) {
|
||||
return {
|
||||
action: () => {
|
||||
const handleModalClose = (): void => {
|
||||
setModal(null);
|
||||
refetchLicense();
|
||||
};
|
||||
setModal(<RegisterWorkspaceModal onClose={handleModalClose} onStatusChange={refetchLicense} />);
|
||||
},
|
||||
label: t('RegisterWorkspace_Button'),
|
||||
};
|
||||
}
|
||||
|
||||
if (versionStatus?.label === 'outdated') {
|
||||
return {
|
||||
action: () => {
|
||||
window.open(RELEASES_EXTERNAL_LINK, '_blank');
|
||||
},
|
||||
label: t('Update_version'),
|
||||
};
|
||||
}
|
||||
|
||||
if (isOverLimits) {
|
||||
return { path: '/admin/subscription', label: t('Manage_subscription') };
|
||||
}
|
||||
}, [canViewRegistrationStatus, isRegistered, versionStatus, isOverLimits, t, setModal, refetchLicense]);
|
||||
|
||||
const actionItems = useMemo(() => {
|
||||
return (
|
||||
[
|
||||
isOverLimits
|
||||
? {
|
||||
danger: true,
|
||||
icon: 'warning',
|
||||
label: t('Plan_limits_reached'),
|
||||
}
|
||||
: {
|
||||
icon: 'check',
|
||||
label: t('Operating_withing_plan_limits'),
|
||||
},
|
||||
(isAirgapped || !versions) && {
|
||||
icon: 'warning',
|
||||
label: (
|
||||
<Trans i18nKey='Check_support_availability'>
|
||||
Check
|
||||
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>support</ExternalLink>
|
||||
availability
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
|
||||
versionStatus?.label !== 'outdated' &&
|
||||
versionStatus?.expiration && {
|
||||
icon: 'check',
|
||||
label: (
|
||||
<Trans i18nKey='Version_supported_until' values={{ date: formatDate(versionStatus?.expiration) }}>
|
||||
Version
|
||||
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>supported</ExternalLink>
|
||||
until {formatDate(versionStatus?.expiration)}
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
versionStatus?.label === 'outdated' && {
|
||||
danger: true,
|
||||
icon: 'warning',
|
||||
label: (
|
||||
<Trans i18nKey='Version_not_supported'>
|
||||
Version
|
||||
<ExternalLink to={SUPPORT_EXTERNAL_LINK}>not supported</ExternalLink>
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
canViewRegistrationStatus &&
|
||||
(isRegistered
|
||||
? {
|
||||
icon: 'check',
|
||||
label: t('Workspace_registered'),
|
||||
}
|
||||
: {
|
||||
danger: true,
|
||||
icon: 'warning',
|
||||
label: t('Workspace_not_registered'),
|
||||
}),
|
||||
].filter(Boolean) as VersionActionItem[]
|
||||
).sort((a) => (a.danger ? -1 : 1));
|
||||
}, [
|
||||
isOverLimits,
|
||||
t,
|
||||
isAirgapped,
|
||||
versions,
|
||||
versionStatus?.label,
|
||||
versionStatus?.expiration,
|
||||
formatDate,
|
||||
canViewRegistrationStatus,
|
||||
isRegistered,
|
||||
]);
|
||||
|
||||
if (isPending && !licenseData) {
|
||||
return (
|
||||
<Card style={{ ...cardBackground }}>
|
||||
<VersionCardSkeleton />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={{ ...cardBackground }}>
|
||||
<CardCol>
|
||||
<CardHeader>
|
||||
<CardTitle variant='h3'>{t('Version_version', { version: serverVersion })}</CardTitle>
|
||||
{!isAirgapped && versions && <VersionTag versionStatus={versionStatus?.label} title={versionStatus.version} />}
|
||||
</CardHeader>
|
||||
|
||||
<Box color='secondary-info'>
|
||||
<Icon name='rocketchat' size={16} /> {licenseName.data}
|
||||
</Box>
|
||||
</CardCol>
|
||||
|
||||
<CardBody flexDirection='column'>
|
||||
{actionItems.length > 0 && actionItems.map((item, index) => <VersionCardActionItem key={index} {...item} />)}
|
||||
</CardBody>
|
||||
|
||||
{actionButton && (
|
||||
<CardControls>
|
||||
<VersionCardActionButton {...actionButton} />
|
||||
</CardControls>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionCard;
|
||||
|
||||
const decodeBase64 = (b64: string): SupportedVersions | undefined => {
|
||||
const [, bodyEncoded] = b64.split('.');
|
||||
if (!bodyEncoded) {
|
||||
return;
|
||||
}
|
||||
|
||||
return JSON.parse(atob(bodyEncoded));
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Button } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import type { LocationPathname } from '@rocket.chat/ui-contexts';
|
||||
import { useRouter } from '@rocket.chat/ui-contexts';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export type VersionCardActionButtonProps =
|
||||
| {
|
||||
path: LocationPathname;
|
||||
label: ReactNode;
|
||||
}
|
||||
| {
|
||||
action: () => void;
|
||||
label: ReactNode;
|
||||
};
|
||||
|
||||
const VersionCardActionButton = (item: VersionCardActionButtonProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleActionButton = useStableCallback(() => {
|
||||
if ('action' in item) {
|
||||
return item.action();
|
||||
}
|
||||
|
||||
router.navigate(item.path);
|
||||
});
|
||||
|
||||
return (
|
||||
<Button primary onClick={() => handleActionButton()}>
|
||||
{item.label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(VersionCardActionButton);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Box, FramedIcon } from '@rocket.chat/fuselage';
|
||||
import type { Keys } from '@rocket.chat/icons';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type VersionActionItem = {
|
||||
danger?: boolean;
|
||||
icon: Keys;
|
||||
label: ReactNode;
|
||||
};
|
||||
|
||||
export type VersionCardActionItemProps = VersionActionItem;
|
||||
|
||||
const VersionCardActionItem = ({ icon, label, danger }: VersionCardActionItemProps) => {
|
||||
return (
|
||||
<Box display='flex' alignItems='center' color={danger ? 'danger' : 'secondary-info'} fontScale='p2m'>
|
||||
<FramedIcon danger={danger} icon={icon} />
|
||||
<Box marginInlineStart={12}>{label}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionCardActionItem;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Box, Skeleton } from '@rocket.chat/fuselage';
|
||||
|
||||
export const VersionCardSkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
<Skeleton height={28} width={160} />
|
||||
<Skeleton height={28} width={150} />
|
||||
<Box display='flex' width={160} height={28} alignItems='center'>
|
||||
<Skeleton height={38} width={28} marginInlineEnd={12} />
|
||||
<Skeleton height={20} width={120} />
|
||||
</Box>
|
||||
<Box display='flex' width={180} height={28} alignItems='center'>
|
||||
<Skeleton height={38} width={28} marginInlineEnd={12} />
|
||||
<Skeleton height={20} width={140} />
|
||||
</Box>
|
||||
<Box display='flex' width={160} height={28} alignItems='center'>
|
||||
<Skeleton height={38} width={28} marginInlineEnd={12} />
|
||||
<Skeleton height={20} width={120} />
|
||||
</Box>
|
||||
<Skeleton height={50} width={160} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Tag } from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type VersionStatus = 'outdated' | 'latest' | 'available_version' | undefined;
|
||||
|
||||
export type VersionTagProps = {
|
||||
versionStatus: VersionStatus;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const VersionTag = ({ versionStatus, title }: VersionTagProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (versionStatus === 'outdated') {
|
||||
return (
|
||||
<Tag title={title} variant='danger'>
|
||||
{t('Outdated')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
if (versionStatus === 'latest') {
|
||||
return (
|
||||
<Tag title={title} variant='primary'>
|
||||
{t('Latest')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag title={title} variant='secondary'>
|
||||
{t('New_version_available')}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { getVersionStatus } from './getVersionStatus';
|
||||
|
||||
describe('if the server version from server and the highest version from cloud are the same', () => {
|
||||
describe('the expiration date is in the future', () => {
|
||||
it('should return as latest version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('latest');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the expiration date is in the past', () => {
|
||||
it('should return as outdated version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date('2020-01-01'),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('outdated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the server version is not in the list of supported versions', () => {
|
||||
it('should return as outdated version', () => {
|
||||
const status = getVersionStatus('2.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('outdated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the server version is in the list of supported versions but is not the highest', () => {
|
||||
describe('the expiration date is in the future', () => {
|
||||
it('should return as available version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
expect(status.label).toBe('available_version');
|
||||
});
|
||||
});
|
||||
describe('the expiration date is in the past', () => {
|
||||
it('should return as outdated version', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date('2020-01-01'),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
expect(status.label).toBe('outdated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the server version is not in the list of supported versions but is the highest', () => {
|
||||
it('should return as latest version', () => {
|
||||
const status = getVersionStatus('4.0.0', [
|
||||
{
|
||||
version: '2.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('outdated');
|
||||
expect(status.version).toBe('4.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prerelease version', () => {
|
||||
it('should not suggest available version the highest version is a prerelease from a different tag', () => {
|
||||
const status = getVersionStatus('3.0.0', [
|
||||
{
|
||||
version: '3.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0-develop',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('latest');
|
||||
expect(status.version).toBe('3.0.0');
|
||||
});
|
||||
|
||||
it('should suggest available version the highest version is a prerelease from the same tag', () => {
|
||||
const status = getVersionStatus('6.5.0-rc.14', [
|
||||
{
|
||||
version: '6.5.0-rc.14',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '6.5.0-rc.15',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '4.0.0-develop',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('available_version');
|
||||
expect(status.version).toBe('6.5.0-rc.15');
|
||||
});
|
||||
|
||||
it('should suggest available version the highest version even if the current one is a prerelease', () => {
|
||||
const status = getVersionStatus('6.5.0-rc.14', [
|
||||
{
|
||||
version: '6.5.0-rc.14',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '6.5.0-rc.15',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
{
|
||||
version: '7.0.0',
|
||||
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
security: false,
|
||||
infoUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(status.label).toBe('available_version');
|
||||
expect(status.version).toBe('7.0.0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
|
||||
import semver from 'semver';
|
||||
|
||||
import type { VersionStatus } from './components/VersionTag';
|
||||
|
||||
export const getVersionStatus = (
|
||||
serverVersion: string,
|
||||
versions: SupportedVersions['versions'],
|
||||
): { label: VersionStatus; expiration: Date | undefined; version: string } => {
|
||||
const highestVersion = versions
|
||||
.sort((prev, current) => semver.compare(current.version, prev.version))
|
||||
.filter((v) => {
|
||||
const [prerelease] = semver.prerelease(v.version) || [];
|
||||
const [currentPrerelease] = semver.prerelease(serverVersion) || [];
|
||||
|
||||
return !prerelease || prerelease === currentPrerelease;
|
||||
})[0];
|
||||
|
||||
const currentVersionData =
|
||||
versions.find((v) => v.version === serverVersion) ||
|
||||
versions.find((v) =>
|
||||
semver.satisfies(String(semver.coerce(v.version)), `=${semver.major(serverVersion)}.${semver.minor(serverVersion)}.x`),
|
||||
);
|
||||
|
||||
const currentVersionIsExpired = currentVersionData?.expiration && new Date(currentVersionData.expiration) < new Date();
|
||||
|
||||
const isSupported = !currentVersionIsExpired && !!currentVersionData;
|
||||
|
||||
return {
|
||||
label: 'outdated',
|
||||
version: serverVersion,
|
||||
...(isSupported && semver.gte(currentVersionData?.version, highestVersion.version) && { label: 'latest' }),
|
||||
...(isSupported &&
|
||||
semver.gt(highestVersion.version, currentVersionData?.version) && { label: 'available_version', version: highestVersion.version }),
|
||||
expiration: currentVersionData?.expiration,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type featureBulletsType = {
|
||||
key: number;
|
||||
title: string;
|
||||
description: string;
|
||||
disconnect: string;
|
||||
};
|
||||
|
||||
const useFeatureBullets = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const featureBullets: featureBulletsType[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 1,
|
||||
title: t('RegisterWorkspace_Features_MobileNotifications_Title'),
|
||||
description: t('RegisterWorkspace_Features_MobileNotifications_Description'),
|
||||
disconnect: t('RegisterWorkspace_Features_MobileNotifications_Disconnect'),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: t('RegisterWorkspace_Features_Marketplace_Title'),
|
||||
description: t('RegisterWorkspace_Features_Marketplace_Description'),
|
||||
disconnect: t('RegisterWorkspace_Features_Marketplace_Disconnect'),
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: t('RegisterWorkspace_Features_Omnichannel_Title'),
|
||||
description: t('RegisterWorkspace_Features_Omnichannel_Description'),
|
||||
disconnect: t('RegisterWorkspace_Features_Omnichannel_Disconnect'),
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return featureBullets;
|
||||
};
|
||||
|
||||
export default useFeatureBullets;
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { ExternalLink } from '@rocket.chat/ui-client';
|
||||
import { useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RegisterWorkspaceSetupModal from './RegisterWorkspaceSetupModal';
|
||||
import RegisterWorkspaceTokenModal from './RegisterWorkspaceTokenModal';
|
||||
import { links } from '../../../../../lib/links';
|
||||
import useFeatureBullets from '../hooks/useFeatureBullets';
|
||||
|
||||
export type RegisterWorkspaceModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const documentationLink = links.go.registerInfoCollected;
|
||||
|
||||
const RegisterWorkspaceModal = ({ onClose, onStatusChange, ...props }: RegisterWorkspaceModalProps) => {
|
||||
const setModal = useSetModal();
|
||||
const bulletFeatures = useFeatureBullets();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleTokenModal = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<RegisterWorkspaceTokenModal onClose={handleModalClose} onStatusChange={onStatusChange} />);
|
||||
};
|
||||
|
||||
const handleSetupModal = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<RegisterWorkspaceSetupModal onClose={handleModalClose} onStatusChange={onStatusChange} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('RegisterWorkspace_NotRegistered_Title')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box withRichContent>
|
||||
<span>{`${t('RegisterWorkspace_NotRegistered_Subtitle')}:`}</span>
|
||||
<ul>
|
||||
{bulletFeatures.map((features) => (
|
||||
<li key={features.key}>
|
||||
<strong>{features.title}</strong>
|
||||
<Box is='p' marginBlockStart={4}>
|
||||
{features.description}
|
||||
</Box>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Box is='p' fontSize='p2'>
|
||||
{t('RegisterWorkspace_Registered_Benefits')}
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Box is='div' display='flex' justifyContent='space-between' alignItems='center' width='full'>
|
||||
<ExternalLink to={documentationLink}>{t('Learn_more')}</ExternalLink>
|
||||
<ButtonGroup align='end'>
|
||||
<Button onClick={handleTokenModal}>{t('Use_token')}</Button>
|
||||
<Button primary onClick={handleSetupModal}>
|
||||
{t('RegisterWorkspace_Button')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceModal;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { validateEmail } from '@rocket.chat/tools';
|
||||
import { useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import RegisteredWorkspaceModal from '../RegisteredWorkspaceModal';
|
||||
import RegisterWorkspaceSetupStepOneModal from './RegisterWorkspaceSetupStepOneModal';
|
||||
import RegisterWorkspaceSetupStepTwoModal from './RegisterWorkspaceSetupStepTwoModal';
|
||||
|
||||
export type RegisterWorkspaceSetupModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const RegisterWorkspaceSetupModal = ({ onClose }: RegisterWorkspaceSetupModalProps) => {
|
||||
const setModal = useSetModal();
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
const [email, setEmail] = useState('');
|
||||
const [terms, setTerms] = useState(false);
|
||||
const [validInfo, setValidInfo] = useState(false);
|
||||
const [intentData, setIntentData] = useState({
|
||||
device_code: '',
|
||||
interval: 0,
|
||||
user_code: '',
|
||||
});
|
||||
|
||||
// reset validInfo when users go back to step 1
|
||||
useEffect(() => {
|
||||
setValidInfo(false);
|
||||
}, [step]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step === 1) {
|
||||
setValidInfo(validateEmail(email) && terms);
|
||||
}
|
||||
}, [email, terms]);
|
||||
|
||||
const onSuccess = () => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<RegisteredWorkspaceModal onClose={handleModalClose} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{step === 1 ? (
|
||||
<RegisterWorkspaceSetupStepOneModal
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
step={step}
|
||||
setStep={setStep}
|
||||
terms={terms}
|
||||
setTerms={setTerms}
|
||||
onClose={onClose}
|
||||
validInfo={validInfo}
|
||||
setIntentData={setIntentData}
|
||||
/>
|
||||
) : (
|
||||
<RegisterWorkspaceSetupStepTwoModal
|
||||
email={email}
|
||||
step={step}
|
||||
setStep={setStep}
|
||||
onClose={onClose}
|
||||
intentData={intentData}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceSetupModal;
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
Modal,
|
||||
Box,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldRow,
|
||||
TextInput,
|
||||
CheckBox,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTagline,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { ExternalLink } from '@rocket.chat/ui-client';
|
||||
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useId } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { links } from '../../../../../../lib/links';
|
||||
import WorkspaceRegistrationModal from '../RegisterWorkspaceModal';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
terms: boolean;
|
||||
setTerms: (terms: boolean) => void;
|
||||
onClose: () => void;
|
||||
validInfo: boolean;
|
||||
setIntentData: (intentData: any) => void;
|
||||
};
|
||||
|
||||
const RegisterWorkspaceSetupStepOneModal = ({
|
||||
email,
|
||||
setEmail,
|
||||
step,
|
||||
setStep,
|
||||
terms,
|
||||
setTerms,
|
||||
onClose,
|
||||
validInfo,
|
||||
setIntentData,
|
||||
...props
|
||||
}: Props) => {
|
||||
const setModal = useSetModal();
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const createRegistrationIntent = useEndpoint('POST', '/v1/cloud.createRegistrationIntent');
|
||||
|
||||
const handleBack = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<WorkspaceRegistrationModal onClose={handleModalClose} />);
|
||||
};
|
||||
|
||||
const handleRegisterWorkspace = async () => {
|
||||
try {
|
||||
const { intentData } = await createRegistrationIntent({ resend: false, email });
|
||||
setIntentData(intentData);
|
||||
setStep(step + 1);
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
};
|
||||
|
||||
const emailField = useId();
|
||||
const termsField = useId();
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTagline>{t('RegisterWorkspace_Setup_Steps', { step, numberOfSteps: 2 })}</ModalTagline>
|
||||
<ModalTitle>{t('RegisterWorkspace_with_email')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box>
|
||||
<Box is='p' fontSize='p2' withRichContent>
|
||||
{t('RegisterWorkspace_Setup_Subtitle')}
|
||||
</Box>
|
||||
<Field paddingBlockStart={10}>
|
||||
<FieldLabel htmlFor={emailField}>{t('RegisterWorkspace_Setup_Label')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput
|
||||
id={emailField}
|
||||
onChange={(e) => {
|
||||
setEmail((e.target as HTMLInputElement).value);
|
||||
}}
|
||||
/>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Box marginBlock={16} fontSize='c1'>
|
||||
<Box is='p'>
|
||||
<strong>{t('RegisterWorkspace_Setup_Have_Account_Title')}</strong>
|
||||
</Box>
|
||||
<Box is='p'>{t('RegisterWorkspace_Setup_Have_Account_Subtitle')}</Box>
|
||||
<Box is='p' paddingBlockStart={16}>
|
||||
<strong>{t('RegisterWorkspace_Setup_No_Account_Title')}</strong>
|
||||
</Box>
|
||||
<Box is='p'>{t('RegisterWorkspace_Setup_No_Account_Subtitle')}</Box>
|
||||
</Box>
|
||||
<Field>
|
||||
<FieldRow justifyContent='initial'>
|
||||
<FieldLabel display='block' fontScale='c1' htmlFor={termsField}>
|
||||
<Trans i18nKey='RegisterWorkspace_Setup_Terms_Privacy'>
|
||||
I agree with <ExternalLink to={links.terms}>Terms and Conditions</ExternalLink> and{' '}
|
||||
<ExternalLink to={links.privacy}>Privacy Policy</ExternalLink>
|
||||
</Trans>
|
||||
</FieldLabel>
|
||||
<CheckBox id={termsField} checked={terms} onChange={() => setTerms(!terms)} />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ButtonGroup align='end'>
|
||||
<Button onClick={handleBack}>{t('Back')}</Button>
|
||||
<Button primary onClick={handleRegisterWorkspace} disabled={!validInfo}>
|
||||
{t('Next')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceSetupStepOneModal;
|
||||
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Modal,
|
||||
Box,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldRow,
|
||||
TextInput,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTagline,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
onClose: () => void;
|
||||
intentData: {
|
||||
device_code: string;
|
||||
interval: number;
|
||||
user_code: string;
|
||||
};
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
const setIntervalTime = (interval?: number): number => (interval ? interval * 1000 : 0);
|
||||
|
||||
const RegisterWorkspaceSetupStepTwoModal = ({ email, step, setStep, onClose, intentData, onSuccess, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const cloudConfirmationPoll = useEndpoint('GET', '/v1/cloud.confirmationPoll');
|
||||
const createRegistrationIntent = useEndpoint('POST', '/v1/cloud.createRegistrationIntent');
|
||||
|
||||
const handleBackFromConfirmation = (): void => setStep(step - 1);
|
||||
|
||||
const handleResendRegistrationEmail = async () => {
|
||||
try {
|
||||
await createRegistrationIntent({ resend: true, email });
|
||||
dispatchToastMessage({ type: 'success', message: t('Email_sent') });
|
||||
} catch (error: unknown) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
};
|
||||
|
||||
const getConfirmation = useCallback(async () => {
|
||||
try {
|
||||
const { pollData } = await cloudConfirmationPoll({
|
||||
deviceCode: intentData.device_code,
|
||||
});
|
||||
|
||||
if ('successful' in pollData && pollData.successful) {
|
||||
dispatchToastMessage({ type: 'success', message: t('Workspace_registered') });
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [cloudConfirmationPoll, intentData.device_code, dispatchToastMessage, t, onSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
const pollInterval = setInterval(() => getConfirmation(), setIntervalTime(intentData.interval));
|
||||
|
||||
return (): void => clearInterval(pollInterval);
|
||||
}, [getConfirmation, intentData]);
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTagline>{t('RegisterWorkspace_Setup_Steps', { step, numberOfSteps: 2 })}</ModalTagline>
|
||||
<ModalTitle>{t('Awaiting_confirmation')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box fontSize='p2'>
|
||||
<Box>
|
||||
<Trans i18nKey='cloud.RegisterWorkspace_Setup_Email_Confirmation'>
|
||||
<Box is='p'>
|
||||
Email sent to{' '}
|
||||
<Box is='span' fontScale='p2b'>
|
||||
{email}{' '}
|
||||
</Box>
|
||||
with a confirmation link.
|
||||
</Box>
|
||||
</Trans>
|
||||
</Box>
|
||||
<Box is='p'>{t('RegisterWorkspace_Setup_Email_Verification')}</Box>
|
||||
<Field paddingBlockStart={10}>
|
||||
<FieldLabel>{t('Security_code')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput defaultValue={intentData.user_code} disabled />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Box is='div' display='flex' justifyContent='start' fontSize='c1' width='full'>
|
||||
<Trans i18nKey='RegisterWorkspace_Didnt_Receive_Email'>
|
||||
Didn't receive email?{' '}
|
||||
<Box is='a' paddingInline={4} onClick={handleResendRegistrationEmail}>
|
||||
Resend
|
||||
</Box>{' '}
|
||||
or{' '}
|
||||
<Box is='a' paddingInline={4} onClick={handleBackFromConfirmation}>
|
||||
change email
|
||||
</Box>
|
||||
</Trans>
|
||||
</Box>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceSetupStepTwoModal;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './RegisterWorkspaceSetupModal';
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldRow,
|
||||
FieldError,
|
||||
Modal,
|
||||
TextInput,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import WorkspaceRegistrationModal from './RegisterWorkspaceModal';
|
||||
|
||||
export type RegisterWorkspaceTokenModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const RegisterWorkspaceTokenModal = ({ onClose, onStatusChange, ...props }: RegisterWorkspaceTokenModalProps) => {
|
||||
const setModal = useSetModal();
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const connectWorkspace = useEndpoint('POST', '/v1/cloud.connectWorkspace');
|
||||
|
||||
const [token, setToken] = useState('');
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleBackAction = (): void => {
|
||||
const handleModalClose = (): void => setModal(null);
|
||||
setModal(<WorkspaceRegistrationModal onClose={handleModalClose} />);
|
||||
};
|
||||
|
||||
const handleTokenChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setToken(event.target.value);
|
||||
};
|
||||
|
||||
const isToken = token.length > 0;
|
||||
|
||||
const handleConnectButtonClick = async (): Promise<void> => {
|
||||
setProcessing(true);
|
||||
setError(false);
|
||||
|
||||
try {
|
||||
await connectWorkspace({ token });
|
||||
|
||||
setModal(null);
|
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Connected') });
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
setError(true);
|
||||
} finally {
|
||||
await onStatusChange?.();
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('RegisterWorkspace_Token_Title')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box is='p'>
|
||||
<Trans i18nKey='RegisterWorkspace_Token_Step_One'>
|
||||
1. Go to:{' '}
|
||||
<Box is='span' fontWeight={600}>
|
||||
cloud.rocket.chat {'>'} Workspaces
|
||||
</Box>{' '}
|
||||
and click{' '}
|
||||
<Box is='span' fontWeight={600}>
|
||||
"Register self-managed"
|
||||
</Box>
|
||||
.
|
||||
</Trans>
|
||||
</Box>
|
||||
<Box is='p' fontSize='p2'>{`2. ${t('RegisterWorkspace_Token_Step_Two')}`}</Box>
|
||||
<Field paddingBlockStart={10}>
|
||||
<FieldLabel>{t('Registration_Token')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput onChange={handleTokenChange} value={token} />
|
||||
</FieldRow>
|
||||
{error && <FieldError>{t('Token_Not_Recognized')}</FieldError>}
|
||||
</Field>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ButtonGroup align='end'>
|
||||
<Button onClick={handleBackAction}>{t('Back')}</Button>
|
||||
<Button primary disabled={!isToken} loading={processing} onClick={handleConnectButtonClick}>
|
||||
{t('Next')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWorkspaceTokenModal;
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useSafely } from '@rocket.chat/fuselage-hooks';
|
||||
import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import useFeatureBullets from '../hooks/useFeatureBullets';
|
||||
|
||||
export type RegisteredWorkspaceModalProps = {
|
||||
onClose: () => void;
|
||||
onStatusChange?: () => void;
|
||||
};
|
||||
|
||||
const RegisteredWorkspaceModal = ({ onClose, onStatusChange, ...props }: RegisteredWorkspaceModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const setModal = useSetModal();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const bulletFeatures = useFeatureBullets();
|
||||
const [isSyncing, setSyncing] = useSafely(useState(false));
|
||||
|
||||
const syncWorkspace = useEndpoint('POST', '/v1/cloud.syncWorkspace');
|
||||
|
||||
const handleSyncAction = async () => {
|
||||
setSyncing(true);
|
||||
|
||||
try {
|
||||
const { success } = await syncWorkspace();
|
||||
|
||||
if (!success) {
|
||||
throw Error(t('RegisterWorkspace_Syncing_Error'));
|
||||
}
|
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('RegisterWorkspace_Syncing_Complete') });
|
||||
setModal(null);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : t('RegisterWorkspace_Syncing_Error');
|
||||
dispatchToastMessage({ type: 'error', message });
|
||||
} finally {
|
||||
onStatusChange?.();
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTitle>{t('Workspace_registered')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box withRichContent>
|
||||
<span>{`${t('RegisterWorkspace_Registered_Subtitle')}: `}</span>
|
||||
<ul>
|
||||
{bulletFeatures.map((item, index) => (
|
||||
<li key={index}>
|
||||
<strong>{item.title}</strong>
|
||||
<Box is='p' marginBlockStart={4}>
|
||||
{item.description}
|
||||
</Box>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ButtonGroup align='end'>
|
||||
<Button icon='reload' onClick={handleSyncAction} loading={isSyncing}>
|
||||
{t('Sync')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisteredWorkspaceModal;
|
||||
Reference in New Issue
Block a user