diff --git a/frontend/app/components/Client/Client.tsx b/frontend/app/components/Client/Client.tsx index be2bfd3cf..06991988d 100644 --- a/frontend/app/components/Client/Client.tsx +++ b/frontend/app/components/Client/Client.tsx @@ -8,6 +8,7 @@ import Integrations from './Integrations'; import UserView from './Users/UsersView'; import AuditView from './Audit/AuditView'; import Sites from './Sites'; +import Projects from './Projects'; import CustomFields from './CustomFields'; import Webhooks from './Webhooks'; import Notifications from './Notifications'; @@ -31,7 +32,7 @@ export default class Client extends React.PureComponent { - + diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx index 2b95055fa..c732b038b 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx @@ -1,10 +1,10 @@ import React, { useRef, useState } from 'react'; -import { Form, Input, confirm } from 'UI'; +import { Form, Input } 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 { Button, Modal } from 'antd'; import { Trash } from 'UI/Icons'; import { observer } from 'mobx-react-lite'; @@ -23,16 +23,14 @@ const CustomFieldForm: React.FC = ({ siteId }) => { 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(() => { + Modal.confirm({ + title: 'Metadata', + content: `Are you sure you want to remove?`, + onOk: async () => { + await store.remove(siteId, field?.index!); hideModal(); - }); - } + } + }); }; const onSave = (field: any) => { @@ -48,7 +46,7 @@ const CustomFieldForm: React.FC = ({ siteId }) => { toast.error('An error occurred while saving metadata.'); }).finally(() => { setLoading(false); - }) + }); }; return ( diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js deleted file mode 100644 index 1b800095a..000000000 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ /dev/null @@ -1,137 +0,0 @@ -import React, { useEffect } 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 { init, fetchList, save, remove } from 'Duck/customField'; -import SiteDropdown from 'Shared/SiteDropdown'; -import styles from './customFields.module.css'; -import CustomFieldForm from './CustomFieldForm'; -import ListItem from './ListItem'; -import { confirm } from 'UI'; -import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; -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(); - - useEffect(() => { - const activeSite = props.sites.get(0); - if (!activeSite) return; - - 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 init = (field) => { - props.init(field); - showModal( 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 ( -
-
-

{'Metadata'}

-
- -
-
- - - -
-
-
- - See additonal user information in sessions. - Learn more -
- - - - - {/*
*/} -
None added yet
-
- } - size="small" - show={fields.size === 0} - > -
- {fields - .filter((i) => i.index) - .map((field) => ( - <> - removeMetadata(field) } - /> - - - ))} -
-
-
-
- ); -} - -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 - } -)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields)); diff --git a/frontend/app/components/Client/CustomFields/CustomFields.tsx b/frontend/app/components/Client/CustomFields/CustomFields.tsx index 408c78961..7effb17cc 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.tsx +++ b/frontend/app/components/Client/CustomFields/CustomFields.tsx @@ -1,107 +1,96 @@ import React, { useEffect, useState } from 'react'; -import cn from 'classnames'; -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 { NoContent } from 'UI'; 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'; +import { List, Space, Typography, Button, Tooltip } from 'antd'; +import { PencilIcon, PlusIcon, Tags } from 'lucide-react'; +import usePageTitle from '@/hooks/usePageTitle'; const CustomFields = () => { + usePageTitle('Metadata - OpenReplay Preferences'); const { customFieldStore: store, projectsStore } = useStore(); - const sites = projectsStore.list; - const [currentSite, setCurrentSite] = useState(sites[0]); - const [deletingItem, setDeletingItem] = useState(null); + const currentSite = projectsStore.config.project; const { showModal, hideModal } = useModal(); const fields = store.list; const [loading, setLoading] = useState(false); useEffect(() => { - const activeSite = sites[0]; - if (!activeSite) return; - - setCurrentSite(activeSite); - setLoading(true); - store.fetchList(activeSite.id).finally(() => { + store.fetchList(currentSite?.id).finally(() => { setLoading(false); }); - }, [sites]); + }, [currentSite]); const handleInit = (field?: any) => { store.init(field); - showModal(, { + showModal(, { title: field ? 'Edit Metadata' : 'Add Metadata', right: true }); }; - const onChangeSelect = ({ value }: { value: { value: number } }) => { - const site = sites.find((s: any) => s.id === value.value); - setCurrentSite(site); - - setLoading(true); - store.fetchList(site.id).finally(() => { - setLoading(false); - }); - }; + const remaining = 10 - fields.length; return ( -
-
-

