@equinor/fusion-framework-react-components-bookmark 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/tsconfig.tsbuildinfo +1 -1
  3. package/dist/types/version.d.ts +1 -1
  4. package/package.json +10 -7
  5. package/CHANGELOG.md +0 -1108
  6. package/src/__tests__/AppNameField.test.tsx +0 -39
  7. package/src/components/Bookmark.tsx +0 -98
  8. package/src/components/BookmarkProvider.tsx +0 -129
  9. package/src/components/create-bookmark/CreateBookmarkModal.tsx +0 -130
  10. package/src/components/create-bookmark/index.ts +0 -1
  11. package/src/components/edit-bookmark/AppNameField.tsx +0 -27
  12. package/src/components/edit-bookmark/EditBookmarkModal.tsx +0 -203
  13. package/src/components/edit-bookmark/index.ts +0 -1
  14. package/src/components/filter/BookmarkFilter.tsx +0 -44
  15. package/src/components/import-bookmark/ImportBookmarkModal.tsx +0 -100
  16. package/src/components/import-bookmark/index.ts +0 -1
  17. package/src/components/loading/Loading.tsx +0 -20
  18. package/src/components/messages/Message.tsx +0 -89
  19. package/src/components/row/MoreMenu.tsx +0 -41
  20. package/src/components/row/Row.tsx +0 -105
  21. package/src/components/section/Section.tsx +0 -63
  22. package/src/components/sectionList/SectionList.tsx +0 -186
  23. package/src/components/shared/SharedIcon.tsx +0 -51
  24. package/src/hooks/index.ts +0 -1
  25. package/src/hooks/useBookmarkGrouping.ts +0 -65
  26. package/src/index.ts +0 -12
  27. package/src/utils/append-bookmark-id-to-url.ts +0 -5
  28. package/src/utils/filter-empty-groups.ts +0 -5
  29. package/src/utils/sort-by-name.ts +0 -4
  30. package/src/utils/to-human-readable.ts +0 -6
  31. package/src/version.ts +0 -2
  32. package/tsconfig.json +0 -18
  33. package/vitest.config.ts +0 -10
