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
101 changes: 101 additions & 0 deletions src/components/core/ExportRetroButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { NotificationType, useNotifyStore } from '../../stores/notifyStore';
import { useRetrospectiveStore } from '../../stores/retrospectiveStore';
import retrospectiveApi from '../../services/retrospectiveApi';
import { ExportType } from '../../services/index';
import DownloadIcon from '../icons/DownloadIcon.vue';
import BaseButton from './BaseButton.vue';
import logger from '../../services/logger';

const notifyStore = useNotifyStore();
const retroStore = useRetrospectiveStore();

const showDropdown = ref(false);
const isExporting = ref(false);
const containerRef = ref<HTMLElement | null>(null);

const exportOptions: { label: string; type: ExportType; extension: string }[] = [
{ label: 'JSON', type: 'JSON', extension: 'json' },
{ label: 'Markdown', type: 'MARKDOWN', extension: 'md' },
{ label: 'PDF', type: 'PDF', extension: 'pdf' },
];

const handleExport = async (exportType: ExportType, extension: string) => {
if (!retroStore.currentRetro?.id) return;

isExporting.value = true;
showDropdown.value = false;

const result = await retrospectiveApi.exportRetrospective(
retroStore.currentRetro.id,
exportType,
);

isExporting.value = false;

if (result.error) {
logger.error('Error exporting retrospective');
notifyStore.notify('Failed to export retrospective', NotificationType.Error);
return;
}

const filename = `retrospective-${retroStore.currentRetro.name.replace(/ /g, '_')}.${extension}`;
const url = window.URL.createObjectURL(result);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);

notifyStore.notify('Retrospective exported successfully!', NotificationType.Success);
};

const toggleDropdown = () => {
showDropdown.value = !showDropdown.value;
};

const handleClickOutside = (event: MouseEvent) => {
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
showDropdown.value = false;
}
};

onMounted(() => {
document.addEventListener('click', handleClickOutside);
});

onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside);
});
</script>

<template>
<div ref="containerRef" class="relative">
<BaseButton
class="font-bold flex gap-2"
:style="'WHITE'"
:disabled="isExporting"
@click="toggleDropdown"
>
<DownloadIcon :size="18" />
{{ isExporting ? 'Exporting...' : 'Export' }}
</BaseButton>

<div
v-if="showDropdown"
class="absolute right-0 mt-2 w-40 bg-white border border-gray-300 rounded-md shadow-lg z-10"
>
<button
v-for="option in exportOptions"
:key="option.type"
class="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 first:rounded-t-md last:rounded-b-md"
@click="handleExport(option.type, option.extension)"
>
{{ option.label }}
</button>
</div>
</div>
</template>
19 changes: 19 additions & 0 deletions src/components/icons/DownloadIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
defineProps<{ size: number }>();
</script>

<template>
<!--Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.!-->

<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="currentColor"
:width="size"
:height="size"
>
<path
d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"
/>
</svg>
</template>
2 changes: 2 additions & 0 deletions src/components/retrospective/RetrospectiveLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ModalifyComponent from '../core/ModalifyComponent.vue';
import QuestionLayout from '../question/QuestionLayout.vue';
import ShareRetro from '../core/ShareRetroButton.vue';
import ExportRetro from '../core/ExportRetroButton.vue';
import CreateQuestion from '../question/CreateQuestion.vue';
import { storeToRefs } from 'pinia';
import { RouterLink } from 'vue-router';
Expand All @@ -28,6 +29,7 @@
</h1>
<div class="flex gap-2 ml-4">
<ShareRetro />
<ExportRetro />
<RouterLink
class="flex self-center flex-shrink-0"
:to="{ name: 'retrospective.edit', params: { id: retrospective.id } }"
Expand Down
3 changes: 3 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ export const API_URL = import.meta.env.VITE_API_URL;
export enum Endpoints {
Question = '/question',
Retrospective = '/retrospective',
RetrospectiveExport = '/retrospective/export',
Answer = '/answer',
VoteAnswer = '/answer/vote',
SocketHello = '/hello',
Limits = '/limits',
}

export type ExportType = 'JSON' | 'MARKDOWN' | 'PDF';

export const apiRequest = axios.create({
baseURL: `${import.meta.env.PROD ? 'https' : 'http'}://${API_URL}`,
withCredentials: true,
Expand Down
30 changes: 28 additions & 2 deletions src/services/retrospectiveApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Endpoints, MayBeError, apiRequest } from './index';
import { Endpoints, ExportType, MayBeError, apiRequest } from './index';
import { ID, Retrospective } from '../stores/retrospectiveStore';

const createRetrospective = async (
Expand Down Expand Up @@ -43,4 +43,30 @@ const updateRetrospective = async (
return result.data;
};

export default { createRetrospective, getRetrospective, deleteRestrospective, updateRetrospective };
const exportRetrospective = async (
retrospectiveId: string,
exportType: ExportType,
): Promise<MayBeError<Blob>> => {
const result = await apiRequest
.post(
Endpoints.RetrospectiveExport,
{
retrospective_id: retrospectiveId,
export_type: exportType,
},
{ responseType: 'blob' },
)
.catch(() => null);

if (!result) return { error: true };

return result.data;
};

export default {
createRetrospective,
getRetrospective,
deleteRestrospective,
updateRetrospective,
exportRetrospective,
};