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,41 @@
|
||||
import { Button, FieldRow, FieldHint } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type ActionInputBaseProps = SettingInputProps & {
|
||||
actionText: TranslationKey;
|
||||
sectionChanged: boolean;
|
||||
onAction: () => Promise<{ message: TranslationKey; params?: string[] }>;
|
||||
};
|
||||
|
||||
function ActionInputBase({ actionText, hint, disabled, sectionChanged, onAction }: ActionInputBaseProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const handleClick = async (): Promise<void> => {
|
||||
try {
|
||||
const data = await onAction();
|
||||
const params = data.params || [];
|
||||
dispatchToastMessage({ type: 'success', message: t(data.message, { postProcess: 'sprintf', sprintf: params }) });
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldRow>
|
||||
<Button disabled={disabled || sectionChanged} primary onClick={handleClick}>
|
||||
{t(actionText)}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
{sectionChanged && <FieldHint>{t('Save_to_enable_this_action')}</FieldHint>}
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionInputBase;
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import ActionSettingInput from './ActionSettingInput';
|
||||
|
||||
export default {
|
||||
component: ActionSettingInput,
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof ActionSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
actionText: 'Action text' as TranslationKey,
|
||||
value: 'methodName' as keyof ServerMethods,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
actionText: 'Action text' as TranslationKey,
|
||||
value: 'methodName' as keyof ServerMethods,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithinChangedSection = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
actionText: 'Action text' as TranslationKey,
|
||||
value: 'methodName' as keyof ServerMethods,
|
||||
sectionChanged: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isActionSettingWithEndpoint } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import type { PathPattern, Method } from '@rocket.chat/rest-typings';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import EndpointActionInput from './EndpointActionInput';
|
||||
import MethodActionInput from './MethodActionInput';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type ActionSettingInputProps = SettingInputProps & {
|
||||
value: keyof ServerMethods | { method: Method; path: PathPattern };
|
||||
actionText: TranslationKey;
|
||||
sectionChanged: boolean;
|
||||
};
|
||||
|
||||
function ActionSettingInput({ value, ...rest }: ActionSettingInputProps) {
|
||||
if (isActionSettingWithEndpoint(value)) {
|
||||
return <EndpointActionInput endpoint={{ method: value.method, path: value.path }} {...rest} />;
|
||||
}
|
||||
|
||||
return <MethodActionInput value={value} {...rest} />;
|
||||
}
|
||||
|
||||
export default ActionSettingInput;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import AssetSettingInput from './AssetSettingInput';
|
||||
|
||||
export default {
|
||||
component: AssetSettingInput,
|
||||
decorators: [
|
||||
(fn) => (
|
||||
<div>
|
||||
<div>
|
||||
<Field>{fn()}</Field>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof AssetSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: { url: 'https://rocket.chat/images/logo.svg' },
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFileConstraints = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
fileConstraints: { extensions: ['png', 'jpg', 'gif'] },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import { css } from '@rocket.chat/css-in-js';
|
||||
import { Box, Button, Field, FieldHint, FieldLabel, FieldRow, Icon, Palette } from '@rocket.chat/fuselage';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { useToastMessageDispatch, useEndpoint, useTranslation, useUpload } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEventHandler, DragEvent, SyntheticEvent } from 'react';
|
||||
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type AssetSettingInputProps = Omit<SettingInputProps<{ url: string }>, 'onChangeValue'> & {
|
||||
asset?: any;
|
||||
fileConstraints?: { extensions: string[] };
|
||||
};
|
||||
|
||||
function AssetSettingInput({ _id, label, value, hint, asset, required, disabled, fileConstraints }: AssetSettingInputProps) {
|
||||
const t = useTranslation();
|
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const setAsset = useUpload('/v1/assets.setAsset');
|
||||
const unsetAsset = useEndpoint('POST', '/v1/assets.unsetAsset');
|
||||
|
||||
const isDataTransferEvent = <T extends SyntheticEvent>(event: T): event is T & DragEvent<HTMLInputElement> =>
|
||||
Boolean('dataTransfer' in event && (event as any).dataTransfer.files);
|
||||
|
||||
const handleUpload: ChangeEventHandler<HTMLInputElement> = (event): void => {
|
||||
let { files } = event.target;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
if (isDataTransferEvent(event)) {
|
||||
files = event.dataTransfer.files;
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(files ?? []).forEach(async (blob) => {
|
||||
dispatchToastMessage({ type: 'info', message: t('Uploading_file') });
|
||||
|
||||
const fileData = new FormData();
|
||||
fileData.append('asset', blob, blob.name);
|
||||
fileData.append('assetName', asset);
|
||||
|
||||
try {
|
||||
await setAsset(fileData);
|
||||
} catch (e) {
|
||||
dispatchToastMessage({ type: 'error', message: e });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteButtonClick = async (): Promise<void> => {
|
||||
try {
|
||||
await unsetAsset({ assetName: asset });
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
};
|
||||
|
||||
const settingsFilePreview = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& input[type='file'] {
|
||||
position: absolute !important;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
|
||||
& * {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
& .preview {
|
||||
overflow: hidden;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
margin-right: 0.75rem;
|
||||
border-width: 1px;
|
||||
border-color: ${Palette.stroke['stroke-light']};
|
||||
border-radius: 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
|
||||
&.no-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
<FieldRow>
|
||||
<Box className={settingsFilePreview}>
|
||||
{value?.url ? (
|
||||
<div
|
||||
className='preview'
|
||||
style={{ backgroundImage: `url(${value.url}?_dc=${Random.id()})` }}
|
||||
role='img'
|
||||
aria-label={t('Asset_preview')}
|
||||
/>
|
||||
) : (
|
||||
<div className='preview no-file background-transparent-light secondary-font-color'>
|
||||
<Icon size='x16' name='upload' />
|
||||
</div>
|
||||
)}
|
||||
<div className='action'>
|
||||
{value?.url ? (
|
||||
<Button icon='trash' disabled={disabled} onClick={handleDeleteButtonClick}>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
) : (
|
||||
<Box position='relative' className={`rcx-button rcx-button--primary ${disabled ? 'is-disabled' : ''}`}>
|
||||
{t('Select_file')}
|
||||
<input type='file' accept={`.${fileConstraints?.extensions?.join(', .')}`} onChange={handleUpload} disabled={disabled} />
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default AssetSettingInput;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import BooleanSettingInput from './BooleanSettingInput';
|
||||
|
||||
export default {
|
||||
component: BooleanSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof BooleanSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Checked = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Box, Field, FieldHint, FieldLabel, FieldRow, ToggleSwitch } from '@rocket.chat/fuselage';
|
||||
import type { ChangeEvent } from 'react';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type BooleanSettingInputProps = SettingInputProps<boolean>;
|
||||
|
||||
function BooleanSettingInput({
|
||||
_id,
|
||||
label,
|
||||
disabled,
|
||||
readonly,
|
||||
required,
|
||||
value,
|
||||
hint,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: BooleanSettingInputProps) {
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = event.currentTarget.checked;
|
||||
onChangeValue?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow marginBlockEnd={8}>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
<Box display='flex' alignItems='center'>
|
||||
{hasResetButton && <ResetSettingButton marginInlineEnd={8} onClick={onResetButtonClick} />}
|
||||
<ToggleSwitch id={_id} checked={value === true} disabled={disabled || readonly} onChange={handleChange} />
|
||||
</Box>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default BooleanSettingInput;
|
||||
@@ -0,0 +1,114 @@
|
||||
import { act, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import CodeMirror from './CodeMirror';
|
||||
|
||||
type ChangeHandler = (doc: { getValue: () => string }) => void;
|
||||
|
||||
const editor = {
|
||||
on: jest.fn<void, [string, ChangeHandler]>(),
|
||||
off: jest.fn<void, [string, ChangeHandler]>(),
|
||||
setOption: jest.fn(),
|
||||
setValue: jest.fn<void, [string]>(),
|
||||
getValue: jest.fn<string, []>(),
|
||||
toTextArea: jest.fn(),
|
||||
};
|
||||
|
||||
const fromTextArea = jest.fn(() => editor);
|
||||
|
||||
jest.mock('codemirror', () => ({
|
||||
__esModule: true,
|
||||
default: { fromTextArea: (...args: unknown[]) => fromTextArea(...(args as [])) },
|
||||
}));
|
||||
|
||||
jest.mock('codemirror/addon/edit/matchbrackets', () => ({}));
|
||||
jest.mock('codemirror/addon/edit/closebrackets', () => ({}));
|
||||
jest.mock('codemirror/addon/edit/matchtags', () => ({}));
|
||||
jest.mock('codemirror/addon/edit/trailingspace', () => ({}));
|
||||
jest.mock('codemirror/addon/search/match-highlighter', () => ({}));
|
||||
jest.mock('codemirror/lib/codemirror.css', () => ({}));
|
||||
jest.mock('../../../../../../lib/codeMirror', () => ({}));
|
||||
|
||||
const flushAsync = () => act(() => Promise.resolve());
|
||||
|
||||
beforeEach(() => {
|
||||
editor.on.mockClear();
|
||||
editor.off.mockClear();
|
||||
editor.setOption.mockClear();
|
||||
editor.setValue.mockClear();
|
||||
editor.getValue.mockReset();
|
||||
editor.getValue.mockReturnValue('');
|
||||
editor.toTextArea.mockClear();
|
||||
fromTextArea.mockClear();
|
||||
});
|
||||
|
||||
it('initializes CodeMirror on mount with the initial value', async () => {
|
||||
render(<CodeMirror id='cm' readOnly={false} value='hello' onChange={jest.fn()} />);
|
||||
|
||||
await waitFor(() => expect(fromTextArea).toHaveBeenCalledTimes(1));
|
||||
expect(editor.setValue).toHaveBeenCalledWith('hello');
|
||||
expect(editor.on).toHaveBeenCalledWith('change', expect.any(Function));
|
||||
});
|
||||
|
||||
it('tears down the editor on unmount', async () => {
|
||||
const { unmount } = render(<CodeMirror id='cm' readOnly={false} value='' onChange={jest.fn()} />);
|
||||
|
||||
await waitFor(() => expect(fromTextArea).toHaveBeenCalledTimes(1));
|
||||
|
||||
unmount();
|
||||
|
||||
expect(editor.off).toHaveBeenCalledWith('change', expect.any(Function));
|
||||
expect(editor.toTextArea).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('updates options without recreating the editor', async () => {
|
||||
const { rerender } = render(<CodeMirror id='cm' readOnly={false} mode='javascript' value='' onChange={jest.fn()} />);
|
||||
|
||||
await waitFor(() => expect(fromTextArea).toHaveBeenCalledTimes(1));
|
||||
editor.setOption.mockClear();
|
||||
|
||||
rerender(<CodeMirror id='cm' readOnly mode='xml' value='' onChange={jest.fn()} />);
|
||||
await flushAsync();
|
||||
|
||||
expect(fromTextArea).toHaveBeenCalledTimes(1);
|
||||
expect(editor.toTextArea).not.toHaveBeenCalled();
|
||||
expect(editor.setOption).toHaveBeenCalledWith('mode', 'xml');
|
||||
expect(editor.setOption).toHaveBeenCalledWith('readOnly', true);
|
||||
});
|
||||
|
||||
it('syncs external value changes into the editor', async () => {
|
||||
const { rerender } = render(<CodeMirror id='cm' readOnly={false} value='a' onChange={jest.fn()} />);
|
||||
|
||||
await waitFor(() => expect(fromTextArea).toHaveBeenCalledTimes(1));
|
||||
editor.getValue.mockReturnValue('a');
|
||||
editor.setValue.mockClear();
|
||||
|
||||
rerender(<CodeMirror id='cm' readOnly={false} value='b' onChange={jest.fn()} />);
|
||||
await flushAsync();
|
||||
|
||||
expect(editor.setValue).toHaveBeenCalledWith('b');
|
||||
});
|
||||
|
||||
it('does not re-set the value when it already matches the editor content', async () => {
|
||||
const { rerender } = render(<CodeMirror id='cm' readOnly={false} value='a' onChange={jest.fn()} />);
|
||||
|
||||
await waitFor(() => expect(fromTextArea).toHaveBeenCalledTimes(1));
|
||||
editor.getValue.mockReturnValue('a');
|
||||
editor.setValue.mockClear();
|
||||
|
||||
rerender(<CodeMirror id='cm' readOnly={false} value='a' onChange={jest.fn()} />);
|
||||
await flushAsync();
|
||||
|
||||
expect(editor.setValue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forwards editor changes to onChange', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<CodeMirror id='cm' readOnly={false} value='' onChange={onChange} />);
|
||||
|
||||
await waitFor(() => expect(editor.on).toHaveBeenCalled());
|
||||
|
||||
const handler = editor.on.mock.calls[0][1];
|
||||
act(() => handler({ getValue: () => 'typed' }));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('typed');
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import type { Editor, EditorConfiguration, EditorFromTextArea } from 'codemirror';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const defaultGutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'];
|
||||
|
||||
type CodeMirrorModule = typeof import('codemirror');
|
||||
|
||||
let codeMirrorPromise: Promise<CodeMirrorModule> | undefined;
|
||||
|
||||
const loadCodeMirror = (): Promise<CodeMirrorModule> => {
|
||||
if (!codeMirrorPromise) {
|
||||
codeMirrorPromise = Promise.all([
|
||||
import('codemirror'),
|
||||
import('../../../../../../lib/codeMirror'),
|
||||
import('codemirror/addon/edit/matchbrackets'),
|
||||
import('codemirror/addon/edit/closebrackets'),
|
||||
import('codemirror/addon/edit/matchtags'),
|
||||
import('codemirror/addon/edit/trailingspace'),
|
||||
import('codemirror/addon/search/match-highlighter'),
|
||||
import('codemirror/lib/codemirror.css'),
|
||||
]).then(([cm]) => (cm as unknown as { default: CodeMirrorModule }).default ?? cm);
|
||||
}
|
||||
return codeMirrorPromise;
|
||||
};
|
||||
|
||||
export type CodeMirrorProps = {
|
||||
id: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
autoComplete?: string | undefined;
|
||||
lineNumbers?: boolean;
|
||||
lineWrapping?: boolean;
|
||||
mode?: string;
|
||||
gutters?: string[];
|
||||
foldGutter?: boolean;
|
||||
matchBrackets?: boolean;
|
||||
autoCloseBrackets?: boolean;
|
||||
matchTags?: boolean;
|
||||
showTrailingSpace?: boolean;
|
||||
highlightSelectionMatches?: boolean;
|
||||
readOnly: boolean;
|
||||
value: string;
|
||||
defaultValue?: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
function CodeMirror({
|
||||
lineNumbers = true,
|
||||
lineWrapping = true,
|
||||
mode = 'javascript',
|
||||
gutters = defaultGutters,
|
||||
foldGutter = true,
|
||||
matchBrackets = true,
|
||||
autoCloseBrackets = true,
|
||||
matchTags = true,
|
||||
showTrailingSpace = true,
|
||||
highlightSelectionMatches = true,
|
||||
readOnly,
|
||||
value,
|
||||
defaultValue,
|
||||
onChange,
|
||||
...props
|
||||
}: CodeMirrorProps) {
|
||||
const handleChange = useStableCallback(onChange);
|
||||
|
||||
const [textArea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
|
||||
const [codeMirror, setCodeMirror] = useState<CodeMirrorModule | null>(null);
|
||||
const editorRef = useRef<EditorFromTextArea | null>(null);
|
||||
|
||||
// Latest-prop refs read by the init effect without forcing it to re-run.
|
||||
const initialValueRef = useRef(value ?? defaultValue ?? '');
|
||||
const optionsRef = useRef<EditorConfiguration>({});
|
||||
optionsRef.current = {
|
||||
lineNumbers,
|
||||
lineWrapping,
|
||||
mode,
|
||||
gutters,
|
||||
foldGutter,
|
||||
matchBrackets,
|
||||
autoCloseBrackets,
|
||||
matchTags,
|
||||
showTrailingSpace,
|
||||
highlightSelectionMatches,
|
||||
readOnly,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
loadCodeMirror()
|
||||
.then((mod) => {
|
||||
if (!cancelled) setCodeMirror(() => mod);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('CodeMirror initialization failed:', error);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!textArea || !codeMirror) return;
|
||||
|
||||
const editor = codeMirror.fromTextArea(textArea, optionsRef.current);
|
||||
editor.setValue(initialValueRef.current);
|
||||
editorRef.current = editor;
|
||||
|
||||
const handleEditorChange = (doc: Editor) => {
|
||||
handleChange(doc.getValue());
|
||||
};
|
||||
editor.on('change', handleEditorChange);
|
||||
|
||||
return () => {
|
||||
editor.off('change', handleEditorChange);
|
||||
editor.toTextArea();
|
||||
editorRef.current = null;
|
||||
};
|
||||
}, [textArea, codeMirror, handleChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
editor.setOption('lineNumbers', lineNumbers);
|
||||
editor.setOption('lineWrapping', lineWrapping);
|
||||
editor.setOption('mode', mode);
|
||||
editor.setOption('gutters', gutters);
|
||||
editor.setOption('foldGutter', foldGutter);
|
||||
editor.setOption('matchBrackets', matchBrackets);
|
||||
editor.setOption('autoCloseBrackets', autoCloseBrackets);
|
||||
editor.setOption('matchTags', matchTags);
|
||||
editor.setOption('showTrailingSpace', showTrailingSpace);
|
||||
editor.setOption('highlightSelectionMatches', highlightSelectionMatches);
|
||||
editor.setOption('readOnly', readOnly);
|
||||
}, [
|
||||
lineNumbers,
|
||||
lineWrapping,
|
||||
mode,
|
||||
gutters,
|
||||
foldGutter,
|
||||
matchBrackets,
|
||||
autoCloseBrackets,
|
||||
matchTags,
|
||||
showTrailingSpace,
|
||||
highlightSelectionMatches,
|
||||
readOnly,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
const next = value ?? '';
|
||||
if (editor.getValue() !== next) {
|
||||
editor.setValue(next);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return <textarea readOnly ref={setTextArea} style={{ display: 'none' }} {...props} />;
|
||||
}
|
||||
|
||||
export default CodeMirror;
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Box, Button, ButtonGroup, FieldError } from '@rocket.chat/fuselage';
|
||||
import { useToggle } from '@rocket.chat/fuselage-hooks';
|
||||
import { useId, type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// A constant 2px border is always reserved so toggling the error state only changes the color, avoiding a layout reflow.
|
||||
const editorBorderProps = { borderWidth: 2, borderStyle: 'solid', borderRadius: 4 } as const;
|
||||
|
||||
export type CodeMirrorBoxProps = { label: ReactNode; children: ReactNode; error?: string };
|
||||
|
||||
const CodeMirrorBox = ({ label, children, error }: CodeMirrorBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [fullScreen, toggleFullScreen] = useToggle(false);
|
||||
const errorId = useId();
|
||||
|
||||
if (fullScreen) {
|
||||
return createPortal(
|
||||
<Box
|
||||
className='code-mirror-box code-mirror-box-fullscreen'
|
||||
color='default'
|
||||
backgroundColor='light'
|
||||
position='absolute'
|
||||
zIndex={100}
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
width='100%'
|
||||
height='100%'
|
||||
padding={40}
|
||||
>
|
||||
<Box fontScale='p1' marginBlockEnd={4}>
|
||||
{label}
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
height='100%'
|
||||
role='code'
|
||||
aria-label={typeof label === 'string' ? label : undefined}
|
||||
aria-describedby={error ? errorId : undefined}
|
||||
{...editorBorderProps}
|
||||
borderColor={error ? 'error' : 'transparent'}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
{error && (
|
||||
<FieldError id={errorId} role='alert' marginBlockStart={4}>
|
||||
{error}
|
||||
</FieldError>
|
||||
)}
|
||||
<Box marginBlockStart={8}>
|
||||
<ButtonGroup>
|
||||
<Button primary onClick={(): void => toggleFullScreen()}>
|
||||
{t('Exit_Full_Screen')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
</Box>,
|
||||
document.getElementById('main-content') as HTMLElement,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className='code-mirror-box'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
height='100%'
|
||||
role='code'
|
||||
aria-label={typeof label === 'string' ? label : undefined}
|
||||
aria-describedby={error ? errorId : undefined}
|
||||
{...editorBorderProps}
|
||||
borderColor={error ? 'error' : 'transparent'}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
{error && (
|
||||
<FieldError id={errorId} role='alert' marginBlockStart={4}>
|
||||
{error}
|
||||
</FieldError>
|
||||
)}
|
||||
<Box marginBlockStart={8}>
|
||||
<ButtonGroup>
|
||||
<Button primary onClick={(): void => toggleFullScreen()}>
|
||||
{t('Full_Screen')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeMirrorBox;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './CodeMirror';
|
||||
@@ -0,0 +1,41 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import CodeSettingInput from './CodeSettingInput';
|
||||
|
||||
const appRoot = mockAppRoot()
|
||||
.withTranslations('en', 'core', {
|
||||
Invalid_JSON: 'Invalid JSON',
|
||||
})
|
||||
.build();
|
||||
|
||||
const baseProps = {
|
||||
_id: 'Test_JSON',
|
||||
label: 'Test JSON',
|
||||
hint: '',
|
||||
code: 'application/json',
|
||||
packageValue: '{}',
|
||||
readonly: false,
|
||||
disabled: false,
|
||||
hasResetButton: false,
|
||||
onChangeValue: () => undefined,
|
||||
} as const;
|
||||
|
||||
describe('CodeSettingInput', () => {
|
||||
it('renders the error when the value is invalid JSON', () => {
|
||||
render(<CodeSettingInput {...baseProps} value='{ invalid' />, { wrapper: appRoot });
|
||||
expect(screen.getByText('Invalid JSON')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('announces the error as an alert associated with the editor', () => {
|
||||
render(<CodeSettingInput {...baseProps} value='{ invalid' />, { wrapper: appRoot });
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveTextContent('Invalid JSON');
|
||||
expect(screen.getByRole('code')).toHaveAttribute('aria-describedby', alert.id);
|
||||
});
|
||||
|
||||
it('does not render an error when the value is valid JSON', () => {
|
||||
render(<CodeSettingInput {...baseProps} value='{}' />, { wrapper: appRoot });
|
||||
expect(screen.queryByText('Invalid JSON')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import CodeSettingInput from './CodeSettingInput';
|
||||
|
||||
export default {
|
||||
component: CodeSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(fn) => (
|
||||
<div>
|
||||
<Field>{fn()}</Field>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof CodeSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
code: 'javascript',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
code: 'javascript',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: 'console.log("Hello World!");',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDescription = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
hint: 'Description: Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
value: 'console.log("Hello World!");',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: 'console.log("Hello World!");',
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { FieldLabel, FieldHint, FieldRow, Field } from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getCodeSettingError } from '../../../../../lib/utils/getCodeSettingError';
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import CodeMirror from './CodeMirror';
|
||||
import CodeMirrorBox from './CodeMirror/CodeMirrorBox';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type CodeSettingInputProps = SettingInputProps & {
|
||||
hint: string;
|
||||
code: string;
|
||||
readonly: boolean;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
function CodeSettingInput({
|
||||
_id,
|
||||
label,
|
||||
hint,
|
||||
value = '',
|
||||
code,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: CodeSettingInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const errorKey = getCodeSettingError(code, value);
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
onChangeValue(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
<CodeMirrorBox label={label} error={errorKey ? t(errorKey) : undefined}>
|
||||
<CodeMirror
|
||||
id={_id}
|
||||
mode={code}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</CodeMirrorBox>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default CodeSettingInput;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import ColorSettingInput from './ColorSettingInput';
|
||||
|
||||
export default {
|
||||
component: ColorSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof ColorSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
editor: 'color',
|
||||
allowedTypes: ['color', 'expression'],
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
editor: 'color',
|
||||
allowedTypes: ['color', 'expression'],
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
editor: 'color',
|
||||
allowedTypes: ['color', 'expression'],
|
||||
value: '#db2323',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithExpressionAsValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
editor: 'expression',
|
||||
allowedTypes: ['color', 'expression'],
|
||||
value: 'var(--rc-color-primary)',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
editor: 'color',
|
||||
allowedTypes: ['color', 'expression'],
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { SettingEditor } from '@rocket.chat/core-typings';
|
||||
import { FieldLabel, FieldRow, FieldHint, FlexItem, InputBox, Margins, TextInput, Select, Field } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEvent, Key } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type ColorSettingInputProps = SettingInputProps & {
|
||||
value: string;
|
||||
editor: string;
|
||||
allowedTypes?: TranslationKey[];
|
||||
};
|
||||
|
||||
function ColorSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
editor,
|
||||
allowedTypes = [],
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onChangeEditor,
|
||||
onResetButtonClick,
|
||||
}: ColorSettingInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChangeValue?.(event.currentTarget.value);
|
||||
},
|
||||
[onChangeValue],
|
||||
);
|
||||
|
||||
const handleEditorTypeChange = useCallback(
|
||||
(value: Key) => {
|
||||
onChangeEditor?.(value as SettingEditor);
|
||||
},
|
||||
[onChangeEditor],
|
||||
);
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<Margins inline={4}>
|
||||
<FieldRow>
|
||||
<Margins inline={4}>
|
||||
<FlexItem grow={2}>
|
||||
{editor === 'color' && (
|
||||
<InputBox
|
||||
type='color'
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
{editor === 'expression' && (
|
||||
<TextInput
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
</FlexItem>
|
||||
<Select
|
||||
type='color'
|
||||
id={`${_id}_editor`}
|
||||
value={editor}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleEditorTypeChange}
|
||||
options={allowedTypes.map((type) => [type, t(type)])}
|
||||
/>
|
||||
</Margins>
|
||||
</FieldRow>
|
||||
</Margins>
|
||||
<FieldHint>Variable name: {_id.replace(/theme-color-/, '@')}</FieldHint>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColorSettingInput;
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Method, PathPattern } from '@rocket.chat/rest-typings';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import type { ActionInputBaseProps } from './ActionInputBase';
|
||||
import ActionInputBase from './ActionInputBase';
|
||||
|
||||
export type EndpointActionInputProps = Omit<ActionInputBaseProps, 'onAction'> & {
|
||||
endpoint: {
|
||||
method: Method;
|
||||
path: PathPattern;
|
||||
};
|
||||
};
|
||||
|
||||
function EndpointActionInput({ endpoint, ...rest }: EndpointActionInputProps) {
|
||||
const callEndpoint = useEndpoint(endpoint.method, endpoint.path);
|
||||
return <ActionInputBase onAction={() => callEndpoint({} as never)} {...rest} />;
|
||||
}
|
||||
|
||||
export default EndpointActionInput;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import FontSettingInput from './FontSettingInput';
|
||||
|
||||
export default {
|
||||
component: FontSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof FontSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
value: 'Roboto',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type FontSettingInputProps = SettingInputProps & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
function FontSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: FontSettingInputProps) {
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event): void => {
|
||||
onChangeValue?.(event.currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<TextInput
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default FontSettingInput;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import GenericSettingInput from './GenericSettingInput';
|
||||
|
||||
export default {
|
||||
component: GenericSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof GenericSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: 'Value',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type GenericSettingInputProps = SettingInputProps & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
function GenericSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: GenericSettingInputProps) {
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event): void => {
|
||||
onChangeValue?.(event.currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<TextInput
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenericSettingInput;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import IntSettingInput from './IntSettingInput';
|
||||
|
||||
export default {
|
||||
component: IntSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof IntSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: '12345',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, InputBox } from '@rocket.chat/fuselage';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type IntSettingInputProps = SettingInputProps<string, string | number> & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
function IntSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
onChangeValue,
|
||||
hasResetButton,
|
||||
onResetButtonClick,
|
||||
}: IntSettingInputProps) {
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
onChangeValue?.(parseInt(event.currentTarget.value, 10));
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputBox
|
||||
id={_id}
|
||||
type='number'
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntSettingInput;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import LanguageSettingInput from './LanguageSettingInput';
|
||||
|
||||
export default {
|
||||
component: LanguageSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof LanguageSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
value: 'en',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
value: '1',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
|
||||
import { useLanguages } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type LanguageSettingInputProps = SettingInputProps<string, string | number>;
|
||||
|
||||
function LanguageSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: LanguageSettingInputProps) {
|
||||
const languages = useLanguages();
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
onChangeValue?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Select
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={(value) => handleChange(String(value))}
|
||||
options={languages.map(({ key, name }) => [key, name])}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSettingInput;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
|
||||
import type { PathPattern } from '@rocket.chat/rest-typings';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
import { miscQueryKeys } from '../../../../../lib/queryKeys';
|
||||
|
||||
export type LookupSettingInputProps = SettingInputProps & {
|
||||
lookupEndpoint: PathPattern extends `/${infer U}` ? U : PathPattern;
|
||||
};
|
||||
|
||||
function LookupSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
lookupEndpoint,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: LookupSettingInputProps) {
|
||||
const handleChange = (value: string): void => {
|
||||
onChangeValue?.(value);
|
||||
};
|
||||
|
||||
const lookup = useEndpoint('GET', lookupEndpoint) as unknown as () => Promise<{ data: { key: string; label: string }[] }>;
|
||||
const { data: values = [] } = useQuery({
|
||||
queryKey: miscQueryKeys.lookup(lookupEndpoint),
|
||||
queryFn: async () => {
|
||||
const { data = [] } = (await lookup()) ?? {};
|
||||
return data as { key: string; label: string }[];
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Select
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={(value) => handleChange(String(value))}
|
||||
options={values.map(({ key, label }) => [key, label])}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default LookupSettingInput;
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { useMethod } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import type { ActionInputBaseProps } from './ActionInputBase';
|
||||
import ActionInputBase from './ActionInputBase';
|
||||
|
||||
export type MethodActionInputProps = Omit<ActionInputBaseProps, 'onAction'> & {
|
||||
value: keyof ServerMethods;
|
||||
};
|
||||
|
||||
function MethodActionInput({ value, ...rest }: MethodActionInputProps) {
|
||||
const actionMethod = useMethod(value);
|
||||
return <ActionInputBase onAction={actionMethod} {...rest} />;
|
||||
}
|
||||
|
||||
export default MethodActionInput;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import type { valuesOption } from './MultiSelectSettingInput';
|
||||
import MultiSelectSettingInput from './MultiSelectSettingInput';
|
||||
|
||||
export default {
|
||||
component: MultiSelectSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof MultiSelectSettingInput>;
|
||||
|
||||
const options: valuesOption[] = [
|
||||
{ key: '1', i18nLabel: '1' as TranslationKey },
|
||||
{ key: '2', i18nLabel: '2' as TranslationKey },
|
||||
{ key: '3', i18nLabel: '3' as TranslationKey },
|
||||
];
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
values: options,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
values: options,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
value: ['1', 'Lorem Ipsum'],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
values: options,
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { FieldLabel, MultiSelectFiltered, MultiSelect, Field, FieldRow, FieldHint } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type valuesOption = { key: string; i18nLabel: TranslationKey };
|
||||
export type MultiSelectSettingInputProps = SettingInputProps<[string, string], string[]> & {
|
||||
values: valuesOption[];
|
||||
};
|
||||
|
||||
function MultiSelectSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
disabled,
|
||||
required,
|
||||
values = [],
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
autocomplete,
|
||||
}: MultiSelectSettingInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = (value: string[]): void => {
|
||||
onChangeValue?.(value);
|
||||
// onChangeValue && onChangeValue([...event.currentTarget.querySelectorAll('option')].filter((e) => e.selected).map((el) => el.value));
|
||||
};
|
||||
const Component = autocomplete ? MultiSelectFiltered : MultiSelect;
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Component
|
||||
max-width='full'
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
// autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
options={values.map(({ key, i18nLabel }) => [key, t(i18nLabel)])}
|
||||
aria-label={_id} // FIXME: Multiselect (fuselage) should be associating the FieldLabel automatically. This is a workaround for accessibility and test locators.
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultiSelectSettingInput;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import PasswordSettingInput from './PasswordSettingInput';
|
||||
|
||||
export default {
|
||||
component: PasswordSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof PasswordSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, PasswordInput } from '@rocket.chat/fuselage';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type PasswordSettingInputProps = SettingInputProps<string | number | readonly string[] | undefined>;
|
||||
|
||||
function PasswordSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: PasswordSettingInputProps) {
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
onChangeValue?.(event.currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<PasswordInput
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'new-password' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default PasswordSettingInput;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { composeStories } from '@storybook/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { axe } from 'jest-axe';
|
||||
|
||||
import * as stories from './RangeSettingInput.stories';
|
||||
|
||||
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
|
||||
|
||||
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
|
||||
const { baseElement } = render(<Story />);
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
|
||||
const { container } = render(<Story />);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import RangeSettingInput from './RangeSettingInput';
|
||||
|
||||
export default {
|
||||
component: RangeSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof RangeSettingInput>;
|
||||
|
||||
const Template: StoryFn<typeof RangeSettingInput> = (args) => (
|
||||
<RangeSettingInput {...args} _id='setting_id' label='Label' minValue={0} maxValue={100} />
|
||||
);
|
||||
|
||||
export const Default = {
|
||||
render: Template,
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
value: 50,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithHint = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
value: 50,
|
||||
hint: 'This is a hint for the slider',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
value: 50,
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Slider, Field, FieldLabel, FieldRow, FieldHint } from '@rocket.chat/fuselage';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type RangeSettingInputProps = SettingInputProps<number> & {
|
||||
hint?: string;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
};
|
||||
|
||||
function RangeSettingInput({
|
||||
_id,
|
||||
label,
|
||||
hint,
|
||||
value,
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
readonly,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: RangeSettingInputProps) {
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
{hint && (
|
||||
<FieldRow>
|
||||
<FieldHint marginBlockEnd={4}>{hint}</FieldHint>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow>
|
||||
<Slider
|
||||
disabled={disabled || readonly}
|
||||
minValue={minValue}
|
||||
maxValue={maxValue}
|
||||
value={Number(value || 0)}
|
||||
onChange={onChangeValue}
|
||||
/>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default RangeSettingInput;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import RelativeUrlSettingInput from './RelativeUrlSettingInput';
|
||||
|
||||
export default {
|
||||
component: RelativeUrlSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof RelativeUrlSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: 'Value',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, UrlInput } from '@rocket.chat/fuselage';
|
||||
import { useAbsoluteUrl } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type RelativeUrlSettingInputProps = SettingInputProps;
|
||||
|
||||
function RelativeUrlSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: RelativeUrlSettingInputProps) {
|
||||
const getAbsoluteUrl = useAbsoluteUrl();
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
onChangeValue?.(event.currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<UrlInput
|
||||
id={_id}
|
||||
value={getAbsoluteUrl(value || '')}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default RelativeUrlSettingInput;
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { SettingValueRoomPick } from '@rocket.chat/core-typings';
|
||||
import { Field, FieldHint, FieldLabel, FieldRow } from '@rocket.chat/fuselage';
|
||||
|
||||
import RoomAutoCompleteMultiple from '../../../../../components/RoomAutoCompleteMultiple';
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type RoomPickSettingInputProps = SettingInputProps<SettingValueRoomPick | '', SettingValueRoomPick>;
|
||||
|
||||
function RoomPickSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: RoomPickSettingInputProps) {
|
||||
const parsedValue = (value || []).map(({ _id }) => _id);
|
||||
|
||||
const handleChange = (value: string | string[]) => {
|
||||
if (typeof value === 'object') {
|
||||
const newValue = value.map((currentValue: string) => ({ _id: currentValue }));
|
||||
onChangeValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<RoomAutoCompleteMultiple
|
||||
readOnly={readonly}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
value={parsedValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoomPickSettingInput;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import SelectSettingInput from './SelectSettingInput';
|
||||
|
||||
export default {
|
||||
component: SelectSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof SelectSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
values: [
|
||||
{ key: '1', i18nLabel: '1' as TranslationKey },
|
||||
{ key: '2', i18nLabel: '2' as TranslationKey },
|
||||
{ key: '3', i18nLabel: '3' as TranslationKey },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
values: [
|
||||
{ key: '1', i18nLabel: '1' as TranslationKey },
|
||||
{ key: '2', i18nLabel: '2' as TranslationKey },
|
||||
{ key: '3', i18nLabel: '3' as TranslationKey },
|
||||
],
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
value: '2',
|
||||
values: [
|
||||
{ key: '1', i18nLabel: '1' as TranslationKey },
|
||||
{ key: '2', i18nLabel: '2' as TranslationKey },
|
||||
{ key: '3', i18nLabel: '3' as TranslationKey },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
values: [
|
||||
{ key: '1', i18nLabel: '1' as TranslationKey },
|
||||
{ key: '2', i18nLabel: '2' as TranslationKey },
|
||||
{ key: '3', i18nLabel: '3' as TranslationKey },
|
||||
],
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type SelectSettingInputProps = SettingInputProps & {
|
||||
values?: { key: string; i18nLabel: TranslationKey }[];
|
||||
};
|
||||
|
||||
function SelectSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
values = [],
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: SelectSettingInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
onChangeValue?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Select
|
||||
id={_id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={(value) => handleChange(String(value))}
|
||||
options={values.map(({ key, i18nLabel }) => [key, t(i18nLabel)])}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectSettingInput;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
|
||||
import { canonicalizeTimezone } from '@rocket.chat/tools';
|
||||
|
||||
import { useTimezoneNameList } from '../../../../../hooks/useTimezoneNameList';
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type SelectTimezoneSettingInputProps = SettingInputProps;
|
||||
|
||||
function SelectTimezoneSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: SelectTimezoneSettingInputProps) {
|
||||
const timezoneNames = useTimezoneNameList();
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
onChangeValue?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Select
|
||||
id={_id}
|
||||
value={typeof value === 'string' ? canonicalizeTimezone(value) : value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={(value) => handleChange(String(value))}
|
||||
options={timezoneNames.map((key) => [key, key])}
|
||||
/>
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectTimezoneSettingInput;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Field } from '@rocket.chat/fuselage';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import StringSettingInput from './StringSettingInput';
|
||||
|
||||
export default {
|
||||
component: StringSettingInput,
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: '^on.*',
|
||||
},
|
||||
},
|
||||
decorators: [(fn) => <Field>{fn()}</Field>],
|
||||
} satisfies Meta<typeof StringSettingInput>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: 'Value',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
};
|
||||
|
||||
export const Multiline = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
value: 'Value\n'.repeat(10),
|
||||
placeholder: 'Placeholder',
|
||||
multiline: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResetButton = {
|
||||
args: {
|
||||
_id: 'setting_id',
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
hasResetButton: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, TextAreaInput, TextInput } from '@rocket.chat/fuselage';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type StringSettingInputProps = SettingInputProps & {
|
||||
name?: string;
|
||||
multiline?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
function StringSettingInput({
|
||||
_id,
|
||||
label,
|
||||
name,
|
||||
disabled,
|
||||
required,
|
||||
multiline,
|
||||
placeholder,
|
||||
readonly,
|
||||
error,
|
||||
autocomplete,
|
||||
value,
|
||||
hint,
|
||||
hasResetButton,
|
||||
onChangeValue,
|
||||
onResetButtonClick,
|
||||
}: StringSettingInputProps) {
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
onChangeValue?.(event.currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={onResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
{multiline ? (
|
||||
<TextAreaInput
|
||||
id={_id}
|
||||
name={name}
|
||||
rows={4}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
error={error}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
id={_id}
|
||||
value={value}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
error={error}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default StringSettingInput;
|
||||
@@ -0,0 +1,162 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { default as TimespanSettingInput, getHighestTimeUnit } from './TimespanSettingInput';
|
||||
import { TIMEUNIT } from '../../../../../lib/convertTimeUnit';
|
||||
|
||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getHighestTimeUnit function', () => {
|
||||
it('should return minutes if milliseconds cannot be evenly divided into hours or days', () => {
|
||||
expect(getHighestTimeUnit(900000)).toBe(TIMEUNIT.minutes); // 15 min
|
||||
expect(getHighestTimeUnit(2100000)).toBe(TIMEUNIT.minutes); // 35 min
|
||||
expect(getHighestTimeUnit(3660000)).toBe(TIMEUNIT.minutes); // 61 minutes
|
||||
expect(getHighestTimeUnit(86460000)).toBe(TIMEUNIT.minutes); // 1441 minutes
|
||||
});
|
||||
|
||||
it('should return hours if milliseconds can be evenly divided into hours but not days', () => {
|
||||
expect(getHighestTimeUnit(3600000)).toBe(TIMEUNIT.hours); // 1 hour
|
||||
expect(getHighestTimeUnit(7200000)).toBe(TIMEUNIT.hours); // 2 hours
|
||||
expect(getHighestTimeUnit(90000000)).toBe(TIMEUNIT.hours); // 25 hours
|
||||
});
|
||||
|
||||
it('should return days if milliseconds can be evenly divided into days', () => {
|
||||
expect(getHighestTimeUnit(86400000)).toBe(TIMEUNIT.days); // 1 day
|
||||
expect(getHighestTimeUnit(172800000)).toBe(TIMEUNIT.days); // 2 days
|
||||
expect(getHighestTimeUnit(604800000)).toBe(TIMEUNIT.days); // 7 days
|
||||
});
|
||||
});
|
||||
|
||||
describe('TimespanSettingInput component', () => {
|
||||
const onChangeValueMock = jest.fn();
|
||||
const onResetButtonClickMock = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call onChangeValue with the correct value when inputting a value and changing time unit', async () => {
|
||||
render(
|
||||
<TimespanSettingInput
|
||||
disabled={false}
|
||||
packageValue='2592000000'
|
||||
hasResetButton={false}
|
||||
_id='timespanInput'
|
||||
label='Timespan'
|
||||
value='86400000' // 1 day
|
||||
placeholder='Enter timespan'
|
||||
onChangeValue={onChangeValueMock}
|
||||
/>,
|
||||
{ wrapper: mockAppRoot().build() },
|
||||
);
|
||||
|
||||
const numberInput = screen.getByRole('spinbutton');
|
||||
await userEvent.clear(numberInput); // Change value to 2
|
||||
await userEvent.type(numberInput, '2');
|
||||
|
||||
expect(onChangeValueMock).toHaveBeenCalledWith(2 * 24 * 60 * 60 * 1000); // 2 days in milliseconds
|
||||
});
|
||||
|
||||
it('should update value to minutes when changing time unit to minutes', async () => {
|
||||
render(
|
||||
<TimespanSettingInput
|
||||
disabled={false}
|
||||
packageValue='2592000000'
|
||||
hasResetButton={false}
|
||||
_id='timespanInput'
|
||||
label='Timespan'
|
||||
value='3600000' // 1 hour in milliseconds
|
||||
placeholder='Enter timespan'
|
||||
onChangeValue={onChangeValueMock}
|
||||
/>,
|
||||
{ wrapper: mockAppRoot().build() },
|
||||
);
|
||||
|
||||
const selectInput = screen.getByRole('button', { name: 'hours' });
|
||||
await userEvent.click(selectInput);
|
||||
const minutesOption = screen.getByRole('option', { name: 'minutes' });
|
||||
await userEvent.click(minutesOption);
|
||||
|
||||
expect(onChangeValueMock).toHaveBeenCalledWith(60000); // 1 min in milliseconds
|
||||
|
||||
expect(screen.getByDisplayValue('1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update value to hours when changing time unit to hours', async () => {
|
||||
render(
|
||||
<TimespanSettingInput
|
||||
disabled={false}
|
||||
hasResetButton={false}
|
||||
packageValue='2592000000'
|
||||
_id='timespanInput'
|
||||
label='Timespan'
|
||||
value='86400000' // 1 day in milliseconds
|
||||
placeholder='Enter timespan'
|
||||
onChangeValue={onChangeValueMock}
|
||||
/>,
|
||||
{ wrapper: mockAppRoot().build() },
|
||||
);
|
||||
|
||||
const selectInput = screen.getByRole('button', { name: 'days' });
|
||||
await userEvent.click(selectInput);
|
||||
const hoursOption = screen.getByRole('option', { name: 'hours' });
|
||||
await userEvent.click(hoursOption);
|
||||
|
||||
expect(onChangeValueMock).toHaveBeenCalledWith(3600000); // 1 hour in milliseconds
|
||||
|
||||
expect(screen.getByDisplayValue('1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update value to days when changing time unit to days', async () => {
|
||||
render(
|
||||
<TimespanSettingInput
|
||||
disabled={false}
|
||||
hasResetButton={false}
|
||||
packageValue='2592000000'
|
||||
_id='timespanInput'
|
||||
label='Timespan'
|
||||
value='43200000' // half a day
|
||||
placeholder='Enter timespan'
|
||||
onChangeValue={onChangeValueMock}
|
||||
/>,
|
||||
{ wrapper: mockAppRoot().build() },
|
||||
);
|
||||
|
||||
const selectInput = screen.getByRole('button', { name: 'hours' });
|
||||
await userEvent.click(selectInput);
|
||||
const daysOption = screen.getByRole('option', { name: 'days' });
|
||||
await userEvent.click(daysOption);
|
||||
|
||||
expect(onChangeValueMock).toHaveBeenCalledWith(1036800000); // 12 days in milliseconds
|
||||
|
||||
expect(screen.getByDisplayValue('12')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call onResetButtonClick when reset button is clicked', async () => {
|
||||
render(
|
||||
<TimespanSettingInput
|
||||
disabled={false}
|
||||
_id='timespanInput'
|
||||
packageValue='2592000000'
|
||||
label='Timespan'
|
||||
value='3600000' // 1 hour in milliseconds
|
||||
placeholder='Enter timespan'
|
||||
onChangeValue={onChangeValueMock}
|
||||
hasResetButton
|
||||
onResetButtonClick={onResetButtonClickMock}
|
||||
/>,
|
||||
{ wrapper: mockAppRoot().build() },
|
||||
);
|
||||
|
||||
const resetButton = screen.getByTitle('Reset');
|
||||
await userEvent.click(resetButton);
|
||||
|
||||
expect(onResetButtonClickMock).toHaveBeenCalled();
|
||||
expect(screen.getByDisplayValue('30')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Field, FieldHint, FieldLabel, FieldRow, InputBox, Select } from '@rocket.chat/fuselage';
|
||||
import type { ChangeEventHandler, Key } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TIMEUNIT, timeUnitToMs, msToTimeUnit } from '../../../../../lib/convertTimeUnit';
|
||||
import ResetSettingButton from '../ResetSettingButton';
|
||||
import type { SettingInputProps } from './types';
|
||||
|
||||
export type TimespanSettingInputProps = SettingInputProps<string, string | number> & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const getHighestTimeUnit = (value: number): TIMEUNIT => {
|
||||
const minutes = msToTimeUnit(TIMEUNIT.minutes, value);
|
||||
if (minutes % 60 !== 0) {
|
||||
return TIMEUNIT.minutes;
|
||||
}
|
||||
|
||||
const hours = msToTimeUnit(TIMEUNIT.hours, value);
|
||||
if (hours % 24 !== 0) {
|
||||
return TIMEUNIT.hours;
|
||||
}
|
||||
|
||||
return TIMEUNIT.days;
|
||||
};
|
||||
|
||||
const sanitizeInputValue = (value: number) => {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sanitizedValue = Math.max(0, value).toFixed(0);
|
||||
|
||||
return Number(sanitizedValue);
|
||||
};
|
||||
|
||||
function TimespanSettingInput({
|
||||
_id,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
placeholder,
|
||||
readonly,
|
||||
autocomplete,
|
||||
disabled,
|
||||
required,
|
||||
onChangeValue,
|
||||
hasResetButton,
|
||||
onResetButtonClick,
|
||||
packageValue,
|
||||
}: TimespanSettingInputProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [timeUnit, setTimeUnit] = useState<TIMEUNIT>(getHighestTimeUnit(Number(value)));
|
||||
const [internalValue, setInternalValue] = useState<number>(msToTimeUnit(timeUnit, Number(value)));
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const newValue = sanitizeInputValue(Number(event.currentTarget.value));
|
||||
|
||||
onChangeValue?.(timeUnitToMs(timeUnit, newValue));
|
||||
|
||||
setInternalValue(newValue);
|
||||
};
|
||||
|
||||
const handleChangeTimeUnit = (nextTimeUnit: Key) => {
|
||||
if (typeof nextTimeUnit !== 'string') {
|
||||
return;
|
||||
}
|
||||
onChangeValue?.(timeUnitToMs(nextTimeUnit as TIMEUNIT, internalValue));
|
||||
setTimeUnit(nextTimeUnit as TIMEUNIT);
|
||||
};
|
||||
|
||||
const timeUnitOptions = useMemo(() => {
|
||||
return Object.entries(TIMEUNIT).map<readonly [TIMEUNIT, string]>(([label, value]) => [value, i18n.exists(label) ? t(label) : label]); // todo translate
|
||||
}, [i18n, t]);
|
||||
|
||||
const handleResetButtonClick = () => {
|
||||
onResetButtonClick?.();
|
||||
const newTimeUnit = getHighestTimeUnit(Number(packageValue));
|
||||
setTimeUnit(newTimeUnit);
|
||||
setInternalValue(msToTimeUnit(newTimeUnit, Number(packageValue)));
|
||||
};
|
||||
|
||||
return (
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={_id} title={_id} required={required}>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
{hasResetButton && <ResetSettingButton onClick={handleResetButtonClick} />}
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputBox
|
||||
id={_id}
|
||||
type='number'
|
||||
value={internalValue}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoComplete={autocomplete === false ? 'off' : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Select value={timeUnit} disabled={disabled} options={timeUnitOptions} onChange={handleChangeTimeUnit} />
|
||||
</FieldRow>
|
||||
{hint && <FieldHint>{hint}</FieldHint>}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimespanSettingInput;
|
||||
@@ -0,0 +1,412 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`renders Default without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label"
|
||||
for="setting_id"
|
||||
title="setting_id"
|
||||
>
|
||||
Label
|
||||
</label>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-146twpt"
|
||||
id="react-aria-_r_0_"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="react-aria-_r_0_-0"
|
||||
>
|
||||
<span
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
data-testid="slider-output"
|
||||
for="react-aria-_r_0_-0"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="rcx-css-vkh2sr"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-5ps27r"
|
||||
style="position: absolute; left: 0%; transform: translate(-50%, -50%);"
|
||||
>
|
||||
<div
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-details=""
|
||||
aria-labelledby="react-aria-_r_0_"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuetext="0"
|
||||
id="react-aria-_r_0_-0"
|
||||
max="100"
|
||||
min="0"
|
||||
step="1"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`renders Disabled without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label"
|
||||
for="setting_id"
|
||||
title="setting_id"
|
||||
>
|
||||
Label
|
||||
</label>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-146twpt"
|
||||
id="react-aria-_r_6_"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="react-aria-_r_6_-0"
|
||||
>
|
||||
<span
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
data-testid="slider-output"
|
||||
for="react-aria-_r_6_-0"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="rcx-css-1usji5y"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-1laaycj"
|
||||
style="position: absolute; left: 0%; transform: translate(-50%, -50%);"
|
||||
>
|
||||
<div
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-details=""
|
||||
aria-labelledby="react-aria-_r_6_"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuetext="0"
|
||||
disabled=""
|
||||
id="react-aria-_r_6_-0"
|
||||
max="100"
|
||||
min="0"
|
||||
step="1"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`renders WithHint without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label"
|
||||
for="setting_id"
|
||||
title="setting_id"
|
||||
>
|
||||
Label
|
||||
</label>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__hint rcx-css-18q594f"
|
||||
>
|
||||
This is a hint for the slider
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-146twpt"
|
||||
id="react-aria-_r_c_"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="react-aria-_r_c_-0"
|
||||
>
|
||||
<span
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
data-testid="slider-output"
|
||||
for="react-aria-_r_c_-0"
|
||||
>
|
||||
50
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="rcx-css-14xs94t"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-5ps27r"
|
||||
style="position: absolute; left: 50%; transform: translate(-50%, -50%);"
|
||||
>
|
||||
<div
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-details=""
|
||||
aria-labelledby="react-aria-_r_c_"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuetext="50"
|
||||
id="react-aria-_r_c_-0"
|
||||
max="100"
|
||||
min="0"
|
||||
step="1"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`renders WithResetButton without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label"
|
||||
for="setting_id"
|
||||
title="setting_id"
|
||||
>
|
||||
Label
|
||||
</label>
|
||||
<button
|
||||
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--icon-danger rcx-button--square rcx-button--icon rcx-button"
|
||||
title="Reset"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-undo rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
❰
|
||||
</i>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-146twpt"
|
||||
id="react-aria-_r_i_"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="react-aria-_r_i_-0"
|
||||
>
|
||||
<span
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
data-testid="slider-output"
|
||||
for="react-aria-_r_i_-0"
|
||||
>
|
||||
50
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="rcx-css-14xs94t"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-5ps27r"
|
||||
style="position: absolute; left: 50%; transform: translate(-50%, -50%);"
|
||||
>
|
||||
<div
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-details=""
|
||||
aria-labelledby="react-aria-_r_i_"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuetext="50"
|
||||
id="react-aria-_r_i_-0"
|
||||
max="100"
|
||||
min="0"
|
||||
step="1"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`renders WithValue without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label"
|
||||
for="setting_id"
|
||||
title="setting_id"
|
||||
>
|
||||
Label
|
||||
</label>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-146twpt"
|
||||
id="react-aria-_r_o_"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="react-aria-_r_o_-0"
|
||||
>
|
||||
<span
|
||||
aria-live="off"
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
data-testid="slider-output"
|
||||
for="react-aria-_r_o_-0"
|
||||
>
|
||||
50
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="rcx-css-14xs94t"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="rcx-css-5ps27r"
|
||||
style="position: absolute; left: 50%; transform: translate(-50%, -50%);"
|
||||
>
|
||||
<div
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-details=""
|
||||
aria-labelledby="react-aria-_r_o_"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuetext="50"
|
||||
id="react-aria-_r_o_-0"
|
||||
max="100"
|
||||
min="0"
|
||||
step="1"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type SettingInputProps<V = string, R = V> = {
|
||||
_id: string;
|
||||
label: ReactNode;
|
||||
value?: V;
|
||||
packageValue: V;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
autocomplete?: boolean;
|
||||
disabled: boolean;
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
editor?: string;
|
||||
hasResetButton: boolean;
|
||||
onChangeValue: (value: R) => void;
|
||||
onResetButtonClick?: () => void;
|
||||
onChangeEditor?: (value: string | undefined) => void;
|
||||
};
|
||||
Reference in New Issue
Block a user