Skip to content

Commit

Permalink
Merge pull request stakwork#955 from aliraza556/feature/workspace-tic…
Browse files Browse the repository at this point in the history
…ket-view

Implement Workspace Ticket View and Store
  • Loading branch information
humansinstitute authored Jan 20, 2025
2 parents 7a91992 + bbe55d9 commit 5973427
Show file tree
Hide file tree
Showing 8 changed files with 961 additions and 14 deletions.
589 changes: 589 additions & 0 deletions src/components/common/TicketEditor/WorkspaceTicketEditor.tsx

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import PeopleHeader from '../people/main/Header';
import TokenRefresh from '../people/utils/TokenRefresh';
import GenerateStoriesView from '../people/widgetViews/GenerateStoriesView';
import PhasePlannerView from '../people/widgetViews/PhasePlannerView';
import WorkspaceTicketView from '../people/widgetViews/workspace/WorkspaceTicketView';
import { HiveChatView } from '../people/hiveChat/index';
import WorkSpacePlanner from '../people/WorkSpacePlanner/index';
import DailyBountyPage from './DailyBountyPage/index';
Expand Down Expand Up @@ -45,6 +46,9 @@ const modeDispatchPages: Record<AppMode, () => React.ReactElement> = {
<Route path="/workspace/bounties/:uuid">
<WorkspaceTicketsPage />
</Route>
<Route path="/workspace/:workspaceId/ticket/:ticketId">
<WorkspaceTicketView />
</Route>
<Route path="/workspace/:uuid/planner">
<WorkSpacePlanner />
</Route>
Expand Down
9 changes: 5 additions & 4 deletions src/people/WorkSpacePlanner/BountyCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const StatusText = styled.span<{ status?: BountyCardStatus }>`
`;

interface BountyCardProps extends BountyCard {
onclick: (bountyId: string) => void;
onclick: (bountyId: string, status?: BountyCardStatus, ticketGroup?: string) => void;
}

const BountyCardComponent: React.FC<BountyCardProps> = ({
Expand All @@ -139,16 +139,17 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
workspace,
status,
onclick,
assignee_name
assignee_name,
ticket_group
}: BountyCardProps) => (
<CardContainer isDraft={status === 'DRAFT'} onClick={() => onclick(id)}>
<CardContainer isDraft={status === 'DRAFT'} onClick={() => onclick(id, status, ticket_group)}>
<CardHeader>
<CardTitle
role="button"
tabIndex={0}
onClick={(e: React.MouseEvent<HTMLHeadingElement>) => {
e.stopPropagation();
onclick(id);
onclick(id, status, ticket_group);
}}
>
{status === 'DRAFT' && <DraftIndicator>Draft</DraftIndicator>}
Expand Down
19 changes: 9 additions & 10 deletions src/people/WorkSpacePlanner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,15 @@ const WorkspacePlanner = observer(() => {
{}
);

const handleCardClick = (bountyId: string, status?: BountyCardStatus) => {
const handleCardClick = (bountyId: string, status?: BountyCardStatus, ticketGroup?: string) => {
bountyCardStore.saveFilterState();
if (status === 'DRAFT') {
window.open(
history.createHref({
pathname: `/workspace/${uuid}/ticket/${bountyId}`,
state: { from: `/workspace/${uuid}/planner` }
}),
'_blank'
);
if (status === 'DRAFT' && ticketGroup) {
const ticketUrl = history.createHref({
pathname: `/workspace/${uuid}/ticket/${ticketGroup}`,
state: { from: `/workspace/${uuid}/planner` }
});
console.log('Opening ticket URL:', ticketUrl);
window.open(ticketUrl, '_blank');
} else {
window.open(
history.createHref({
Expand Down Expand Up @@ -247,7 +246,7 @@ const WorkspacePlanner = observer(() => {
<BountyCardComp
key={card.id}
{...card}
onclick={() => handleCardClick(card.id, card.status)}
onclick={() => handleCardClick(card.id, card.status, card.ticket_group)}
/>
))
)}
Expand Down
232 changes: 232 additions & 0 deletions src/people/widgetViews/workspace/WorkspaceTicketView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useParams, useHistory } from 'react-router-dom';
import { useStores } from 'store';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import MaterialIcon from '@material/react-material-icon';
import { createSocketInstance, SOCKET_MSG } from 'config/socket';
import WorkspaceTicketEditor from 'components/common/TicketEditor/WorkspaceTicketEditor';
import { workspaceTicketStore } from '../../../store/workspace-ticket';
import { TicketMessage } from '../../../store/interface';
import { FeatureBody, FeatureDataWrap } from '../../../pages/tickets/style';
import { FeatureHeadWrap, FeatureHeadNameWrap, WorkspaceName } from './style';

const WorkspaceTicketView: React.FC = observer(() => {
const { workspaceId, ticketId } = useParams<{ workspaceId: string; ticketId: string }>();
const [websocketSessionId, setWebsocketSessionId] = useState<string>('');
const [swwfLinks, setSwwfLinks] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(true);
const { main } = useStores();
const history = useHistory();
const [currentTicketId, setCurrentTicketId] = useState<string>(ticketId);

useEffect(() => {
const socket = createSocketInstance();

socket.onopen = () => {
console.log('Socket connected in Workspace Ticket View');
};

const refreshSingleTicket = async (ticketUuid: string) => {
try {
const ticket = await main.getTicketDetails(ticketUuid);

if (ticket.UUID) {
ticket.uuid = ticket.UUID;
}

workspaceTicketStore.addTicket(ticket);

const groupId = ticket.ticket_group || ticket.uuid;
const latestTicket = workspaceTicketStore.getLatestVersionFromGroup(groupId);

if (latestTicket) {
workspaceTicketStore.updateTicket(latestTicket.uuid, latestTicket);
setCurrentTicketId(latestTicket.uuid);
}
} catch (error) {
console.error('Error on refreshing ticket:', error);
}
};

socket.onmessage = async (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);

if (data.msg === SOCKET_MSG.user_connect) {
const sessionId = data.body;
setWebsocketSessionId(sessionId);
return;
}

if (data.action === 'swrun' && data.message && data.ticketDetails?.ticketUUID) {
try {
const stakworkId = data.message.replace(
'https://jobs.stakwork.com/admin/projects/',
''
);
if (stakworkId) {
setSwwfLinks((prev: Record<string, string>) => ({
...prev,
[data.ticketDetails.ticketUUID]: stakworkId
}));
}
} catch (error) {
console.error('Error processing Stakwork URL:', error);
}
return;
}

const ticketMessage = data as TicketMessage;

switch (ticketMessage.action) {
case 'message':
console.log('Received ticket message:', ticketMessage.message);
break;

case 'process':
console.log('Processing ticket update:', ticketMessage.ticketDetails.ticketUUID);
await refreshSingleTicket(ticketMessage.ticketDetails.ticketUUID as string);
break;
}
} catch (error) {
console.error('Error processing websocket message:', error);
}
};

return () => {
if (socket.readyState === WebSocket.OPEN) {
socket.close();
}
};
}, [main]);

useEffect(() => {
const fetchTicketAndVersions = async () => {
try {
setIsLoading(true);
const groupTickets = await main.getTicketsByGroup(ticketId);

if (groupTickets && Array.isArray(groupTickets) && groupTickets.length > 0) {
workspaceTicketStore.clearTickets();

for (const ticket of groupTickets) {
if (ticket.UUID) {
ticket.uuid = ticket.UUID;
}
workspaceTicketStore.addTicket(ticket);
}

const groupId = groupTickets[0].ticket_group || ticketId;

const latestVersion = workspaceTicketStore.getLatestVersionFromGroup(groupId);

if (latestVersion) {
workspaceTicketStore.addTicket(latestVersion);
setCurrentTicketId(latestVersion.uuid);
}
}
} catch (error) {
console.error('Error fetching ticket and versions:', error);
} finally {
setIsLoading(false);
}
};

fetchTicketAndVersions();
}, [ticketId, main]);

const handleClose = () => {
history.push(`/workspace/${workspaceId}/planner`);
};

const getTickets = async () => {
const ticket = workspaceTicketStore.getTicket(currentTicketId);
return ticket ? [ticket] : [];
};

const currentTicket = workspaceTicketStore.getTicket(currentTicketId);

if (isLoading) {
return (
<FeatureBody>
<FeatureHeadWrap>
<FeatureHeadNameWrap>
<MaterialIcon
onClick={handleClose}
icon={'arrow_back'}
style={{
fontSize: 25,
cursor: 'pointer'
}}
/>
<WorkspaceName>Ticket Editor</WorkspaceName>
</FeatureHeadNameWrap>
</FeatureHeadWrap>
<EuiFlexGroup
alignItems="center"
justifyContent="center"
style={{ height: 'calc(100vh - 100px)' }}
>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
</FeatureBody>
);
}

if (!currentTicket) {
return (
<FeatureBody>
<FeatureHeadWrap>
<FeatureHeadNameWrap>
<MaterialIcon
onClick={handleClose}
icon={'arrow_back'}
style={{
fontSize: 25,
cursor: 'pointer'
}}
/>
<WorkspaceName>Ticket Editor</WorkspaceName>
</FeatureHeadNameWrap>
</FeatureHeadWrap>
<FeatureDataWrap>
<div>Ticket not found</div>
</FeatureDataWrap>
</FeatureBody>
);
}

return (
<FeatureBody>
<FeatureHeadWrap>
<FeatureHeadNameWrap>
<MaterialIcon
onClick={handleClose}
icon={'arrow_back'}
style={{
fontSize: 25,
cursor: 'pointer'
}}
/>
<WorkspaceName>Ticket Editor</WorkspaceName>
</FeatureHeadNameWrap>
</FeatureHeadWrap>
<FeatureDataWrap>
<WorkspaceTicketEditor
ticketData={currentTicket}
websocketSessionId={websocketSessionId}
index={0}
draggableId={currentTicket.uuid}
hasInteractiveChildren={false}
swwfLink={swwfLinks[currentTicket.uuid]}
getPhaseTickets={getTickets}
/>
</FeatureDataWrap>
</FeatureBody>
);
});

export default WorkspaceTicketView;
2 changes: 2 additions & 0 deletions src/store/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,8 @@ export interface BountyCard {
assignee?: any;
assignee_name?: string;
pow?: number;
ticket_uuid?: string;
ticket_group?: string;
}

export type BountyReviewStatus = 'New' | 'Accepted' | 'Rejected' | 'Change Requested';
Expand Down
30 changes: 30 additions & 0 deletions src/store/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3929,6 +3929,36 @@ export class MainStore {
}
}

async getTicketsByGroup(groupId: string): Promise<Ticket[]> {
try {
if (!uiStore.meInfo) return [];
const info = uiStore.meInfo;

const response = await fetch(`${TribesURL}/bounties/ticket/group/${groupId}`, {
method: 'GET',
mode: 'cors',
headers: {
'x-jwt': info.tribe_jwt,
'Content-Type': 'application/json',
'x-session-id': this.sessionId
}
});

if (!response.ok) {
throw new Error('Failed to fetch tickets by group');
}

const data = await response.json();
return data.map((ticket: Ticket) => ({
...ticket,
uuid: ticket.UUID || ticket.uuid
}));
} catch (error) {
console.error('Error fetching tickets by group:', error);
return [];
}
}

async createConnectionCodes({
users_number,
sats_amount,
Expand Down
Loading

0 comments on commit 5973427

Please sign in to comment.