chore: empty state loaders and filter cancel state update for modules and cycles (#3653)

This commit is contained in:
guru_sainath 2024-02-13 20:58:44 +05:30 committed by GitHub
parent 571b89632c
commit 25a2816a76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 244 additions and 211 deletions

View File

@ -36,22 +36,34 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
if (!value) { if (!value) {
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { updateFilters(
[key]: null, workspaceSlug,
}); projectId,
EIssueFilterType.FILTERS,
{
[key]: null,
},
cycleId
);
return; return;
} }
let newValues = issueFilters?.filters?.[key] ?? []; let newValues = issueFilters?.filters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value); newValues = newValues.filter((val) => val !== value);
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { updateFilters(
[key]: newValues, workspaceSlug,
}); projectId,
EIssueFilterType.FILTERS,
{
[key]: newValues,
},
cycleId
);
}; };
const handleClearAllFilters = () => { const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId || !cycleId) return;
const newFilters: IIssueFilterOptions = {}; const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => { Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null; newFilters[key as keyof IIssueFilterOptions] = null;

View File

@ -33,24 +33,36 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
}); });
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId || !moduleId) return;
if (!value) { if (!value) {
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { updateFilters(
[key]: null, workspaceSlug,
}); projectId,
EIssueFilterType.FILTERS,
{
[key]: null,
},
moduleId
);
return; return;
} }
let newValues = issueFilters?.filters?.[key] ?? []; let newValues = issueFilters?.filters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value); newValues = newValues.filter((val) => val !== value);
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { updateFilters(
[key]: newValues, workspaceSlug,
}); projectId,
EIssueFilterType.FILTERS,
{
[key]: newValues,
},
moduleId
);
}; };
const handleClearAllFilters = () => { const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId || !moduleId) return;
const newFilters: IIssueFilterOptions = {}; const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => { Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null; newFilters[key as keyof IIssueFilterOptions] = null;

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from "react"; import React, { Fragment, useCallback, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -181,58 +181,58 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
return <SpreadsheetLayoutLoader />; return <SpreadsheetLayoutLoader />;
} }
if (issueIds.length === 0) {
return (
<EmptyState
image={emptyStateImage}
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
description={
(workspaceProjectIds ?? []).length > 0
? currentViewDetails.description
: "To create issues or manage your work, you need to create a project or be a part of one."
}
size="sm"
primaryButton={
(workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed"
? {
text: "Create new issue",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}
: undefined
: {
text: "Start your first project",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateProjectModal(true);
},
}
}
disabled={!isEditingAllowed}
/>
);
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
<div className="relative h-full w-full overflow-auto"> <div className="relative h-full w-full overflow-auto">
<SpreadsheetView <GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
displayProperties={issueFilters?.displayProperties ?? {}} {issueIds.length === 0 ? (
displayFilters={issueFilters?.displayFilters ?? {}} <EmptyState
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} image={emptyStateImage}
issueIds={issueIds} title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
quickActions={renderQuickActions} description={
handleIssues={handleIssues} (workspaceProjectIds ?? []).length > 0
canEditProperties={canEditProperties} ? currentViewDetails.description
viewId={globalViewId} : "To create issues or manage your work, you need to create a project or be a part of one."
/> }
size="sm"
primaryButton={
(workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed"
? {
text: "Create new issue",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}
: undefined
: {
text: "Start your first project",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateProjectModal(true);
},
}
}
disabled={!isEditingAllowed}
/>
) : (
<Fragment>
<SpreadsheetView
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds}
quickActions={renderQuickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
viewId={globalViewId}
/>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
</div> </div>
{/* peek overview */}
<IssuePeekOverview />
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { Fragment } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -40,22 +40,23 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
return <ListLayoutLoader />; return <ListLayoutLoader />;
} }
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectArchivedEmptyState />
</div>
);
}
if (!workspaceSlug || !projectId) return <></>; if (!workspaceSlug || !projectId) return <></>;
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ArchivedIssueAppliedFiltersRoot /> <ArchivedIssueAppliedFiltersRoot />
<div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout /> {issues?.groupedIssueIds?.length === 0 ? (
</div> <div className="relative h-full w-full overflow-y-auto">
<IssuePeekOverview is_archived /> <ProjectArchivedEmptyState />
</div>
) : (
<Fragment>
<div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
<IssuePeekOverview is_archived />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { Fragment, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -69,42 +69,41 @@ export const CycleLayoutRoot: React.FC = observer(() => {
); );
} }
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<CycleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId.toString()}
activeLayout={activeLayout}
/>
</div>
);
}
return ( return (
<> <>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> <TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />} {cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleAppliedFiltersRoot /> <CycleAppliedFiltersRoot />
<div className="h-full w-full overflow-auto"> {issues?.groupedIssueIds?.length === 0 ? (
{activeLayout === "list" ? ( <div className="relative h-full w-full overflow-y-auto">
<CycleListLayout /> <CycleEmptyState
) : activeLayout === "kanban" ? ( workspaceSlug={workspaceSlug.toString()}
<CycleKanBanLayout /> projectId={projectId.toString()}
) : activeLayout === "calendar" ? ( cycleId={cycleId.toString()}
<CycleCalendarLayout /> activeLayout={activeLayout}
) : activeLayout === "gantt_chart" ? ( />
<CycleGanttLayout /> </div>
) : activeLayout === "spreadsheet" ? ( ) : (
<CycleSpreadsheetLayout /> <Fragment>
) : null} <div className="h-full w-full overflow-auto">
</div> {activeLayout === "list" ? (
{/* peek overview */} <CycleListLayout />
<IssuePeekOverview /> ) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
</div> </div>
</> </>
); );

View File

@ -55,22 +55,25 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
); );
} }
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectDraftEmptyState />
</div>
);
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<DraftIssueAppliedFiltersRoot /> <DraftIssueAppliedFiltersRoot />
<div className="relative h-full w-full overflow-auto"> {issues?.groupedIssueIds?.length === 0 ? (
{activeLayout === "list" ? <DraftIssueListLayout /> : activeLayout === "kanban" ? <DraftKanBanLayout /> : null} <div className="relative h-full w-full overflow-y-auto">
{/* issue peek overview */} <ProjectDraftEmptyState />
<IssuePeekOverview /> </div>
</div> ) : (
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<DraftIssueListLayout />
) : activeLayout === "kanban" ? (
<DraftKanBanLayout />
) : null}
{/* issue peek overview */}
<IssuePeekOverview />
</div>
)}
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { Fragment } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -63,38 +63,38 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
); );
} }
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ModuleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId.toString()}
activeLayout={activeLayout}
/>
</div>
);
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ModuleAppliedFiltersRoot /> <ModuleAppliedFiltersRoot />
<div className="h-full w-full overflow-auto"> {issues?.groupedIssueIds?.length === 0 ? (
{activeLayout === "list" ? ( <div className="relative h-full w-full overflow-y-auto">
<ModuleListLayout /> <ModuleEmptyState
) : activeLayout === "kanban" ? ( workspaceSlug={workspaceSlug.toString()}
<ModuleKanBanLayout /> projectId={projectId.toString()}
) : activeLayout === "calendar" ? ( moduleId={moduleId.toString()}
<ModuleCalendarLayout /> activeLayout={activeLayout}
) : activeLayout === "gantt_chart" ? ( />
<ModuleGanttLayout /> </div>
) : activeLayout === "spreadsheet" ? ( ) : (
<ModuleSpreadsheetLayout /> <Fragment>
) : null} <div className="h-full w-full overflow-auto">
</div> {activeLayout === "list" ? (
{/* peek overview */} <ModuleListLayout />
<IssuePeekOverview /> ) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, Fragment } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -48,40 +48,38 @@ export const ProjectLayoutRoot: FC = observer(() => {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>; return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
} }
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectEmptyState />
</div>
);
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectAppliedFiltersRoot /> <ProjectAppliedFiltersRoot />
<div className="relative h-full w-full overflow-auto bg-custom-background-90"> {issues?.groupedIssueIds?.length === 0 ? (
{/* mutation loader */} <ProjectEmptyState />
{issues?.loader === "mutation" && ( ) : (
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded"> <Fragment>
<Spinner className="w-4 h-4" /> <div className="relative h-full w-full overflow-auto bg-custom-background-90">
{/* mutation loader */}
{issues?.loader === "mutation" && (
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
<Spinner className="w-4 h-4" />
</div>
)}
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div> </div>
)}
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
{/* peek overview */} {/* peek overview */}
<IssuePeekOverview /> <IssuePeekOverview />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import React, { Fragment, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -79,34 +79,34 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
); );
} }
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectViewEmptyState />
</div>
);
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectViewAppliedFiltersRoot /> <ProjectViewAppliedFiltersRoot />
<div className="relative h-full w-full overflow-auto"> {issues?.groupedIssueIds?.length === 0 ? (
{activeLayout === "list" ? ( <div className="relative h-full w-full overflow-y-auto">
<ProjectViewListLayout issueActions={issueActions} /> <ProjectViewEmptyState />
) : activeLayout === "kanban" ? ( </div>
<ProjectViewKanBanLayout issueActions={issueActions} /> ) : (
) : activeLayout === "calendar" ? ( <Fragment>
<ProjectViewCalendarLayout issueActions={issueActions} /> <div className="relative h-full w-full overflow-auto">
) : activeLayout === "gantt_chart" ? ( {activeLayout === "list" ? (
<ProjectViewGanttLayout issueActions={issueActions} /> <ProjectViewListLayout issueActions={issueActions} />
) : activeLayout === "spreadsheet" ? ( ) : activeLayout === "kanban" ? (
<ProjectViewSpreadsheetLayout issueActions={issueActions} /> <ProjectViewKanBanLayout issueActions={issueActions} />
) : null} ) : activeLayout === "calendar" ? (
</div> <ProjectViewCalendarLayout issueActions={issueActions} />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout issueActions={issueActions} />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout issueActions={issueActions} />
) : null}
</div>
{/* peek overview */} {/* peek overview */}
<IssuePeekOverview /> <IssuePeekOverview />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -67,10 +67,11 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
const orderBy = displayFilters?.order_by; const orderBy = displayFilters?.order_by;
const layout = displayFilters?.layout; const layout = displayFilters?.layout;
const archivedIssueIds = this.issues[projectId] ?? []; const archivedIssueIds = this.issues[projectId];
if (!archivedIssueIds) return undefined;
const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds); const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds);
if (!_issues) return undefined; if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;

View File

@ -78,10 +78,11 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
const orderBy = displayFilters?.order_by; const orderBy = displayFilters?.order_by;
const layout = displayFilters?.layout; const layout = displayFilters?.layout;
const draftIssueIds = this.issues[projectId] ?? []; const draftIssueIds = this.issues[projectId];
if (!draftIssueIds) return undefined;
const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds); const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds);
if (!_issues) return undefined; if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;

View File

@ -5012,7 +5012,7 @@ fault@^2.0.0:
dependencies: dependencies:
format "^0.2.0" format "^0.2.0"
fflate@^0.4.1: fflate@^0.4.8:
version "0.4.8" version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
@ -7171,12 +7171,18 @@ postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
posthog-js@^1.88.4: posthog-js@^1.105.0:
version "1.96.1" version "1.105.8"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.96.1.tgz#4f9719a24e4e14037b0e72d430194d7cdb576447" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.105.8.tgz#934602f0c7a5e522a25828062b5841ad8780756f"
integrity sha512-kv1vQqYMt2BV3YHS+wxsbGuP+tz+M3y1AzNhz8TfkpY1HT8W/ONT0i0eQpeRr9Y+d4x/fZ6M4cXG5GMvi9lRCA== integrity sha512-zKZKNVLLQQgkJyY3DnzHHTasu6x4xM4MOvH7UbMz6BmrgUPboS6/3akgz+WKD+JV6qFj68bm80iJw0Jtj+pt8Q==
dependencies: dependencies:
fflate "^0.4.1" fflate "^0.4.8"
preact "^10.19.3"
preact@^10.19.3:
version "10.19.4"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.4.tgz#735d331d5b1bd2182cc36f2ba481fd6f0da3fe3b"
integrity sha512-dwaX5jAh0Ga8uENBX1hSOujmKWgx9RtL80KaKUFLc6jb4vCEAc3EeZ0rnQO/FO4VgjfPMfoLFWnNG8bHuZ9VLw==
prebuild-install@^7.1.1: prebuild-install@^7.1.1:
version "7.1.1" version "7.1.1"