mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
refactor of webapp listing
This commit is contained in:
parent
5dc6cc926b
commit
ef73615d20
@ -23,7 +23,6 @@ export function useCards() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const contacts = Array.from(card.state.cards.values()).map(item => {
|
||||
const profile = item?.data?.cardProfile;
|
||||
const detail = item?.data?.cardDetail;
|
||||
|
@ -182,11 +182,9 @@ export function useChannels() {
|
||||
useEffect(() => {
|
||||
const login = store.state['login:timestamp'];
|
||||
const conversations = new Map();
|
||||
const { sealKey } = account.state;
|
||||
card.state.cards.forEach((cardValue, cardId) => {
|
||||
cardValue.channels.forEach((channelValue, channelId) => {
|
||||
const key = `${channelId}::${cardId}`;
|
||||
const { detailRevision, topicRevision } = channelValue.data;
|
||||
let item = channels.current.get(key);
|
||||
if (!item) {
|
||||
item = { cardId, channelId };
|
||||
@ -196,6 +194,7 @@ export function useChannels() {
|
||||
syncChannelSummary(item, channelValue);
|
||||
|
||||
const revision = store.state[key];
|
||||
const topicRevision = channelValue.data?.topicRevision;
|
||||
if (login && item.updated && item.updated > login && topicRevision !== revision) {
|
||||
item.updatedFlag = true;
|
||||
}
|
||||
@ -207,7 +206,6 @@ export function useChannels() {
|
||||
});
|
||||
channel.state.channels.forEach((channelValue, channelId) => {
|
||||
const key = `${channelId}::${undefined}`;
|
||||
const { detailRevision, topicRevision } = channelValue.data;
|
||||
let item = channels.current.get(key);
|
||||
if (!item) {
|
||||
item = { channelId };
|
||||
@ -216,6 +214,7 @@ export function useChannels() {
|
||||
syncChannelSummary(item, channelValue);
|
||||
|
||||
const revision = store.state[key];
|
||||
const topicRevision = channelValue.data?.topicRevision;
|
||||
if (login && item.updated && item.updated > login && topicRevision !== revision) {
|
||||
item.updatedFlag = true;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { ListingItem } from './listingItem/ListingItem';
|
||||
|
||||
export function Listing({ closeListing, openContact }) {
|
||||
|
||||
const [ modal, modalContext ] = Modal.useModal();
|
||||
const { state, actions } = useListing();
|
||||
|
||||
const getListing = async () => {
|
||||
@ -14,7 +15,7 @@ export function Listing({ closeListing, openContact }) {
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
Modal.error({
|
||||
modal.error({
|
||||
title: 'Communication Error',
|
||||
content: 'Please confirm your server name.',
|
||||
});
|
||||
@ -23,6 +24,7 @@ export function Listing({ closeListing, openContact }) {
|
||||
|
||||
return (
|
||||
<ListingWrapper>
|
||||
{ modalContext }
|
||||
<div class="search">
|
||||
{ !state.showFilter && (
|
||||
<div class="showfilter" onClick={actions.showFilter}>
|
||||
@ -64,7 +66,7 @@ export function Listing({ closeListing, openContact }) {
|
||||
{ state.contacts.length > 0 && (
|
||||
<List local={{ emptyText: '' }} itemLayout="horizontal" dataSource={state.contacts} gutter="0"
|
||||
renderItem={item => (
|
||||
<ListingItem item={item} node={state.node} open={openContact} />
|
||||
<ListingItem item={item} open={() => openContact(item.guid, item)} />
|
||||
)} />
|
||||
)}
|
||||
{ state.contacts.length === 0 && (
|
||||
|
@ -1,17 +1,14 @@
|
||||
import { ListingItemWrapper } from './ListingItem.styled';
|
||||
import { useListingItem } from './useListingItem.hook';
|
||||
import { Logo } from 'logo/Logo';
|
||||
|
||||
export function ListingItem({ item, node, open }) {
|
||||
|
||||
const { state } = useListingItem(node, item);
|
||||
export function ListingItem({ item, open }) {
|
||||
|
||||
return (
|
||||
<ListingItemWrapper onClick={() => open(item.guid, item)}>
|
||||
<Logo url={state.logo} width={32} height={32} radius={8} />
|
||||
<ListingItemWrapper onClick={open}>
|
||||
<Logo url={item.logo} width={32} height={32} radius={4} />
|
||||
<div class="details">
|
||||
<div class="name">{ state.name }</div>
|
||||
<div class="handle">{ state.handle }</div>
|
||||
<div class="name">{ item.name }</div>
|
||||
<div class="handle">{ item.handle }</div>
|
||||
</div>
|
||||
</ListingItemWrapper>
|
||||
);
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getListingImageUrl } from 'api/getListingImageUrl';
|
||||
|
||||
export function useListingItem(server, item) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
});
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateState({
|
||||
logo: item.imageSet ? getListingImageUrl(server, item.guid) : null,
|
||||
name: item.name,
|
||||
handle: item.handle,
|
||||
});
|
||||
}, [server, item]);
|
||||
|
||||
const actions = {
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -2,17 +2,18 @@ import { useContext, useState, useEffect } from 'react';
|
||||
import { ProfileContext } from 'context/ProfileContext';
|
||||
import { ViewportContext } from 'context/ViewportContext';
|
||||
import { getListing } from 'api/getListing';
|
||||
import { getListingImageUrl } from 'api/getListingImageUrl';
|
||||
|
||||
export function useListing() {
|
||||
|
||||
const [state, setState] = useState({
|
||||
contacts: [],
|
||||
username: null,
|
||||
node: null,
|
||||
busy: false,
|
||||
disabled: true,
|
||||
display: null,
|
||||
showFilter: false,
|
||||
username: null,
|
||||
display: null,
|
||||
});
|
||||
|
||||
const profile = useContext(ProfileContext);
|
||||
@ -42,9 +43,19 @@ export function useListing() {
|
||||
getListing: async () => {
|
||||
updateState({ busy: true });
|
||||
try {
|
||||
let contacts = await getListing(state.node, state.username);
|
||||
let filtered = contacts.filter(contact => (contact.guid !== profile.state.identity.guid));
|
||||
let sorted = filtered.sort((a, b) => {
|
||||
const listing = await getListing(state.node, state.username);
|
||||
const filtered = listing.filter(item => {
|
||||
return item.guid !== profile.state.identity.guid;
|
||||
});
|
||||
const contacts = filtered.map(item => {
|
||||
return {
|
||||
guid: item.guid,
|
||||
logo: item.imageSet ? getListingImageUrl(state.node, item.guid) : null,
|
||||
name: item.name,
|
||||
handle: item.handle,
|
||||
};
|
||||
});
|
||||
const sorted = contacts.sort((a, b) => {
|
||||
if (a?.name < b?.name) {
|
||||
return -1;
|
||||
}
|
||||
@ -61,13 +72,13 @@ export function useListing() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let node = profile?.state?.identity?.node;
|
||||
const node = profile?.state?.identity?.node;
|
||||
updateState({ disabled: node == null || node === '', node });
|
||||
}, [profile]);
|
||||
}, [profile.state]);
|
||||
|
||||
useEffect(() => {
|
||||
updateState({ display: viewport.state.display });
|
||||
}, [viewport]);
|
||||
}, [viewport.state]);
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
172
net/web/test/Contacts.test.js
Normal file
172
net/web/test/Contacts.test.js
Normal file
@ -0,0 +1,172 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import {render, act, screen, waitFor, fireEvent} from '@testing-library/react'
|
||||
import { AppContextProvider } from 'context/AppContext';
|
||||
import { AccountContextProvider } from 'context/AccountContext';
|
||||
import { ProfileContextProvider } from 'context/ProfileContext';
|
||||
import { CardContext, CardContextProvider } from 'context/CardContext';
|
||||
import { ChannelContextProvider } from 'context/ChannelContext';
|
||||
import { StoreContextProvider } from 'context/StoreContext';
|
||||
import { UploadContextProvider } from 'context/UploadContext';
|
||||
import { ViewportContextProvider } from 'context/ViewportContext';
|
||||
import { useCards } from 'session/cards/useCards.hook';
|
||||
import * as fetchUtil from 'api/fetchUtil';
|
||||
|
||||
let cardContext;
|
||||
function ContactsView() {
|
||||
const { state, actions } = useCards();
|
||||
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
const [cards, setCards] = useState([]);
|
||||
const card = useContext(CardContext);
|
||||
cardContext = card;
|
||||
|
||||
useEffect(() => {
|
||||
const rendered = [];
|
||||
state.cards.forEach(c => {
|
||||
rendered.push(
|
||||
<div key={c.cardId} data-testid="card">
|
||||
<span key={c.cardId} data-testid={'cardid-' + c.cardId}>{ c.name }</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
setCards(rendered);
|
||||
setRenderCount(renderCount + 1);
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<div data-testid="cards" count={renderCount}>
|
||||
{ cards }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContactsTestApp() {
|
||||
return (
|
||||
<UploadContextProvider>
|
||||
<ChannelContextProvider>
|
||||
<CardContextProvider>
|
||||
<ProfileContextProvider>
|
||||
<StoreContextProvider>
|
||||
<AccountContextProvider>
|
||||
<ViewportContextProvider>
|
||||
<ContactsView />
|
||||
</ViewportContextProvider>
|
||||
</AccountContextProvider>
|
||||
</StoreContextProvider>
|
||||
</ProfileContextProvider>
|
||||
</CardContextProvider>
|
||||
</ChannelContextProvider>
|
||||
</UploadContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
let fetchCards;
|
||||
let fetchProfile;
|
||||
const realFetchWithTimeout = fetchUtil.fetchWithTimeout;
|
||||
const realFetchWithCustomTimeout = fetchUtil.fetchWithCustomTimeout;
|
||||
beforeEach(() => {
|
||||
|
||||
fetchCards = [];
|
||||
fetchProfile = {};
|
||||
|
||||
const mockFetch = jest.fn().mockImplementation((url, options) => {
|
||||
|
||||
if (url.startsWith('/contact/cards?agent')) {
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve(fetchCards)
|
||||
});
|
||||
}
|
||||
else if (url.startsWith('/contact/cards/000a/profile?agent')) {
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve(fetchProfile)
|
||||
});
|
||||
}
|
||||
else {
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve([])
|
||||
});
|
||||
}
|
||||
});
|
||||
fetchUtil.fetchWithTimeout = mockFetch;
|
||||
fetchUtil.fetchWithCustomTimeout = mockFetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchUtil.fetchWithTimeout = realFetchWithTimeout;
|
||||
fetchUtil.fetchWithCustomTimeout = realFetchWithCustomTimeout;
|
||||
});
|
||||
|
||||
test('add, update and remove contact', async () => {
|
||||
render(<ContactsTestApp />);
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(cardContext).not.toBe(null);
|
||||
});
|
||||
|
||||
fetchCards = [{
|
||||
id: '000a',
|
||||
revision: 1,
|
||||
data: {
|
||||
detailRevision: 2,
|
||||
profileRevision: 3,
|
||||
notifiedProfile: 3,
|
||||
notifiedArticle: 5,
|
||||
notifiedChannel: 6,
|
||||
notifiedView: 7,
|
||||
cardDetail: { status: 'connected', statusUpdate: 136, token: '01ab', },
|
||||
cardProfile: { guid: '01ab23', handle: 'test1', name: 'tester', imageSet: false,
|
||||
seal: 'abc', version: '1.1.1', node: 'test.org' },
|
||||
},
|
||||
}];
|
||||
|
||||
await act(async () => {
|
||||
cardContext.actions.setToken('abc123');
|
||||
cardContext.actions.setRevision(1);
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(screen.getByTestId('cards').children).toHaveLength(1);
|
||||
expect(screen.getByTestId('cardid-000a').textContent).toBe('tester');
|
||||
});
|
||||
|
||||
fetchCards = [{
|
||||
id: '000a',
|
||||
revision: 2,
|
||||
data: {
|
||||
detailRevision: 2,
|
||||
profileRevision: 4,
|
||||
notifiedProfile: 3,
|
||||
notifiedArticle: 5,
|
||||
notifiedChannel: 6,
|
||||
notifiedView: 7,
|
||||
}
|
||||
}];
|
||||
|
||||
fetchProfile = { guid: '01ab23', handle: 'test1', name: 'tested', imageSet: false,
|
||||
seal: 'abc', version: '1.1.1', node: 'test.org' };
|
||||
|
||||
await act(async () => {
|
||||
cardContext.actions.setRevision(2);
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(screen.getByTestId('cardid-000a').textContent).toBe('tested');
|
||||
});
|
||||
|
||||
fetchCards = [{
|
||||
id: '000a',
|
||||
revision: 3,
|
||||
}];
|
||||
|
||||
await act(async () => {
|
||||
cardContext.actions.setRevision(3);
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(screen.getByTestId('cards').children).toHaveLength(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
98
net/web/test/Listing.test.js
Normal file
98
net/web/test/Listing.test.js
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import {render, act, screen, waitFor, fireEvent} from '@testing-library/react'
|
||||
import { ProfileContextProvider } from 'context/ProfileContext';
|
||||
import { ViewportContextProvider } from 'context/ViewportContext';
|
||||
import { useListing } from 'session/listing/useListing.hook';
|
||||
import * as fetchUtil from 'api/fetchUtil';
|
||||
|
||||
let listing = null;
|
||||
function ListingView() {
|
||||
const { state, actions } = useListing();
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
const [contacts, setContacts] = useState([]);
|
||||
|
||||
listing = actions;
|
||||
useEffect(() => {
|
||||
|
||||
const rendered = [];
|
||||
state.contacts.forEach(item => {
|
||||
rendered.push(
|
||||
<div key={item.guid} data-testid="contact">
|
||||
<span key={item.guid} data-testid={'contact-' + item.guid}>{ item.name }</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
setContacts(rendered);
|
||||
setRenderCount(renderCount + 1);
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<div data-testid="contacts" count={renderCount}>
|
||||
{ contacts }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ListingTestApp() {
|
||||
return (
|
||||
<ProfileContextProvider>
|
||||
<ViewportContextProvider>
|
||||
<ListingView />
|
||||
</ViewportContextProvider>
|
||||
</ProfileContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
let fetchListing;
|
||||
const realFetchWithTimeout = fetchUtil.fetchWithTimeout;
|
||||
const realFetchWithCustomTimeout = fetchUtil.fetchWithCustomTimeout;
|
||||
beforeEach(() => {
|
||||
fetchListing = [];
|
||||
|
||||
const mockFetch = jest.fn().mockImplementation((url, options) => {
|
||||
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve(fetchListing)
|
||||
});
|
||||
});
|
||||
fetchUtil.fetchWithTimeout = mockFetch;
|
||||
fetchUtil.fetchWithCustomTimeout = mockFetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchUtil.fetchWithTimeout = realFetchWithTimeout;
|
||||
fetchUtil.fetchWithCustomTimeout = realFetchWithCustomTimeout;
|
||||
});
|
||||
|
||||
test('retrieve listing', async () => {
|
||||
render(<ListingTestApp />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(listing).not.toBe(null);
|
||||
});
|
||||
|
||||
fetchListing = [
|
||||
{
|
||||
guid: 'abc123',
|
||||
handle: 'tester',
|
||||
name: 'mr. tester',
|
||||
description: 'a tester',
|
||||
location: 'here',
|
||||
imageSet: false,
|
||||
version: '0.0.1',
|
||||
node: 'test.org',
|
||||
},
|
||||
];
|
||||
|
||||
await act(async () => {
|
||||
await listing.getListing();
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(screen.getByTestId('contact-abc123').textContent).toBe('mr. tester');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import {render, act, screen, waitFor, fireEvent} from '@testing-library/react'
|
||||
import { AppContext, AppContextProvider } from 'context/AppContext';
|
||||
import { AppContextProvider } from 'context/AppContext';
|
||||
import { AccountContextProvider } from 'context/AccountContext';
|
||||
import { ProfileContextProvider } from 'context/ProfileContext';
|
||||
import { CardContext, CardContextProvider } from 'context/CardContext';
|
||||
@ -43,7 +43,7 @@ function TopicsView() {
|
||||
);
|
||||
}
|
||||
|
||||
function AccessTestApp() {
|
||||
function TopicsTestApp() {
|
||||
return (
|
||||
<UploadContextProvider>
|
||||
<ChannelContextProvider>
|
||||
@ -107,7 +107,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
test('view merged channels', async () => {
|
||||
render(<AccessTestApp />);
|
||||
render(<TopicsTestApp />);
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(cardContext).not.toBe(null);
|
||||
|
Loading…
Reference in New Issue
Block a user