diff --git a/.changeset/poor-schools-retire.md b/.changeset/poor-schools-retire.md new file mode 100644 index 00000000000..08d735b0232 --- /dev/null +++ b/.changeset/poor-schools-retire.md @@ -0,0 +1,8 @@ +--- +"@wso2is/admin.organizations.v1": minor +"@wso2is/react-components": minor +"@wso2is/admin.core.v1": minor +"@wso2is/i18n": patch +--- + +UI support for Filtering Organizations by Metadata Attributes diff --git a/features/admin.core.v1/components/advanced-search-with-basic-filters.tsx b/features/admin.core.v1/components/advanced-search-with-basic-filters.tsx index 5a9f9883125..0a824d1c9e0 100644 --- a/features/admin.core.v1/components/advanced-search-with-basic-filters.tsx +++ b/features/admin.core.v1/components/advanced-search-with-basic-filters.tsx @@ -27,30 +27,21 @@ import { PrimaryButton, SessionTimedOutContext } from "@wso2is/react-components"; -import React, { CSSProperties, FunctionComponent, ReactElement, useState } from "react"; +import React, { CSSProperties, FunctionComponent, ReactElement, ReactNode, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Divider, Form, Grid } from "semantic-ui-react"; import { getAdvancedSearchIcons } from "../configs"; - -/** - * Filter attribute field identifier. - */ -const FILTER_ATTRIBUTE_FIELD_IDENTIFIER: string = "filterAttribute"; - -/** - * Filter condition field identifier. - */ -const FILTER_CONDITION_FIELD_IDENTIFIER: string = "filterCondition"; - -/** - * Filter value field identifier. - */ -const FILTER_VALUES_FIELD_IDENTIFIER: string = "filterValues"; +import { AdvanceSearchConstants } from "../constants/advance-search"; /** * Prop types for the application search component. */ export interface AdvancedSearchWithBasicFiltersPropsInterface extends TestableComponentInterface { + + /** + * Children node form field passed from parent + */ + children?: ReactNode; /** * Default Search attribute. ex: "name" */ @@ -71,10 +62,26 @@ export interface AdvancedSearchWithBasicFiltersPropsInterface extends TestableCo * Fill color. */ fill?: AdvancedSearchPropsInterface[ "fill" ]; + /** + * Callback to be triggered on advance search close. + */ + onClose?: () => void; /** * Callback to be triggered on filter query change. */ onFilter: (query: string) => void; + /** + * Callback to be triggered on filter attribute option change. + */ + onFilterAttributeOptionsChange?: (values) => void; + /** + * Callback to be triggered on submit error. + */ + onSubmitError?: () => boolean; + /** + * Callback to be get custom query. + */ + getQuery?: (values) => string; /** * Filter attributes options. */ @@ -152,6 +159,7 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent { const { + children, defaultSearchAttribute, defaultSearchOperator, disableSearchFilterDropdown, @@ -163,7 +171,11 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent(false); const [ externalSearchQuery, setExternalSearchQuery ] = useState(""); const sessionTimedOut: boolean = React.useContext(SessionTimedOutContext); + const formRef: React.RefObject = useRef(null); /** * Handles the form submit. @@ -188,12 +201,26 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent): void => { - const query: string = values.get(FILTER_ATTRIBUTE_FIELD_IDENTIFIER) - + " " - + values.get(FILTER_CONDITION_FIELD_IDENTIFIER) - + " " - + values?.get(FILTER_VALUES_FIELD_IDENTIFIER); + if (onSubmitError) { + const shouldReturn: boolean = onSubmitError(); + + if (shouldReturn) { + return; + } + } + let query: string; + const customQuery: string = getQuery ? getQuery(values) : null; + + if (customQuery !== null) { + query = customQuery; + } else { + query = values.get(AdvanceSearchConstants.FILTER_ATTRIBUTE_FIELD_IDENTIFIER) + + " " + + values.get(AdvanceSearchConstants.FILTER_CONDITION_FIELD_IDENTIFIER) + + " " + + values?.get(AdvanceSearchConstants.FILTER_VALUES_FIELD_IDENTIFIER); + } setExternalSearchQuery(query); onFilter(query); setIsFormSubmitted(true); @@ -247,6 +274,28 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent { + if (formRef.current && !formRef.current.contains(event.target as Node)) { + if (onClose) { + onClose(); + } + } + }; + + /** + * Adds an event listener for detecting clicks outside the form on component mount. + */ + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + /** * Default filter condition options. */ @@ -305,6 +354,8 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent) => handleFormSubmit(values) } resetState={ isFiltersReset } onChange={ () => setIsFiltersReset(false) } + onSubmitError={ () => {onSubmitError?.();} } + ref={ formRef } > ) => onFilterAttributeOptionsChange?.(values) } /> + { children } ( + filter?: string, + limit?: number, + after?: string, + before?: string, + recursive: boolean = false, + isRoot: boolean = false + ): RequestResultInterface => { + const requestConfig: RequestConfigInterface = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + params: { + after, + before, + filter, + limit, + recursive + }, + url: (isRoot + ? store.getState().config.endpoints.rootOrganization + : store.getState().config.endpoints.organizations) + "/organizations/meta-attributes" + }; + + const { data, error, isLoading, isValidating, mutate } = useRequest( + requestConfig, + { revalidateIfStale: false } + ); + + return { + data, + error, + isLoading, + isValidating, + mutate + }; + }; diff --git a/features/admin.organizations.v1/components/index.ts b/features/admin.organizations.v1/components/index.ts index 7631ff3cf60..a679492ea3c 100644 --- a/features/admin.organizations.v1/components/index.ts +++ b/features/admin.organizations.v1/components/index.ts @@ -1,20 +1,20 @@ /** -* Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. -* -* WSO2 LLC. licenses this file to you under the Apache License, -* Version 2.0 (the "License"); you may not use this file except -* in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ + * Copyright (c) 2022, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ export * from "./organization-list"; export * from "./add-organization-modal"; diff --git a/features/admin.organizations.v1/components/meta-attribute-auto-complete.scss b/features/admin.organizations.v1/components/meta-attribute-auto-complete.scss new file mode 100644 index 00000000000..3b3a811e39d --- /dev/null +++ b/features/admin.organizations.v1/components/meta-attribute-auto-complete.scss @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.ui.form { + .meta-attribute-autocomplete { + margin-bottom: 1em; + + input { + border: none; + } + + input::placeholder { + font-size: 1em; + color: rgba(0, 0, 0, .95) !important; + } + + label { + font-size: .92857143em; + color: rgba(0, 0, 0, .87); + margin-bottom: .28571429rem; + } + + .ui.label { + white-space: normal; + background: #fff !important; + border: 1px solid #e0b4b4 !important; + color: #9f3a38 !important; + } + + .list-item-loader { + margin: 8px; + } + + .list-item-content { + display: flex !important; + align-items: center; + } + + .prompt.label { + white-space: normal; + background: #fff !important; + border: 1px solid #e0b4b4 !important; + color: #9f3a38 !important; + } + + &.error { + color: #9f3a38; + + .MuiOutlinedInput-notchedOutline { + border-color: #e0b4b4; + } + + .MuiSvgIcon-root { + color: #9f3a38; + } + + .MuiTextField-root { + background: #fff6f6; + border-radius: 8px; + } + + .MuiInputBase-input { + background: #fff6f6; + } + + label { + color: #9f3a38; + } + + input::placeholder { + font-size: 1em; + color: #9f3a38 !important; + } + } + } +} diff --git a/features/admin.organizations.v1/components/meta-attribute-auto-complete.tsx b/features/admin.organizations.v1/components/meta-attribute-auto-complete.tsx new file mode 100644 index 00000000000..6bd5602c7b5 --- /dev/null +++ b/features/admin.organizations.v1/components/meta-attribute-auto-complete.tsx @@ -0,0 +1,308 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Autocomplete, { AutocompleteRenderInputParams } from "@oxygen-ui/react/Autocomplete"; +import CircularProgress from "@oxygen-ui/react/CircularProgress"; +import TextField from "@oxygen-ui/react/TextField"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import classNames from "classnames"; +import debounce from "lodash-es/debounce"; +import React, { + FunctionComponent, + HTMLAttributes, + HTMLProps, + ReactElement, + SyntheticEvent, + useCallback, + useEffect, + useMemo, + useState +} from "react"; +import { useTranslation } from "react-i18next"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import "./meta-attribute-auto-complete.scss"; +import { Header, Item } from "semantic-ui-react"; +import { useGetOrganizationsMetaAttributes } from "../api/use-get-organizations-meta-attributes"; +import { OrganizationLinkInterface } from "../models"; + +/** + * Prop types for the meta attribute autocomplete component. + */ +export interface MetaAttributeAutoCompleteProps extends IdentifiableComponentInterface{ + + /** + * Callback to handle changes in the selected meta attribute. + */ + onMetaAttributeChange: (value: string) => void; + /** + * Boolean indicating validation errors for the meta attribute. + */ + hasErrors?: boolean; +} + +interface Params { + after?: string; + before?: string; + filter?: string; + isRoot?: boolean; + limit?: number; + recursive?: boolean; +} + +type MetaAttributeOption = { + text: string; + value: string; +}; + +/** + * Autocomplete component with infinite scroll for the organizations' meta attributes. + * + * @param props - Props injected to the component. + * @returns React element. + */ +const MetaAttributeAutoComplete: FunctionComponent = ({ + onMetaAttributeChange, + hasErrors, + "data-componentid": _componentId = "organization-meta-attribute" +}: MetaAttributeAutoCompleteProps): ReactElement => { + + const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + + const [ metaAttributeListOptions, setMetaAttributeListOptions ] = useState([]); + const [ inputValue, setInputValue ] = useState(""); + const [ hasNextPage, setHasNextPage ] = useState(true); + const [ cursor, setCursor ] = useState(null); + const [ params, setParams ] = useState({ after: undefined, filter: undefined, limit: 10 }); + + const { + data: metaAttributes, + isLoading: isMetaAttributesFetchRequestLoading, + error: metaAttributesFetchRequestError, + isValidating: metaAttributesIsValidating + } = useGetOrganizationsMetaAttributes(params.filter, params.limit, params.after); + + const queryPrefix: string = "attributes co "; + + useEffect(() => { + if (!metaAttributes || isMetaAttributesFetchRequestLoading || metaAttributesIsValidating) return; + + const updatedSubAttributeListOptions: MetaAttributeOption[] = ( + metaAttributes.attributes?.map((attribute: string) => ({ + text: attribute, + value: attribute + })) || [] + ); + + setMetaAttributeListOptions((prevOptions: MetaAttributeOption[]) => [ + ...prevOptions, + ...updatedSubAttributeListOptions + ]); + + let nextFound: boolean = false; + + metaAttributes?.links?.forEach((link: OrganizationLinkInterface) => { + if (link.rel === "next") { + const nextCursor: string = link.href.split("after=")[1]; + + setCursor(nextCursor); + setHasNextPage(!!nextCursor); + nextFound = true; + } + }); + + if (!nextFound) { + setCursor(""); + setHasNextPage(false); + } + }, [ metaAttributes ]); + + useEffect(() => { + if (metaAttributesFetchRequestError) { + dispatch( + addAlert({ + description: t("organizations:notifications.getMetaAttributesList.genericError.description"), + level: AlertLevels.ERROR, + message: t("organizations:notifications.getMetaAttributesList.genericError.message") + }) + ); + } + }, [ metaAttributesFetchRequestError ]); + + /** + * Update the params state whenever inputValue or cursor changes + */ + const updateParams = (newCursor: string, newInputValue: string) => { + setCursor(newCursor); + setInputValue(newInputValue); + setParams((prevParams: Params) => ({ + ...prevParams, + after: newCursor, + filter: newInputValue ? `${queryPrefix}${newInputValue}` : "" + })); + }; + + /** + * Handles changes in the meta attribute input field. + * + * @param _event - The change event. + * @param data - The selected data. + */ + const handleMetaAttributeChange = (_event: React.ChangeEvent, data: { value: string } | null) => { + const value: string = data && data.value ? data.value : ""; + + onMetaAttributeChange(value); + }; + + /** + * Handles changes in the input field. + * + * @param _event - The change event. + * @param data - The new input value. + */ + const handleInputChange: (_event: SyntheticEvent, data: string | null) => void = useCallback( + debounce((_event: SyntheticEvent, data: string | null) => { + const newInputValue: string = data ?? ""; + + setMetaAttributeListOptions([]); + updateParams("", newInputValue); + }, 300), + [ updateParams ] + ); + + /** + * Loads more meta attribute options when scrolled to the bottom. + */ + const loadMoreOptions = () => { + if (hasNextPage && !isMetaAttributesFetchRequestLoading && !metaAttributesIsValidating) { + updateParams(cursor, inputValue); + } + }; + + /** + * Loading component. + */ + const loadingComponent = () => { + return ( + + + + + ); + }; + + /** + * Custom listbox component with infinite scroll. + */ + const customListboxComponent: (listboxProps: HTMLProps) => JSX.Element = useMemo( + () => (listboxProps: HTMLProps) => ( + 4 ? 155 : "auto" } + > +
+ + ), + [ metaAttributeListOptions.length, loadMoreOptions, hasNextPage ] + ); + + /** + * Known bug: Autocomplete scrolls to the top when new options are added. + * TODO: Update MUI version when resolved. + * @see {@link https://github.com/mui/material-ui/issues/40250} + */ + return ( +
+ attributeListOption.text } + isOptionEqualToValue={ ( + option: MetaAttributeOption, + value: MetaAttributeOption + ) => option.value === value.value } + renderOption={ ( + props: HTMLAttributes, + attributeListOption: MetaAttributeOption + ) => ( +
  • + + { attributeListOption.text } + +
  • + ) } + options={ metaAttributeListOptions } + onChange={ handleMetaAttributeChange } + onInputChange={ handleInputChange } + noOptionsText={ t("common:noResultsFound") } + ListboxComponent={ customListboxComponent } + renderInput={ (params: AutocompleteRenderInputParams) => ( + ) => { + if (event.key === "Enter") { + event.preventDefault(); + } + } } + data-componentid={ `${ _componentId }-text-field` } + /> + ) } + data-componentid={ `${ _componentId }-autocomplete` } + /> + { hasErrors && ( +
    +

    { t("organizations:advancedSearch.form.inputs.filterMetaAttribute.validations.empty") }

    +
    + ) } +
    + ); +}; + +export default MetaAttributeAutoComplete; diff --git a/features/admin.organizations.v1/components/organization-list.tsx b/features/admin.organizations.v1/components/organization-list.tsx index 0ddc1c912ba..1a35bf5a96c 100644 --- a/features/admin.organizations.v1/components/organization-list.tsx +++ b/features/admin.organizations.v1/components/organization-list.tsx @@ -501,11 +501,7 @@ export const OrganizationList: FunctionComponent imageSize="tiny" title={ t("console:manage.placeholders.emptySearchResult.title") } subtitle={ [ - t("console:manage.placeholders.emptySearchResult.subtitles.0", { - // searchQuery looks like "name co OrgName", so we only remove the filter string only to get - // the actual user entered query - query: searchQuery.split("name co ")[1] - }), + t("console:manage.placeholders.emptySearchResult.subtitles.0", { query: searchQuery }), t("console:manage.placeholders.emptySearchResult.subtitles.1") ] } data-componentid={ `${ componentId }-empty-search-placeholder` } diff --git a/features/admin.organizations.v1/models/organizations.ts b/features/admin.organizations.v1/models/organizations.ts index 603def53842..c0971c2e7fa 100644 --- a/features/admin.organizations.v1/models/organizations.ts +++ b/features/admin.organizations.v1/models/organizations.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,3 +114,8 @@ export interface ShareApplicationRequestInterface { shareWithAllChildren: boolean; sharedOrganizations?: string[]; } + +export interface OrganizationsMetaAttributesListInterface { + links?: OrganizationLinkInterface[]; + attributes: string[]; +} diff --git a/features/admin.organizations.v1/pages/organizations.tsx b/features/admin.organizations.v1/pages/organizations.tsx index 1b91e1712e8..2b40d431884 100644 --- a/features/admin.organizations.v1/pages/organizations.tsx +++ b/features/admin.organizations.v1/pages/organizations.tsx @@ -25,6 +25,8 @@ import { FeatureConfigInterface, UIConstants } from "@wso2is/admin.core.v1"; +import { AdvanceSearchConstants } from "@wso2is/admin.core.v1/constants/advance-search"; +import { isFeatureEnabled } from "@wso2is/core/helpers"; import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { I18n } from "@wso2is/i18n"; @@ -52,8 +54,10 @@ import { Icon, PaginationProps } from "semantic-ui-react"; +import { FormValue } from "../../../modules/form/src"; import { getOrganizations, useAuthorizedOrganizationsList } from "../api"; import { AddOrganizationModal, OrganizationList } from "../components"; +import MetaAttributeAutoComplete from "../components/meta-attribute-auto-complete"; import { OrganizationInterface, OrganizationLinkInterface, @@ -93,6 +97,10 @@ const OrganizationsPage: FunctionComponent = ( const hasOrganizationListViewPermissions: boolean = useRequiredScopes( featureConfig?.organizations?.scopes?.read ); + const isFilterByMetadataAttributesEnabled: boolean = isFeatureEnabled( + featureConfig.organizations, + "organizations.filterByMetadataAttributes" + ); const [ organizationList, setOrganizationList ] = useState(null); const [ searchQuery, setSearchQuery ] = useState(""); @@ -111,6 +119,10 @@ const OrganizationsPage: FunctionComponent = ( const [ authorizedListPrevCursor, setAuthorizedListPrevCursor ] = useState(""); const [ authorizedListNextCursor, setAuthorizedListNextCursor ] = useState(""); const [ activePage, setActivePage ] = useState(1); + const [ shouldShowMetaAttributeComponent, setShouldShowMetaAttributeComponent ] = useState(false); + const [ selectedMetaAttribute, setSelectedMetaAttribute ] = useState(""); + const [ hasErrors, setHasErrors ] = useState(false); + const [ triggerClearQuery, setTriggerClearQuery ] = useState(false); const eventPublisher: EventPublisher = EventPublisher.getInstance(); @@ -320,6 +332,8 @@ const OrganizationsPage: FunctionComponent = ( ) => void = useCallback((query: string): void => { resetPagination(); setSearchQuery(query); + setSelectedMetaAttribute(""); + setShouldShowMetaAttributeComponent(false); }, [ resetPagination, setSearchQuery ]); /** @@ -386,9 +400,10 @@ const OrganizationsPage: FunctionComponent = ( * Handles the `onSearchQueryClear` callback action. */ const handleSearchQueryClear: () => void = useCallback((): void => { + setTriggerClearQuery((triggerClearQuery: boolean) => !triggerClearQuery); setSearchQuery(""); resetPagination(); - }, [ setSearchQuery, resetPagination ]); + }, [ setSearchQuery, resetPagination, setTriggerClearQuery ]); const handleListItemClick = ( _e: SyntheticEvent, @@ -408,6 +423,77 @@ const OrganizationsPage: FunctionComponent = ( setOrganizations(newOrganizations); }; + /** + * Handles changes in filter attribute options. + * + * @param values - The collection of form values. + */ + const handleFilterAttributeOptionsChange = (values: Map): void => { + setHasErrors(false); + if (values.get(AdvanceSearchConstants.FILTER_ATTRIBUTE_FIELD_IDENTIFIER) === "attributes") { + setShouldShowMetaAttributeComponent(true); + } else { + setSelectedMetaAttribute(""); + setShouldShowMetaAttributeComponent(false); + } + }; + + /** + * Handles changes in filter meta attribute options. + * + * @param value - The field value. + */ + const handleMetaAttributeChange = (value: string) => { + + setHasErrors(false); + setSelectedMetaAttribute(value); + }; + + /** + * Handles the on close of the advanced search component. + */ + const handleAdvancedSearchClose = () => { + setSelectedMetaAttribute(""); + setShouldShowMetaAttributeComponent(false); + }; + + /** + * Constructs the custom search query. + * + * @param values - The collection of form values. + * @returns The constructed search query string. + */ + const getQuery = (values: Map): string => { + if (shouldShowMetaAttributeComponent && selectedMetaAttribute) { + return values.get(AdvanceSearchConstants.FILTER_ATTRIBUTE_FIELD_IDENTIFIER) + + "." + + selectedMetaAttribute + + " " + + values.get(AdvanceSearchConstants.FILTER_CONDITION_FIELD_IDENTIFIER) + + " " + + values.get(AdvanceSearchConstants.FILTER_VALUES_FIELD_IDENTIFIER); + } + + return null; + }; + + /** + * Checks for submission errors in the meta attribute component. + * + * @returns A boolean indicating true if there is an error, otherwise false. + */ + const handleMetaAttributeSubmitError = (): boolean => { + + let hasError: boolean = false; + + if (shouldShowMetaAttributeComponent && (selectedMetaAttribute === null || selectedMetaAttribute === "")) { + hasError = true; + } + setHasErrors(hasError); + + return hasError; + }; + return ( <> = ( = ( ) } defaultSearchAttribute="name" defaultSearchOperator="co" + triggerClearQuery={ triggerClearQuery } data-componentid={ `${ testId }-list-advanced-search` } - />) + > + { shouldShowMetaAttributeComponent && isFilterByMetadataAttributesEnabled && ( + + ) } + + ) } currentListSize={ organizationList?.organizations?.length } listItemLimit={ listItemLimit } diff --git a/modules/i18n/src/models/namespaces/common-ns.ts b/modules/i18n/src/models/namespaces/common-ns.ts index 66fbdcd98ed..ece16adc9b1 100644 --- a/modules/i18n/src/models/namespaces/common-ns.ts +++ b/modules/i18n/src/models/namespaces/common-ns.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -104,6 +104,7 @@ export interface CommonNS { logout: string; maximize: string; maxValidation: string; + metaAttributes: string; minimize: string; minValidation: string; more: string; diff --git a/modules/i18n/src/models/namespaces/organizations-ns.ts b/modules/i18n/src/models/namespaces/organizations-ns.ts index 38f070f9426..51f228b0aad 100644 --- a/modules/i18n/src/models/namespaces/organizations-ns.ts +++ b/modules/i18n/src/models/namespaces/organizations-ns.ts @@ -28,6 +28,13 @@ export interface organizationsNS { filterValue: { placeholder: string; }; + filterMetaAttribute: { + label: string; + placeholder: string; + validations: { + empty: string; + }; + }; }; }; placeholder: string; @@ -153,6 +160,16 @@ export interface organizationsNS { description: string; }; }; + getMetaAttributesList: { + error: { + message: string; + description: string; + }; + genericError: { + message: string; + description: string; + }; + }; }; confirmations: { deleteOrganization: { diff --git a/modules/i18n/src/translations/de-DE/portals/common.ts b/modules/i18n/src/translations/de-DE/portals/common.ts index 22f024916ae..0911a13a1db 100644 --- a/modules/i18n/src/translations/de-DE/portals/common.ts +++ b/modules/i18n/src/translations/de-DE/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { "logout": "Logout", "maxValidation": "Dieser Wert sollte kleiner oder gleich {{max}} sein.", "maximize": "Maximieren", + "metaAttributes": "Metaattribute", "minValidation": "Dieser Wert sollte größer oder gleich {{min}} sein.", "minimize": "Minimieren", "more": "Mehr", diff --git a/modules/i18n/src/translations/en-US/portals/common.ts b/modules/i18n/src/translations/en-US/portals/common.ts index 966656a16a5..af966decbdc 100755 --- a/modules/i18n/src/translations/en-US/portals/common.ts +++ b/modules/i18n/src/translations/en-US/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { logout: "Logout", maxValidation: "This value should be less than or equal to {{max}}.", maximize: "Maximize", + metaAttributes: "Meta Attributes", minValidation: "This value should be greater than or equal to {{min}}.", minimize: "Minimize", more: "More", diff --git a/modules/i18n/src/translations/en-US/portals/organizations.ts b/modules/i18n/src/translations/en-US/portals/organizations.ts index 243fa7a28fc..9e9704b845e 100644 --- a/modules/i18n/src/translations/en-US/portals/organizations.ts +++ b/modules/i18n/src/translations/en-US/portals/organizations.ts @@ -27,12 +27,19 @@ export const organizations: organizationsNS = { filterCondition: { placeholder: "E.g. Starts With etc." }, + filterMetaAttribute: { + label: "Filter meta attribute", + placeholder: "Search by meta attribute name", + validations: { + empty: "Filter meta attribute is a required field." + } + }, filterValue: { placeholder: "Enter value to search" } } }, - placeholder: "Search by Name" + placeholder: "Search organizations by name, or meta attribute" }, confirmations: { deleteOrganization: { @@ -222,6 +229,16 @@ export const organizations: organizationsNS = { message: "Organization fetched successfully" } }, + getMetaAttributesList: { + error: { + description: "{{description}}", + message: "Error while getting the organization's meta attribute list" + }, + genericError: { + description: "An error occurred while getting the organization's meta attribute list", + message: "Something went wrong" + } + }, getOrganizationList: { error: { description: "{{description}}", @@ -273,24 +290,14 @@ export const organizations: organizationsNS = { title: "Add a new Organization" } }, - shareApplicationSubTitle: "Select one of the following options to share the application.", - shareApplicationRadio: "Share with all organizations", shareApplicationInfo: "Select this to share the application with all the existing organizations " + "and all new organizations that you create under your current organization.", - unshareApplicationRadio: "Do not share with any organization", + shareApplicationRadio: "Share with all organizations", + shareApplicationSubTitle: "Select one of the following options to share the application.", shareWithSelectedOrgsRadio: "Share with only selected organizations", - unshareApplicationInfo: "This will allow you to prevent sharing this application with any of the " + - "existing organizations or new organizations that you create under this organization " + - "in the future.", switching: { emptyList: "There are no organizations to show.", goBack: "Go back", - search: { - placeholder: "Search by Name" - }, - subOrganizations: "Organizations", - switchLabel: "Organization", - switchButton: "Switch to Organization", notifications: { switchOrganization: { genericError: { @@ -298,9 +305,19 @@ export const organizations: organizationsNS = { message: "Something went wrong" } } - } + }, + search: { + placeholder: "Search by Name" + }, + subOrganizations: "Organizations", + switchButton: "Switch to Organization", + switchLabel: "Organization" }, title: "Organizations", + unshareApplicationInfo: "This will allow you to prevent sharing this application with any of the " + + "existing organizations or new organizations that you create under this organization " + + "in the future.", + unshareApplicationRadio: "Do not share with any organization", view: { description: "View Organization" } diff --git a/modules/i18n/src/translations/es-ES/portals/common.ts b/modules/i18n/src/translations/es-ES/portals/common.ts index f126bf26453..cd8d42e8c5a 100644 --- a/modules/i18n/src/translations/es-ES/portals/common.ts +++ b/modules/i18n/src/translations/es-ES/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { logout: "Cerrar sesión", maxValidation: "Este valor debe ser menor o igual a {{max}}.", maximize: "Maximizar", + metaAttributes: "Metaatributos", minValidation: "Este valor debe ser mayor o igual a {{min}}.", minimize: "Minimizar", more: "más", diff --git a/modules/i18n/src/translations/fr-FR/portals/common.ts b/modules/i18n/src/translations/fr-FR/portals/common.ts index c5d39412419..9a9f94ec193 100755 --- a/modules/i18n/src/translations/fr-FR/portals/common.ts +++ b/modules/i18n/src/translations/fr-FR/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -115,6 +115,7 @@ export const common: CommonNS = { logout: "Déconnexion", maxValidation: "Cette valeur doit être inférieure ou égale à {{max}}.", maximize: "Maximiser", + metaAttributes: "Méta-attributs", minValidation: "Cette valeur doit être supérieure ou égale à {{min}}.", minimize: "Minimiser", more: "Plus", diff --git a/modules/i18n/src/translations/ja-JP/portals/common.ts b/modules/i18n/src/translations/ja-JP/portals/common.ts index 34538f08064..72559214a7b 100644 --- a/modules/i18n/src/translations/ja-JP/portals/common.ts +++ b/modules/i18n/src/translations/ja-JP/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { "logout": "ログアウト", "maxValidation": "この値は、{{max}}以下でなければなりません。", "maximize": "最大化します", + "metaAttributes": "メタ属性", "minValidation": "この値は{{min}}以上である必要があります。", "minimize": "最小化します", "more": "もっと", diff --git a/modules/i18n/src/translations/pt-BR/portals/common.ts b/modules/i18n/src/translations/pt-BR/portals/common.ts index ab071f6349b..b8024c105e0 100755 --- a/modules/i18n/src/translations/pt-BR/portals/common.ts +++ b/modules/i18n/src/translations/pt-BR/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { logout: "Logout", maxValidation: "Este valor deve ser menor ou igual a {{max}}.", maximize: "Maximizar", + metaAttributes: "Metaatributos", minValidation: "Este valor deve ser maior ou igual a {{min}}.", minimize: "Minimizar", more: "Mais", diff --git a/modules/i18n/src/translations/pt-PT/portals/common.ts b/modules/i18n/src/translations/pt-PT/portals/common.ts index f1042c6b17e..3896e003e16 100755 --- a/modules/i18n/src/translations/pt-PT/portals/common.ts +++ b/modules/i18n/src/translations/pt-PT/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { logout: "Sair", maxValidation: "Este valor deve ser menor ou igual a {{max}}.", maximize: "maximizar", + metaAttributes: "Metaatributos", minValidation: "Este valor deve ser maior ou igual a {{min}}.", minimize: "minimizar", more: "Mais", diff --git a/modules/i18n/src/translations/si-LK/portals/common.ts b/modules/i18n/src/translations/si-LK/portals/common.ts index 01fa09c9e6c..6a474be30c6 100755 --- a/modules/i18n/src/translations/si-LK/portals/common.ts +++ b/modules/i18n/src/translations/si-LK/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { logout: "වරන්න", maxValidation: "මෙම අගය {{max}} ට වඩා අඩු හෝ සමාන විය යුතුය.", maximize: "උපරිම කරන්න", + metaAttributes: "අතිරේක ගුණාංග", minValidation: "මෙම අගය {{min}} ට වඩා වැඩි හෝ සමාන විය යුතුය.", minimize: "අවම කරන්න", more: "තව", diff --git a/modules/i18n/src/translations/ta-IN/portals/common.ts b/modules/i18n/src/translations/ta-IN/portals/common.ts index a7ece5ef710..38fd648748f 100755 --- a/modules/i18n/src/translations/ta-IN/portals/common.ts +++ b/modules/i18n/src/translations/ta-IN/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -115,6 +115,7 @@ export const common: CommonNS = { logout: "வெளியேறு", maxValidation: "இந்த மதிப்பு {{max}} ஐ விட குறைவாகவோ அல்லது சமமாகவோ இருக்க வேண்டும்.", maximize: "பெரிதாக்கு", + metaAttributes: "மெட்டா பண்புக்கூறுகள்", minValidation: "இந்த மதிப்பு {{min}} ஐ விட அதிகமாகவோ அல்லது சமமாகவோ இருக்க வேண்டும்.", minimize: "குறைத்தல்", more: "மேலும்", diff --git a/modules/i18n/src/translations/zh-CN/portals/common.ts b/modules/i18n/src/translations/zh-CN/portals/common.ts index a0b60b1a48a..b7fcd679a5a 100755 --- a/modules/i18n/src/translations/zh-CN/portals/common.ts +++ b/modules/i18n/src/translations/zh-CN/portals/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -114,6 +114,7 @@ export const common: CommonNS = { "logout": "登出", "maxValidation": "此值应小于或等于{{max}}。", "maximize": "最大化", + "metaAttributes": "元属性", "minValidation": "该值应大于或等于{{min}}。", "minimize": "最小化", "more": "更多的", diff --git a/modules/react-components/src/components/input/advanced-search.tsx b/modules/react-components/src/components/input/advanced-search.tsx index ba84caa2d3e..7a5bb0b5829 100755 --- a/modules/react-components/src/components/input/advanced-search.tsx +++ b/modules/react-components/src/components/input/advanced-search.tsx @@ -307,8 +307,13 @@ export const AdvancedSearch: FunctionComponent 2) { + const filterAttributeParts = terms[0].split("."); + const filterAttribute = filterAttributeParts[0] === "attributes" + ? filterAttributeParts[0] + : terms[0]; + const attributes = filterAttributeOptions.filter((attribute) => { - return attribute.value === terms[0]; + return attribute.value === filterAttribute; }); if (attributes.length > 0) {