Skip to content

Commit

Permalink
feat: add in multi-edit form for submissions (#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdicarlo authored Oct 30, 2024
1 parent a2c1454 commit 3238a62
Show file tree
Hide file tree
Showing 34 changed files with 671 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IFormGenerationRequestDto, SubmissionType } from '@postybirb/types';
import { IsEnum, IsString } from 'class-validator';
import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';

export class FormGenerationRequestDto implements IFormGenerationRequestDto {
@ApiProperty()
Expand All @@ -10,4 +10,9 @@ export class FormGenerationRequestDto implements IFormGenerationRequestDto {
@ApiProperty({ enum: SubmissionType })
@IsEnum(SubmissionType)
type: SubmissionType;

@ApiProperty()
@IsOptional()
@IsBoolean()
isMultiSubmission?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class FormGeneratorController {
})
getFormForWebsite(@Body() request: FormGenerationRequestDto) {
return request.accountId === NULL_ACCOUNT_ID
? this.service.getDefaultForm(request.type)
? this.service.getDefaultForm(request.type, request.isMultiSubmission)
: this.service.generateForm(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,33 @@ export class FormGeneratorService {
}

const form = formBuilder(formModel, data);
return this.populateUserDefaults(form, request.accountId, request.type);
const formWithPopulatedDefaults = await this.populateUserDefaults(
form,
request.accountId,
request.type,
);

if (request.isMultiSubmission) {
delete formWithPopulatedDefaults.title; // Having title here just causes confusion for multi this flow
}

return formWithPopulatedDefaults;
}

/**
* Returns the default fields form.
* @param {SubmissionType} type
*/
getDefaultForm(type: SubmissionType) {
return this.populateUserDefaults(
async getDefaultForm(type: SubmissionType, isMultiSubmission = false) {
const form = await this.populateUserDefaults(
formBuilder(new DefaultWebsiteOptions(), {}),
new NullAccount().id,
type,
);
if (isMultiSubmission) {
delete form.title; // Having title here just causes confusion for multi this flow
}
return form;
}

private async populateUserDefaults(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IApplyMultiSubmissionDto, SubmissionId } from '@postybirb/types';
import { IsArray, IsBoolean, IsNotEmpty, IsString } from 'class-validator';

export class ApplyMultiSubmissionDto implements IApplyMultiSubmissionDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
originId: SubmissionId;

@ApiProperty()
@IsArray()
@IsString({ each: true })
@IsNotEmpty()
submissionIds: SubmissionId[];

@ApiProperty()
@IsBoolean()
merge: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ export class CreateSubmissionDto implements ICreateSubmissionDto {
@IsOptional()
@IsBoolean()
isTemplate?: boolean;

@ApiProperty()
@IsOptional()
@IsBoolean()
isMultiSubmission?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Inject,
Injectable,
NotFoundException,
OnModuleInit,
Optional,
forwardRef,
} from '@nestjs/common';
Expand Down Expand Up @@ -42,6 +43,7 @@ import { MulterFileInfo } from '../../file/models/multer-file-info';
import { IsTestEnvironment } from '../../utils/test.util';
import { WSGateway } from '../../web-socket/web-socket-gateway';
import { WebsiteOptionsService } from '../../website-options/website-options.service';
import { ApplyMultiSubmissionDto } from '../dtos/apply-multi-submission.dto';
import { CreateSubmissionDto } from '../dtos/create-submission.dto';
import { UpdateSubmissionTemplateNameDto } from '../dtos/update-submission-template-name.dto';
import { UpdateSubmissionDto } from '../dtos/update-submission.dto';
Expand All @@ -55,7 +57,10 @@ type SubmissionEntity = Submission<SubmissionMetadataType>;
* @class SubmissionService
*/
@Injectable()
export class SubmissionService extends PostyBirbService<SubmissionEntity> {
export class SubmissionService
extends PostyBirbService<SubmissionEntity>
implements OnModuleInit
{
constructor(
dbSubscriber: DatabaseUpdateSubscriber,
@InjectRepository(Submission)
Expand Down Expand Up @@ -86,6 +91,12 @@ export class SubmissionService extends PostyBirbService<SubmissionEntity> {
);
}

onModuleInit() {
Object.values(SubmissionType).forEach((type) => {
this.populateMultiSubmission(type);
});
}

/**
* Emits submissions onto websocket.
*/
Expand Down Expand Up @@ -116,6 +127,18 @@ export class SubmissionService extends PostyBirbService<SubmissionEntity> {
);
}

private async populateMultiSubmission(type: SubmissionType) {
const existing = await this.repository.findOne({
type,
metadata: { isMultiSubmission: true },
});
if (existing) {
return;
}

await this.create({ name: type, type, isMultiSubmission: true });
}

/**
* Creates a submission.
*
Expand Down Expand Up @@ -144,6 +167,11 @@ export class SubmissionService extends PostyBirbService<SubmissionEntity> {
};
}

if (createSubmissionDto.isMultiSubmission) {
submission.metadata.isMultiSubmission = true;
submission.id = `MULTI-${submission.type}`;
}

let name = 'New submission';
if (createSubmissionDto.name) {
name = createSubmissionDto.name;
Expand Down Expand Up @@ -177,7 +205,10 @@ export class SubmissionService extends PostyBirbService<SubmissionEntity> {
}

case SubmissionType.FILE: {
if (createSubmissionDto.isTemplate) {
if (
createSubmissionDto.isTemplate ||
createSubmissionDto.isMultiSubmission
) {
// Don't need to populate on a template
break;
}
Expand Down Expand Up @@ -341,6 +372,67 @@ export class SubmissionService extends PostyBirbService<SubmissionEntity> {
this.emit();
}

async applyMultiSubmission(applyMultiSubmissionDto: ApplyMultiSubmissionDto) {
const { originId, submissionIds, merge } = applyMultiSubmissionDto;
const origin = await this.repository.findOneOrFail({ id: originId });
const submissions = await this.repository.find({
id: { $in: submissionIds },
});
if (merge) {
// Keeps unique options, overwrites overlapping options\
// eslint-disable-next-line no-restricted-syntax
for (const submission of submissions) {
// eslint-disable-next-line no-restricted-syntax
for (const option of origin.options.getItems()) {
const existingOption = submission.options
.getItems()
.find((o) => o.account.id === option.account.id);
if (existingOption) {
// Don't overwrite set title
const opts = { ...option.data, title: existingOption.data.title };
existingOption.data = opts;
} else {
submission.options.add(
await this.websiteOptionsService.createOption(
submission,
option.account.id,
option.data,
option.isDefault ? undefined : option.data.title,
),
);
}
}
}
} else {
// Removes all options not included in the origin submission
// eslint-disable-next-line no-restricted-syntax
for (const submission of submissions) {
const items = submission.options.getItems();
const defaultOptions = items.find((option) => option.isDefault);
const defaultTitle = defaultOptions?.data.title;
submission.options.removeAll();
// eslint-disable-next-line no-restricted-syntax
for (const option of origin.options.getItems()) {
const opts = { ...option.data };
if (option.isDefault) {
opts.title = defaultTitle;
}
submission.options.add(
await this.websiteOptionsService.createOption(
submission,
option.account.id,
opts,
option.isDefault ? defaultTitle : option.data.title,
),
);
}
}
}

await this.repository.persistAndFlush(submissions);
this.emit();
}

/**
* Duplicates a submission.
* !Somewhat janky method of doing a clone.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { parse } from 'path';
import { PostyBirbController } from '../common/controller/postybirb-controller';
import { Submission } from '../database/entities';
import { MulterFileInfo } from '../file/models/multer-file-info';
import { ApplyMultiSubmissionDto } from './dtos/apply-multi-submission.dto';
import { CreateSubmissionDto } from './dtos/create-submission.dto';
import { UpdateSubmissionTemplateNameDto } from './dtos/update-submission-template-name.dto';
import { UpdateSubmissionDto } from './dtos/update-submission.dto';
Expand Down Expand Up @@ -115,4 +116,11 @@ export class SubmissionController extends PostyBirbController<Submission> {
.updateTemplateName(id, updateSubmissionDto)
.then((entity) => entity.toJSON());
}

@Patch('apply/multi')
@ApiOkResponse({ description: 'Submission applied to multiple submissions.' })
@ApiNotFoundResponse({ description: 'Submission Id not found.' })
async applyMulti(@Body() applyMultiSubmissionDto: ApplyMultiSubmissionDto) {
return this.service.applyMultiSubmission(applyMultiSubmissionDto);
}
}
7 changes: 0 additions & 7 deletions apps/client-server/src/app/validation/validation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export class ValidationService {
public async validateSubmission(
submission: ISubmission,
): Promise<ValidationResult[]> {
this.logger.debug(`Validating submission ${submission.id}`);
return Promise.all(
submission.options.map((website) => this.validate(submission, website)),
);
Expand All @@ -58,12 +57,6 @@ export class ValidationService {
websiteOption: IWebsiteOptions,
): Promise<ValidationResult> {
try {
this.logger.debug(
`Validating submission ${submission.id} with website ${websiteOption.account.id} (${websiteOption.id})`,
);
// TODO figure out why the instances cannot be found
// TODO consider removing the debounce you added to the submission service since the behavior is scuffed visually on the ui
// this will mean fixing the underlying issue of insertion time
const website = websiteOption.isDefault
? new DefaultWebsite(websiteOption.account)
: this.websiteRegistry.findInstance(websiteOption.account);
Expand Down
5 changes: 5 additions & 0 deletions apps/postybirb-ui/src/api/submission.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
IApplyMultiSubmissionDto,
ICreateSubmissionDto,
ISubmissionDto,
IUpdateSubmissionDto,
Expand Down Expand Up @@ -44,6 +45,10 @@ class SubmissionsApi extends BaseApi<
reorder(id: SubmissionId, index: number) {
return this.client.patch(`reorder/${id}/${index}`);
}

applyToMultipleSubmissions(dto: IApplyMultiSubmissionDto) {
return this.client.patch('apply/multi', dto);
}
}

export default new SubmissionsApi();
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function LoginPanel(props: Omit<WebsiteLoginPanelProps, 'onClose'>) {
}

return (
// eslint-disable-next-line lingui/no-unlocalized-strings
<Box h="calc(100% - 50px)" p="sm">
{loginMethod}
</Box>
Expand Down
1 change: 1 addition & 0 deletions apps/postybirb-ui/src/components/form/fields/tag-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function TagField(props: FormFieldProps<TagFieldType>): JSX.Element {
/>
)}
<TagsInput
inputWrapperOrder={['label', 'input', 'description', 'error']}
clearable
required={field.required}
value={tagValue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ function InnerForm({
typeof a.field.row === 'number' &&
typeof b.field.row === 'number'
? a.field.row - b.field.row
: 0
: 0,
)
.map((entry) => (
<Field
Expand Down Expand Up @@ -220,7 +220,11 @@ export function WebsiteOptionForm(props: WebsiteOptionFormProps) {
`website-option-${option.id}`,
() =>
formGeneratorApi
.getForm({ accountId: account, type: submission.type })
.getForm({
accountId: account,
type: submission.type,
isMultiSubmission: submission.isMultiSubmission(),
})
.then((res) => res.body),
);
const defaultOption = submission.getDefaultOptions();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Trans } from '@lingui/macro';
import { ComboboxItemGroup, MultiSelect } from '@mantine/core';
import { ComboboxItemGroup, Group, MultiSelect } from '@mantine/core';
import { IAccountDto, NULL_ACCOUNT_ID } from '@postybirb/types';
import { IconCheck } from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { useWebsites } from '../../../hooks/account/use-websites';
import { SubmissionDto } from '../../../models/dtos/submission.dto';
Expand All @@ -21,7 +22,7 @@ export function WebsiteSelect(props: WebsiteSelectProps) {
filteredAccounts.map((website) => ({
group: website.displayName,
items: website.accounts.map((account) => ({
label: account.name,
label: `[${website.displayName}] ${account.name}`,
value: account.id,
})),
})),
Expand Down Expand Up @@ -57,6 +58,15 @@ export function WebsiteSelect(props: WebsiteSelectProps) {
onCommitChanges(selectedAccounts, true);
setIsOpen(false);
}}
renderOption={(item) => {
const label = item.option.label.split('] ')[1];
return (
<Group>
{item.checked ? <IconCheck /> : null}
{label}
</Group>
);
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export default function TemplatePickerModal(props: TemplatePickerModalProps) {
// On first option pick
if (!selectedWebsiteOptions && newOpts.length) {
const sub: Record<AccountId, WebsiteOptionsDto> = {};
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const template = [...templates, ...submissions].find(
(t) => t.id === newOpts[0],
);
Expand Down
Loading

0 comments on commit 3238a62

Please sign in to comment.