mirror of
https://github.com/laurent22/joplin.git
synced 2025-03-23 21:09:30 +02:00
Desktop: Allow unsharing a note
This commit is contained in:
parent
6f2f24171d
commit
f7d164be6e
@ -18,6 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
enabledCondition: 'joplinServerConnected && oneNoteSelected',
|
||||
enabledCondition: 'joplinServerConnected && someNotesSelected',
|
||||
};
|
||||
};
|
||||
|
@ -110,6 +110,7 @@ function NoteListItem(props: NoteListItemProps, ref: any) {
|
||||
|
||||
let listItemTitleStyle = Object.assign({}, props.style.listItemTitle);
|
||||
listItemTitleStyle.paddingLeft = !item.is_todo ? hPadding : 4;
|
||||
if (item.is_shared) listItemTitleStyle.color = theme.colorWarn;
|
||||
if (item.is_todo && !!item.todo_completed) listItemTitleStyle = Object.assign(listItemTitleStyle, props.style.listItemTitleCompleted);
|
||||
|
||||
const displayTitle = Note.displayTitle(item);
|
||||
|
@ -10,19 +10,21 @@ import { reg } from '@joplin/lib/registry';
|
||||
import Dialog from './Dialog';
|
||||
import DialogTitle from './DialogTitle';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import { StateShare } from '@joplin/lib/services/share/reducer';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import Button from './Button/Button';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../app';
|
||||
const { clipboard } = require('electron');
|
||||
|
||||
interface ShareNoteDialogProps {
|
||||
interface Props {
|
||||
themeId: number;
|
||||
noteIds: Array<string>;
|
||||
onClose: Function;
|
||||
shares: StateShare[];
|
||||
}
|
||||
|
||||
interface SharesMap {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function styles_(props: ShareNoteDialogProps) {
|
||||
function styles_(props: Props) {
|
||||
return buildStyle('ShareNoteDialog', props.themeId, (theme: any) => {
|
||||
return {
|
||||
noteList: {
|
||||
@ -60,17 +62,21 @@ function styles_(props: ShareNoteDialogProps) {
|
||||
});
|
||||
}
|
||||
|
||||
export default function ShareNoteDialog(props: ShareNoteDialogProps) {
|
||||
export function ShareNoteDialog(props: Props) {
|
||||
console.info('Render ShareNoteDialog');
|
||||
|
||||
const [notes, setNotes] = useState<any[]>([]);
|
||||
const [notes, setNotes] = useState<NoteEntity[]>([]);
|
||||
const [sharesState, setSharesState] = useState<string>('unknown');
|
||||
const [shares, setShares] = useState<SharesMap>({});
|
||||
// const [shares, setShares] = useState<SharesMap>({});
|
||||
|
||||
const noteCount = notes.length;
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = styles_(props);
|
||||
|
||||
useEffect(() => {
|
||||
void ShareService.instance().refreshShares();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchNotes() {
|
||||
const result = [];
|
||||
@ -87,9 +93,9 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const copyLinksToClipboard = (shares: SharesMap) => {
|
||||
const copyLinksToClipboard = (shares: StateShare[]) => {
|
||||
const links = [];
|
||||
for (const n in shares) links.push(ShareService.instance().shareUrl(shares[n]));
|
||||
for (const share of shares) links.push(ShareService.instance().shareUrl(share));
|
||||
clipboard.writeText(links.join('\n'));
|
||||
};
|
||||
|
||||
@ -109,15 +115,13 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
|
||||
|
||||
setSharesState('creating');
|
||||
|
||||
const newShares = Object.assign({}, shares);
|
||||
const newShares: StateShare[] = [];
|
||||
|
||||
for (const note of notes) {
|
||||
const share = await service.shareNote(note.id);
|
||||
newShares[note.id] = share;
|
||||
newShares.push(share);
|
||||
}
|
||||
|
||||
setShares(newShares);
|
||||
|
||||
setSharesState('synchronizing');
|
||||
await reg.waitForSyncFinishedThenSync();
|
||||
setSharesState('creating');
|
||||
@ -125,6 +129,8 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
|
||||
copyLinksToClipboard(newShares);
|
||||
|
||||
setSharesState('created');
|
||||
|
||||
await ShareService.instance().refreshShares();
|
||||
} catch (error) {
|
||||
if (error.code === 404 && !hasSynced) {
|
||||
reg.logger().info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
|
||||
@ -142,34 +148,53 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const removeNoteButton_click = (event: any) => {
|
||||
const newNotes = [];
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const n = notes[i];
|
||||
if (n.id === event.noteId) continue;
|
||||
newNotes.push(n);
|
||||
}
|
||||
setNotes(newNotes);
|
||||
// const removeNoteButton_click = (event: any) => {
|
||||
// const newNotes = [];
|
||||
// for (let i = 0; i < notes.length; i++) {
|
||||
// const n = notes[i];
|
||||
// if (n.id === event.noteId) continue;
|
||||
// newNotes.push(n);
|
||||
// }
|
||||
// setNotes(newNotes);
|
||||
// };
|
||||
|
||||
const unshareNoteButton_click = async (event: any) => {
|
||||
await ShareService.instance().unshareNote(event.noteId);
|
||||
await ShareService.instance().refreshShares();
|
||||
};
|
||||
|
||||
const renderNote = (note: any) => {
|
||||
const removeButton = notes.length <= 1 ? null : (
|
||||
<button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
|
||||
<i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
|
||||
</button>
|
||||
const renderNote = (note: NoteEntity) => {
|
||||
const unshareButton = !props.shares.find(s => s.note_id === note.id) ? null : (
|
||||
<Button tooltip={_('Unshare note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
|
||||
);
|
||||
|
||||
// const removeButton = notes.length <= 1 ? null : (
|
||||
// <Button iconName="fa fa-times" onClick={() => removeNoteButton_click({ noteId: note.id })}/>
|
||||
// );
|
||||
|
||||
// const unshareButton = !shares[note.id] ? null : (
|
||||
// <button onClick={() => unshareNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
|
||||
// <i style={styles.noteRemoveButtonIcon} className={'fas fa-share-alt'}></i>
|
||||
// </button>
|
||||
// );
|
||||
|
||||
// const removeButton = notes.length <= 1 ? null : (
|
||||
// <button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
|
||||
// <i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
|
||||
// </button>
|
||||
// );
|
||||
|
||||
return (
|
||||
<div key={note.id} style={styles.note}>
|
||||
<span style={styles.noteTitle}>{note.title}</span>{removeButton}
|
||||
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderNoteList = (notes: any) => {
|
||||
const noteComps = [];
|
||||
for (const noteId of Object.keys(notes)) {
|
||||
noteComps.push(renderNote(notes[noteId]));
|
||||
for (const note of notes) {
|
||||
noteComps.push(renderNote(note));
|
||||
}
|
||||
return <div style={styles.noteList}>{noteComps}</div>;
|
||||
};
|
||||
@ -194,7 +219,12 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
|
||||
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
|
||||
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
|
||||
{renderEncryptionWarningMessage()}
|
||||
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
onClick={buttonRow_click}
|
||||
okButtonShow={false}
|
||||
cancelButtonLabel={_('Close')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -203,3 +233,11 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
|
||||
<Dialog renderContent={renderContent}/>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
shares: state.shareService.shares.filter(s => !!s.note_id),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ShareNoteDialog as any);
|
||||
|
@ -69,7 +69,6 @@ function listItemTextColor(props: any) {
|
||||
|
||||
export const StyledListItemAnchor = styled.a`
|
||||
font-size: ${(props: any) => Math.round(props.theme.fontSize * 1.0833333)}px;
|
||||
// font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: ${(props: any) => listItemTextColor(props)};
|
||||
cursor: default;
|
||||
|
@ -35,6 +35,7 @@ if [ "$1" == "1" ]; then
|
||||
echo 'mkbook "other"' >> "$CMD_FILE"
|
||||
echo 'use "shared"' >> "$CMD_FILE"
|
||||
echo 'mknote "note 1"' >> "$CMD_FILE"
|
||||
echo 'mknote "note 2"' >> "$CMD_FILE"
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR/packages/app-cli"
|
||||
|
@ -347,7 +347,8 @@ export default class Folder extends BaseItem {
|
||||
public static async updateResourceShareIds() {
|
||||
// Find all resources where share_id is different from parent note
|
||||
// share_id. Then update share_id on all these resources. Essentially it
|
||||
// makes it match the resource share_id to the note share_id.
|
||||
// makes it match the resource share_id to the note share_id. At the
|
||||
// same time we also process the is_shared property.
|
||||
const rows = await this.db().selectAll(`
|
||||
SELECT r.id, n.share_id, n.is_shared
|
||||
FROM note_resources nr
|
||||
|
@ -307,7 +307,7 @@ export default class Note extends BaseItem {
|
||||
includeTimestamps: true,
|
||||
}, options);
|
||||
|
||||
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language', 'is_conflict'];
|
||||
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language', 'is_conflict', 'is_shared'];
|
||||
|
||||
if (options.includeTimestamps) {
|
||||
output.push('updated_time');
|
||||
|
@ -104,7 +104,7 @@ export default class ShareService {
|
||||
await Folder.updateAllShareIds();
|
||||
}
|
||||
|
||||
public async shareNote(noteId: string) {
|
||||
public async shareNote(noteId: string): Promise<StateShare> {
|
||||
const note = await Note.load(noteId);
|
||||
if (!note) throw new Error(`No such note: ${noteId}`);
|
||||
|
||||
@ -115,6 +115,24 @@ export default class ShareService {
|
||||
return share;
|
||||
}
|
||||
|
||||
public async unshareNote(noteId: string) {
|
||||
const note = await Note.load(noteId);
|
||||
if (!note) throw new Error(`No such note: ${noteId}`);
|
||||
|
||||
const shares = await this.refreshShares();
|
||||
const noteShares = shares.filter(s => s.note_id === noteId);
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const share of noteShares) {
|
||||
promises.push(this.deleteShare(share.id));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
await Note.save({ id: note.id, is_shared: 0 });
|
||||
}
|
||||
|
||||
public shareUrl(share: StateShare): string {
|
||||
return `${this.api().baseUrl()}/shares/${share.id}`;
|
||||
}
|
||||
@ -170,13 +188,15 @@ export default class ShareService {
|
||||
});
|
||||
}
|
||||
|
||||
public async refreshShares() {
|
||||
public async refreshShares(): Promise<StateShare[]> {
|
||||
const result = await this.loadShares();
|
||||
|
||||
this.store.dispatch({
|
||||
type: 'SHARE_SET',
|
||||
shares: result.items,
|
||||
});
|
||||
|
||||
return result.items;
|
||||
}
|
||||
|
||||
public async refreshShareUsers(shareId: string) {
|
||||
|
@ -11,7 +11,7 @@ const theme: Theme = {
|
||||
oddBackgroundColor: '#eeeeee',
|
||||
color: '#32373F', // For regular text
|
||||
colorError: 'red',
|
||||
colorWarn: '#9A5B00',
|
||||
colorWarn: 'rgb(228 86 0)',
|
||||
colorFaded: '#7C8B9E', // For less important text
|
||||
colorBright: '#000000', // For important text
|
||||
dividerColor: '#dddddd',
|
||||
|
Loading…
x
Reference in New Issue
Block a user