mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 12:12:15 +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'
|
- './docker/local/minio:/etc/minio'
|
||||||
networks:
|
networks:
|
||||||
- sail
|
- sail
|
||||||
|
- reverse-proxy
|
||||||
entrypoint: /etc/minio/create_bucket.sh
|
entrypoint: /etc/minio/create_bucket.sh
|
||||||
|
extra_hosts:
|
||||||
|
- "storage.${NGINX_HOST_NAME}:${REVERSE_PROXY_IP:-10.100.100.10}"
|
||||||
networks:
|
networks:
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
name: "${NETWORK_NAME}"
|
name: "${NETWORK_NAME}"
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ const page = usePage<{
|
|||||||
href="/billing"></NavigationSidebarItem>
|
href="/billing"></NavigationSidebarItem>
|
||||||
<NavigationSidebarItem
|
<NavigationSidebarItem
|
||||||
v-if="canUpdateOrganization()"
|
v-if="canUpdateOrganization()"
|
||||||
title="Import"
|
title="Import / Export"
|
||||||
:icon="ArrowsRightLeftIcon"
|
:icon="ArrowsRightLeftIcon"
|
||||||
:current="route().current('import')"
|
:current="route().current('import')"
|
||||||
:href="route('import')"></NavigationSidebarItem>
|
:href="route('import')"></NavigationSidebarItem>
|
||||||
|
|||||||
@@ -4,19 +4,23 @@ import AppLayout from '@/Layouts/AppLayout.vue';
|
|||||||
import { ArrowsRightLeftIcon } from '@heroicons/vue/16/solid';
|
import { ArrowsRightLeftIcon } from '@heroicons/vue/16/solid';
|
||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
import ImportData from '@/Pages/Teams/Partials/ImportData.vue';
|
import ImportData from '@/Pages/Teams/Partials/ImportData.vue';
|
||||||
|
import ExportData from '@/Pages/Teams/Partials/ExportData.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title="Import" data-testid="import_view">
|
<AppLayout title="Import / Export" data-testid="import_view">
|
||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-5 border-b border-default-background-separator flex justify-between items-center">
|
class="py-5 border-b border-default-background-separator flex justify-between items-center">
|
||||||
<div class="flex items-center space-x-6">
|
<div class="flex items-center space-x-6">
|
||||||
<PageTitle :icon="ArrowsRightLeftIcon" title="Import">
|
<PageTitle :icon="ArrowsRightLeftIcon" title="Import / Export">
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
</div>
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<MainContainer class="py-6">
|
<MainContainer class="py-6 space-y-4">
|
||||||
<ImportData></ImportData>
|
<div class="grid lg:grid-cols-2 gap-6">
|
||||||
|
<ImportData></ImportData>
|
||||||
|
<ExportData></ExportData>
|
||||||
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import FormSection from '@/Components/FormSection.vue';
|
|
||||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||||
import { DocumentIcon } from '@heroicons/vue/24/solid';
|
import { DocumentIcon } from '@heroicons/vue/24/solid';
|
||||||
|
import {
|
||||||
|
ArrowDownOnSquareIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import type { ImportReport, ImportType } from '@/packages/api/src';
|
import type { ImportReport, ImportType } from '@/packages/api/src';
|
||||||
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import { initializeStores } from '@/utils/init';
|
import { initializeStores } from '@/utils/init';
|
||||||
|
import { CardTitle } from '@/packages/ui/src';
|
||||||
|
import Card from '@/Components/Common/Card.vue';
|
||||||
|
|
||||||
const importTypeOptions = ref<ImportType[]>([]);
|
const importTypeOptions = ref<ImportType[]>([]);
|
||||||
|
|
||||||
@@ -163,21 +169,30 @@ const showResultModal = ref(false);
|
|||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</template>
|
</template>
|
||||||
</DialogModal>
|
</DialogModal>
|
||||||
<FormSection>
|
<div>
|
||||||
<template #title> Import Data</template>
|
<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>
|
<Card>
|
||||||
Import existing data from Toggl or Clockify.
|
<div
|
||||||
</template>
|
class="px-4 py-5 sm:px-5 bg-card-background shadow sm:rounded-tl-md sm:rounded-tr-md">
|
||||||
|
<div>
|
||||||
<template #form>
|
<InputLabel for="importType" value="Import Type" />
|
||||||
<!-- Organization Owner Information -->
|
|
||||||
<div class="col-span-6">
|
|
||||||
<div class="col-span-6 sm:col-span-4">
|
|
||||||
<InputLabel for="currency" value="Import Type" />
|
|
||||||
<select
|
<select
|
||||||
name="currency"
|
name="importType"
|
||||||
id="currency"
|
id="importType"
|
||||||
v-model="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">
|
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>
|
<option :value="null" selected disabled>
|
||||||
@@ -232,11 +247,13 @@ const showResultModal = ref(false);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template #actions>
|
<div
|
||||||
<PrimaryButton :loading @click="importData"
|
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">
|
||||||
>Import Data</PrimaryButton
|
<PrimaryButton :loading @click="importData"
|
||||||
>
|
>Import Data
|
||||||
</template>
|
</PrimaryButton>
|
||||||
</FormSection>
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import SectionBorder from '@/Components/SectionBorder.vue';
|
|||||||
import UpdateTeamNameForm from '@/Pages/Teams/Partials/UpdateTeamNameForm.vue';
|
import UpdateTeamNameForm from '@/Pages/Teams/Partials/UpdateTeamNameForm.vue';
|
||||||
import type { Organization } from '@/types/models';
|
import type { Organization } from '@/types/models';
|
||||||
import type { Permissions, Role } from '@/types/jetstream';
|
import type { Permissions, Role } from '@/types/jetstream';
|
||||||
import ImportData from '@/Pages/Teams/Partials/ImportData.vue';
|
|
||||||
import { canUpdateOrganization } from '@/utils/permissions';
|
import { canUpdateOrganization } from '@/utils/permissions';
|
||||||
import OrganizationBillableRate from '@/Pages/Teams/Partials/OrganizationBillableRate.vue';
|
import OrganizationBillableRate from '@/Pages/Teams/Partials/OrganizationBillableRate.vue';
|
||||||
|
|
||||||
@@ -34,14 +33,8 @@ defineProps<{
|
|||||||
:team="team" />
|
:team="team" />
|
||||||
<SectionBorder />
|
<SectionBorder />
|
||||||
|
|
||||||
<ImportData
|
|
||||||
v-if="canUpdateOrganization()"
|
|
||||||
:team="team"></ImportData>
|
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-if="permissions.canDeleteTeam && !team.personal_team">
|
v-if="permissions.canDeleteTeam && !team.personal_team">
|
||||||
<SectionBorder />
|
|
||||||
|
|
||||||
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
|
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ export type MyMemberships = ZodiosResponseByAlias<
|
|||||||
|
|
||||||
export type MyMembership = MyMemberships[0];
|
export type MyMembership = MyMemberships[0];
|
||||||
|
|
||||||
|
export type OrganizationExportResponse = ZodiosResponseByAlias<
|
||||||
|
SolidTimeApi,
|
||||||
|
'exportOrganization'
|
||||||
|
>;
|
||||||
|
|
||||||
const api = createApiClient('/api', { validate: 'none' });
|
const api = createApiClient('/api', { validate: 'none' });
|
||||||
|
|
||||||
export { createApiClient, api };
|
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',
|
method: 'post',
|
||||||
path: '/v1/organizations/:organization/import',
|
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',
|
method: 'put',
|
||||||
path: '/v1/organizations/:organization/project-members/:projectMember',
|
path: '/v1/organizations/:organization/project-members/:projectMember',
|
||||||
|
|||||||
Reference in New Issue
Block a user