superdesk/superdesk-client-core

View on GitHub
scripts/apps/dashboard/user-activity/components/UserActivityWidget.tsx

Summary

Maintainability
D
1 day
Test Coverage
import React from 'react';
import {IArticle, IUser, IStage} from 'superdesk-api';
import {AuthoringWorkspaceService} from 'apps/authoring/authoring/services';
import ng from 'core/services/ng';
import {gettext} from 'core/utils';
import {SelectUser} from 'core/ui/components/SelectUser';
import {logger} from 'core/services/logger';
import {extensions} from 'appConfig';
import {Loader} from 'core/ui/components/Loader';
import {GroupComponent} from './Group';

type FetchFunction = (repo: string, criteria: any) => Promise<any>;

export interface IGroup {
    id: string;
    label: string;
    precondition?: () => boolean;
    dataSource:
        {repo: string, query: object}
        | ((f: FetchFunction) => Promise<any>);
    collapsed?: boolean;
}

export interface IGroupData {
    id: any;
    itemIds: any;
    itemsById: any;
    total: number;
}

interface IProps {
    header?: boolean;
    user: IUser;
    onUserChange(user: IUser): void;
}

interface IState {
    userSearchField: string;
    groups: Array<IGroup> | null;
    groupsData: Array<IGroupData> | null;
    loading: boolean;
}

function genericFetch({search, api}, repo, filters) {
    let query = search.query();

    query.clear_filters();
    query.size(200).filter(filters);

    let criteria = query.getCriteria(true);

    criteria.repo = repo;
    criteria.source.from = 0;
    criteria.source.size = 200;

    return api.query('search', criteria);
}

function getQueryLockedByUser(userId) {
    return {
        term: {
            lock_user: userId,
        },
    };
}

function getQueryNotLockedByMe(userId) {
    return {
        bool: {
            must_not: {
                term: {
                    lock_user: userId,
                },
            },
        },
    };
}

function getQueryCreatedByUser(userId) {
    return {
        terms: {
            original_creator: [userId],
        },
    };
}

const GET_GROUPS = (userId, services: any): Array<IGroup> => {
    return [
        {
            id: 'locked',
            label: gettext('Locked by this user'),
            dataSource: {
                repo: 'archive',
                query: {...getQueryLockedByUser(userId)},
            },
        },
        {
            id: 'marked',
            label: gettext('Marked for this user'),
            precondition() {
                return extensions.hasOwnProperty('markForUser');
            },
            dataSource(fetchFn) {
                const query = extensions['markForUser'].extension.exposes.getQueryMarkedForUser(userId);

                return fetchFn('archive', query);
            },
        },
        {
            id: 'created',
            label: gettext('Created by this user'),
            dataSource(fetchFn) {
                const markedQuery = extensions['markForUser']
                    ?.extension
                    .exposes
                    .getQueryNotMarkedForAnyoneOrMarkedForMe(userId);

                const mustQuery = [
                    {...getQueryCreatedByUser(userId)},
                    {...getQueryNotLockedByMe(userId)},
                ];

                if (markedQuery != null) {
                    mustQuery.push({...markedQuery});
                }

                return ng.get('desks').fetchStages().then((stages) => {
                    const incomingStages = stages._items
                        .filter(({default_incoming}) => default_incoming === true)
                        .map(({_id}) => _id);

                    const isNotOnIncomingStage: any = {
                        bool: {
                            must_not: {
                                terms: {
                                    'task.stage': incomingStages,
                                },
                            },
                        },
                    };

                    mustQuery.push({...isNotOnIncomingStage});

                    return fetchFn('archive', {
                        bool: {
                            must: mustQuery,
                        },
                    });
                });
            },
        },
        {
            id: 'moved',
            label: gettext('Moved to a working stage by this user'),
            dataSource(fetchFn) {
                return ng.get('desks').fetchStages().then((stages) => {
                    const workingStages = stages._items
                        .filter(({working_stage}) => working_stage === true)
                        .map(({_id}) => _id);

                    const inOnWorkingStage: any = {
                        terms: {
                            'task.stage': workingStages,
                        },
                    };

                    const movedByUser = [
                        {match: {'task.user': userId}},
                        {term: {operation: 'move'}},
                    ];

                    return fetchFn(
                        'archive_history',
                        {
                            bool: {
                                must: [
                                    ...movedByUser,
                                    inOnWorkingStage,
                                ],
                            },
                        },
                    );
                });
            },
        },
    ];
};

export default class UserActivityWidget extends React.Component<IProps, IState> {
    services: any;
    removeListeners: Array<() => void>;

    constructor(props) {
        super(props);

        this.services = {
            $anchorScroll: ng.get('$anchorScroll'),
            $location: ng.get('$location'),
            $q: ng.get('$q'),
            $rootScope: ng.get('$rootScope'),
            $timeout: ng.get('$timeout'),
            activityService: ng.get('activityService'),
            api: ng.get('api'),
            authoringWorkspace: ng.get(
                'authoringWorkspace',
            ) as AuthoringWorkspaceService,
            cards: ng.get('cards'),
            datetime: ng.get('datetime'),
            desks: ng.get('desks'),
            metadata: ng.get('metadata'),
            search: ng.get('search'),
            superdesk: ng.get('superdesk'),
            session: ng.get('session'),
        };

        this.state = {
            userSearchField: '',
            groups: null,
            groupsData: null,
            loading: false,
        };

        this.refreshItems = this.refreshItems.bind(this);
        this.toggleCollapseExpand = this.toggleCollapseExpand.bind(this);
        this.updateGroupDataOnUserChange = this.updateGroupDataOnUserChange.bind(this);
    }

    componentDidMount() {
        this.addListeners();

        this.updateGroupDataOnUserChange(this.props.user);
    }

    componentDidUpdate(prevProps: IProps) {
        if (this.props.user._id !== prevProps.user._id) {
            this.updateGroupDataOnUserChange(this.props.user);
        }
    }

    componentWillUnmount() {
        this.removeListeners.forEach((remove) => remove());
    }

    toggleCollapseExpand(group: IGroup) {
        const newGroups = this.state.groups.map((g) => {
            if (g.id === group.id) {
                return {...g, collapsed: !g.collapsed};
            }

            return g;
        });

        this.setState({
            groups: newGroups,
        });
    }

    addListeners() {
        this.removeListeners = [
            'item:lock',
            'item:unlock',
            'item:spike',
            'item:move',
            'item:publish',
            'item:update',
        ].map((event) =>
            this.services.$rootScope.$on(event, this.refreshItems),
        );
    }

    refreshItems() {
        if (this.props.user) {
            this.fetchGroupsData();
        }
    }

    fetchItems(group: IGroup): Promise<IGroupData> {
        const {api, search} = this.services;
        const promise = typeof group.dataSource === 'function'
            ? group.dataSource((repo, filters) => genericFetch({search, api}, repo, filters))
            : genericFetch({search, api}, group.dataSource.repo, group.dataSource.query);

        return promise.then((res) => {
            const itemIds = [];
            const itemsById = {};

            for (const item of res._items) {
                itemIds.push(item._id);
                itemsById[item._id] = item;
            }

            const groupData: IGroupData = {
                id: group.id,
                itemIds,
                itemsById,
                total: res._meta.total,
            };

            return groupData;
        });
    }

    fetchGroupsData() {
        const {groups} = this.state;
        const promises = groups
            .filter((group) => {
                if (group.precondition != null) {
                    const shouldBeEnabled = group.precondition();

                    return shouldBeEnabled;
                }

                return true;
            })
            .map((group) => this.fetchItems(group));

        Promise.all(promises).then((data) => {
            this.setState({groupsData: data, loading: false});
        });
    }

    updateGroupDataOnUserChange(user: IUser) {
        this.setState(
            {groups: GET_GROUPS(user._id, this.services), loading: true},
            () => {
                this.fetchGroupsData();
            },
        );
    }

    render() {
        const {loading} = this.state;

        return (
            <div className="widget-container">
                <div className="main-list" style={{insetBlockStart: 0}}>
                    {this.props.header ? (
                        <div className="widget-header">
                            <h3 className="widget-title">
                                {gettext('User Activity')}
                            </h3>
                        </div>
                    ) : null}
                    <div
                        className="search-box search-box--no-shadow search-box--fluid-height"
                    >
                        <form className="search-box__content">
                            <SelectUser
                                selectedUserId={this.props.user?._id ?? null}
                                autoFocus={false}
                                onSelect={(user) => {
                                    this.props.onUserChange(user);
                                }}
                                horizontalSpacing={true}
                                clearable={false}
                            />
                        </form>
                    </div>
                    {
                        loading ? <Loader /> :
                            this.state.groupsData && (
                                <div className="content-list-holder">
                                    <div className="shadow-list-holder">
                                        <div className="content-list">
                                            {
                                                this.state.groups.map((group) => {
                                                    const {groupsData} = this.state;
                                                    const data = groupsData.find((g) => g.id === group.id);

                                                    if (!data) {
                                                        logger.warn(
                                                            `Tried to render group '${group.id}' but no data was found`,
                                                        );
                                                        return null;
                                                    }

                                                    return (
                                                        <div key={`user-activity-${group.id}`}>
                                                            <GroupComponent
                                                                group={group}
                                                                data={data}
                                                                toggleCollapseExpand={this.toggleCollapseExpand}
                                                            />
                                                        </div>
                                                    );
                                                })
                                            }
                                        </div>
                                    </div>
                                </div>
                            )}
                </div>
            </div>
        );
    }
}