linked-data-browser 0.0.2 → 0.0.4

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 (47) hide show
  1. package/.ldo/profile.context.ts +14 -0
  2. package/.ldo/profile.typings.ts +6 -4
  3. package/app/index.tsx +2 -1
  4. package/components/ResourceView.tsx +7 -2
  5. package/components/common/LoadingBar.tsx +27 -0
  6. package/components/common/ProfileAvatar.tsx +28 -0
  7. package/components/nav/DialogProvider.tsx +5 -8
  8. package/components/nav/Layout.tsx +20 -81
  9. package/components/nav/header/AddressBox.tsx +8 -7
  10. package/components/nav/header/AvatarMenu.tsx +54 -57
  11. package/components/nav/header/Header.tsx +18 -2
  12. package/components/nav/header/SignInMenu.tsx +11 -14
  13. package/components/nav/header/ViewMenu.tsx +4 -4
  14. package/components/sharing/AccessDropdown.tsx +95 -0
  15. package/components/sharing/CopyLink.tsx +21 -0
  16. package/components/sharing/PermissionRow.tsx +38 -0
  17. package/components/sharing/SharingModal.tsx +149 -0
  18. package/components/sharing/WacRuleForm.tsx +44 -0
  19. package/components/sharing/agentPermissions/AgentInformation.tsx +37 -0
  20. package/components/sharing/agentPermissions/AgentInput.tsx +126 -0
  21. package/components/sharing/agentPermissions/AgentPermissionRow.tsx +36 -0
  22. package/components/sharing/agentPermissions/AgentPermissions.tsx +56 -0
  23. package/components/sharing/agentPermissions/useContactFilter.ts +35 -0
  24. package/components/ui/button.tsx +52 -5
  25. package/components/ui/dialog.tsx +1 -1
  26. package/components/ui/input-dropdown.tsx +105 -0
  27. package/components/ui/input.tsx +34 -2
  28. package/components/ui/text.tsx +47 -0
  29. package/components/useViewContext.tsx +141 -0
  30. package/components/{nav/utilityResourceViews → utilityResourceViews}/ErrorMessageResourceView.tsx +2 -2
  31. package/lib/icons/Fingerprint.tsx +4 -0
  32. package/lib/icons/Link.tsx +4 -0
  33. package/lib/icons/Loader.tsx +4 -0
  34. package/lib/icons/LogOut.tsx +4 -0
  35. package/lib/icons/Plus.tsx +4 -0
  36. package/lib/icons/Save.tsx +4 -0
  37. package/lib/icons/User.tsx +4 -0
  38. package/lib/icons/UserPlus.tsx +4 -0
  39. package/lib/icons/Users.tsx +4 -0
  40. package/package.json +12 -7
  41. package/resourceViews/Container/ContainerView.tsx +5 -6
  42. package/resourceViews/Profile/ProfileConfig.tsx +20 -0
  43. package/resourceViews/Profile/ProfileKnows.tsx +65 -0
  44. package/resourceViews/Profile/ProfileView.tsx +59 -0
  45. package/resourceViews/RawCode/RawCodeView.tsx +35 -9
  46. package/test-server/server-config.json +1 -1
  47. package/components/nav/useValidView.tsx +0 -51
@@ -13,7 +13,7 @@ import { View } from 'react-native';
13
13
  import type { ViewRef } from '@rn-primitives/types';
14
14
  import { cn } from '../../../lib/utils';
15
15
  import { ViewIcon } from '../../../lib/icons/ViewIcon';
16
- import { useValidView } from '../useValidView';
16
+ import { useViewContext } from '../../useViewContext';
17
17
  import { ResourceViewConfig } from '../../../components/ResourceView';
18
18
 
19
19
  export const ViewMenu: FunctionComponent = () => {
@@ -24,7 +24,7 @@ export const ViewMenu: FunctionComponent = () => {
24
24
  left: 12,
25
25
  right: 12,
26
26
  };
27
- const { validViews } = useValidView();
27
+ const { validViews } = useViewContext();
28
28
 
29
29
  const [isOpen, setIsOpen] = React.useState<string>();
30
30
 
@@ -63,7 +63,7 @@ const ListItem = React.forwardRef<
63
63
  }
64
64
  >(({ className, viewConfig, ...props }, ref) => {
65
65
  // TODO: add navigationn to `href` on `NavigationMenuLink` onPress
66
- const { curViewConfig, setCurViewConfig } = useValidView();
66
+ const { curViewConfig, setCurViewConfig } = useViewContext();
67
67
  const Icon = viewConfig.displayIcon;
68
68
  return (
69
69
  <View role="listitem">
@@ -78,7 +78,7 @@ const ListItem = React.forwardRef<
78
78
  {...props}
79
79
  >
80
80
  <Icon size={20} />
81
- <Text className="text-sm native:text-base font-medium text-foreground leading-none ml-2">
81
+ <Text className="ml-2" size="sm">
82
82
  {viewConfig.displayName}
83
83
  </Text>
84
84
  </NavigationMenuLink>
@@ -0,0 +1,95 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { AccessModeList } from '@ldo/connected-solid';
3
+ import { FunctionComponent } from 'react';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuGroup,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ } from '../ui/dropdown-menu';
13
+ import { Text } from '../ui/text';
14
+ import { Button } from '../ui/button';
15
+ import { Checkbox } from '../ui/checkbox';
16
+ import { ChevronDown } from '../../lib/icons/ChevronDown';
17
+ import { View } from 'react-native';
18
+
19
+ interface AccessDropdownProps {
20
+ value: AccessModeList;
21
+ onChange: (newAccess: AccessModeList) => void;
22
+ }
23
+
24
+ export const AccessDropdown: FunctionComponent<AccessDropdownProps> = ({
25
+ value,
26
+ onChange,
27
+ }) => {
28
+ const accessDescription = useMemo(() => {
29
+ if (value.read && value.append && value.write && value.control) {
30
+ return 'Owner';
31
+ } else if (value.read && value.append && value.write && !value.control) {
32
+ return 'Editor';
33
+ } else if (value.read && value.append && !value.write && !value.control) {
34
+ return 'Contributor';
35
+ } else if (value.read && !value.append && !value.write && !value.control) {
36
+ return 'Viewer';
37
+ } else if (!value.read && !value.append && !value.write && !value.control) {
38
+ return 'No Access';
39
+ } else {
40
+ return 'Custom';
41
+ }
42
+ }, [value.append, value.control, value.read, value.write]);
43
+
44
+ const onChangeAccessMode = useCallback(
45
+ (field: string, newValue: boolean) => {
46
+ onChange({
47
+ ...value,
48
+ [field]: newValue,
49
+ });
50
+ },
51
+ [onChange, value],
52
+ );
53
+
54
+ return (
55
+ <DropdownMenu>
56
+ <DropdownMenuTrigger asChild>
57
+ <Button variant="outline" className=" w-[130px] justify-between gap-0">
58
+ <Text>{accessDescription}</Text>
59
+ <Text>
60
+ <ChevronDown size={14} />
61
+ </Text>
62
+ </Button>
63
+ </DropdownMenuTrigger>
64
+ <DropdownMenuContent>
65
+ <DropdownMenuLabel>Permissions</DropdownMenuLabel>
66
+ <DropdownMenuSeparator />
67
+ <DropdownMenuGroup>
68
+ {['read', 'append', 'write', 'control'].map((accessModeName) => (
69
+ <DropdownMenuItem
70
+ key={accessModeName}
71
+ closeOnPress={false}
72
+ onPress={() =>
73
+ onChangeAccessMode(
74
+ accessModeName,
75
+ !value[accessModeName as keyof AccessModeList],
76
+ )
77
+ }
78
+ >
79
+ <Checkbox
80
+ checked={value[accessModeName as keyof AccessModeList]}
81
+ onCheckedChange={(newVal) =>
82
+ onChangeAccessMode(accessModeName, newVal)
83
+ }
84
+ />
85
+ <Text>
86
+ {accessModeName.charAt(0).toUpperCase() +
87
+ accessModeName.slice(1)}
88
+ </Text>
89
+ </DropdownMenuItem>
90
+ ))}
91
+ </DropdownMenuGroup>
92
+ </DropdownMenuContent>
93
+ </DropdownMenu>
94
+ );
95
+ };
@@ -0,0 +1,21 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { Button } from '../ui/button';
3
+ import { Text } from '../ui/text';
4
+ import { Link } from '../../lib/icons/Link';
5
+ import { FunctionComponent } from 'react';
6
+ import { useViewContext } from '../useViewContext';
7
+
8
+ export const CopyLink: FunctionComponent = () => {
9
+ const { targetUri } = useViewContext();
10
+ const [message, setMessage] = useState('Copy Link');
11
+ const onCopy = useCallback(async () => {
12
+ await navigator.clipboard.writeText(targetUri ?? '');
13
+ setMessage('Copied');
14
+ await new Promise((res) => setTimeout(res, 2000));
15
+ setMessage('Copy Link');
16
+ }, [targetUri]);
17
+
18
+ return (
19
+ <Button variant="outline" className="flex-row" onPress={onCopy} text={message} iconLeft={<Link />} />
20
+ );
21
+ };
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { AccessModeList } from '@ldo/connected-solid';
3
+ import { Text } from '../ui/text';
4
+ import { FunctionComponent } from 'react';
5
+ import { AccessDropdown } from './AccessDropdown';
6
+ import { View } from 'react-native';
7
+ import { Avatar, AvatarFallback } from 'components/ui/avatar';
8
+ import { LucideIcon } from 'lucide-react-native';
9
+
10
+ interface PermissionRowProps {
11
+ Icon: LucideIcon;
12
+ displayName: string;
13
+ value: AccessModeList;
14
+ onChange: (newValue: AccessModeList) => void;
15
+ }
16
+
17
+ export const PermissionRow: FunctionComponent<PermissionRowProps> = ({
18
+ Icon,
19
+ displayName,
20
+ value,
21
+ onChange,
22
+ }) => {
23
+ return (
24
+ <View className="flex-row justify-between items-center">
25
+ <View className="flex-row gap-4 flex-1 items-center">
26
+ <Avatar alt={displayName}>
27
+ <AvatarFallback>
28
+ <Text>
29
+ <Icon />
30
+ </Text>
31
+ </AvatarFallback>
32
+ </Avatar>
33
+ <Text bold>{displayName}</Text>
34
+ </View>
35
+ <AccessDropdown value={value} onChange={onChange} />
36
+ </View>
37
+ );
38
+ };
@@ -0,0 +1,149 @@
1
+ import React, {
2
+ createContext,
3
+ FunctionComponent,
4
+ PropsWithChildren,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ } from 'react';
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogFooter,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ } from '../ui/dialog';
18
+ import { Button } from '../ui/button';
19
+ import { Text } from '../ui/text';
20
+ import { CopyLink } from './CopyLink';
21
+ import {
22
+ GetWacRuleError,
23
+ GetWacRuleSuccess,
24
+ SolidContainer,
25
+ SolidLeaf,
26
+ WacRule,
27
+ } from '@ldo/connected-solid';
28
+ import { useViewContext } from '../useViewContext';
29
+ import { LoadingBar } from '../common/LoadingBar';
30
+ import { ScrollView } from 'react-native';
31
+ import { WacRuleForm } from './WacRuleForm';
32
+ import { isEqual } from 'lodash';
33
+
34
+ interface SharingModalMethods {
35
+ openSharingModal: () => void;
36
+ closeSharingModal: () => void;
37
+ isModalOpen: boolean;
38
+ }
39
+ const sharingModalContext = createContext<SharingModalMethods>({
40
+ openSharingModal: () => {},
41
+ closeSharingModal: () => {},
42
+ isModalOpen: false,
43
+ });
44
+
45
+ export const useSharingModal = () => {
46
+ return useContext(sharingModalContext);
47
+ };
48
+
49
+ export const SharingModalProvider: FunctionComponent<PropsWithChildren<{}>> = ({
50
+ children,
51
+ }) => {
52
+ const { targetResource } = useViewContext();
53
+ const [isOpen, setIsOpen] = useState(false);
54
+ const [wacResult, setWacResult] = useState<
55
+ | GetWacRuleError<SolidLeaf | SolidContainer>
56
+ | GetWacRuleSuccess<SolidLeaf | SolidContainer>
57
+ | undefined
58
+ >();
59
+ const [isLoading, setIsLoading] = useState(false);
60
+ const [editedRules, setEditedRules] = useState<WacRule>({
61
+ public: { read: false, write: false, append: false, control: false },
62
+ authenticated: { read: false, write: false, append: false, control: false },
63
+ agent: {},
64
+ });
65
+
66
+ useEffect(() => {
67
+ if (
68
+ targetResource &&
69
+ targetResource.type !== 'InvalidIdentifierResource' &&
70
+ isOpen
71
+ ) {
72
+ setIsLoading(true);
73
+
74
+ targetResource.getWac().then((wac) => {
75
+ setWacResult(
76
+ wac as
77
+ | GetWacRuleError<SolidLeaf | SolidContainer>
78
+ | GetWacRuleSuccess<SolidLeaf | SolidContainer>,
79
+ );
80
+ if (wac.type === 'getWacRuleSuccess') {
81
+ setEditedRules(wac.wacRule);
82
+ }
83
+ setIsLoading(false);
84
+ });
85
+ }
86
+ }, [targetResource, isOpen]);
87
+
88
+ const didEdit = useMemo(() => {
89
+ if (wacResult?.type !== 'getWacRuleSuccess') return false;
90
+ return !isEqual(editedRules, wacResult.wacRule);
91
+ }, [editedRules, wacResult]);
92
+
93
+ const context = useMemo(
94
+ () => ({
95
+ isModalOpen: isOpen,
96
+ openSharingModal: () => setIsOpen(true),
97
+ closeSharingModal: () => setIsOpen(false),
98
+ }),
99
+ [isOpen],
100
+ );
101
+
102
+ const onApplyChanges = useCallback(async () => {
103
+ if (
104
+ didEdit &&
105
+ (targetResource?.type === 'SolidContainer' ||
106
+ targetResource?.type === 'SolidLeaf')
107
+ ) {
108
+ setIsLoading(true);
109
+ const result = await targetResource.setWac(editedRules);
110
+ // TODO throw error with toast
111
+ setIsLoading(false);
112
+ }
113
+ setIsOpen(false);
114
+ }, [editedRules, targetResource, didEdit]);
115
+
116
+ return (
117
+ <sharingModalContext.Provider value={context}>
118
+ <Dialog open={isOpen} onOpenChange={(value) => setIsOpen(value)}>
119
+ <DialogContent className="sm:w-[640px] w-[95vw] h-[95vh]">
120
+ <LoadingBar isLoading={isLoading} />
121
+ <DialogHeader>
122
+ <DialogTitle>Resource Sharing Preferences</DialogTitle>
123
+ </DialogHeader>
124
+ <ScrollView className="flex-1 border-border border-b border-t pt-2 pb-2 ml-[-24px] mr-[-24px] pl-6 pr-6">
125
+ {(() => {
126
+ if (!wacResult) return <></>;
127
+ if (wacResult.isError) {
128
+ return (
129
+ <Text className="color-red-800">{wacResult.message}</Text>
130
+ );
131
+ }
132
+ return (
133
+ <WacRuleForm value={editedRules} onChange={setEditedRules} />
134
+ );
135
+ })()}
136
+ </ScrollView>
137
+ <DialogFooter>
138
+ <CopyLink />
139
+ <Button
140
+ text={didEdit ? 'Apply Changes' : 'Done'}
141
+ onPress={onApplyChanges}
142
+ />
143
+ </DialogFooter>
144
+ </DialogContent>
145
+ </Dialog>
146
+ {children}
147
+ </sharingModalContext.Provider>
148
+ );
149
+ };
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { Separator } from '../ui/separator';
3
+ import { FunctionComponent } from 'react';
4
+ import { AgentPermissions } from './agentPermissions/AgentPermissions';
5
+ import { WacRule } from '@ldo/connected-solid';
6
+ import { View } from 'react-native';
7
+ import { PermissionRow } from './PermissionRow';
8
+ import { Users } from '../../lib/icons/Users';
9
+ import { Fingerprint } from '../../lib/icons/Fingerprint';
10
+
11
+ interface WacRuleFormProps {
12
+ value: WacRule;
13
+ onChange: (newWacRule: WacRule) => void;
14
+ }
15
+
16
+ export const WacRuleForm: FunctionComponent<WacRuleFormProps> = ({
17
+ value,
18
+ onChange,
19
+ }) => {
20
+ return (
21
+ <View className="gap-4 mt-2 mb-2">
22
+ <PermissionRow
23
+ displayName="Public Access"
24
+ Icon={Users}
25
+ value={value.public}
26
+ onChange={(newRule) => onChange({ ...value, public: newRule })}
27
+ />
28
+ <Separator />
29
+ <PermissionRow
30
+ displayName="Authenticated Agents"
31
+ Icon={Fingerprint}
32
+ value={value.authenticated}
33
+ onChange={(newRule) => onChange({ ...value, authenticated: newRule })}
34
+ />
35
+ <Separator />
36
+ <AgentPermissions
37
+ value={value.agent}
38
+ onChange={(newAgentRules) =>
39
+ onChange({ ...value, agent: newAgentRules })
40
+ }
41
+ />
42
+ </View>
43
+ );
44
+ };
@@ -0,0 +1,37 @@
1
+ import { SolidProfileShapeShapeType } from '.ldo/profile.shapeTypes';
2
+ import { useResource, useSolidAuth, useSubject } from '@ldo/solid-react';
3
+ import React, { FunctionComponent, ReactNode } from 'react';
4
+ import { View } from 'react-native';
5
+ import { ProfileAvatar } from 'components/common/ProfileAvatar';
6
+ import { Text } from '../../ui/text';
7
+
8
+ interface AgentInformationProps {
9
+ webId: string;
10
+ accessoryRight?: ReactNode;
11
+ }
12
+
13
+ export const AgentInformation: FunctionComponent<AgentInformationProps> = ({
14
+ webId,
15
+ accessoryRight,
16
+ }) => {
17
+ const { session } = useSolidAuth();
18
+ useResource(webId);
19
+ const agentProfile = useSubject(SolidProfileShapeShapeType, webId);
20
+
21
+ return (
22
+ <View className="flex-row gap-4 flex-1 items-center">
23
+ <ProfileAvatar profile={agentProfile} />
24
+ <View className="flex-1">
25
+ <Text>
26
+ {agentProfile['@id'] === session.webId
27
+ ? 'You'
28
+ : (agentProfile.fn ?? agentProfile.name ?? 'Unnamed Agent')}
29
+ </Text>
30
+ <Text size="xs" muted>
31
+ {webId}
32
+ </Text>
33
+ </View>
34
+ {accessoryRight && <View>{accessoryRight}</View>}
35
+ </View>
36
+ );
37
+ };
@@ -0,0 +1,126 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { FunctionComponent } from 'react';
3
+ import { Pressable } from 'react-native';
4
+ import { useLinkQuery, useSolidAuth } from '@ldo/solid-react';
5
+ import { SolidProfileShapeShapeType } from '../../../.ldo/profile.shapeTypes';
6
+ import { AgentInformation } from './AgentInformation';
7
+ import { InputDropdown } from '../../ui/input-dropdown';
8
+ import { useContactFilter } from './useContactFilter';
9
+ import { Plus } from '../../../lib/icons/Plus';
10
+
11
+ interface AgentInputProps {
12
+ onAddAgent: (webId: string) => void;
13
+ existingAgents: string[];
14
+ className?: string;
15
+ }
16
+
17
+ // Component for dropdown items with profile data
18
+ const ContactDropdownItem: FunctionComponent<{
19
+ webId: string;
20
+ onSelect: (webId: string) => void;
21
+ }> = ({ webId, onSelect }) => {
22
+ return (
23
+ <Pressable
24
+ className="p-2 border-b border-border last:border-b-0 hover:bg-accent active:bg-accent cursor-pointer"
25
+ onPress={() => onSelect(webId)}
26
+ >
27
+ <AgentInformation webId={webId} />
28
+ </Pressable>
29
+ );
30
+ };
31
+
32
+ const friendsLinkQuery = {
33
+ knows: {
34
+ '@id': true,
35
+ fn: true,
36
+ },
37
+ } as const;
38
+
39
+ export const AgentInput: FunctionComponent<AgentInputProps> = ({
40
+ onAddAgent,
41
+ existingAgents,
42
+ className,
43
+ }) => {
44
+ const { session } = useSolidAuth();
45
+ const [inputValue, setInputValue] = useState('');
46
+
47
+ // Get current user's profile to access their "knows" list
48
+ const currentUserProfile = useLinkQuery(
49
+ SolidProfileShapeShapeType,
50
+ session.webId!,
51
+ session.webId!,
52
+ friendsLinkQuery,
53
+ );
54
+
55
+ // Filter out contacts that are already in the individual agents section
56
+ const availableContacts = useMemo(() => {
57
+ if (!currentUserProfile?.knows) return [];
58
+
59
+ return currentUserProfile.knows
60
+ .filter((contact) => !existingAgents.includes(contact['@id']))
61
+ .map((contact) => contact['@id']);
62
+ }, [currentUserProfile?.knows, existingAgents]);
63
+
64
+ // Use the custom hook for filtering
65
+ const filteredContacts = useContactFilter(
66
+ availableContacts,
67
+ inputValue,
68
+ currentUserProfile?.knows ? Array.from(currentUserProfile.knows) : [],
69
+ );
70
+
71
+ const handleInputSubmit = useCallback(() => {
72
+ if (inputValue.trim()) {
73
+ // Check if it's a valid URI
74
+ try {
75
+ const url = new URL(inputValue);
76
+ onAddAgent(url.toString());
77
+ setInputValue('');
78
+ } catch {
79
+ // Do nothing
80
+ }
81
+ }
82
+ }, [inputValue, onAddAgent]);
83
+
84
+ const handleContactSelect = useCallback(
85
+ (webId: string) => {
86
+ onAddAgent(webId);
87
+ setInputValue('');
88
+ },
89
+ [onAddAgent],
90
+ );
91
+
92
+ // Filter function for the dropdown (now just returns the filtered contacts)
93
+ const filterContacts = useCallback(
94
+ (_contacts: string[], _searchText: string) => {
95
+ return filteredContacts;
96
+ },
97
+ [filteredContacts],
98
+ );
99
+
100
+ // Render function for dropdown items
101
+ const renderContactItem = useCallback(
102
+ (webId: string, onSelect: (webId: string) => void) => (
103
+ <ContactDropdownItem webId={webId} onSelect={onSelect} />
104
+ ),
105
+ [],
106
+ );
107
+
108
+ return (
109
+ <InputDropdown
110
+ placeholder="Add Contact or WebId"
111
+ value={inputValue}
112
+ onChangeText={setInputValue}
113
+ onSubmitEditing={handleInputSubmit}
114
+ items={availableContacts}
115
+ renderItem={renderContactItem}
116
+ filterItems={filterContacts}
117
+ buttonRight={{
118
+ iconRight: <Plus />,
119
+ onPress: handleInputSubmit,
120
+ variant: 'secondary',
121
+ }}
122
+ onItemSelect={handleContactSelect}
123
+ className={className}
124
+ />
125
+ );
126
+ };
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { AccessModeList } from '@ldo/connected-solid';
3
+ import { Text } from '../../ui/text';
4
+ import { FunctionComponent } from 'react';
5
+ import { AgentInformation } from './AgentInformation';
6
+ import { AccessDropdown } from '../AccessDropdown';
7
+ import { View } from 'react-native';
8
+ import { Button } from '../../ui/button';
9
+ import { Trash } from '../../../lib/icons/Trash';
10
+
11
+ interface AgentPermissionRowProps {
12
+ webId: string;
13
+ value: AccessModeList;
14
+ onChange: (newValue: AccessModeList | undefined) => void;
15
+ }
16
+
17
+ export const AgentPermissionRow: FunctionComponent<AgentPermissionRowProps> = ({
18
+ webId,
19
+ value,
20
+ onChange,
21
+ }) => {
22
+ return (
23
+ <View className="flex-row justify-between align-center">
24
+ <AgentInformation webId={webId} />
25
+ <View className="flex-row">
26
+ <Button
27
+ variant="ghost"
28
+ onPress={() => onChange(undefined)}
29
+ className="w-10"
30
+ iconLeft={<Trash size={14} />}
31
+ />
32
+ <AccessDropdown value={value} onChange={onChange} />
33
+ </View>
34
+ </View>
35
+ );
36
+ };
@@ -0,0 +1,56 @@
1
+ import React, { useCallback } from 'react';
2
+ import { WacRule } from '@ldo/connected-solid';
3
+ import { FunctionComponent } from 'react';
4
+ import { AgentPermissionRow } from './AgentPermissionRow';
5
+ import { Text } from '../../ui/text';
6
+ import { AgentInput } from './AgentInput';
7
+
8
+ interface AgentPermissionsProps {
9
+ value: WacRule['agent'];
10
+ onChange: (newValue: WacRule['agent']) => void;
11
+ }
12
+
13
+ export const AgentPermissions: FunctionComponent<AgentPermissionsProps> = ({
14
+ value,
15
+ onChange,
16
+ }) => {
17
+ const addAgent = useCallback(
18
+ (webId: string) => {
19
+ onChange({
20
+ ...value,
21
+ [webId]: { read: false, write: false, append: false, control: false },
22
+ });
23
+ },
24
+ [value, onChange],
25
+ );
26
+
27
+ return (
28
+ <>
29
+ <Text bold>Individual Agents</Text>
30
+ <AgentInput
31
+ onAddAgent={addAgent}
32
+ existingAgents={Object.keys(value)}
33
+ className="z-[999]"
34
+ />
35
+ {Object.entries(value).map(([webId, accessModeList]) => (
36
+ <AgentPermissionRow
37
+ key={webId}
38
+ webId={webId}
39
+ value={accessModeList}
40
+ onChange={(newAccessModeList) => {
41
+ if (!newAccessModeList) {
42
+ const newVal = { ...value };
43
+ delete newVal[webId];
44
+ onChange(newVal);
45
+ } else {
46
+ onChange({
47
+ ...value,
48
+ [webId]: newAccessModeList,
49
+ });
50
+ }
51
+ }}
52
+ />
53
+ ))}
54
+ </>
55
+ );
56
+ };
@@ -0,0 +1,35 @@
1
+ import { useMemo } from 'react';
2
+
3
+ export function useContactFilter(
4
+ webIds: string[],
5
+ searchText: string,
6
+ contactsWithNames: Array<{ '@id': string; fn?: string }>,
7
+ ) {
8
+ // Filter contacts based on search text (URI and name)
9
+ const filteredContacts = useMemo(() => {
10
+ if (!searchText.trim()) return webIds;
11
+
12
+ const lowerInput = searchText.toLowerCase();
13
+
14
+ return webIds.filter((webId) => {
15
+ // Check if the URI matches
16
+ if (webId.toLowerCase().includes(lowerInput)) {
17
+ return true;
18
+ }
19
+
20
+ // Check if the name matches
21
+ const contact = contactsWithNames.find((c) => c['@id'] === webId);
22
+ if (
23
+ contact &&
24
+ contact.fn &&
25
+ contact.fn.toLowerCase().includes(lowerInput)
26
+ ) {
27
+ return true;
28
+ }
29
+
30
+ return false;
31
+ });
32
+ }, [webIds, searchText, contactsWithNames]);
33
+
34
+ return filteredContacts;
35
+ }