Compare commits

...

92 Commits

Author SHA1 Message Date
Tom Moor 2c1657c7f2 Merge 2022-07-17 21:55:54 +01:00
Tom Moor 16541560eb performance 2022-07-17 21:50:45 +01:00
Tom Moor 832eb20a10 Merge branch 'main' of github.com:outline/outline into feat/dropdown-pagination 2022-07-17 21:00:37 +01:00
Tom Moor 501221a038 Styling tweaks 2022-07-17 14:22:37 +01:00
CuriousCorrelation 2f4a9378ce Change initial load limit to DEFAULT_PAGINATION_LIMIT 2022-07-17 12:22:25 +01:00
CuriousCorrelation ddab370640 Add ability to search for users 2022-07-17 12:22:25 +01:00
CuriousCorrelation c01312db24 Add user store fn to get unordered data
Getting users via pagination API returns a sorted list.
If we fetch for 3 users, they'll be sorted alphabetically,
next 3 will also be sorted alphabetically.
That means a final list will look like
[a, i, p] + [c, d, f] = [a, c, d, f, i, p]
Final result is unsorted.

At UX level, it looks like jerky dropdown
with users appearing in the middle of the list,
sometimes appearing just as user scrolls past.

`users.unorderedData` fixes that by sending
users consistently, although unsorted.
2022-07-17 12:22:25 +01:00
CuriousCorrelation f2cffc9354 fix: Typo 2022-07-17 12:22:25 +01:00
CuriousCorrelation 6fbbbcbf87 Revert TFilterOption interface changes 2022-07-17 12:22:25 +01:00
CuriousCorrelation dad3ea104b Add docs for else branch 2022-07-17 12:22:25 +01:00
CuriousCorrelation e1f6fbab7f Remove test intermediate paginateFetch proxy fn 2022-07-17 12:22:25 +01:00
CuriousCorrelation efc891d22a Rename Data suffix to Options for readability 2022-07-17 12:22:25 +01:00
CuriousCorrelation 22ca319d25 Move mount useEffect to top, convention 2022-07-17 12:22:25 +01:00
CuriousCorrelation e7d96f9257 fix: Search box not filling all space horizontally 2022-07-17 12:22:25 +01:00
CuriousCorrelation 507e6ea926 Update UserFilter paginateFetch def 2022-07-17 12:22:25 +01:00
CuriousCorrelation 5544521524 Remove generic qualifiers (used for testing) 2022-07-17 12:22:25 +01:00
CuriousCorrelation 7a57d8fbf9 Move filteredData to top level for readability 2022-07-17 12:22:25 +01:00
CuriousCorrelation d119bc65e0 Change options to be generic
Now function expects `options` to impl `PaginatedList`
2022-07-17 12:22:25 +01:00
CuriousCorrelation 175e9ca597 Change paginateFetch type
`paginateFetch` now has concrete types
2022-07-17 12:22:25 +01:00
CuriousCorrelation a7fc3494c8 Fix handleFilter dependency 2022-07-17 12:22:25 +01:00
CuriousCorrelation 384259f662 Change searchable to be optional 2022-07-17 12:22:25 +01:00
CuriousCorrelation b6e5e7ed80 Add filter clear action on blank search
This fixes stale search persisting when
clearing search characters.
2022-07-17 12:22:25 +01:00
CuriousCorrelation a7d5b70440 Update FilterOptions component with new API 2022-07-17 12:22:25 +01:00
CuriousCorrelation fec7695711 Impl PaginatedItem interface for options 2022-07-17 12:22:25 +01:00
CuriousCorrelation da1a8287f0 Reduce initial number of user to load 2022-07-17 12:22:25 +01:00
CuriousCorrelation d3934606e6 Add ability to populate filteredData on mount 2022-07-17 12:22:25 +01:00
CuriousCorrelation 6e9363b551 Fix clicking on menu not clearing previous search 2022-07-17 12:22:25 +01:00
CuriousCorrelation 0926b32957 Add searchable and paginateFetch var 2022-07-17 12:22:25 +01:00
CuriousCorrelation 1b2a5a7ee6 Implement options filter 2022-07-17 12:22:25 +01:00
CuriousCorrelation 6319e54fc0 Add PaginatedItem type req to TFilterOption
For use with `PaginatedList`
2022-07-17 12:22:25 +01:00
CuriousCorrelation b6813272c1 [WIP] Change map with MenuItem to PaginatedList 2022-07-17 12:22:25 +01:00
CuriousCorrelation 0a0946cf12 Add InputSearch styling 2022-07-17 12:22:25 +01:00
CuriousCorrelation f7d7159805 Remove PaginatedDropdown in favor of merge with FilterOptions 2022-07-17 12:22:24 +01:00
CuriousCorrelation 2fca88f621 Revert UserFilter changes 2022-07-17 12:22:24 +01:00
CuriousCorrelation 2b97a5d593 Made default item component dependency
Now parent component can set a default value,
instead of it being hardcoded "Any authors".
2022-07-17 12:22:24 +01:00
CuriousCorrelation b9e013185d Fix dropdown button label not setting 2022-07-17 12:22:24 +01:00
CuriousCorrelation a55b0b3872 Use user.id as PaginatedItem id 2022-07-17 12:22:24 +01:00
CuriousCorrelation 1cf83da73f Remove onFocus
Has no adverse effects so far, fingers crossed
2022-07-17 12:22:24 +01:00
CuriousCorrelation 61d4158d8c Impl Paginated list
Pagination seems to work fine.
`onFocus` isn't useful because
mount is handled already.

