Skip to content

Commit

Permalink
Rename attachment item after metadata retrieval
Browse files Browse the repository at this point in the history
  • Loading branch information
tnajdek committed Feb 24, 2025
1 parent f69707b commit 9ab7680
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 28 deletions.
2 changes: 1 addition & 1 deletion src/js/actions/items-write.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ const createItem = (properties, libraryKey) => {
otherItems: state.libraries[libraryKey].items,
response
});
return response.getEntityByIndex(0);
return item;
} catch(error) {
dispatch({
type: ERROR_CREATE_ITEM,
Expand Down
22 changes: 16 additions & 6 deletions src/js/actions/recognize.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import { createItem, deleteItem, getAttachmentUrl, updateItem } from '.';
import { createItem, deleteItem, getAttachmentUrl, updateItem, renameAttachment } from '.';
import { BEGIN_RECOGNIZE_DOCUMENT, COMPLETE_RECOGNIZE_DOCUMENT, ERROR_RECOGNIZE_DOCUMENT,
UPDATE_RECOGNIZE_DOCUMENT, BEGIN_UNRECOGNIZE_DOCUMENT, COMPLETE_UNRECOGNIZE_DOCUMENT,
ERROR_UNRECOGNIZE_DOCUMENT, } from '../constants/actions';
import { PDFWorker } from '../common/pdf-worker.js';
import { pick } from 'web-common/utils';
import { getItemFromIdentifier } from '../common/identifiers';
import { getFileBaseNameFromItem } from '../common/format';

const retrieveMetadata = (itemKey, libraryKey, backgroundTaskId) => {
return async (dispatch, getState) => {
dispatch({ type: BEGIN_RECOGNIZE_DOCUMENT, itemKey, libraryKey, backgroundTaskId });
const state = getState();
const attachmentItem = state.libraries[state.current.libraryKey]?.items?.[itemKey];
const originalFilename = attachmentItem.filename;
const originalTitle = attachmentItem.title;
try {
const recognizerData = await dispatch(getRecognizerData(itemKey));
dispatch({ type: UPDATE_RECOGNIZE_DOCUMENT, itemKey, libraryKey, stage: 1 });
const recognizedItem = await dispatch(recognizePDF(recognizerData));

dispatch({ type: UPDATE_RECOGNIZE_DOCUMENT, itemKey, libraryKey, stage: 2 });
delete recognizedItem.key;
delete recognizedItem.version;
recognizedItem.collections = [...attachmentItem.collections];
const item = await dispatch(createItem(recognizedItem, libraryKey));

dispatch({ type: UPDATE_RECOGNIZE_DOCUMENT, itemKey, libraryKey, stage: 3 });
await dispatch(updateItem(itemKey, { parentItem: item.key, collections: [] }, libraryKey));
dispatch({ type: COMPLETE_RECOGNIZE_DOCUMENT, itemKey, libraryKey, parentItemKey: item.key });
await dispatch(updateItem(itemKey, { parentItem: item.key, title: 'PDF', collections: [] }, libraryKey));
const newFileName = `${getFileBaseNameFromItem(item, state.meta.mappings)}.pdf`;
await dispatch(renameAttachment(itemKey, newFileName, libraryKey));
dispatch({ type: COMPLETE_RECOGNIZE_DOCUMENT, itemKey, libraryKey, parentItemKey: item.key, originalFilename, originalTitle });
} catch (error) {
dispatch({
type: ERROR_RECOGNIZE_DOCUMENT,
Expand Down Expand Up @@ -164,6 +171,7 @@ const recognizePDF = (recognizerData) => {
}

const undoRetrieveMetadata = (itemKey, libraryKey) => {
// itemKey is the recognized item, that needs to be deleted; originalItemKey is the attachment item, that needs to be restored as top-level item
return async (dispatch, getState) => {
const state = getState();
dispatch({
Expand All @@ -172,17 +180,19 @@ const undoRetrieveMetadata = (itemKey, libraryKey) => {
libraryKey,
});
try {
const originalItemKey = state.recognize.lookup[`${libraryKey}-${itemKey}`];
if(!originalItemKey) {
const originalItemData = state.recognize.lookup[`${libraryKey}-${itemKey}`];
if(!originalItemData) {
throw new Error('Original item not found');
}
const { originalItemKey, originalFilename, originalTitle } = originalItemData;
const item = state.libraries[libraryKey].items?.[itemKey];
if(!item) {
throw new Error('Item not found');
}

const collections = item.collections;
await dispatch(updateItem(originalItemKey, { parentItem: false, collections }, libraryKey));
await dispatch(updateItem(originalItemKey, { parentItem: false, title: originalTitle, collections }, libraryKey));
await dispatch(renameAttachment(originalItemKey, originalFilename, libraryKey));
await dispatch(deleteItem(item));
dispatch({
type: COMPLETE_UNRECOGNIZE_DOCUMENT,
Expand Down
52 changes: 34 additions & 18 deletions src/js/common/format.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { unescapeHTML } from '../utils';
import { getItemTitle } from './item';

const entityToChar = str => {
const textarea = document.createElement('textarea');
Expand All @@ -13,7 +14,7 @@ const noteSummary = note => {
note
.replace(/<\s*\/?br\s*[/]?>/gi, ' ') // replace <br /> to spaces
.replace(/<(?:.|\n)*?>/gm, '') // remove html tags. This is still going to be sanitized by React.
).replace(/[\u202F\u00A0]/g, ' ') // replace no-break spaces to normal spaces
).replace(/[\u202F\u00A0]/g, ' ') // replace no-break spaces to normal spaces
.replace(/ +/g, " ") // remove series of spaces with just one
.substring(0, 180); // truncate to 180 chars
};
Expand Down Expand Up @@ -49,7 +50,7 @@ const dateLocalized = date => (date instanceof Date && !isNaN(date)) ?

//@TODO: figure out better place for this
const itemsSourceLabel = itemsSource => {
switch(itemsSource) {
switch (itemsSource) {
case 'trash':
return "Trash";
case 'publications':
Expand All @@ -69,14 +70,14 @@ const pluralize = (word, count) => count === 1 ? word : `${word}s`;

//@NOTE: should only be used for trusted/sanitized input
const stripTagsUsingDOM = html => {
const tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
const tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
}

const lpad = (string, pad, length) => {
string = string ? string + '' : '';
while(string.length < length) {
while (string.length < length) {
string = pad + string;
}
return string;
Expand All @@ -94,7 +95,7 @@ const formatDateTime = date =>

const parseDescriptiveString = str => {
var lc = str.toLowerCase().trim();
switch(lc) {
switch (lc) {
case 'yesterday':
return formatDate(new Date(new Date().getTime() - 86400000));
case 'today':
Expand Down Expand Up @@ -199,16 +200,31 @@ const renderItemTitle = (title, targetNode) => {
return textContent;
}

// {{ firstCreator suffix=" - " }}{{ year suffix=" - " }}{{ title truncate="100" }}
const getFileBaseNameFromItem = (item, mappings) => {
const title = getItemTitle(mappings, item);
const date = item[Symbol.for('meta')] && item[Symbol.for('meta')].parsedDate ?
item[Symbol.for('meta')].parsedDate :
'';
const creator = item[Symbol.for('meta')] && item[Symbol.for('meta')].creatorSummary ?
item[Symbol.for('meta')].creatorSummary :
'';
const year = date.substr(0, 4);

return `${creator}${creator ? ' - ' : ''}${year}${year ? ' - ' : ''}${title.substring(0, 100)}`;
}

export {
creator,
dateLocalized,
formatDate,
formatDateTime,
itemsSourceLabel,
noteSummary,
noteAsTitle,
parseDescriptiveString,
pluralize,
renderItemTitle,
stripTagsUsingDOM,
creator,
dateLocalized,
formatDate,
formatDateTime,
getFileBaseNameFromItem,
itemsSourceLabel,
noteAsTitle,
noteSummary,
parseDescriptiveString,
pluralize,
renderItemTitle,
stripTagsUsingDOM
};
6 changes: 3 additions & 3 deletions src/js/reducers/recognize.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getBaseMappedValue } from '../common/item';
import { omit } from 'web-common/utils';
import { omit, pick } from 'web-common/utils';
import { BEGIN_RECOGNIZE_DOCUMENT, CLEAR_RECOGNIZE_DOCUMENT, CLEAR_RECOGNIZE_DOCUMENTS, COMPLETE_RECOGNIZE_DOCUMENT,
ERROR_RECOGNIZE_DOCUMENT, UPDATE_RECOGNIZE_DOCUMENT, COMPLETE_UNRECOGNIZE_DOCUMENT, ERROR_UNRECOGNIZE_DOCUMENT } from '../constants/actions';

Expand All @@ -21,7 +21,7 @@ const getDefaultState = () => ({
backgroundTaskId: null, // id of the background task for all recognition processes, can only be updated by BEGIN_RECOGNIZE_DOCUMENT action
progress: 0,
entries: [], // items being recognized: { itemKey, itemTitle, libraryKey, stage, error, completed },
lookup: {}, // items previously recognized: { libraryKey-itemKey: parentItemKey }
lookup: {}, // items previously recognized: { libraryKey-itemKey: { originalItemKey, originalTitle, originalFilename }
});

const recognize = (state = getDefaultState(), action, globalState) => {
Expand Down Expand Up @@ -75,7 +75,7 @@ const recognize = (state = getDefaultState(), action, globalState) => {
}),
lookup: {
...state.lookup,
[`${action.libraryKey}-${action.parentItemKey}`]: action.itemKey,
[`${action.libraryKey}-${action.parentItemKey}`]: { originalItemKey: action.itemKey, ...pick(action, ['originalTitle', 'originalFilename']) },
}
});
case ERROR_RECOGNIZE_DOCUMENT:
Expand Down
20 changes: 20 additions & 0 deletions test/recognize.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('Metadata Retrieval', () => {
let hasPatchedAttachmentItem = false;
let hasDeleted = false;
let patchCounter = 0;
let renameCounter = 0;
let version = state.libraries.u1.sync.version;

server.use(
Expand Down Expand Up @@ -112,9 +113,11 @@ describe('Metadata Retrieval', () => {
const item = await request.json();
if (patchCounter === 0) {
expect(item.parentItem).toBe('S8CIV6VJ');
expect(item.title).toBe('PDF');
expect(item.collections).toEqual([]);
} else {
expect(item.parentItem).toBe(false);
expect(item.title).toBe('attention-is-all-you-need.pdf');
expect(item.collections).toEqual(['CSB4KZUU']);
}
hasPatchedAttachmentItem = true;
Expand All @@ -123,6 +126,19 @@ describe('Metadata Retrieval', () => {
await delay(100);
return new HttpResponse(null, { status: 204, headers: { 'Last-Modified-Version': version } });
}),
// rename attachment file
http.post('https://api.zotero.org/users/1/items/UMPPCXU4/file', async ({ request }) => {
const bodyParams = (await request.text()).split('&');
if(renameCounter === 0) {
expect(bodyParams).toContain('filename=Vaswani et al. - 2023 - Attention Is All You Need.pdf');
expect(bodyParams).toContain('md5=18e1b007a1dab45b30cc861ba2dfda25');
} else {
expect(bodyParams).toContain('filename=attention-is-all-you-need.pdf');
expect(bodyParams).toContain('md5=18e1b007a1dab45b30cc861ba2dfda25');
}
renameCounter++;
return HttpResponse.json({ 'exists': 1 });
}),
http.delete('https://api.zotero.org/users/1/items/S8CIV6VJ', async () => {
hasDeleted = true;
version++;
Expand Down Expand Up @@ -187,6 +203,8 @@ describe('Metadata Retrieval', () => {
expect(screen.queryByRole('row', { name: 'attention-is-all-you-need.pdf' })).not.toBeInTheDocument();

await user.click(screen.getByRole('row', { name: 'Attention Is All You Need' }));
expect(patchCounter).toBe(1);
expect(renameCounter).toBe(1);

// unregonize
const toolbar = screen.getByRole('toolbar', { name: 'items toolbar' });
Expand All @@ -200,6 +218,8 @@ describe('Metadata Retrieval', () => {
await waitFor(() => {
expect(screen.queryByRole('row', { name: 'Attention Is All You Need' })).not.toBeInTheDocument();
});
expect(patchCounter).toBe(2);
expect(renameCounter).toBe(2);
expect(hasDeleted).toBe(true);
});
});

0 comments on commit 9ab7680

Please sign in to comment.