change(ui): add editing etc
This commit is contained in:
parent
4a20b287a7
commit
7bd5d13e61
4 changed files with 218 additions and 156 deletions
|
|
@ -13,8 +13,8 @@ function RecordingsList() {
|
|||
const recordsSearch = recordingsStore.search;
|
||||
|
||||
React.useEffect(() => {
|
||||
recordingsStore.fetchRecordings()
|
||||
}, [])
|
||||
recordingsStore.fetchRecordings();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setRecordings(filterList(recordings, recordsSearch, ['createdBy', 'name']));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue