F: Improve versioning UI
Some checks failed
Frontend CI / build (22.x) (push) Waiting to run
Backend CI / build (3.12) (push) Has been cancelled

This commit is contained in:
Ivan 2024-08-24 12:29:38 +03:00
parent c897966886
commit 8bceeb2b38
12 changed files with 70 additions and 15 deletions

View File

@ -64,10 +64,12 @@ class VersionInnerSerializer(serializers.ModelSerializer):
class VersionCreateSerializer(serializers.ModelSerializer): class VersionCreateSerializer(serializers.ModelSerializer):
''' Serializer: Version create data. ''' ''' Serializer: Version create data. '''
items = PKField(many=True, required=False, default=None, queryset=Constituenta.objects.all().only('pk'))
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
model = Version model = Version
fields = 'version', 'description' fields = 'version', 'description', 'items'
class LibraryItemDetailsSerializer(serializers.ModelSerializer): class LibraryItemDetailsSerializer(serializers.ModelSerializer):

View File

@ -6,6 +6,7 @@ from zipfile import ZipFile
from rest_framework import status from rest_framework import status
from apps.library.models import Version
from apps.rsform.models import Constituenta, RSForm from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
@ -41,6 +42,20 @@ class TestVersionViews(EndpointTester):
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']]) self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
@decl_endpoint('/api/library/{schema}/create-version', method='post')
def test_create_version_filter(self):
x2 = self.owned.insert_new('X2')
data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]}
response = self.executeCreated(data=data, schema=self.owned_id)
version = Version.objects.get(pk=response.data['version'])
items = version.data['items']
self.assertTrue('version' in response.data)
self.assertTrue('schema' in response.data)
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
self.assertEqual(len(items), 1)
self.assertEqual(items[0]['id'], x2.pk)
@decl_endpoint('/api/library/{schema}/versions/{version}', method='get') @decl_endpoint('/api/library/{schema}/versions/{version}', method='get')
def test_retrieve_version(self): def test_retrieve_version(self):
version_id = self._create_version({'version': '1.0.0', 'description': 'test'}) version_id = self._create_version({'version': '1.0.0', 'description': 'test'})

View File

