Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added search to organization page #9151

Merged
merged 12 commits into from
Mar 5, 2025
3 changes: 3 additions & 0 deletions changelog.d/20250226_153639_klakhov_org_search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Search bar and filtering components on the organization page (<https://github.com/cvat-ai/cvat/pull/9151>)
38 changes: 27 additions & 11 deletions cvat-core/src/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
// SPDX-License-Identifier: MIT

import {
OrganizationMembersFilter,
SerializedInvitationData, SerializedOrganization, SerializedOrganizationContact, SerializedUser,
} from './server-response-types';
import { checkObjectType, isEnum } from './common';
import {
checkFilter, checkObjectType, fieldsToSnakeCase, isEnum, isInteger, isString,
} from './common';
import config from './config';
import { MembershipRole } from './enums';
import { ArgumentError, DataError } from './exceptions';
Expand Down Expand Up @@ -143,13 +146,14 @@ export default class Organization {
}

// Method returns paginatable list of organization members
public async members(page = 1, page_size = 10): Promise<Membership[]> {
public async members(filter: OrganizationMembersFilter = { page: 1, pageSize: 10 }): Promise<Membership[]> {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.members,
this.slug,
page,
page_size,
{
...filter,
org: this.slug,
},
);
return result;
}
Expand Down Expand Up @@ -318,12 +322,21 @@ Object.defineProperties(Organization.prototype.members, {
implementation: {
writable: false,
enumerable: false,
value: async function implementation(orgSlug: string, page: number, pageSize: number) {
checkObjectType('orgSlug', orgSlug, 'string');
checkObjectType('page', page, 'number');
checkObjectType('pageSize', pageSize, 'number');
value: async function implementation(
filter: Parameters<typeof Organization.prototype.members>[0],
) {
checkFilter(filter, {
org: isString,
page: isInteger,
pageSize: isInteger,
search: isString,
filter: isString,
sort: isString,
});

const params = fieldsToSnakeCase(filter);
const result = await serverProxy.organizations.members(params);

const result = await serverProxy.organizations.members(orgSlug, page, pageSize);
const memberships = await Promise.all(result.results.map(async (rawMembership) => {
const { invitation } = rawMembership;
let rawInvitation = null;
Expand Down Expand Up @@ -415,7 +428,10 @@ Object.defineProperties(Organization.prototype.leave, {
value: async function implementation(user: User) {
checkObjectType('user', user, null, User);
if (typeof this.id === 'number') {
const result = await serverProxy.organizations.members(this.slug, 1, 10, {
const result = await serverProxy.organizations.members({
page: 1,
pageSize: 10,
org: this.slug,
filter: JSON.stringify({
and: [{
'==': [{ var: 'user' }, user.username],
Expand Down
9 changes: 2 additions & 7 deletions cvat-core/src/server-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@
return new ServerError(message, 0);
}

function prepareData(details) {

Check warning on line 248 in cvat-core/src/server-proxy.ts

View workflow job for this annotation

GitHub Actions / Linter

Missing return type on function
const data = new FormData();
for (const [key, value] of Object.entries(details)) {
if (Array.isArray(value)) {
Expand Down Expand Up @@ -287,7 +287,7 @@
return requestId++;
}

async function get(url: string, requestConfig) {

Check warning on line 290 in cvat-core/src/server-proxy.ts

View workflow job for this annotation

GitHub Actions / Linter

Missing return type on function
return new Promise((resolve, reject) => {
const newRequestId = getRequestId();
requests[newRequestId] = { resolve, reject };
Expand Down Expand Up @@ -783,7 +783,7 @@
else requestBody.job_id = id;

return new Promise<void>((resolve, reject) => {
async function request() {

Check warning on line 786 in cvat-core/src/server-proxy.ts

View workflow job for this annotation

GitHub Actions / Linter

Missing return type on function
try {
const response = await Axios.post(url, requestBody, { params });
params.rq_id = response.data.rq_id;
Expand Down Expand Up @@ -1915,18 +1915,13 @@
}
}

async function getOrganizationMembers(orgSlug, page, pageSize, filters = {}) {
async function getOrganizationMembers(params = {}) {
const { backendAPI } = config;

let response = null;
try {
response = await Axios.get(`${backendAPI}/memberships`, {
params: {
...filters,
org: orgSlug,
page,
page_size: pageSize,
},
params,
});
} catch (errorData) {
throw generateError(errorData);
Expand Down
3 changes: 3 additions & 0 deletions cvat-core/src/server-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,6 @@ export interface SerializedTaskValidationLayout extends SerializedJobValidationL
validation_frames?: number[];
disabled_frames?: number[];
}

export interface APIOrganizationMembersFilter extends APICommonFilterParams {}
export type OrganizationMembersFilter = Camelized<APIOrganizationMembersFilter>;
17 changes: 17 additions & 0 deletions cvat-ui/src/components/organization-page/empty-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React from 'react';
import Text from 'antd/lib/typography/Text';
import Empty from 'antd/lib/empty';

function EmptyListComponent(): JSX.Element {
return (
<div className='cvat-empty-members-list'>
<Empty description={<Text strong>No results matched your search...</Text>} />
</div>
);
}

export default React.memo(EmptyListComponent);
113 changes: 113 additions & 0 deletions cvat-ui/src/components/organization-page/invitation-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React from 'react';
import { useForm } from 'antd/lib/form/Form';
import Form from 'antd/lib/form';
import { Row, Col } from 'antd/lib/grid';
import Modal from 'antd/lib/modal';
import Paragraph from 'antd/lib/typography/Paragraph';
import Text from 'antd/lib/typography/Text';
import Select from 'antd/lib/select';
import { Store } from 'antd/lib/form/interface';
import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons';
import Button from 'antd/lib/button';
import Input from 'antd/lib/input';

interface Props {
onInvite: (values: Store) => void;
onCancelInvite: () => void;
}

function InvitationModal(props: Props): JSX.Element {
const { onInvite, onCancelInvite } = props;
const [form] = useForm();

return (
<Modal
className='cvat-organization-invitation-modal'
open
onCancel={() => {
form.resetFields(['users']);
onCancelInvite();
}}
destroyOnClose
onOk={() => {
form.submit();
}}
>
<Form
initialValues={{
users: [{ email: '', role: 'worker' }],
}}
onFinish={(values: Store) => {
form.resetFields(['users']);
onInvite(values);
}}
layout='vertical'
form={form}
>
<Paragraph>
<Text>Invite CVAT users to collaborate </Text>
</Paragraph>
<Paragraph>
<Text type='secondary'>
If the email address is registered on CVAT, the user will be added to the organization
</Text>
</Paragraph>
<Form.List name='users'>
{(fields, { add, remove }) => (
<>
{fields.map((field: any, index: number) => (
<Row className='cvat-organization-invitation-field' key={field.key}>
<Col span={10}>
<Form.Item
className='cvat-organization-invitation-field-email'
hasFeedback
name={[field.name, 'email']}
fieldKey={[field.fieldKey, 'email']}
rules={[
{ required: true, message: 'This field is required' },
{ type: 'email', message: 'The input is not a valid email' },
]}
>
<Input placeholder='Enter an email address' />
</Form.Item>
</Col>
<Col span={10} offset={1}>
<Form.Item
className='cvat-organization-invitation-field-role'
name={[field.name, 'role']}
fieldKey={[field.fieldKey, 'role']}
initialValue='worker'
rules={[{ required: true, message: 'This field is required' }]}
>
<Select>
<Select.Option value='worker'>Worker</Select.Option>
<Select.Option value='supervisor'>Supervisor</Select.Option>
<Select.Option value='maintainer'>Maintainer</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={1} offset={1}>
{index > 0 ? (
<DeleteOutlined onClick={() => remove(field.name)} />
) : null}
</Col>
</Row>
))}
<Form.Item>
<Button className='cvat-invite-more-org-members-button' icon={<PlusCircleOutlined />} onClick={() => add()}>
Invite more
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
);
}

export default React.memo(InvitationModal);
15 changes: 11 additions & 4 deletions cvat-ui/src/components/organization-page/members-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { resendInvitationAsync } from 'actions/invitations-actions';
import { Membership } from 'cvat-core-wrapper';
import MemberItem from './member-item';
import EmptyListComponent from './empty-list';

export interface Props {
organizationInstance: any;
Expand All @@ -37,11 +38,13 @@ function MembersList(props: Props): JSX.Element {
const updatingMember = useSelector((state: CombinedState) => state.organizations.updatingMember);
const removingMember = useSelector((state: CombinedState) => state.organizations.removingMember);

return fetching || inviting || updatingMember || removingMember ? (
<Spin className='cvat-spinner' />
) : (
if (fetching || inviting || updatingMember || removingMember) {
return <Spin className='cvat-spinner' />;
}

const content = members.length ? (
<>
<div>
<div className='cvat-organization-members-list'>
{members.map(
(member: Membership): JSX.Element => (
<MemberItem
Expand Down Expand Up @@ -94,7 +97,11 @@ function MembersList(props: Props): JSX.Element {
/>
</div>
</>
) : (
<EmptyListComponent />
);

return content;
}

export default React.memo(MembersList);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import { Config } from '@react-awesome-query-builder/antd';
import asyncFetchUsers from 'components/resource-sorting-filtering/request-users';

export const config: Partial<Config> = {
fields: {
user: {
label: 'User',
type: 'select',
valueSources: ['value'],
operators: ['select_equals'],
fieldSettings: {
useAsyncSearch: true,
forceAsyncSearch: true,
asyncFetch: asyncFetchUsers,
},
},
role: {
label: 'Role',
type: 'select',
operators: ['select_equals'],
valueSources: ['value'],
fieldSettings: {
listValues: [
{ value: 'worker', title: 'Worker' },
{ value: 'supervisor', title: 'Supervisor' },
{ value: 'maintainer', title: 'Maintainer' },
{ value: 'owner', title: 'Owner' },
],
},
},
},
};

export const localStorageRecentCapacity = 10;
export const localStorageRecentKeyword = 'recentlyAppliedMembershipsFilters';
export const predefinedFilterValues = {
Workers: '{"and":[{"==":[{"var":"role"},"worker"]}]}',
Supervisors: '{"and":[{"==":[{"var":"role"},"supervisor"]}]}',
Maintainers: '{"and":[{"==":[{"var":"role"},"maintainer"]}]}',
};
Loading
Loading