Skip to content

Commit

Permalink
Merge pull request #119 from CentreForDigitalHumanities/feature/save-…
Browse files Browse the repository at this point in the history
…agent-form

Feature/save agent form
  • Loading branch information
lukavdplas authored Sep 18, 2024
2 parents 5499b63 + 18f9e74 commit ed377d9
Show file tree
Hide file tree
Showing 29 changed files with 620 additions and 68 deletions.
65 changes: 59 additions & 6 deletions frontend/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,20 @@ export type DataEntryAgentQueryVariables = Exact<{

export type DataEntryAgentQuery = { __typename?: 'Query', agentDescription?: { __typename?: 'AgentDescriptionType', id: string, name: string, description: string, isGroup: boolean, source: { __typename?: 'SourceType', id: string, name: string } } | null };

export type DataEntryUpdateAgentMutationVariables = Exact<{
input: UpdateAgentInput;
}>;


export type DataEntryUpdateAgentMutation = { __typename?: 'Mutation', updateAgent?: { __typename?: 'UpdateAgentMutation', ok: boolean, errors: Array<{ __typename?: 'LettercraftErrorType', field: string, messages: Array<string> }> } | null };

export type DataEntryDeleteAgentMutationVariables = Exact<{
id: Scalars['ID']['input'];
}>;


export type DataEntryDeleteAgentMutation = { __typename?: 'Mutation', deleteAgent?: { __typename?: 'DeleteAgentMutation', ok: boolean, errors: Array<{ __typename?: 'LettercraftErrorType', messages: Array<string>, field: string }> } | null };

export type DataEntryEpisodeContentsQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
Expand Down Expand Up @@ -1263,7 +1277,7 @@ export type DataEntrySourceDetailQueryVariables = Exact<{
}>;


export type DataEntrySourceDetailQuery = { __typename?: 'Query', source: { __typename?: 'SourceType', id: string, name: string, editionAuthor: string, editionTitle: string, medievalAuthor: string, medievalTitle: string, numOfEpisodes: number, episodes: Array<{ __typename?: 'EpisodeType', id: string, name: string, description: string, summary: string, book: string, chapter: string, page: string, contributors: Array<{ __typename?: 'UserType', id: string, fullName: string }>, agents: Array<{ __typename?: 'AgentDescriptionType', id: string, name: string, isGroup: boolean, describes?: Array<{ __typename?: 'HistoricalPersonType', id: string, identifiable: boolean } | null> | null }>, gifts: Array<{ __typename?: 'GiftDescriptionType', id: string, name: string }>, letters: Array<{ __typename?: 'LetterDescriptionType', id: string, name: string }>, spaces: Array<{ __typename?: 'SpaceDescriptionType', id: string, name: string }> }> } };
export type DataEntrySourceDetailQuery = { __typename?: 'Query', source: { __typename?: 'SourceType', id: string, name: string, episodes: Array<{ __typename?: 'EpisodeType', id: string, name: string, description: string, summary: string, book: string, chapter: string, page: string, contributors: Array<{ __typename?: 'UserType', id: string, fullName: string }>, agents: Array<{ __typename?: 'AgentDescriptionType', id: string, name: string, isGroup: boolean, describes?: Array<{ __typename?: 'HistoricalPersonType', id: string, identifiable: boolean } | null> | null }>, gifts: Array<{ __typename?: 'GiftDescriptionType', id: string, name: string }>, letters: Array<{ __typename?: 'LetterDescriptionType', id: string, name: string }>, spaces: Array<{ __typename?: 'SpaceDescriptionType', id: string, name: string }> }> } };

export type DataEntrySourceListQueryVariables = Exact<{ [key: string]: never; }>;

Expand Down Expand Up @@ -1393,6 +1407,50 @@ export const DataEntryAgentDocument = gql`
export class DataEntryAgentGQL extends Apollo.Query<DataEntryAgentQuery, DataEntryAgentQueryVariables> {
override document = DataEntryAgentDocument;

constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const DataEntryUpdateAgentDocument = gql`
mutation DataEntryUpdateAgent($input: UpdateAgentInput!) {
updateAgent(agentData: $input) {
ok
errors {
field
messages
}
}
}
`;

@Injectable({
providedIn: 'root'
})
export class DataEntryUpdateAgentGQL extends Apollo.Mutation<DataEntryUpdateAgentMutation, DataEntryUpdateAgentMutationVariables> {
override document = DataEntryUpdateAgentDocument;

constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const DataEntryDeleteAgentDocument = gql`
mutation DataEntryDeleteAgent($id: ID!) {
deleteAgent(id: $id) {
ok
errors {
messages
field
}
}
}
`;

@Injectable({
providedIn: 'root'
})
export class DataEntryDeleteAgentGQL extends Apollo.Mutation<DataEntryDeleteAgentMutation, DataEntryDeleteAgentMutationVariables> {
override document = DataEntryDeleteAgentDocument;

constructor(apollo: Apollo.Apollo) {
super(apollo);
}
Expand Down Expand Up @@ -1874,11 +1932,6 @@ export const DataEntrySourceDetailDocument = gql`
source(id: $id) {
id
name
editionAuthor
editionTitle
medievalAuthor
medievalTitle
numOfEpisodes
episodes {
id
name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AgentDescriptionFormComponent } from './agent-description-form.component';
import { SharedTestingModule } from '@shared/shared-testing.module';
import { DataEntrySharedModule } from '../../shared/data-entry-shared.module';
import { FormService } from '../../shared/form.service';

describe('AgentDescriptionFormComponent', () => {
let component: AgentDescriptionFormComponent;
Expand All @@ -12,6 +13,7 @@ describe('AgentDescriptionFormComponent', () => {
TestBed.configureTestingModule({
declarations: [AgentDescriptionFormComponent],
imports: [SharedTestingModule, DataEntrySharedModule,],
providers: [FormService],
});
fixture = TestBed.createComponent(AgentDescriptionFormComponent);
component = fixture.componentInstance;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup } from '@angular/forms';
import { ToastService } from '@services/toast.service';
import { MutationResult } from 'apollo-angular';
import {
DataEntryAgentDescriptionGQL,
DataEntryAgentDescriptionQuery,
DataEntryUpdateAgentGQL,
DataEntryUpdateAgentMutation,
Gender,
PersonAgentDescriptionGenderGenderChoices as GenderChoices,
PersonAgentDescriptionGenderSourceMentionChoices as GenderSourceMentionChoices,
LocationsInSourceListGQL,
LocationsInSourceListQuery,
PersonAgentDescriptionSourceMentionChoices as LocationSourceMentionChoices,
SourceMention,
UpdateAgentInput,
} from 'generated/graphql';
import { Observable, map, Subject, switchMap, shareReplay, filter } from 'rxjs';
import { Observable, map, switchMap, shareReplay, filter, debounceTime, distinctUntilChanged, withLatestFrom, BehaviorSubject, tap, skip } from 'rxjs';
import _ from 'underscore';
import { FormService } from '../../shared/form.service';
import { FormStatus } from '../../shared/types';


@Component({
selector: 'lc-agent-description-form',
templateUrl: './agent-description-form.component.html',
styleUrls: ['./agent-description-form.component.scss'],
})
export class AgentDescriptionFormComponent implements OnChanges, OnDestroy {
@Input() id?: string;

export class AgentDescriptionFormComponent implements OnDestroy {
genderOptions: { value: GenderChoices, label: string }[] = [
{ value: GenderChoices.Female, label: 'Female' },
{ value: GenderChoices.Male, label: 'Male' },
Expand Down Expand Up @@ -58,13 +65,20 @@ export class AgentDescriptionFormComponent implements OnChanges, OnDestroy {
isGroup$: Observable<boolean>;
locations$: Observable<LocationsInSourceListQuery>;

private id$ = new Subject<string>();
status$ = new BehaviorSubject<FormStatus>('idle');

private id$ = this.formService.id$;
private data$: Observable<DataEntryAgentDescriptionQuery>;
private formName = 'description';

constructor(
private agentQuery: DataEntryAgentDescriptionGQL,
private locationsQuery: LocationsInSourceListGQL,
private agentMutation: DataEntryUpdateAgentGQL,
private toastService: ToastService,
private formService: FormService,
) {
this.formService.attachForm(this.formName, this.status$);
this.data$ = this.id$.pipe(
switchMap(id => this.agentQuery.watch({ id }).valueChanges),
map(result => result.data),
Expand All @@ -82,16 +96,18 @@ export class AgentDescriptionFormComponent implements OnChanges, OnDestroy {
shareReplay(1),
);
this.data$.subscribe(this.updateFormData.bind(this));
}

ngOnChanges(changes: SimpleChanges): void {
if (changes['id'] && this.id) {
this.id$.next(this.id);
}
}

ngOnDestroy(): void {
this.id$.complete();
this.form.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged(_.isEqual),
skip(1),
filter(this.isValid.bind(this)),
tap(() => this.status$.next('loading')),
withLatestFrom(this.id$),
map(this.toMutationInput),
switchMap(this.makeMutation.bind(this)),
takeUntilDestroyed(),
).subscribe(this.onMutationResult.bind(this));
}

updateFormData(data: DataEntryAgentDescriptionQuery) {
Expand All @@ -111,4 +127,62 @@ export class AgentDescriptionFormComponent implements OnChanges, OnDestroy {
});
}

ngOnDestroy(): void {
this.formService.detachForm(this.formName);
}

private isValid(): boolean {
return this.form.valid;
}

private toMutationInput([data, id]: [Partial<typeof this.form.value>, string]): UpdateAgentInput {
return {
id,
designators: data.designators,
gender: {
gender: data.gender?.gender as Gender,
sourceMention: data.gender?.sourceMention as SourceMention,
note: data.gender?.note,
},
location: data.location?.hasLocation && data.location.location ? {
location: data.location.location,
sourceMention: data.location.sourceMention as SourceMention,
note: data.location.note,
} : null,
};
}

private makeMutation(input: UpdateAgentInput):
Observable<MutationResult<DataEntryUpdateAgentMutation>> {
return this.agentMutation.mutate({ input }, {
errorPolicy: 'all',
refetchQueries: [
'DataEntryAgentDescription'
],
});
}

private onMutationResult(result: MutationResult<DataEntryUpdateAgentMutation>): void {
if (result.errors?.length) {
this.status$.next('error');
const messages = result.errors.map(error => error.message);
this.toastService.show({
type: 'danger',
header: 'Failed to save form',
body: messages.join('\n\n'),
});
} else if (result.data?.updateAgent?.errors.length) {
this.status$.next('error');
const errors = result.data.updateAgent.errors;
const messages = errors.map(error => `${error.field}: ${error.messages.join('\n')}`);
this.toastService.show({
type: 'danger',
header: 'Failed to save form',
body: messages.join('\n\n'),
});
} else {
this.status$.next('saved');
}
}

}
19 changes: 15 additions & 4 deletions frontend/src/app/data-entry/agent-form/agent-form.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,26 @@ <h1 class="mb-4">
</span>
</h1>

<p class="lead mb-4" *ngIf="agentDescription.description">
{{agentDescription.description}}
<p class="lead mb-4">
<span *ngIf="agentDescription.description; else noDescription">
{{agentDescription.description}}
</span>
<ng-template #noDescription>
<i>(No description)</i>
</ng-template>
</p>

<lc-form-status [status$]="status$" />

<div class="hstack">
<lc-delete-agent class="ms-auto" [navigateOnDelete]="sourceLink(data)" />
</div>

<h2>Identification</h2>
<lc-agent-identification-form *ngIf="id$ | async as id" [id]="id" />
<lc-agent-identification-form />

<h2>Description in source text</h2>
<lc-agent-description-form *ngIf="id$ | async as id" [id]="id" />
<lc-agent-description-form />

<h2>Episodes</h2>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SharedTestingModule } from '@shared/shared-testing.module';
import { AgentDescriptionFormComponent } from './agent-description-form/agent-description-form.component';
import { AgentIdentificationFormComponent } from './agent-identification-form/agent-identification-form.component';
import { DataEntrySharedModule } from '../shared/data-entry-shared.module';
import { FormService } from '../shared/form.service';

describe('AgentFormComponent', () => {
let component: AgentFormComponent;
Expand All @@ -21,6 +22,9 @@ describe('AgentFormComponent', () => {
SharedTestingModule,
DataEntrySharedModule,
],
providers: [
FormService,
]
});
fixture = TestBed.createComponent(AgentFormComponent);
component = fixture.componentInstance;
Expand Down
28 changes: 19 additions & 9 deletions frontend/src/app/data-entry/agent-form/agent-form.component.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Breadcrumb } from '@shared/breadcrumb/breadcrumb.component';
import { dataIcons } from '@shared/icons';
import { DataEntryAgentGQL, DataEntryAgentQuery } from 'generated/graphql';
import { map, Observable, switchMap } from 'rxjs';
import { FormService } from '../shared/form.service';

@Component({
selector: 'lc-agent-form',
templateUrl: './agent-form.component.html',
styleUrls: ['./agent-form.component.scss']
selector: 'lc-agent-form',
templateUrl: './agent-form.component.html',
styleUrls: ['./agent-form.component.scss'],
providers: [FormService],
})
export class AgentFormComponent {
id$: Observable<string>;
id$: Observable<string> = this.formService.id$;
data$: Observable<DataEntryAgentQuery>;

dataIcons = dataIcons;

constructor(private route: ActivatedRoute, private agentQuery: DataEntryAgentGQL) {
this.id$ = this.route.params.pipe(
map(params => params['id']),
);
status$ = this.formService.status$;

constructor(
private agentQuery: DataEntryAgentGQL,
private formService: FormService,
) {
this.data$ = this.id$.pipe(
switchMap(id => this.agentQuery.watch({ id }).valueChanges),
map(result => result.data),
);
}

sourceLink(data: DataEntryAgentQuery): string[] | undefined {
if (data.agentDescription?.source) {
return ['/data-entry', 'sources', data.agentDescription.source.id]
}
return undefined;
}

getBreadcrumbs(data: DataEntryAgentQuery): Breadcrumb[] {
if (data.agentDescription) {
return [
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/data-entry/agent-form/agent-form.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SharedModule } from '@shared/shared.module';
import { AgentIdentificationFormComponent } from './agent-identification-form/agent-identification-form.component';
import { AgentDescriptionFormComponent } from './agent-description-form/agent-description-form.component';
import { DataEntrySharedModule } from "../shared/data-entry-shared.module";
import { DeleteAgentComponent } from './delete-agent/delete-agent.component';



Expand All @@ -12,6 +13,7 @@ import { DataEntrySharedModule } from "../shared/data-entry-shared.module";
AgentFormComponent,
AgentIdentificationFormComponent,
AgentDescriptionFormComponent,
DeleteAgentComponent,
],
imports: [
SharedModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
</small>
</p>
<input id="input-name" type="text" class="form-control"
[formControl]="form.controls.name">
[formControl]="form.controls.name"
[class.is-invalid]="form.controls.name.invalid"
aria-required="true"
[attr.aria-invalid]="form.controls.name.invalid">
<div class="invalid-feedback">
This field is required
</div>
</div>

<div class="mb-4">
Expand Down
Loading

0 comments on commit ed377d9

Please sign in to comment.