change(ui): add editing etc

This commit is contained in:
sylenien 2022-11-17 17:54:36 +01:00 committed by Delirium
parent 4a20b287a7
commit 7bd5d13e61
4 changed files with 218 additions and 156 deletions

View file

@ -13,8 +13,8 @@ function RecordingsList() {
const recordsSearch = recordingsStore.search;
React.useEffect(() => {
recordingsStore.fetchRecordings()
}, [])
recordingsStore.fetchRecordings();
}, []);
React.useEffect(() => {
setRecordings(filterList(recordings, recordsSearch, ['createdBy', 'name']));

View file

@ -1,71 +1,113 @@
import React from 'react';
import { Icon, ItemMenu } from 'UI';
import { durationFromMs, checkForRecent, getDateFromMill } from 'App/date'
import { IRecord } from 'App/services/RecordingsService'
import { useStore } from 'App/mstore'
import { toast } from 'react-toastify'
import { durationFromMs, checkForRecent, getDateFromMill } from 'App/date';
import { IRecord } from 'App/services/RecordingsService';
import { useStore } from 'App/mstore';
import { toast } from 'react-toastify';
import cn from 'classnames';
interface Props {
record: IRecord;
record: IRecord;
}
function RecordsListItem(props: Props) {
const { record } = props;
const { recordingsStore } = useStore()
const { record } = props;
const { recordingsStore } = useStore();
const [isEdit, setEdit] = React.useState(false);
const [recordingTitle, setRecordingTitle] = React.useState(record.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const onRecordClick = () => {
recordingsStore.fetchRecordingUrl(record.recordId).then(url => {
window.open(url, "_blank");
})
const onRecordClick = () => {
recordingsStore.fetchRecordingUrl(record.recordId).then((url) => {
window.open(url, '_blank');
});
};
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.style.width = `${record.name.length}ch`;
}
}, [isEdit, inputRef.current]);
const onDelete = () => {
recordingsStore.deleteRecording(record.recordId).then(() => {
toast.success("Recording deleted")
})
}
const onDelete = () => {
recordingsStore.deleteRecording(record.recordId).then(() => {
recordingsStore.setRecordings(recordingsStore.recordings.filter(rec => rec.recordId !== record.recordId))
toast.success('Recording deleted');
});
};
const menuItems = [
{ icon: 'trash', text: 'Delete', onClick: onDelete },
]
return (
<div className="hover:bg-active-blue border-t px-6">
<div className="grid grid-cols-12 py-4 select-none items-center">
<div className="col-span-8 flex items-start">
<div className="flex items-center capitalize-first">
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
<Icon name="columns-gap" size="16" color="tealx" />
</div>
<div className="flex flex-col">
<div className="border-b border-dashed">{record.name}</div>
<div className="text-gray-medium text-sm">
{durationFromMs(record.duration)}
</div>
</div>
</div>
</div>
<div className="col-span-2">
<div className="flex flex-col">
<div>
{record.createdBy}
</div>
<div className="text-gray-medium text-sm">{checkForRecent(getDateFromMill(record.createdAt), 'LLL dd, yyyy, hh:mm a')}</div>
</div>
</div>
<div className="col-span-2 w-full justify-end flex items-center gap-2">
<div className="group flex items-center gap-1 cursor-pointer link" onClick={onRecordClick}>
<Icon name="play" size={18} color="teal" className="!block group-hover:!hidden"/>
<Icon name="play-fill-new" size={18} color="teal" className="!hidden group-hover:!block" />
<div>Play Video</div>
</div>
<ItemMenu
bold
items={menuItems}
/>
</div>
const menuItems = [{ icon: 'trash', text: 'Delete', onClick: onDelete }];
const onSave = () => {
recordingsStore.updateRecordingName(record.recordId, recordingTitle);
setEdit(false);
};
return (
<div className="hover:bg-active-blue border-t px-6">
<div className="grid grid-cols-12 py-4 select-none items-center">
<div className="col-span-8 flex items-start">
<div className="flex items-center capitalize-first">
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
<Icon name="camera-video" size="16" color="tealx" />
</div>
<div className="flex flex-col">
{isEdit ? (
<input
ref={inputRef}
name="recordName"
placeholder="Recording name"
autoFocus
style={{ minWidth: 200 }}
className="rounded fluid border-0 -mx-2 px-2 -mt-1"
value={recordingTitle}
onChange={(e) => setRecordingTitle(e.target.value)}
onBlur={onSave}
onFocus={() => setEdit(true)}
/>
) : (
<div
onDoubleClick={() => setEdit(true)}
className={cn(
'border-dotted border-gray-medium',
'pt-1 w-fit -mt-2',
'cursor-pointer select-none border-b'
)}
>
{recordingTitle}
</div>
)}
<div className="text-gray-medium text-sm">{durationFromMs(record.duration)}</div>
</div>
</div>
</div>
);
<div className="col-span-2">
<div className="flex flex-col">
<div>{record.createdBy}</div>
<div className="text-gray-medium text-sm">
{checkForRecent(getDateFromMill(record.createdAt), 'LLL dd, yyyy, hh:mm a')}
</div>
</div>
</div>
<div className="col-span-2 w-full justify-end flex items-center gap-2">
<div
className="group flex items-center gap-1 cursor-pointer link"
onClick={onRecordClick}
>
<Icon name="play" size={18} color="teal" className="!block group-hover:!hidden" />
<Icon
name="play-fill-new"
size={18}
color="teal"
className="!hidden group-hover:!block"
/>
<div>Play Video</div>
</div>
<ItemMenu bold items={menuItems} />
</div>
</div>
</div>
);
}
export default RecordsListItem;

View file

@ -1,27 +1,31 @@
import { makeAutoObservable } from "mobx"
import { recordingsService } from "App/services"
import { IRecord } from 'App/services/RecordingsService'
import { makeAutoObservable } from 'mobx';
import { recordingsService } from 'App/services';
import { IRecord } from 'App/services/RecordingsService';
export default class RecordingsStore {
recordings: IRecord[] = []
loading: boolean
recordings: IRecord[] = [];
loading: boolean;
page = 1
pageSize = 15
order: 'desc' | 'asc' = 'desc'
search = ''
page = 1;
pageSize = 15;
order: 'desc' | 'asc' = 'desc';
search = '';
// not later we will add search by user id
userId: number
userId: number;
constructor() {
makeAutoObservable(this)
makeAutoObservable(this);
}
setRecordings(records: IRecord[]) {
this.recordings = records;
}
updateSearch(val: string) {
this.search = val
this.search = val;
}
updatePage(page: number) {
this.page = page
this.page = page;
}
async fetchRecordings() {
@ -30,45 +34,53 @@ export default class RecordingsStore {
limit: this.pageSize,
order: this.order,
search: this.search,
}
};
this.loading = true
this.loading = true;
try {
const recordings = await recordingsService.fetchRecordings(filter)
this.recordings = recordings;
this.fetchRecordingUrl(recordings[0].recordId)
const recordings = await recordingsService.fetchRecordings(filter);
this.setRecordings(recordings);
return recordings;
} catch (e) {
console.error(e)
console.error(e);
} finally {
this.loading = false
this.loading = false;
}
}
async fetchRecordingUrl(id: number): Promise<string> {
this.loading = true
this.loading = true;
try {
const recording = await recordingsService.fetchRecording(id)
const recording = await recordingsService.fetchRecording(id);
return recording.URL;
} catch (e) {
console.error(e)
console.error(e);
} finally {
this.loading = false
this.loading = false;
}
}
async deleteRecording(id: number) {
this.loading = true
this.loading = true;
try {
const recording = await recordingsService.deleteRecording(id)
console.log(recording)
return recording
const recording = await recordingsService.deleteRecording(id);
return recording;
} catch (e) {
console.error(e)
console.error(e);
} finally {
this.loading = false
this.loading = false;
}
}
async updateRecordingName(id: number, name: string) {
this.loading = true;
try {
const recording = await recordingsService.updateRecordingName(id, name);
return recording;
} catch (e) {
console.error(e);
} finally {
this.loading = false;
}
}
}

View file

@ -6,87 +6,95 @@ interface RecordingData {
}
interface FetchFilter {
page: number
limit: number
order: 'asc' | 'desc'
search: string
page: number;
limit: number;
order: 'asc' | 'desc';
search: string;
}
export interface IRecord {
createdAt: number
createdBy: string
duration: number
name: string
recordId: number
sessionId: number
userId: number
URL?: string
createdAt: number;
createdBy: string;
duration: number;
name: string;
recordId: number;
sessionId: number;
userId: number;
URL?: string;
}
export default class RecordingsService {
private client: APIClient;
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
initClient(client?: APIClient) {
this.client = client || new APIClient();
}
initClient(client?: APIClient) {
this.client = client || new APIClient();
}
reserveUrl(siteId: string, recordingData: RecordingData): Promise<string> {
return this.client.put(`/${siteId}/assist/save`, recordingData)
.then(r => {
if (r.ok) {
return r.json().then(j => j.data.URL)
} else {
throw new Error("Can't reserve space for recording: " + r.status);
}
})
}
reserveUrl(siteId: string, recordingData: RecordingData): Promise<string> {
return this.client.put(`/${siteId}/assist/save`, recordingData).then((r) => {
if (r.ok) {
return r.json().then((j) => j.data.URL);
} else {
throw new Error("Can't reserve space for recording: " + r.status);
}
});
}
saveFile(url: string, file: Blob) {
return fetch(url, { method: 'PUT', headers: { 'Content-Type': 'video/webm' }, body: file })
.then(r => {
if (r.ok) {
return true
} else {
throw new Error("Can't upload file: " + r.status)
}
})
}
saveFile(url: string, file: Blob) {
return fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'video/webm' },
body: file,
}).then((r) => {
if (r.ok) {
return true;
} else {
throw new Error("Can't upload file: " + r.status);
}
});
}
fetchRecordings(filters: FetchFilter): Promise<IRecord[]> {
return this.client.post(`/assist/records`, filters)
.then(r => {
if (r.ok) {
return r.json().then(j => j.data)
} else {
throw new Error("Can't get recordings: " + r.status);
}
})
}
fetchRecordings(filters: FetchFilter): Promise<IRecord[]> {
return this.client.post(`/assist/records`, filters).then((r) => {
if (r.ok) {
return r.json().then((j) => j.data);
} else {
throw new Error("Can't get recordings: " + r.status);
}
});
}
fetchRecording(id: number): Promise<IRecord> {
return this.client.get(`/assist/records/${id}`)
.then(r => {
if (r.ok) {
return r.json().then(j => j.data)
} else {
throw new Error("Can't get recordings: " + r.status);
}
})
}
fetchRecording(id: number): Promise<IRecord> {
return this.client.get(`/assist/records/${id}`).then((r) => {
if (r.ok) {
return r.json().then((j) => j.data);
} else {
throw new Error("Can't get recordings: " + r.status);
}
});
}
deleteRecording(id: number): Promise<any> {
return this.client.delete(`/assist/records/${id}`)
.then(r => {
if (r.ok) {
return r.json().then(j => j.data)
} else {
throw new Error("Can't get recordings: " + r.status);
}
})
}
updateRecordingName(id: number, name: string): Promise<IRecord> {
return this.client.post(`/assist/records/${id}`, { name }).then((r) => {
if (r.ok) {
return r.json().then((j) => j.data);
} else {
throw new Error("Can't get recordings: " + r.status);
}
});
}
deleteRecording(id: number): Promise<any> {
return this.client.delete(`/assist/records/${id}`).then((r) => {
if (r.ok) {
return r.json().then((j) => j.data);
} else {
throw new Error("Can't get recordings: " + r.status);
}
});
}
}