refactor of webapp listing

This commit is contained in:
Roland Osborne 2023-01-21 22:38:02 -08:00
parent 5dc6cc926b
commit ef73615d20
9 changed files with 303 additions and 51 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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 && (

View File

@ -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>
);

View File

@ -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 };
}

View File

@ -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 };
}

View 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);
});
});

View 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');
});
});

View File

@ -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);