change(ui): custom fields
This commit is contained in:
parent
4f2d61d1cf
commit
d4d836ad24
10 changed files with 548 additions and 135 deletions
|
|
@ -55,7 +55,7 @@ const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, o
|
|||
const mapStateToProps = (state) => ({
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors'])
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { edit, save })(CustomFieldForm);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { Form, Input, confirm } from 'UI';
|
||||
import styles from './customFieldForm.module.css';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'Components/Modal';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Button } from 'antd';
|
||||
import { Trash } from 'UI/Icons';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface CustomFieldFormProps {
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
|
||||
console.log('siteId', siteId);
|
||||
const focusElementRef = useRef<HTMLInputElement>(null);
|
||||
const { customFieldStore: store } = useStore();
|
||||
const field = store.instance;
|
||||
const { hideModal } = useModal();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const write = ({ target: { value, name } }: any) => store.edit({ [name]: value });
|
||||
const exists = field?.exists();
|
||||
|
||||
const onDelete = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`
|
||||
})
|
||||
) {
|
||||
store.remove(siteId, field?.index!).then(() => {
|
||||
hideModal();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = (field: any) => {
|
||||
setLoading(true);
|
||||
store.save(siteId, field).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
hideModal();
|
||||
toast.success('Metadata added successfully!');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<Input
|
||||
ref={focusElementRef}
|
||||
name="key"
|
||||
value={field?.key}
|
||||
onChange={write}
|
||||
placeholder="Field Name"
|
||||
maxLength={50}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={() => onSave(field)}
|
||||
disabled={!field?.validate()}
|
||||
loading={loading}
|
||||
type="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{exists ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
<Button data-hidden={!exists} onClick={hideModal}>
|
||||
{'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button type="text" icon={<Trash />} data-hidden={!exists} onClick={onDelete}></Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(CustomFieldForm);
|
||||
|
|
@ -14,124 +14,124 @@ import { useModal } from 'App/components/Modal';
|
|||
import { toast } from 'react-toastify';
|
||||
|
||||
function CustomFields(props) {
|
||||
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
|
||||
const [deletingItem, setDeletingItem] = React.useState(null);
|
||||
const { showModal, hideModal } = useModal();
|
||||
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
|
||||
const [deletingItem, setDeletingItem] = React.useState(null);
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
const activeSite = props.sites.get(0);
|
||||
if (!activeSite) return;
|
||||
useEffect(() => {
|
||||
const activeSite = props.sites.get(0);
|
||||
if (!activeSite) return;
|
||||
|
||||
props.fetchList(activeSite.id);
|
||||
}, []);
|
||||
props.fetchList(activeSite.id);
|
||||
}, []);
|
||||
|
||||
const save = (field) => {
|
||||
props.save(currentSite.id, field).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
hideModal();
|
||||
toast.success('Metadata added successfully!');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
const save = (field) => {
|
||||
props.save(currentSite.id, field).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
hideModal();
|
||||
toast.success('Metadata added successfully!');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const init = (field) => {
|
||||
props.init(field);
|
||||
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => removeMetadata(field)} />);
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }) => {
|
||||
const site = props.sites.find((s) => s.id === value.value);
|
||||
setCurrentSite(site);
|
||||
props.fetchList(site.id);
|
||||
};
|
||||
|
||||
const removeMetadata = async (field) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`
|
||||
})
|
||||
) {
|
||||
setDeletingItem(field.index);
|
||||
props
|
||||
.remove(currentSite.id, field.index)
|
||||
.then(() => {
|
||||
hideModal();
|
||||
})
|
||||
.finally(() => {
|
||||
setDeletingItem(null);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const init = (field) => {
|
||||
props.init(field);
|
||||
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => removeMetadata(field)} />);
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }) => {
|
||||
const site = props.sites.find((s) => s.id === value.value);
|
||||
setCurrentSite(site);
|
||||
props.fetchList(site.id);
|
||||
};
|
||||
|
||||
const removeMetadata = async (field) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`,
|
||||
})
|
||||
) {
|
||||
setDeletingItem(field.index);
|
||||
props
|
||||
.remove(currentSite.id, field.index)
|
||||
.then(() => {
|
||||
hideModal();
|
||||
})
|
||||
.finally(() => {
|
||||
setDeletingItem(null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { fields, loading } = props;
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-5 ">
|
||||
<div className={cn(styles.tabHeader)}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
|
||||
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex px-5 items-center my-3">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
See additonal user information in sessions.
|
||||
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
|
||||
{/* <div className="mt-4" /> */}
|
||||
<div className="text-center my-4">None added yet</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={fields.size === 0}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{fields
|
||||
.filter((i) => i.index)
|
||||
.map((field) => (
|
||||
<>
|
||||
<ListItem
|
||||
disabled={deletingItem && deletingItem === field.index}
|
||||
key={field._key}
|
||||
field={field}
|
||||
onEdit={init}
|
||||
// onDelete={ () => removeMetadata(field) }
|
||||
/>
|
||||
<Divider className="m-0" />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
const { fields, loading } = props;
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-5 ">
|
||||
<div className={cn(styles.tabHeader)}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
|
||||
</div>
|
||||
);
|
||||
<div className="ml-auto">
|
||||
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
|
||||
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex px-5 items-center my-3">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
See additonal user information in sessions.
|
||||
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
|
||||
{/* <div className="mt-4" /> */}
|
||||
<div className="text-center my-4">None added yet</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={fields.size === 0}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{fields
|
||||
.filter((i) => i.index)
|
||||
.map((field) => (
|
||||
<>
|
||||
<ListItem
|
||||
disabled={deletingItem && deletingItem === field.index}
|
||||
key={field._key}
|
||||
field={field}
|
||||
onEdit={init}
|
||||
// onDelete={ () => removeMetadata(field) }
|
||||
/>
|
||||
<Divider className="m-0" />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
save,
|
||||
remove,
|
||||
}
|
||||
(state) => ({
|
||||
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors'])
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
save,
|
||||
remove
|
||||
}
|
||||
)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields));
|
||||
|
|
|
|||
114
frontend/app/components/Client/CustomFields/CustomFields.tsx
Normal file
114
frontend/app/components/Client/CustomFields/CustomFields.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import styles from './customFields.module.css';
|
||||
import CustomFieldForm from './CustomFieldForm';
|
||||
import ListItem from './ListItem';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface CustomFieldsProps {
|
||||
sites: any;
|
||||
}
|
||||
|
||||
const CustomFields: React.FC<CustomFieldsProps> = (props) => {
|
||||
const [currentSite, setCurrentSite] = useState(props.sites.get(0));
|
||||
const [deletingItem, setDeletingItem] = useState<number | null>(null);
|
||||
const { showModal, hideModal } = useModal();
|
||||
const { customFieldStore: store } = useStore();
|
||||
const fields = store.list;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const activeSite = props.sites.get(0);
|
||||
if (!activeSite) return;
|
||||
|
||||
setCurrentSite(activeSite);
|
||||
|
||||
setLoading(true);
|
||||
store.fetchList(activeSite.id).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [props.sites]);
|
||||
|
||||
const handleInit = (field?: any) => {
|
||||
console.log('field', field);
|
||||
store.init(field);
|
||||
showModal(<CustomFieldForm siteId={currentSite.id} />, {
|
||||
title: field ? 'Edit Metadata' : 'Add Metadata', right: true
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }: { value: { value: number } }) => {
|
||||
const site = props.sites.find((s: any) => s.id === value.value);
|
||||
setCurrentSite(site);
|
||||
|
||||
setLoading(true);
|
||||
store.fetchList(site.id).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-5">
|
||||
<div className={cn(styles.tabHeader)}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.length < 10}>
|
||||
<Button disabled={fields.length >= 10} variant="primary" onClick={() => handleInit()}>
|
||||
Add Metadata
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex px-5 items-center my-3">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
See additional user information in sessions.
|
||||
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
|
||||
<div className="text-center my-4">None added yet</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={fields.length === 0}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{fields
|
||||
.filter((i: any) => i.index)
|
||||
.map((field: any) => (
|
||||
<>
|
||||
<ListItem
|
||||
disabled={deletingItem !== null && deletingItem === field.index}
|
||||
key={field._key}
|
||||
field={field}
|
||||
onEdit={handleInit}
|
||||
/>
|
||||
<Divider className="m-0" />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: any) => ({
|
||||
sites: state.getIn(['site', 'list'])
|
||||
}))(withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields)));
|
||||
|
|
@ -4,23 +4,23 @@ import { Button } from 'UI';
|
|||
import styles from './listItem.module.css';
|
||||
|
||||
const ListItem = ({ field, onEdit, disabled }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group hover:bg-active-blue flex items-center justify-between py-3 px-5 cursor-pointer',
|
||||
field.index === 0 ? styles.preDefined : '',
|
||||
{
|
||||
[styles.disabled]: disabled,
|
||||
}
|
||||
)}
|
||||
onClick={() => field.index != 0 && onEdit(field)}
|
||||
>
|
||||
<span>{field.key}</span>
|
||||
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
|
||||
<Button variant="text-primary" icon="pencil" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group hover:bg-active-blue flex items-center justify-between py-3 px-5 cursor-pointer',
|
||||
field.index === 0 ? styles.preDefined : '',
|
||||
{
|
||||
[styles.disabled]: disabled
|
||||
}
|
||||
)}
|
||||
onClick={() => field.index !== 0 && onEdit(field)}
|
||||
>
|
||||
<span>{field.key}</span>
|
||||
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
|
||||
<Button variant="text-primary" icon="pencil" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
|
|
|
|||
117
frontend/app/mstore/customFieldStore.ts
Normal file
117
frontend/app/mstore/customFieldStore.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { makeAutoObservable } from 'mobx';
|
||||
import { customFieldService } from 'App/services';
|
||||
|
||||
import {
|
||||
addElementToConditionalFiltersMap,
|
||||
addElementToMobileConditionalFiltersMap,
|
||||
addElementToFiltersMap,
|
||||
addElementToFlagConditionsMap,
|
||||
addElementToLiveFiltersMap,
|
||||
clearMetaFilters
|
||||
} from 'Types/filter/newFilter';
|
||||
import { FilterCategory } from 'Types/filter/filterType';
|
||||
import CustomField from 'App/mstore/types/customField';
|
||||
import customFields from 'Components/Client/CustomFields';
|
||||
|
||||
class CustomFieldStore {
|
||||
isLoading: boolean = false;
|
||||
isSaving: boolean = false;
|
||||
list: CustomField[] = [];
|
||||
instance: CustomField = new CustomField();
|
||||
sources: CustomField[] = [];
|
||||
fetchedMetadata: boolean = false;
|
||||
search: string = '';
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
edit = (field: Partial<CustomField>) => {
|
||||
Object.assign(this.instance!, field);
|
||||
};
|
||||
|
||||
async fetchList(siteId?: string): Promise<any> {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await customFieldService.get(siteId);
|
||||
this.list = response.map((item: any) => new CustomField(item));
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchListActive(siteId?: string): Promise<any> {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await customFieldService.get(siteId);
|
||||
clearMetaFilters();
|
||||
response.forEach((item: any) => {
|
||||
addElementToFiltersMap(FilterCategory.METADATA, '_' + item.key);
|
||||
addElementToLiveFiltersMap(FilterCategory.METADATA, '_' + item.key);
|
||||
addElementToFlagConditionsMap(FilterCategory.METADATA, '_' + item.key);
|
||||
addElementToConditionalFiltersMap(FilterCategory.METADATA, '_' + item.key);
|
||||
addElementToMobileConditionalFiltersMap(FilterCategory.METADATA, '_' + item.key);
|
||||
});
|
||||
this.list = response.map((item_1: any) => new CustomField(item_1));
|
||||
this.fetchedMetadata = true;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSources(): Promise<any> {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await customFieldService.get('/integration/sources');
|
||||
this.sources = response.map(({ value, ...item }: any) => new CustomField({
|
||||
label: value,
|
||||
key: value,
|
||||
...item
|
||||
}));
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async save(siteId: string, instance: CustomField): Promise<any> {
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const wasCreating = !instance.exists();
|
||||
const response = instance.exists() ? await customFieldService.create(siteId, instance.toData()) :
|
||||
await customFieldService.update(siteId, instance.toData());
|
||||
const updatedInstance = new CustomField(response);
|
||||
|
||||
if (wasCreating) {
|
||||
this.list.push(updatedInstance);
|
||||
} else {
|
||||
const index = this.list.findIndex(item => item.index === instance.index);
|
||||
if (index >= 0)
|
||||
this.list[index] = updatedInstance;
|
||||
}
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(siteId: string, index: string): Promise<any> {
|
||||
this.isSaving = true;
|
||||
try {
|
||||
await customFieldService.delete(siteId, index);
|
||||
this.list = this.list.filter(item => item.index !== index);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
init(instance?: any) {
|
||||
// this.instance = new CustomField(instance);
|
||||
if (instance) {
|
||||
this.instance = new CustomField().fromJson(instance);
|
||||
} else {
|
||||
this.instance = new CustomField();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default CustomFieldStore;
|
||||
|
|
@ -18,14 +18,15 @@ import WeeklyReportStore from './weeklyReportConfigStore';
|
|||
import AlertStore from './alertsStore';
|
||||
import FeatureFlagsStore from './featureFlagsStore';
|
||||
import UxtestingStore from './uxtestingStore';
|
||||
import TagWatchStore from './tagWatchStore';
|
||||
import AiSummaryStore from "./aiSummaryStore";
|
||||
import AiFiltersStore from "./aiFiltersStore";
|
||||
import SpotStore from "./spotStore";
|
||||
import LoginStore from "./loginStore";
|
||||
import FilterStore from "./filterStore";
|
||||
import TagWatchStore from './tagWatchStore';
|
||||
import AiSummaryStore from './aiSummaryStore';
|
||||
import AiFiltersStore from './aiFiltersStore';
|
||||
import SpotStore from './spotStore';
|
||||
import LoginStore from './loginStore';
|
||||
import FilterStore from './filterStore';
|
||||
import UiPlayerStore from './uiPlayerStore';
|
||||
import IssueReportingStore from './issueReportingStore';
|
||||
import CustomFieldStore from './customFieldStore';
|
||||
|
||||
export class RootStore {
|
||||
dashboardStore: DashboardStore;
|
||||
|
|
@ -53,6 +54,7 @@ export class RootStore {
|
|||
filterStore: FilterStore;
|
||||
uiPlayerStore: UiPlayerStore;
|
||||
issueReportingStore: IssueReportingStore;
|
||||
customFieldStore: CustomFieldStore;
|
||||
|
||||
constructor() {
|
||||
this.dashboardStore = new DashboardStore();
|
||||
|
|
@ -80,6 +82,7 @@ export class RootStore {
|
|||
this.filterStore = new FilterStore();
|
||||
this.uiPlayerStore = new UiPlayerStore();
|
||||
this.issueReportingStore = new IssueReportingStore();
|
||||
this.customFieldStore = new CustomFieldStore();
|
||||
}
|
||||
|
||||
initClient() {
|
||||
|
|
|
|||
55
frontend/app/mstore/types/customField.ts
Normal file
55
frontend/app/mstore/types/customField.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
|
||||
const varRegExp = new RegExp('^[A-Za-z_-][A-Za-z0-9_-]*$');
|
||||
|
||||
export const BOOLEAN = 'boolean';
|
||||
export const STRING = 'string';
|
||||
export const NUMBER = 'number';
|
||||
export const MAX_COUNT = 20;
|
||||
|
||||
interface CustomRecord {
|
||||
index?: string;
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
validate: () => boolean;
|
||||
toData: () => any;
|
||||
}
|
||||
|
||||
class CustomField implements CustomRecord {
|
||||
index: string = '';
|
||||
key: string = '';
|
||||
label: string = '';
|
||||
type: string = STRING;
|
||||
|
||||
constructor(props: Partial<CustomRecord> = {}) {
|
||||
Object.assign(this, props);
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
fromJson(json: any) {
|
||||
runInAction(() => {
|
||||
Object.assign(this, json);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
exists(): boolean {
|
||||
return Boolean(this.index);
|
||||
}
|
||||
|
||||
validate(): boolean {
|
||||
return varRegExp.test(this.key) && this.type !== '';
|
||||
}
|
||||
|
||||
toData(): any {
|
||||
return {
|
||||
index: this.index,
|
||||
key: this.key,
|
||||
label: this.label,
|
||||
type: this.type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomField;
|
||||
29
frontend/app/services/CustomFieldService.ts
Normal file
29
frontend/app/services/CustomFieldService.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import BaseService from './BaseService';
|
||||
|
||||
export default class CustomFieldService extends BaseService {
|
||||
async fetchList(siteId: string): Promise<any> {
|
||||
return this.client.get(siteId ? `/${siteId}/metadata` : '/metadata')
|
||||
.then(r => r.json()).then(j => j.data);
|
||||
}
|
||||
|
||||
async get(siteId?: string): Promise<any> {
|
||||
const url = siteId ? `/${siteId}/metadata` : '/metadata';
|
||||
return this.client.get(url)
|
||||
.then(r => r.json()).then(j => j.data);
|
||||
}
|
||||
|
||||
async create(siteId: string, customField: any): Promise<any> {
|
||||
return this.client.post(`/${siteId}/metadata`, customField)
|
||||
.then(r => r.json()).then(j => j.data);
|
||||
}
|
||||
|
||||
async update(siteId: string, instance: any): Promise<any> {
|
||||
return this.client.put(`/${siteId}/metadata/${instance.index}`, instance)
|
||||
.then(r => r.json()).then(j => j.data);
|
||||
}
|
||||
|
||||
async delete(siteId: string, index: string): Promise<any> {
|
||||
return this.client.delete(`/${siteId}/metadata/${index}`)
|
||||
.then(r => r.json()).then(j => j.data);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import SpotService from './spotService';
|
|||
import LoginService from "./loginService";
|
||||
import FilterService from "./FilterService";
|
||||
import IssueReportsService from "./IssueReportsService";
|
||||
import CustomFieldService from './CustomFieldService';
|
||||
import IntegrationsService from './IntegrationsService';
|
||||
|
||||
export const dashboardService = new DashboardService();
|
||||
|
|
@ -45,6 +46,7 @@ export const spotService = new SpotService();
|
|||
export const loginService = new LoginService();
|
||||
export const filterService = new FilterService();
|
||||
export const issueReportsService = new IssueReportsService();
|
||||
export const customFieldService = new CustomFieldService();
|
||||
export const integrationsService = new IntegrationsService();
|
||||
|
||||
export const services = [
|
||||
|
|
@ -70,5 +72,6 @@ export const services = [
|
|||
loginService,
|
||||
filterService,
|
||||
issueReportsService,
|
||||
customFieldService,
|
||||
integrationsService,
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue