{t('locales')}
@@ -35,27 +38,27 @@ const Localizer: React.FC = () => {
const localeClasses = [
baseLocaleClass,
- locale === localeOption && `${baseLocaleClass}--active`,
+ locale.code === localeOption.code && `${baseLocaleClass}--active`,
].filter(Boolean).join('');
const newParams = {
...searchParams,
- locale: localeOption,
+ locale: localeOption.code,
};
const search = qs.stringify(newParams);
- if (localeOption !== locale) {
+ if (localeOption.code !== locale.code) {
return (
- {localeOption}
+ {localeOption.label}
);
diff --git a/src/admin/components/elements/Logout/index.tsx b/src/admin/components/elements/Logout/index.tsx
index 4a92fbae9a..3478b49ca3 100644
--- a/src/admin/components/elements/Logout/index.tsx
+++ b/src/admin/components/elements/Logout/index.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import LogOut from '../../icons/LogOut';
@@ -7,16 +8,21 @@ import LogOut from '../../icons/LogOut';
const baseClass = 'nav';
const DefaultLogout = () => {
+ const { t } = useTranslation('authentication');
const config = useConfig();
const {
routes: { admin },
admin: {
logoutRoute,
- components: { logout }
- }
+ components: { logout },
+ },
} = config;
return (
-
+
);
diff --git a/src/admin/components/elements/Nav/index.scss b/src/admin/components/elements/Nav/index.scss
index e125b5c26b..64738244c3 100644
--- a/src/admin/components/elements/Nav/index.scss
+++ b/src/admin/components/elements/Nav/index.scss
@@ -8,7 +8,12 @@
height: 100vh;
width: var(--nav-width);
overflow: hidden;
- border-right: 1px solid var(--theme-elevation-100);
+ [dir=ltr] & {
+ border-right: 1px solid var(--theme-elevation-100);
+ }
+ [dir=rtl] & {
+ border-left: 1px solid var(--theme-elevation-100);
+ }
header {
width: 100%;
@@ -27,7 +32,12 @@
}
&__brand {
- margin-right: base(1);
+ [dir=ltr] & {
+ margin-right: base(1);
+ }
+ [dir=rtl] & {
+ margin-left: base(1);
+ }
}
&__mobile-menu-btn {
@@ -91,6 +101,9 @@
padding: base(.125) base(1.5) base(.125) 0;
display: flex;
text-decoration: none;
+ [dir=rtl] & {
+ padding: base(.125) 0 base(.125) base(1.5);
+ }
&:focus:not(:focus-visible) {
box-shadow: none;
@@ -103,8 +116,13 @@
&.active {
font-weight: normal;
- padding-left: base(.6);
font-weight: 600;
+ [dir=ltr] & {
+ padding-left: base(.6);
+ }
+ [dir=rtl] & {
+ padding-right: base(.6);
+ }
}
}
}
@@ -113,8 +131,14 @@
svg {
opacity: 0;
position: absolute;
- left: - base(.5);
- transform: rotate(-90deg);
+ [dir=ltr] & {
+ left: - base(.5);
+ transform: rotate(-90deg);
+ }
+ [dir=rtl] & {
+ right: - base(.5);
+ transform: rotate(90deg);
+ }
}
&.active {
diff --git a/src/admin/components/elements/Nav/index.tsx b/src/admin/components/elements/Nav/index.tsx
index f2540331d1..833d4833da 100644
--- a/src/admin/components/elements/Nav/index.tsx
+++ b/src/admin/components/elements/Nav/index.tsx
@@ -24,7 +24,7 @@ const DefaultNav = () => {
const [menuActive, setMenuActive] = useState(false);
const [groups, setGroups] = useState
([]);
const history = useHistory();
- const { i18n } = useTranslation('general');
+ const { t, i18n } = useTranslation('general');
const {
collections,
globals,
@@ -81,6 +81,7 @@ const DefaultNav = () => {
@@ -141,6 +142,7 @@ const DefaultNav = () => {
diff --git a/src/admin/components/elements/NavGroup/index.scss b/src/admin/components/elements/NavGroup/index.scss
index 45445a0423..c522fc1dd8 100644
--- a/src/admin/components/elements/NavGroup/index.scss
+++ b/src/admin/components/elements/NavGroup/index.scss
@@ -8,14 +8,22 @@
cursor: pointer;
color: var(--theme-elevation-400);
background: transparent;
- padding-left: 0;
border: 0;
margin-top: base(.25);
width: 100%;
- text-align: left;
display: flex;
- align-items: flex-start;
- padding-right: base(.5);
+ [dir=ltr] & {
+ padding-right: base(.5);
+ padding-left: 0;
+ align-items: flex-start;
+ text-align: left;
+ }
+ [dir=rtl] & {
+ padding-left: base(.5);
+ padding-right: 0;
+ align-items: flex-start;
+ text-align: right;
+ }
svg {
flex-shrink: 0;
@@ -36,7 +44,12 @@
}
&__indicator {
- margin-left: auto;
+ [dir=ltr] & {
+ margin-left: auto;
+ }
+ [dir=rtl] & {
+ margin-right: auto;
+ }
.stroke {
stroke: var(--theme-elevation-200);
diff --git a/src/admin/components/elements/Pill/index.scss b/src/admin/components/elements/Pill/index.scss
index 86c90b33c8..74363b9013 100644
--- a/src/admin/components/elements/Pill/index.scss
+++ b/src/admin/components/elements/Pill/index.scss
@@ -47,6 +47,16 @@
}
}
+ [dir=rtl] &--align-icon-left {
+ padding-right: 0;
+ padding-left: 10px;
+ }
+
+ [dir=rtl] &--align-icon-right {
+ padding-right: 10px;
+ padding-left: 0;
+ }
+
&--align-icon-left {
padding-left: 0;
}
diff --git a/src/admin/components/elements/Pill/index.tsx b/src/admin/components/elements/Pill/index.tsx
index 83210a60a0..2d8cbda8a2 100644
--- a/src/admin/components/elements/Pill/index.tsx
+++ b/src/admin/components/elements/Pill/index.tsx
@@ -45,6 +45,10 @@ const StaticPill: React.FC = (props) => {
children,
elementProps,
rounded,
+ 'aria-label': ariaLabel,
+ 'aria-expanded': ariaExpanded,
+ 'aria-controls': ariaControls,
+ 'aria-checked': ariaChecked,
} = props;
const classes = [
@@ -67,6 +71,10 @@ const StaticPill: React.FC = (props) => {
return (
& {
ref: React.RefCallback
}
diff --git a/src/admin/components/elements/Popup/index.scss b/src/admin/components/elements/Popup/index.scss
index f91ffd360c..c680d9b8b8 100644
--- a/src/admin/components/elements/Popup/index.scss
+++ b/src/admin/components/elements/Popup/index.scss
@@ -51,7 +51,12 @@
&--size-small {
.popup__scroll {
- padding: base(.75) calc(var(--scrollbar-width) + #{base(.75)}) base(.75) base(.75);
+ [dir=ltr] & {
+ padding: base(.75) calc(var(--scrollbar-width) + #{base(.75)}) base(.75) base(.75);
+ }
+ [dir=rtl] & {
+ padding: base(.75) base(.75) base(.75) calc(var(--scrollbar-width) + #{base(.75)});
+ }
}
.popup__content {
@@ -133,9 +138,14 @@
&--h-align-right {
.popup__content {
right: - base(1.75);
-
+ [dir=rtl] & {
+ right: - base(0.75);
+ }
&:after {
right: base(1.75);
+ [dir=rtl] & {
+ right: base(0.75);
+ }
}
}
}
diff --git a/src/admin/components/elements/PreviewButton/index.tsx b/src/admin/components/elements/PreviewButton/index.tsx
index 04b193fa72..8d1fe68a45 100644
--- a/src/admin/components/elements/PreviewButton/index.tsx
+++ b/src/admin/components/elements/PreviewButton/index.tsx
@@ -43,7 +43,7 @@ const PreviewButton: React.FC = ({
const { id, collection, global } = useDocumentInfo();
const [isLoading, setIsLoading] = useState(false);
- const locale = useLocale();
+ const { code: locale } = useLocale();
const { token } = useAuth();
const { serverURL, routes: { api } } = useConfig();
const { t } = useTranslation('version');
diff --git a/src/admin/components/elements/ReactSelect/MultiValueLabel/index.tsx b/src/admin/components/elements/ReactSelect/MultiValueLabel/index.tsx
index 4feafc164e..37647b7875 100644
--- a/src/admin/components/elements/ReactSelect/MultiValueLabel/index.tsx
+++ b/src/admin/components/elements/ReactSelect/MultiValueLabel/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { components as SelectComponents, MultiValueProps } from 'react-select';
import type { Option } from '../types';
+
import './index.scss';
const baseClass = 'multi-value-label';
diff --git a/src/admin/components/elements/ReactSelect/MultiValueRemove/index.tsx b/src/admin/components/elements/ReactSelect/MultiValueRemove/index.tsx
index b123ed79ac..252819dce9 100644
--- a/src/admin/components/elements/ReactSelect/MultiValueRemove/index.tsx
+++ b/src/admin/components/elements/ReactSelect/MultiValueRemove/index.tsx
@@ -4,6 +4,7 @@ import { MultiValueRemoveProps } from 'react-select';
import X from '../../../icons/X';
import Tooltip from '../../Tooltip';
import { Option as OptionType } from '../types';
+
import './index.scss';
const baseClass = 'multi-value-remove';
diff --git a/src/admin/components/elements/ReactSelect/index.tsx b/src/admin/components/elements/ReactSelect/index.tsx
index 75cf284c5f..77e5920268 100644
--- a/src/admin/components/elements/ReactSelect/index.tsx
+++ b/src/admin/components/elements/ReactSelect/index.tsx
@@ -45,6 +45,7 @@ const SelectAdapter: React.FC = (props) => {
components,
isCreatable,
selectProps,
+ noOptionsMessage,
} = props;
const classes = [
@@ -72,6 +73,7 @@ const SelectAdapter: React.FC = (props) => {
filterOption={filterOption}
onMenuOpen={onMenuOpen}
menuPlacement="auto"
+ noOptionsMessage={noOptionsMessage}
components={{
ValueContainer,
SingleValue,
@@ -134,6 +136,7 @@ const SelectAdapter: React.FC = (props) => {
inputValue={inputValue}
onInputChange={(newValue) => setInputValue(newValue)}
onKeyDown={handleKeyDown}
+ noOptionsMessage={noOptionsMessage}
components={{
ValueContainer,
SingleValue,
diff --git a/src/admin/components/elements/ReactSelect/types.ts b/src/admin/components/elements/ReactSelect/types.ts
index 81b6e05e98..ea3a7fc00a 100644
--- a/src/admin/components/elements/ReactSelect/types.ts
+++ b/src/admin/components/elements/ReactSelect/types.ts
@@ -43,6 +43,7 @@ export type OptionGroup = {
}
export type Props = {
+ inputId?: string
className?: string
value?: Option | Option[],
onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -76,4 +77,5 @@ export type Props = {
*/
selectProps?: CustomSelectProps
backspaceRemovesValue?: boolean
+ noOptionsMessage?: (obj: { inputValue: string }) => string
}
diff --git a/src/admin/components/elements/SaveDraft/index.tsx b/src/admin/components/elements/SaveDraft/index.tsx
index f912078007..334cffa0fe 100644
--- a/src/admin/components/elements/SaveDraft/index.tsx
+++ b/src/admin/components/elements/SaveDraft/index.tsx
@@ -58,7 +58,7 @@ export const SaveDraft: React.FC = ({ CustomComponent }) => {
const { submit } = useForm();
const { collection, global, id } = useDocumentInfo();
const modified = useFormModified();
- const locale = useLocale();
+ const { code: locale } = useLocale();
const { t } = useTranslation('version');
const canSaveDraft = modified;
diff --git a/src/admin/components/elements/SearchFilter/index.scss b/src/admin/components/elements/SearchFilter/index.scss
index 4d2f6ae5da..02dfdc0e18 100644
--- a/src/admin/components/elements/SearchFilter/index.scss
+++ b/src/admin/components/elements/SearchFilter/index.scss
@@ -1,5 +1,14 @@
@import '../../../scss/styles';
-
+[dir=rtl] .search-filter{
+ svg {
+ right: base(.5);
+ left:0
+ }
+ &__input {
+ padding-right: base(2);
+ padding-left: 0;
+ }
+}
.search-filter {
position: relative;
diff --git a/src/admin/components/elements/SortColumn/index.tsx b/src/admin/components/elements/SortColumn/index.tsx
index 90390223b4..6eb6966b31 100644
--- a/src/admin/components/elements/SortColumn/index.tsx
+++ b/src/admin/components/elements/SortColumn/index.tsx
@@ -18,7 +18,7 @@ const SortColumn: React.FC = (props) => {
} = props;
const params = useSearchParams();
const history = useHistory();
- const { i18n } = useTranslation();
+ const { t, i18n } = useTranslation('general');
const { sort } = params;
@@ -50,6 +50,7 @@ const SortColumn: React.FC = (props) => {
buttonStyle="none"
className={ascClasses.join(' ')}
onClick={() => setSort(asc)}
+ aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('ascending') })}
>
@@ -58,6 +59,7 @@ const SortColumn: React.FC = (props) => {
buttonStyle="none"
className={descClasses.join(' ')}
onClick={() => setSort(desc)}
+ aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('descending') })}
>
diff --git a/src/admin/components/elements/Status/index.tsx b/src/admin/components/elements/Status/index.tsx
index 94b0b6800c..ef9e7832a0 100644
--- a/src/admin/components/elements/Status/index.tsx
+++ b/src/admin/components/elements/Status/index.tsx
@@ -32,7 +32,7 @@ const Status: React.FC = () => {
} = useConfig();
const [processing, setProcessing] = useState(false);
const { reset: resetForm } = useForm();
- const locale = useLocale();
+ const { code: locale } = useLocale();
const { t, i18n } = useTranslation('version');
const unPublishModalSlug = `confirm-un-publish-${id}`;
diff --git a/src/admin/components/elements/StepNav/index.scss b/src/admin/components/elements/StepNav/index.scss
index 12a39cf737..4e80c80a3c 100644
--- a/src/admin/components/elements/StepNav/index.scss
+++ b/src/admin/components/elements/StepNav/index.scss
@@ -9,7 +9,12 @@
}
a {
- margin-right: base(.25);
+ [dir=ltr] & {
+ margin-right: base(.25);
+ }
+ [dir=rtl] & {
+ margin-left: base(.25);
+ }
border: 0;
display: flex;
align-items: center;
@@ -17,8 +22,14 @@
text-decoration: none;
svg {
- margin-left: base(.25);
- transform: rotate(-90deg);
+ [dir=ltr] & {
+ margin-left: base(.25);
+ transform: rotate(-90deg);
+ }
+ [dir=rtl] & {
+ margin-right: base(.25);
+ transform: rotate(90deg);
+ }
}
label {
diff --git a/src/admin/components/elements/Table/index.scss b/src/admin/components/elements/Table/index.scss
index 7dda019e82..13e04ba8d3 100644
--- a/src/admin/components/elements/Table/index.scss
+++ b/src/admin/components/elements/Table/index.scss
@@ -11,6 +11,9 @@
th {
font-weight: normal;
text-align: left;
+ [dir = rtl] & {
+ text-align: right;
+ }
}
}
diff --git a/src/admin/components/elements/ViewDescription/index.tsx b/src/admin/components/elements/ViewDescription/index.tsx
index a253f18426..52f7912852 100644
--- a/src/admin/components/elements/ViewDescription/index.tsx
+++ b/src/admin/components/elements/ViewDescription/index.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props, isComponent } from './types';
import { getTranslation } from '../../../../utilities/getTranslation';
+
import './index.scss';
const ViewDescription: React.FC = (props) => {
diff --git a/src/admin/components/elements/WhereBuilder/Condition/index.scss b/src/admin/components/elements/WhereBuilder/Condition/index.scss
index c7e06d0c6e..15d58f8c98 100644
--- a/src/admin/components/elements/WhereBuilder/Condition/index.scss
+++ b/src/admin/components/elements/WhereBuilder/Condition/index.scss
@@ -15,6 +15,12 @@
}
}
+ [dir=rtl] &__field,
+ &__operator {
+ margin-left: $baseline;
+ margin-right:0;
+ }
+
&__field,
&__operator {
margin-right: $baseline;
@@ -27,6 +33,9 @@
.btn {
vertical-align: middle;
margin: 0 0 0 $baseline;
+ [dir=rtl] & {
+ margin: 0 $baseline 0 0;
+ }
}
@include mid-break {
diff --git a/src/admin/components/elements/WhereBuilder/field-types.tsx b/src/admin/components/elements/WhereBuilder/field-types.tsx
index 9cfb1cd1a4..41f92ef837 100644
--- a/src/admin/components/elements/WhereBuilder/field-types.tsx
+++ b/src/admin/components/elements/WhereBuilder/field-types.tsx
@@ -57,6 +57,16 @@ const geo = [
},
];
+const within = {
+ label: 'within',
+ value: 'within',
+};
+
+const intersects = {
+ label: 'intersects',
+ value: 'intersects',
+};
+
const like = {
label: 'isLike',
value: 'like',
@@ -86,7 +96,7 @@ const fieldTypeConditions = {
},
json: {
component: 'Text',
- operators: [...base, like, contains],
+ operators: [...base, like, contains, within, intersects],
},
richText: {
component: 'Text',
@@ -102,7 +112,7 @@ const fieldTypeConditions = {
},
point: {
component: 'Point',
- operators: [...geo],
+ operators: [...geo, within, intersects],
},
upload: {
component: 'Text',
diff --git a/src/admin/components/elements/WhereBuilder/index.tsx b/src/admin/components/elements/WhereBuilder/index.tsx
index d215c42b77..3bf71a4918 100644
--- a/src/admin/components/elements/WhereBuilder/index.tsx
+++ b/src/admin/components/elements/WhereBuilder/index.tsx
@@ -13,6 +13,7 @@ import { useSearchParams } from '../../utilities/SearchParams';
import validateWhereQuery from './validateWhereQuery';
import { Where } from '../../../../types';
import { getTranslation } from '../../../../utilities/getTranslation';
+import { transformWhereQuery } from './transformWhereQuery';
import './index.scss';
@@ -43,6 +44,10 @@ const reduceFields = (fields, i18n) => flattenTopLevelFields(fields).reduce((red
return reduced;
}, []);
+/**
+ * The WhereBuilder component is used to render the filter controls for a collection's list view.
+ * It is part of the {@link ListControls} component which is used to render the controls (search, filter, where).
+ */
const WhereBuilder: React.FC = (props) => {
const {
collection,
@@ -59,16 +64,30 @@ const WhereBuilder: React.FC = (props) => {
const params = useSearchParams();
const { t, i18n } = useTranslation('general');
+ // This handles initializing the where conditions from the search query (URL). That way, if you pass in
+ // query params to the URL, the where conditions will be initialized from those and displayed in the UI.
+ // Example: /admin/collections/posts?where[or][0][and][0][text][equals]=example%20post
const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => {
- if (modifySearchQuery && validateWhereQuery(whereFromSearch)) {
- return whereFromSearch.or;
- }
+ if (modifySearchQuery && whereFromSearch) {
+ if (validateWhereQuery(whereFromSearch)) {
+ return whereFromSearch.or;
+ }
+ // Transform the where query to be in the right format. This will transform something simple like [text][equals]=example%20post to the right format
+ const transformedWhere = transformWhereQuery(whereFromSearch);
+
+ if (validateWhereQuery(transformedWhere)) {
+ return transformedWhere.or;
+ }
+
+ console.warn('Invalid where query in URL. Ignoring.');
+ }
return [];
});
const [reducedFields] = useState(() => reduceFields(collection.fields, i18n));
+ // This handles updating the search query (URL) when the where conditions change
useThrottledEffect(() => {
const currentParams = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 }) as { where: Where };
@@ -83,8 +102,11 @@ const WhereBuilder: React.FC = (props) => {
];
}, []) : [];
+ const hasNewWhereConditions = conditions.length > 0;
+
+
const newWhereQuery = {
- ...typeof currentParams?.where === 'object' ? currentParams.where : {},
+ ...typeof currentParams?.where === 'object' && (validateWhereQuery(currentParams?.where) || !hasNewWhereConditions) ? currentParams.where : {},
or: [
...conditions,
...paramsToKeep,
@@ -94,7 +116,6 @@ const WhereBuilder: React.FC = (props) => {
if (handleChange) handleChange(newWhereQuery as Where);
const hasExistingConditions = typeof currentParams?.where === 'object' && 'or' in currentParams.where;
- const hasNewWhereConditions = conditions.length > 0;
if (modifySearchQuery && ((hasExistingConditions && !hasNewWhereConditions) || hasNewWhereConditions)) {
history.replace({
diff --git a/src/admin/components/elements/WhereBuilder/transformWhereQuery.ts b/src/admin/components/elements/WhereBuilder/transformWhereQuery.ts
new file mode 100644
index 0000000000..1edb9a8bcf
--- /dev/null
+++ b/src/admin/components/elements/WhereBuilder/transformWhereQuery.ts
@@ -0,0 +1,51 @@
+import type { Where } from '../../../../types';
+
+/**
+ * Something like [or][0][and][0][text][equals]=example%20post will work and pass through the validateWhereQuery check.
+ * However, something like [text][equals]=example%20post will not work and will fail the validateWhereQuery check,
+ * even though it is a valid Where query. This needs to be transformed here.
+ */
+export const transformWhereQuery = (whereQuery): Where => {
+ if (!whereQuery) {
+ return {};
+ }
+ // Check if 'whereQuery' has 'or' field but no 'and'. This is the case for "correct" queries
+ if (whereQuery.or && !whereQuery.and) {
+ return {
+ or: whereQuery.or.map((query) => {
+ // ...but if the or query does not have an and, we need to add it
+ if(!query.and) {
+ return {
+ and: [query]
+ }
+ }
+ return query;
+ }),
+ };
+ }
+
+ // Check if 'whereQuery' has 'and' field but no 'or'.
+ if (whereQuery.and && !whereQuery.or) {
+ return {
+ or: [
+ {
+ and: whereQuery.and,
+ },
+ ],
+ };
+ }
+
+ // Check if 'whereQuery' has neither 'or' nor 'and'.
+ if (!whereQuery.or && !whereQuery.and) {
+ return {
+ or: [
+ {
+ and: [whereQuery], // top-level siblings are considered 'and'
+ },
+ ],
+ };
+ }
+
+ // If 'whereQuery' has 'or' and 'and', just return it as it is.
+ return whereQuery;
+};
diff --git a/src/admin/components/elements/WhereBuilder/validateWhereQuery.ts b/src/admin/components/elements/WhereBuilder/validateWhereQuery.ts
index f0f62656fc..f1789e73dc 100644
--- a/src/admin/components/elements/WhereBuilder/validateWhereQuery.ts
+++ b/src/admin/components/elements/WhereBuilder/validateWhereQuery.ts
@@ -1,8 +1,37 @@
-import { Where } from '../../../../types';
+import type { Operator, Where } from '../../../../types';
+import { validOperators } from '../../../../types/constants';
const validateWhereQuery = (whereQuery): whereQuery is Where => {
if (whereQuery?.or?.length > 0 && whereQuery?.or?.[0]?.and && whereQuery?.or?.[0]?.and?.length > 0) {
- return true;
+ // At this point we know that the whereQuery has 'or' and 'and' fields,
+ // now let's check the structure and content of these fields.
+
+ const isValid = whereQuery.or.every((orQuery) => {
+ if (orQuery.and && Array.isArray(orQuery.and)) {
+ return orQuery.and.every((andQuery) => {
+ if (typeof andQuery !== 'object') {
+ return false;
+ }
+ const andKeys = Object.keys(andQuery);
+ // If there are no keys, it's not a valid WhereField.
+ if (andKeys.length === 0) {
+ return false;
+ }
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key of andKeys) {
+ const operator = Object.keys(andQuery[key])[0];
+ // Check if the key is a valid Operator.
+ if (!operator || !validOperators.includes(operator as Operator)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+ return false;
+ });
+
+ return isValid;
}
return false;
diff --git a/src/admin/components/forms/Form/index.tsx b/src/admin/components/forms/Form/index.tsx
index 98e23a9321..5b5d338916 100644
--- a/src/admin/components/forms/Form/index.tsx
+++ b/src/admin/components/forms/Form/index.tsx
@@ -51,7 +51,7 @@ const Form: React.FC = (props) => {
} = props;
const history = useHistory();
- const locale = useLocale();
+ const { code: locale } = useLocale();
const { t, i18n } = useTranslation('general');
const { refreshCookie, user } = useAuth();
const { id, getDocPreferences, collection, global } = useDocumentInfo();
diff --git a/src/admin/components/forms/Label/index.scss b/src/admin/components/forms/Label/index.scss
index a1f4391000..80bc02795b 100644
--- a/src/admin/components/forms/Label/index.scss
+++ b/src/admin/components/forms/Label/index.scss
@@ -9,7 +9,13 @@ label.field-label {
.required {
color: var(--theme-error-500);
- margin-left: base(.25);
- margin-right: auto;
+ [dir=ltr] & {
+ margin-left: base(.25);
+ margin-right: auto;
+ }
+ [dir=rtl] & {
+ margin-right: base(.25);
+ margin-left: auto;
+ }
}
}
\ No newline at end of file
diff --git a/src/admin/components/forms/NullifyField/index.tsx b/src/admin/components/forms/NullifyField/index.tsx
index 1eb42ef26b..0b07c60ccf 100644
--- a/src/admin/components/forms/NullifyField/index.tsx
+++ b/src/admin/components/forms/NullifyField/index.tsx
@@ -13,7 +13,7 @@ type NullifyLocaleFieldProps = {
}
export const NullifyLocaleField: React.FC = ({ localized, path, fieldValue }) => {
const { dispatchFields, setModified } = useForm();
- const currentLocale = useLocale();
+ const { code: currentLocale } = useLocale();
const { localization } = useConfig();
const [checked, setChecked] = React.useState(typeof fieldValue !== 'number');
const defaultLocale = (localization && localization.defaultLocale) ? localization.defaultLocale : 'en';
diff --git a/src/admin/components/forms/field-types/Array/index.scss b/src/admin/components/forms/field-types/Array/index.scss
index 8d33dd2f87..533e6ea81a 100644
--- a/src/admin/components/forms/field-types/Array/index.scss
+++ b/src/admin/components/forms/field-types/Array/index.scss
@@ -31,11 +31,12 @@
display: flex;
align-items: flex-end;
width: 100%;
+ justify-content: space-between;
}
&__header-actions {
list-style: none;
- margin: 0 0 0 auto;
+ margin: 0;
padding: 0;
display: flex;
}
diff --git a/src/admin/components/forms/field-types/Array/index.tsx b/src/admin/components/forms/field-types/Array/index.tsx
index 519b9d9f19..c4e128e006 100644
--- a/src/admin/components/forms/field-types/Array/index.tsx
+++ b/src/admin/components/forms/field-types/Array/index.tsx
@@ -56,7 +56,7 @@ const ArrayFieldType: React.FC = (props) => {
const { setDocFieldPreferences } = useDocumentInfo();
const { dispatchFields, setModified, addFieldRow, removeFieldRow } = useForm();
const submitted = useFormSubmitted();
- const locale = useLocale();
+ const { code: locale } = useLocale();
const { t, i18n } = useTranslation('fields');
const { localization } = useConfig();
diff --git a/src/admin/components/forms/field-types/Blocks/index.scss b/src/admin/components/forms/field-types/Blocks/index.scss
index 5022acf1ea..ce989684bf 100644
--- a/src/admin/components/forms/field-types/Blocks/index.scss
+++ b/src/admin/components/forms/field-types/Blocks/index.scss
@@ -15,6 +15,7 @@
display: flex;
align-items: flex-end;
width: 100%;
+ justify-content: space-between;
}
&__heading-with-error {
@@ -41,7 +42,7 @@
&__header-actions {
list-style: none;
- margin: 0 0 0 auto;
+ margin: 0;
padding: 0;
display: flex;
}
@@ -73,10 +74,6 @@
line-height: unset;
}
- .section-title {
- min-width: 0;
- }
-
&__row {
margin-bottom: base(.5);
}
diff --git a/src/admin/components/forms/field-types/Blocks/index.tsx b/src/admin/components/forms/field-types/Blocks/index.tsx
index 5d2f8d0f41..f38d8c5c55 100644
--- a/src/admin/components/forms/field-types/Blocks/index.tsx
+++ b/src/admin/components/forms/field-types/Blocks/index.tsx
@@ -56,7 +56,7 @@ const BlocksField: React.FC = (props) => {
const { setDocFieldPreferences } = useDocumentInfo();
const { dispatchFields, setModified, addFieldRow, removeFieldRow } = useForm();
- const locale = useLocale();
+ const { code: locale } = useLocale();
const { localization } = useConfig();
const drawerSlug = useDrawerSlug('blocks-drawer');
const submitted = useFormSubmitted();
diff --git a/src/admin/components/forms/field-types/Checkbox/Input.tsx b/src/admin/components/forms/field-types/Checkbox/Input.tsx
index 8532a8c609..040cb82148 100644
--- a/src/admin/components/forms/field-types/Checkbox/Input.tsx
+++ b/src/admin/components/forms/field-types/Checkbox/Input.tsx
@@ -1,62 +1,74 @@
import React from 'react';
import Check from '../../../icons/Check';
import Label from '../../Label';
+import Line from '../../../icons/Line';
import './index.scss';
const baseClass = 'custom-checkbox';
type CheckboxInputProps = {
- onToggle: React.MouseEventHandler
+ onToggle: React.FormEventHandler
inputRef?: React.MutableRefObject
readOnly?: boolean
checked?: boolean
+ partialChecked?: boolean
name?: string
id?: string
label?: string
+ 'aria-label'?: string
required?: boolean
}
+
export const CheckboxInput: React.FC = (props) => {
const {
onToggle,
checked,
+ partialChecked,
inputRef,
name,
id,
label,
+ 'aria-label': ariaLabel,
readOnly,
required,
} = props;
return (
-
-
-
-
+ )}
+
);
};
diff --git a/src/admin/components/forms/field-types/Checkbox/index.scss b/src/admin/components/forms/field-types/Checkbox/index.scss
index aabd160a24..8bada90490 100644
--- a/src/admin/components/forms/field-types/Checkbox/index.scss
+++ b/src/admin/components/forms/field-types/Checkbox/index.scss
@@ -4,10 +4,6 @@
position: relative;
margin-bottom: $baseline;
- input[type=checkbox] {
- display: none;
- }
-
.tooltip:not([aria-hidden="true"]) {
right: auto;
position: relative;
@@ -22,32 +18,88 @@
.custom-checkbox {
+ display: inline-flex;
+
label {
padding-bottom: 0;
+ padding-left: base(.5);
}
-
- input {
- // hidden HTML checkbox
- position: absolute;
- top: 0;
- left: 0;
- opacity: 0;
+ [dir=rtl] &__input {
+ margin-right: 0;
+ margin-left: base(.5);
}
&__input {
- // visible checkbox
@include formInput;
+ display: flex;
padding: 0;
line-height: 0;
position: relative;
width: $baseline;
height: $baseline;
- margin-right: base(.5);
+
+ & input[type="checkbox"] {
+ position: absolute;
+ // Without the extra 4px, there is an uncheckable area due to the border of the parent element
+ width: calc(100% + 4px);
+ height: calc(100% + 4px);
+ padding: 0;
+ margin: 0;
+ margin-left: -2px;
+ margin-top: -2px;
+ opacity: 0;
+ border-radius: 0;
+ z-index: 1;
+ cursor: pointer;
+ }
+ }
+
+ &__icon {
+ position: absolute;
svg {
opacity: 0;
}
}
+
+
+ &:not(&--read-only) {
+ &:active,
+ &:focus-within,
+ &:focus {
+ .custom-checkbox__input, & input[type="checkbox"] {
+ @include inputShadowActive;
+
+ outline: 0;
+ box-shadow: 0 0 3px 3px var(--theme-success-400)!important;
+ border: 1px solid var(--theme-elevation-150);
+ }
+ }
+
+ &:hover {
+ .custom-checkbox__input, & input[type="checkbox"] {
+ border-color: var(--theme-elevation-250);
+ }
+ }
+ }
+
+ &:not(&--read-only):not(&--checked) {
+ &:hover {
+ cursor: pointer;
+
+ svg {
+ opacity: 0.2;
+ }
+ }
+ }
+
+ &--checked {
+ .custom-checkbox__icon {
+ svg {
+ opacity: 1;
+ }
+ }
+ }
&--read-only {
.custom-checkbox__input {
@@ -58,40 +110,6 @@
color: var(--theme-elevation-400);
}
}
-
- button {
- @extend %btn-reset;
- display: flex;
- align-items: center;
- cursor: pointer;
-
- &:focus,
- &:active {
- outline: none;
- }
-
- &:focus {
- .custom-checkbox__input {
- box-shadow: 0 0 3px 3px var(--theme-success-400);
- }
- }
-
- &:hover {
- svg {
- opacity: .2;
- }
- }
- }
-
- &--checked {
- button {
- .custom-checkbox__input {
- svg {
- opacity: 1;
- }
- }
- }
- }
}
html[data-theme=light] {
diff --git a/src/admin/components/forms/field-types/Checkbox/index.tsx b/src/admin/components/forms/field-types/Checkbox/index.tsx
index 1fdb7785d0..be798ad00c 100644
--- a/src/admin/components/forms/field-types/Checkbox/index.tsx
+++ b/src/admin/components/forms/field-types/Checkbox/index.tsx
@@ -88,6 +88,7 @@ const Checkbox: React.FC