superdesk / superdesk-ui-framework

Superdesk UI Framework
https://ui-framework.superdesk.org
GNU Affero General Public License v3.0
9 stars 24 forks source link

Component request - tree select #597

Open tomaskikutis opened 2 years ago

tomaskikutis commented 2 years ago

It needs to work in a generic way, but use cases I'm looking at right now are selecting subjects and authors.

Required features:

optionTemplate and valueTemplate are needed for custom rendering. For example, when selecting authors, avatars need to be rendered - optionTemplate would be used for this. valueTemplate would be used when listing selected values - see authors screenshot.

interface ITreeNode<T> {
    value: T;
    parent?: ITreeNode<T>;
    children?: Array<ITreeNode<T>>;
}

interface ITreeWithLookup<T> {
    nodes: Array<ITreeNode<T>>;
    lookup: {
        [id: string]: ITreeNode<T>;
    };
}

interface IPropsBase<T> {
    values: Array<T>;
    onChange(values: Array<T>): void;
    optionTemplate?: React.ComponentType<{item: T}>; // not required, it should fallback to rendering a label (from getLabel method)
    valueTemplate?: React.ComponentType<{item: T}>; // not required, it should fallback `optionTemplate` if not provided
    getId(item: T): string;
    getLabel(item: T): string;
    canSelectBranchWithChildren?: boolean;
    allowMultiple?: boolean;
    readOnly?: boolean;
}

interface IPropsSync<T> extends IPropsBase<T> {
    kind: 'synchronous';
    getOptions(): ITreeWithLookup<T>;
}

type ICancelFn = () => void;

interface IPropsAsync<T> extends IPropsBase<T> {
    kind: 'asynchronous';

    /**
     * The function will be called when a search is initiated from UI.
     * `callback` will be invoked with matching options after they are retrieved.
     * A function to cancel the asynchronous search is returned.
     */
    searchOptions(term: string, callback: (options: ITreeWithLookup<T>) => void): ICancelFn;
}

type IProps<T> = IPropsSync<T> | IPropsAsync<T>;

Note: see if you need to do lookups in the implementation. If not, use Array<ITreeNode<T>> instead of ITreeWithLookup<T>

tomaskikutis commented 2 years ago

I have modified the initial interface to support asynchronous mode that is required for selecting locations from a remote geonames API.

2022-03-24_10-47

tomaskikutis commented 2 years ago

I've added another property to the interface - allowMultiple. When it's not true, it should only allow one value and display a appropriate UI component, something like this: 2022-03-24_12-44

dzonidoo commented 2 years ago

https://github.com/superdesk/superdesk-ui-framework/pull/614

tomaskikutis commented 2 years ago

The interface doesn't match the one requested in #614

dzonidoo commented 2 years ago

https://github.com/superdesk/superdesk-ui-framework/pull/621/files

tomaskikutis commented 2 years ago
tomaskikutis commented 2 years ago

@dzonidoo I tried testing it, but as mentioned in the previous comment, tree select component isn't exported from ui-framework and thus can't be used. It should be exported via app-typescript/index.ts.

Add "testing" label again when this is fixed.

tomaskikutis commented 2 years ago

It looks better @dzonidoo, but there are still issues:

import React from 'react';
import {TreeSelect} from 'superdesk-ui-framework/react';
import {ITreeNode} from 'superdesk-ui-framework/react/components/TreeSelect';

type IProps = {};

interface IVocabularyItem {
    qcode: string;
    name: string;
}

interface IState {
    value: Array<IVocabularyItem>;
}

const source = [
    {
        'name': 'Article (news)',
        'qcode': 'Article',
    },
    {
        'name': 'Sidebar',
        'qcode': 'Sidebar',
    },
    {
        'name': 'Factbox',
        'qcode': 'Factbox',
    },
];

function searchOptions(
    term: string,
    callback: (res: Array<ITreeNode<{name: string; qcode: string;}>>) => void,
): void {
    setTimeout(() => {
        callback(
            source
                .filter((item) => item.name.toLocaleLowerCase().includes(term.toLocaleLowerCase()))
                .map((item) => ({value: item})),
        );
    }, 1000);
}

export class MultiSelectDemo extends React.PureComponent<IProps, IState> {
    constructor(props: IProps) {
        super(props);

        this.state = {
            value: [],
        };
    }

    render() {
        return (
            <TreeSelect
                kind="asynchronous"
                searchOptions={searchOptions}
                value={this.state.value}
                onChange={(val) => {
                    this.setState({value: val});
                }}
                getLabel={({name}) => name}
                getId={({qcode}) => qcode}
                selectBranchWithChildren={false}
                optionTemplate={(item) => <span style={{color: 'blue'}}>{item.name}</span>}
                allowMultiple={true}
            />
        );
    }
}
tomaskikutis commented 2 years ago

✔️ ~One more thing, could you add a prop that would limit search only to a level that is being shown at any time? It's needed in authors select, because showing "editor" role doesn't make sense when it's not clear to which user does it belong.~

tomaskikutis commented 2 years ago

@dzonidoo here are the issues I found after latest testing:

tomaskikutis commented 2 years ago

Results from latest testing on https://github.com/superdesk/superdesk-ui-framework/pull/669 at commit 8946db4

menu should open to the top if there's not enough space at the bottom (was already reported before)

This doesn't work well in async mode - video

fix flickering - after I finish typing, dropdown results change multiple times - it should only change once

doesn't look fixed - video


Newly found issues:

ACTUAL: search results for a are displayed EXPECTED: search results for b are displayed

tomaskikutis commented 2 years ago

Newly found issues:

tomaskikutis commented 2 years ago

Newly found issues:

To reproduce:

tomaskikutis commented 1 year ago

@dzonidoo I didn't do much testing yet, but one of the issues I noticed when using it is that when readOnly is true, appropriate styling isn't being applied like for other fields. image