Skip to content

Commit

Permalink
fix: Un-regress publication editor widget
Browse files Browse the repository at this point in the history
  • Loading branch information
strogonoff committed Nov 27, 2019
1 parent 65fa0a1 commit 3ef90a7
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 63 deletions.
26 changes: 23 additions & 3 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { SettingManager } from 'sse/settings/main';
import { QuerySet, sortIntegerAscending } from 'sse/storage/query';
import { initRepo } from 'sse/storage/main/git/controller';
import { IDTakenError, CommitError } from 'sse/storage/main/store/base';
import { provideAll, provideModified, listenToBatchCommits, listenToBatchDiscardRequests } from 'sse/storage/main/api';
import { provideOne, provideAll, provideModified, listenToBatchCommits, listenToBatchDiscardRequests } from 'sse/storage/main/api';

import { OBIssue, ScheduledIssue } from 'models/issues';
import { Publication } from 'models/publications';

import { buildAppMenu } from './menu';
import { getStorage, MainStorage } from 'storage/main';
Expand Down Expand Up @@ -224,6 +225,10 @@ then(gitCtrl => {
provideAll(storage, 'publications');
provideAll(storage, 'recommendations');

provideOne(storage, 'issues');
provideOne(storage, 'publications');
provideOne(storage, 'recommendations');

provideModified(storage, 'issues');
provideModified(storage, 'publications');
provideModified(storage, 'recommendations');
Expand Down Expand Up @@ -312,15 +317,30 @@ then(gitCtrl => {
return { modified: false };
}
});

listen<{ data: Publication }, { success: true }>
('publication-create', async ({ data }) => {
await storage.publications.create(data, true);
return { success: true };
});

listen<{ pubId: string, data: Publication, commit: boolean }, { modified: boolean }>
('publication-update', async ({ pubId, data, commit }) => {
await storage.publications.update(pubId, data, commit);
if (!commit) {
return { modified: (await storage.publications.listUncommitted()).indexOf(pubId) >= 0 };
} else {
return { modified: false };
}
});


/* Set up window-opening endpoints */

makeWindowEndpoint('publication-editor', ({ pubId }: { pubId: string }) => ({
makeWindowEndpoint('publication-editor', ({ pubId, create }: { pubId: string, create: boolean }) => ({
component: 'publicationEditor',
title: `Publication ${pubId}`,
componentParams: `pubId=${pubId}`,
componentParams: `pubId=${pubId}&create=${create ? '1' : '0'}`,
frameless: true,
dimensions: { width: 800, height: 600, },
}));
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ const App: React.FC<{}> = function () {
component = <IssueEditor issueId={searchParams.get('issueId') || ''} />;

} else if (searchParams.get('c') === 'publicationEditor') {
component = <PublicationEditor publicationId={searchParams.get('pubId') || ''} />;
component = <PublicationEditor
publicationId={searchParams.get('pubId') || ''}
create={searchParams.get('create') === '1'} />;

} else if (searchParams.get('c') === 'dataSynchronizer') {
component = <DataSynchronizer
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/issue-editor/item-list/new-amendment-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const renderLaunchPublicationCreator = function (query: string) {
}

function launchPublicationEditor(forPublicationWithId: string) {
openWindow('publication-editor', { pubId: forPublicationWithId });
openWindow('publication-editor', { pubId: forPublicationWithId, create: true });
}

const NewAmendmentSelector = Select.ofType<AmendablePublication>();
2 changes: 1 addition & 1 deletion src/renderer/issue-editor/item-list/new-annex-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const renderLaunchPublicationCreator = function (query: string) {
}

function launchPublicationEditor(forPublicationWithId: string) {
openWindow('publication-editor', { pubId: forPublicationWithId });
openWindow('publication-editor', { pubId: forPublicationWithId, create: true });
}

const NewAnnexSelector = Select.ofType<AnnexablePublication>();
182 changes: 126 additions & 56 deletions src/renderer/publication-editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import AsyncLock from 'async-lock';
import { remote, ipcRenderer } from 'electron';

import React, { useContext, useState, useEffect } from 'react';
import { Button, FormGroup, InputGroup } from '@blueprintjs/core';

Expand All @@ -11,10 +14,14 @@ import { HelpButton } from 'renderer/widgets/help-button';
import * as styles from './styles.scss';


const pubOperationQueue = new AsyncLock();


interface PublicationEditorProps {
publicationId: string,
create: boolean,
}
export const PublicationEditor: React.FC<PublicationEditorProps> = function ({ publicationId }) {
export const PublicationEditor: React.FC<PublicationEditorProps> = function ({ publicationId, create }) {
const lang = useContext(LangConfigContext);

const [publication, setPublication] = useState({
Expand All @@ -24,77 +31,87 @@ export const PublicationEditor: React.FC<PublicationEditorProps> = function ({ p
title: { [lang.default]: '' },
} as Publication);

const fieldRequirements = {
const fieldRequirements: FieldRequirements<Publication> = {
title: {
specified: `have a title in ${lang.available[lang.default]}`,
specified: {
err: `have a title in ${lang.available[lang.default]}`,
didFail: async (pub) => (pub.title[lang.default] === ''),
},
},
id: {
unique: `have a unique ID (“${publication.id}” is already taken)`,
specified: `have a unique string ID`,
unique: {
err: `have a unique ID (“${publication.id}” is already taken)`,
didFail: async (pub) => (create === true && (await get(pub.id)) !== null),
},
specified: {
err: `have a unique string ID`,
didFail: async (pub) => (pub.id.trim() === ''),
},
},
};

type UnmetRequirements = {
[F in keyof typeof fieldRequirements]?: {
[R in keyof (typeof fieldRequirements)[F]]?: boolean
}
};
const UnmetReq = UnmetRequirementNotice as _UnmetRequirementNotice<typeof fieldRequirements>;

const [isNew, setIsNew] = useState(true);
const [canSave, setCanSave] = useState(false);
const [unmetRequirements, setUnmetRequirements] = useState({} as UnmetRequirements);

useEffect(() => { load(); }, []);

useEffect(() => { setCanSave(Object.keys(unmetRequirements).length === 0); }, [unmetRequirements]);
const [unmetRequirements, setUnmetRequirements] = useState({} as UnmetRequirements<typeof fieldRequirements>);

useEffect(() => {
setCanSave(false);
(async () => {
const reqs = await getUnmetRequirements();
setUnmetRequirements(reqs);
const pub = await get(publication.id);
if (pub) {
setPublication(pub);
}
})();
}, [isNew, publication.id, publication.title]);
}, []);

async function getUnmetRequirements() {
var result: UnmetRequirements = {};
useEffect(() => {
setCanSave(false);

if (publication.title[lang.default] === '') {
result['title'] = { ...result.title, specified: false };
}
console.debug("Checking updated pub");
pubOperationQueue.acquire('1', async () => {
console.debug("Checking updated pub, acquired");
const reqs = await getUnmetRequirements(publication, fieldRequirements);
const canSave = Object.keys(unmetRequirements).length === 0;

setCanSave(canSave);
setUnmetRequirements(reqs);

if (publication.id === '') {
result['id'] = { ...result.id, specified: false };
} else if (isNew) {
const alreadyExists = (await get(publication.id)) !== null;
if (alreadyExists) {
result['id'] = { ...result.id, unique: false };
if (canSave && !create) {
update(publication);
}
}
});
}, [JSON.stringify(publication)]);

return result;
}

async function save() {
await request<Publication>('storage-store-one-in-publications', {
objectId: publication.id,
newData: publication,
async function update(publication: Publication) {
await pubOperationQueue.acquire('1', async () => {
await request<Publication>('publication-update', { pubId: publication.id, data: publication });
});
await load();
}

async function load() {
const result = await get(publication.id);
if (result !== null) {
setIsNew(false);
setPublication(result);
async function commitAndClose() {
if (!canSave) {
return;
}
setCanSave(false);

await pubOperationQueue.acquire('1', async () => {
if (create) {
await request<Publication>('publication-create', { data: publication });
} else {
await request<Publication>('publication-update', { pubId: publication.id, data: publication, commit: true });
}
});

await ipcRenderer.send('remote-storage-trigger-sync');
await ipcRenderer.send('publications-changed');
await remote.getCurrentWindow().close();
}

return (
<div className={styles.pubEditorWindow}>
<PaneHeader major={true} actions={<HelpButton path="amend-publication/" />}>
{isNew ? "Create" : "Edit"} publication
{create ? "Create" : "Edit"} publication
</PaneHeader>

<main className={styles.windowBody}>
Expand All @@ -104,9 +121,7 @@ export const PublicationEditor: React.FC<PublicationEditorProps> = function ({ p
intent={unmetRequirements.id ? "danger" : undefined}
helperText={
<ul>
{unmetRequirements.id
? Object.keys(unmetRequirements.id).map(msgId => <li>Must {fieldRequirements.id[msgId as keyof typeof fieldRequirements.id]}.</li>)
: ''}
<UnmetReq field="id" reqSpec={fieldRequirements} unmetReqs={unmetRequirements} />
<li>Use uppercase English string as publication ID: e.g., BUREAUFAX.</li>
<li>Choose an ID consistent with publication URL on ITU website, if possible.</li>
<li>Note: you can’t change this later easily.</li>
Expand All @@ -116,7 +131,7 @@ export const PublicationEditor: React.FC<PublicationEditorProps> = function ({ p
value={publication.id}
type="text"
large={true}
readOnly={!isNew}
readOnly={!create}
onChange={(evt: React.FormEvent<HTMLElement>) => {
setPublication({
...publication,
Expand All @@ -131,9 +146,7 @@ export const PublicationEditor: React.FC<PublicationEditorProps> = function ({ p
intent={unmetRequirements.title ? "danger" : undefined}
helperText={
<ul>
{unmetRequirements.title
? Object.keys(unmetRequirements.title).map(msgId => <li>Must {fieldRequirements.title[msgId as keyof typeof fieldRequirements.title]}.</li>)
: ''}
<UnmetReq field="title" reqSpec={fieldRequirements} unmetReqs={unmetRequirements} />
</ul>
}
label={`Title in ${lang.available[lang.default]}`}>
Expand Down Expand Up @@ -171,14 +184,71 @@ export const PublicationEditor: React.FC<PublicationEditorProps> = function ({ p
<footer className={styles.actionButtons}>
<Button
disabled={canSave !== true}
intent="primary"
onClick={save}>Save and Close</Button>
intent="success"
icon="git-commit"
onClick={commitAndClose}>Commit and Close</Button>
</footer>
</div>
);
};


async function get(id: string) {
return await request<Publication>('storage-get-one-in-publications', { objectId: id });
async function get(id: string): Promise<Publication | null> {
try {
const pub = await request<Publication>('storage-read-one-in-publications', { objectId: id });
console.debug("Got pub");
return pub;
} catch (e) {
console.error(e);
return null;
}
}


type UnmetRequirementNoticeProps<F extends FieldRequirements<any>> = {
field: string & keyof F,
reqSpec: F,
unmetReqs: UnmetRequirements<F>,
}
type _UnmetRequirementNotice<F = FieldRequirements<any>> = React.FC<UnmetRequirementNoticeProps<F>>
const UnmetRequirementNotice: _UnmetRequirementNotice = function ({ field, reqSpec, unmetReqs }) {
if (!reqSpec[field]) { throw new Error("Unknown requirements key"); }
return <>
{Object.keys(unmetReqs[field] || {}).
map((checkName: string) => <li>Must {(reqSpec[field] as CheckSpecs<any>)[checkName].err}.</li>)}
</>;
}


type CheckSpecs<O> = {
[checkName: string]: {
err: string,
didFail: (obj: O) => Promise<boolean>,
}
}

type FieldRequirements<O> = {
[F in keyof O]?: CheckSpecs<O>
};


type UnmetRequirements<R extends FieldRequirements<any>> = {
[F in keyof R]?: {
[C in keyof R[F]]?: boolean
}
};


async function getUnmetRequirements<O>(obj: O, reqSpec: FieldRequirements<O>): Promise<UnmetRequirements<FieldRequirements<O>>> {
var reqs: UnmetRequirements<FieldRequirements<O>> = {};

for (const [field, checks] of Object.entries(reqSpec)) {
for (const [checkName, checkSpec] of Object.entries(checks as CheckSpecs<O>)) {
if (await checkSpec.didFail(obj)) {
reqs[field as keyof O] = { ...reqs[field as keyof O], [checkName]: false };
}
}
}

return reqs;
}
2 changes: 1 addition & 1 deletion src/renderer/publication-editor/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
.actionButtons {
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
justify-content: flex-end;
padding-bottom: 1rem;
}
}

0 comments on commit 3ef90a7

Please sign in to comment.