TODO: Try removing `onFocus`
2022-07-17 12:22:24 +01:00
CuriousCorrelation 8b70a52114 Add memoized options
TODO: Use `options` to create filtered data
2022-07-17 12:22:24 +01:00
CuriousCorrelation 2f7789088a Add users param to PaginatedDropdown
Not sure what `users` type should be.

TODO: Use `users` to calculate `options`
2022-07-17 12:22:24 +01:00
CuriousCorrelation 7c43d937c4 Change PaginatedDropdown to consume user store
`PaginatedDropdown` should consume user store directly,
so the pagination and refresh can be encapsulated
into one single component.

TODO: Impl memoized options calculation for
`PaginatedDropdown`
2022-07-17 12:22:24 +01:00
CuriousCorrelation 10daeb41e6 Impl author filter and mount set
TODO: Limit user display using pagination.
- Limit user display
- Scroll limited users
- Query for more

Because data would be provided by parent component,
`PaginatedDropdown` might need to use a callback
to fetch additional data.
2022-07-17 12:22:24 +01:00
CuriousCorrelation d20c6c456e Add static positioning for search box
Now search box doesn't scroll away with the content

TODO: Impl `handleOnChange`
2022-07-17 12:22:24 +01:00
CuriousCorrelation 180a41bd59 Replace FilterOptions with PaginatedDropdown
TODO: Make search bar sticky
2022-07-17 12:22:24 +01:00
CuriousCorrelation fada7032be Add PaginatedDropdown component
Based on `FilterOptions.tsx`

TODO: Replace `FilterOptions` with `PaginatedDropdown`
in `UserFilter.tsx`
2022-07-17 12:22:24 +01:00
CuriousCorrelation ba7b555dd4 Change initial load limit to DEFAULT_PAGINATION_LIMIT 2022-07-16 11:18:45 +05:30
CuriousCorrelation d6e0250b2c Merge remote-tracking branch 'origin' into feat/dropdown-pagination 2022-07-16 11:04:44 +05:30
CuriousCorrelation 6ce8eaa910 Add ability to search for users 2022-07-15 21:15:25 +05:30
CuriousCorrelation 23123e1175 Add user store fn to get unordered data
Getting users via pagination API returns a sorted list.
If we fetch for 3 users, they'll be sorted alphabetically,
next 3 will also be sorted alphabetically.
That means a final list will look like
[a, i, p] + [c, d, f] = [a, c, d, f, i, p]
Final result is unsorted.

At UX level, it looks like jerky dropdown
with users appearing in the middle of the list,
sometimes appearing just as user scrolls past.

`users.unorderedData` fixes that by sending
users consistently, although unsorted.
2022-07-15 21:07:54 +05:30
CuriousCorrelation 880d50cc87 fix: Typo 2022-07-15 13:35:24 +05:30
CuriousCorrelation 4573ed6f62 Revert TFilterOption interface changes 2022-07-15 13:02:17 +05:30
CuriousCorrelation 68f04130b8 Add docs for else branch 2022-07-15 12:52:30 +05:30
CuriousCorrelation e4b58d7119 Remove test intermediate paginateFetch proxy fn 2022-07-13 16:09:50 +05:30
CuriousCorrelation 9782f6ea21 Rename Data suffix to Options for readability 2022-07-13 15:52:19 +05:30
CuriousCorrelation 2ad2dbc69e Move mount useEffect to top, convention 2022-07-13 15:51:31 +05:30
CuriousCorrelation 64992aab51 Merge branch 'main' into feat/dropdown-pagination 2022-07-13 14:54:57 +05:30
CuriousCorrelation 2cbe07fc6a fix: Search box not filling all space horizontally 2022-07-12 23:40:59 +05:30
CuriousCorrelation ec51e495a1 Update UserFilter paginateFetch def 2022-07-12 17:35:25 +05:30
CuriousCorrelation 6a0d1de57a Remove generic qualifiers (used for testing) 2022-07-12 17:34:45 +05:30
CuriousCorrelation 4efa4103c3 Move filteredData to top level for readability 2022-07-11 23:34:25 +05:30
CuriousCorrelation 87ac604344 Change options to be generic
Now function expects `options` to impl `PaginatedList`
2022-07-11 23:12:54 +05:30
CuriousCorrelation 25acbe04f5 Change paginateFetch type
`paginateFetch` now has concrete types
2022-07-11 23:11:19 +05:30
CuriousCorrelation 1d39359e36 Fix handleFilter dependency 2022-07-11 22:26:47 +05:30
CuriousCorrelation bbfcc7f96c Change searchable to be optional 2022-07-11 19:30:27 +05:30
CuriousCorrelation 0fe220164e Add filter clear action on blank search
This fixes stale search persisting when
clearing search characters.
2022-07-11 19:20:06 +05:30
CuriousCorrelation 933fb815e7 Update FilterOptions component with new API 2022-07-11 18:16:42 +05:30
CuriousCorrelation 9ecd40ce76 Impl PaginatedItem interface for options 2022-07-11 18:16:17 +05:30
CuriousCorrelation e41aaac07b Reduce initial number of user to load 2022-07-11 18:15:41 +05:30
CuriousCorrelation e51468f50b Add ability to populate filteredData on mount 2022-07-11 18:14:55 +05:30
CuriousCorrelation 11018578b7 Fix clicking on menu not clearing previous search 2022-07-11 18:14:39 +05:30
CuriousCorrelation 63622c2f66 Add searchable and paginateFetch var 2022-07-11 18:14:03 +05:30
CuriousCorrelation 2b790cdc2f Implement options filter 2022-07-11 18:13:30 +05:30
CuriousCorrelation 06360fd7c2 Add PaginatedItem type req to TFilterOption
For use with `PaginatedList`
2022-07-11 18:11:30 +05:30
CuriousCorrelation a72700b408 [WIP] Change map with MenuItem to PaginatedList 2022-07-11 18:10:21 +05:30
CuriousCorrelation 5972eb2232 Add InputSearch styling 2022-07-11 18:07:17 +05:30
CuriousCorrelation 1201bd85a9 Remove PaginatedDropdown in favor of merge with FilterOptions 2022-07-11 17:56:18 +05:30
CuriousCorrelation 10d5941e85 Revert UserFilter changes 2022-07-11 17:55:53 +05:30
CuriousCorrelation 734e752b76 Made default item component dependency
Now parent component can set a default value,
instead of it being hardcoded "Any authors".
2022-07-10 22:17:27 +05:30
CuriousCorrelation 3941474191 Fix dropdown button label not setting 2022-07-10 21:48:01 +05:30
CuriousCorrelation 3becb00102 Use user.id as PaginatedItem id 2022-07-10 21:01:47 +05:30
CuriousCorrelation dac60cd4a0 Remove onFocus
Has no adverse effects so far, fingers crossed
2022-07-10 20:07:21 +05:30
CuriousCorrelation 966f449156 Impl Paginated list
Pagination seems to work fine.
`onFocus` isn't useful because
mount is handled already.

TODO: Try removing `onFocus`
2022-07-10 20:06:01 +05:30
CuriousCorrelation 5e0893ebc9 Add memoized options
TODO: Use `options` to create filtered data
2022-07-10 20:03:56 +05:30
CuriousCorrelation 3ad1187977 Add users param to PaginatedDropdown
Not sure what `users` type should be.

TODO: Use `users` to calculate `options`
2022-07-10 20:01:45 +05:30
CuriousCorrelation 57b03936eb Change PaginatedDropdown to consume user store
`PaginatedDropdown` should consume user store directly,
so the pagination and refresh can be encapsulated
into one single component.

