Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
>
<router-view />
<FeedbackButton />
<Popover />
</div>
</template>

<script lang="ts">
import * as api from './api/';

Check warning on line 14 in src/App.vue

View workflow job for this annotation

GitHub Actions / ESlint

Can't resolve './api/' in '/home/runner/work/hawk.garage/hawk.garage/src'
import { setLanguage } from './i18n';

Check warning on line 15 in src/App.vue

View workflow job for this annotation

GitHub Actions / ESlint

Can't resolve './i18n' in '/home/runner/work/hawk.garage/hawk.garage/src'
import { defineComponent } from 'vue';
import FeedbackButton from './components/utils/FeedbackButton.vue';
import notifier from 'codex-notifier';
import { Popover } from '@codexteam/ui/vue';

export default defineComponent({
name: 'App',
components: {
FeedbackButton,
Popover,
},
computed: {
/**
Expand Down
17 changes: 16 additions & 1 deletion src/api/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
QUERY_EVENT,
QUERY_EVENT_REPETITIONS_PORTION,
QUERY_PROJECT_DAILY_EVENTS,
QUERY_CHART_DATA
QUERY_CHART_DATA,
MUTATION_REMOVE_EVENT
} from './queries';
import * as api from '@/api';
import type {
Expand Down Expand Up @@ -196,3 +197,17 @@ export async function fetchChartData(
timezoneOffset,
})).project.event.chartData;
}

/**
* Remove event and all related data (repetitions, daily events)
* @param projectId - project event is related to
* @param eventId — original event id to remove
*/
export async function removeEvent(projectId: string, eventId: string): Promise<APIResponse<{ removeEvent: boolean }>> {
return await api.call<{ removeEvent: boolean }>(MUTATION_REMOVE_EVENT, {
projectId,
eventId,
}, undefined, {
allowErrors: true,
});
}
10 changes: 10 additions & 0 deletions src/api/events/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,13 @@ export const MUTATION_REMOVE_EVENT_ASSIGNEE = `
}
}
`;

// language=GraphQL
/**
* GraphQL Mutation to remove an event and all related data
*/
export const MUTATION_REMOVE_EVENT = `
mutation removeEvent($projectId: ID!, $eventId: ID!) {
removeEvent(projectId: $projectId, eventId: $eventId)
}
`;
28 changes: 28 additions & 0 deletions src/components/event/EventActionsMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<ContextMenu
class="event-actions-menu"
:items="items"
/>
</template>

<script setup lang="ts">
import { ContextMenu, type ContextMenuItem } from '@codexteam/ui/vue';

/**
* Event actions menu props
*/
interface Props {
/**
* List of action items to show in the menu
*/
items: ContextMenuItem[];
}

defineProps<Props>();
</script>

<style scoped>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, use nesting

.event-actions-menu {
min-width: 140px;
}
</style>
142 changes: 134 additions & 8 deletions src/components/event/EventHeader.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
<template>
<div class="event-header">
<div class="event-layout__container">
<span
v-if="!loading"
class="event-header__date"
>
{{ formattedFullDate }}
</span>
<div class="event-header__top-right">
<span
v-if="!loading"
class="event-header__date"
>
{{ formattedFullDate }}
</span>
<Icon
v-if="isAdmin"
class="event-header__button--more"
symbol="dots"
@click="onMoreClick($event)"
/>
Comment on lines +11 to +16
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3-dots control is an SVG Icon with a click handler, which is not keyboard-focusable and has no accessible name. Please render it as a real <button type="button"> (or add appropriate role="button", tabindex="0", key handlers) and provide an aria-label like "More actions" so keyboard/screen-reader users can access the menu.

Suggested change
<Icon
class="event-header__button--more"
symbol="dots"
@click="onMoreClick($event)"
/>
<button
type="button"
class="event-header__button--more"
aria-label="More actions"
@click="onMoreClick($event)"
>
<Icon
symbol="dots"
aria-hidden="true"
/>
</button>

Copilot uses AI. Check for mistakes.
</div>

<div
v-if="workspace"
Expand Down Expand Up @@ -124,12 +132,17 @@ import AssigneeBar from '../utils/AssigneeBar.vue';
import EntityImage from '../utils/EntityImage.vue';

import { HawkEvent, HawkEventBacktraceFrame } from '@/types/events';
import { TOGGLE_EVENT_MARK } from '@/store/modules/events/actionTypes';
import { REMOVE_EVENT, TOGGLE_EVENT_MARK } from '@/store/modules/events/actionTypes';
import { Project } from '@/types/project';
import { Workspace } from '@/types/workspaces';
import { projectBadges } from '../../mixins/projectBadges';
import ProjectBadge from '../project/ProjectBadge.vue';
import { JavaScriptAddons } from '@hawk.so/types';
import { ContextMenuItem, usePopover } from '@codexteam/ui/vue';
import EventActionsMenu from './EventActionsMenu.vue';
import { ActionType } from '../utils/ConfirmationWindow/types';
import notifier from 'codex-notifier';
import Icon from '../utils/Icon.vue';

export default defineComponent({
name: 'EventHeader',
Expand All @@ -141,6 +154,7 @@ export default defineComponent({
AssigneeBar,
EntityImage,
ProjectBadge,
Icon,
},
mixins: [projectBadges],
props: {
Expand All @@ -153,6 +167,15 @@ export default defineComponent({
validator: prop => typeof prop === 'object' || prop === null,
},
},
emits: ['event-deleted'],
setup() {
const { showPopover, hide } = usePopover();

return {
showPopover,
hidePopover: hide,
};
},
data() {
return {
/**
Expand Down Expand Up @@ -253,6 +276,13 @@ export default defineComponent({
return this.$store.getters.getWorkspaceByProjectId(this.projectId);
},

/**
* Is current user admin in workspace with this project
*/
isAdmin(): boolean {
return this.workspace ? this.$store.getters.isCurrentUserAdmin(this.workspace.id) : false;
},

/**
* Computed property that returns formatted full date for event timestamp
*/
Expand Down Expand Up @@ -302,6 +332,84 @@ export default defineComponent({
window.open(this.event.taskManagerItem.url, '_blank', 'noopener');
}
},

/**
* Build "more options" context menu items
*/
eventActionsMenuItems(): ContextMenuItem[] {
return [
{
type: 'default',
title: this.$t('event.remove') as string,
icon: 'Trash',
onActivate: () => {
this.hidePopover();
this.confirmRemoveEvent();
},
},
];
},

/**
* Open the "more options" context menu near the 3-dot button
*
* @param event - native click mouse event
*/
onMoreClick(event: MouseEvent) {
if (!this.isAdmin) {
return;
}

this.showPopover({
targetEl: event.currentTarget as HTMLElement,
with: {
Comment on lines +358 to +365
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onMoreClick anchors the popover to event.currentTarget from an SVG click, then force-casts it to HTMLElement. This is brittle (and currently anchors to the SVG, not the full button). If you move the click handler to the wrapper element, you can pass a real HTML element to the popover without unsafe casting.

Copilot uses AI. Check for mistakes.
component: EventActionsMenu,
props: {
items: this.eventActionsMenuItems(),
},
},
align: {
vertically: 'below',
horizontally: 'right',
},
});
},

/**
* Show confirmation dialog and, on confirm, delete the event then navigate back
*/
confirmRemoveEvent() {
const { projectId, eventId } = this.$route.params;

this.$confirm.open({
description: this.$t('event.removeConfirmation').toString(),
actionType: ActionType.DELETION,
continueButtonText: this.$t('event.removeButton').toString(),
onConfirm: async () => {
const isRemoved = await this.$store.dispatch(REMOVE_EVENT, {
projectId,
eventId,
});

if (isRemoved) {
notifier.show({
message: this.$t('event.removeSuccess').toString(),
style: 'success',
time: 5000,
});
this.$emit('event-deleted');

return;
}

notifier.show({
message: this.$t('event.removeError').toString(),
style: 'error',
time: 5000,
});
},
Comment on lines +388 to +410
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deletion flow treats any resolved dispatch as success, but eventsApi.removeEvent() uses api.callOld(), which does not throw on GraphQL errors and may return false/null while still resolving. Please check the boolean result from dispatch(REMOVE_EVENT, ...) and show the error notifier (and avoid emitting event-deleted) when the result is falsy (or switch this API call to api.call() and handle response.errors).

Copilot uses AI. Check for mistakes.
});
},
},
});
</script>
Expand Down Expand Up @@ -352,8 +460,14 @@ export default defineComponent({
text-overflow: ellipsis;
}

&__date {
&__top-right {
display: flex;
float: right;
align-items: center;
gap: 12px;
}

&__date {
font-size: 12px;
line-height: 23px;
}
Expand Down Expand Up @@ -395,6 +509,18 @@ export default defineComponent({
&:hover {
color: var(--color-text-main)
}

&--more {
width: 16px;
height: 16px;
opacity: 0.5;
cursor: pointer;
transition: opacity 0.2s ease-in-out;

&:hover {
opacity: 1;
}
}
}

&__nav-bar, &__viewed-by {
Expand Down
14 changes: 14 additions & 0 deletions src/components/event/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<EventHeader
v-if="event || loading"
:event="event"
@event-deleted="onEventDeleted"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this event is redundant, you can handle it inside

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neSpecc if i remove this handling, the events list won’t refresh after deleting an event.
Closing the event modal only changes the route; it does not automatically reload EventsList, so the deleted item can still appear until a manual reload.

/>
<div class="event-layout__info">
<div class="event-layout__container">
Expand Down Expand Up @@ -45,6 +46,7 @@ export default defineComponent({
PopupDialog,
EventHeader,
},
emits: ['event-deleted'],
data() {
return {
/**
Expand Down Expand Up @@ -125,6 +127,18 @@ export default defineComponent({
});
}
},

/**
* Called when EventHeader signals that the event was deleted.
* and navigates back to the project overview to close the modal.
*/
onEventDeleted() {
this.$emit('event-deleted');
this.$router.push({
name: 'project-overview',
params: { projectId: this.projectId },
});
},
},
});
</script>
Expand Down
11 changes: 10 additions & 1 deletion src/components/project/Overview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
<EventsList ref="eventsList" />
</div>
</div>
<router-view />
<router-view v-slot="{ Component }">
<component
:is="Component"
@event-deleted="eventDeleted"
/>
</router-view>
</div>
</template>

Expand Down Expand Up @@ -145,6 +150,10 @@ export default {
this.$refs.eventsList.reloadDailyEvents();
}
},

async eventDeleted() {
this.reloadDailyEvents();
},
},
};
</script>
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,12 @@
"title": "Event Hidden",
"description": "We received this event but have hidden it because your subscription has expired.",
"upgradeButton": "Pay and view event"
}
},
"remove": "Remove event",
"removeConfirmation": "Are you sure you want to remove this event? All repetitions and related data will also be removed.",
"removeButton": "Remove",
"removeSuccess": "Event successfully removed",
"removeError": "Failed to remove the event"
},
"common": {
"workspace": "Workspace",
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,12 @@
"title": "Событие скрыто",
"description": "Мы получили это событие, но скрыли его, потому что ваша подписка закончилась.",
"upgradeButton": "Оплатить и посмотреть событие"
}
},
"remove": "Удалить событие",
"removeConfirmation": "Вы уверены, что хотите удалить это событие? Все повторения и связанные данные также будут удалены.",
"removeButton": "Удалить",
"removeSuccess": "Событие успешно удалено",
"removeError": "Не удалось удалить событие"
},
"common": {
"workspace": "Воркспейc",
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@codexteam/ui/styles';
import './styles/base.css';
import { createApp } from 'vue';
import 'virtual:svg-icons-register';
Expand All @@ -11,7 +12,6 @@ import * as api from './api/index';
import { REFRESH_TOKENS } from './store/modules/user/actionTypes';
import { RESET_STORE } from './store/methodsTypes';

import '@codexteam/ui/styles';

const DEBOUNCE_TIMEOUT = 1000;

Expand Down
Loading
Loading