@@ -1,100 +0,0 @@
1
- import { Button, Dialog } from '@equinor/eds-core-react';
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
3
-
4
- import styled from 'styled-components';
5
- import { useBookmarkComponentContext } from '../BookmarkProvider';
6
- import { filter, from, of } from 'rxjs';
7
- import { map, switchMap } from 'rxjs/operators';
8
- import { useObservableState } from '@equinor/fusion-observable/react';
9
-
10
- const Styled = {
11
- Actions: styled.div`
12
- display: flex;
13
- gap: 1rem;
14
- `,
15
- };
16
-
17
- /**
18
- * Modal that prompts the user to import a bookmark shared via a `bookmarkId` URL parameter.
19
- *
20
- * @returns The import bookmark modal
21
- */
22
- export const ImportBookmarkModal = () => {
23
- const [isOpen, setIsOpen] = useState(false);
24
-
25
- const { provider, currentUser } = useBookmarkComponentContext();
26
-
27
- // Observe changes to current bookmark and check if it is already in bookmarks or favorites
28
- const newBookmark$ = useMemo(() => {
29
- // Without a provider there is no bookmark data source to observe
30
- if (!provider) {
31
- return of(null);
32
- }
33
- // Watch the current bookmark and resolve whether it should prompt an import
34
- return provider.currentBookmark$.pipe(
35
- // filter out null values, not interested in those
36
- filter((bookmark) => !!bookmark),
37
- switchMap((bookmark) => {
38
- const checkBookmark = bookmark && bookmark.createdBy.id !== currentUser?.id;
39
- // Only prompt to import bookmarks created by someone else
40
- if (!checkBookmark) {
41
- return of(null);
42
- }
43
- // Resolve to null once the bookmark is already a favorite, otherwise pass it through
44
- return from(provider.isBookmarkInFavorites(bookmark.id)).pipe(
45
- map((isFavorite) => (isFavorite ? null : bookmark)),
46
- );
47
- }),
48
- );
49
- }, [provider, currentUser?.id]);
50
-
51
- const { value: newBookmark } = useObservableState(newBookmark$);
52
-
53
- useEffect(() => {
54
- // Open the import dialog whenever a new importable bookmark is detected
55
- if (newBookmark) {
56
- setIsOpen(true);
57
- }
58
- }, [newBookmark]);
59
-
60
- const onAddBookmarkFavorite = useCallback(
61
- (e: React.MouseEvent<HTMLButtonElement>) => {
62
- // Without a provider we cannot persist the favorite, so bail out
63
- if (!provider) {
64
- console.error('Provider not available');
65
- return;
66
- }
67
- from(provider.addBookmarkToFavorites(e.currentTarget.value)).subscribe({
68
- complete: () => {
69
- setIsOpen(false);
70
- },
71
- });
72
- },
73
- [provider],
74
- );
75
-
76
- // Nothing to import when there is no candidate bookmark
77
- if (!newBookmark) {
78
- return null;
79
- }
80
-
81
- return (
82
- <Dialog style={{ width: '600px' }} open={isOpen}>
83
- <Dialog.Header>Import bookmark</Dialog.Header>
84
- <Dialog.Content>
85
- <p>This bookmark was created by {newBookmark.createdBy.name}</p>
86
- <p>Would you like to import it?</p>
87
- </Dialog.Content>
88
- <Dialog.Actions>
89
- <Styled.Actions>
90
- <Button onClick={() => setIsOpen(false)} variant={'ghost'} style={{ width: '70px' }}>
91
- No
92
- </Button>
93
- <Button value={newBookmark.id} onClick={onAddBookmarkFavorite} variant="ghost">
94
- Import
95
- </Button>
96
- </Styled.Actions>
97
- </Dialog.Actions>
98
- </Dialog>
99
- );
100
- };
@@ -1 +0,0 @@
1
- export * from './ImportBookmarkModal';
@@ -1,20 +0,0 @@
1
- import { CircularProgress } from '@equinor/eds-core-react';
2
-
3
- /**
4
- * A centered loading spinner.
5
- *
6
- * @returns The loading indicator
7
- */
8
- export const Loading = () => (
9
- <div
10
- style={{
11
- display: 'flex',
12
- alignItems: 'center',
13
- justifyContent: 'center',
14
- height: '100%',
15
- width: '100%',
16
- }}
17
- >
18
- <CircularProgress />
19
- </div>
20
- );
@@ -1,89 +0,0 @@
1
- import { Icon, Typography } from '@equinor/eds-core-react';
2
- import { tokens } from '@equinor/eds-tokens';
3
- import type { PropsWithChildren } from 'react';
4
- import styled from 'styled-components';
5
-
6
- import { error_outlined, file_description } from '@equinor/eds-icons';
7
-
8
- export type PortalMessageType = 'Error' | 'Info' | 'Warning' | 'NoContent';
9
-
10
- const getMessageType = (type?: PortalMessageType) => {
11
- // Map each message type to its icon and color; unknown types render nothing
12
- switch (type) {
13
- case 'Error':
14
- return { color: tokens.colors.interactive.danger__resting.hex, icon: error_outlined };
15
- case 'Info':
16
- return { color: tokens.colors.interactive.primary__resting.hex, icon: error_outlined };
17
- case 'Warning':
18
- return { color: tokens.colors.interactive.warning__resting.hex, icon: error_outlined };
19
- case 'NoContent':
20
- return {
21
- color: tokens.colors.interactive.primary__resting.hex,
22
- icon: file_description,
23
- };
24
- default:
25
- return undefined;
26
- }
27
- };
28
-
29
- const Styles = {
30
- Wrapper: styled.div`
31
- width: 100%;
32
- height: 100%;
33
- display: flex;
34
- flex-direction: column;
35
- align-items: center;
36
- gap: 1rem;
37
- justify-content: center;
38
- `,
39
- Content: styled.div`
40
- display: flex;
41
- flex-direction: column;
42
- align-items: center;
43
- gap: 0.5rem;
44
- justify-content: center;
45
- `,
46
- };
47
-
48
- type MessageProps = {
49
- readonly title: string;
50
- readonly body?: React.FC | string;
51
- readonly type?: PortalMessageType;
52
- readonly color?: string;
53
- };
54
-
55
- /**
56
- * Displays a titled message with an icon matching its type.
57
- *
58
- * @param props - The component's props
59
- * @returns The message
60
- */
61
- export const Message = ({
62
- title,
63
- type = 'Info',
64
- color,
65
- children,
66
- }: PropsWithChildren<MessageProps>) => {
67
- const currentType = getMessageType(type);
68
- return (
69
- <Styles.Wrapper>
70
- <Icon
71
- data-testid="icon"
72
- size={40}
73
- color={currentType?.color || color || tokens.colors.text.static_icons__tertiary.hex}
74
- data={currentType?.icon || error_outlined}
75
- />
76
- <Styles.Content>
77
- <Typography
78
- color={tokens.colors.text.static_icons__default.hex}
79
- variant={'h3'}
80
- aria-label={`Title for ${type} message`}
81
- >
82
- {title}
83
- </Typography>
84
-
85
- <Typography>{children && children}</Typography>
86
- </Styles.Content>
87
- </Styles.Wrapper>
88
- );
89
- };
@@ -1,41 +0,0 @@
1
- import { Menu } from '@equinor/eds-core-react';
2
- import type { MutableRefObject } from 'react';
3
- import type { MenuOption } from './Row';
4
-
5
- type MenuProps = {
6
- readonly pRef: MutableRefObject<HTMLElement | null>;
7
- readonly onClose: VoidFunction;
8
- readonly open: boolean;
9
- readonly options: MenuOption[];
10
- };
11
-
12
- /**
13
- * A dropdown menu of row actions anchored to a reference element.
14
- *
15
- * @param props - The component's props
16
- * @returns The menu
17
- */
18
- export const MoreMenu = ({ pRef, onClose, open, options }: MenuProps) => {
19
- // Render one menu item per configured row action
20
- const items = options.map(({ onClick, name, disabled, Icon }) => (
21
- <Menu.Item
22
- disabled={disabled}
23
- onClick={(e) => {
24
- e.stopPropagation();
25
- e.preventDefault();
26
- onClick();
27
- onClose();
28
- }}
29
- key={name}
30
- >
31
- {Icon && Icon}
32
- {name}
33
- </Menu.Item>
34
- ));
35
-
36
- return (
37
- <Menu open={open} anchorEl={pRef.current}>
38
- {items}
39
- </Menu>
40
- );
41
- };
@@ -1,105 +0,0 @@
1
- import { Icon, Typography } from '@equinor/eds-core-react';
2
- import { tokens } from '@equinor/eds-tokens';
3
- import { useOutsideClick } from '@equinor/eds-utils';
4
- import { type MutableRefObject, type ReactNode, useCallback, useRef } from 'react';
5
- import { MoreMenu } from './MoreMenu';
6
-
7
- import styled from 'styled-components';
8
- import { useBookmarkComponentContext } from '../BookmarkProvider';
9
- import { from } from 'rxjs';
10
-
11
- export type MenuOption = {
12
- name: string;
13
- disabled: boolean;
14
- onClick: VoidFunction;
15
- Icon?: ReactNode;
16
- };
17
-
18
- type RowProps = {
19
- readonly name: string;
20
- readonly id: string;
21
- readonly menuOpen: boolean;
22
- readonly onMenuOpen: (id: string) => void;
23
- readonly menuOptions: MenuOption[];
24
- readonly children?: ReactNode;
25
- };
26
-
27
- const Styled = {
28
- Icons: styled.div`
29
- display: flex;
30
- flex-direction: row;
31
- gap: 0.2em;
32
- align-items: center;
33
- `,
34
- ListItem: styled.div`
35
- cursor: pointer;
36
- &:hover {
37
- background-color: ${tokens.colors.ui.background__light.hex};
38
- }
39
-
40
- display: flex;
41
- align-items: center;
42
- height: 32px;
43
- min-height: 32px;
44
- max-height: 32px;
45
- justify-content: space-between;
46
- border-bottom: 1px solid ${tokens.colors.ui.background__medium.hex};
47
- `,
48
- };
49
-
50
- /**
51
- * A single bookmark row, with actions and click-to-set-current behavior.
52
- *
53
- * @param props - The component's props
54
- * @returns The row
55
- */
56
- export const Row = ({ name, menuOptions, children, id, menuOpen, onMenuOpen }: RowProps) => {
57
- const pRef = useRef<HTMLElement | null>(null);
58
- const { provider } = useBookmarkComponentContext();
59
-
60
- const onListItemClick = useCallback(
61
- (e: React.MouseEvent) => {
62
- e.preventDefault();
63
- e.stopPropagation();
64
- // Only attempt to set the current bookmark when a provider is available
65
- if (provider) {
66
- from(provider.setCurrentBookmark(id)).subscribe();
67
- }
68
- },
69
- [provider, id],
70
- );
71
-
72
- useOutsideClick(pRef.current, () => onMenuOpen(''));
73
-
74
- // TODO(#5093): fix accessibility (keyboard support) for this click handler
75
- return (
76
- /* eslint-disable-next-line styled-components-a11y/click-events-have-key-events, styled-components-a11y/no-static-element-interactions*/
77
- <Styled.ListItem onClick={onListItemClick}>
78
- <Typography>{name}</Typography>
79
- <Styled.Icons>
80
- {children}
81
- {!!menuOptions?.length && (
82
- <Icon
83
- name="more_vertical"
84
- onClick={(e) => {
85
- e.preventDefault();
86
- e.stopPropagation();
87
- onMenuOpen(id);
88
- }}
89
- color={tokens.colors.interactive.primary__resting.hex}
90
- ref={pRef as unknown as MutableRefObject<SVGSVGElement>}
91
- />
92
- )}
93
-
94
- <MoreMenu
95
- open={menuOpen}
96
- options={menuOptions}
97
- onClose={() => {
98
- onMenuOpen('');
99
- }}
100
- pRef={pRef}
101
- />
102
- </Styled.Icons>
103
- </Styled.ListItem>
104
- );
105
- };
@@ -1,63 +0,0 @@
1
- import { useState } from 'react';
2
- import type { ReactNode } from 'react';
3
- import { Button, Icon, Typography } from '@equinor/eds-core-react';
4
- import { chevron_down, chevron_right } from '@equinor/eds-icons';
5
-
6
- import { tokens } from '@equinor/eds-tokens';
7
- import styled from 'styled-components';
8
-
9
- const Styled = {
10
- Button: styled(Button)`
11
- width: 100%;
12
- padding: 0;
13
- padding-top: 0.5rem;
14
- margin-bottom: 0.5rem;
15
- display: flex;
16
- justify-content: space-between;
17
- align-items: center;
18
- background: none;
19
- border: none;
20
- color: ${tokens.colors.infographic.primary__moss_green_100.hex};
21
- cursor: pointer;
22
- :hover {
23
- background: none;
24
- }
25
- `,
26
- List: styled.ol`
27
- margin-left: 0.7rem;
28
- padding-left: 1rem;
29
- display: flex;
30
- flex-direction: column;
31
- gap: 1rem;
32
- border-left: 1px solid ${tokens.colors.infographic.primary__moss_green_100.hex};
33
- `,
34
- };
35
-
36
- Icon.add({
37
- chevron_down,
38
- chevron_right,
39
- });
40
-
41
- type SectionProps = {
42
- readonly name: string;
43
- readonly children: ReactNode;
44
- };
45
- /**
46
- * A collapsible section with a named header and its contents.
47
- *
48
- * @param props - The component's props
49
- * @returns The section
50
- */
51
- export const Section = ({ children, name }: SectionProps) => {
52
- const [isExpanded, setIsExpanded] = useState(true);
53
-
54
- return (
55
- <div>
56
- <Styled.Button variant="ghost" onClick={() => setIsExpanded((s) => !s)}>
57
- <Icon name={isExpanded ? chevron_down.name : chevron_right.name} />
58
- <Typography variant="h6">{name}</Typography>
59
- </Styled.Button>
60
- {isExpanded && <Styled.List>{children}</Styled.List>}
61
- </div>
62
- );
63
- };
@@ -1,186 +0,0 @@
1
- import { Icon } from '@equinor/eds-core-react';
2
- import type { useBookmarkGrouping } from '../../hooks';
3
- import { type MenuOption, Row } from '../row/Row';
4
- import { Section } from '../section/Section';
5
- import { SharedIcon } from '../shared/SharedIcon';
6
- // TODO(#5094): export from `@equinor/fusion-framework-react-module-bookmark`
7
- import type { BookmarkWithoutData } from '@equinor/fusion-framework-module-bookmark';
8
- import { useCurrentUser } from '@equinor/fusion-framework-react/hooks';
9
- import { useCallback, useState } from 'react';
10
- import { delete_to_trash, share, edit, close, update } from '@equinor/eds-icons';
11
- import { filterEmptyGroups } from '../../utils/filter-empty-groups';
12
- import { sortByName } from '../../utils/sort-by-name';
13
- import { toHumanReadable } from '../../utils/to-human-readable';
14
- import { useBookmarkComponentContext } from '../BookmarkProvider';
15
- import { from } from 'rxjs';
16
-
17
- Icon.add({
18
- delete_to_trash,
19
- edit,
20
- share,
21
- close,
22
- update,
23
- });
24
-
25
- type SectionListProps = {
26
- readonly bookmarkGroups: ReturnType<typeof useBookmarkGrouping>['bookmarkGroups'];
27
- };
28
-
29
- /**
30
- * Renders bookmark groups as sections of rows with edit/share/delete actions.
31
- *
32
- * @param props - The component's props
33
- * @returns The section list
34
- */
35
- export const SectionList = ({ bookmarkGroups }: SectionListProps) => {
36
- // const { deleteBookmarkById, updateBookmark, removeBookmarkFavorite } = useBookmark();
37
-
38
- const { provider, currentApp, showEditBookmark, addBookmarkToClipboard } =
39
- useBookmarkComponentContext();
40
-
41
- // TODO(#5095): should be removed when bookmark has property for isOwner
42
- const user = useCurrentUser();
43
-
44
- const [isMenuByIdOpen, setIsMenuByIdOpen] = useState('');
45
-
46
- const createMenuOptions = useCallback(
47
- (bookmark: BookmarkWithoutData) => {
48
- // Without a provider there are no actions that can be performed on the bookmark
49
- if (!provider) return [];
50
- const appKey = currentApp?.appKey;
51
- const bookmarkId = bookmark.id;
52
- const isOwner = bookmark.createdBy.id === user?.localAccountId;
53
-
54
- const copyUrl = {
55
- name: 'Copy URL',
56
- disabled: false,
57
- onClick: async () => {
58
- addBookmarkToClipboard(bookmark.id);
59
- },
60
- Icon: <Icon name="share" />,
61
- };
62
-
63
- // Only the bookmark owner is allowed to edit, update, remove, or share it
64
- if (isOwner) {
65
- const ownerOptions = [
66
- {
67
- name: 'Edit',
68
- disabled: appKey !== bookmark.appKey,
69
- onClick: () => {
70
- showEditBookmark(bookmarkId);
71
- },
72
- Icon: <Icon name="edit" />,
73
- },
74
- {
75
- name: 'Update with current view',
76
- disabled: appKey !== bookmark.appKey,
77
- onClick: () => {
78
- // TODO(#5089): add success and failure message
79
- from(
80
- provider.updateBookmark(bookmark.id, undefined, {
81
- excludePayloadGeneration: false,
82
- }),
83
- ).subscribe({
84
- error(err) {
85
- console.error('Failed to update bookmark with current view', err);
86
- },
87
- complete() {
88
- console.debug('Bookmark updated with current view');
89
- },
90
- });
91
- },
92
- Icon: <Icon name="update" />,
93
- },
94
- {
95
- name: 'Remove',
96
- disabled: false,
97
- onClick: () => {
98
- // TODO(#5089): add success and failure message
99
- from(provider.deleteBookmark(bookmark.id)).subscribe({
100
- error(err) {
101
- console.error('Failed to remove bookmark', err);
102
- },
103
- complete() {
104
- console.debug('Bookmark removed');
105
- },
106
- });
107
- },
108
- Icon: <Icon name="delete_to_trash" />,
109
- },
110
- {
111
- name: bookmark.isShared ? 'Unshare' : 'Share',
112
- disabled: false,
113
- onClick: async () => {
114
- from(
115
- provider.updateBookmark(bookmark.id, {
116
- isShared: !bookmark.isShared,
117
- }),
118
- ).subscribe({
119
- next(value) {
120
- // Copy the bookmark URL to the clipboard once it becomes shared
121
- if (value.isShared) {
122
- addBookmarkToClipboard(value.id);
123
- }
124
- },
125
- error(err) {
126
- console.error('Failed to update bookmark', err);
127
- },
128
- complete() {
129
- console.debug('Bookmark updated');
130
- },
131
- });
132
- },
133
- Icon: <Icon name="share" />,
134
- },
135
- ] as MenuOption[];
136
-
137
- return bookmark.isShared ? [...ownerOptions, copyUrl] : ownerOptions;
138
- }
139
-
140
- return [
141
- copyUrl,
142
- {
143
- name: 'Unfavourite',
144
- disabled: false,
145
- onClick: () => {
146
- from(provider.removeBookmarkAsFavorite(bookmark.id)).subscribe({
147
- error(err) {
148
- console.error('Failed to remove bookmark from favorites', err);
149
- },
150
- complete() {
151
- console.debug('Bookmark removed from favorites');
152
- },
153
- });
154
- },
155
- Icon: <Icon name="close" />,
156
- },
157
- ];
158
- },
159
- [provider, showEditBookmark, addBookmarkToClipboard, user, currentApp?.appKey],
160
- );
161
-
162
- return (
163
- <>
164
- {bookmarkGroups.filter(filterEmptyGroups).map(({ groupingProperty, values }) => (
165
- <Section key={groupingProperty} name={toHumanReadable(groupingProperty)}>
166
- {values.sort(sortByName).map((b) => (
167
- <Row
168
- menuOptions={createMenuOptions(b)}
169
- key={b.id}
170
- name={b.name}
171
- id={b.id}
172
- menuOpen={isMenuByIdOpen === b.id}
173
- onMenuOpen={(id: string) => {
174
- setIsMenuByIdOpen(id);
175
- }}
176
- >
177
- {b.isShared && (
178
- <SharedIcon createdById={b.createdBy.id} createdBy={b.createdBy.name} />
179
- )}
180
- </Row>
181
- ))}
182
- </Section>
183
- ))}
184
- </>
185
- );
186
- };
@@ -1,51 +0,0 @@
1
- import { Icon } from '@equinor/eds-core-react';
2
- import { tokens } from '@equinor/eds-tokens';
3
- import { useFramework } from '@equinor/fusion-framework-react';
4
-
5
- import styled from 'styled-components';
6
-
7
- const Styled = {
8
- Row: styled.div`
9
- display: flex;
10
- align-items: center;
11
- gap: 0.2em;
12
- `,
13
- CreatedBy: styled.p`
14
- font-size: 12px;
15
- font-weight: 500;
16
- line-height: 16px;
17
- letter-spacing: 0em;
18
- text-align: right;
19
- `,
20
- };
21
-
22
- type SharedIconProps = {
23
- readonly createdBy: string;
24
- readonly createdById: string;
25
- };
26
-
27
- /**
28
- * Displays a "shared by" indicator when a bookmark was created by another user.
29
- *
30
- * @param props - The component's props
31
- * @returns The shared icon, or nothing when the current user created the bookmark
32
- */
33
- export const SharedIcon = ({ createdBy, createdById }: SharedIconProps) => {
34
- const myId = useFramework().modules.auth.account?.localAccountId;
35
-
36
- const isMe = createdById === myId;
37
-
38
- return (
39
- <Styled.Row>
40
- {!isMe && <Styled.CreatedBy>shared by {createdBy}</Styled.CreatedBy>}
41
- <Icon
42
- name="share"
43
- color={
44
- isMe
45
- ? tokens.colors.interactive.primary__resting.hex
46
- : tokens.colors.interactive.disabled__border.hex
47
- }
48
- />
49
- </Styled.Row>
50
- );
51
- };
@@ -1 +0,0 @@
1
- export * from './useBookmarkGrouping';