mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 02:44:55 +00:00
Merge branch 'main' into tests/drop16
This commit is contained in:
commit
af6ddbba9b
3
.github/actions/run-api-tests/action.yml
vendored
3
.github/actions/run-api-tests/action.yml
vendored
@ -6,6 +6,8 @@ inputs:
|
||||
required: true
|
||||
runEE:
|
||||
description: 'Should run EE or CE tests'
|
||||
jestOptions:
|
||||
description: 'Jest options'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@ -13,4 +15,5 @@ runs:
|
||||
env:
|
||||
DB_OPTIONS: ${{ inputs.dbOptions }}
|
||||
RUN_EE: ${{ inputs.runEE }}
|
||||
JEST_OPTIONS: ${{ inputs.jestOptions }}
|
||||
shell: bash
|
||||
|
||||
3
.github/actions/run-api-tests/script.sh
vendored
3
.github/actions/run-api-tests/script.sh
vendored
@ -9,7 +9,8 @@ export ENV_PATH="$(pwd)/test-apps/api/.env"
|
||||
export JWT_SECRET="aSecret"
|
||||
|
||||
opts=($DB_OPTIONS)
|
||||
jestOptions=($JEST_OPTIONS)
|
||||
|
||||
yarn nx run-many --target=build --nx-ignore-cycles --skip-nx-cache
|
||||
yarn run test:generate-app --appPath=test-apps/api "${opts[@]}"
|
||||
yarn run test:api --no-generate-app
|
||||
yarn run test:api --no-generate-app "${jestOptions[@]}"
|
||||
|
||||
28
.github/workflows/tests.yml
vendored
28
.github/workflows/tests.yml
vendored
@ -181,10 +181,11 @@ jobs:
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, lint, typescript, unit_back, unit_front]
|
||||
name: '[CE] API Integration (postgres, node: ${{ matrix.node }})'
|
||||
name: '[CE] API Integration (postgres, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
shard: [1/2, 2/2]
|
||||
services:
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
@ -214,16 +215,18 @@ jobs:
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
jestOptions: '--shard=${{ matrix.shard }}'
|
||||
|
||||
api_ce_mysql:
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, lint, typescript, unit_back, unit_front]
|
||||
name: '[CE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }})'
|
||||
name: '[CE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
db_client: ['mysql', 'mysql2']
|
||||
shard: [1/2, 2/2]
|
||||
services:
|
||||
mysql:
|
||||
image: bitnami/mysql:latest
|
||||
@ -251,16 +254,18 @@ jobs:
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
jestOptions: '--shard=${{ matrix.shard }}'
|
||||
|
||||
api_ce_mysql_5:
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, lint, typescript, unit_back, unit_front]
|
||||
name: '[CE] API Integration (mysql:5, client: ${{ matrix.db_client }} , node: ${{ matrix.node }})'
|
||||
name: '[CE] API Integration (mysql:5, client: ${{ matrix.db_client }} , node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
db_client: ['mysql', 'mysql2']
|
||||
shard: [1/2, 2/2]
|
||||
services:
|
||||
mysql:
|
||||
image: bitnami/mysql:5.7
|
||||
@ -287,16 +292,18 @@ jobs:
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
jestOptions: '--shard=${{ matrix.shard }}'
|
||||
|
||||
api_ce_sqlite:
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, lint, typescript, unit_back, unit_front]
|
||||
name: '[CE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})'
|
||||
name: '[CE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
sqlite_pkg: ['better-sqlite3', 'sqlite3']
|
||||
shard: [1/2, 2/2]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
@ -309,18 +316,20 @@ jobs:
|
||||
SQLITE_PKG: ${{ matrix.sqlite_pkg }}
|
||||
with:
|
||||
dbOptions: '--dbclient=sqlite-legacy --dbfile=./tmp/data.db'
|
||||
jestOptions: '--shard=${{ matrix.shard }}'
|
||||
|
||||
# EE
|
||||
api_ee_pg:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, lint, typescript, unit_back, unit_front]
|
||||
name: '[EE] API Integration (postgres, node: ${{ matrix.node }})'
|
||||
name: '[EE] API Integration (postgres, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
|
||||
if: needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
|
||||
env:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
shard: [1/2, 2/2]
|
||||
services:
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
@ -351,11 +360,12 @@ jobs:
|
||||
with:
|
||||
dbOptions: '--dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
runEE: true
|
||||
jestOptions: '--shard=${{ matrix.shard }}'
|
||||
|
||||
api_ee_mysql:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, lint, typescript, unit_back, unit_front]
|
||||
name: '[EE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }})'
|
||||
name: '[EE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
|
||||
if: needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
|
||||
env:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
@ -363,6 +373,7 @@ jobs:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
db_client: ['mysql', 'mysql2']
|
||||
shard: [1/2, 2/2]
|
||||
services:
|
||||
mysql:
|
||||
image: bitnami/mysql:latest
|
||||
@ -391,11 +402,12 @@ jobs:
|
||||
with:
|
||||
dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
runEE: true
|
||||
jestOptions: '--shard=${{ matrix.shard }}'
|
||||
|
||||
api_ee_sqlite:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, lint, typescript, unit_back, unit_front]
|
||||
name: '[EE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})'
|
||||
name: '[EE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
|
||||
if: needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
|
||||
env:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
@ -403,6 +415,7 @@ jobs:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
sqlite_pkg: ['better-sqlite3', 'sqlite3']
|
||||
shard: [1/2, 2/2]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
@ -416,3 +429,4 @@ jobs:
|
||||
with:
|
||||
dbOptions: '--dbclient=sqlite --dbfile=./tmp/data.db'
|
||||
runEE: true
|
||||
jestOptions: '--shard=${{ matrix.shard }}'
|
||||
|
||||
@ -199,6 +199,8 @@ const ImageDialog = ({ handleClose }) => {
|
||||
const MediaLibraryDialog = components['media-library'];
|
||||
|
||||
const insertImages = (images) => {
|
||||
// Image node created using select or existing selection node needs to be deleted before adding new image nodes
|
||||
Transforms.removeNodes(editor);
|
||||
images.forEach((img) => {
|
||||
const image = { type: 'image', image: img, children: [{ type: 'text', text: '' }] };
|
||||
Transforms.insertNodes(editor, image);
|
||||
@ -290,10 +292,7 @@ const BlocksDropdown = ({ disabled }) => {
|
||||
* @param {string} optionKey - key of the heading selected
|
||||
*/
|
||||
const selectOption = (optionKey) => {
|
||||
if (optionKey === 'image') {
|
||||
// Image node created using select or existing selection node needs to be deleted before adding new image nodes
|
||||
Transforms.removeNodes(editor);
|
||||
} else if (['list-ordered', 'list-unordered'].includes(optionKey)) {
|
||||
if (['list-ordered', 'list-unordered'].includes(optionKey)) {
|
||||
// retrieve the list format
|
||||
const listFormat = blocks[optionKey].value.format;
|
||||
|
||||
@ -302,7 +301,7 @@ const BlocksDropdown = ({ disabled }) => {
|
||||
|
||||
// toggle the list
|
||||
toggleList(editor, isActive, listFormat);
|
||||
} else {
|
||||
} else if (optionKey !== 'image') {
|
||||
toggleBlock(editor, blocks[optionKey].value);
|
||||
}
|
||||
|
||||
@ -507,6 +506,31 @@ const LinkButton = ({ disabled }) => {
|
||||
return Boolean(match);
|
||||
};
|
||||
|
||||
const isLinkDisabled = () => {
|
||||
// Always disabled when the whole editor is disabled
|
||||
if (disabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Always enabled when there's no selection
|
||||
if (!editor.selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the block node closest to the anchor and focus
|
||||
const anchorNodeEntry = Editor.above(editor, {
|
||||
at: editor.selection.anchor,
|
||||
match: (node) => node.type !== 'text',
|
||||
});
|
||||
const focusNodeEntry = Editor.above(editor, {
|
||||
at: editor.selection.focus,
|
||||
match: (node) => node.type !== 'text',
|
||||
});
|
||||
|
||||
// Disabled if the anchor and focus are not in the same block
|
||||
return anchorNodeEntry[0] !== focusNodeEntry[0];
|
||||
};
|
||||
|
||||
const addLink = () => {
|
||||
// We insert an empty anchor, so we split the DOM to have a element we can use as reference for the popover
|
||||
insertLink(editor, { url: '' });
|
||||
@ -522,7 +546,7 @@ const LinkButton = ({ disabled }) => {
|
||||
}}
|
||||
isActive={isLinkActive()}
|
||||
handleClick={addLink}
|
||||
disabled={disabled}
|
||||
disabled={isLinkDisabled()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -32,7 +32,10 @@ const mixedInitialValue = [
|
||||
{
|
||||
type: 'heading',
|
||||
level: 1,
|
||||
children: [{ type: 'text', text: 'A heading one' }],
|
||||
children: [
|
||||
{ type: 'text', text: 'A heading one' },
|
||||
{ type: 'text', text: ' with modifiers', bold: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
@ -433,4 +436,25 @@ describe('BlocksEditor toolbar', () => {
|
||||
const blocksDropdown = screen.getByRole('combobox', { name: /Select a block/i });
|
||||
expect(within(blocksDropdown).getByText(/heading 1/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable the link button when multiple blocks are selected', async () => {
|
||||
setup(mixedInitialValue);
|
||||
|
||||
// Set the selection to cover the first and second
|
||||
await select({
|
||||
anchor: { path: [0, 0], offset: 0 },
|
||||
focus: { path: [1, 0], offset: 0 },
|
||||
});
|
||||
|
||||
const linkButton = screen.getByLabelText(/link/i);
|
||||
expect(linkButton).toBeDisabled();
|
||||
|
||||
// Set the selection to a range inside the same block node
|
||||
await select({
|
||||
anchor: { path: [0, 0], offset: 0 },
|
||||
focus: { path: [0, 1], offset: 2 },
|
||||
});
|
||||
|
||||
expect(linkButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -221,7 +221,7 @@ describe('useBlocksStore', () => {
|
||||
const { result } = renderHook(useBlocksStore, { wrapper: Wrapper });
|
||||
|
||||
render(
|
||||
result.current['heading-six'].renderElement({
|
||||
result.current['heading-two'].renderElement({
|
||||
children: 'Some heading',
|
||||
element: { level: 2 },
|
||||
attributes: {},
|
||||
|
||||
@ -37,6 +37,10 @@ import styled, { css } from 'styled-components';
|
||||
import { composeRefs } from '../../../utils';
|
||||
import { editLink, removeLink } from '../utils/links';
|
||||
|
||||
const StyledBaseLink = styled(BaseLink)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
const H1 = styled(Typography).attrs({ as: 'h1' })`
|
||||
font-size: ${42 / 16}rem;
|
||||
line-height: ${({ theme }) => theme.lineHeights[1]};
|
||||
@ -67,33 +71,6 @@ const H6 = styled(Typography).attrs({ as: 'h6' })`
|
||||
line-height: ${({ theme }) => theme.lineHeights[1]};
|
||||
`;
|
||||
|
||||
const Heading = ({ attributes, children, element }) => {
|
||||
switch (element.level) {
|
||||
case 1:
|
||||
return <H1 {...attributes}>{children}</H1>;
|
||||
case 2:
|
||||
return <H2 {...attributes}>{children}</H2>;
|
||||
case 3:
|
||||
return <H3 {...attributes}>{children}</H3>;
|
||||
case 4:
|
||||
return <H4 {...attributes}>{children}</H4>;
|
||||
case 5:
|
||||
return <H5 {...attributes}>{children}</H5>;
|
||||
case 6:
|
||||
return <H6 {...attributes}>{children}</H6>;
|
||||
default: // do nothing
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Heading.propTypes = {
|
||||
attributes: PropTypes.object.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
element: PropTypes.shape({
|
||||
level: PropTypes.oneOf([1, 2, 3, 4, 5, 6]).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const CodeBlock = styled.pre.attrs({ role: 'code' })`
|
||||
border-radius: ${({ theme }) => theme.borderRadius};
|
||||
background-color: ${({ theme }) => theme.colors.neutral100};
|
||||
@ -113,8 +90,9 @@ const CodeBlock = styled.pre.attrs({ role: 'code' })`
|
||||
const Blockquote = styled.blockquote.attrs({ role: 'blockquote' })`
|
||||
margin: ${({ theme }) => `${theme.spaces[4]} 0`};
|
||||
font-weight: ${({ theme }) => theme.fontWeights.regular};
|
||||
border-left: ${({ theme }) => `${theme.spaces[1]} solid ${theme.colors.neutral150}`};
|
||||
border-left: ${({ theme }) => `${theme.spaces[1]} solid ${theme.colors.neutral200}`};
|
||||
padding: ${({ theme }) => theme.spaces[2]} ${({ theme }) => theme.spaces[5]};
|
||||
color: ${({ theme }) => theme.colors.neutral600};
|
||||
`;
|
||||
|
||||
const listStyle = css`
|
||||
@ -320,7 +298,7 @@ const Link = React.forwardRef(({ element, children, ...attributes }, forwardedRe
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseLink
|
||||
<StyledBaseLink
|
||||
{...attributes}
|
||||
ref={composedRefs}
|
||||
href={element.url}
|
||||
@ -328,7 +306,7 @@ const Link = React.forwardRef(({ element, children, ...attributes }, forwardedRe
|
||||
color="primary600"
|
||||
>
|
||||
{children}
|
||||
</BaseLink>
|
||||
</StyledBaseLink>
|
||||
{popoverOpen && (
|
||||
<Popover source={linkRef} onDismiss={handleDismiss} padding={4} contentEditable={false}>
|
||||
{isEditing ? (
|
||||
@ -382,9 +360,11 @@ const Link = React.forwardRef(({ element, children, ...attributes }, forwardedRe
|
||||
) : (
|
||||
<Flex direction="column" gap={4} alignItems="start" width="400px">
|
||||
<Typography>{elementText}</Typography>
|
||||
<BaseLink href={element.url} target="_blank" color="primary600">
|
||||
{element.url}
|
||||
</BaseLink>
|
||||
<Typography>
|
||||
<StyledBaseLink href={element.url} target="_blank" color="primary600">
|
||||
{element.url}
|
||||
</StyledBaseLink>
|
||||
</Typography>
|
||||
<Flex justifyContent="end" width="100%" gap={2}>
|
||||
<IconButton
|
||||
icon={<Trash />}
|
||||
@ -503,7 +483,7 @@ export function useBlocksStore() {
|
||||
},
|
||||
},
|
||||
'heading-one': {
|
||||
renderElement: (props) => <Heading {...props} />,
|
||||
renderElement: (props) => <H1 {...props.attributes}>{props.children}</H1>,
|
||||
icon: HeadingOne,
|
||||
label: {
|
||||
id: 'components.Blocks.blocks.heading1',
|
||||
@ -517,7 +497,7 @@ export function useBlocksStore() {
|
||||
isInBlocksSelector: true,
|
||||
},
|
||||
'heading-two': {
|
||||
renderElement: (props) => <Heading {...props} />,
|
||||
renderElement: (props) => <H2 {...props.attributes}>{props.children}</H2>,
|
||||
icon: HeadingTwo,
|
||||
label: {
|
||||
id: 'components.Blocks.blocks.heading2',
|
||||
@ -531,7 +511,7 @@ export function useBlocksStore() {
|
||||
isInBlocksSelector: true,
|
||||
},
|
||||
'heading-three': {
|
||||
renderElement: (props) => <Heading {...props} />,
|
||||
renderElement: (props) => <H3 {...props.attributes}>{props.children}</H3>,
|
||||
icon: HeadingThree,
|
||||
label: {
|
||||
id: 'components.Blocks.blocks.heading3',
|
||||
@ -545,7 +525,7 @@ export function useBlocksStore() {
|
||||
isInBlocksSelector: true,
|
||||
},
|
||||
'heading-four': {
|
||||
renderElement: (props) => <Heading {...props} />,
|
||||
renderElement: (props) => <H4 {...props.attributes}>{props.children}</H4>,
|
||||
icon: HeadingFour,
|
||||
label: {
|
||||
id: 'components.Blocks.blocks.heading4',
|
||||
@ -559,7 +539,7 @@ export function useBlocksStore() {
|
||||
isInBlocksSelector: true,
|
||||
},
|
||||
'heading-five': {
|
||||
renderElement: (props) => <Heading {...props} />,
|
||||
renderElement: (props) => <H5 {...props.attributes}>{props.children}</H5>,
|
||||
icon: HeadingFive,
|
||||
label: {
|
||||
id: 'components.Blocks.blocks.heading5',
|
||||
@ -573,7 +553,7 @@ export function useBlocksStore() {
|
||||
isInBlocksSelector: true,
|
||||
},
|
||||
'heading-six': {
|
||||
renderElement: (props) => <Heading {...props} />,
|
||||
renderElement: (props) => <H6 {...props.attributes}>{props.children}</H6>,
|
||||
icon: HeadingSix,
|
||||
label: {
|
||||
id: 'components.Blocks.blocks.heading6',
|
||||
|
||||
@ -133,7 +133,7 @@ const Notification = ({
|
||||
},
|
||||
onClose,
|
||||
timeout = 2500,
|
||||
title = 'success',
|
||||
title,
|
||||
type,
|
||||
}: NotificationProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user