Sess header fixes (#2124)
* feat ui: cross dead tabs, some ui fixes * ? lost changes?
This commit is contained in:
parent
690ec9bf34
commit
4444ecf6e0
10 changed files with 150 additions and 80 deletions
|
|
@ -38,9 +38,13 @@ function Modal({ tabs, currentTab, changeTab, hideModal }: Props) {
|
|||
function SessionTabs({ isLive }: { isLive?: boolean }) {
|
||||
const { showModal, hideModal } = useModal();
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { tabs = new Set('back-compat'), currentTab } = store.get();
|
||||
const { tabs = new Set('back-compat'), currentTab, closedTabs } = store.get();
|
||||
|
||||
const tabsArr = Array.from(tabs).map((tab, idx) => ({ tab, idx }));
|
||||
const tabsArr = Array.from(tabs).map((tab, idx) => ({
|
||||
tab,
|
||||
idx,
|
||||
isClosed: closedTabs.includes(tab)
|
||||
}));
|
||||
const shouldTruncate = tabsArr.length > 10;
|
||||
const actualTabs = shouldTruncate ? tabsArr.slice(0, 10) : tabsArr;
|
||||
|
||||
|
|
@ -49,6 +53,7 @@ function SessionTabs({ isLive }: { isLive?: boolean }) {
|
|||
? actualTabs
|
||||
: actualTabs.concat({
|
||||
tab: currentTab,
|
||||
isClosed: false,
|
||||
idx: tabsArr.findIndex((tEl) => tEl.tab === currentTab),
|
||||
});
|
||||
const changeTab = (tab: string) => {
|
||||
|
|
@ -74,6 +79,7 @@ function SessionTabs({ isLive }: { isLive?: boolean }) {
|
|||
currentTab={actualTabs.length === 1 ? tab.tab : currentTab}
|
||||
changeTab={changeTab}
|
||||
isLive={isLive}
|
||||
isClosed={tab.isClosed}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ interface Props {
|
|||
currentTab: string;
|
||||
changeTab?: (tab: string) => void;
|
||||
isLive?: boolean;
|
||||
isClosed?: boolean;
|
||||
}
|
||||
|
||||
function Tab({ i, tab, currentTab, changeTab, isLive }: Props) {
|
||||
function Tab({ i, tab, currentTab, changeTab, isLive, isClosed }: Props) {
|
||||
return (
|
||||
<div
|
||||
key={tab}
|
||||
|
|
@ -20,7 +21,8 @@ function Tab({ i, tab, currentTab, changeTab, isLive }: Props) {
|
|||
changeTab && !isLive ? 'cursor-pointer' : 'cursor-default',
|
||||
currentTab === tab
|
||||
? 'border-gray-lighter border-t border-l border-r !border-b-white bg-white rounded-tl rounded-tr font-semibold'
|
||||
: 'cursor-pointer border-gray-lighter !border-b !border-t-transparent !border-l-transparent !border-r-transparent'
|
||||
: 'cursor-pointer border-gray-lighter !border-b !border-t-transparent !border-l-transparent !border-r-transparent',
|
||||
isClosed ? 'line-through': ''
|
||||
)}
|
||||
>
|
||||
Tab {i + 1}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function SelectDateRange(props: Props) {
|
|||
defaultValue={selectedValue?.value ?? undefined}
|
||||
>
|
||||
<div
|
||||
className={'cursor-pointer font-semibold flex items-center gap-2'}
|
||||
className={'cursor-pointer flex items-center gap-2'}
|
||||
>
|
||||
<div>{isCustomRange ? customRange : selectedValue?.label}</div>
|
||||
<DownOutlined />
|
||||
|
|
|
|||
|
|
@ -1,34 +1,27 @@
|
|||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, Segmented } from 'antd';
|
||||
import { Segmented } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { TAGS, iTag } from 'App/services/NotesService';
|
||||
|
||||
|
||||
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
import { SortDropdown } from '../SessionSort/SessionSort';
|
||||
|
||||
const sortOptionsMap = {
|
||||
'createdAt-DESC': 'Newest',
|
||||
'createdAt-ASC': 'Oldest',
|
||||
};
|
||||
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({ value, label }));
|
||||
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
const notesOwner = [
|
||||
{ value: '0', label: 'All Notes' },
|
||||
{ value: '1', label: 'My Notes' },
|
||||
];
|
||||
function toTitleCase(str) {
|
||||
return str.replace(
|
||||
/\w\S*/g,
|
||||
function(txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
}
|
||||
);
|
||||
return str.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
}
|
||||
function NoteTags() {
|
||||
const { notesStore } = useStore();
|
||||
|
|
@ -40,55 +33,56 @@ function NoteTags() {
|
|||
options={[
|
||||
{
|
||||
value: 'ALL',
|
||||
label: 'All',
|
||||
label: (
|
||||
<div
|
||||
className={
|
||||
notesStore.activeTags.includes('ALL') ||
|
||||
notesStore.activeTags.length === 0
|
||||
? 'text-main'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
All
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...TAGS.map((tag: iTag) => ({
|
||||
value: tag,
|
||||
label: toTitleCase(tag),
|
||||
label: (
|
||||
<div
|
||||
className={
|
||||
notesStore.activeTags.includes(tag) ? 'text-main' : ''
|
||||
}
|
||||
>
|
||||
{toTitleCase(tag)}
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
]}
|
||||
onChange={(value: iTag) => notesStore.toggleTag(value)}
|
||||
/>
|
||||
<div className="ml-auto" />
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: notesOwner.map(({ value, label }) => ({ key: value, label })),
|
||||
onClick: ({ key }) => {
|
||||
notesStore.toggleShared(key === '1');
|
||||
},
|
||||
<SortDropdown
|
||||
sortOptions={notesOwner.map(({ value, label }) => ({
|
||||
key: value,
|
||||
label,
|
||||
}))}
|
||||
onSort={({ key }) => {
|
||||
notesStore.toggleShared(key === '1');
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'cursor-pointer flex items-center justify-end gap-2 font-semibold'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{notesStore.ownOnly ? notesOwner[1].label : notesOwner[0].label}
|
||||
</div>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
current={notesStore.ownOnly ? notesOwner[1].label : notesOwner[0].label}
|
||||
/>
|
||||
<div className="ml-2 w-2" />
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: sortOptions.map(({ value, label }) => ({ key: value, label })),
|
||||
onClick: ({ key }) => {
|
||||
notesStore.toggleSort(key);
|
||||
},
|
||||
<SortDropdown
|
||||
sortOptions={sortOptions.map(({ value, label }) => ({
|
||||
key: value,
|
||||
label,
|
||||
}))}
|
||||
onSort={({ key }) => {
|
||||
notesStore.toggleSort(key);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'cursor-pointer flex items-center justify-end gap-2 font-semibold'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{notesStore.order === "DESC" ? "Newest" : "Oldest"}
|
||||
</div>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
current={notesStore.order === 'DESC' ? 'Newest' : 'Oldest'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,34 @@ interface Props {
|
|||
sort: (sort: string, sign: number) => void;
|
||||
}
|
||||
|
||||
export function SortDropdown<T>({ defaultOption, onSort, sortOptions, current }: {
|
||||
defaultOption?: string,
|
||||
onSort: ({ key, item }: { key: string, item: T }) => void,
|
||||
sortOptions: any,
|
||||
current: string
|
||||
}) {
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: sortOptions,
|
||||
defaultSelectedKeys: defaultOption ? [defaultOption] : undefined,
|
||||
// @ts-ignore
|
||||
onClick: onSort,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'cursor-pointer flex items-center justify-end gap-2'
|
||||
}
|
||||
>
|
||||
<div>{current}</div>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionSort(props: Props) {
|
||||
const { sort, order } = props.filter;
|
||||
const onSort = ({ key }: { key: string }) => {
|
||||
|
|
@ -38,22 +66,12 @@ function SessionSort(props: Props) {
|
|||
const defaultOption = `${sort}-${order}`;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: sortOptions,
|
||||
defaultSelectedKeys: [defaultOption],
|
||||
onClick: onSort,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'cursor-pointer flex items-center justify-end gap-2 font-semibold'
|
||||
}
|
||||
>
|
||||
<div>{sortOptionsMap[defaultOption]}</div>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
<SortDropdown
|
||||
defaultOption={defaultOption}
|
||||
onSort={onSort}
|
||||
sortOptions={sortOptions}
|
||||
current={sortOptionsMap[defaultOption]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ const SessionTags: React.FC<Props> = memo(({ activeTab, tags, total, setActiveTa
|
|||
label: <div className={'flex items-center gap-2'}>
|
||||
{tag.icon ? <Icon
|
||||
name={tag.icon}
|
||||
color={activeTab.type === tag.type ? "teal" : "gray-medium"}
|
||||
color={activeTab.type === tag.type ? "main" : undefined}
|
||||
size="14"
|
||||
className={cn("group-hover:fill-teal mr-2")}
|
||||
className={cn("group-hover:fill-teal")}
|
||||
/> : null}
|
||||
<div>{tag.name}</div>
|
||||
<div className={activeTab.type === tag.type ? 'text-main' : ''}>{tag.name}</div>
|
||||
</div>,
|
||||
value: tag.type,
|
||||
disabled: disable && tag.type !== 'all',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { PlayerMsg, SessionFilesInfo, Store } from 'Player';
|
||||
import type { PlayerMsg, SessionFilesInfo, Store } from 'Player';
|
||||
import unpackTar from 'Player/common/tarball';
|
||||
import unpack from 'Player/common/unpack';
|
||||
import IOSMessageManager from 'Player/mobile/IOSMessageManager';
|
||||
|
|
@ -132,7 +132,9 @@ export default class MessageLoader {
|
|||
this.messageManager.distributeMessage(msg);
|
||||
});
|
||||
logger.info('Messages count: ', msgs.length, msgs, file);
|
||||
|
||||
if (file === 'd:dom 2' && 'createTabCloseEvents' in this.messageManager) {
|
||||
this.messageManager.createTabCloseEvents();
|
||||
}
|
||||
this.messageManager.sortDomRemoveMessages(msgs);
|
||||
this.messageManager.setMessagesLoading(false);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import ListWalker from '../common/ListWalker';
|
|||
import MouseMoveManager from './managers/MouseMoveManager';
|
||||
|
||||
import ActivityManager from './managers/ActivityManager';
|
||||
import TabClosingManager from "./managers/TabClosingManager";
|
||||
|
||||
import { MouseThrashing, MType } from './messages';
|
||||
import type { Message, MouseClick } from './messages';
|
||||
|
|
@ -63,6 +64,7 @@ export interface State extends ScreenState {
|
|||
currentTab: string;
|
||||
tabs: Set<string>;
|
||||
tabChangeEvents: TabChangeEvent[];
|
||||
closedTabs: string[];
|
||||
sessionStart: number;
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +93,7 @@ export default class MessageManager {
|
|||
currentTab: '',
|
||||
tabs: new Set(),
|
||||
tabChangeEvents: [],
|
||||
closedTabs: [],
|
||||
sessionStart: 0,
|
||||
};
|
||||
|
||||
|
|
@ -99,6 +102,7 @@ export default class MessageManager {
|
|||
private activityManager: ActivityManager | null = null;
|
||||
private mouseMoveManager: MouseMoveManager;
|
||||
private activeTabManager = new ActiveTabManager();
|
||||
private tabCloseManager = new TabClosingManager();
|
||||
|
||||
public readonly decoder = new Decoder();
|
||||
|
||||
|
|
@ -177,6 +181,19 @@ export default class MessageManager {
|
|||
this.state.update({ messagesProcessed: true });
|
||||
};
|
||||
|
||||
public createTabCloseEvents = () => {
|
||||
const lastMsgArr: [string, number][] = []
|
||||
Object.entries(this.tabs).forEach((entry, i) => {
|
||||
const [tabId, tab] = entry
|
||||
const { lastMessageTs } = tab
|
||||
if (lastMessageTs && tabId) lastMsgArr.push([tabId, lastMessageTs])
|
||||
})
|
||||
lastMsgArr.sort((a, b) => a[1] - b[1])
|
||||
lastMsgArr.forEach(([tabId, lastMessageTs]) => {
|
||||
this.tabCloseManager.append({ tabId, time: lastMessageTs })
|
||||
})
|
||||
}
|
||||
|
||||
public startLoading = () => {
|
||||
this.waitingForFiles = true;
|
||||
this.state.update({ messagesProcessed: false });
|
||||
|
|
@ -195,6 +212,12 @@ export default class MessageManager {
|
|||
move(t: number): any {
|
||||
// usually means waiting for messages from live session
|
||||
if (Object.keys(this.tabs).length === 0) return;
|
||||
this.tabCloseManager.moveReady(t).then(m => {
|
||||
if (m) {
|
||||
const closedTabs = this.tabCloseManager.closedTabs
|
||||
this.state.update({ closedTabs: Array.from(closedTabs) })
|
||||
}
|
||||
})
|
||||
this.activeTabManager.moveReady(t).then((tabId) => {
|
||||
// Moving mouse and setting :hover classes on ready view
|
||||
this.mouseMoveManager.move(t);
|
||||
|
|
@ -210,6 +233,7 @@ export default class MessageManager {
|
|||
if (!this.activeTab) {
|
||||
this.activeTab = this.state.get().currentTab ?? Object.keys(this.tabs)[0];
|
||||
}
|
||||
|
||||
if (tabId) {
|
||||
if (this.activeTab !== tabId) {
|
||||
this.state.update({ currentTab: tabId });
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export default class TabSessionManager {
|
|||
private scrollManager: ListWalker<SetViewportScroll> = new ListWalker();
|
||||
|
||||
public readonly decoder = new Decoder();
|
||||
public lastMessageTs = 0;
|
||||
private lists: Lists;
|
||||
private navigationStartOffset = 0;
|
||||
private canvasManagers: {
|
||||
|
|
@ -167,6 +168,7 @@ export default class TabSessionManager {
|
|||
}
|
||||
|
||||
distributeMessage(msg: Message): void {
|
||||
this.lastMessageTs = msg.time
|
||||
switch (msg.tp) {
|
||||
case MType.CanvasNode:
|
||||
const managerId = `${msg.timestamp}_${msg.nodeId}`;
|
||||
|
|
|
|||
22
frontend/app/player/web/managers/TabClosingManager.ts
Normal file
22
frontend/app/player/web/managers/TabClosingManager.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import ListWalker from '../../common/ListWalker';
|
||||
|
||||
export default class TabClosingManager extends ListWalker<{ tabId: string, time: number }> {
|
||||
currentTime = 0;
|
||||
closedTabs: Set<string> = new Set();
|
||||
|
||||
moveReady(t: number): Promise<string | null> {
|
||||
if (t < this.currentTime) {
|
||||
this.reset()
|
||||
}
|
||||
this.currentTime = t
|
||||
const msg = this.moveGetLast(t)
|
||||
|
||||
if (msg) {
|
||||
const ids = this.listNow.map(m => m.tabId);
|
||||
this.closedTabs = new Set(ids)
|
||||
return Promise.resolve(msg.tabId)
|
||||
} else {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue