diff --git a/frontend/generated/graphql.ts b/frontend/generated/graphql.ts
index 7910ea5a..7765a056 100644
--- a/frontend/generated/graphql.ts
+++ b/frontend/generated/graphql.ts
@@ -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 }> } | 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, field: string }> } | null };
+
export type DataEntryEpisodeContentsQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
@@ -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; }>;
@@ -1393,6 +1407,50 @@ export const DataEntryAgentDocument = gql`
export class DataEntryAgentGQL extends Apollo.Query {
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 {
+ 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 {
+ override document = DataEntryDeleteAgentDocument;
+
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
@@ -1874,11 +1932,6 @@ export const DataEntrySourceDetailDocument = gql`
source(id: $id) {
id
name
- editionAuthor
- editionTitle
- medievalAuthor
- medievalTitle
- numOfEpisodes
episodes {
id
name
diff --git a/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.spec.ts b/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.spec.ts
index 6ea7499f..dc016b60 100644
--- a/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.spec.ts
+++ b/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.spec.ts
@@ -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;
@@ -12,6 +13,7 @@ describe('AgentDescriptionFormComponent', () => {
TestBed.configureTestingModule({
declarations: [AgentDescriptionFormComponent],
imports: [SharedTestingModule, DataEntrySharedModule,],
+ providers: [FormService],
});
fixture = TestBed.createComponent(AgentDescriptionFormComponent);
component = fixture.componentInstance;
diff --git a/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.ts b/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.ts
index ae7025c3..9704d1a2 100644
--- a/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.ts
+++ b/frontend/src/app/data-entry/agent-form/agent-description-form/agent-description-form.component.ts
@@ -1,17 +1,26 @@
-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({
@@ -19,9 +28,7 @@ import _ from 'underscore';
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' },
@@ -58,13 +65,20 @@ export class AgentDescriptionFormComponent implements OnChanges, OnDestroy {
isGroup$: Observable;
locations$: Observable;
- private id$ = new Subject();
+ status$ = new BehaviorSubject('idle');
+
+ private id$ = this.formService.id$;
private data$: Observable;
+ 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),
@@ -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) {
@@ -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, 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> {
+ return this.agentMutation.mutate({ input }, {
+ errorPolicy: 'all',
+ refetchQueries: [
+ 'DataEntryAgentDescription'
+ ],
+ });
+ }
+
+ private onMutationResult(result: MutationResult): 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');
+ }
+ }
+
}
diff --git a/frontend/src/app/data-entry/agent-form/agent-form.component.html b/frontend/src/app/data-entry/agent-form/agent-form.component.html
index 5ca85210..b16dd437 100644
--- a/frontend/src/app/data-entry/agent-form/agent-form.component.html
+++ b/frontend/src/app/data-entry/agent-form/agent-form.component.html
@@ -10,15 +10,26 @@
-
- {{agentDescription.description}}
+
+
+ {{agentDescription.description}}
+
+
+ (No description)
+
+
+
+
+
+
+
Identification
-
+
Description in source text
-
+
Episodes
diff --git a/frontend/src/app/data-entry/agent-form/agent-form.component.spec.ts b/frontend/src/app/data-entry/agent-form/agent-form.component.spec.ts
index d09b111c..2b24a606 100644
--- a/frontend/src/app/data-entry/agent-form/agent-form.component.spec.ts
+++ b/frontend/src/app/data-entry/agent-form/agent-form.component.spec.ts
@@ -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;
@@ -21,6 +22,9 @@ describe('AgentFormComponent', () => {
SharedTestingModule,
DataEntrySharedModule,
],
+ providers: [
+ FormService,
+ ]
});
fixture = TestBed.createComponent(AgentFormComponent);
component = fixture.componentInstance;
diff --git a/frontend/src/app/data-entry/agent-form/agent-form.component.ts b/frontend/src/app/data-entry/agent-form/agent-form.component.ts
index 499cc96e..a7bf3d1d 100644
--- a/frontend/src/app/data-entry/agent-form/agent-form.component.ts
+++ b/frontend/src/app/data-entry/agent-form/agent-form.component.ts
@@ -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;
+ id$: Observable = this.formService.id$;
data$: Observable;
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 [
diff --git a/frontend/src/app/data-entry/agent-form/agent-form.module.ts b/frontend/src/app/data-entry/agent-form/agent-form.module.ts
index 498f1ca3..46c71a58 100644
--- a/frontend/src/app/data-entry/agent-form/agent-form.module.ts
+++ b/frontend/src/app/data-entry/agent-form/agent-form.module.ts
@@ -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';
@@ -12,6 +13,7 @@ import { DataEntrySharedModule } from "../shared/data-entry-shared.module";
AgentFormComponent,
AgentIdentificationFormComponent,
AgentDescriptionFormComponent,
+ DeleteAgentComponent,
],
imports: [
SharedModule,
diff --git a/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.html b/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.html
index c9cebc5f..63e84889 100644
--- a/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.html
+++ b/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.html
@@ -10,7 +10,13 @@
+ [formControl]="form.controls.name"
+ [class.is-invalid]="form.controls.name.invalid"
+ aria-required="true"
+ [attr.aria-invalid]="form.controls.name.invalid">
+
+ This field is required
+
diff --git a/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.spec.ts b/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.spec.ts
index 2531c1ee..877736a1 100644
--- a/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.spec.ts
+++ b/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.spec.ts
@@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AgentIdentificationFormComponent } from './agent-identification-form.component';
import { SharedTestingModule } from '@shared/shared-testing.module';
+import { FormService } from '../../shared/form.service';
describe('AgentIdentificationFormComponent', () => {
let component: AgentIdentificationFormComponent;
@@ -11,6 +12,7 @@ describe('AgentIdentificationFormComponent', () => {
TestBed.configureTestingModule({
declarations: [AgentIdentificationFormComponent],
imports: [SharedTestingModule],
+ providers: [FormService],
});
fixture = TestBed.createComponent(AgentIdentificationFormComponent);
component = fixture.componentInstance;
diff --git a/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.ts b/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.ts
index 70e7f1be..de8efdba 100644
--- a/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.ts
+++ b/frontend/src/app/data-entry/agent-form/agent-identification-form/agent-identification-form.component.ts
@@ -1,41 +1,86 @@
-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 { DataEntryAgentIdentificationGQL, DataEntryAgentIdentificationQuery } from 'generated/graphql';
-import { map, Subject, switchMap } from 'rxjs';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { ToastService } from '@services/toast.service';
+import { MutationResult } from 'apollo-angular';
+import {
+ DataEntryAgentIdentificationGQL, DataEntryAgentIdentificationQuery,
+ DataEntryUpdateAgentGQL, DataEntryUpdateAgentMutation, UpdateAgentInput
+} from 'generated/graphql';
+import {
+ map, switchMap, filter, debounceTime, withLatestFrom, Observable,
+ distinctUntilChanged,
+ tap,
+ BehaviorSubject,
+ skip
+} from 'rxjs';
+import _ from 'underscore';
+import { FormService } from '../../shared/form.service';
+import { FormStatus } from '../../shared/types';
+
+interface FormData {
+ name: string;
+ description: string;
+ isGroup: boolean;
+}
@Component({
selector: 'lc-agent-identification-form',
templateUrl: './agent-identification-form.component.html',
styleUrls: ['./agent-identification-form.component.scss']
})
-export class AgentIdentificationFormComponent implements OnChanges, OnDestroy {
- @Input() id?: string;
-
+export class AgentIdentificationFormComponent implements OnDestroy {
form = new FormGroup({
- name: new FormControl
(''),
- description: new FormControl(''),
- isGroup: new FormControl(false),
+ name: new FormControl('', {
+ nonNullable: true,
+ validators: [Validators.required]
+ }),
+ description: new FormControl('', { nonNullable: true }),
+ isGroup: new FormControl(false, { nonNullable: true }),
});
- private id$ = new Subject();
+ status$ = new BehaviorSubject('idle');
+
+ private id$ = this.formService.id$;
+ private formName = 'identification';
+
+ constructor(
+ private agentQuery: DataEntryAgentIdentificationGQL,
+ private agentMutation: DataEntryUpdateAgentGQL,
+ private toastService: ToastService,
+ private formService: FormService,
+ ) {
+ this.formService.attachForm(this.formName, this.status$);
- constructor(private agentQuery: DataEntryAgentIdentificationGQL) {
this.id$.pipe(
switchMap(id => this.agentQuery.watch({ id }).valueChanges),
map(result => result.data),
takeUntilDestroyed(),
).subscribe(this.updateFormData.bind(this));
- }
- ngOnChanges(changes: SimpleChanges): void {
- if (changes['id'] && this.id) {
- this.id$.next(this.id);
- }
+
+
+
+ this.form.statusChanges.pipe(
+ filter(status => status == 'INVALID'),
+ takeUntilDestroyed(),
+ ).subscribe(() => this.status$.next('invalid'));
+
+ 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));
}
ngOnDestroy(): void {
- this.id$.complete();
+ this.formService.detachForm(this.formName);
}
updateFormData(data: DataEntryAgentIdentificationQuery) {
@@ -45,4 +90,48 @@ export class AgentIdentificationFormComponent implements OnChanges, OnDestroy {
isGroup: data.agentDescription?.isGroup || false,
});
}
+
+ private isValid(): boolean {
+ return this.form.valid;
+ }
+
+ private toMutationInput([data, id]: [Partial, string]): UpdateAgentInput {
+ return { id, ...data };
+ }
+
+ private makeMutation(input: UpdateAgentInput):
+ Observable> {
+ return this.agentMutation.mutate({ input }, {
+ errorPolicy: 'all',
+ refetchQueries: [
+ 'DataEntryAgent',
+ 'DataEntryAgentIdentification',
+ 'DataEntryAgentDescription'
+ ],
+ });
+ }
+
+ private onMutationResult(result: MutationResult): 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');
+ }
+ }
+
}
diff --git a/frontend/src/app/data-entry/agent-form/agent.graphql b/frontend/src/app/data-entry/agent-form/agent.graphql
index 3fd81287..b3d56359 100644
--- a/frontend/src/app/data-entry/agent-form/agent.graphql
+++ b/frontend/src/app/data-entry/agent-form/agent.graphql
@@ -10,3 +10,17 @@ query DataEntryAgent($id: ID!) {
}
}
}
+
+mutation DataEntryUpdateAgent(
+ $input: UpdateAgentInput!
+) {
+ updateAgent(
+ agentData: $input
+ ) {
+ ok
+ errors {
+ field
+ messages
+ }
+ }
+}
diff --git a/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.html b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.html
new file mode 100644
index 00000000..a44b74a6
--- /dev/null
+++ b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Delete this agent from this source text? They will be removed from all
+ episodes they are involved in!
+
+
+
+
diff --git a/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.scss b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.spec.ts b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.spec.ts
new file mode 100644
index 00000000..2729fc7d
--- /dev/null
+++ b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeleteAgentComponent } from './delete-agent.component';
+import { FormService } from '../../shared/form.service';
+import { SharedTestingModule } from '@shared/shared-testing.module';
+
+describe('DeleteAgentComponent', () => {
+ let component: DeleteAgentComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [DeleteAgentComponent],
+ imports: [SharedTestingModule],
+ providers: [FormService],
+ });
+ fixture = TestBed.createComponent(DeleteAgentComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.ts b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.ts
new file mode 100644
index 00000000..68e15142
--- /dev/null
+++ b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.component.ts
@@ -0,0 +1,91 @@
+import { Component, Input, OnDestroy, TemplateRef } from '@angular/core';
+import { Router } from '@angular/router';
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastService } from '@services/toast.service';
+import { actionIcons } from '@shared/icons';
+import { DataEntryDeleteAgentGQL, LettercraftErrorType } from 'generated/graphql';
+import _ from 'underscore';
+import { FormService } from '../../shared/form.service';
+import { BehaviorSubject } from 'rxjs';
+import { FormStatus } from '../../shared/types';
+
+@Component({
+ selector: 'lc-delete-agent',
+ templateUrl: './delete-agent.component.html',
+ styleUrls: ['./delete-agent.component.scss']
+})
+export class DeleteAgentComponent implements OnDestroy {
+ @Input() navigateOnDelete?: any[];
+
+ actionIcons = actionIcons;
+
+ id$ = this.formService.id$;
+ status$ = new BehaviorSubject('idle');
+ private formName = 'delete';
+
+ constructor(
+ private modalService: NgbModal,
+ private deleteMutation: DataEntryDeleteAgentGQL,
+ private toastService: ToastService,
+ private router: Router,
+ private formService: FormService,
+ ) {
+ this.formService.attachForm(this.formName, this.status$);
+ }
+
+ ngOnDestroy(): void {
+ this.formService.detachForm(this.formName);
+ }
+
+ open(content: TemplateRef) {
+ this.modalService.open(content, { ariaLabelledBy: 'modal-title' }).result.then(
+ this.deleteAgent.bind(this),
+ _.constant(undefined), // do nothing on dismiss
+ );
+ }
+
+ deleteAgent(id: string) {
+ this.status$.next('loading');
+ this.deleteMutation.mutate({ id }, {
+ refetchQueries: [
+ 'source'
+ ],
+ }).subscribe({
+ next: (result) => {
+ if (result.data?.deleteAgent?.ok) {
+ this.onSuccess();
+ } else {
+ this.onFail(result.data?.deleteAgent?.errors);
+ }
+ },
+ error: (error) => {
+ this.onFail(error);
+ }
+ });
+ }
+
+ onSuccess() {
+ this.status$.next('saved');
+ this.toastService.show({
+ type: 'success',
+ header: 'Agent deleted',
+ body: 'Agent successfully deleted'
+ })
+ if (this.navigateOnDelete) {
+ this.router.navigate(this.navigateOnDelete);
+ }
+ }
+
+ onFail(errors?: LettercraftErrorType[] | any) {
+ this.status$.next('error');
+ console.error(errors);
+ const messages = errors?.map?.call(
+ (error: any) => error.messages?.join('\n')
+ ) || 'Unexpected error';
+ this.toastService.show({
+ type: 'danger',
+ header: 'Deletion failed',
+ body: `Deleting agent failed:\n${messages}`
+ });
+ }
+}
diff --git a/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.graphql b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.graphql
new file mode 100644
index 00000000..5c2f98a3
--- /dev/null
+++ b/frontend/src/app/data-entry/agent-form/delete-agent/delete-agent.graphql
@@ -0,0 +1,9 @@
+mutation DataEntryDeleteAgent($id: ID!) {
+ deleteAgent(id: $id) {
+ ok
+ errors {
+ messages
+ field
+ }
+ }
+}
diff --git a/frontend/src/app/data-entry/shared/data-entry-shared.module.ts b/frontend/src/app/data-entry/shared/data-entry-shared.module.ts
index 5395a06f..b333a16a 100644
--- a/frontend/src/app/data-entry/shared/data-entry-shared.module.ts
+++ b/frontend/src/app/data-entry/shared/data-entry-shared.module.ts
@@ -3,18 +3,21 @@ import { SharedModule } from "@shared/shared.module";
import { DesignatorsControlComponent } from "./designators-control/designators-control.component";
import { MultiselectComponent } from "./multiselect/multiselect.component";
import { LabelSelectComponent } from "./label-select/label-select.component";
+import { FormStatusComponent } from './form-status/form-status.component';
@NgModule({
declarations: [
DesignatorsControlComponent,
MultiselectComponent,
LabelSelectComponent,
+ FormStatusComponent,
],
imports: [SharedModule],
exports: [
DesignatorsControlComponent,
MultiselectComponent,
LabelSelectComponent,
+ FormStatusComponent,
],
})
export class DataEntrySharedModule {}
diff --git a/frontend/src/app/data-entry/shared/form-status/form-status.component.html b/frontend/src/app/data-entry/shared/form-status/form-status.component.html
new file mode 100644
index 00000000..667f06e3
--- /dev/null
+++ b/frontend/src/app/data-entry/shared/form-status/form-status.component.html
@@ -0,0 +1,4 @@
+
+
+ {{messages[status]}}
+
diff --git a/frontend/src/app/data-entry/shared/form-status/form-status.component.scss b/frontend/src/app/data-entry/shared/form-status/form-status.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/frontend/src/app/data-entry/shared/form-status/form-status.component.spec.ts b/frontend/src/app/data-entry/shared/form-status/form-status.component.spec.ts
new file mode 100644
index 00000000..9fcc0c8c
--- /dev/null
+++ b/frontend/src/app/data-entry/shared/form-status/form-status.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FormStatusComponent } from './form-status.component';
+
+describe('FormStatusComponent', () => {
+ let component: FormStatusComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [FormStatusComponent]
+ });
+ fixture = TestBed.createComponent(FormStatusComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/data-entry/shared/form-status/form-status.component.ts b/frontend/src/app/data-entry/shared/form-status/form-status.component.ts
new file mode 100644
index 00000000..869f9a91
--- /dev/null
+++ b/frontend/src/app/data-entry/shared/form-status/form-status.component.ts
@@ -0,0 +1,31 @@
+import { Component, Input } from '@angular/core';
+import { FormStatus } from '../types';
+import { Observable } from 'rxjs';
+import { statusIcons } from '@shared/icons';
+
+@Component({
+ selector: 'lc-form-status',
+ templateUrl: './form-status.component.html',
+ styleUrls: ['./form-status.component.scss']
+})
+export class FormStatusComponent {
+ @Input({ required: true }) status$!: Observable;
+
+ statusIcons = statusIcons;
+
+ messages: Record = {
+ 'idle': 'No changes made',
+ 'invalid': 'Form contains errors',
+ 'loading': 'Loading...',
+ 'saved': 'Changes saved',
+ 'error': 'Saving failed',
+ };
+
+ classes: Record = {
+ 'idle': 'text-secondary',
+ 'invalid': 'text-danger',
+ 'loading': 'text-secondary',
+ 'saved': 'text-success',
+ 'error': 'text-danger',
+ };
+}
diff --git a/frontend/src/app/data-entry/shared/form.service.spec.ts b/frontend/src/app/data-entry/shared/form.service.spec.ts
new file mode 100644
index 00000000..e084d534
--- /dev/null
+++ b/frontend/src/app/data-entry/shared/form.service.spec.ts
@@ -0,0 +1,20 @@
+import { TestBed } from '@angular/core/testing';
+
+import { FormService } from './form.service';
+import { SharedTestingModule } from '@shared/shared-testing.module';
+
+describe('FormService', () => {
+ let service: FormService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [FormService],
+ imports: [SharedTestingModule],
+ });
+ service = TestBed.inject(FormService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/data-entry/shared/form.service.ts b/frontend/src/app/data-entry/shared/form.service.ts
new file mode 100644
index 00000000..62f3fd15
--- /dev/null
+++ b/frontend/src/app/data-entry/shared/form.service.ts
@@ -0,0 +1,54 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { asyncScheduler, BehaviorSubject, combineLatest, map, Observable, shareReplay, switchMap, throttleTime } from 'rxjs';
+import { FormStatus } from './types';
+import _ from 'underscore';
+
+@Injectable()
+export class FormService {
+ id$: Observable;
+
+ status$: Observable;
+
+ private statuses$ = new BehaviorSubject>>({});
+
+ constructor(private route: ActivatedRoute) {
+ this.id$ = this.route.params.pipe(
+ map(params => params['id'])
+ );
+ this.status$ = this.statuses$.pipe(
+ switchMap(statuses => combineLatest(_.values(statuses))),
+ map(this.combinedStatus),
+ throttleTime(400, asyncScheduler, { trailing: true }),
+ );
+ }
+
+ attachForm(name: string, status$: Observable) {
+ const statuses = this.statuses$.value;
+ if (_.has(statuses, name)) {
+ throw Error(`A form named ${name} is already registered`);
+ }
+ const newValue = _.extend(statuses, { [name]: status$.pipe(shareReplay(1)) })
+ this.statuses$.next(newValue);
+ }
+
+ detachForm(name: string) {
+ const statuses = this.statuses$.value;
+ const newValue = _.omit(statuses, name);
+ this.statuses$.next(newValue);
+ }
+
+ private combinedStatus(statuses: FormStatus[]) {
+ if (statuses.includes('error')) {
+ return 'error';
+ } else if (statuses.includes('loading')) {
+ return 'loading';
+ } else if (statuses.includes('invalid')) {
+ return 'invalid';
+ } else if (statuses.includes('saved')) {
+ return 'saved';
+ } else {
+ return 'idle';
+ }
+ }
+}
diff --git a/frontend/src/app/data-entry/shared/types.d.ts b/frontend/src/app/data-entry/shared/types.d.ts
new file mode 100644
index 00000000..38e67f13
--- /dev/null
+++ b/frontend/src/app/data-entry/shared/types.d.ts
@@ -0,0 +1 @@
+export type FormStatus = 'idle' | 'invalid' | 'loading' | 'saved' | 'error';
diff --git a/frontend/src/app/data-entry/source/episode-preview/episode-preview.component.html b/frontend/src/app/data-entry/source/episode-preview/episode-preview.component.html
index 7adfc134..cd62b2fb 100644
--- a/frontend/src/app/data-entry/source/episode-preview/episode-preview.component.html
+++ b/frontend/src/app/data-entry/source/episode-preview/episode-preview.component.html
@@ -30,7 +30,7 @@
class="inline-list-item"
>
@@ -51,7 +51,7 @@
class="inline-list-item"
>
@@ -90,12 +90,7 @@
class="inline-list-item"
>
diff --git a/frontend/src/app/data-entry/source/source.component.html b/frontend/src/app/data-entry/source/source.component.html
index c008649b..3d0cc876 100644
--- a/frontend/src/app/data-entry/source/source.component.html
+++ b/frontend/src/app/data-entry/source/source.component.html
@@ -3,7 +3,7 @@
- {{ source.medievalAuthor }}, {{ source.medievalTitle }}
+ {{ source.name }}
diff --git a/frontend/src/app/data-entry/source/source.component.ts b/frontend/src/app/data-entry/source/source.component.ts
index b958bee8..8f0016d5 100644
--- a/frontend/src/app/data-entry/source/source.component.ts
+++ b/frontend/src/app/data-entry/source/source.component.ts
@@ -36,7 +36,7 @@ export class SourceComponent {
public sourceTitle = toSignal(
this.source$.pipe(
- map((source) => `${source.medievalAuthor}, ${source.medievalTitle}`)
+ map((source) => source.name)
),
{ initialValue: "" }
);
diff --git a/frontend/src/app/data-entry/source/source.graphql b/frontend/src/app/data-entry/source/source.graphql
index 06d2004c..0d3d633e 100644
--- a/frontend/src/app/data-entry/source/source.graphql
+++ b/frontend/src/app/data-entry/source/source.graphql
@@ -2,11 +2,6 @@ query DataEntrySourceDetail($id: ID!) {
source(id: $id) {
id
name
- editionAuthor
- editionTitle
- medievalAuthor
- medievalTitle
- numOfEpisodes
episodes {
id
name
diff --git a/frontend/src/app/shared/icons.ts b/frontend/src/app/shared/icons.ts
index 254f116b..33ba1cac 100644
--- a/frontend/src/app/shared/icons.ts
+++ b/frontend/src/app/shared/icons.ts
@@ -35,5 +35,13 @@ export const dataIcons = {
letter: 'envelope',
};
+export const statusIcons = {
+ idle: 'pencil',
+ invalid: 'exclamation-circle',
+ loading: 'hourglass',
+ saved: 'check-lg',
+ error: 'exclamation-circle',
+}
+