Sess header fixes (#2122)

* fix ui: fixes for session list header

* same for notes

* console
This commit is contained in:
Delirium 2024-04-23 16:15:00 +02:00 committed by GitHub
parent 398041de16
commit 690ec9bf34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 356 additions and 102 deletions

View file

@ -0,0 +1,157 @@
// @ts-nocheck
import { DatePicker } from 'antd';
import { PickerTimeProps } from 'antd/es/time-picker';
import moment from 'moment';
import type { Moment } from 'moment';
import React from 'react';
const generateConfig = {
// get
getNow: () => moment(),
getFixedDate: (string: string) => moment(string, 'YYYY-MM-DD'),
getEndDate: (date) => {
const clone = date.clone();
return clone.endOf('month');
},
getWeekDay: (date) => {
const clone = date.clone().locale('en_US');
return clone.weekday() + clone.localeData().firstDayOfWeek();
},
getYear: (date) => date.year(),
getMonth: (date) => date.month(),
getDate: (date) => date.date(),
getHour: (date) => date.hour(),
getMinute: (date) => date.minute(),
getSecond: (date) => date.second(),
getMillisecond: (date) => date.millisecond(),
// set
addYear: (date, diff) => {
const clone = date.clone();
return clone.add(diff, 'year');
},
addMonth: (date, diff) => {
const clone = date.clone();
return clone.add(diff, 'month');
},
addDate: (date, diff) => {
const clone = date.clone();
return clone.add(diff, 'day');
},
setYear: (date, year) => {
const clone = date.clone();
return clone.year(year);
},
setMonth: (date, month) => {
const clone = date.clone();
return clone.month(month);
},
setDate: (date, num) => {
const clone = date.clone();
return clone.date(num);
},
setHour: (date, hour) => {
const clone = date.clone();
return clone.hour(hour);
},
setMinute: (date, minute) => {
const clone = date.clone();
return clone.minute(minute);
},
setSecond: (date, second) => {
const clone = date.clone();
return clone.second(second);
},
setMillisecond: (date, millisecond) => {
const clone = date.clone();
return clone.millisecond(millisecond);
},
// Compare
isAfter: (date1, date2) => date1.isAfter(date2),
isValidate: (date) => date.isValid(),
locale: {
getWeekFirstDay: (locale) => {
const date = moment().locale(locale);
return date.localeData().firstDayOfWeek();
},
getWeekFirstDate: (locale, date) => {
const clone = date.clone();
const result = clone.locale(locale);
return result.weekday(0);
},
getWeek: (locale, date) => {
const clone = date.clone();
const result = clone.locale(locale);
return result.week();
},
getShortWeekDays: (locale) => {
const date = moment().locale(locale);
return date.localeData().weekdaysMin();
},
getShortMonths: (locale) => {
const date = moment().locale(locale);
return date.localeData().monthsShort();
},
format: (locale, date, format) => {
const clone = date.clone();
const result = clone.locale(locale);
return result.format(format);
},
parse: (locale, text, formats) => {
const fallbackFormatList: string[] = [];
for (let i = 0; i < formats.length; i += 1) {
let format = formats[i];
let formatText = text;
if (format.includes('wo') || format.includes('Wo')) {
format = format.replace(/wo/g, 'w').replace(/Wo/g, 'W');
const matchFormat = format.match(/[-YyMmDdHhSsWwGg]+/g);
const matchText = formatText.match(/[-\d]+/g);
if (matchFormat && matchText) {
format = matchFormat.join('');
formatText = matchText.join('');
} else {
fallbackFormatList.push(format.replace(/o/g, ''));
}
}
const date = moment(formatText, format, locale, true);
if (date.isValid()) {
return date;
}
}
// Fallback to fuzzy matching, this should always not reach match or need fire a issue
for (let i = 0; i < fallbackFormatList.length; i += 1) {
const date = moment(text, fallbackFormatList[i], locale, false);
/* istanbul ignore next */
if (date.isValid()) {
console.error(
'Not match any format strictly and fallback to fuzzy match. Please help to fire a issue about this.'
);
return date;
}
}
return null;
},
},
};
const CustomPicker = DatePicker.generatePicker<Moment>(generateConfig);
export interface TimePickerProps
extends Omit<PickerTimeProps<Moment>, 'picker'> {}
const TimePicker = React.forwardRef<any, TimePickerProps>((props, ref) => (
<CustomPicker {...props} picker="time" mode={undefined} ref={ref} />
));
TimePicker.displayName = 'TimePicker';
export { CustomPicker as DatePicker, TimePicker };

View file

@ -1,8 +1,8 @@
import React from 'react';
import DateRangePicker from 'react-daterange-picker'
import TimePicker from 'rc-time-picker';
import { Button } from 'UI';
import { getDateRangeFromValue, getDateRangeLabel, dateRangeValues, CUSTOM_RANGE, moment, DATE_RANGE_VALUES } from 'App/dateRange';
import { Button } from 'antd'
import { TimePicker } from 'App/components/shared/DatePicker'
import styles from './dateRangePopup.module.css';
@ -24,8 +24,8 @@ export default class DateRangePopup extends React.PureComponent {
if (value.isAfter(this.state.range.end)) {
return;
}
this.setState({
range: moment.range(
this.setState({
range: moment.range(
value,
this.state.range.end,
),
@ -36,8 +36,8 @@ export default class DateRangePopup extends React.PureComponent {
if (value && value.isBefore(this.state.range.start)) {
return;
}
this.setState({
range: moment.range(
this.setState({
range: moment.range(
this.state.range.start,
value,
),
@ -72,7 +72,7 @@ export default class DateRangePopup extends React.PureComponent {
<div className={ styles.preSelections }>
{ dateRangeValues.filter(value => value !== CUSTOM_RANGE && value !== DATE_RANGE_VALUES.LAST_30_MINUTES).map(value => (
<div
key={ value }
key={ value }
onClick={ () => this.selectValue(value) }
>
{ getDateRangeLabel(value) }
@ -92,29 +92,31 @@ export default class DateRangePopup extends React.PureComponent {
/>
</div>
<div className="flex items-center justify-between py-2 px-3">
<div>
<div className="flex items-center gap-2">
<label>From: </label>
<span>{range.start.format("DD/MM")} </span>
<TimePicker
value={ range.start }
showSecond={ false }
allowEmpty={false}
onChange={ this.setRangeTimeStart }
className="mr-2 w-24"
format={"HH:mm"}
defaultValue={ range.start }
className="w-24"
onChange={this.setRangeTimeStart}
needConfirm={false}
showNow={false}
/>
<label>To: </label>
<span>{range.end.format("DD/MM")} </span>
<TimePicker
value={ range.end }
showSecond={ false }
allowEmpty={false}
onChange={ this.setRangeTimeEnd }
<TimePicker
format={"HH:mm"}
defaultValue={ range.end }
onChange={this.setRangeTimeEnd}
className="w-24"
needConfirm={false}
showNow={false}
/>
</div>
<div className="flex items-center">
<Button onClick={ onCancel }>{ 'Cancel' }</Button>
<Button variant="primary" className="ml-2" onClick={ this.onApply } disabled={ !range }>{ 'Apply' }</Button>
<Button type="primary" className="ml-2" onClick={ this.onApply } disabled={ !range }>{ 'Apply' }</Button>
</div>
</div>
</div>

View file

@ -1,13 +1,16 @@
import React from 'react';
import { Select as AntSelect } from 'antd';
import { DATE_RANGE_OPTIONS, CUSTOM_RANGE } from 'App/dateRange';
import Select from 'Shared/Select';
import { DownOutlined } from '@ant-design/icons';
import Period from 'Types/app/period';
import { components } from 'react-select';
import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import { Dropdown, DatePicker } from 'antd';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { components } from 'react-select';
import { CUSTOM_RANGE, DATE_RANGE_OPTIONS } from 'App/dateRange';
import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import Select from 'Shared/Select';
interface Props {
period: any;
@ -23,8 +26,11 @@ interface Props {
function SelectDateRange(props: Props) {
const [isCustom, setIsCustom] = React.useState(false);
const [isCustomOpen, setIsCustomOpen] = React.useState(false);
const { right = false, period, disableCustom = false, timezone } = props;
let selectedValue = DATE_RANGE_OPTIONS.find((obj: any) => obj.value === period.rangeName);
let selectedValue = DATE_RANGE_OPTIONS.find(
(obj: any) => obj.value === period.rangeName
);
const options = DATE_RANGE_OPTIONS.filter((obj: any) =>
disableCustom ? obj.value !== CUSTOM_RANGE : true
);
@ -33,7 +39,7 @@ function SelectDateRange(props: Props) {
if (value === CUSTOM_RANGE) {
setTimeout(() => {
setIsCustom(true);
}, 1)
}, 1);
} else {
// @ts-ignore
props.onChange(new Period({ rangeName: value }));
@ -42,27 +48,48 @@ function SelectDateRange(props: Props) {
const onApplyDateRange = (value: any) => {
// @ts-ignore
const range = new Period({ rangeName: CUSTOM_RANGE, start: value.start, end: value.end });
const range = new Period({
rangeName: CUSTOM_RANGE,
start: value.start,
end: value.end,
});
props.onChange(range);
setIsCustom(false);
};
const isCustomRange = period.rangeName === CUSTOM_RANGE;
const customRange = isCustomRange ? period.rangeFormatted() : '';
if (props.isAnt) {
const onAntUpdate = (val: any) => {
onChange(val);
};
return (
<div className={'relative'}>
<AntSelect
size={props.small ? 'small' : undefined}
options={options}
<Dropdown
menu={{
items: options.map((opt) => ({
label: opt.label,
key: opt.value,
})),
defaultSelectedKeys: selectedValue?.value
? [selectedValue.value]
: undefined,
onClick: (e: any) => {
onChange(e.key);
},
}}
onChange={onAntUpdate}
style={{ width: 170 }}
defaultValue={selectedValue?.value ?? undefined}
/>
>
<div
className={'cursor-pointer font-semibold flex items-center gap-2'}
>
<div>{isCustomRange ? customRange : selectedValue?.label}</div>
<DownOutlined />
</div>
</Dropdown>
{isCustom && (
<OutsideClickDetectingDiv
onClickOutside={(e: any) => {
@ -70,7 +97,12 @@ function SelectDateRange(props: Props) {
e.target.parentElement.parentElement.classList.contains(
'rc-time-picker-panel-select'
) ||
e.target.parentElement.parentElement.classList[0]?.includes('-menu')
e.target.parentElement.parentElement.classList[0]?.includes(
'-menu'
)
|| e.target.className.includes(
'ant-picker'
)
) {
return false;
}
@ -78,7 +110,10 @@ function SelectDateRange(props: Props) {
}}
>
<div
className={cn('absolute top-0 mt-10 z-40', { 'right-0': right })}
className={cn(
'absolute top-0 mt-10 z-40',
{ 'right-0': right }
)}
style={{
width: '770px',
fontSize: '14px',
@ -124,11 +159,12 @@ function SelectDateRange(props: Props) {
e.target.parentElement.parentElement.classList.contains(
'rc-time-picker-panel-select'
) ||
e.target.parentElement.parentElement.classList[0]?.includes('-menu')
e.target.parentElement.parentElement.classList[0]?.includes(
'-menu'
)
) {
return false;
}
console.log('outside')
setIsCustom(false);
}}
>

View file

@ -1,9 +1,17 @@
import React from 'react';
import Select from 'Shared/Select';
import { TAGS, iTag } from 'App/services/NotesService';
import { TagItem } from '../SessionTags';
import { useStore } from 'App/mstore';
import { DownOutlined } from "@ant-design/icons";
import { Dropdown, 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';
const sortOptionsMap = {
'createdAt-DESC': 'Newest',
@ -14,45 +22,73 @@ 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();
}
);
}
function NoteTags() {
const { notesStore } = useStore();
return (
<div className="flex items-center w-full">
<div>
<TagItem
onClick={() => notesStore.toggleTag()}
label="ALL"
isActive={notesStore.activeTags.length === 0}
/>
</div>
{TAGS.map((tag: iTag) => (
<div key={tag}>
<TagItem
onClick={() => notesStore.toggleTag(tag)}
label={tag}
isActive={notesStore.activeTags.includes(tag)}
/>
</div>
))}
<Segmented
size="small"
options={[
{
value: 'ALL',
label: 'All',
},
...TAGS.map((tag: iTag) => ({
value: tag,
label: toTitleCase(tag),
})),
]}
onChange={(value: iTag) => notesStore.toggleTag(value)}
/>
<div className="ml-auto" />
<Select
name="notesOwner"
plain
right
options={notesOwner}
onChange={({ value }) => notesStore.toggleShared(value.value === '1')}
defaultValue={notesOwner[0].value}
/>
<div className="ml-2" />
<Select
name="sortNotes"
plain
right
options={sortOptions}
onChange={({ value }) => notesStore.toggleSort(value.value)}
defaultValue={sortOptions[0].value}
/>
<Dropdown
menu={{
items: notesOwner.map(({ value, label }) => ({ key: value, label })),
onClick: ({ 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>
<div className="ml-2 w-2" />
<Dropdown
menu={{
items: sortOptions.map(({ value, label }) => ({ key: value, label })),
onClick: ({ 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>
</div>
);
}

View file

@ -1,4 +1,5 @@
import { Select } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import { Dropdown } from 'antd';
import React from 'react';
import { connect } from 'react-redux';
@ -13,8 +14,9 @@ const sortOptionsMap = {
};
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({
value,
// value,
label,
key: value,
}));
interface Props {
@ -26,21 +28,32 @@ interface Props {
function SessionSort(props: Props) {
const { sort, order } = props.filter;
const onSort = (value: string) => {
const [sort, order] = value.split('-');
const onSort = ({ key }: { key: string }) => {
const [sort, order] = key.split('-');
const sign = order === 'desc' ? -1 : 1;
props.applyFilter({ order, sort });
props.sort(sort, sign);
};
const defaultOption = `${sort}-${order}`;
return (
<Select
style={{ width: 160 }}
options={sortOptions}
onChange={onSort}
defaultValue={defaultOption}
/>
<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>
);
}

View file

@ -41,12 +41,20 @@ const SessionTags: React.FC<Props> = memo(({ activeTab, tags, total, setActiveTa
value: tag.type,
disabled: disable && tag.type !== 'all',
}))
const onPick = (tabValue: string) => {
const tab = tags.find((t) => t.type === tabValue);
if (tab) {
setActiveTab(tab);
}
}
return (
<div className='flex items-center'>
<Segmented
options={options}
value={activeTab.type}
onChange={setActiveTab}
onChange={onPick}
size={'small'}
/>
</div>
);

View file

@ -17,6 +17,12 @@ export default class NotesStore {
makeAutoObservable(this)
}
setLoading(loading: boolean) {
this.loading = loading
}
setNotes(notes: Note[]) {
this.notes = notes
}
async fetchNotes() {
const filter: NotesFilter = {
page: this.page,
@ -28,20 +34,20 @@ export default class NotesStore {
sharedOnly: false
}
this.loading = true
this.setLoading(true)
try {
const notes = await notesService.fetchNotes(filter)
this.notes = notes;
this.setNotes(notes);
return notes;
} catch (e) {
console.error(e)
} finally {
this.loading = false
this.setLoading(false)
}
}
async fetchSessionNotes(sessionId: string) {
this.loading = true
this.setLoading(true)
try {
const notes = await notesService.getNotesBySessionId(sessionId)
notes.forEach(note => note.time = note.timestamp)
@ -50,47 +56,43 @@ export default class NotesStore {
} catch (e) {
console.error(e)
} finally {
this.loading = false
this.setLoading(false)
}
}
setNotes(notes: Note[]) {
this.sessionNotes = notes
}
async addNote(sessionId: string, note: WriteNote) {
this.loading = true
this.setLoading(true)
try {
const addedNote = await notesService.addNote(sessionId, note)
return addedNote
} catch (e) {
console.error(e)
} finally {
this.loading = false
this.setLoading(false)
}
}
async deleteNote(noteId: number) {
this.loading = true
this.setLoading(true)
try {
const deleted = await notesService.deleteNote(noteId)
return deleted
} catch (e) {
console.error(e)
} finally {
this.loading = false
this.setLoading(false)
}
}
async updateNote(noteId: string, note: WriteNote) {
this.loading = true
this.setLoading(true)
try {
const updated = await notesService.updateNote(noteId, note)
return updated
} catch (e) {
console.error(e)
} finally {
this.loading = false
this.setLoading(false)
}
}