Browse Source

Add search in password profiles page

tags/mobile-v9.4.0
Guillaume Vincent 3 years ago
parent
commit
3557e68e3d
7 changed files with 61 additions and 212 deletions
  1. +2
    -2
      mobile/ios/LessPass.xcodeproj/project.pbxproj
  2. +2
    -2
      mobile/package.json
  3. +52
    -23
      mobile/src/profiles/ProfilesScreen.js
  4. +0
    -13
      mobile/src/profiles/filter.js
  5. +0
    -167
      mobile/src/profiles/filter.test.js
  6. +1
    -1
      mobile/src/version.json
  7. +4
    -4
      mobile/yarn.lock

+ 2
- 2
mobile/ios/LessPass.xcodeproj/project.pbxproj View File

@@ -355,7 +355,7 @@
); );
INFOPLIST_FILE = LessPass/Info.plist; INFOPLIST_FILE = LessPass/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 9.3.0;
MARKETING_VERSION = 9.4.0;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@@ -384,7 +384,7 @@
DEVELOPMENT_TEAM = 5Y4MF2AT83; DEVELOPMENT_TEAM = 5Y4MF2AT83;
INFOPLIST_FILE = LessPass/Info.plist; INFOPLIST_FILE = LessPass/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 9.3.0;
MARKETING_VERSION = 9.4.0;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",


+ 2
- 2
mobile/package.json View File

@@ -1,6 +1,6 @@
{ {
"name": "lesspass-mobile", "name": "lesspass-mobile",
"version": "9.3.0",
"version": "9.4.0",
"description": "LessPass mobile application", "description": "LessPass mobile application",
"license": "(MPL-2.0 OR GPL-3.0)", "license": "(MPL-2.0 OR GPL-3.0)",
"author": "Guillaume Vincent <guillaume@oslab.fr>", "author": "Guillaume Vincent <guillaume@oslab.fr>",
@@ -30,7 +30,7 @@
"@react-navigation/native": "^6.0.6", "@react-navigation/native": "^6.0.6",
"@react-navigation/stack": "^6.0.11", "@react-navigation/stack": "^6.0.11",
"axios": "^0.23.0", "axios": "^0.23.0",
"fuse.js": "^6.4.6",
"fuzzysort": "^1.1.4",
"lesspass-fingerprint": "latest", "lesspass-fingerprint": "latest",
"lesspass-render-password": "latest", "lesspass-render-password": "latest",
"lodash": "^4.17.21", "lodash": "^4.17.21",


+ 52
- 23
mobile/src/profiles/ProfilesScreen.js View File

@@ -10,6 +10,7 @@ import {
Provider, Provider,
Paragraph, Paragraph,
Button, Button,
Searchbar,
} from "react-native-paper"; } from "react-native-paper";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
@@ -18,15 +19,21 @@ import {
} from "../password/profilesActions"; } from "../password/profilesActions";
import routes from "../routes"; import routes from "../routes";
import Styles from "../ui/Styles"; import Styles from "../ui/Styles";
import fuzzysort from "fuzzysort";
import { cleanPasswordProfile, setPasswordProfile } from "./profileActions"; import { cleanPasswordProfile, setPasswordProfile } from "./profileActions";
import { sortByNewestFirst } from "./sort"; import { sortByNewestFirst } from "./sort";


export default function ProfilesScreen() { export default function ProfilesScreen() {
const navigation = useNavigation(); const navigation = useNavigation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const profiles = useSelector((state) => state.profiles);
const profiles = useSelector((state) => Object.values(state.profiles));
const [profileToDelete, setProfileToDelete] = useState(null); const [profileToDelete, setProfileToDelete] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [query, setQuery] = useState("");
const results =
query === ""
? profiles
: fuzzysort.go(query, profiles, { key: "site" }).map((r) => r.obj);


const _getPasswordProfilesCallback = useCallback(() => { const _getPasswordProfilesCallback = useCallback(() => {
setIsLoading(true); setIsLoading(true);
@@ -73,37 +80,59 @@ export default function ProfilesScreen() {
</Dialog.Actions> </Dialog.Actions>
</Dialog> </Dialog>
</Portal> </Portal>
<List.Section title="Password profiles">
{isLoading ? null : Object.keys(profiles).length === 0 ? (
<List.Section>
{isLoading ? null : profiles.length === 0 ? (
<List.Item <List.Item
description={ description={
"You don't have any password profiles. Save a password profile when you generate it." "You don't have any password profiles. Save a password profile when you generate it."
} }
/> />
) : ( ) : (
sortByNewestFirst(Object.values(profiles)).map((profile) => (
<View key={profile.id}>
<View>
<View style={{ paddingHorizontal: 10 }}>
<Searchbar
placeholder={`Search in your ${profiles.length} profile${
profiles.length === 1 ? "" : "s"
}`}
onChangeText={setQuery}
value={query}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
{results.length === 0 ? (
<List.Item <List.Item
title={profile.site}
description={profile.login}
onPress={() => {
dispatch(setPasswordProfile(profile));
navigation.navigate(routes.PASSWORD_GENERATOR);
}}
right={(props) => (
<IconButton
{...props}
icon="delete"
onPress={(event) => {
setProfileToDelete(profile);
event.stopPropagation();
description={
"There is no password profile that matches this search."
}
/>
) : (
sortByNewestFirst(results).map((profile) => (
<View key={profile.id}>
<List.Item
title={profile.site}
description={profile.login}
onPress={() => {
dispatch(setPasswordProfile(profile));
setQuery("");
navigation.navigate(routes.PASSWORD_GENERATOR);
}} }}
right={(props) => (
<IconButton
{...props}
icon="delete"
onPress={(event) => {
setProfileToDelete(profile);
event.stopPropagation();
}}
/>
)}
/> />
)}
/>
<Divider />
</View>
))
<Divider />
</View>
))
)}
</View>
)} )}
</List.Section> </List.Section>
</ScrollView> </ScrollView>


+ 0
- 13
mobile/src/profiles/filter.js View File

@@ -1,13 +0,0 @@
import { isEmpty } from "lodash";
import Fuse from "fuse.js";

export function returnMatchingData(query, data, dataKey) {
if (isEmpty(query)) return [];
const options = {
keys: [dataKey],
minMatchCharLength: 2,
includeMatches: true,
};
const fuse = new Fuse(data, options);
return fuse.search(query).slice(0, 3);
}

+ 0
- 167
mobile/src/profiles/filter.test.js View File

@@ -1,167 +0,0 @@
import { returnMatchingData } from "./filter";

test("returnMatchingData", () => {
const matches = returnMatchingData(
"exam",
[
{ site: "example.org", login: "test@example.org" },
{ site: "lesspass.com" },
],
"site"
);
expect(matches).toEqual([
{
item: {
site: "example.org",
login: "test@example.org",
},
matches: [
{
indices: [[0, 3]],
value: "example.org",
key: "site",
},
],
refIndex: 0,
},
]);
});

test("returnMatchingData substring", () => {
const matches = returnMatchingData(
"exam",
[
{ site: "www.example.org", login: "test@example.org" },
{ site: "lesspass.com" },
],
"site"
);
expect(matches).toEqual([
{
item: {
site: "www.example.org",
login: "test@example.org",
},
matches: [
{
indices: [[4, 7]],
value: "www.example.org",
key: "site",
},
],
refIndex: 0,
},
]);
});

test("returnMatchingData typo", () => {
const matches = returnMatchingData(
"exem",
[
{ site: "www.example.org", login: "test@example.org" },
{ site: "lesspass.com" },
],
"site"
);
expect(matches).toEqual([
{
item: {
site: "www.example.org",
login: "test@example.org",
},
matches: [
{
indices: [[4, 5]],
value: "www.example.org",
key: "site",
},
],
refIndex: 0,
},
]);
});

test("returnMatchingData max length is 3", () => {
const matches = returnMatchingData(
"exam",
[
{ site: "example.org" },
{ site: "www.example.org" },
{ site: "https://www.example.org" },
{ site: "example" },
],
"site"
);
expect(matches.length).toBe(3);
});

test("returnMatchingData ignore one char match", () => {
const matches = returnMatchingData(
"exemp",
[
{ site: "www.example.org", login: "test@example.org" },
{ site: "lesspass.com" },
],
"site"
);
expect(matches).toEqual([
{
item: {
site: "www.example.org",
login: "test@example.org",
},
matches: [
{
indices: [
[4, 5],
[7, 8],
],
value: "www.example.org",
key: "site",
},
],
refIndex: 0,
},
]);
});

test("returnMatchingData no match", () => {
const matches = returnMatchingData(
"no match",
[
{ site: "example.org", login: "test@example.org" },
{ site: "lesspass.com" },
],
"site"
);
expect(matches).toEqual([]);
});

test("returnMatchingData no data", () => {
const matches = returnMatchingData("lesspass", [], "site");
expect(matches).toEqual([]);
});

test("returnMatchingData no valid key", () => {
const matches = returnMatchingData(
"lesspass",
[
{ site: "example.org", login: "test@example.org" },
{ site: "lesspass.com" },
],
"unknown key"
);
expect(matches).toEqual([]);
});

test("returnMatchingData no query", () => {
const matches = returnMatchingData(
"",
[
{ site: "example.org", login: "test@example.org" },
{ site: "lesspass.com" },
],
"site"
);
expect(matches).toEqual([]);
});

+ 1
- 1
mobile/src/version.json View File

@@ -1,3 +1,3 @@
{ {
"version": "9.3.0"
"version": "9.4.0"
} }

+ 4
- 4
mobile/yarn.lock View File

@@ -3260,10 +3260,10 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=


fuse.js@^6.4.6:
version "6.4.6"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.4.6.tgz#62f216c110e5aa22486aff20be7896d19a059b79"
integrity sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw==
fuzzysort@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.4.tgz#a0510206ed44532cbb52cf797bf5a3cb12acd4ba"
integrity sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ==


gensync@^1.0.0-beta.2: gensync@^1.0.0-beta.2:
version "1.0.0-beta.2" version "1.0.0-beta.2"


Loading…
Cancel
Save