{'Metadata'}

-
- -
-
- - - -
-
-
- - See additional user information in sessions. +
+ + Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user + sessions. Learn More. Learn more -
+ - - - -
None added yet
-
- } - size="small" - show={fields.length === 0} + + 0 ? '' : 'You\'ve reached the limit of 10 metadata.'} > -
- {fields - .filter((i: any) => i.index) - .map((field: any) => ( - <> - - - - ))} + + + {/*{remaining === 0 && }*/} + + {remaining === 0 ? 'You have reached the limit of 10 metadata.' : `${remaining}/10 Remaining for this project`} + + + + + + +
None added yet
- - + } + size="small" + show={fields.length === 0} + > + ( + handleInit(field)} + className="cursor-pointer group hover:bg-active-blue !px-4" + actions={[ +
); }; -export default withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields)); +export default observer(CustomFields); diff --git a/frontend/app/components/Client/Projects/ProjectList.tsx b/frontend/app/components/Client/Projects/ProjectList.tsx new file mode 100644 index 000000000..fa473ee3e --- /dev/null +++ b/frontend/app/components/Client/Projects/ProjectList.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Avatar, Input, List, Typography } from 'antd'; +import { useStore } from '@/mstore'; +import Project from '@/mstore/types/project'; +import { observer } from 'mobx-react-lite'; +import { AppWindowMac, Smartphone } from 'lucide-react'; + +function ProjectList() { + const { projectsStore } = useStore(); + const list = projectsStore.list; + const [search, setSearch] = React.useState(''); + const config = projectsStore.config; + console.log('config', config.pid); + + const onSearch = (value: string) => { + setSearch(value); + }; + + const onProjectClick = (project: Project) => { + projectsStore.setConfigProject(project.projectId); + }; + + return ( +
+ +
+ item.name.toLowerCase().includes(search.toLowerCase()))} + renderItem={(item: Project) => ( + onProjectClick(item)} + className={`!py-2 mb-2 rounded-lg cursor-pointer !border-b-0 ${config.pid == item.projectId ? 'bg-teal-light' : 'bg-white'}`} + > + : + } + /> + } + title={{item.name}} + /> + + )} + /> +
+ ); +} + +export default observer(ProjectList); diff --git a/frontend/app/components/Client/Projects/ProjectTabContent.tsx b/frontend/app/components/Client/Projects/ProjectTabContent.tsx new file mode 100644 index 000000000..34588a879 --- /dev/null +++ b/frontend/app/components/Client/Projects/ProjectTabContent.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useStore } from '@/mstore'; +import { observer } from 'mobx-react-lite'; +import ProjectTabTracking from 'Components/Client/Projects/ProjectTabTracking'; +import CustomFields from 'Components/Client/CustomFields'; +import ProjectTags from 'Components/Client/Projects/ProjectTags'; + +function ProjectTabContent() { + const { projectsStore } = useStore(); + const { pid, tab } = projectsStore.config; + + const tabContent: Record = React.useMemo(() => { + const project = projectsStore.list.find((p) => p.projectId == pid); + return { + installation: , + captureRate:
Capture Rate Content
, + metadata: , + tags: , + groupKeys:
Group Keys Content
+ }; + }, [pid, projectsStore.list]); + + return ( +
+ {tabContent[tab]} +
+ ); +} + +export default observer(ProjectTabContent); diff --git a/frontend/app/components/Client/Projects/ProjectTabTracking.tsx b/frontend/app/components/Client/Projects/ProjectTabTracking.tsx new file mode 100644 index 000000000..c8aad161d --- /dev/null +++ b/frontend/app/components/Client/Projects/ProjectTabTracking.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import Project from '@/mstore/types/project'; +import InstallMobileDocs from 'Shared/TrackingCodeModal/InstallIosDocs'; +import { Tabs } from 'UI'; +import ProjectCodeSnippet from 'Shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet'; +import InstallDocs from 'Shared/TrackingCodeModal/InstallDocs'; +import usePageTitle from '@/hooks/usePageTitle'; + +const PROJECT = 'Using Script'; +const DOCUMENTATION = 'Using NPM'; +const TABS = [ + { key: DOCUMENTATION, text: DOCUMENTATION }, + { key: PROJECT, text: PROJECT } +]; + +interface Props { + project: Project; +} + +function ProjectTabTracking(props: Props) { + usePageTitle('Installation - OpenReplay Preferences'); + const { project } = props; + const [activeTab, setActiveTab] = React.useState(PROJECT); + const ingestPoint = `https://${window.location.hostname}/ingest`; + + const renderActiveTab = () => { + switch (activeTab) { + case PROJECT: + return ; + case DOCUMENTATION: + return ; + } + return null; + }; + + return ( +
+ {project.platform === 'ios' ? ( + + ) : ( +
+ setActiveTab(tab)} + /> +
{renderActiveTab()}
+
+ )} +
+ ); +} + +export default ProjectTabTracking; diff --git a/frontend/app/components/Client/Projects/ProjectTabs.tsx b/frontend/app/components/Client/Projects/ProjectTabs.tsx new file mode 100644 index 000000000..26599666d --- /dev/null +++ b/frontend/app/components/Client/Projects/ProjectTabs.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Tabs } from 'antd'; +import { useStore } from '@/mstore'; +import { observer } from 'mobx-react-lite'; + +function ProjectTabs() { + const { projectsStore } = useStore(); + const activeTab = projectsStore.config.tab; + + const tabItems = [ + { key: 'installation', label: 'Installation', content:
Installation Content
}, + { key: 'captureRate', label: 'Capture Rate', content:
Capture Rate Content
}, + { key: 'metadata', label: 'Metadata', content:
Metadata Content
}, + { key: 'tags', label: 'Tags', content:
Tags Content
}, + { key: 'groupKeys', label: 'Group Keys', content:
Group Keys Content
} + ]; + + const onTabChange = (key: string) => { + projectsStore.setConfigTab(key); + }; + + return ( + ({ + key: tab.key, + label: tab.label + // children: tab.content, + }))} + /> + ); +} + +export default observer(ProjectTabs); diff --git a/frontend/app/components/Client/Projects/ProjectTags.tsx b/frontend/app/components/Client/Projects/ProjectTags.tsx new file mode 100644 index 000000000..5197fd0c9 --- /dev/null +++ b/frontend/app/components/Client/Projects/ProjectTags.tsx @@ -0,0 +1,65 @@ +import React, { useEffect } from 'react'; +import { useStore } from '@/mstore'; +import { List, Button, Typography, Space } from 'antd'; +import { observer } from 'mobx-react-lite'; +import { PencilIcon, ScanSearch } from 'lucide-react'; +import { useModal } from 'Components/ModalContext'; +import TagForm from 'Components/Client/Projects/TagForm'; + +function ProjectTags() { + const { tagWatchStore, projectsStore } = useStore(); + const list = tagWatchStore.tags; + const { openModal } = useModal(); + const { pid } = projectsStore.config; + + useEffect(() => { + void tagWatchStore.getTags(pid); + }, [pid]); + + const handleInit = (tag?: any) => { + openModal(, { + title: tag ? 'Edit Tag' : 'Add Tag' + }); + }; + + return ( +
+ + + Manage Tag Elements here. Rename tags for easy identification or delete those you no longer need. + +
    +
  • To create new tags, navigate to the Tags tab while playing a session. +
  • +
  • Use tags in OmniSearch to quickly find relevant sessions. +
  • +
+
+ ( + } /> + ]} + onClick={() => handleInit(item)} + > + } + /> + + )} + pagination={{ + pageSize: 5, + showSizeChanger: false, + size: 'small' + }} + /> +
+ ); +} + +export default observer(ProjectTags); diff --git a/frontend/app/components/Client/Projects/Projects.tsx b/frontend/app/components/Client/Projects/Projects.tsx new file mode 100644 index 000000000..434f24028 --- /dev/null +++ b/frontend/app/components/Client/Projects/Projects.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Button, Card, Col, Divider, Row, Space, Typography } from 'antd'; +import ProjectList from 'Components/Client/Projects/ProjectList'; +import ProjectTabs from 'Components/Client/Projects/ProjectTabs'; +import { useHistory } from 'react-router-dom'; +import { useStore } from '@/mstore'; +import { observer } from 'mobx-react-lite'; +import { PlusIcon } from 'lucide-react'; +import ProjectTabContent from 'Components/Client/Projects/ProjectTabContent'; + +function Projects() { + const { projectsStore } = useStore(); + const history = useHistory(); + const { project, pid, tab } = projectsStore.config; + + React.useEffect(() => { + const params = new URLSearchParams(history.location.search); + const pid = params.get('pid'); + const tab = params.get('tab'); + projectsStore.setConfigProject(pid ? parseInt(pid) : undefined); + projectsStore.setConfigTab(tab); + }, []); + + React.useEffect(() => { + const params = new URLSearchParams(history.location.search); + if (projectsStore.config.pid) { + params.set('pid', projectsStore.config.pid + ''); + } + + if (projectsStore.config.tab) { + params.set('tab', projectsStore.config.tab); + } + history.push({ search: params.toString() }); + }, [pid, tab]); + + return ( + + + + } + > + + + + + + + {project?.name} + + + +
+ {project && } +
+ +
+
+ ); +} + +export default observer(Projects); diff --git a/frontend/app/components/Client/Projects/TagForm.tsx b/frontend/app/components/Client/Projects/TagForm.tsx new file mode 100644 index 000000000..f3bdcee83 --- /dev/null +++ b/frontend/app/components/Client/Projects/TagForm.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Button, Form, Input, Space, Modal } from 'antd'; +import { Trash } from 'UI/Icons'; +import { useStore } from '@/mstore'; +import { useModal } from 'Components/ModalContext'; + +interface Props { + tag: any; + projectId: number; +} + +function TagForm(props: Props) { + const { tag, projectId } = props; + const { tagWatchStore } = useStore(); + const [name, setName] = React.useState(tag.name); + const [loading, setLoading] = React.useState(false); + const { closeModal } = useModal(); + + const write = ({ target: { value, name } }: any) => { + setName(value); + }; + + const onDelete = async () => { + Modal.confirm({ + title: 'Tag', + content: `Are you sure you want to remove?`, + onOk: async () => { + await tagWatchStore.deleteTag(tag.tagId, projectId); + closeModal(); + } + }); + }; + + const onSave = () => { + void tagWatchStore.updateTagName(tag.tagId, name, projectId); + }; + + return ( +
+ + + + +
+ + + + + + +
+
+ ); +} + +export default TagForm; diff --git a/frontend/app/components/Client/Projects/index.ts b/frontend/app/components/Client/Projects/index.ts new file mode 100644 index 000000000..3b68dc8cf --- /dev/null +++ b/frontend/app/components/Client/Projects/index.ts @@ -0,0 +1 @@ +export { default } from './Projects'; diff --git a/frontend/app/components/shared/SessionSettings/components/CaptureRateSettings.tsx b/frontend/app/components/shared/SessionSettings/components/CaptureRateSettings.tsx new file mode 100644 index 000000000..993d8f053 --- /dev/null +++ b/frontend/app/components/shared/SessionSettings/components/CaptureRateSettings.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +function CaptureRateSettings() { + return ( +
+ ); +} + +export default CaptureRateSettings; diff --git a/frontend/app/mstore/projectsStore.ts b/frontend/app/mstore/projectsStore.ts index c30e1f862..527f4e693 100644 --- a/frontend/app/mstore/projectsStore.ts +++ b/frontend/app/mstore/projectsStore.ts @@ -2,7 +2,13 @@ import { makeAutoObservable, runInAction } from 'mobx'; import Project from './types/project'; import GDPR from './types/gdpr'; import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys'; -import { projectsService } from "App/services"; +import { projectsService } from 'App/services'; + +interface Config { + project: Project | null; + pid: number | undefined; + tab: string; +} export default class ProjectsStore { list: Project[] = []; @@ -11,6 +17,11 @@ export default class ProjectsStore { active: Project | null = null; sitesLoading = true; loading = false; + config: Config = { + project: null, + pid: undefined, + tab: 'installation' + }; constructor() { const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY); @@ -25,42 +36,42 @@ export default class ProjectsStore { getSiteId = () => { return { siteId: this.siteId, - active: this.active, + active: this.active }; - } + }; initProject = (project: Partial) => { this.instance = new Project(project); - } + }; setSitesLoading = (loading: boolean) => { this.sitesLoading = loading; - } + }; setLoading = (loading: boolean) => { this.loading = loading; - } + }; setSiteId = (siteId: string) => { localStorage.setItem(SITE_ID_STORAGE_KEY, siteId.toString()); this.siteId = siteId; this.active = this.list.find((site) => site.id! === siteId) ?? null; - } + }; editGDPR = (gdprData: Partial) => { if (this.instance) { this.instance.gdpr.edit(gdprData); } - } + }; editInstance = (instance: Partial) => { if (!this.instance) return; this.instance = this.instance.edit(instance); - } + }; fetchGDPR = async (siteId: string) => { try { - const response = await projectsService.fetchGDPR(siteId) + const response = await projectsService.fetchGDPR(siteId); runInAction(() => { if (this.instance) { Object.assign(this.instance.gdpr, response.data); @@ -69,7 +80,7 @@ export default class ProjectsStore { } catch (error) { console.error('Failed to fetch GDPR:', error); } - } + }; saveGDPR = async (siteId: string) => { if (!this.instance) return; @@ -80,7 +91,7 @@ export default class ProjectsStore { } catch (error) { console.error('Failed to save GDPR:', error); } - } + }; fetchList = async (siteIdFromPath?: string) => { this.setSitesLoading(true); @@ -115,7 +126,7 @@ export default class ProjectsStore { } finally { this.setSitesLoading(false); } - } + }; save = async (projectData: Partial) => { this.setLoading(true); @@ -137,7 +148,7 @@ export default class ProjectsStore { } finally { this.setLoading(false); } - } + }; updateProjectRecordingStatus = (siteId: string, status: boolean) => { const site = this.list.find(site => site.id === siteId); @@ -150,7 +161,7 @@ export default class ProjectsStore { localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS); } } - } + }; removeProject = async (projectId: string) => { this.setLoading(true); @@ -161,13 +172,13 @@ export default class ProjectsStore { if (this.siteId === projectId) { this.setSiteId(this.list[0].id!); } - }) + }); } catch (e) { console.error('Failed to remove project:', e); } finally { this.setLoading(false); } - } + }; updateProject = async (projectId: string, projectData: Partial) => { this.setLoading(true); @@ -183,7 +194,24 @@ export default class ProjectsStore { } catch (error) { console.error('Failed to update site:', error); } finally { - this.setLoading(false) + this.setLoading(false); } - } + }; + + setConfigProject = (pid?: number) => { + if (!pid) { + const firstProject = this.list[0]; + this.config.pid = firstProject?.projectId ?? undefined; + this.config.project = firstProject ?? null; + return; + } + + const project = this.list.find(site => site.projectId === pid); + this.config.pid = project?.projectId ?? undefined; + this.config.project = project ?? null; + }; + + setConfigTab = (tab: string | null) => { + this.config.tab = tab ?? 'installation'; + }; } diff --git a/frontend/app/mstore/tagWatchStore.ts b/frontend/app/mstore/tagWatchStore.ts index 433b27451..2cc39b81c 100644 --- a/frontend/app/mstore/tagWatchStore.ts +++ b/frontend/app/mstore/tagWatchStore.ts @@ -1,6 +1,7 @@ import { makeAutoObservable } from 'mobx'; import { tagWatchService } from 'App/services'; import { CreateTag, Tag } from 'App/services/TagWatchService'; +import { projectStore } from '@/mstore'; export default class TagWatchStore { tags: Tag[] = []; @@ -18,13 +19,14 @@ export default class TagWatchStore { this.isLoading = loading; }; - getTags = async () => { + getTags = async (projectId?: number) => { if (this.isLoading) { return; } this.setLoading(true); try { - const tags: Tag[] = await tagWatchService.getTags(); + const pid = projectId || projectStore.active?.projectId; + const tags: Tag[] = await tagWatchService.getTags(pid!); this.setTags(tags); return tags; } catch (e) { @@ -34,28 +36,31 @@ export default class TagWatchStore { } }; - createTag = async (data: CreateTag) => { + createTag = async (data: CreateTag, projectId?: number) => { try { - const tagId: number = await tagWatchService.createTag(data); + const pid = projectId || projectStore.active?.projectId; + const tagId: number = await tagWatchService.createTag(pid!, data); return tagId; } catch (e) { console.error(e); } }; - deleteTag = async (id: number) => { + deleteTag = async (id: number, projectId?: number) => { try { - await tagWatchService.deleteTag(id); + const pid = projectId || projectStore.active?.projectId; + await tagWatchService.deleteTag(pid!, id); this.setTags(this.tags.filter((t) => t.tagId !== id)); } catch (e) { console.error(e); } }; - updateTagName = async (id: number, name: string) => { + updateTagName = async (id: number, name: string, projectId?: number) => { try { - await tagWatchService.updateTagName(id, name); - const updatedTag = this.tags.find((t) => t.tagId === id) + const pid = projectId || projectStore.active?.projectId; + await tagWatchService.updateTagName(pid!, id, name); + const updatedTag = this.tags.find((t) => t.tagId === id); if (updatedTag) { this.setTags(this.tags.map((t) => t.tagId === id ? { ...updatedTag, name } : t)); } diff --git a/frontend/app/services/TagWatchService.ts b/frontend/app/services/TagWatchService.ts index f8c9ff461..ab12d3e0c 100644 --- a/frontend/app/services/TagWatchService.ts +++ b/frontend/app/services/TagWatchService.ts @@ -12,27 +12,27 @@ export interface Tag extends CreateTag { } export default class TagWatchService extends BaseService { - createTag(data: CreateTag) { - return this.client.post('/tags', data) - .then(r => r.json()) - .then((response: { data: any; }) => response.data || {}) + async createTag(projectId: number, data: CreateTag) { + const r = await this.client.post(`/${projectId}/tags`, data); + const response = await r.json(); + return response.data || {}; } - getTags() { - return this.client.get('/tags') - .then(r => r.json()) - .then((response: { data: any; }) => response.data || {}) + async getTags(projectId: number) { + const r = await this.client.get(`/${projectId}/tags`); + const response = await r.json(); + return response.data || {}; } - deleteTag(id: number) { - return this.client.delete(`/tags/${id}`) - .then(r => r.json()) - .then((response: { data: any; }) => response.data || {}) + async deleteTag(projectId: number, id: number) { + const r = await this.client.delete(`/${projectId}/tags/${id}`); + const response = await r.json(); + return response.data || {}; } - updateTagName(id: number, name: string) { - return this.client.put(`/tags/${id}`, { name }) - .then(r => r.json()) - .then((response: { data: any; }) => response.data || {}) + async updateTagName(projectId: number, id: number, name: string) { + const r = await this.client.put(`/${projectId}/tags/${id}`, { name }); + const response = await r.json(); + return response.data || {}; } -} \ No newline at end of file +}