mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
add exporter in frontend, fixes ST-382
This commit is contained in:
committed by
Constantin Graf
parent
9a8945b0dc
commit
6dd9d5bab0
@@ -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}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
102
resources/js/Pages/Teams/Partials/ExportData.vue
Normal file
102
resources/js/Pages/Teams/Partials/ExportData.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user