add exporter in frontend, fixes ST-382

This commit is contained in:
Gregor Vostrak
2024-09-11 12:49:26 +02:00
committed by Constantin Graf
parent 9a8945b0dc
commit 6dd9d5bab0
8 changed files with 258 additions and 33 deletions

View File

@@ -185,7 +185,10 @@ services:
- './docker/local/minio:/etc/minio'
networks:
- sail
- reverse-proxy
entrypoint: /etc/minio/create_bucket.sh
extra_hosts:
- "storage.${NGINX_HOST_NAME}:${REVERSE_PROXY_IP:-10.100.100.10}"
networks:
reverse-proxy:
name: "${NETWORK_NAME}"

View File

@@ -167,7 +167,7 @@ const page = usePage<{
href="/billing"></NavigationSidebarItem>
<NavigationSidebarItem
v-if="canUpdateOrganization()"
title="Import"
title="Import / Export"
:icon="ArrowsRightLeftIcon"
:current="route().current('import')"
:href="route('import')"></NavigationSidebarItem>

View File

@@ -4,19 +4,23 @@ import AppLayout from '@/Layouts/AppLayout.vue';
import { ArrowsRightLeftIcon } from '@heroicons/vue/16/solid';
import PageTitle from '@/Components/Common/PageTitle.vue';
import ImportData from '@/Pages/Teams/Partials/ImportData.vue';
import ExportData from '@/Pages/Teams/Partials/ExportData.vue';
</script>
<template>
<AppLayout title="Import" data-testid="import_view">
<AppLayout title="Import / Export" data-testid="import_view">
<MainContainer
class="py-5 border-b border-default-background-separator flex justify-between items-center">
<div class="flex items-center space-x-6">
<PageTitle :icon="ArrowsRightLeftIcon" title="Import">
<PageTitle :icon="ArrowsRightLeftIcon" title="Import / Export">
</PageTitle>
</div>
</MainContainer>
<MainContainer class="py-6">
<ImportData></ImportData>
<MainContainer class="py-6 space-y-4">
<div class="grid lg:grid-cols-2 gap-6">
<ImportData></ImportData>
<ExportData></ExportData>
</div>
</MainContainer>
</AppLayout>
</template>

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { ref } from 'vue';
import { useNotificationsStore } from '@/utils/notification';
import { api, type OrganizationExportResponse } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import {
ArrowUpOnSquareIcon,
InformationCircleIcon,
} from '@heroicons/vue/24/outline';
import { CardTitle } from '@/packages/ui/src';
import Card from '@/Components/Common/Card.vue';
import { useOrganizationStore } from '@/utils/useOrganization';
const showResultModal = ref(false);
const loading = ref(false);
const exportResponse = ref<OrganizationExportResponse | null>(null);
const { organization } = useOrganizationStore();
const { handleApiRequestNotifications } = useNotificationsStore();
async function exportData() {
loading.value = true;
const organizationId = getCurrentOrganizationId();
if (organizationId) {
const response = await handleApiRequestNotifications(
() =>
api.exportOrganization(
{},
{
params: {
organization: organizationId,
},
}
),
'Organization data exported successfully.',
'Exporting organization data failed.'
);
if (response) {
showResultModal.value = true;
loading.value = false;
exportResponse.value = response;
window.open(response.download_url, '_blank')?.focus();
}
}
}
</script>
<template>
<DialogModal
closeable
:show="showResultModal"
@close="showResultModal = false">
<template #title>The export was successful!</template>
<template #content>
<div class="pb-6">
The download should start automatically. If it does not
<a
class="font-semibold text-accent-200 hover:text-accent-300"
target="_blank"
:href="exportResponse?.download_url"
>click here</a
>
</div>
</template>
<template #footer>
<SecondaryButton @click="showResultModal = false">
Close
</SecondaryButton>
</template>
</DialogModal>
<div>
<CardTitle title="Export Data" :icon="ArrowUpOnSquareIcon"></CardTitle>
<Card class="mb-3">
<div class="py-2 px-3 sm:px-4 text-sm flex items-center space-x-3">
<InformationCircleIcon
class="h-5 min-w-0 w-5 text-bg-tertiary" />
<p class="flex-1">
Export your solidtime organization data. This will include
all clients, projects, tasks, and time entries. You will
receive a zip file with json files for each entity.
</p>
</div>
</Card>
<Card>
<div
class="py-6 flex-col bg-card-background items-center flex space-y-5 text-center text-sm">
<div>
The following organization will be exported: <br />
<strong class="font-semibold">{{
organization?.name
}}</strong>
</div>
<PrimaryButton :loading @click="exportData"
>Export Organization Data
</PrimaryButton>
</div>
</Card>
</div>
</template>

View File

@@ -1,16 +1,22 @@
<script setup lang="ts">
import FormSection from '@/Components/FormSection.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { computed, onMounted, ref } from 'vue';
import { useNotificationsStore } from '@/utils/notification';
import { api } from '@/packages/api/src';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { DocumentIcon } from '@heroicons/vue/24/solid';
import {
ArrowDownOnSquareIcon,
InformationCircleIcon,
} from '@heroicons/vue/24/outline';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { ImportReport, ImportType } from '@/packages/api/src';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import { initializeStores } from '@/utils/init';
import { CardTitle } from '@/packages/ui/src';
import Card from '@/Components/Common/Card.vue';
const importTypeOptions = ref<ImportType[]>([]);
@@ -163,21 +169,30 @@ const showResultModal = ref(false);
</SecondaryButton>
</template>
</DialogModal>
<FormSection>
<template #title> Import Data</template>
<div>
<CardTitle
title="Import Data"
:icon="ArrowDownOnSquareIcon"></CardTitle>
<Card class="mb-3">
<div class="py-2 px-3 sm:px-4 text-sm flex items-center space-x-3">
<InformationCircleIcon
class="h-5 min-w-0 w-5 text-bg-tertiary" />
<p class="flex-1">
Import existing data from Toggl, Clockify or a different
solidtime instance. Please select the type of data you want
to import and follow the instructions.
</p>
</div>
</Card>
<template #description>
Import existing data from Toggl or Clockify.
</template>
<template #form>
<!-- Organization Owner Information -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="currency" value="Import Type" />
<Card>
<div
class="px-4 py-5 sm:px-5 bg-card-background shadow sm:rounded-tl-md sm:rounded-tr-md">
<div>
<InputLabel for="importType" value="Import Type" />
<select
name="currency"
id="currency"
name="importType"
id="importType"
v-model="importType"
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
<option :value="null" selected disabled>
@@ -232,11 +247,13 @@ const showResultModal = ref(false);
</div>
</div>
</div>
</template>
<template #actions>
<PrimaryButton :loading @click="importData"
>Import Data</PrimaryButton
>
</template>
</FormSection>
<div
class="flex items-center justify-end px-4 py-3 bg-card-background border-t border-card-background-separator text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
<PrimaryButton :loading @click="importData"
>Import Data
</PrimaryButton>
</div>
</Card>
</div>
</template>

View File

@@ -5,7 +5,6 @@ import SectionBorder from '@/Components/SectionBorder.vue';
import UpdateTeamNameForm from '@/Pages/Teams/Partials/UpdateTeamNameForm.vue';
import type { Organization } from '@/types/models';
import type { Permissions, Role } from '@/types/jetstream';
import ImportData from '@/Pages/Teams/Partials/ImportData.vue';
import { canUpdateOrganization } from '@/utils/permissions';
import OrganizationBillableRate from '@/Pages/Teams/Partials/OrganizationBillableRate.vue';
@@ -34,14 +33,8 @@ defineProps<{
:team="team" />
<SectionBorder />
<ImportData
v-if="canUpdateOrganization()"
:team="team"></ImportData>
<template
v-if="permissions.canDeleteTeam && !team.personal_team">
<SectionBorder />
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
</template>
</div>

View File

@@ -134,6 +134,11 @@ export type MyMemberships = ZodiosResponseByAlias<
export type MyMembership = MyMemberships[0];
export type OrganizationExportResponse = ZodiosResponseByAlias<
SolidTimeApi,
'exportOrganization'
>;
const api = createApiClient('/api', { validate: 'none' });
export { createApiClient, api };

View File

@@ -542,6 +542,55 @@ const endpoints = makeApi([
},
],
},
{
method: 'post',
path: '/v1/organizations/:organization/export',
alias: 'exportOrganization',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: z.object({}).partial().passthrough(),
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
],
response: z
.object({ success: z.boolean(), download_url: z.string() })
.passthrough(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 401,
description: `Unauthenticated`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 403,
description: `Authorization error`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 404,
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
],
},
{
method: 'post',
path: '/v1/organizations/:organization/import',
@@ -1104,6 +1153,58 @@ const endpoints = makeApi([
},
],
},
{
method: 'post',
path: '/v1/organizations/:organization/members/:member/make-placeholder',
alias: 'v1.members.make-placeholder',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: z.object({}).partial().passthrough(),
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'member',
type: 'Path',
schema: z.string(),
},
],
response: z.null(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 401,
description: `Unauthenticated`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 403,
description: `Authorization error`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 404,
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
],
},
{
method: 'put',
path: '/v1/organizations/:organization/project-members/:projectMember',