TODO: Impl memoized options calculation for
`PaginatedDropdown`
2022-07-10 19:59:09 +05:30
CuriousCorrelation 324f4b133d Impl author filter and mount set
TODO: Limit user display using pagination.
- Limit user display
- Scroll limited users
- Query for more

Because data would be provided by parent component,
`PaginatedDropdown` might need to use a callback
to fetch additional data.
2022-07-10 18:49:54 +05:30
CuriousCorrelation b0ca6b5567 Add static positioning for search box
Now search box doesn't scroll away with the content

TODO: Impl `handleOnChange`
2022-07-10 17:53:11 +05:30
CuriousCorrelation 28178846fa Replace FilterOptions with PaginatedDropdown
TODO: Make search bar sticky
2022-07-10 16:56:43 +05:30
CuriousCorrelation c80a490340 Add PaginatedDropdown component
Based on `FilterOptions.tsx`

TODO: Replace `FilterOptions` with `PaginatedDropdown`
in `UserFilter.tsx`
2022-07-10 16:55:07 +05:30
CuriousCorrelation c39c907d72 Merge remote-tracking branch 'origin/tom/chore-developer-test-users' into feat/dropdown-pagination 2022-07-10 16:04:30 +05:30
Tom Moor eeff46b6c9 Add ability to quickly create test users in development 2022-07-10 09:13:24 +02:00
6 changed files with 160 additions and 35 deletions
+3
View File
@@ -41,6 +41,7 @@ type Props = {
visible?: boolean;
placement?: Placement;
animating?: boolean;
className?: string;
unstable_disclosureRef?: React.RefObject<HTMLElement | null>;
onOpen?: () => void;
onClose?: () => void;
@@ -51,6 +52,7 @@ const ContextMenu: React.FC<Props> = ({
children,
onOpen,
onClose,
className,
...rest
}) => {
const previousVisible = usePrevious(rest.visible);
@@ -131,6 +133,7 @@ const ContextMenu: React.FC<Props> = ({
topAnchor={topAnchor}
rightAnchor={rightAnchor}
ref={backgroundRef}
className={className}
hiddenScrollbars
style={
maxHeight && topAnchor
+123 -25
View File
@@ -1,12 +1,16 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
import Button, { Inner } from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import Text from "~/components/Text";
import { Outline } from "./Input";
import InputSearch from "./InputSearch";
import PaginatedList, { PaginatedItem } from "./PaginatedList";
type TFilterOption = {
type TFilterOption = PaginatedItem & {
key: string;
label: string;
note?: string;
@@ -19,54 +23,121 @@ type Props = {
selectedPrefix?: string;
className?: string;
onSelect: (key: string | null | undefined) => void;
search?: (query: string) => Promise<TFilterOption[]>;
paginateFetch?: (
options: PaginatedItem
) => Promise<PaginatedItem[] | undefined>;
};
const FilterOptions = ({
options,
activeKey = "",
defaultLabel = "Filter options",
defaultLabel,
selectedPrefix = "",
className,
onSelect,
search,
paginateFetch,
}: Props) => {
const { t } = useTranslation();
const tDefaultLabel: string = defaultLabel ?? t("Filter options");
const menu = useMenuState({
modal: true,
});
const [filteredOptions, setFilteredOptions] = React.useState<TFilterOption[]>(
[]
);
React.useEffect(() => {
setFilteredOptions(options);
}, [options]);
const selected =
options.find((option) => option.key === activeKey) || options[0];
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
const clearFilter = React.useCallback(() => {
setFilteredOptions(options);
}, [options]);
// Simple case-insensitive filter to
// check if text appears in any author's name.
const handleFilter = React.useCallback(
async (event) => {
const { value } = event.target;
if (value) {
const res = options.filter((option) =>
option.label.toLowerCase().includes(value.toLowerCase())
);
if (search) {
const more = await search(value);
const missing = more.filter(
(item) => !res.map((r) => r.key).includes(item.key)
);
setFilteredOptions([...missing, ...res]);
} else {
setFilteredOptions(res);
}
} else {
// Clears filter options cache.
// This part fires off when search term is "".
// Either by user clearing it entirely or
// by deleting one character at a time,
// gradually decreasing relevance.
clearFilter();
}
},
[clearFilter, options, search]
);
const renderItem = React.useCallback(
(option: TFilterOption) => (
<MenuItem
key={option.key}
onClick={() => {
onSelect(option.key);
menu.hide();
}}
selected={option.key === activeKey}
{...menu}
>
{option.note ? (
<LabelWithNote>
{option.label}
<Note>{option.note}</Note>
</LabelWithNote>
) : (
option.label
)}
</MenuItem>
),
[activeKey, activeKey, onSelect]
);
return (
<Wrapper>
<MenuButton {...menu}>
<MenuButton {...menu} onClick={clearFilter}>
{(props) => (
<StyledButton {...props} className={className} neutral disclosure>
{activeKey ? selectedLabel : defaultLabel}
{activeKey ? selectedLabel : tDefaultLabel}
</StyledButton>
)}
</MenuButton>
<ContextMenu aria-label={defaultLabel} {...menu}>
{options.map((option) => (
<MenuItem
key={option.key}
onClick={() => {
onSelect(option.key);
menu.hide();
}}
selected={option.key === activeKey}
{...menu}
>
{option.note ? (
<LabelWithNote>
{option.label}
<Note>{option.note}</Note>
</LabelWithNote>
) : (
option.label
)}
</MenuItem>
))}
<ContextMenu aria-label={tDefaultLabel} {...menu}>
{search && (
<>
<StyledInputSearch onChange={handleFilter} autoFocus />
<br />
</>
)}
<PaginatedList
items={filteredOptions}
fetch={paginateFetch}
renderItem={renderItem}
/>
</ContextMenu>
</Wrapper>
);
@@ -110,4 +181,31 @@ const Wrapper = styled.div`
margin-right: 8px;
`;
// `position: sticky` leaves a bit of space above the search box,
// which shows author names moving past behind it.
const StyledInputSearch = styled(InputSearch)`
position: absolute;
width: 100%;
border: none;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
overflow: hidden;
top: 0;
z-index: 1;
${Outline} {
border-top-style: unset;
border-right-style: unset;
border-left-style: unset;
border-radius: unset;
border-bottom: 1px solid ${(props) => props.theme.divider};
background: ${(props) => props.theme.menuBackground};
font-size: 14px;
input {
margin-left: 8px;
}
}
`;
export default FilterOptions;
+1 -1
View File
@@ -94,7 +94,7 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
}
this.isFetching = true;
const counter = ++this.fetchCounter;
const limit = DEFAULT_PAGINATION_LIMIT;
const limit = this.props.options?.limit ?? DEFAULT_PAGINATION_LIMIT;
this.error = undefined;
try {
+18 -8
View File
@@ -14,25 +14,33 @@ function UserFilter(props: Props) {
const { t } = useTranslation();
const { users } = useStores();
React.useEffect(() => {
users.fetchPage({
limit: 100,
});
}, [users]);
const options = React.useMemo(() => {
const userOptions = users.all.map((user) => ({
const userOptions = users.unorderedData.map((user) => ({
key: user.id,
id: user.id,
label: user.name,
}));
return [
{
key: "",
id: "",
label: t("Any author"),
},
...userOptions,
];
}, [users.all, t]);
}, [users.unorderedData, t]);
const search = React.useCallback(
async (query: string) => {
const res = await users.find(query);
return res.map((user) => ({
key: user.id,
id: user.id,
label: user.name,
}));
},
[users]
);
return (
<FilterOptions
@@ -41,6 +49,8 @@ function UserFilter(props: Props) {
onSelect={onSelect}
defaultLabel={t("Any author")}
selectedPrefix={`${t("Author")}:`}
search={search}
paginateFetch={users.fetchPage}
/>
);
}
+14 -1
View File
@@ -68,7 +68,12 @@ export default class UsersStore extends BaseStore<User> {
@computed
get orderedData(): User[] {
return orderBy(Array.from(this.data.values()), "name", "asc");
return orderBy(this.unorderedData, "name", "asc");
}
@computed
get unorderedData(): User[] {
return Array.from(this.data.values());
}
@action
@@ -152,6 +157,14 @@ export default class UsersStore extends BaseStore<User> {
return res.data;
};
@action
async find(name: string): Promise<User[]> {
const res = await client.post(`/users.list`, {
query: name,
});
return res.data;
}
@action
async delete(user: User, options: Record<string, any> = {}) {
super.delete(user, options);
@@ -131,6 +131,7 @@
"{{userName}} published": "{{userName}} published",
"{{userName}} unpublished": "{{userName}} unpublished",
"{{userName}} moved": "{{userName}} moved",
"Filter options": "Filter options",
"Icon": "Icon",
"Show menu": "Show menu",
"Choose icon": "Choose icon",