feat: add uuid widget (#719)
This commit is contained in:
parent
79877fcd1f
commit
9d96a27952
@ -190,7 +190,8 @@ collections:
|
|||||||
- name: pattern
|
- name: pattern
|
||||||
label: Pattern Validation
|
label: Pattern Validation
|
||||||
widget: color
|
widget: color
|
||||||
pattern: ['^#[a-fA-F0-9]{3}$|^[a-fA-F0-9]{4}$|^[a-fA-F0-9]{6}$', 'Must be a valid hex code']
|
pattern:
|
||||||
|
['^#[a-fA-F0-9]{3}$|^[a-fA-F0-9]{4}$|^[a-fA-F0-9]{6}$', 'Must be a valid hex code']
|
||||||
allow_input: true
|
allow_input: true
|
||||||
required: false
|
required: false
|
||||||
- name: alpha
|
- name: alpha
|
||||||
@ -851,6 +852,18 @@ collections:
|
|||||||
widget: text
|
widget: text
|
||||||
pattern: ['.{12,}', 'Must have at least 12 characters']
|
pattern: ['.{12,}', 'Must have at least 12 characters']
|
||||||
required: false
|
required: false
|
||||||
|
- name: uuid
|
||||||
|
label: UUID
|
||||||
|
file: _widgets/uuid.json
|
||||||
|
description: UUID widget
|
||||||
|
fields:
|
||||||
|
- name: uuid
|
||||||
|
label: UUID
|
||||||
|
widget: uuid
|
||||||
|
- name: no_regenerate
|
||||||
|
label: Does not allow regeneration
|
||||||
|
widget: uuid
|
||||||
|
allow_regenerate: false
|
||||||
- name: settings
|
- name: settings
|
||||||
label: Settings
|
label: Settings
|
||||||
delete: false
|
delete: false
|
||||||
|
3
packages/core/src/__mocks__/uuid.ts
Normal file
3
packages/core/src/__mocks__/uuid.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const v4 = jest.fn().mockReturnValue('I_AM_A_UUID');
|
||||||
|
|
||||||
|
export const validate = jest.fn();
|
@ -22,6 +22,7 @@ export interface FieldProps {
|
|||||||
noHightlight?: boolean;
|
noHightlight?: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
disableClick?: boolean;
|
disableClick?: boolean;
|
||||||
|
endAdornment?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Field: FC<FieldProps> = ({
|
const Field: FC<FieldProps> = ({
|
||||||
@ -37,6 +38,7 @@ const Field: FC<FieldProps> = ({
|
|||||||
noHightlight = false,
|
noHightlight = false,
|
||||||
disabled,
|
disabled,
|
||||||
disableClick = false,
|
disableClick = false,
|
||||||
|
endAdornment,
|
||||||
}) => {
|
}) => {
|
||||||
const finalCursor = useCursor(cursor, disabled);
|
const finalCursor = useCursor(cursor, disabled);
|
||||||
|
|
||||||
@ -95,6 +97,8 @@ const Field: FC<FieldProps> = ({
|
|||||||
`
|
`
|
||||||
relative
|
relative
|
||||||
flex
|
flex
|
||||||
|
items-center
|
||||||
|
gap-2
|
||||||
border-b
|
border-b
|
||||||
border-slate-400
|
border-slate-400
|
||||||
focus-within:border-blue-800
|
focus-within:border-blue-800
|
||||||
@ -153,6 +157,16 @@ const Field: FC<FieldProps> = ({
|
|||||||
{renderedHint}
|
{renderedHint}
|
||||||
{renderedErrorMessage}
|
{renderedErrorMessage}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
pr-2
|
||||||
|
`,
|
||||||
|
!noPadding && '-mb-3',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{endAdornment}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
|
||||||
import type { ChangeEventHandler, FC, MouseEventHandler, Ref } from 'react';
|
import type { ChangeEventHandler, FC, MouseEventHandler, ReactNode, Ref } from 'react';
|
||||||
|
|
||||||
export interface BaseTextFieldProps {
|
export interface BaseTextFieldProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -17,6 +17,8 @@ export interface BaseTextFieldProps {
|
|||||||
variant?: 'borderless' | 'contained';
|
variant?: 'borderless' | 'contained';
|
||||||
inputRef?: Ref<HTMLInputElement>;
|
inputRef?: Ref<HTMLInputElement>;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
endAdornment?: ReactNode;
|
||||||
|
startAdornment?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberTextFieldProps extends BaseTextFieldProps {
|
export interface NumberTextFieldProps extends BaseTextFieldProps {
|
||||||
@ -45,6 +47,8 @@ const TextField: FC<TextFieldProps> = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
onChange,
|
onChange,
|
||||||
onClick,
|
onClick,
|
||||||
|
startAdornment,
|
||||||
|
endAdornment,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const finalCursor = useCursor(cursor, disabled);
|
const finalCursor = useCursor(cursor, disabled);
|
||||||
@ -58,6 +62,8 @@ const TextField: FC<TextFieldProps> = ({
|
|||||||
data-testid={dataTestId ?? `${type}-input`}
|
data-testid={dataTestId ?? `${type}-input`}
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
startAdornment={startAdornment}
|
||||||
|
endAdornment={endAdornment}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
className: `
|
className: `
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
SelectWidget,
|
SelectWidget,
|
||||||
StringWidget,
|
StringWidget,
|
||||||
TextWidget,
|
TextWidget,
|
||||||
|
UUIDWidget,
|
||||||
} from './widgets';
|
} from './widgets';
|
||||||
|
|
||||||
export default function addExtensions() {
|
export default function addExtensions() {
|
||||||
@ -53,6 +54,7 @@ export default function addExtensions() {
|
|||||||
SelectWidget(),
|
SelectWidget(),
|
||||||
StringWidget(),
|
StringWidget(),
|
||||||
TextWidget(),
|
TextWidget(),
|
||||||
|
UUIDWidget(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Object.keys(locales).forEach(locale => {
|
Object.keys(locales).forEach(locale => {
|
||||||
|
@ -689,11 +689,15 @@ export interface HiddenField extends BaseField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StringOrTextField extends BaseField {
|
export interface StringOrTextField extends BaseField {
|
||||||
// This is the default widget, so declaring its type is optional.
|
|
||||||
widget: 'string' | 'text';
|
widget: 'string' | 'text';
|
||||||
default?: string;
|
default?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UUIDField extends BaseField {
|
||||||
|
widget: 'uuid';
|
||||||
|
allow_regenerate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UnknownField extends BaseField {
|
export interface UnknownField extends BaseField {
|
||||||
widget: 'unknown';
|
widget: 'unknown';
|
||||||
}
|
}
|
||||||
@ -713,6 +717,7 @@ export type Field<EF extends BaseField = UnknownField> =
|
|||||||
| SelectField
|
| SelectField
|
||||||
| HiddenField
|
| HiddenField
|
||||||
| StringOrTextField
|
| StringOrTextField
|
||||||
|
| UUIDField
|
||||||
| EF;
|
| EF;
|
||||||
|
|
||||||
export interface ViewFilter {
|
export interface ViewFilter {
|
||||||
|
@ -28,3 +28,5 @@ export * from './string';
|
|||||||
export { default as StringWidget } from './string';
|
export { default as StringWidget } from './string';
|
||||||
export * from './text';
|
export * from './text';
|
||||||
export { default as TextWidget } from './text';
|
export { default as TextWidget } from './text';
|
||||||
|
export * from './uuid';
|
||||||
|
export { default as UUIDWidget } from './uuid';
|
||||||
|
@ -13,6 +13,8 @@ import ListControl from '../ListControl';
|
|||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
import type { ListField, ValueOrNestedValue } from '@staticcms/core/interface';
|
import type { ListField, ValueOrNestedValue } from '@staticcms/core/interface';
|
||||||
|
|
||||||
|
jest.unmock('uuid');
|
||||||
|
|
||||||
const singletonListField: ListField = {
|
const singletonListField: ListField = {
|
||||||
widget: 'list',
|
widget: 'list',
|
||||||
name: 'singleton',
|
name: 'singleton',
|
||||||
|
79
packages/core/src/widgets/uuid/UUIDControl.tsx
Normal file
79
packages/core/src/widgets/uuid/UUIDControl.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Refresh as RefreshIcon } from '@styled-icons/material/Refresh';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { v4 as uuid, validate } from 'uuid';
|
||||||
|
|
||||||
|
import IconButton from '@staticcms/core/components/common/button/IconButton';
|
||||||
|
import Field from '@staticcms/core/components/common/field/Field';
|
||||||
|
import TextField from '@staticcms/core/components/common/text-field/TextField';
|
||||||
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
|
||||||
|
import type { UUIDField, WidgetControlProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
const UUIDControl: FC<WidgetControlProps<string, UUIDField>> = ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
errors,
|
||||||
|
disabled,
|
||||||
|
field,
|
||||||
|
forSingleList,
|
||||||
|
duplicate,
|
||||||
|
controlled,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const [internalRawValue, setInternalValue] = useState(value ?? '');
|
||||||
|
const internalValue = useMemo(
|
||||||
|
() => (controlled || duplicate ? value ?? '' : internalRawValue),
|
||||||
|
[controlled, duplicate, value, internalRawValue],
|
||||||
|
);
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newUUID: string) => {
|
||||||
|
setInternalValue(newUUID);
|
||||||
|
onChange(newUUID);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateUUID = useCallback(() => {
|
||||||
|
handleChange(uuid());
|
||||||
|
}, [handleChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmpty(internalValue) || !validate(internalValue)) {
|
||||||
|
generateUUID();
|
||||||
|
}
|
||||||
|
}, [generateUUID, internalValue]);
|
||||||
|
|
||||||
|
const allowRegenerate = useMemo(() => field.allow_regenerate ?? true, [field.allow_regenerate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
inputRef={ref}
|
||||||
|
label={label}
|
||||||
|
errors={errors}
|
||||||
|
hint={field.hint}
|
||||||
|
forSingleList={forSingleList}
|
||||||
|
cursor="text"
|
||||||
|
disabled={disabled}
|
||||||
|
endAdornment={
|
||||||
|
allowRegenerate ? (
|
||||||
|
<IconButton
|
||||||
|
data-testid="generate-new-uuid"
|
||||||
|
title="Generate new UUID"
|
||||||
|
aria-label="generate new uuid"
|
||||||
|
onClick={generateUUID}
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<RefreshIcon className="w-5 h-5" />
|
||||||
|
</IconButton>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextField type="text" inputRef={ref} value={internalValue} disabled={disabled} readonly />
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UUIDControl;
|
10
packages/core/src/widgets/uuid/UUIDPreview.tsx
Normal file
10
packages/core/src/widgets/uuid/UUIDPreview.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type { UUIDField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
const StringPreview: FC<WidgetPreviewProps<string, UUIDField>> = ({ value = '' }) => {
|
||||||
|
return <div>{value}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StringPreview;
|
192
packages/core/src/widgets/uuid/__tests__/UUIDControl.spec.ts
Normal file
192
packages/core/src/widgets/uuid/__tests__/UUIDControl.spec.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { act } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { v4, validate } from 'uuid';
|
||||||
|
|
||||||
|
import { mockUUIDField } from '@staticcms/test/data/fields.mock';
|
||||||
|
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||||
|
import UUIDControl from '../UUIDControl';
|
||||||
|
|
||||||
|
describe(UUIDControl.name, () => {
|
||||||
|
const renderControl = createWidgetControlHarness(UUIDControl, { field: mockUUIDField });
|
||||||
|
|
||||||
|
const mockValidate = validate as jest.Mock;
|
||||||
|
const mockUUID = v4 as jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockValidate.mockReturnValue(true);
|
||||||
|
mockUUID.mockReturnValue('I_AM_A_NEW_UUID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render', () => {
|
||||||
|
const { getByTestId } = renderControl({ label: 'I am a label' });
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).toBeInTheDocument();
|
||||||
|
expect(input).toHaveAttribute('readonly');
|
||||||
|
|
||||||
|
const label = getByTestId('label');
|
||||||
|
expect(label.textContent).toBe('I am a label');
|
||||||
|
expect(label).toHaveClass('text-slate-500');
|
||||||
|
|
||||||
|
const field = getByTestId('field');
|
||||||
|
expect(field).toHaveClass('group/active');
|
||||||
|
|
||||||
|
const fieldWrapper = getByTestId('field-wrapper');
|
||||||
|
expect(fieldWrapper).not.toHaveClass('mr-14');
|
||||||
|
|
||||||
|
// String Widget uses text cursor
|
||||||
|
expect(label).toHaveClass('cursor-text');
|
||||||
|
expect(field).toHaveClass('cursor-text');
|
||||||
|
|
||||||
|
// String Widget uses default label layout, with bottom padding on field
|
||||||
|
expect(label).toHaveClass('px-3', 'pt-3');
|
||||||
|
expect(field).toHaveClass('pb-3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render as single list item', () => {
|
||||||
|
const { getByTestId } = renderControl({ label: 'I am a label', forSingleList: true });
|
||||||
|
|
||||||
|
expect(getByTestId('text-input')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const fieldWrapper = getByTestId('field-wrapper');
|
||||||
|
expect(fieldWrapper).toHaveClass('mr-14');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only use prop value as initial value', async () => {
|
||||||
|
const { rerender, getByTestId } = renderControl({ value: 'I_AM_A_VALID_UUID' });
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).toHaveValue('I_AM_A_VALID_UUID');
|
||||||
|
|
||||||
|
rerender({ value: 'i am a new value' });
|
||||||
|
expect(input).toHaveValue('I_AM_A_VALID_UUID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use prop value exclusively if field is i18n duplicate', async () => {
|
||||||
|
const { rerender, getByTestId } = renderControl({
|
||||||
|
field: { ...mockUUIDField, i18n: 'duplicate' },
|
||||||
|
duplicate: true,
|
||||||
|
value: 'I_AM_A_VALID_UUID',
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).toHaveValue('I_AM_A_VALID_UUID');
|
||||||
|
|
||||||
|
rerender({ value: 'I_AM_ANOTHER_VALID_UUID' });
|
||||||
|
expect(input).toHaveValue('I_AM_ANOTHER_VALID_UUID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a new UUID if provided on is invalid', async () => {
|
||||||
|
mockValidate.mockReturnValue(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getByTestId,
|
||||||
|
props: { onChange },
|
||||||
|
} = renderControl({ value: 'I_AM_AN_INVALID_UUID' });
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).toHaveValue('I_AM_A_NEW_UUID');
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith('I_AM_A_NEW_UUID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a new UUID if none is provided', async () => {
|
||||||
|
const {
|
||||||
|
getByTestId,
|
||||||
|
props: { onChange },
|
||||||
|
} = renderControl();
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).toHaveValue('I_AM_A_NEW_UUID');
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith('I_AM_A_NEW_UUID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows generate new uuid button by default', async () => {
|
||||||
|
const { queryByTestId } = renderControl();
|
||||||
|
|
||||||
|
expect(queryByTestId('generate-new-uuid')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides generate new uuid button if allow_regenerate is false', async () => {
|
||||||
|
const { queryByTestId } = renderControl({
|
||||||
|
field: {
|
||||||
|
...mockUUIDField,
|
||||||
|
allow_regenerate: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByTestId('generate-new-uuid')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a new UUID when generate new uuid button is clicked', async () => {
|
||||||
|
const {
|
||||||
|
getByTestId,
|
||||||
|
props: { onChange },
|
||||||
|
} = renderControl();
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).toHaveValue('I_AM_A_NEW_UUID');
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith('I_AM_A_NEW_UUID');
|
||||||
|
|
||||||
|
mockUUID.mockReturnValue('I_AM_ANOTHER_NEW_UUID');
|
||||||
|
const generateNewUUIDButton = getByTestId('generate-new-uuid');
|
||||||
|
|
||||||
|
// No change yet
|
||||||
|
expect(input).toHaveValue('I_AM_A_NEW_UUID');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(generateNewUUIDButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(input).toHaveValue('I_AM_ANOTHER_NEW_UUID');
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith('I_AM_ANOTHER_NEW_UUID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error', async () => {
|
||||||
|
const { getByTestId } = renderControl({
|
||||||
|
errors: [{ type: 'error-type', message: 'i am an error' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = getByTestId('error');
|
||||||
|
expect(error.textContent).toBe('i am an error');
|
||||||
|
|
||||||
|
const field = getByTestId('field');
|
||||||
|
expect(field).not.toHaveClass('group/active');
|
||||||
|
|
||||||
|
const label = getByTestId('label');
|
||||||
|
expect(label).toHaveClass('text-red-500');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus input on field click', async () => {
|
||||||
|
const { getByTestId } = renderControl();
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).not.toHaveFocus();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const field = getByTestId('field');
|
||||||
|
await userEvent.click(field);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(input).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable input if disabled', async () => {
|
||||||
|
const { getByTestId } = renderControl({ disabled: true });
|
||||||
|
|
||||||
|
const inputWrapper = getByTestId('text-input');
|
||||||
|
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||||
|
expect(input).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
20
packages/core/src/widgets/uuid/index.ts
Normal file
20
packages/core/src/widgets/uuid/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import schema from './schema';
|
||||||
|
import controlComponent from './UUIDControl';
|
||||||
|
import previewComponent from './UUIDPreview';
|
||||||
|
|
||||||
|
import type { UUIDField, WidgetParam } from '@staticcms/core/interface';
|
||||||
|
|
||||||
|
const UUIDWidget = (): WidgetParam<string, UUIDField> => {
|
||||||
|
return {
|
||||||
|
name: 'uuid',
|
||||||
|
controlComponent,
|
||||||
|
previewComponent,
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { controlComponent as UUIDControl, previewComponent as UUIDPreview, schema as uuidSchema };
|
||||||
|
|
||||||
|
export default UUIDWidget;
|
5
packages/core/src/widgets/uuid/schema.ts
Normal file
5
packages/core/src/widgets/uuid/schema.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
properties: {
|
||||||
|
allow_regenerate: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
@ -8,6 +8,7 @@ import type {
|
|||||||
RelationField,
|
RelationField,
|
||||||
SelectField,
|
SelectField,
|
||||||
StringOrTextField,
|
StringOrTextField,
|
||||||
|
UUIDField,
|
||||||
} from '@staticcms/core';
|
} from '@staticcms/core';
|
||||||
|
|
||||||
export const mockBooleanField: BooleanField = {
|
export const mockBooleanField: BooleanField = {
|
||||||
@ -89,3 +90,9 @@ export const mockTextField: StringOrTextField = {
|
|||||||
name: 'mock_text',
|
name: 'mock_text',
|
||||||
widget: 'text',
|
widget: 'text',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mockUUIDField: UUIDField = {
|
||||||
|
label: 'UUID',
|
||||||
|
name: 'mock_uuid',
|
||||||
|
widget: 'uuid',
|
||||||
|
};
|
||||||
|
@ -190,7 +190,8 @@ collections:
|
|||||||
- name: pattern
|
- name: pattern
|
||||||
label: Pattern Validation
|
label: Pattern Validation
|
||||||
widget: color
|
widget: color
|
||||||
pattern: ['^#[a-fA-F0-9]{3}$|^[a-fA-F0-9]{4}$|^[a-fA-F0-9]{6}$', 'Must be a valid hex code']
|
pattern:
|
||||||
|
['^#[a-fA-F0-9]{3}$|^[a-fA-F0-9]{4}$|^[a-fA-F0-9]{6}$', 'Must be a valid hex code']
|
||||||
allow_input: true
|
allow_input: true
|
||||||
required: false
|
required: false
|
||||||
- name: alpha
|
- name: alpha
|
||||||
@ -851,6 +852,18 @@ collections:
|
|||||||
widget: text
|
widget: text
|
||||||
pattern: ['.{12,}', 'Must have at least 12 characters']
|
pattern: ['.{12,}', 'Must have at least 12 characters']
|
||||||
required: false
|
required: false
|
||||||
|
- name: uuid
|
||||||
|
label: UUID
|
||||||
|
file: _widgets/uuid.json
|
||||||
|
description: UUID widget
|
||||||
|
fields:
|
||||||
|
- name: uuid
|
||||||
|
label: UUID
|
||||||
|
widget: uuid
|
||||||
|
- name: no_regenerate
|
||||||
|
label: Does not allow regeneration
|
||||||
|
widget: uuid
|
||||||
|
allow_regenerate: false
|
||||||
- name: settings
|
- name: settings
|
||||||
label: Settings
|
label: Settings
|
||||||
delete: false
|
delete: false
|
||||||
|
36
packages/docs/content/docs/widget-uuid.mdx
Normal file
36
packages/docs/content/docs/widget-uuid.mdx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
group: Widgets
|
||||||
|
title: Text
|
||||||
|
weight: 25
|
||||||
|
---
|
||||||
|
|
||||||
|
- **Name:** `uuid`
|
||||||
|
- **UI:** Text input
|
||||||
|
- **Data type:** `string`
|
||||||
|
|
||||||
|
The uuid widget generates a unique id (uuid) and saves it as a string.
|
||||||
|
|
||||||
|
## Widget Options
|
||||||
|
|
||||||
|
For common options, see [Common widget options](/docs/widgets#common-widget-options).
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| ---------------- | ------- | ------- | -------------------------------------------------------------------------------------- |
|
||||||
|
| allow_regenerate | boolean | `true` | _Optional_. If set to `false` the `Generate new UUID` button does not appear in the UI |
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<CodeTabs>
|
||||||
|
```yaml
|
||||||
|
name: id
|
||||||
|
label: Id
|
||||||
|
widget: uuid
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
name: 'id',
|
||||||
|
label: 'Id',
|
||||||
|
widget: 'uuid',
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeTabs>
|
@ -30,21 +30,22 @@ To see working examples of all of the built-in widgets, try making a 'Kitchen Si
|
|||||||
| [Select](/docs/widget-select) | The select widget allows you to pick a string value from a dropdown menu |
|
| [Select](/docs/widget-select) | The select widget allows you to pick a string value from a dropdown menu |
|
||||||
| [String](/docs/widget-string) | The string widget translates a basic text input to a string value |
|
| [String](/docs/widget-string) | The string widget translates a basic text input to a string value |
|
||||||
| [Text](/docs/widget-text) | The text widget takes a multiline text field and saves it as a string |
|
| [Text](/docs/widget-text) | The text widget takes a multiline text field and saves it as a string |
|
||||||
|
| [Text](/docs/widget-text) | The uuid widget generates a unique id (uuid) and saves it as a string |
|
||||||
|
|
||||||
## Common widget options
|
## Common widget options
|
||||||
|
|
||||||
The following options are available on all fields:
|
The following options are available on all fields:
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| -------- | ----------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------- | ----------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| name | string | | The name of the field |
|
| name | string | | The name of the field |
|
||||||
| widget | string | `'string'` | _Optional_. The type of widget to render for the field |
|
| widget | string | `'string'` | _Optional_. The type of widget to render for the field |
|
||||||
| label | string | `name` | _Optional_. The display name of the field |
|
| label | string | `name` | _Optional_. The display name of the field |
|
||||||
| required | boolean | `true` | _Optional_. Specify as `false` to make a field optional |
|
| required | boolean | `true` | _Optional_. Specify as `false` to make a field optional |
|
||||||
| hint | string | | _Optional_. Adds helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links. |
|
| hint | string | | _Optional_. Adds helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links. |
|
||||||
| pattern | list of strings | | _Optional_. Adds field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) |
|
| pattern | list of strings | | _Optional_. Adds field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) |
|
||||||
| i18n | boolean<br />\|'translate'<br />\|'duplicate'<br />\|'none' | | _Optional_. <BetaImage /><ul><li>`translate` - Allows translation of the field</li><li>`duplicate` - Duplicates the value from the default locale</li><li>`true` - Accept parent values as default</li><li>`none` or `false` - Exclude field from translations</li></ul> |
|
| i18n | boolean<br />\|'translate'<br />\|'duplicate'<br />\|'none' | | _Optional_. <BetaImage /><ul><li>`translate` - Allows translation of the field</li><li>`duplicate` - Duplicates the value from the default locale</li><li>`true` - Accept parent values as default</li><li>`none` or `false` - Exclude field from translations</li></ul> |
|
||||||
| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) |
|
| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) |
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user