feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,201 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, forwardRef, LegacyRef, ChangeEvent, CSSProperties, MouseEventHandler, DragEventHandler, useMemo, memo } from 'react';
|
||||
import { ItemFlow, ListRenderer, NoteListColumns, OnChangeEvent, OnChangeHandler } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import useRootElement from './utils/useRootElement';
|
||||
import useItemElement from './utils/useItemElement';
|
||||
import { ItemEventHandlers, OnInputChange } from './utils/types';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled';
|
||||
import NoteLockNote from '@joplin/lib/services/noteLock/NoteLockNote';
|
||||
import NoteLockSession from '@joplin/lib/services/noteLock/NoteLockSession';
|
||||
import { noteIsLockedInShare } from '@joplin/lib/models/utils/readOnly';
|
||||
import bridge from '../../services/bridge';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useRenderedNote from './utils/useRenderedNote';
|
||||
import { Dispatch } from 'redux';
|
||||
import getNoteElementIdFromJoplinId from './utils/getNoteElementIdFromJoplinId';
|
||||
import { HighlightedWord } from '@joplin/lib/reducer';
|
||||
|
||||
interface NoteItemProps {
|
||||
dragIndex: number;
|
||||
flow: ItemFlow;
|
||||
highlightedWords: HighlightedWord[];
|
||||
index: number;
|
||||
isProvisional: boolean;
|
||||
itemSize: Size;
|
||||
noteCount: number;
|
||||
onChange: OnChangeHandler;
|
||||
onClick: MouseEventHandler<HTMLDivElement>;
|
||||
onDoubleClick: MouseEventHandler<HTMLDivElement>;
|
||||
onContextMenu: MouseEventHandler;
|
||||
onDragOver: DragEventHandler;
|
||||
onDragStart: DragEventHandler;
|
||||
style: CSSProperties;
|
||||
note: NoteEntity;
|
||||
isWatched: boolean;
|
||||
isPublished: boolean;
|
||||
|
||||
isSelected: boolean;
|
||||
tabIndex: number;
|
||||
focusVisible: boolean;
|
||||
|
||||
listRenderer: ListRenderer;
|
||||
columns: NoteListColumns;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
const NoteListItem = (props: NoteItemProps, ref: LegacyRef<HTMLDivElement>) => {
|
||||
const noteId = props.note.id;
|
||||
const elementId = getNoteElementIdFromJoplinId(noteId);
|
||||
|
||||
const onInputChange: OnInputChange = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const getValue = (element: HTMLInputElement) => {
|
||||
if (element.type === 'checkbox') return element.checked;
|
||||
if (element.type === 'text') return element.value;
|
||||
throw new Error(`Unsupported element: ${element.type}`);
|
||||
};
|
||||
|
||||
const changeEvent: OnChangeEvent = {
|
||||
noteId: noteId,
|
||||
elementId: event.currentTarget.getAttribute('data-id'),
|
||||
value: getValue(event.currentTarget),
|
||||
};
|
||||
|
||||
if (changeEvent.elementId === 'todo-checkbox') {
|
||||
const lockState = await Note.load(changeEvent.noteId, { fields: ['is_locked', 'share_id'] });
|
||||
if (noteIsLockedInShare(lockState)) {
|
||||
event.target.checked = !changeEvent.value;
|
||||
bridge().showErrorMessageBox(_('This note is read-only because it is locked and contained within a share. To enable editing, it must be moved outside of the share.'));
|
||||
return;
|
||||
}
|
||||
if (isNoteLockEnabled()) {
|
||||
if (NoteLockNote.isLocked(lockState) && !NoteLockSession.instance().isUnlocked()) {
|
||||
// event.currentTarget is already cleared here, after the await - target is the same input
|
||||
event.target.checked = !changeEvent.value;
|
||||
bridge().showErrorMessageBox(_('Cannot change a locked note while the session is locked'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
await Note.save({
|
||||
id: changeEvent.noteId,
|
||||
todo_completed: changeEvent.value ? Date.now() : 0,
|
||||
}, { userSideValidation: true });
|
||||
|
||||
props.dispatch({ type: 'NOTE_SORT' });
|
||||
} else {
|
||||
if (props.onChange) await props.onChange(changeEvent);
|
||||
}
|
||||
}, [props.onChange, noteId, props.dispatch]);
|
||||
|
||||
const rootElement = useRootElement(elementId);
|
||||
|
||||
const renderedNote = useRenderedNote(props.note, props.isSelected, props.isWatched, props.isPublished, props.listRenderer, props.highlightedWords, props.index, props.columns);
|
||||
|
||||
const itemEventHandlers = useMemo((): ItemEventHandlers => ({ onInputChange, onClick: null }), [onInputChange]);
|
||||
|
||||
const displayTitle = useMemo(() => Note.displayTitle(props.note), [props.note]);
|
||||
|
||||
useItemElement(
|
||||
rootElement,
|
||||
noteId,
|
||||
renderedNote ? renderedNote.html : '',
|
||||
props.focusVisible,
|
||||
props.style,
|
||||
props.itemSize,
|
||||
props.onClick,
|
||||
props.onDoubleClick,
|
||||
props.flow,
|
||||
itemEventHandlers,
|
||||
displayTitle,
|
||||
);
|
||||
|
||||
|
||||
const className = useMemo(() => {
|
||||
return [
|
||||
'note-list-item-wrapper',
|
||||
|
||||
// This is not used by the app, but kept here because it may be used
|
||||
// by users for custom CSS.
|
||||
(props.index + 1) % 2 === 0 ? 'even' : 'odd',
|
||||
|
||||
props.isProvisional && '-provisional',
|
||||
].filter(e => !!e).join(' ');
|
||||
}, [props.index, props.isProvisional]);
|
||||
|
||||
const isActiveDragItem = props.dragIndex === props.index;
|
||||
const isLastActiveDragItem = props.index === props.noteCount - 1 && props.dragIndex >= props.noteCount;
|
||||
|
||||
const dragCursorStyle = useMemo(() => {
|
||||
if (props.flow === ItemFlow.TopToBottom) {
|
||||
let dragItemPosition = '';
|
||||
if (isActiveDragItem) {
|
||||
dragItemPosition = 'top';
|
||||
} else if (isLastActiveDragItem) {
|
||||
dragItemPosition = 'bottom';
|
||||
}
|
||||
|
||||
const output: React.CSSProperties = {
|
||||
width: props.itemSize.width,
|
||||
display: dragItemPosition ? 'block' : 'none',
|
||||
left: 0,
|
||||
};
|
||||
|
||||
if (dragItemPosition === 'top') {
|
||||
output.top = 0;
|
||||
} else {
|
||||
output.bottom = 0;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
if (props.flow === ItemFlow.LeftToRight) {
|
||||
let dragItemPosition = '';
|
||||
if (isActiveDragItem) {
|
||||
dragItemPosition = 'left';
|
||||
} else if (isLastActiveDragItem) {
|
||||
dragItemPosition = 'right';
|
||||
}
|
||||
|
||||
const output: React.CSSProperties = {
|
||||
height: props.itemSize.height,
|
||||
display: dragItemPosition ? 'block' : 'none',
|
||||
top: 0,
|
||||
};
|
||||
|
||||
if (dragItemPosition === 'left') {
|
||||
output.left = 0;
|
||||
} else {
|
||||
output.right = 0;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
throw new Error('Unreachable');
|
||||
}, [isActiveDragItem, isLastActiveDragItem, props.flow, props.itemSize]);
|
||||
|
||||
return <div
|
||||
id={elementId}
|
||||
ref={ref}
|
||||
draggable={true}
|
||||
tabIndex={props.tabIndex}
|
||||
className={className}
|
||||
data-id={noteId}
|
||||
style={{ height: props.itemSize.height }}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDragStart={props.onDragStart}
|
||||
onDragOver={props.onDragOver}
|
||||
|
||||
aria-selected={props.isSelected}
|
||||
aria-posinset={1 + props.index}
|
||||
aria-setsize={props.noteCount}
|
||||
role='option'
|
||||
>
|
||||
<div className="dragcursor" style={dragCursorStyle}></div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default memo(forwardRef(NoteListItem));
|
||||
在新工单中引用
屏蔽一个用户