feat(editor): add ui for display block meta in toolbar (#10817)
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
export * from './signal-watcher.js';
|
export * from './signal-watcher.js';
|
||||||
|
export * from './watch.js';
|
||||||
export * from './with-disposable.js';
|
export * from './with-disposable.js';
|
||||||
|
|||||||
68
blocksuite/framework/global/src/lit/watch.ts
Normal file
68
blocksuite/framework/global/src/lit/watch.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Signal } from '@preact/signals-core';
|
||||||
|
import { AsyncDirective } from 'lit/async-directive.js';
|
||||||
|
import { directive } from 'lit/directive.js';
|
||||||
|
|
||||||
|
class WatchDirective extends AsyncDirective {
|
||||||
|
private __signal?: Signal;
|
||||||
|
private __dispose?: () => void;
|
||||||
|
|
||||||
|
override render(signal: Signal<unknown>) {
|
||||||
|
if (signal !== this.__signal) {
|
||||||
|
this.__dispose?.();
|
||||||
|
this.__signal = signal;
|
||||||
|
|
||||||
|
// Whether the subscribe() callback is called because of this render
|
||||||
|
// pass, or because of a separate signal update.
|
||||||
|
let updateFromLit = true;
|
||||||
|
this.__dispose = signal.subscribe(value => {
|
||||||
|
// The subscribe() callback is called synchronously during subscribe.
|
||||||
|
// Ignore the first call since we return the value below in that case.
|
||||||
|
if (updateFromLit === false) {
|
||||||
|
this.setValue(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateFromLit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use peek() so that the signal access is not tracked by the effect
|
||||||
|
// created by SignalWatcher.performUpdate(). This means that a signal
|
||||||
|
// update won't trigger a full element update if it's only passed to
|
||||||
|
// watch() and not otherwise accessed by the element.
|
||||||
|
return signal.peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override disconnected(): void {
|
||||||
|
this.__dispose?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override reconnected(): void {
|
||||||
|
// Since we disposed the subscription in disconnected() we need to
|
||||||
|
// resubscribe here. We don't ignore the synchronous callback call because
|
||||||
|
// the signal might have changed while the directive is disconnected.
|
||||||
|
//
|
||||||
|
// There are two possible reasons for a disconnect:
|
||||||
|
// 1. The host element was disconnected.
|
||||||
|
// 2. The directive was not rendered during a render
|
||||||
|
// In the first case the element will not schedule an update on reconnect,
|
||||||
|
// so we need the synchronous call here to set the current value.
|
||||||
|
// In the second case, we're probably reconnecting *because* of a render,
|
||||||
|
// so the synchronous call here will go before a render call, and we'll get
|
||||||
|
// two sets of the value (setValue() here and the return in render()), but
|
||||||
|
// this is ok because the value will be dirty-checked by lit-html.
|
||||||
|
this.__dispose = this.__signal?.subscribe(value => {
|
||||||
|
this.setValue(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a signal and subscribes to it, updating the part when the signal
|
||||||
|
* changes.
|
||||||
|
*/
|
||||||
|
export const watch = directive(WatchDirective);
|
||||||
@@ -39,6 +39,7 @@ import type {
|
|||||||
MenuContext,
|
MenuContext,
|
||||||
MenuItemGroup,
|
MenuItemGroup,
|
||||||
} from '@blocksuite/affine/components/toolbar';
|
} from '@blocksuite/affine/components/toolbar';
|
||||||
|
import { watch } from '@blocksuite/affine/global/lit';
|
||||||
import {
|
import {
|
||||||
BookmarkBlockModel,
|
BookmarkBlockModel,
|
||||||
EmbedLinkedDocModel,
|
EmbedLinkedDocModel,
|
||||||
@@ -52,13 +53,16 @@ import { getSelectedModelsCommand } from '@blocksuite/affine/shared/commands';
|
|||||||
import { ImageSelection } from '@blocksuite/affine/shared/selection';
|
import { ImageSelection } from '@blocksuite/affine/shared/selection';
|
||||||
import {
|
import {
|
||||||
ActionPlacement,
|
ActionPlacement,
|
||||||
|
FeatureFlagService,
|
||||||
GenerateDocUrlProvider,
|
GenerateDocUrlProvider,
|
||||||
|
isRemovedUserInfo,
|
||||||
type OpenDocMode,
|
type OpenDocMode,
|
||||||
type ToolbarAction,
|
type ToolbarAction,
|
||||||
type ToolbarActionGroup,
|
type ToolbarActionGroup,
|
||||||
type ToolbarContext,
|
type ToolbarContext,
|
||||||
type ToolbarModuleConfig,
|
type ToolbarModuleConfig,
|
||||||
ToolbarModuleExtension,
|
ToolbarModuleExtension,
|
||||||
|
UserProvider,
|
||||||
} from '@blocksuite/affine/shared/services';
|
} from '@blocksuite/affine/shared/services';
|
||||||
import { matchModels } from '@blocksuite/affine/shared/utils';
|
import { matchModels } from '@blocksuite/affine/shared/utils';
|
||||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||||
@@ -73,11 +77,13 @@ import {
|
|||||||
OpenInNewIcon,
|
OpenInNewIcon,
|
||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
|
import { computed } from '@preact/signals-core';
|
||||||
import type { FrameworkProvider } from '@toeverything/infra';
|
import type { FrameworkProvider } from '@toeverything/infra';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { keyed } from 'lit/directives/keyed.js';
|
import { keyed } from 'lit/directives/keyed.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { createCopyAsPngMenuItem } from './copy-as-image';
|
import { createCopyAsPngMenuItem } from './copy-as-image';
|
||||||
|
|
||||||
@@ -297,6 +303,77 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
placement: ActionPlacement.More,
|
||||||
|
id: 'z.block-meta',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'block-meta-display',
|
||||||
|
when: cx => {
|
||||||
|
const featureFlag = cx.std.get(FeatureFlagService);
|
||||||
|
const isEnabled = featureFlag.getFlag('enable_block_meta');
|
||||||
|
if (!isEnabled) return false;
|
||||||
|
|
||||||
|
// only display when one block is selected by block selection
|
||||||
|
const { selection, getCurrentBlockBy } = cx;
|
||||||
|
const hasBlockSelection =
|
||||||
|
selection.filter(BlockSelection).length === 1;
|
||||||
|
if (!hasBlockSelection) return false;
|
||||||
|
const block = getCurrentBlockBy.call(cx, BlockSelection);
|
||||||
|
if (!block) return false;
|
||||||
|
|
||||||
|
const createdAt = 'meta:createdAt';
|
||||||
|
const createdBy = 'meta:createdBy';
|
||||||
|
return (
|
||||||
|
createdAt in block.model &&
|
||||||
|
block.model[createdAt] !== undefined &&
|
||||||
|
createdBy in block.model &&
|
||||||
|
block.model[createdBy] !== undefined &&
|
||||||
|
typeof block.model[createdBy] === 'string' &&
|
||||||
|
typeof block.model[createdAt] === 'number'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
content: cx => {
|
||||||
|
const model = cx.getCurrentModelBy(BlockSelection);
|
||||||
|
if (!model) return null;
|
||||||
|
const createdAt = 'meta:createdAt';
|
||||||
|
if (!(createdAt in model)) return null;
|
||||||
|
const createdBy = 'meta:createdBy';
|
||||||
|
if (!(createdBy in model)) return null;
|
||||||
|
const createdByUserId = model[createdBy] as string;
|
||||||
|
const createdAtTimestamp = model[createdAt] as number;
|
||||||
|
const date = new Date(createdAtTimestamp);
|
||||||
|
const userProvider = cx.std.get(UserProvider);
|
||||||
|
userProvider.revalidateUserInfo(createdByUserId);
|
||||||
|
const userSignal = userProvider.userInfo$(createdByUserId);
|
||||||
|
userSignal.subscribe(console.log);
|
||||||
|
const user = computed(() => {
|
||||||
|
const value = userSignal.value;
|
||||||
|
if (!value) return 'Unknown User';
|
||||||
|
const removed = isRemovedUserInfo(value);
|
||||||
|
if (removed) return 'Deleted User';
|
||||||
|
return value.name;
|
||||||
|
});
|
||||||
|
const createdAtString = date.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
const wrapperStyle = {
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
};
|
||||||
|
return html`<div style=${styleMap(wrapperStyle)}>
|
||||||
|
<div>Created by ${watch(user)}</div>
|
||||||
|
<div>${createdAtString}</div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
} as const satisfies ToolbarModuleConfig;
|
} as const satisfies ToolbarModuleConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user