@ -103,6 +103,9 @@ def create_version(request: Request, pk_item: int) -> HttpResponse:
version_input = s.VersionCreateSerializer(data=request.data) version_input = s.VersionCreateSerializer(data=request.data)
version_input.is_valid(raise_exception=True) version_input.is_valid(raise_exception=True)
data = RSFormSerializer(item).to_versioned_data() data = RSFormSerializer(item).to_versioned_data()
items: list[int] = [] if 'items' not in request.data else request.data['items']
if items:
data['items'] = [cst for cst in data['items'] if cst['id'] in items]
result = RSForm(item).create_version( result = RSForm(item).create_version(
version=version_input.validated_data['version'], version=version_input.validated_data['version'],
description=version_input.validated_data['description'], description=version_input.validated_data['description'],

View File

@ -9,7 +9,7 @@ import {
IRenameLocationData, IRenameLocationData,
ITargetAccessPolicy, ITargetAccessPolicy,
ITargetLocation, ITargetLocation,
IVersionData IVersionCreateData
} from '@/models/library'; } from '@/models/library';
import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform'; import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
import { ITargetUser, ITargetUsers } from '@/models/user'; import { ITargetUser, ITargetUsers } from '@/models/user';
@ -109,7 +109,7 @@ export function patchSetEditors(target: string, request: FrontPush<ITargetUsers>
}); });
} }
export function postCreateVersion(target: string, request: FrontExchange<IVersionData, IVersionCreatedResponse>) { export function postCreateVersion(target: string, request: FrontExchange<IVersionCreateData, IVersionCreatedResponse>) {
AxiosPost({ AxiosPost({
endpoint: `/api/library/${target}/create-version`, endpoint: `/api/library/${target}/create-version`,
request: request request: request

View File

@ -92,7 +92,7 @@ export { LuView as IconDBStructure } from 'react-icons/lu';
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu'; export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
export { LuImage as IconImage } from 'react-icons/lu'; export { LuImage as IconImage } from 'react-icons/lu';
export { TbColumns as IconList } from 'react-icons/tb'; export { TbColumns as IconList } from 'react-icons/tb';
export { ImStack as IconVersions } from 'react-icons/im'; export { GoVersions as IconVersions } from 'react-icons/go';
export { TbColumnsOff as IconListOff } from 'react-icons/tb'; export { TbColumnsOff as IconListOff } from 'react-icons/tb';
export { LuAtSign as IconTerm } from 'react-icons/lu'; export { LuAtSign as IconTerm } from 'react-icons/lu';
export { LuSubscript as IconAlias } from 'react-icons/lu'; export { LuSubscript as IconAlias } from 'react-icons/lu';
@ -122,6 +122,7 @@ export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi';
export { TbHexagonPlus2 as IconNewRSForm } from 'react-icons/tb'; export { TbHexagonPlus2 as IconNewRSForm } from 'react-icons/tb';
export { BiPlusCircle as IconNewItem } from 'react-icons/bi'; export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6'; export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
export { PiStackPlus as IconNewVersion } from 'react-icons/pi';
export { BiDuplicate as IconClone } from 'react-icons/bi'; export { BiDuplicate as IconClone } from 'react-icons/bi';
export { LuReplace as IconReplace } from 'react-icons/lu'; export { LuReplace as IconReplace } from 'react-icons/lu';
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa'; export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';

View File

@ -3,30 +3,38 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import Checkbox from '@/components/ui/Checkbox';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { IVersionData, IVersionInfo } from '@/models/library'; import { IVersionCreateData, IVersionInfo } from '@/models/library';
import { nextVersion } from '@/models/libraryAPI'; import { nextVersion } from '@/models/libraryAPI';
import { ConstituentaID } from '@/models/rsform';
interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> { interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> {
versions: IVersionInfo[]; versions: IVersionInfo[];
onCreate: (data: IVersionData) => void; onCreate: (data: IVersionCreateData) => void;
selected: ConstituentaID[];
totalCount: number;
} }
function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionProps) { function DlgCreateVersion({ hideWindow, versions, selected, totalCount, onCreate }: DlgCreateVersionProps) {
const [version, setVersion] = useState(versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0'); const [version, setVersion] = useState(versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [onlySelected, setOnlySelected] = useState(false);
const canSubmit = useMemo(() => { const canSubmit = useMemo(() => {
return !versions.find(ver => ver.version === version); return !versions.find(ver => ver.version === version);
}, [versions, version]); }, [versions, version]);
function handleSubmit() { function handleSubmit() {
const data: IVersionData = { const data: IVersionCreateData = {
version: version, version: version,
description: description description: description
}; };
if (onlySelected) {
data.items = selected;
}
onCreate(data); onCreate(data);
} }
@ -55,6 +63,12 @@ function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionPr
value={description} value={description}
onChange={event => setDescription(event.target.value)} onChange={event => setDescription(event.target.value)}
/> />
<Checkbox
id='dlg_only_selected'
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
value={onlySelected}
setValue={value => setOnlySelected(value)}
/>
</Modal> </Modal>
); );
} }

View File

@ -2,6 +2,7 @@
* Module: Models for LibraryItem. * Module: Models for LibraryItem.
*/ */
import { ConstituentaID } from './rsform';
import { UserID } from './user'; import { UserID } from './user';
/** /**
@ -52,10 +53,17 @@ export interface IVersionInfo {
} }
/** /**
* Represents user data, intended to create or update version metadata in persistent storage. * Represents version data, intended to update version metadata in persistent storage.
*/ */
export interface IVersionData extends Omit<IVersionInfo, 'id' | 'time_create'> {} export interface IVersionData extends Omit<IVersionInfo, 'id' | 'time_create'> {}
/**
* Create version metadata in persistent storage.
*/
export interface IVersionCreateData extends IVersionData {
items?: ConstituentaID[];
}
/** /**
* Represents library item common data typical for all item types. * Represents library item common data typical for all item types.
*/ */

View File

@ -1,4 +1,4 @@
import { IconEditor, IconNewItem, IconShare, IconUpload, IconVersions } from '@/components/Icons'; import { IconEditor, IconNewVersion, IconShare, IconUpload, IconVersions } from '@/components/Icons';
function HelpVersions() { function HelpVersions() {
return ( return (
@ -18,7 +18,7 @@ function HelpVersions() {
<IconUpload size='1.25rem' className='inline-icon icon-red' /> Загрузить версию в актуальную схему <IconUpload size='1.25rem' className='inline-icon icon-red' /> Загрузить версию в актуальную схему
</li> </li>
<li> <li>
<IconNewItem size='1.25rem' className='inline-icon icon-green' /> Создать версию можно только из актуальной <IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Создать версию можно только из актуальной
схемы схемы
</li> </li>

View File

@ -8,6 +8,7 @@ import {
IconEdit2, IconEdit2,
IconEditor, IconEditor,
IconMenu, IconMenu,
IconNewVersion,
IconOwner, IconOwner,
IconReader, IconReader,
IconShare, IconShare,
@ -53,6 +54,9 @@ function HelpRSMenu() {
<li> <li>
<IconClone className='inline-icon icon-green' /> Клонировать создать копию схемы <IconClone className='inline-icon icon-green' /> Клонировать создать копию схемы
</li> </li>
<li>
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Сохранить версию
</li>
<li> <li>
<IconDownload className='inline-icon' /> Выгрузить сохранить в файле формата Экстеор <IconDownload className='inline-icon' /> Выгрузить сохранить в файле формата Экстеор
</li> </li>

View File

@ -1,4 +1,4 @@
import { IconNewItem, IconUpload, IconVersions } from '@/components/Icons'; import { IconNewVersion, IconUpload, IconVersions } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
@ -33,7 +33,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'} titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable}
onClick={controller.createVersion} onClick={controller.createVersion}
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewVersion size='1.25rem' className='icon-green' />}
/> />
<MiniButton <MiniButton
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'} title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}

View File

@ -16,6 +16,7 @@ import {
IconLibrary, IconLibrary,
IconMenu, IconMenu,
IconNewItem, IconNewItem,
IconNewVersion,
IconOSS, IconOSS,
IconOwner, IconOwner,
IconReader, IconReader,
@ -156,6 +157,12 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleClone} onClick={handleClone}
/> />
) : null} ) : null}
<DropdownButton
text='Сохранить версию'
disabled={!controller.isContentEditable}
onClick={controller.createVersion}
icon={<IconNewVersion size='1rem' className='icon-green' />}
/>
<DropdownButton <DropdownButton
text='Выгрузить в Экстеор' text='Выгрузить в Экстеор'
icon={<IconDownload size='1rem' className='icon-primary' />} icon={<IconDownload size='1rem' className='icon-primary' />}

View File

@ -335,9 +335,8 @@ export const RSEditState = ({
if (!model.schema) { if (!model.schema) {
return; return;
} }
model.versionCreate(data, newVersion => { model.versionCreate(data, () => {
toast.success(information.newVersion(data.version)); toast.success(information.newVersion(data.version));
viewVersion(newVersion);
}); });
}, },
[model, viewVersion] [model, viewVersion]
@ -725,6 +724,8 @@ export const RSEditState = ({
versions={model.schema.versions} versions={model.schema.versions}
hideWindow={() => setShowCreateVersion(false)} hideWindow={() => setShowCreateVersion(false)}
onCreate={handleCreateVersion} onCreate={handleCreateVersion}
selected={selected}
totalCount={model.schema.items.length}
/> />
) : null} ) : null}
{showEditVersions ? ( {showEditVersions ? (