feat(core): guard service (#9816)
This commit is contained in:
89 files changed
+1520
-522
No files matched your search
@@ -11,13 +11,13 @@ export class PermissionModule {}
|
|||||||
export { PermissionService } from './service';
|
export { PermissionService } from './service';
|
||||||
export {
|
export {
|
||||||
DOC_ACTIONS,
|
DOC_ACTIONS,
|
||||||
type DocActionPermissions,
|
type DocAction,
|
||||||
DocRole,
|
DocRole,
|
||||||
fixupDocRole,
|
fixupDocRole,
|
||||||
mapDocRoleToPermissions,
|
mapDocRoleToPermissions,
|
||||||
mapWorkspaceRoleToPermissions,
|
mapWorkspaceRoleToPermissions,
|
||||||
PublicDocMode,
|
PublicDocMode,
|
||||||
WORKSPACE_ACTIONS,
|
WORKSPACE_ACTIONS,
|
||||||
type WorkspaceActionPermissions,
|
type WorkspaceAction,
|
||||||
WorkspaceRole,
|
WorkspaceRole,
|
||||||
} from './types';
|
} from './types';
|
||||||
@@ -146,12 +146,6 @@ type ResourceActionName<T extends keyof typeof Actions> =
|
|||||||
export type WorkspaceAction = ResourceActionName<'Workspace'>;
|
export type WorkspaceAction = ResourceActionName<'Workspace'>;
|
||||||
export type DocAction = ResourceActionName<'Doc'>;
|
export type DocAction = ResourceActionName<'Doc'>;
|
||||||
export type Action = WorkspaceAction | DocAction;
|
export type Action = WorkspaceAction | DocAction;
|
||||||
export type WorkspaceActionPermissions = {
|
|
||||||
[key in WorkspaceAction]: boolean;
|
|
||||||
};
|
|
||||||
export type DocActionPermissions = {
|
|
||||||
[key in DocAction]: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cache = new WeakMap<object, any>();
|
const cache = new WeakMap<object, any>();
|
||||||
const buildPathReader = (
|
const buildPathReader = (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { Models } from '../../../models';
|
|||||||
import { CurrentUser } from '../../auth';
|
import { CurrentUser } from '../../auth';
|
||||||
import {
|
import {
|
||||||
DOC_ACTIONS,
|
DOC_ACTIONS,
|
||||||
type DocActionPermissions,
|
DocAction,
|
||||||
DocRole,
|
DocRole,
|
||||||
fixupDocRole,
|
fixupDocRole,
|
||||||
mapDocRoleToPermissions,
|
mapDocRoleToPermissions,
|
||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
import { PublicUserType } from '../../user';
|
import { PublicUserType } from '../../user';
|
||||||
import { DocID } from '../../utils/doc';
|
import { DocID } from '../../utils/doc';
|
||||||
import { WorkspaceType } from '../types';
|
import { WorkspaceType } from '../types';
|
||||||
|
import { DotToUnderline, mapPermissionToGraphqlPermissions } from './workspace';
|
||||||
|
|
||||||
registerEnumType(PublicDocMode, {
|
registerEnumType(PublicDocMode, {
|
||||||
name: 'PublicDocMode',
|
name: 'PublicDocMode',
|
||||||
@@ -128,10 +129,12 @@ class GrantedDocUserType {
|
|||||||
@ObjectType()
|
@ObjectType()
|
||||||
class PaginatedGrantedDocUserType extends Paginated(GrantedDocUserType) {}
|
class PaginatedGrantedDocUserType extends Paginated(GrantedDocUserType) {}
|
||||||
|
|
||||||
const DocPermissions = registerObjectType<DocActionPermissions>(
|
const DocPermissions = registerObjectType<
|
||||||
|
Record<DotToUnderline<DocAction>, boolean>
|
||||||
|
>(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
DOC_ACTIONS.map(action => [
|
DOC_ACTIONS.map(action => [
|
||||||
action,
|
action.replaceAll('.', '_'),
|
||||||
{
|
{
|
||||||
type: () => Boolean,
|
type: () => Boolean,
|
||||||
options: {
|
options: {
|
||||||
@@ -143,15 +146,6 @@ const DocPermissions = registerObjectType<DocActionPermissions>(
|
|||||||
{ name: 'DocPermissions' }
|
{ name: 'DocPermissions' }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class DocRolePermissions {
|
|
||||||
@Field(() => DocRole)
|
|
||||||
role!: DocRole;
|
|
||||||
|
|
||||||
@Field(() => DocPermissions)
|
|
||||||
permissions!: DocActionPermissions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Resolver(() => WorkspaceType)
|
@Resolver(() => WorkspaceType)
|
||||||
export class WorkspaceDocResolver {
|
export class WorkspaceDocResolver {
|
||||||
private readonly logger = new Logger(WorkspaceDocResolver.name);
|
private readonly logger = new Logger(WorkspaceDocResolver.name);
|
||||||
@@ -365,7 +359,7 @@ export class DocResolver {
|
|||||||
async permissions(
|
async permissions(
|
||||||
@CurrentUser() user: CurrentUser,
|
@CurrentUser() user: CurrentUser,
|
||||||
@Parent() doc: DocType
|
@Parent() doc: DocType
|
||||||
): Promise<DocRolePermissions> {
|
): Promise<InstanceType<typeof DocPermissions>> {
|
||||||
const [permission, workspacePermission] = await this.prisma.$transaction(
|
const [permission, workspacePermission] = await this.prisma.$transaction(
|
||||||
tx =>
|
tx =>
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -385,12 +379,11 @@ export class DocResolver {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return mapPermissionToGraphqlPermissions(
|
||||||
role: permission?.type ?? DocRole.External,
|
mapDocRoleToPermissions(
|
||||||
permissions: mapDocRoleToPermissions(
|
|
||||||
fixupDocRole(workspacePermission?.type, permission?.type)
|
fixupDocRole(workspacePermission?.type, permission?.type)
|
||||||
),
|
)
|
||||||
};
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => PaginatedGrantedDocUserType, {
|
@ResolveField(() => PaginatedGrantedDocUserType, {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
mapWorkspaceRoleToPermissions,
|
mapWorkspaceRoleToPermissions,
|
||||||
PermissionService,
|
PermissionService,
|
||||||
WORKSPACE_ACTIONS,
|
WORKSPACE_ACTIONS,
|
||||||
type WorkspaceActionPermissions,
|
WorkspaceAction,
|
||||||
WorkspaceRole,
|
WorkspaceRole,
|
||||||
} from '../../permission';
|
} from '../../permission';
|
||||||
import { QuotaService, WorkspaceQuotaType } from '../../quota';
|
import { QuotaService, WorkspaceQuotaType } from '../../quota';
|
||||||
@@ -51,6 +51,22 @@ import {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import { WorkspaceService } from './service';
|
import { WorkspaceService } from './service';
|
||||||
|
|
||||||
|
export type DotToUnderline<T extends string> =
|
||||||
|
T extends `${infer Prefix}.${infer Suffix}`
|
||||||
|
? `${Prefix}_${DotToUnderline<Suffix>}`
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export function mapPermissionToGraphqlPermissions<A extends string>(
|
||||||
|
permission: Record<A, boolean>
|
||||||
|
): Record<DotToUnderline<A>, boolean> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(permission).map(([key, value]) => [
|
||||||
|
key.replaceAll('.', '_'),
|
||||||
|
value,
|
||||||
|
])
|
||||||
|
) as Record<DotToUnderline<A>, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class EditorType implements Partial<Editor> {
|
export class EditorType implements Partial<Editor> {
|
||||||
@Field()
|
@Field()
|
||||||
@@ -75,10 +91,12 @@ class WorkspacePageMeta {
|
|||||||
updatedBy!: EditorType | null;
|
updatedBy!: EditorType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkspacePermissions = registerObjectType<WorkspaceActionPermissions>(
|
const WorkspacePermissions = registerObjectType<
|
||||||
|
Record<DotToUnderline<WorkspaceAction>, boolean>
|
||||||
|
>(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
WORKSPACE_ACTIONS.map(action => [
|
WORKSPACE_ACTIONS.map(action => [
|
||||||
action,
|
action.replaceAll('.', '_'),
|
||||||
{
|
{
|
||||||
type: () => Boolean,
|
type: () => Boolean,
|
||||||
options: {
|
options: {
|
||||||
@@ -96,7 +114,7 @@ export class WorkspaceRolePermissions {
|
|||||||
role!: WorkspaceRole;
|
role!: WorkspaceRole;
|
||||||
|
|
||||||
@Field(() => WorkspacePermissions)
|
@Field(() => WorkspacePermissions)
|
||||||
permissions!: WorkspaceActionPermissions;
|
permissions!: Record<DotToUnderline<WorkspaceAction>, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -342,7 +360,7 @@ export class WorkspaceResolver {
|
|||||||
async workspaceRolePermissions(
|
async workspaceRolePermissions(
|
||||||
@CurrentUser() user: CurrentUser,
|
@CurrentUser() user: CurrentUser,
|
||||||
@Args('id') id: string
|
@Args('id') id: string
|
||||||
) {
|
): Promise<WorkspaceRolePermissions> {
|
||||||
const workspace = await this.prisma.workspaceUserPermission.findFirst({
|
const workspace = await this.prisma.workspaceUserPermission.findFirst({
|
||||||
where: { workspaceId: id, userId: user.id },
|
where: { workspaceId: id, userId: user.id },
|
||||||
});
|
});
|
||||||
@@ -351,7 +369,9 @@ export class WorkspaceResolver {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
role: workspace.type,
|
role: workspace.type,
|
||||||
permissions: mapWorkspaceRoleToPermissions(workspace.type),
|
permissions: mapPermissionToGraphqlPermissions(
|
||||||
|
mapWorkspaceRoleToPermissions(workspace.type)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ summary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
button {
|
button:not([disabled]) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export const button = style({
|
|||||||
outline: 0,
|
outline: 0,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
transition: 'all .3s',
|
transition: 'all .3s',
|
||||||
cursor: 'pointer',
|
|
||||||
['WebkitAppRegion' as string]: 'no-drag',
|
['WebkitAppRegion' as string]: 'no-drag',
|
||||||
|
|
||||||
// hover layer
|
// hover layer
|
||||||
@@ -169,6 +168,10 @@ export const button = style({
|
|||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'&:not([data-disabled])': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
|
||||||
// default keyboard focus style
|
// default keyboard focus style
|
||||||
'&:focus-visible::after': {
|
'&:focus-visible::after': {
|
||||||
content: '""',
|
content: '""',
|
||||||
|
|||||||
@@ -316,7 +316,11 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
|||||||
|
|
||||||
// external data is only available in drop event thus
|
// external data is only available in drop event thus
|
||||||
// this is the only case for getAdaptedEventArgs
|
// this is the only case for getAdaptedEventArgs
|
||||||
const args = getAdaptedEventArgs(_args, options.fromExternalData, true);
|
const args = {
|
||||||
|
...getAdaptedEventArgs(_args, options.fromExternalData, true),
|
||||||
|
treeInstruction: extractInstruction(_args.self.data),
|
||||||
|
closestEdge: extractClosestEdge(_args.self.data),
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
isExternalDrag(_args) &&
|
isExternalDrag(_args) &&
|
||||||
options.fromExternalData &&
|
options.fromExternalData &&
|
||||||
@@ -329,11 +333,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.location.current.dropTargets[0]?.element === element) {
|
if (args.location.current.dropTargets[0]?.element === element) {
|
||||||
options.onDrop?.({
|
options.onDrop?.(args as DropTargetDropEvent<D>);
|
||||||
...args,
|
|
||||||
treeInstruction: extractInstruction(args.self.data),
|
|
||||||
closestEdge: extractClosestEdge(args.self.data),
|
|
||||||
} as DropTargetDropEvent<D>);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getData: (args: DropTargetGetFeedback<D>) => {
|
getData: (args: DropTargetGetFeedback<D>) => {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const menuItem = style({
|
|||||||
'&.block': {
|
'&.block': {
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
},
|
},
|
||||||
'&[data-disabled]': {
|
'&[data-disabled], &.disabled': {
|
||||||
vars: {
|
vars: {
|
||||||
[iconColor]: cssVarV2('icon/disable'),
|
[iconColor]: cssVarV2('icon/disable'),
|
||||||
[labelColor]: cssVarV2('text/secondary'),
|
[labelColor]: cssVarV2('text/secondary'),
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ export const useMenuItem = <T extends MenuItemProps>({
|
|||||||
checked,
|
checked,
|
||||||
selected,
|
selected,
|
||||||
block,
|
block,
|
||||||
|
disabled,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: T) => {
|
}: T) => {
|
||||||
const className = clsx(
|
const className = clsx(
|
||||||
styles.menuItem,
|
styles.menuItem,
|
||||||
{
|
{
|
||||||
danger: type === 'danger',
|
danger: disabled ? false : type === 'danger',
|
||||||
warning: type === 'warning',
|
warning: disabled ? false : type === 'warning',
|
||||||
|
disabled,
|
||||||
checked,
|
checked,
|
||||||
selected,
|
selected,
|
||||||
block,
|
block,
|
||||||
|
|||||||
@@ -133,10 +133,12 @@ export const propertyValueContainer = style({
|
|||||||
'&[data-readonly="false"][data-hoverable="true"]': {
|
'&[data-readonly="false"][data-hoverable="true"]': {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
},
|
},
|
||||||
'&[data-readonly="false"][data-hoverable="true"]:is(:hover, :focus-within)':
|
'&[data-readonly="true"][data-hoverable="true"]': {
|
||||||
{
|
cursor: 'default',
|
||||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
},
|
||||||
},
|
'&[data-hoverable="true"]:is(:hover, :focus-within)': {
|
||||||
|
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export const RadioGroup = memo(function RadioGroup({
|
|||||||
indicatorStyle,
|
indicatorStyle,
|
||||||
iconMode,
|
iconMode,
|
||||||
onChange,
|
onChange,
|
||||||
|
disabled,
|
||||||
}: RadioProps) {
|
}: RadioProps) {
|
||||||
const animationTImerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const animationTImerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const finalItems = useMemo(() => {
|
const finalItems = useMemo(() => {
|
||||||
@@ -158,6 +159,7 @@ export const RadioGroup = memo(function RadioGroup({
|
|||||||
className={clsx(styles.radioButtonGroup, className)}
|
className={clsx(styles.radioButtonGroup, className)}
|
||||||
style={finalStyle}
|
style={finalStyle}
|
||||||
data-icon-mode={iconMode}
|
data-icon-mode={iconMode}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{finalItems.map(({ customRender, ...item }, index) => {
|
{finalItems.map(({ customRender, ...item }, index) => {
|
||||||
const testId = item.testId ? { 'data-testid': item.testId } : {};
|
const testId = item.testId ? { 'data-testid': item.testId } : {};
|
||||||
@@ -179,6 +181,7 @@ export const RadioGroup = memo(function RadioGroup({
|
|||||||
style={style}
|
style={style}
|
||||||
{...testId}
|
{...testId}
|
||||||
{...item.attrs}
|
{...item.attrs}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<RadixRadioGroup.Indicator
|
<RadixRadioGroup.Indicator
|
||||||
forceMount
|
forceMount
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const radioButton = style({
|
|||||||
'&[data-state="checked"]': {
|
'&[data-state="checked"]': {
|
||||||
color: cssVarV2('switch/fontColor/primary'),
|
color: cssVarV2('switch/fontColor/primary'),
|
||||||
},
|
},
|
||||||
'&[data-state="unchecked"]:hover': {
|
'&[data-state="unchecked"]:hover:not([disabled])': {
|
||||||
background: cssVarV2('switch/buttonBackground/hover'),
|
background: cssVarV2('switch/buttonBackground/hover'),
|
||||||
},
|
},
|
||||||
'[data-icon-mode=true] &': {
|
'[data-icon-mode=true] &': {
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ const REPLACE_SELECTION = {
|
|||||||
icon: ReplaceIcon,
|
icon: ReplaceIcon,
|
||||||
title: 'Replace selection',
|
title: 'Replace selection',
|
||||||
showWhen: (host: EditorHost) => {
|
showWhen: (host: EditorHost) => {
|
||||||
|
if (host.std.store.readonly$.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const textSelection = host.selection.find(TextSelection);
|
const textSelection = host.selection.find(TextSelection);
|
||||||
const blockSelections = host.selection.filter(BlockSelection);
|
const blockSelections = host.selection.filter(BlockSelection);
|
||||||
if (
|
if (
|
||||||
@@ -252,7 +255,12 @@ const REPLACE_SELECTION = {
|
|||||||
const INSERT_BELOW = {
|
const INSERT_BELOW = {
|
||||||
icon: InsertBelowIcon,
|
icon: InsertBelowIcon,
|
||||||
title: 'Insert below',
|
title: 'Insert below',
|
||||||
showWhen: () => true,
|
showWhen: (host: EditorHost) => {
|
||||||
|
if (host.std.store.readonly$.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
toast: 'Successfully inserted',
|
toast: 'Successfully inserted',
|
||||||
handler: async (
|
handler: async (
|
||||||
host: EditorHost,
|
host: EditorHost,
|
||||||
@@ -282,7 +290,12 @@ const SAVE_CHAT_TO_BLOCK_ACTION: ChatAction = {
|
|||||||
icon: BlockIcon,
|
icon: BlockIcon,
|
||||||
title: 'Save chat to block',
|
title: 'Save chat to block',
|
||||||
toast: 'Successfully saved chat to a block',
|
toast: 'Successfully saved chat to a block',
|
||||||
showWhen: () => true,
|
showWhen: (host: EditorHost) => {
|
||||||
|
if (host.std.store.readonly$.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
handler: async (
|
handler: async (
|
||||||
host: EditorHost,
|
host: EditorHost,
|
||||||
_,
|
_,
|
||||||
@@ -378,7 +391,12 @@ const SAVE_CHAT_TO_BLOCK_ACTION: ChatAction = {
|
|||||||
const ADD_TO_EDGELESS_AS_NOTE = {
|
const ADD_TO_EDGELESS_AS_NOTE = {
|
||||||
icon: CreateIcon,
|
icon: CreateIcon,
|
||||||
title: 'Add to edgeless as note',
|
title: 'Add to edgeless as note',
|
||||||
showWhen: () => true,
|
showWhen: (host: EditorHost) => {
|
||||||
|
if (host.std.store.readonly$.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
toast: 'New note created',
|
toast: 'New note created',
|
||||||
handler: async (host: EditorHost, content: string) => {
|
handler: async (host: EditorHost, content: string) => {
|
||||||
reportResponse('result:add-note');
|
reportResponse('result:add-note');
|
||||||
@@ -451,7 +469,12 @@ const CREATE_AS_DOC = {
|
|||||||
const CREATE_AS_LINKED_DOC = {
|
const CREATE_AS_LINKED_DOC = {
|
||||||
icon: CreateIcon,
|
icon: CreateIcon,
|
||||||
title: 'Create as a linked doc',
|
title: 'Create as a linked doc',
|
||||||
showWhen: () => true,
|
showWhen: (host: EditorHost) => {
|
||||||
|
if (host.std.store.readonly$.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
toast: 'New doc created',
|
toast: 'New doc created',
|
||||||
handler: async (host: EditorHost, content: string) => {
|
handler: async (host: EditorHost, content: string) => {
|
||||||
reportResponse('result:add-page');
|
reportResponse('result:add-page');
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { Modal, useConfirmModal } from '@affine/component/ui/modal';
|
|||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import {
|
||||||
|
GuardService,
|
||||||
|
WorkspacePermissionService,
|
||||||
|
} from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { i18nTime, Trans, useI18n } from '@affine/i18n';
|
import { i18nTime, Trans, useI18n } from '@affine/i18n';
|
||||||
@@ -409,6 +412,8 @@ const PageHistoryManager = ({
|
|||||||
const workspaceId = docCollection.id;
|
const workspaceId = docCollection.id;
|
||||||
const [activeVersion, setActiveVersion] = useState<string>();
|
const [activeVersion, setActiveVersion] = useState<string>();
|
||||||
|
|
||||||
|
const guardService = useService(GuardService);
|
||||||
|
|
||||||
const pageDocId = useMemo(() => {
|
const pageDocId = useMemo(() => {
|
||||||
return docCollection.getDoc(pageId)?.spaceDoc.guid ?? pageId;
|
return docCollection.getDoc(pageId)?.spaceDoc.guid ?? pageId;
|
||||||
}, [pageId, docCollection]);
|
}, [pageId, docCollection]);
|
||||||
@@ -440,6 +445,7 @@ const PageHistoryManager = ({
|
|||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const title = useLiveData(docDisplayMetaService.title$(pageId));
|
const title = useLiveData(docDisplayMetaService.title$(pageId));
|
||||||
|
const canEdit = useLiveData(guardService.can$('Doc_Update', pageDocId));
|
||||||
|
|
||||||
const onConfirmRestore = useCallback(() => {
|
const onConfirmRestore = useCallback(() => {
|
||||||
openConfirmModal({
|
openConfirmModal({
|
||||||
@@ -499,7 +505,7 @@ const PageHistoryManager = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onConfirmRestore}
|
onClick={onConfirmRestore}
|
||||||
disabled={isMutating || !activeVersion}
|
disabled={isMutating || !activeVersion || !canEdit}
|
||||||
>
|
>
|
||||||
{t['com.affine.history.restore-current-version']()}
|
{t['com.affine.history.restore-current-version']()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+5
-3
@@ -31,6 +31,7 @@ interface BlocksuiteEditorContainerProps {
|
|||||||
page: Store;
|
page: Store;
|
||||||
mode: DocMode;
|
mode: DocMode;
|
||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultOpenProperty?: DefaultOpenProperty;
|
defaultOpenProperty?: DefaultOpenProperty;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
@@ -40,7 +41,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
AffineEditorContainer,
|
AffineEditorContainer,
|
||||||
BlocksuiteEditorContainerProps
|
BlocksuiteEditorContainerProps
|
||||||
>(function AffineEditorContainer(
|
>(function AffineEditorContainer(
|
||||||
{ page, mode, className, style, shared, defaultOpenProperty },
|
{ page, mode, className, style, shared, readonly, defaultOpenProperty },
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -119,7 +120,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleClickPageModeBlank = useCallback(() => {
|
const handleClickPageModeBlank = useCallback(() => {
|
||||||
if (shared || page.readonly) return;
|
if (shared || readonly || page.readonly) return;
|
||||||
const std = affineEditorContainerProxy.host?.std;
|
const std = affineEditorContainerProxy.host?.std;
|
||||||
if (!std) {
|
if (!std) {
|
||||||
return;
|
return;
|
||||||
@@ -141,7 +142,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
std.command.exec(appendParagraphCommand);
|
std.command.exec(appendParagraphCommand);
|
||||||
}, [affineEditorContainerProxy, page, shared]);
|
}, [affineEditorContainerProxy.host?.std, page, readonly, shared]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -161,6 +162,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
shared={shared}
|
shared={shared}
|
||||||
page={page}
|
page={page}
|
||||||
ref={docRef}
|
ref={docRef}
|
||||||
|
readonly={readonly}
|
||||||
titleRef={docTitleRef}
|
titleRef={docTitleRef}
|
||||||
onClickBlank={handleClickPageModeBlank}
|
onClickBlank={handleClickPageModeBlank}
|
||||||
defaultOpenProperty={defaultOpenProperty}
|
defaultOpenProperty={defaultOpenProperty}
|
||||||
|
|||||||
+3
@@ -22,6 +22,7 @@ export type EditorProps = {
|
|||||||
page: Store;
|
page: Store;
|
||||||
mode: DocMode;
|
mode: DocMode;
|
||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
defaultOpenProperty?: DefaultOpenProperty;
|
defaultOpenProperty?: DefaultOpenProperty;
|
||||||
// on Editor ready
|
// on Editor ready
|
||||||
onEditorReady?: (editor: AffineEditorContainer) => (() => void) | void;
|
onEditorReady?: (editor: AffineEditorContainer) => (() => void) | void;
|
||||||
@@ -34,6 +35,7 @@ const BlockSuiteEditorImpl = ({
|
|||||||
page,
|
page,
|
||||||
className,
|
className,
|
||||||
shared,
|
shared,
|
||||||
|
readonly,
|
||||||
style,
|
style,
|
||||||
onEditorReady,
|
onEditorReady,
|
||||||
defaultOpenProperty,
|
defaultOpenProperty,
|
||||||
@@ -111,6 +113,7 @@ const BlockSuiteEditorImpl = ({
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
page={page}
|
page={page}
|
||||||
shared={shared}
|
shared={shared}
|
||||||
|
readonly={readonly}
|
||||||
defaultOpenProperty={defaultOpenProperty}
|
defaultOpenProperty={defaultOpenProperty}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className={className}
|
className={className}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const adapted = {
|
|||||||
|
|
||||||
interface BlocksuiteEditorProps {
|
interface BlocksuiteEditorProps {
|
||||||
page: Store;
|
page: Store;
|
||||||
|
readonly?: boolean;
|
||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
defaultOpenProperty?: DefaultOpenProperty;
|
defaultOpenProperty?: DefaultOpenProperty;
|
||||||
}
|
}
|
||||||
@@ -220,6 +221,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
onClickBlank,
|
onClickBlank,
|
||||||
titleRef: externalTitleRef,
|
titleRef: externalTitleRef,
|
||||||
defaultOpenProperty,
|
defaultOpenProperty,
|
||||||
|
readonly,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
@@ -334,7 +336,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
data-testid="page-editor-blank"
|
data-testid="page-editor-blank"
|
||||||
onClick={onClickBlank}
|
onClick={onClickBlank}
|
||||||
></div>
|
></div>
|
||||||
<StarterBar doc={page} />
|
{!readonly && <StarterBar doc={page} />}
|
||||||
{!shared && displayBiDirectionalLink ? (
|
{!shared && displayBiDirectionalLink ? (
|
||||||
<BiDirectionalLinkPanel />
|
<BiDirectionalLinkPanel />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
+92
-77
@@ -1,4 +1,4 @@
|
|||||||
import { notify, useConfirmModal } from '@affine/component';
|
import { notify, toast, useConfirmModal } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -9,13 +9,13 @@ import { PageHistoryModal } from '@affine/core/components/affine/page-history-mo
|
|||||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
|
||||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { ShareMenuContent } from '@affine/core/modules/share-menu';
|
import { ShareMenuContent } from '@affine/core/modules/share-menu';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
LocalWorkspaceIcon,
|
LocalWorkspaceIcon,
|
||||||
OpenInNewIcon,
|
OpenInNewIcon,
|
||||||
PageIcon,
|
PageIcon,
|
||||||
SaveIcon,
|
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
TocIcon,
|
TocIcon,
|
||||||
@@ -56,24 +55,95 @@ type PageMenuProps = {
|
|||||||
isJournal?: boolean;
|
isJournal?: boolean;
|
||||||
containerWidth: number;
|
containerWidth: number;
|
||||||
};
|
};
|
||||||
// fixme: refactor this file
|
|
||||||
export const PageHeaderMenuButton = ({
|
export const PageHeaderMenuButton = ({
|
||||||
rename,
|
rename,
|
||||||
page,
|
page,
|
||||||
isJournal,
|
isJournal,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
}: PageMenuProps) => {
|
}: PageMenuProps) => {
|
||||||
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
const editorService = useService(EditorService);
|
||||||
|
const isInTrash = useLiveData(
|
||||||
|
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||||
|
const [openHistoryTipsModal, setOpenHistoryTipsModal] = useState(false);
|
||||||
|
|
||||||
|
const handleMenuOpenChange = useCallback((open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
track.$.header.docOptions.open();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openHistoryModal = useCallback(() => {
|
||||||
|
track.$.header.history.open();
|
||||||
|
if (workspace.flavour === 'affine-cloud') {
|
||||||
|
return setHistoryModalOpen(true);
|
||||||
|
}
|
||||||
|
return setOpenHistoryTipsModal(true);
|
||||||
|
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
||||||
|
|
||||||
|
if (isInTrash) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<PageHeaderMenuItem
|
||||||
|
page={page}
|
||||||
|
containerWidth={containerWidth}
|
||||||
|
rename={rename}
|
||||||
|
isJournal={isJournal}
|
||||||
|
openHistoryModal={openHistoryModal}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
contentOptions={{
|
||||||
|
align: 'center',
|
||||||
|
}}
|
||||||
|
rootOptions={{
|
||||||
|
onOpenChange: handleMenuOpenChange,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HeaderDropDownButton />
|
||||||
|
</Menu>
|
||||||
|
{workspace.flavour !== 'local' ? (
|
||||||
|
<PageHistoryModal
|
||||||
|
docCollection={workspace.docCollection}
|
||||||
|
open={historyModalOpen}
|
||||||
|
pageId={page.id}
|
||||||
|
onOpenChange={setHistoryModalOpen}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<HistoryTipsModal
|
||||||
|
open={openHistoryTipsModal}
|
||||||
|
setOpen={setOpenHistoryTipsModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// fixme: refactor this file
|
||||||
|
const PageHeaderMenuItem = ({
|
||||||
|
rename,
|
||||||
|
page,
|
||||||
|
isJournal,
|
||||||
|
containerWidth,
|
||||||
|
openHistoryModal,
|
||||||
|
}: PageMenuProps & {
|
||||||
|
openHistoryModal: () => void;
|
||||||
|
}) => {
|
||||||
const pageId = page?.id;
|
const pageId = page?.id;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { hideShare } = useDetailPageHeaderResponsive(containerWidth);
|
const { hideShare } = useDetailPageHeaderResponsive(containerWidth);
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
const guardService = useService(GuardService);
|
||||||
const editorService = useService(EditorService);
|
const editorService = useService(EditorService);
|
||||||
const isInTrash = useLiveData(
|
|
||||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
|
||||||
);
|
|
||||||
const currentMode = useLiveData(editorService.editor.mode$);
|
const currentMode = useLiveData(editorService.editor.mode$);
|
||||||
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
||||||
|
|
||||||
@@ -84,9 +154,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
|
|
||||||
const { duplicate } = useBlockSuiteMetaHelper();
|
const { duplicate } = useBlockSuiteMetaHelper();
|
||||||
|
|
||||||
const [isEditing, setEditing] = useState(!page.readonly);
|
|
||||||
const { setDocReadonly } = useDocMetaHelper();
|
|
||||||
|
|
||||||
const view = useService(ViewService).view;
|
const view = useService(ViewService).view;
|
||||||
|
|
||||||
const openSidePanel = useCallback(
|
const openSidePanel = useCallback(
|
||||||
@@ -105,17 +172,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
openSidePanel('outline');
|
openSidePanel('outline');
|
||||||
}, [openSidePanel]);
|
}, [openSidePanel]);
|
||||||
|
|
||||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
|
||||||
const [openHistoryTipsModal, setOpenHistoryTipsModal] = useState(false);
|
|
||||||
|
|
||||||
const openHistoryModal = useCallback(() => {
|
|
||||||
track.$.header.history.open();
|
|
||||||
if (workspace.flavour === 'affine-cloud') {
|
|
||||||
return setHistoryModalOpen(true);
|
|
||||||
}
|
|
||||||
return setOpenHistoryTipsModal(true);
|
|
||||||
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
|
||||||
|
|
||||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||||
const openInfoModal = useCallback(() => {
|
const openInfoModal = useCallback(() => {
|
||||||
track.$.header.pageInfo.open();
|
track.$.header.pageInfo.open();
|
||||||
@@ -148,11 +204,16 @@ export const PageHeaderMenuButton = ({
|
|||||||
confirmButtonOptions: {
|
confirmButtonOptions: {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
},
|
},
|
||||||
onConfirm: () => {
|
onConfirm: async () => {
|
||||||
|
const canTrash = await guardService.can('Doc_Trash', pageId);
|
||||||
|
if (!canTrash) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
editorService.editor.doc.moveToTrash();
|
editorService.editor.doc.moveToTrash();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [editorService.editor.doc, openConfirmModal, t]);
|
}, [editorService.editor.doc, guardService, openConfirmModal, pageId, t]);
|
||||||
|
|
||||||
const handleRename = useCallback(() => {
|
const handleRename = useCallback(() => {
|
||||||
rename?.();
|
rename?.();
|
||||||
@@ -178,12 +239,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
});
|
});
|
||||||
}, [primaryMode, editorService, t]);
|
}, [primaryMode, editorService, t]);
|
||||||
|
|
||||||
const handleMenuOpenChange = useCallback((open: boolean) => {
|
|
||||||
if (open) {
|
|
||||||
track.$.header.docOptions.open();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const exportHandler = useExportPage();
|
const exportHandler = useExportPage();
|
||||||
|
|
||||||
const handleDuplicate = useCallback(() => {
|
const handleDuplicate = useCallback(() => {
|
||||||
@@ -238,21 +293,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
toggleFavorite();
|
toggleFavorite();
|
||||||
}, [toggleFavorite]);
|
}, [toggleFavorite]);
|
||||||
|
|
||||||
const handleToggleEdit = useCallback(() => {
|
|
||||||
setDocReadonly(page.id, !page.readonly);
|
|
||||||
setEditing(!isEditing);
|
|
||||||
}, [isEditing, page.id, page.readonly, setDocReadonly]);
|
|
||||||
|
|
||||||
const isMobile = environment.isMobile;
|
|
||||||
const mobileEditMenuItem = (
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={isEditing ? <SaveIcon /> : <EditIcon />}
|
|
||||||
onSelect={handleToggleEdit}
|
|
||||||
>
|
|
||||||
{t[isEditing ? 'Save' : 'Edit']()}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
const showResponsiveMenu = hideShare;
|
const showResponsiveMenu = hideShare;
|
||||||
const ResponsiveMenuItems = (
|
const ResponsiveMenuItems = (
|
||||||
<>
|
<>
|
||||||
@@ -293,15 +333,18 @@ export const PageHeaderMenuButton = ({
|
|||||||
openInAppService?.showOpenInAppPage();
|
openInAppService?.showOpenInAppPage();
|
||||||
}, [openInAppService]);
|
}, [openInAppService]);
|
||||||
|
|
||||||
const EditMenu = (
|
const canEdit = useLiveData(guardService.can$('Doc_Update', pageId));
|
||||||
|
const canMoveToTrash = useLiveData(guardService.can$('Doc_Trash', pageId));
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
||||||
{isMobile && mobileEditMenuItem}
|
|
||||||
{!isJournal && (
|
{!isJournal && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
prefixIcon={<EditIcon />}
|
prefixIcon={<EditIcon />}
|
||||||
data-testid="editor-option-menu-rename"
|
data-testid="editor-option-menu-rename"
|
||||||
onSelect={handleRename}
|
onSelect={handleRename}
|
||||||
|
disabled={!canEdit}
|
||||||
>
|
>
|
||||||
{t['Rename']()}
|
{t['Rename']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -310,6 +353,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||||
data-testid="editor-option-menu-edgeless"
|
data-testid="editor-option-menu-edgeless"
|
||||||
onSelect={handleSwitchMode}
|
onSelect={handleSwitchMode}
|
||||||
|
disabled={!canEdit}
|
||||||
>
|
>
|
||||||
{primaryMode === 'page'
|
{primaryMode === 'page'
|
||||||
? t['com.affine.editorDefaultMode.edgeless']()
|
? t['com.affine.editorDefaultMode.edgeless']()
|
||||||
@@ -396,6 +440,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
<MoveToTrash
|
<MoveToTrash
|
||||||
data-testid="editor-option-menu-delete"
|
data-testid="editor-option-menu-delete"
|
||||||
onSelect={handleOpenTrashModal}
|
onSelect={handleOpenTrashModal}
|
||||||
|
disabled={!canMoveToTrash}
|
||||||
/>
|
/>
|
||||||
{BUILD_CONFIG.isWeb && workspace.flavour === 'affine-cloud' ? (
|
{BUILD_CONFIG.isWeb && workspace.flavour === 'affine-cloud' ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -408,34 +453,4 @@ export const PageHeaderMenuButton = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
if (isInTrash) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Menu
|
|
||||||
items={EditMenu}
|
|
||||||
contentOptions={{
|
|
||||||
align: 'center',
|
|
||||||
}}
|
|
||||||
rootOptions={{
|
|
||||||
onOpenChange: handleMenuOpenChange,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HeaderDropDownButton />
|
|
||||||
</Menu>
|
|
||||||
{workspace.flavour !== 'local' ? (
|
|
||||||
<PageHistoryModal
|
|
||||||
docCollection={workspace.docCollection}
|
|
||||||
open={historyModalOpen}
|
|
||||||
pageId={pageId}
|
|
||||||
onOpenChange={setHistoryModalOpen}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<HistoryTipsModal
|
|
||||||
open={openHistoryTipsModal}
|
|
||||||
setOpen={setOpenHistoryTipsModal}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
+13
-9
@@ -1,7 +1,8 @@
|
|||||||
import type { InlineEditProps } from '@affine/component';
|
import type { InlineEditProps } from '@affine/component';
|
||||||
import { InlineEdit } from '@affine/component';
|
import { InlineEdit } from '@affine/component';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -11,7 +12,6 @@ import type { HTMLAttributes } from 'react';
|
|||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
export interface BlockSuiteHeaderTitleProps {
|
export interface BlockSuiteHeaderTitleProps {
|
||||||
docId: string;
|
|
||||||
/** if set, title cannot be edited */
|
/** if set, title cannot be edited */
|
||||||
inputHandleRef?: InlineEditProps['handleRef'];
|
inputHandleRef?: InlineEditProps['handleRef'];
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -21,20 +21,24 @@ const inputAttrs = {
|
|||||||
'data-testid': 'title-content',
|
'data-testid': 'title-content',
|
||||||
} as HTMLAttributes<HTMLInputElement>;
|
} as HTMLAttributes<HTMLInputElement>;
|
||||||
export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||||
const { inputHandleRef, docId } = props;
|
const { inputHandleRef } = props;
|
||||||
const workspaceService = useService(WorkspaceService);
|
const workspaceService = useService(WorkspaceService);
|
||||||
const isSharedMode = workspaceService.workspace.openOptions.isSharedMode;
|
const isSharedMode = workspaceService.workspace.openOptions.isSharedMode;
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
|
const guardService = useService(GuardService);
|
||||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
const docService = useService(DocService);
|
||||||
const docTitle = useLiveData(docRecord?.title$);
|
const docTitle = useLiveData(docService.doc.record.title$);
|
||||||
|
|
||||||
const onChange = useAsyncCallback(
|
const onChange = useAsyncCallback(
|
||||||
async (v: string) => {
|
async (v: string) => {
|
||||||
await docsService.changeDocTitle(docId, v);
|
await docsService.changeDocTitle(docService.doc.id, v);
|
||||||
track.$.header.actions.renameDoc();
|
track.$.header.actions.renameDoc();
|
||||||
},
|
},
|
||||||
[docId, docsService]
|
[docService.doc.id, docsService]
|
||||||
|
);
|
||||||
|
|
||||||
|
const canEdit = useLiveData(
|
||||||
|
guardService.can$('Doc_Update', docService.doc.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,7 +46,7 @@ export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
|||||||
className={clsx(styles.title, props.className)}
|
className={clsx(styles.title, props.className)}
|
||||||
value={docTitle}
|
value={docTitle}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
editable={!isSharedMode}
|
editable={!isSharedMode && canEdit}
|
||||||
exitible={true}
|
exitible={true}
|
||||||
placeholder="Untitled"
|
placeholder="Untitled"
|
||||||
data-testid="title-edit-button"
|
data-testid="title-edit-button"
|
||||||
|
|||||||
@@ -51,8 +51,14 @@ export const iconSelectorButton = style({
|
|||||||
border: `1px solid ${cssVar('borderColor')}`,
|
border: `1px solid ${cssVar('borderColor')}`,
|
||||||
background: cssVar('backgroundSecondaryColor'),
|
background: cssVar('backgroundSecondaryColor'),
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
':hover': {
|
|
||||||
backgroundColor: cssVar('backgroundTertiaryColor'),
|
selectors: {
|
||||||
|
'&:hover:not([data-readonly=true])': {
|
||||||
|
backgroundColor: cssVar('backgroundTertiaryColor'),
|
||||||
|
},
|
||||||
|
'&[data-readonly=true]': {
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,20 @@ const IconsSelectorPanel = ({
|
|||||||
|
|
||||||
export const DocPropertyIconSelector = ({
|
export const DocPropertyIconSelector = ({
|
||||||
propertyInfo,
|
propertyInfo,
|
||||||
|
readonly,
|
||||||
onSelectedChange,
|
onSelectedChange,
|
||||||
}: {
|
}: {
|
||||||
propertyInfo: DocCustomPropertyInfo;
|
propertyInfo: DocCustomPropertyInfo;
|
||||||
|
readonly?: boolean;
|
||||||
onSelectedChange: (icon: DocPropertyIconName) => void;
|
onSelectedChange: (icon: DocPropertyIconName) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (readonly) {
|
||||||
|
return (
|
||||||
|
<div className={styles.iconSelectorButton} data-readonly={readonly}>
|
||||||
|
<DocPropertyIcon propertyInfo={propertyInfo} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
items={
|
items={
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -37,9 +38,13 @@ const PropertyItem = ({
|
|||||||
) => void;
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const guardService = useService(GuardService);
|
||||||
const workspaceService = useService(WorkspaceService);
|
const workspaceService = useService(WorkspaceService);
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const [moreMenuOpen, setMoreMenuOpen] = useState(defaultOpenEditMenu);
|
const [moreMenuOpen, setMoreMenuOpen] = useState(defaultOpenEditMenu);
|
||||||
|
const canEditPropertyInfo = useLiveData(
|
||||||
|
guardService.can$('Workspace_Properties_Update')
|
||||||
|
);
|
||||||
|
|
||||||
const typeInfo = isSupportedDocPropertyType(propertyInfo.type)
|
const typeInfo = isSupportedDocPropertyType(propertyInfo.type)
|
||||||
? DocPropertyTypes[propertyInfo.type]
|
? DocPropertyTypes[propertyInfo.type]
|
||||||
@@ -51,6 +56,7 @@ const PropertyItem = ({
|
|||||||
|
|
||||||
const { dragRef } = useDraggable<AffineDNDData>(
|
const { dragRef } = useDraggable<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
|
canDrag: canEditPropertyInfo,
|
||||||
data: {
|
data: {
|
||||||
entity: {
|
entity: {
|
||||||
type: 'custom-property',
|
type: 'custom-property',
|
||||||
@@ -62,13 +68,14 @@ const PropertyItem = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[propertyInfo, workspaceService]
|
[propertyInfo, workspaceService, canEditPropertyInfo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
canDrop(data) {
|
canDrop(data) {
|
||||||
return (
|
return (
|
||||||
|
canEditPropertyInfo &&
|
||||||
data.source.data.entity?.type === 'custom-property' &&
|
data.source.data.entity?.type === 'custom-property' &&
|
||||||
data.source.data.from?.at === 'doc-property:manager' &&
|
data.source.data.from?.at === 'doc-property:manager' &&
|
||||||
data.source.data.from?.workspaceId ===
|
data.source.data.from?.workspaceId ===
|
||||||
@@ -97,7 +104,7 @@ const PropertyItem = ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[docsService, propertyInfo, workspaceService]
|
[docsService, propertyInfo, workspaceService, canEditPropertyInfo]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -139,6 +146,7 @@ const PropertyItem = ({
|
|||||||
<EditDocPropertyMenuItems
|
<EditDocPropertyMenuItems
|
||||||
propertyId={propertyInfo.id}
|
propertyId={propertyInfo.id}
|
||||||
onPropertyInfoChange={onPropertyInfoChange}
|
onPropertyInfoChange={onPropertyInfoChange}
|
||||||
|
readonly={!canEditPropertyInfo}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ import * as styles from './edit-doc-property.css';
|
|||||||
export const EditDocPropertyMenuItems = ({
|
export const EditDocPropertyMenuItems = ({
|
||||||
propertyId,
|
propertyId,
|
||||||
onPropertyInfoChange,
|
onPropertyInfoChange,
|
||||||
|
readonly,
|
||||||
}: {
|
}: {
|
||||||
propertyId: string;
|
propertyId: string;
|
||||||
|
readonly?: boolean;
|
||||||
onPropertyInfoChange?: (
|
onPropertyInfoChange?: (
|
||||||
field: keyof DocCustomPropertyInfo,
|
field: keyof DocCustomPropertyInfo,
|
||||||
value: string
|
value: string
|
||||||
@@ -142,9 +144,10 @@ export const EditDocPropertyMenuItems = ({
|
|||||||
>
|
>
|
||||||
<DocPropertyIconSelector
|
<DocPropertyIconSelector
|
||||||
propertyInfo={propertyInfo}
|
propertyInfo={propertyInfo}
|
||||||
|
readonly={readonly}
|
||||||
onSelectedChange={handleIconChange}
|
onSelectedChange={handleIconChange}
|
||||||
/>
|
/>
|
||||||
{typeInfo?.renameable === false ? (
|
{typeInfo?.renameable === false || readonly ? (
|
||||||
<span className={styles.propertyName}>{name}</span>
|
<span className={styles.propertyName}>{name}</span>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
@@ -180,6 +183,7 @@ export const EditDocPropertyMenuItems = ({
|
|||||||
propertyInfo.show !== 'always-hide'
|
propertyInfo.show !== 'always-hide'
|
||||||
}
|
}
|
||||||
data-property-visibility="always-show"
|
data-property-visibility="always-show"
|
||||||
|
disabled={readonly}
|
||||||
>
|
>
|
||||||
{t['com.affine.page-properties.property.always-show']()}
|
{t['com.affine.page-properties.property.always-show']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -188,6 +192,7 @@ export const EditDocPropertyMenuItems = ({
|
|||||||
onClick={handleClickHideWhenEmpty}
|
onClick={handleClickHideWhenEmpty}
|
||||||
selected={propertyInfo.show === 'hide-when-empty'}
|
selected={propertyInfo.show === 'hide-when-empty'}
|
||||||
data-property-visibility="hide-when-empty"
|
data-property-visibility="hide-when-empty"
|
||||||
|
disabled={readonly}
|
||||||
>
|
>
|
||||||
{t['com.affine.page-properties.property.hide-when-empty']()}
|
{t['com.affine.page-properties.property.hide-when-empty']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -196,6 +201,7 @@ export const EditDocPropertyMenuItems = ({
|
|||||||
onClick={handleClickAlwaysHide}
|
onClick={handleClickAlwaysHide}
|
||||||
selected={propertyInfo.show === 'always-hide'}
|
selected={propertyInfo.show === 'always-hide'}
|
||||||
data-property-visibility="always-hide"
|
data-property-visibility="always-hide"
|
||||||
|
disabled={readonly}
|
||||||
>
|
>
|
||||||
{t['com.affine.page-properties.property.always-hide']()}
|
{t['com.affine.page-properties.property.always-hide']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -203,6 +209,7 @@ export const EditDocPropertyMenuItems = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
prefixIcon={<DeleteIcon />}
|
prefixIcon={<DeleteIcon />}
|
||||||
type="danger"
|
type="danger"
|
||||||
|
disabled={readonly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirmModal.openConfirmModal({
|
confirmModal.openConfirmModal({
|
||||||
title:
|
title:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Divider, IconButton, Tooltip } from '@affine/component';
|
import { Divider, IconButton, Tooltip } from '@affine/component';
|
||||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name';
|
import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
@@ -28,9 +29,12 @@ export const DocPropertySidebar = () => {
|
|||||||
const [newPropertyId, setNewPropertyId] = useState<string>();
|
const [newPropertyId, setNewPropertyId] = useState<string>();
|
||||||
|
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
|
const guardService = useService(GuardService);
|
||||||
const propertyList = docsService.propertyList;
|
const propertyList = docsService.propertyList;
|
||||||
const properties = useLiveData(propertyList.properties$);
|
const properties = useLiveData(propertyList.properties$);
|
||||||
|
const canEditPropertyInfo = useLiveData(
|
||||||
|
guardService.can$('Workspace_Properties_Update')
|
||||||
|
);
|
||||||
const onAddProperty = useCallback(
|
const onAddProperty = useCallback(
|
||||||
(option: { type: string; name: string }) => {
|
(option: { type: string; name: string }) => {
|
||||||
if (!isSupportedDocPropertyType(option.type)) {
|
if (!isSupportedDocPropertyType(option.type)) {
|
||||||
@@ -104,12 +108,15 @@ export const DocPropertySidebar = () => {
|
|||||||
<div
|
<div
|
||||||
className={styles.itemContainer}
|
className={styles.itemContainer}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (!canEditPropertyInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onAddProperty({
|
onAddProperty({
|
||||||
type: key,
|
type: key,
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
data-disabled={isUniqueExist}
|
data-disabled={isUniqueExist || !canEditPropertyInfo}
|
||||||
>
|
>
|
||||||
<Icon className={styles.itemIcon} />
|
<Icon className={styles.itemIcon} />
|
||||||
<span className={styles.itemName}>{t.t(value.name)}</span>
|
<span className={styles.itemName}>{t.t(value.name)}</span>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
DatabaseValueCell,
|
DatabaseValueCell,
|
||||||
} from '@affine/core/modules/doc-info/types';
|
} from '@affine/core/modules/doc-info/types';
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -111,6 +112,8 @@ interface DocPropertyRowProps {
|
|||||||
propertyInfo: DocCustomPropertyInfo;
|
propertyInfo: DocCustomPropertyInfo;
|
||||||
showAll?: boolean;
|
showAll?: boolean;
|
||||||
defaultOpenEditMenu?: boolean;
|
defaultOpenEditMenu?: boolean;
|
||||||
|
propertyInfoReadonly?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
onChange?: (value: unknown) => void;
|
onChange?: (value: unknown) => void;
|
||||||
onPropertyInfoChange?: (
|
onPropertyInfoChange?: (
|
||||||
field: keyof DocCustomPropertyInfo,
|
field: keyof DocCustomPropertyInfo,
|
||||||
@@ -122,6 +125,8 @@ export const DocPropertyRow = ({
|
|||||||
propertyInfo,
|
propertyInfo,
|
||||||
defaultOpenEditMenu,
|
defaultOpenEditMenu,
|
||||||
onChange,
|
onChange,
|
||||||
|
propertyInfoReadonly,
|
||||||
|
readonly,
|
||||||
onPropertyInfoChange,
|
onPropertyInfoChange,
|
||||||
}: DocPropertyRowProps) => {
|
}: DocPropertyRowProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -160,6 +165,7 @@ export const DocPropertyRow = ({
|
|||||||
const docId = docService.doc.id;
|
const docId = docService.doc.id;
|
||||||
const { dragRef } = useDraggable<AffineDNDData>(
|
const { dragRef } = useDraggable<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
|
canDrag: !propertyInfoReadonly,
|
||||||
data: {
|
data: {
|
||||||
entity: {
|
entity: {
|
||||||
type: 'custom-property',
|
type: 'custom-property',
|
||||||
@@ -171,7 +177,7 @@ export const DocPropertyRow = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[docId, propertyInfo.id]
|
[docId, propertyInfo.id, propertyInfoReadonly]
|
||||||
);
|
);
|
||||||
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -180,6 +186,7 @@ export const DocPropertyRow = ({
|
|||||||
},
|
},
|
||||||
canDrop: data => {
|
canDrop: data => {
|
||||||
return (
|
return (
|
||||||
|
!propertyInfoReadonly &&
|
||||||
data.source.data.entity?.type === 'custom-property' &&
|
data.source.data.entity?.type === 'custom-property' &&
|
||||||
data.source.data.entity.id !== propertyInfo.id &&
|
data.source.data.entity.id !== propertyInfo.id &&
|
||||||
data.source.data.from?.at === 'doc-property:table' &&
|
data.source.data.from?.at === 'doc-property:table' &&
|
||||||
@@ -204,7 +211,7 @@ export const DocPropertyRow = ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[docId, docsService.propertyList, propertyInfo.id]
|
[docId, docsService.propertyList, propertyInfo.id, propertyInfoReadonly]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!ValueRenderer || typeof ValueRenderer !== 'function') return null;
|
if (!ValueRenderer || typeof ValueRenderer !== 'function') return null;
|
||||||
@@ -221,6 +228,8 @@ export const DocPropertyRow = ({
|
|||||||
dropIndicatorEdge={closestEdge}
|
dropIndicatorEdge={closestEdge}
|
||||||
hideEmpty={hideEmpty}
|
hideEmpty={hideEmpty}
|
||||||
hide={hide}
|
hide={hide}
|
||||||
|
data-property-info-readonly={propertyInfoReadonly}
|
||||||
|
data-readonly={readonly}
|
||||||
data-testid="doc-property-row"
|
data-testid="doc-property-row"
|
||||||
data-info-id={propertyInfo.id}
|
data-info-id={propertyInfo.id}
|
||||||
>
|
>
|
||||||
@@ -235,6 +244,7 @@ export const DocPropertyRow = ({
|
|||||||
<EditDocPropertyMenuItems
|
<EditDocPropertyMenuItems
|
||||||
propertyId={propertyInfo.id}
|
propertyId={propertyInfo.id}
|
||||||
onPropertyInfoChange={onPropertyInfoChange}
|
onPropertyInfoChange={onPropertyInfoChange}
|
||||||
|
readonly={propertyInfoReadonly}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
data-testid="doc-property-name"
|
data-testid="doc-property-name"
|
||||||
@@ -243,6 +253,7 @@ export const DocPropertyRow = ({
|
|||||||
propertyInfo={propertyInfo}
|
propertyInfo={propertyInfo}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={customPropertyValue}
|
value={customPropertyValue}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
</PropertyRoot>
|
</PropertyRoot>
|
||||||
);
|
);
|
||||||
@@ -284,11 +295,20 @@ const DocWorkspacePropertiesTableBody = forwardRef<
|
|||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const workbenchService = useService(WorkbenchService);
|
const workbenchService = useService(WorkbenchService);
|
||||||
const viewService = useServiceOptional(ViewService);
|
const viewService = useServiceOptional(ViewService);
|
||||||
|
const docService = useService(DocService);
|
||||||
|
const guardService = useService(GuardService);
|
||||||
const properties = useLiveData(docsService.propertyList.sortedProperties$);
|
const properties = useLiveData(docsService.propertyList.sortedProperties$);
|
||||||
const [addMoreCollapsed, setAddMoreCollapsed] = useState(true);
|
const [addMoreCollapsed, setAddMoreCollapsed] = useState(true);
|
||||||
|
|
||||||
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
|
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const canEditProperty = useLiveData(
|
||||||
|
guardService.can$('Doc_Update', docService.doc.id)
|
||||||
|
);
|
||||||
|
const canEditPropertyInfo = useLiveData(
|
||||||
|
guardService.can$('Workspace_Properties_Update')
|
||||||
|
);
|
||||||
|
|
||||||
const handlePropertyAdded = useCallback(
|
const handlePropertyAdded = useCallback(
|
||||||
(property: DocCustomPropertyInfo) => {
|
(property: DocCustomPropertyInfo) => {
|
||||||
setNewPropertyId(property.id);
|
setNewPropertyId(property.id);
|
||||||
@@ -340,34 +360,48 @@ const DocWorkspacePropertiesTableBody = forwardRef<
|
|||||||
propertyInfo={property}
|
propertyInfo={property}
|
||||||
defaultOpenEditMenu={newPropertyId === property.id}
|
defaultOpenEditMenu={newPropertyId === property.id}
|
||||||
onChange={value => onChange?.(property, value)}
|
onChange={value => onChange?.(property, value)}
|
||||||
|
readonly={!canEditProperty}
|
||||||
|
propertyInfoReadonly={!canEditPropertyInfo}
|
||||||
onPropertyInfoChange={(...args) =>
|
onPropertyInfoChange={(...args) =>
|
||||||
onPropertyInfoChange?.(property, ...args)
|
onPropertyInfoChange?.(property, ...args)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className={styles.actionContainer}>
|
<div className={styles.actionContainer}>
|
||||||
<Menu
|
{!canEditPropertyInfo ? (
|
||||||
items={
|
|
||||||
<CreatePropertyMenuItems
|
|
||||||
at="after"
|
|
||||||
onCreated={handlePropertyAdded}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
contentOptions={{
|
|
||||||
onClick(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
prefix={<PlusIcon />}
|
prefix={<PlusIcon />}
|
||||||
className={styles.propertyActionButton}
|
className={styles.propertyActionButton}
|
||||||
data-testid="add-property-button"
|
data-testid="add-property-button"
|
||||||
|
disabled={!canEditPropertyInfo}
|
||||||
>
|
>
|
||||||
{t['com.affine.page-properties.add-property']()}
|
{t['com.affine.page-properties.add-property']()}
|
||||||
</Button>
|
</Button>
|
||||||
</Menu>
|
) : (
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<CreatePropertyMenuItems
|
||||||
|
at="after"
|
||||||
|
onCreated={handlePropertyAdded}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
contentOptions={{
|
||||||
|
onClick(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
prefix={<PlusIcon />}
|
||||||
|
className={styles.propertyActionButton}
|
||||||
|
data-testid="add-property-button"
|
||||||
|
>
|
||||||
|
{t['com.affine.page-properties.add-property']()}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
{viewService ? (
|
{viewService ? (
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { useCallback } from 'react';
|
|||||||
import * as styles from './checkbox.css';
|
import * as styles from './checkbox.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const CheckboxValue = ({ value, onChange }: PropertyValueProps) => {
|
export const CheckboxValue = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
const parsedValue = value === 'true' ? true : false;
|
const parsedValue = value === 'true' ? true : false;
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onChange(parsedValue ? 'false' : 'true');
|
onChange(parsedValue ? 'false' : 'true');
|
||||||
},
|
},
|
||||||
[onChange, parsedValue]
|
[onChange, parsedValue, readonly]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<PropertyValue onClick={handleClick} className={styles.container}>
|
<PropertyValue onClick={handleClick} className={styles.container}>
|
||||||
@@ -19,6 +26,7 @@ export const CheckboxValue = ({ value, onChange }: PropertyValueProps) => {
|
|||||||
className={styles.checkboxProperty}
|
className={styles.checkboxProperty}
|
||||||
checked={parsedValue}
|
checked={parsedValue}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ export const CreatedByValue = () => {
|
|||||||
|
|
||||||
if (!isCloud) {
|
if (!isCloud) {
|
||||||
return (
|
return (
|
||||||
<PropertyValue>
|
<PropertyValue readonly>
|
||||||
<LocalUserValue />
|
<LocalUserValue />
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue>
|
<PropertyValue readonly>
|
||||||
<CloudUserAvatar type="CreatedBy" />
|
<CloudUserAvatar type="CreatedBy" />
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
@@ -96,14 +96,14 @@ export const UpdatedByValue = () => {
|
|||||||
|
|
||||||
if (!isCloud) {
|
if (!isCloud) {
|
||||||
return (
|
return (
|
||||||
<PropertyValue>
|
<PropertyValue readonly>
|
||||||
<LocalUserValue />
|
<LocalUserValue />
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue>
|
<PropertyValue readonly>
|
||||||
<CloudUserAvatar type="UpdatedBy" />
|
<CloudUserAvatar type="UpdatedBy" />
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,9 +23,25 @@ const useParsedDate = (value: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateValue = ({ value, onChange }: PropertyValueProps) => {
|
export const DateValue = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
const { parsedValue, displayValue } = useParsedDate(value);
|
const { parsedValue, displayValue } = useParsedDate(value);
|
||||||
|
|
||||||
|
if (readonly) {
|
||||||
|
return (
|
||||||
|
<PropertyValue
|
||||||
|
className={parsedValue ? '' : styles.empty}
|
||||||
|
isEmpty={!parsedValue}
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</PropertyValue>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import * as styles from './doc-primary-mode.css';
|
import * as styles from './doc-primary-mode.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const DocPrimaryModeValue = ({ onChange }: PropertyValueProps) => {
|
export const DocPrimaryModeValue = ({
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
|
|
||||||
@@ -51,13 +54,18 @@ export const DocPrimaryModeValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
[doc, t, onChange]
|
[doc, t, onChange]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<PropertyValue className={styles.container} hoverable={false}>
|
<PropertyValue
|
||||||
|
className={styles.container}
|
||||||
|
hoverable={false}
|
||||||
|
readonly={readonly}
|
||||||
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
||||||
itemHeight={24}
|
itemHeight={24}
|
||||||
value={primaryMode}
|
value={primaryMode}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
items={DocModeItems}
|
items={DocModeItems}
|
||||||
|
disabled={readonly}
|
||||||
className={styles.radioGroup}
|
className={styles.radioGroup}
|
||||||
/>
|
/>
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
|
|||||||
},
|
},
|
||||||
] satisfies RadioItem[];
|
] satisfies RadioItem[];
|
||||||
|
|
||||||
export const EdgelessThemeValue = ({ onChange }: PropertyValueProps) => {
|
export const EdgelessThemeValue = ({
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
const edgelessTheme = useLiveData(doc.properties$).edgelessColorTheme;
|
const edgelessTheme = useLiveData(doc.properties$).edgelessColorTheme;
|
||||||
@@ -38,13 +41,18 @@ export const EdgelessThemeValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
const themeItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
|
const themeItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue className={styles.container} hoverable={false}>
|
<PropertyValue
|
||||||
|
className={styles.container}
|
||||||
|
hoverable={false}
|
||||||
|
readonly={readonly}
|
||||||
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
||||||
itemHeight={24}
|
itemHeight={24}
|
||||||
value={edgelessTheme || 'system'}
|
value={edgelessTheme || 'system'}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
items={themeItems}
|
items={themeItems}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ export const date = style({
|
|||||||
padding: '0 4px',
|
padding: '0 4px',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
':hover': {
|
|
||||||
background: cssVarV2('layer/background/hoverOverlay'),
|
selectors: {
|
||||||
|
'&:hover:not([data-disabled])': {
|
||||||
|
background: cssVarV2('layer/background/hoverOverlay'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import * as styles from './journal.css';
|
|||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
const stopPropagation = (e: React.MouseEvent) => e.stopPropagation();
|
const stopPropagation = (e: React.MouseEvent) => e.stopPropagation();
|
||||||
export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
export const JournalValue = ({ readonly }: PropertyValueProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const journalService = useService(JournalService);
|
const journalService = useService(JournalService);
|
||||||
@@ -53,9 +53,8 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
const date = dayjs(day).format('YYYY-MM-DD');
|
const date = dayjs(day).format('YYYY-MM-DD');
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
journalService.setJournalDate(doc.id, date);
|
journalService.setJournalDate(doc.id, date);
|
||||||
onChange?.(date, true);
|
|
||||||
},
|
},
|
||||||
[journalService, doc.id, onChange]
|
[journalService, doc.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCheck = useCallback(
|
const handleCheck = useCallback(
|
||||||
@@ -63,12 +62,11 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
if (!v) {
|
if (!v) {
|
||||||
journalService.removeJournalDate(doc.id);
|
journalService.removeJournalDate(doc.id);
|
||||||
setShowDatePicker(false);
|
setShowDatePicker(false);
|
||||||
onChange?.(null, true);
|
|
||||||
} else {
|
} else {
|
||||||
handleDateSelect(selectedDate);
|
handleDateSelect(selectedDate);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange, journalService, doc.id, handleDateSelect, selectedDate]
|
[journalService, doc.id, handleDateSelect, selectedDate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
@@ -88,11 +86,12 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
|
|
||||||
const toggle = useCallback(
|
const toggle = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
if (readonly) return;
|
||||||
if (propertyRef.current?.contains(e.target as Node)) {
|
if (propertyRef.current?.contains(e.target as Node)) {
|
||||||
handleCheck(null, !checked);
|
handleCheck(null, !checked);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[checked, handleCheck]
|
[checked, handleCheck, readonly]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -100,9 +99,14 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
ref={propertyRef}
|
ref={propertyRef}
|
||||||
className={styles.property}
|
className={styles.property}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
|
readonly={readonly}
|
||||||
>
|
>
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<Checkbox className={styles.checkbox} checked={checked} />
|
<Checkbox
|
||||||
|
className={styles.checkbox}
|
||||||
|
checked={checked}
|
||||||
|
disabled={readonly}
|
||||||
|
/>
|
||||||
{checked ? (
|
{checked ? (
|
||||||
<Menu
|
<Menu
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
@@ -113,7 +117,7 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
}}
|
}}
|
||||||
rootOptions={{
|
rootOptions={{
|
||||||
modal: true,
|
modal: true,
|
||||||
open: showDatePicker,
|
open: !readonly && showDatePicker,
|
||||||
onOpenChange: setShowDatePicker,
|
onOpenChange: setShowDatePicker,
|
||||||
}}
|
}}
|
||||||
items={
|
items={
|
||||||
@@ -132,6 +136,7 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
data-disabled={readonly ? 'true' : undefined}
|
||||||
>
|
>
|
||||||
{displayDate}
|
{displayDate}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
import * as styles from './number.css';
|
import * as styles from './number.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
export const NumberValue = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
const parsedValue = isNaN(Number(value)) ? null : value;
|
const parsedValue = isNaN(Number(value)) ? null : value;
|
||||||
const [tempValue, setTempValue] = useState(parsedValue);
|
const [tempValue, setTempValue] = useState(parsedValue);
|
||||||
const handleBlur = useCallback(
|
const handleBlur = useCallback(
|
||||||
@@ -33,6 +37,7 @@ export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
|||||||
<PropertyValue
|
<PropertyValue
|
||||||
className={styles.numberPropertyValueContainer}
|
className={styles.numberPropertyValueContainer}
|
||||||
isEmpty={!parsedValue}
|
isEmpty={!parsedValue}
|
||||||
|
readonly={readonly}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
className={styles.numberPropertyValueInput}
|
className={styles.numberPropertyValueInput}
|
||||||
@@ -45,6 +50,7 @@ export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
|||||||
placeholder={t[
|
placeholder={t[
|
||||||
'com.affine.page-properties.property-value-placeholder'
|
'com.affine.page-properties.property-value-placeholder'
|
||||||
]()}
|
]()}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import { container } from './page-width.css';
|
import { container } from './page-width.css';
|
||||||
import type { PageLayoutMode, PropertyValueProps } from './types';
|
import type { PageLayoutMode, PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const PageWidthValue = ({ onChange }: PropertyValueProps) => {
|
export const PageWidthValue = ({ readonly }: PropertyValueProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const editorSetting = useService(EditorSettingService).editorSetting;
|
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||||
const defaultPageWidth = useLiveData(editorSetting.settings$).fullWidthLayout;
|
const defaultPageWidth = useLiveData(editorSetting.settings$).fullWidthLayout;
|
||||||
@@ -45,18 +45,18 @@ export const PageWidthValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(value: PageLayoutMode) => {
|
(value: PageLayoutMode) => {
|
||||||
doc.record.setProperty('pageWidth', value);
|
doc.record.setProperty('pageWidth', value);
|
||||||
onChange?.(value, true);
|
|
||||||
},
|
},
|
||||||
[doc, onChange]
|
[doc]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<PropertyValue className={container} hoverable={false}>
|
<PropertyValue className={container} hoverable={false} readonly={readonly}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
||||||
itemHeight={24}
|
itemHeight={24}
|
||||||
value={radioValue}
|
value={radioValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
items={radioItems}
|
items={radioItems}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { TagsInlineEditor } from '../tags-inline-editor';
|
|||||||
import * as styles from './tags.css';
|
import * as styles from './tags.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const TagsValue = ({ onChange }: PropertyValueProps) => {
|
export const TagsValue = ({ readonly }: PropertyValueProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
@@ -22,6 +22,7 @@ export const TagsValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
className={styles.container}
|
className={styles.container}
|
||||||
isEmpty={empty}
|
isEmpty={empty}
|
||||||
data-testid="property-tags-value"
|
data-testid="property-tags-value"
|
||||||
|
readonly={readonly}
|
||||||
>
|
>
|
||||||
<TagsInlineEditor
|
<TagsInlineEditor
|
||||||
className={styles.tagInlineEditor}
|
className={styles.tagInlineEditor}
|
||||||
@@ -29,7 +30,8 @@ export const TagsValue = ({ onChange }: PropertyValueProps) => {
|
|||||||
'com.affine.page-properties.property-value-placeholder'
|
'com.affine.page-properties.property-value-placeholder'
|
||||||
]()}
|
]()}
|
||||||
pageId={doc.id}
|
pageId={doc.id}
|
||||||
onChange={value => onChange(value, true)}
|
onChange={() => {}}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { type ChangeEvent, useCallback } from 'react';
|
|||||||
import * as styles from './template.css';
|
import * as styles from './template.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const TemplateValue = ({
|
export const TemplateValue = ({ readonly }: PropertyValueProps) => {
|
||||||
onChange: propOnChange,
|
|
||||||
}: PropertyValueProps) => {
|
|
||||||
const docService = useService(DocService);
|
const docService = useService(DocService);
|
||||||
|
|
||||||
const isTemplate = useLiveData(
|
const isTemplate = useLiveData(
|
||||||
@@ -17,24 +15,26 @@ export const TemplateValue = ({
|
|||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (readonly) return;
|
||||||
const value = e.target.checked;
|
const value = e.target.checked;
|
||||||
docService.doc.record.setProperty('isTemplate', value);
|
docService.doc.record.setProperty('isTemplate', value);
|
||||||
propOnChange?.(value, true);
|
|
||||||
},
|
},
|
||||||
[docService.doc.record, propOnChange]
|
[docService.doc.record, readonly]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
|
if (readonly) return;
|
||||||
docService.doc.record.setProperty('isTemplate', !isTemplate);
|
docService.doc.record.setProperty('isTemplate', !isTemplate);
|
||||||
}, [docService.doc.record, isTemplate]);
|
}, [docService.doc.record, isTemplate, readonly]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue className={styles.property} onClick={toggle}>
|
<PropertyValue className={styles.property} onClick={toggle} readonly>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
data-testid="toggle-template-checkbox"
|
data-testid="toggle-template-checkbox"
|
||||||
checked={!!isTemplate}
|
checked={!!isTemplate}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={styles.checkbox}
|
className={styles.checkbox}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import { ConfigModal } from '../../mobile';
|
|||||||
import * as styles from './text.css';
|
import * as styles from './text.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
const DesktopTextValue = ({ value, onChange }: PropertyValueProps) => {
|
const DesktopTextValue = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
const [tempValue, setTempValue] = useState<string>(value);
|
const [tempValue, setTempValue] = useState<string>(value);
|
||||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -50,6 +54,7 @@ const DesktopTextValue = ({ value, onChange }: PropertyValueProps) => {
|
|||||||
className={styles.textPropertyValueContainer}
|
className={styles.textPropertyValueContainer}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
isEmpty={!value}
|
isEmpty={!value}
|
||||||
|
readonly={readonly}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -62,6 +67,7 @@ const DesktopTextValue = ({ value, onChange }: PropertyValueProps) => {
|
|||||||
placeholder={t[
|
placeholder={t[
|
||||||
'com.affine.page-properties.property-value-placeholder'
|
'com.affine.page-properties.property-value-placeholder'
|
||||||
]()}
|
]()}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
<div className={styles.textInvisible}>
|
<div className={styles.textInvisible}>
|
||||||
{tempValue}
|
{tempValue}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
|||||||
export interface PropertyValueProps {
|
export interface PropertyValueProps {
|
||||||
propertyInfo?: DocCustomPropertyInfo;
|
propertyInfo?: DocCustomPropertyInfo;
|
||||||
value: any;
|
value: any;
|
||||||
|
readonly?: boolean;
|
||||||
onChange: (value: any, skipCommit?: boolean) => void; // if skipCommit is true, the change will be handled in the component itself
|
onChange: (value: any, skipCommit?: boolean) => void; // if skipCommit is true, the change will be handled in the component itself
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
type DocPermissionActions,
|
||||||
|
GuardService,
|
||||||
|
} from '@affine/core/modules/permissions';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
export const DocPermissionGuard = ({
|
||||||
|
docId,
|
||||||
|
children,
|
||||||
|
permission,
|
||||||
|
}: {
|
||||||
|
docId: string;
|
||||||
|
permission: DocPermissionActions;
|
||||||
|
children: (can: boolean) => React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const guardService = useService(GuardService);
|
||||||
|
const can = useLiveData(guardService.can$(permission, docId));
|
||||||
|
|
||||||
|
if (typeof children === 'function') {
|
||||||
|
return children(can);
|
||||||
|
}
|
||||||
|
throw new Error('children must be a function');
|
||||||
|
};
|
||||||
@@ -12,8 +12,7 @@ import { useNavigateHelper } from '../use-navigate-helper';
|
|||||||
|
|
||||||
export function useBlockSuiteMetaHelper() {
|
export function useBlockSuiteMetaHelper() {
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const { setDocMeta, getDocMeta, setDocTitle, setDocReadonly } =
|
const { setDocMeta, getDocMeta, setDocTitle } = useDocMetaHelper();
|
||||||
useDocMetaHelper();
|
|
||||||
const { createDoc } = useDocCollectionHelper(workspace.docCollection);
|
const { createDoc } = useDocCollectionHelper(workspace.docCollection);
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
const docRecordList = useService(DocsService).list;
|
const docRecordList = useService(DocsService).list;
|
||||||
@@ -25,10 +24,9 @@ export function useBlockSuiteMetaHelper() {
|
|||||||
const docRecord = docRecordList.doc$(docId).value;
|
const docRecord = docRecordList.doc$(docId).value;
|
||||||
if (docRecord) {
|
if (docRecord) {
|
||||||
docRecord.moveToTrash();
|
docRecord.moveToTrash();
|
||||||
setDocReadonly(docId, true);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[docRecordList, setDocReadonly]
|
[docRecordList]
|
||||||
);
|
);
|
||||||
|
|
||||||
const restoreFromTrash = useCallback(
|
const restoreFromTrash = useCallback(
|
||||||
@@ -36,10 +34,9 @@ export function useBlockSuiteMetaHelper() {
|
|||||||
const docRecord = docRecordList.doc$(docId).value;
|
const docRecord = docRecordList.doc$(docId).value;
|
||||||
if (docRecord) {
|
if (docRecord) {
|
||||||
docRecord.restoreFromTrash();
|
docRecord.restoreFromTrash();
|
||||||
setDocReadonly(docId, false);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[docRecordList, setDocReadonly]
|
[docRecordList]
|
||||||
);
|
);
|
||||||
|
|
||||||
const permanentlyDeletePage = useCallback(
|
const permanentlyDeletePage = useCallback(
|
||||||
|
|||||||
+29
-4
@@ -9,7 +9,9 @@ import type { Editor } from '@affine/core/modules/editor';
|
|||||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
|
import { UserFriendlyError } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +37,7 @@ export function useRegisterBlocksuiteEditorCommands(
|
|||||||
active: boolean
|
active: boolean
|
||||||
) {
|
) {
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
|
const guardService = useService(GuardService);
|
||||||
const docId = doc.id;
|
const docId = doc.id;
|
||||||
const mode = useLiveData(editor.mode$);
|
const mode = useLiveData(editor.mode$);
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -74,11 +77,22 @@ export function useRegisterBlocksuiteEditorCommands(
|
|||||||
}),
|
}),
|
||||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||||
confirmText: t.Delete(),
|
confirmText: t.Delete(),
|
||||||
onConfirm: () => {
|
onConfirm: async () => {
|
||||||
doc.moveToTrash();
|
try {
|
||||||
|
const canTrash = await guardService.can('Doc_Trash', docId);
|
||||||
|
if (!canTrash) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doc.moveToTrash();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
const userFriendlyError = UserFriendlyError.fromAnyError(error);
|
||||||
|
toast(t[`error.${userFriendlyError.name}`](userFriendlyError.data));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [doc, openConfirmModal, t]);
|
}, [doc, docId, guardService, openConfirmModal, t]);
|
||||||
|
|
||||||
const isCloudWorkspace = workspace.flavour !== 'local';
|
const isCloudWorkspace = workspace.flavour !== 'local';
|
||||||
|
|
||||||
@@ -190,6 +204,11 @@ export function useRegisterBlocksuiteEditorCommands(
|
|||||||
? t['com.affine.cmdk.affine.current-page-width-layout.standard']()
|
? t['com.affine.cmdk.affine.current-page-width-layout.standard']()
|
||||||
: t['com.affine.cmdk.affine.current-page-width-layout.full-width'](),
|
: t['com.affine.cmdk.affine.current-page-width-layout.full-width'](),
|
||||||
async run() {
|
async run() {
|
||||||
|
const canEdit = await guardService.can('Doc_Update', docId);
|
||||||
|
if (!canEdit) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
doc.record.setProperty(
|
doc.record.setProperty(
|
||||||
'pageWidth',
|
'pageWidth',
|
||||||
checked ? 'standard' : 'fullWidth'
|
checked ? 'standard' : 'fullWidth'
|
||||||
@@ -306,7 +325,12 @@ export function useRegisterBlocksuiteEditorCommands(
|
|||||||
category: `editor:${mode}`,
|
category: `editor:${mode}`,
|
||||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
label: t['com.affine.cmdk.affine.editor.restore-from-trash'](),
|
label: t['com.affine.cmdk.affine.editor.restore-from-trash'](),
|
||||||
run() {
|
async run() {
|
||||||
|
const canRestore = await guardService.can('Doc_Restore', docId);
|
||||||
|
if (!canRestore) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
track.$.cmdk.editor.restoreDoc();
|
track.$.cmdk.editor.restoreDoc();
|
||||||
|
|
||||||
doc.restoreFromTrash();
|
doc.restoreFromTrash();
|
||||||
@@ -383,5 +407,6 @@ export function useRegisterBlocksuiteEditorCommands(
|
|||||||
checked,
|
checked,
|
||||||
openInAppService,
|
openInAppService,
|
||||||
active,
|
active,
|
||||||
|
guardService,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
|
||||||
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@@ -26,7 +25,6 @@ export function useBlockSuiteDocMeta(docCollection: Workspace) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useDocMetaHelper() {
|
export function useDocMetaHelper() {
|
||||||
const workspaceService = useService(WorkspaceService);
|
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
|
|
||||||
const setDocTitle = useAsyncCallback(
|
const setDocTitle = useAsyncCallback(
|
||||||
@@ -53,23 +51,13 @@ export function useDocMetaHelper() {
|
|||||||
},
|
},
|
||||||
[docsService]
|
[docsService]
|
||||||
);
|
);
|
||||||
const setDocReadonly = useCallback(
|
|
||||||
(docId: string, readonly: boolean) => {
|
|
||||||
const doc = workspaceService.workspace.docCollection.getDoc(docId);
|
|
||||||
if (doc) {
|
|
||||||
doc.readonly = readonly;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[workspaceService]
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
setDocTitle,
|
setDocTitle,
|
||||||
setDocMeta,
|
setDocMeta,
|
||||||
getDocMeta,
|
getDocMeta,
|
||||||
setDocReadonly,
|
|
||||||
}),
|
}),
|
||||||
[getDocMeta, setDocMeta, setDocReadonly, setDocTitle]
|
[getDocMeta, setDocMeta, setDocTitle]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import './page-detail-editor.css';
|
|||||||
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { DocService } from '../modules/doc';
|
import { DocService } from '../modules/doc';
|
||||||
import { EditorService } from '../modules/editor';
|
import { EditorService } from '../modules/editor';
|
||||||
@@ -24,9 +25,13 @@ export type OnLoadEditor = (
|
|||||||
|
|
||||||
export interface PageDetailEditorProps {
|
export interface PageDetailEditorProps {
|
||||||
onLoad?: OnLoadEditor;
|
onLoad?: OnLoadEditor;
|
||||||
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
|
export const PageDetailEditor = ({
|
||||||
|
onLoad,
|
||||||
|
readonly,
|
||||||
|
}: PageDetailEditorProps) => {
|
||||||
const editor = useService(EditorService).editor;
|
const editor = useService(EditorService).editor;
|
||||||
const mode = useLiveData(editor.mode$);
|
const mode = useLiveData(editor.mode$);
|
||||||
const defaultOpenProperty = useLiveData(editor.defaultOpenProperty$);
|
const defaultOpenProperty = useLiveData(editor.defaultOpenProperty$);
|
||||||
@@ -47,6 +52,10 @@ export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
|
|||||||
? pageWidth === 'fullWidth'
|
? pageWidth === 'fullWidth'
|
||||||
: settings.fullWidthLayout;
|
: settings.fullWidthLayout;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editor.doc.blockSuiteDoc.readonly = readonly ?? false;
|
||||||
|
}, [editor, readonly]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomEditorWrapper>
|
<CustomEditorWrapper>
|
||||||
<Editor
|
<Editor
|
||||||
@@ -58,6 +67,7 @@ export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
|
|||||||
defaultOpenProperty={defaultOpenProperty}
|
defaultOpenProperty={defaultOpenProperty}
|
||||||
page={editor.doc.blockSuiteDoc}
|
page={editor.doc.blockSuiteDoc}
|
||||||
shared={isSharedMode}
|
shared={isSharedMode}
|
||||||
|
readonly={readonly}
|
||||||
onEditorReady={onLoad}
|
onEditorReady={onLoad}
|
||||||
/>
|
/>
|
||||||
</CustomEditorWrapper>
|
</CustomEditorWrapper>
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ export const ListFloatingToolbar = ({
|
|||||||
<FloatingToolbar className={styles.floatingToolbar} open={open}>
|
<FloatingToolbar className={styles.floatingToolbar} open={open}>
|
||||||
<FloatingToolbar.Item>{content}</FloatingToolbar.Item>
|
<FloatingToolbar.Item>{content}</FloatingToolbar.Item>
|
||||||
<FloatingToolbar.Button onClick={onClose} icon={<CloseIcon />} />
|
<FloatingToolbar.Button onClick={onClose} icon={<CloseIcon />} />
|
||||||
<FloatingToolbar.Separator />
|
{(!!onRestore || !!onDelete) && <FloatingToolbar.Separator />}
|
||||||
{!!onRestore && (
|
{!!onRestore && (
|
||||||
<FloatingToolbar.Button
|
<FloatingToolbar.Button
|
||||||
onClick={onRestore}
|
onClick={onRestore}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { Checkbox, Tooltip, useDraggable } from '@affine/component';
|
import { Checkbox, Tooltip, useDraggable } from '@affine/component';
|
||||||
|
import { type Doc, DocsService } from '@affine/core/modules/doc';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { stopPropagation } from '@affine/core/utils';
|
import { stopPropagation } from '@affine/core/utils';
|
||||||
import { i18nTime } from '@affine/i18n';
|
import { i18nTime } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||||
import type { ForwardedRef, PropsWithChildren } from 'react';
|
import type { ForwardedRef, PropsWithChildren } from 'react';
|
||||||
import { forwardRef, useCallback, useEffect, useMemo } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
|
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
|
||||||
import {
|
import {
|
||||||
@@ -142,7 +150,7 @@ const PageListOperationsCell = ({
|
|||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageListItem = (props: PageListItemProps) => {
|
const PagelistItemInner = (props: PageListItemProps) => {
|
||||||
const [displayProperties] = useAllDocDisplayProperties();
|
const [displayProperties] = useAllDocDisplayProperties();
|
||||||
const pageTitleElement = useMemo(() => {
|
const pageTitleElement = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -249,6 +257,29 @@ export const PageListItem = (props: PageListItemProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PageListItem = (props: PageListItemProps) => {
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const [doc, setDoc] = useState<Doc | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const { doc, release } = docsService.open(props.pageId);
|
||||||
|
setDoc(doc);
|
||||||
|
return () => {
|
||||||
|
release();
|
||||||
|
};
|
||||||
|
}, [props.pageId, docsService.list, docsService]);
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FrameworkScope scope={doc.scope}>
|
||||||
|
<PagelistItemInner {...props} />
|
||||||
|
</FrameworkScope>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type PageListWrapperProps = PropsWithChildren<
|
type PageListWrapperProps = PropsWithChildren<
|
||||||
Pick<PageListItemProps, 'to' | 'pageId' | 'onClick' | 'draggable'> & {
|
Pick<PageListItemProps, 'to' | 'pageId' | 'onClick' | 'draggable'> & {
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
|
|||||||
@@ -56,12 +56,14 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
|||||||
filters,
|
filters,
|
||||||
listItem,
|
listItem,
|
||||||
setHideHeaderCreateNewPage,
|
setHideHeaderCreateNewPage,
|
||||||
|
disableMultiDelete,
|
||||||
}: {
|
}: {
|
||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
collection?: Collection;
|
collection?: Collection;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
listItem?: DocMeta[];
|
listItem?: DocMeta[];
|
||||||
setHideHeaderCreateNewPage?: (hide: boolean) => void;
|
setHideHeaderCreateNewPage?: (hide: boolean) => void;
|
||||||
|
disableMultiDelete?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const listRef = useRef<ItemListHandle>(null);
|
const listRef = useRef<ItemListHandle>(null);
|
||||||
@@ -186,7 +188,7 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
|||||||
/>
|
/>
|
||||||
<ListFloatingToolbar
|
<ListFloatingToolbar
|
||||||
open={showFloatingToolbar}
|
open={showFloatingToolbar}
|
||||||
onDelete={handleMultiDelete}
|
onDelete={disableMultiDelete ? undefined : handleMultiDelete}
|
||||||
onClose={hideFloatingToolbar}
|
onClose={hideFloatingToolbar}
|
||||||
content={
|
content={
|
||||||
<Trans
|
<Trans
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
FavoriteService,
|
FavoriteService,
|
||||||
} from '@affine/core/modules/favorite';
|
} from '@affine/core/modules/favorite';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||||
@@ -57,7 +58,7 @@ export interface PageOperationCellProps {
|
|||||||
onRemoveFromAllowList?: () => void;
|
onRemoveFromAllowList?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageOperationCell = ({
|
const PageOperationCellMenuItem = ({
|
||||||
isInAllowList,
|
isInAllowList,
|
||||||
page,
|
page,
|
||||||
onRemoveFromAllowList,
|
onRemoveFromAllowList,
|
||||||
@@ -67,12 +68,15 @@ export const PageOperationCell = ({
|
|||||||
workspaceService,
|
workspaceService,
|
||||||
compatibleFavoriteItemsAdapter: favAdapter,
|
compatibleFavoriteItemsAdapter: favAdapter,
|
||||||
workbenchService,
|
workbenchService,
|
||||||
|
guardService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
|
GuardService,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canMoveToTrash = useLiveData(guardService.can$('Doc_Trash', page.id));
|
||||||
const currentWorkspace = workspaceService.workspace;
|
const currentWorkspace = workspaceService.workspace;
|
||||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||||
const workbench = workbenchService.workbench;
|
const workbench = workbenchService.workbench;
|
||||||
@@ -159,7 +163,7 @@ export const PageOperationCell = ({
|
|||||||
}
|
}
|
||||||
}, [onRemoveFromAllowList]);
|
}, [onRemoveFromAllowList]);
|
||||||
|
|
||||||
const OperationMenu = (
|
return (
|
||||||
<>
|
<>
|
||||||
{page.isPublic && (
|
{page.isPublic && (
|
||||||
<DisablePublicSharing
|
<DisablePublicSharing
|
||||||
@@ -199,9 +203,36 @@ export const PageOperationCell = ({
|
|||||||
{t['com.affine.header.option.duplicate']()}
|
{t['com.affine.header.option.duplicate']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MoveToTrash data-testid="move-to-trash" onSelect={onRemoveToTrash} />
|
<MoveToTrash
|
||||||
|
data-testid="move-to-trash"
|
||||||
|
onSelect={onRemoveToTrash}
|
||||||
|
disabled={!canMoveToTrash}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageOperationCell = ({
|
||||||
|
isInAllowList,
|
||||||
|
page,
|
||||||
|
onRemoveFromAllowList,
|
||||||
|
}: PageOperationCellProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const { compatibleFavoriteItemsAdapter: favAdapter } = useServices({
|
||||||
|
CompatibleFavoriteItemsAdapter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||||
|
|
||||||
|
const onToggleFavoritePage = useCallback(() => {
|
||||||
|
const status = favAdapter.isFavorite(page.id, 'doc');
|
||||||
|
favAdapter.toggle(page.id, 'doc');
|
||||||
|
toast(
|
||||||
|
status
|
||||||
|
? t['com.affine.toastMessage.removedFavorites']()
|
||||||
|
: t['com.affine.toastMessage.addedFavorites']()
|
||||||
|
);
|
||||||
|
}, [page.id, favAdapter, t]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ColWrapper
|
<ColWrapper
|
||||||
@@ -214,7 +245,13 @@ export const PageOperationCell = ({
|
|||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
<ColWrapper alignment="start">
|
<ColWrapper alignment="start">
|
||||||
<Menu
|
<Menu
|
||||||
items={OperationMenu}
|
items={
|
||||||
|
<PageOperationCellMenuItem
|
||||||
|
page={page}
|
||||||
|
isInAllowList={isInAllowList}
|
||||||
|
onRemoveFromAllowList={onRemoveFromAllowList}
|
||||||
|
/>
|
||||||
|
}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
align: 'end',
|
align: 'end',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { toast, useConfirmModal } from '@affine/component';
|
import { toast, useConfirmModal } from '@affine/component';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
@@ -16,8 +17,15 @@ import type { ItemListHandle, ListItem } from './types';
|
|||||||
import { useFilteredPageMetas } from './use-filtered-page-metas';
|
import { useFilteredPageMetas } from './use-filtered-page-metas';
|
||||||
import { VirtualizedList } from './virtualized-list';
|
import { VirtualizedList } from './virtualized-list';
|
||||||
|
|
||||||
export const VirtualizedTrashList = () => {
|
export const VirtualizedTrashList = ({
|
||||||
|
disableMultiDelete,
|
||||||
|
disableMultiRestore,
|
||||||
|
}: {
|
||||||
|
disableMultiDelete?: boolean;
|
||||||
|
disableMultiRestore?: boolean;
|
||||||
|
}) => {
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
const guardService = useService(GuardService);
|
||||||
const docCollection = currentWorkspace.docCollection;
|
const docCollection = currentWorkspace.docCollection;
|
||||||
const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
|
const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
|
||||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||||
@@ -82,16 +90,38 @@ export const VirtualizedTrashList = () => {
|
|||||||
(item: ListItem) => {
|
(item: ListItem) => {
|
||||||
const page = item as DocMeta;
|
const page = item as DocMeta;
|
||||||
const onRestorePage = () => {
|
const onRestorePage = () => {
|
||||||
restoreFromTrash(page.id);
|
guardService
|
||||||
toast(
|
.can('Doc_Delete', page.id)
|
||||||
t['com.affine.toastMessage.restored']({
|
.then(can => {
|
||||||
title: page.title || 'Untitled',
|
if (can) {
|
||||||
|
restoreFromTrash(page.id);
|
||||||
|
toast(
|
||||||
|
t['com.affine.toastMessage.restored']({
|
||||||
|
title: page.title || 'Untitled',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const onPermanentlyDeletePage = () => {
|
const onPermanentlyDeletePage = () => {
|
||||||
permanentlyDeletePage(page.id);
|
guardService
|
||||||
toast(t['com.affine.toastMessage.permanentlyDeleted']());
|
.can('Doc_Delete', page.id)
|
||||||
|
.then(can => {
|
||||||
|
if (can) {
|
||||||
|
permanentlyDeletePage(page.id);
|
||||||
|
toast(t['com.affine.toastMessage.permanentlyDeleted']());
|
||||||
|
} else {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -102,7 +132,7 @@ export const VirtualizedTrashList = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
[permanentlyDeletePage, restoreFromTrash, t]
|
[guardService, permanentlyDeletePage, restoreFromTrash, t]
|
||||||
);
|
);
|
||||||
const pageItemRenderer = useCallback((item: ListItem) => {
|
const pageItemRenderer = useCallback((item: ListItem) => {
|
||||||
return <PageListItemRenderer {...item} />;
|
return <PageListItemRenderer {...item} />;
|
||||||
@@ -128,9 +158,9 @@ export const VirtualizedTrashList = () => {
|
|||||||
/>
|
/>
|
||||||
<ListFloatingToolbar
|
<ListFloatingToolbar
|
||||||
open={showFloatingToolbar}
|
open={showFloatingToolbar}
|
||||||
onDelete={onConfirmPermanentlyDelete}
|
onDelete={disableMultiDelete ? undefined : onConfirmPermanentlyDelete}
|
||||||
onClose={hideFloatingToolbar}
|
onClose={hideFloatingToolbar}
|
||||||
onRestore={handleMultiRestore}
|
onRestore={disableMultiRestore ? undefined : handleMultiRestore}
|
||||||
content={
|
content={
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="com.affine.page.toolbar.selected"
|
i18nKey="com.affine.page.toolbar.selected"
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
AnimatedDeleteIcon,
|
AnimatedDeleteIcon,
|
||||||
|
toast,
|
||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
useDropTarget,
|
useDropTarget,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { MenuLinkItem } from '@affine/core/modules/app-sidebar/views';
|
import { MenuLinkItem } from '@affine/core/modules/app-sidebar/views';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
|
import { UserFriendlyError } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
|
||||||
@@ -16,6 +19,7 @@ export const TrashButton = () => {
|
|||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const globalContextService = useService(GlobalContextService);
|
const globalContextService = useService(GlobalContextService);
|
||||||
const trashActive = useLiveData(globalContextService.globalContext.isTrash.$);
|
const trashActive = useLiveData(globalContextService.globalContext.isTrash.$);
|
||||||
|
const guardService = useService(GuardService);
|
||||||
|
|
||||||
const { dropTargetRef, draggedOver } = useDropTarget<AffineDNDData>(
|
const { dropTargetRef, draggedOver } = useDropTarget<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -41,15 +45,32 @@ export const TrashButton = () => {
|
|||||||
confirmButtonOptions: {
|
confirmButtonOptions: {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
},
|
},
|
||||||
onConfirm() {
|
async onConfirm() {
|
||||||
docRecord.moveToTrash();
|
try {
|
||||||
|
const canTrash = await guardService.can(
|
||||||
|
'Doc_Trash',
|
||||||
|
docRecord.id
|
||||||
|
);
|
||||||
|
if (!canTrash) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
docRecord.moveToTrash();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
const userFriendlyError =
|
||||||
|
UserFriendlyError.fromAnyError(error);
|
||||||
|
toast(
|
||||||
|
t[`error.${userFriendlyError.name}`](userFriendlyError.data)
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[docsService.list, openConfirmModal, t]
|
[docsService.list, guardService, openConfirmModal, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export const tagsInlineEditor = style({
|
|||||||
'&[data-empty=true]': {
|
'&[data-empty=true]': {
|
||||||
color: cssVar('placeholderColor'),
|
color: cssVar('placeholderColor'),
|
||||||
},
|
},
|
||||||
|
'&[data-readonly="true"]': {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ export const DocInfoDialog = ({
|
|||||||
className={styles.titleContainer}
|
className={styles.titleContainer}
|
||||||
data-testid="info-modal-title"
|
data-testid="info-modal-title"
|
||||||
>
|
>
|
||||||
<BlocksuiteHeaderTitle
|
<BlocksuiteHeaderTitle className={styles.titleStyle} />
|
||||||
docId={docId}
|
|
||||||
className={styles.titleStyle}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<InfoTable docId={docId} onClose={() => close()} />
|
<InfoTable docId={docId} onClose={() => close()} />
|
||||||
</Scrollable.Viewport>
|
</Scrollable.Viewport>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
DatabaseValueCell,
|
DatabaseValueCell,
|
||||||
} from '@affine/core/modules/doc-info/types';
|
} from '@affine/core/modules/doc-info/types';
|
||||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -33,10 +34,15 @@ export const InfoTable = ({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { docsSearchService, docsService } = useServices({
|
const { docsSearchService, docsService, guardService } = useServices({
|
||||||
DocsSearchService,
|
DocsSearchService,
|
||||||
DocsService,
|
DocsService,
|
||||||
|
GuardService,
|
||||||
});
|
});
|
||||||
|
const canEditPropertyInfo = useLiveData(
|
||||||
|
guardService.can$('Workspace_Properties_Update')
|
||||||
|
);
|
||||||
|
const canEditProperty = useLiveData(guardService.can$('Doc_Update', docId));
|
||||||
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
|
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
|
||||||
const properties = useLiveData(docsService.propertyList.sortedProperties$);
|
const properties = useLiveData(docsService.propertyList.sortedProperties$);
|
||||||
const links = useLiveData(
|
const links = useLiveData(
|
||||||
@@ -133,6 +139,8 @@ export const InfoTable = ({
|
|||||||
<DocPropertyRow
|
<DocPropertyRow
|
||||||
key={property.id}
|
key={property.id}
|
||||||
propertyInfo={property}
|
propertyInfo={property}
|
||||||
|
readonly={!canEditProperty}
|
||||||
|
propertyInfoReadonly={!canEditPropertyInfo}
|
||||||
defaultOpenEditMenu={newPropertyId === property.id}
|
defaultOpenEditMenu={newPropertyId === property.id}
|
||||||
onChange={value => onPropertyChange(property, value)}
|
onChange={value => onPropertyChange(property, value)}
|
||||||
onPropertyInfoChange={(...args) =>
|
onPropertyInfoChange={(...args) =>
|
||||||
@@ -140,22 +148,33 @@ export const InfoTable = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Menu
|
{!canEditPropertyInfo ? (
|
||||||
items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
|
|
||||||
contentOptions={{
|
|
||||||
onClick(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
|
disabled
|
||||||
variant="plain"
|
variant="plain"
|
||||||
prefix={<PlusIcon />}
|
prefix={<PlusIcon />}
|
||||||
className={styles.addPropertyButton}
|
className={styles.addPropertyButton}
|
||||||
>
|
>
|
||||||
{t['com.affine.page-properties.add-property']()}
|
{t['com.affine.page-properties.add-property']()}
|
||||||
</Button>
|
</Button>
|
||||||
</Menu>
|
) : (
|
||||||
|
<Menu
|
||||||
|
items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
|
||||||
|
contentOptions={{
|
||||||
|
onClick(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
prefix={<PlusIcon />}
|
||||||
|
className={styles.addPropertyButton}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-properties.add-property']()}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</PropertyCollapsibleContent>
|
</PropertyCollapsibleContent>
|
||||||
</PropertyCollapsibleSection>
|
</PropertyCollapsibleSection>
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import {
|
|||||||
VirtualizedPageList,
|
VirtualizedPageList,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Filter } from '@affine/env/filter';
|
import type { Filter } from '@affine/env/filter';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -26,8 +27,11 @@ import { AllPageHeader } from './all-page-header';
|
|||||||
export const AllPage = () => {
|
export const AllPage = () => {
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
const globalContext = useService(GlobalContextService).globalContext;
|
const globalContext = useService(GlobalContextService).globalContext;
|
||||||
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||||
|
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||||
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<Filter[]>([]);
|
const [filters, setFilters] = useState<Filter[]>([]);
|
||||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||||
@@ -65,6 +69,7 @@ export const AllPage = () => {
|
|||||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
||||||
{filteredPageMetas.length > 0 ? (
|
{filteredPageMetas.length > 0 ? (
|
||||||
<VirtualizedPageList
|
<VirtualizedPageList
|
||||||
|
disableMultiDelete={!isAdmin && !isOwner}
|
||||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { VirtualizedPageList } from '@affine/core/components/page-list';
|
|||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -30,6 +31,9 @@ export const CollectionDetail = ({
|
|||||||
const { workspaceDialogService } = useServices({
|
const { workspaceDialogService } = useServices({
|
||||||
WorkspaceDialogService,
|
WorkspaceDialogService,
|
||||||
});
|
});
|
||||||
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
|
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||||
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||||
|
|
||||||
const handleEditCollection = useCallback(() => {
|
const handleEditCollection = useCallback(() => {
|
||||||
@@ -50,6 +54,7 @@ export const CollectionDetail = ({
|
|||||||
<VirtualizedPageList
|
<VirtualizedPageList
|
||||||
collection={collection}
|
collection={collection}
|
||||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||||
|
disableMultiDelete={!isAdmin && !isOwner}
|
||||||
/>
|
/>
|
||||||
</ViewBody>
|
</ViewBody>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+1
-4
@@ -158,10 +158,7 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
<ViewTitle title={title} />
|
<ViewTitle title={title} />
|
||||||
<ViewIcon icon={currentMode ?? 'page'} />
|
<ViewIcon icon={currentMode ?? 'page'} />
|
||||||
<EditorModeSwitch />
|
<EditorModeSwitch />
|
||||||
<BlocksuiteHeaderTitle
|
<BlocksuiteHeaderTitle inputHandleRef={titleInputHandleRef} />
|
||||||
docId={page.id}
|
|
||||||
inputHandleRef={titleInputHandleRef}
|
|
||||||
/>
|
|
||||||
<TemplateMark />
|
<TemplateMark />
|
||||||
<div className={styles.iconButtonContainer}>
|
<div className={styles.iconButtonContainer}>
|
||||||
{hideCollect ? null : (
|
{hideCollect ? null : (
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
|||||||
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
|
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
|
||||||
import { DocPropertySidebar } from '@affine/core/components/doc-properties/sidebar';
|
import { DocPropertySidebar } from '@affine/core/components/doc-properties/sidebar';
|
||||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { RecentDocsService } from '@affine/core/modules/quicksearch';
|
import { RecentDocsService } from '@affine/core/modules/quicksearch';
|
||||||
import { ViewService } from '@affine/core/modules/workbench';
|
import { ViewService } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
@@ -73,6 +73,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
workspaceService,
|
workspaceService,
|
||||||
globalContextService,
|
globalContextService,
|
||||||
featureFlagService,
|
featureFlagService,
|
||||||
|
guardService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
ViewService,
|
ViewService,
|
||||||
@@ -81,6 +82,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
|
GuardService,
|
||||||
});
|
});
|
||||||
const workbench = workbenchService.workbench;
|
const workbench = workbenchService.workbench;
|
||||||
const editor = editorService.editor;
|
const editor = editorService.editor;
|
||||||
@@ -98,7 +100,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
|
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const chatPanelRef = useRef<ChatPanel | null>(null);
|
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||||
const { setDocReadonly } = useDocMetaHelper();
|
|
||||||
|
|
||||||
const peekView = useService(PeekViewService).peekView;
|
const peekView = useService(PeekViewService).peekView;
|
||||||
|
|
||||||
@@ -149,12 +150,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
return;
|
return;
|
||||||
}, [doc, globalContext, isActiveView, mode]);
|
}, [doc, globalContext, isActiveView, mode]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ('isMobile' in environment && environment.isMobile) {
|
|
||||||
setDocReadonly(doc.id, true);
|
|
||||||
}
|
|
||||||
}, [doc.id, setDocReadonly]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActiveView) {
|
if (isActiveView) {
|
||||||
globalContext.isTrashDoc.set(!!isInTrash);
|
globalContext.isTrashDoc.set(!!isInTrash);
|
||||||
@@ -273,6 +268,10 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
|
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
const canEdit = useLiveData(guardService.can$('Doc_Update', doc.id));
|
||||||
|
|
||||||
|
const readonly = !canEdit || isInTrash;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FrameworkScope scope={editor.scope}>
|
<FrameworkScope scope={editor.scope}>
|
||||||
<ViewHeader>
|
<ViewHeader>
|
||||||
@@ -302,7 +301,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
styles.editorContainer
|
styles.editorContainer
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PageDetailEditor onLoad={onLoad} />
|
<PageDetailEditor onLoad={onLoad} readonly={readonly} />
|
||||||
</Scrollable.Viewport>
|
</Scrollable.Viewport>
|
||||||
<Scrollable.Scrollbar
|
<Scrollable.Scrollbar
|
||||||
className={clsx({
|
className={clsx({
|
||||||
|
|||||||
+31
-14
@@ -8,6 +8,7 @@ import {
|
|||||||
Scrollable,
|
Scrollable,
|
||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
|
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
|
||||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||||
import {
|
import {
|
||||||
@@ -362,22 +363,38 @@ const ConflictList = ({
|
|||||||
}}
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<DocPermissionGuard
|
||||||
prefixIcon={<CalendarXmarkIcon />}
|
docId={docRecord.id}
|
||||||
onClick={e => {
|
permission="Doc_Update"
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveJournalMark(docRecord.id);
|
|
||||||
}}
|
|
||||||
data-testid="journal-conflict-remove-mark"
|
|
||||||
>
|
>
|
||||||
{t[
|
{canEdit => (
|
||||||
'com.affine.page-properties.property.journal-remove'
|
<MenuItem
|
||||||
]()}
|
prefixIcon={<CalendarXmarkIcon />}
|
||||||
</MenuItem>
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveJournalMark(docRecord.id);
|
||||||
|
}}
|
||||||
|
data-testid="journal-conflict-remove-mark"
|
||||||
|
disabled={!canEdit}
|
||||||
|
>
|
||||||
|
{t[
|
||||||
|
'com.affine.page-properties.property.journal-remove'
|
||||||
|
]()}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
<MoveToTrash
|
<DocPermissionGuard
|
||||||
onSelect={() => handleOpenTrashModal(docRecord)}
|
docId={docRecord.id}
|
||||||
/>
|
permission="Doc_Trash"
|
||||||
|
>
|
||||||
|
{canTrash => (
|
||||||
|
<MoveToTrash
|
||||||
|
onSelect={() => handleOpenTrashModal(docRecord)}
|
||||||
|
disabled={!canTrash}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { DocMode } from '@blocksuite/affine/blocks';
|
|||||||
import * as styles from './share-header.css';
|
import * as styles from './share-header.css';
|
||||||
|
|
||||||
export function ShareHeader({
|
export function ShareHeader({
|
||||||
pageId,
|
|
||||||
publishMode,
|
publishMode,
|
||||||
isTemplate,
|
isTemplate,
|
||||||
templateName,
|
templateName,
|
||||||
@@ -21,7 +20,7 @@ export function ShareHeader({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<EditorModeSwitch />
|
<EditorModeSwitch />
|
||||||
<BlocksuiteHeaderTitle docId={pageId} />
|
<BlocksuiteHeaderTitle />
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<ShareHeaderRightItem
|
<ShareHeaderRightItem
|
||||||
publishMode={publishMode}
|
publishMode={publishMode}
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ const SharePageInner = ({
|
|||||||
styles.editorContainer
|
styles.editorContainer
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PageDetailEditor onLoad={onEditorLoad} />
|
<PageDetailEditor onLoad={onEditorLoad} readonly />
|
||||||
{publishMode === 'page' && !BUILD_CONFIG.isElectron ? (
|
{publishMode === 'page' && !BUILD_CONFIG.isElectron ? (
|
||||||
<ShareFooter />
|
<ShareFooter />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
VirtualizedPageList,
|
VirtualizedPageList,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import {
|
import {
|
||||||
useIsActiveView,
|
useIsActiveView,
|
||||||
@@ -26,6 +27,9 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
const globalContext = useService(GlobalContextService).globalContext;
|
const globalContext = useService(GlobalContextService).globalContext;
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||||
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
|
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||||
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
|
|
||||||
const tagList = useService(TagService).tagList;
|
const tagList = useService(TagService).tagList;
|
||||||
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
|
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
|
||||||
@@ -72,6 +76,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
<VirtualizedPageList
|
<VirtualizedPageList
|
||||||
tag={currentTag}
|
tag={currentTag}
|
||||||
listItem={filteredPageMetas}
|
listItem={filteredPageMetas}
|
||||||
|
disableMultiDelete={!isAdmin && !isOwner}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyPageList
|
<EmptyPageList
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import {
|
|||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
import { Header } from '@affine/core/components/pure/header';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||||
import { DeleteIcon } from '@blocksuite/icons/rc';
|
import { DeleteIcon } from '@blocksuite/icons/rc';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +40,9 @@ const TrashHeader = () => {
|
|||||||
export const TrashPage = () => {
|
export const TrashPage = () => {
|
||||||
const globalContextService = useService(GlobalContextService);
|
const globalContextService = useService(GlobalContextService);
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
|
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||||
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
const docCollection = currentWorkspace.docCollection;
|
const docCollection = currentWorkspace.docCollection;
|
||||||
assertExists(docCollection);
|
assertExists(docCollection);
|
||||||
|
|
||||||
@@ -71,7 +75,10 @@ export const TrashPage = () => {
|
|||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{filteredPageMetas.length > 0 ? (
|
{filteredPageMetas.length > 0 ? (
|
||||||
<VirtualizedTrashList />
|
<VirtualizedTrashList
|
||||||
|
disableMultiDelete={!isAdmin && !isOwner}
|
||||||
|
disableMultiRestore={!isAdmin && !isOwner}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyPageList type="trash" />
|
<EmptyPageList type="trash" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
|||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
|
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
|
||||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import { LiveData, useLiveData, useServices } from '@toeverything/infra';
|
import { LiveData, useLiveData, useServices } from '@toeverything/infra';
|
||||||
@@ -30,12 +31,17 @@ export const DocInfoSheet = ({
|
|||||||
docId: string;
|
docId: string;
|
||||||
defaultOpenProperty?: DefaultOpenProperty;
|
defaultOpenProperty?: DefaultOpenProperty;
|
||||||
}) => {
|
}) => {
|
||||||
const { docsSearchService, docsService } = useServices({
|
const { docsSearchService, docsService, guardService } = useServices({
|
||||||
DocsSearchService,
|
DocsSearchService,
|
||||||
DocsService,
|
DocsService,
|
||||||
|
GuardService,
|
||||||
});
|
});
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
|
const canEditPropertyInfo = useLiveData(
|
||||||
|
guardService.can$('Workspace_Properties_Update')
|
||||||
|
);
|
||||||
|
const canEditProperty = useLiveData(guardService.can$('Doc_Update', docId));
|
||||||
const links = useLiveData(
|
const links = useLiveData(
|
||||||
useMemo(
|
useMemo(
|
||||||
() => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
|
() => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
|
||||||
@@ -102,16 +108,11 @@ export const DocInfoSheet = ({
|
|||||||
key={property.id}
|
key={property.id}
|
||||||
propertyInfo={property}
|
propertyInfo={property}
|
||||||
defaultOpenEditMenu={newPropertyId === property.id}
|
defaultOpenEditMenu={newPropertyId === property.id}
|
||||||
|
propertyInfoReadonly={!canEditPropertyInfo}
|
||||||
|
readonly={!canEditProperty}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Menu
|
{!canEditPropertyInfo ? (
|
||||||
items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
|
|
||||||
contentOptions={{
|
|
||||||
onClick(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
prefix={<PlusIcon />}
|
prefix={<PlusIcon />}
|
||||||
@@ -119,7 +120,26 @@ export const DocInfoSheet = ({
|
|||||||
>
|
>
|
||||||
{t['com.affine.page-properties.add-property']()}
|
{t['com.affine.page-properties.add-property']()}
|
||||||
</Button>
|
</Button>
|
||||||
</Menu>
|
) : (
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<CreatePropertyMenuItems onCreated={onPropertyAdded} />
|
||||||
|
}
|
||||||
|
contentOptions={{
|
||||||
|
onClick(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
prefix={<PlusIcon />}
|
||||||
|
className={styles.addPropertyButton}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-properties.add-property']()}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</PropertyCollapsibleContent>
|
</PropertyCollapsibleContent>
|
||||||
</PropertyCollapsibleSection>
|
</PropertyCollapsibleSection>
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Loading } from '@affine/component';
|
import { Loading } from '@affine/component';
|
||||||
|
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
@@ -136,10 +137,16 @@ export const ExplorerDocNode = ({
|
|||||||
<ExplorerDocNode key={child.docId} docId={child.docId} isLinked />
|
<ExplorerDocNode key={child.docId} docId={child.docId} isLinked />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<AddItemPlaceholder
|
<DocPermissionGuard docId={docId} permission="Doc_Update">
|
||||||
label={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
|
{canEdit =>
|
||||||
onClick={handleAddLinkedPage}
|
canEdit ? (
|
||||||
/>
|
<AddItemPlaceholder
|
||||||
|
label={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
|
||||||
|
onClick={handleAddLinkedPage}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</DocPermissionGuard>
|
||||||
</ExplorerTreeNode>
|
</ExplorerTreeNode>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
IconButton,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuSeparator,
|
MenuSeparator,
|
||||||
MenuSub,
|
MenuSub,
|
||||||
@@ -7,6 +6,7 @@ import {
|
|||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
@@ -24,8 +24,6 @@ import {
|
|||||||
InformationIcon,
|
InformationIcon,
|
||||||
LinkedPageIcon,
|
LinkedPageIcon,
|
||||||
OpenInNewIcon,
|
OpenInNewIcon,
|
||||||
PlusIcon,
|
|
||||||
SplitViewIcon,
|
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@@ -121,7 +119,7 @@ export const useExplorerDocNodeOperations = (
|
|||||||
track.$.navigationPanel.docs.createDoc({ control: 'linkDoc' });
|
track.$.navigationPanel.docs.createDoc({ control: 'linkDoc' });
|
||||||
track.$.navigationPanel.docs.linkDoc({ control: 'createDoc' });
|
track.$.navigationPanel.docs.linkDoc({ control: 'createDoc' });
|
||||||
options.openNodeCollapsed();
|
options.openNodeCollapsed();
|
||||||
}, [createPage, docsService, docId, options]);
|
}, [createPage, docId, docsService, options]);
|
||||||
|
|
||||||
const handleToggleFavoriteDoc = useCallback(() => {
|
const handleToggleFavoriteDoc = useCallback(() => {
|
||||||
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
|
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
|
||||||
@@ -175,7 +173,6 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
handleAddLinkedPage,
|
handleAddLinkedPage,
|
||||||
handleDuplicate,
|
handleDuplicate,
|
||||||
handleToggleFavoriteDoc,
|
handleToggleFavoriteDoc,
|
||||||
handleOpenInSplitView,
|
|
||||||
handleOpenInNewTab,
|
handleOpenInNewTab,
|
||||||
handleMoveToTrash,
|
handleMoveToTrash,
|
||||||
handleRename,
|
handleRename,
|
||||||
@@ -187,21 +184,19 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
inline: true,
|
|
||||||
view: (
|
|
||||||
<IconButton
|
|
||||||
size="16"
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
tooltip={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
|
|
||||||
onClick={handleAddLinkedPage}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
index: 10,
|
index: 10,
|
||||||
view: <DocRenameSubMenu onConfirm={handleRename} initialName={title} />,
|
view: (
|
||||||
|
<DocPermissionGuard docId={docId} permission="Doc_Update">
|
||||||
|
{canEdit => (
|
||||||
|
<DocRenameSubMenu
|
||||||
|
onConfirm={handleRename}
|
||||||
|
initialName={title}
|
||||||
|
disabled={!canEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 11,
|
index: 11,
|
||||||
@@ -229,12 +224,17 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
{
|
{
|
||||||
index: 97,
|
index: 97,
|
||||||
view: (
|
view: (
|
||||||
<MenuItem
|
<DocPermissionGuard docId={docId} permission="Doc_Update">
|
||||||
prefixIcon={<LinkedPageIcon />}
|
{canEdit => (
|
||||||
onClick={handleAddLinkedPage}
|
<MenuItem
|
||||||
>
|
prefixIcon={<LinkedPageIcon />}
|
||||||
{t['com.affine.page-operation.add-linked-page']()}
|
onClick={handleAddLinkedPage}
|
||||||
</MenuItem>
|
disabled={!canEdit}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-operation.add-linked-page']()}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -253,21 +253,6 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
...(BUILD_CONFIG.isElectron
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
index: 100,
|
|
||||||
view: (
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={<SplitViewIcon />}
|
|
||||||
onClick={handleOpenInSplitView}
|
|
||||||
>
|
|
||||||
{t['com.affine.workbench.split-view.page-menu-open']()}
|
|
||||||
</MenuItem>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
index: 199,
|
index: 199,
|
||||||
view: (
|
view: (
|
||||||
@@ -288,13 +273,18 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
{
|
{
|
||||||
index: 10000,
|
index: 10000,
|
||||||
view: (
|
view: (
|
||||||
<MenuItem
|
<DocPermissionGuard docId={docId} permission="Doc_Trash">
|
||||||
type={'danger'}
|
{canMoveToTrash => (
|
||||||
prefixIcon={<DeleteIcon />}
|
<MenuItem
|
||||||
onClick={handleMoveToTrash}
|
type={'danger'}
|
||||||
>
|
prefixIcon={<DeleteIcon />}
|
||||||
{t['com.affine.moveToTrash.title']()}
|
onClick={handleMoveToTrash}
|
||||||
</MenuItem>
|
disabled={!canMoveToTrash}
|
||||||
|
>
|
||||||
|
{t['com.affine.moveToTrash.title']()}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -305,7 +295,6 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
handleDuplicate,
|
handleDuplicate,
|
||||||
handleMoveToTrash,
|
handleMoveToTrash,
|
||||||
handleOpenInNewTab,
|
handleOpenInNewTab,
|
||||||
handleOpenInSplitView,
|
|
||||||
handleRename,
|
handleRename,
|
||||||
handleToggleFavoriteDoc,
|
handleToggleFavoriteDoc,
|
||||||
t,
|
t,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const RenameSubMenu = ({
|
|||||||
children,
|
children,
|
||||||
menuProps,
|
menuProps,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
disabled,
|
||||||
...props
|
...props
|
||||||
}: RenameSubMenuProps) => {
|
}: RenameSubMenuProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -33,6 +34,7 @@ export const RenameSubMenu = ({
|
|||||||
triggerOptions={{
|
triggerOptions={{
|
||||||
prefixIcon: icon ?? <EditIcon />,
|
prefixIcon: icon ?? <EditIcon />,
|
||||||
suffixIcon: null,
|
suffixIcon: null,
|
||||||
|
disabled,
|
||||||
...triggerOptions,
|
...triggerOptions,
|
||||||
}}
|
}}
|
||||||
items={
|
items={
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface RenameSubMenuProps
|
|||||||
/** MenuItem.text */
|
/** MenuItem.text */
|
||||||
text?: string;
|
text?: string;
|
||||||
menuProps?: Partial<MenuSubProps>;
|
menuProps?: Partial<MenuSubProps>;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenameDialogProps
|
export interface RenameDialogProps
|
||||||
|
|||||||
+23
-10
@@ -5,6 +5,7 @@ import {
|
|||||||
MobileMenuSub,
|
MobileMenuSub,
|
||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
|
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
|
||||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||||
import {
|
import {
|
||||||
type DocRecord,
|
type DocRecord,
|
||||||
@@ -58,16 +59,28 @@ export const ResolveConflictOperations = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MobileMenuItem
|
<DocPermissionGuard docId={docRecord.id} permission="Doc_Update">
|
||||||
prefixIcon={<CalendarXmarkIcon />}
|
{canEdit => (
|
||||||
onClick={() => {
|
<MobileMenuItem
|
||||||
handleRemoveJournalMark(docRecord.id);
|
prefixIcon={<CalendarXmarkIcon />}
|
||||||
}}
|
onClick={() => {
|
||||||
data-testid="journal-conflict-remove-mark"
|
handleRemoveJournalMark(docRecord.id);
|
||||||
>
|
}}
|
||||||
{t['com.affine.page-properties.property.journal-remove']()}
|
data-testid="journal-conflict-remove-mark"
|
||||||
</MobileMenuItem>
|
disabled={!canEdit}
|
||||||
<MoveToTrash onSelect={() => handleOpenTrashModal(docRecord)} />
|
>
|
||||||
|
{t['com.affine.page-properties.property.journal-remove']()}
|
||||||
|
</MobileMenuItem>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
|
<DocPermissionGuard docId={docRecord.id} permission="Doc_Trash">
|
||||||
|
{canTrash => (
|
||||||
|
<MoveToTrash
|
||||||
|
onSelect={() => handleOpenTrashModal(docRecord)}
|
||||||
|
disabled={!canTrash}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useThemeColorV2 } from '@affine/component';
|
|||||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
import { useActiveBlocksuiteEditor } from '@affine/core/components/hooks/use-block-suite-editor';
|
import { useActiveBlocksuiteEditor } from '@affine/core/components/hooks/use-block-suite-editor';
|
||||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
|
||||||
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
|
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
|
||||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||||
@@ -17,6 +16,7 @@ import { EditorService } from '@affine/core/modules/editor';
|
|||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
@@ -56,6 +56,7 @@ const DetailPageImpl = () => {
|
|||||||
globalContextService,
|
globalContextService,
|
||||||
featureFlagService,
|
featureFlagService,
|
||||||
aIButtonService,
|
aIButtonService,
|
||||||
|
guardService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
ViewService,
|
ViewService,
|
||||||
@@ -65,6 +66,7 @@ const DetailPageImpl = () => {
|
|||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
AIButtonService,
|
AIButtonService,
|
||||||
|
GuardService,
|
||||||
});
|
});
|
||||||
const editor = editorService.editor;
|
const editor = editorService.editor;
|
||||||
const workspace = workspaceService.workspace;
|
const workspace = workspaceService.workspace;
|
||||||
@@ -84,7 +86,6 @@ const DetailPageImpl = () => {
|
|||||||
featureFlagService.flags.enable_mobile_keyboard_toolbar.value;
|
featureFlagService.flags.enable_mobile_keyboard_toolbar.value;
|
||||||
const enableEdgelessEditing =
|
const enableEdgelessEditing =
|
||||||
featureFlagService.flags.enable_mobile_edgeless_editing.value;
|
featureFlagService.flags.enable_mobile_edgeless_editing.value;
|
||||||
const { setDocReadonly } = useDocMetaHelper();
|
|
||||||
|
|
||||||
// TODO(@eyhn): remove jotai here
|
// TODO(@eyhn): remove jotai here
|
||||||
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
|
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
|
||||||
@@ -111,19 +112,6 @@ const DetailPageImpl = () => {
|
|||||||
};
|
};
|
||||||
}, [doc, globalContext, mode]);
|
}, [doc, globalContext, mode]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDocReadonly(
|
|
||||||
doc.id,
|
|
||||||
!enableKeyboardToolbar || (mode === 'edgeless' && !enableEdgelessEditing)
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
enableKeyboardToolbar,
|
|
||||||
doc.id,
|
|
||||||
setDocReadonly,
|
|
||||||
mode,
|
|
||||||
enableEdgelessEditing,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
aIButtonService.presentAIButton(true);
|
aIButtonService.presentAIButton(true);
|
||||||
|
|
||||||
@@ -202,6 +190,14 @@ const DetailPageImpl = () => {
|
|||||||
[docCollection.id, editor, jumpToPageBlock, openPage, server]
|
[docCollection.id, editor, jumpToPageBlock, openPage, server]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canEdit = useLiveData(guardService.can$('Doc_Update', doc.id));
|
||||||
|
|
||||||
|
const readonly =
|
||||||
|
!canEdit ||
|
||||||
|
isInTrash ||
|
||||||
|
!enableKeyboardToolbar ||
|
||||||
|
(mode === 'edgeless' && !enableEdgelessEditing);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FrameworkScope scope={editor.scope}>
|
<FrameworkScope scope={editor.scope}>
|
||||||
<div className={styles.mainContainer}>
|
<div className={styles.mainContainer}>
|
||||||
@@ -216,7 +212,7 @@ const DetailPageImpl = () => {
|
|||||||
>
|
>
|
||||||
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
||||||
<AffineErrorBoundary key={doc.id} className={styles.errorBoundary}>
|
<AffineErrorBoundary key={doc.id} className={styles.errorBoundary}>
|
||||||
<PageDetailEditor onLoad={onLoad} />
|
<PageDetailEditor onLoad={onLoad} readonly={readonly} />
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { DocInfoSheet } from '@affine/core/mobile/components';
|
|||||||
import { MobileTocMenu } from '@affine/core/mobile/components/toc-menu';
|
import { MobileTocMenu } from '@affine/core/mobile/components/toc-menu';
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
import { preventDefault } from '@affine/core/utils';
|
import { preventDefault } from '@affine/core/utils';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -34,6 +35,8 @@ export const PageHeaderMenuButton = () => {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const docId = useService(DocService).doc.id;
|
const docId = useService(DocService).doc.id;
|
||||||
|
const guardService = useService(GuardService);
|
||||||
|
const canEdit = useLiveData(guardService.can$('Doc_Update', docId));
|
||||||
|
|
||||||
const editorService = useService(EditorService);
|
const editorService = useService(EditorService);
|
||||||
const editorContainer = useLiveData(editorService.editor.editorContainer$);
|
const editorContainer = useLiveData(editorService.editor.editorContainer$);
|
||||||
@@ -94,6 +97,7 @@ export const PageHeaderMenuButton = () => {
|
|||||||
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||||
data-testid="editor-option-menu-mode-switch"
|
data-testid="editor-option-menu-mode-switch"
|
||||||
onSelect={handleSwitchMode}
|
onSelect={handleSwitchMode}
|
||||||
|
disabled={!canEdit}
|
||||||
>
|
>
|
||||||
{primaryMode === 'page'
|
{primaryMode === 'page'
|
||||||
? t['com.affine.editorDefaultMode.edgeless']()
|
? t['com.affine.editorDefaultMode.edgeless']()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
|||||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
@@ -50,12 +51,14 @@ export const ExplorerDocNode = ({
|
|||||||
globalContextService,
|
globalContextService,
|
||||||
docDisplayMetaService,
|
docDisplayMetaService,
|
||||||
featureFlagService,
|
featureFlagService,
|
||||||
|
guardService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
DocsSearchService,
|
DocsSearchService,
|
||||||
DocsService,
|
DocsService,
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
|
GuardService,
|
||||||
});
|
});
|
||||||
|
|
||||||
const active =
|
const active =
|
||||||
@@ -134,6 +137,11 @@ export const ExplorerDocNode = ({
|
|||||||
async (data: DropTargetDropEvent<AffineDNDData>) => {
|
async (data: DropTargetDropEvent<AffineDNDData>) => {
|
||||||
if (data.treeInstruction?.type === 'make-child') {
|
if (data.treeInstruction?.type === 'make-child') {
|
||||||
if (data.source.data.entity?.type === 'doc') {
|
if (data.source.data.entity?.type === 'doc') {
|
||||||
|
const canEdit = await guardService.can('Doc_Update', docId);
|
||||||
|
if (!canEdit) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
await docsService.addLinkedDoc(docId, data.source.data.entity.id);
|
await docsService.addLinkedDoc(docId, data.source.data.entity.id);
|
||||||
track.$.navigationPanel.docs.linkDoc({
|
track.$.navigationPanel.docs.linkDoc({
|
||||||
control: 'drag',
|
control: 'drag',
|
||||||
@@ -148,7 +156,7 @@ export const ExplorerDocNode = ({
|
|||||||
onDrop?.(data);
|
onDrop?.(data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[docId, docsService, onDrop, t]
|
[docId, docsService, guardService, onDrop, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDropEffectOnDoc = useCallback<ExplorerTreeNodeDropEffect>(
|
const handleDropEffectOnDoc = useCallback<ExplorerTreeNodeDropEffect>(
|
||||||
@@ -168,6 +176,11 @@ export const ExplorerDocNode = ({
|
|||||||
const handleDropOnPlaceholder = useAsyncCallback(
|
const handleDropOnPlaceholder = useAsyncCallback(
|
||||||
async (data: DropTargetDropEvent<AffineDNDData>) => {
|
async (data: DropTargetDropEvent<AffineDNDData>) => {
|
||||||
if (data.source.data.entity?.type === 'doc') {
|
if (data.source.data.entity?.type === 'doc') {
|
||||||
|
const canEdit = await guardService.can('Doc_Update', docId);
|
||||||
|
if (!canEdit) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO(eyhn): timeout&error handling
|
// TODO(eyhn): timeout&error handling
|
||||||
await docsService.addLinkedDoc(docId, data.source.data.entity.id);
|
await docsService.addLinkedDoc(docId, data.source.data.entity.id);
|
||||||
track.$.navigationPanel.docs.linkDoc({
|
track.$.navigationPanel.docs.linkDoc({
|
||||||
@@ -180,7 +193,7 @@ export const ExplorerDocNode = ({
|
|||||||
toast(t['com.affine.rootAppSidebar.doc.link-doc-only']());
|
toast(t['com.affine.rootAppSidebar.doc.link-doc-only']());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[docId, docsService, t]
|
[docId, docsService, guardService, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||||
@@ -242,6 +255,10 @@ export const ExplorerDocNode = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
reorderable={reorderable}
|
reorderable={reorderable}
|
||||||
|
renameableGuard={{
|
||||||
|
docId,
|
||||||
|
action: 'Doc_Update',
|
||||||
|
}}
|
||||||
onRename={handleRename}
|
onRename={handleRename}
|
||||||
childrenPlaceholder={
|
childrenPlaceholder={
|
||||||
searching ? null : <Empty onDrop={handleDropOnPlaceholder} />
|
searching ? null : <Empty onDrop={handleDropOnPlaceholder} />
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
IconButton,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuSeparator,
|
MenuSeparator,
|
||||||
toast,
|
toast,
|
||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
InformationIcon,
|
InformationIcon,
|
||||||
LinkedPageIcon,
|
LinkedPageIcon,
|
||||||
OpenInNewIcon,
|
OpenInNewIcon,
|
||||||
PlusIcon,
|
|
||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
@@ -42,11 +42,13 @@ export const useExplorerDocNodeOperations = (
|
|||||||
workspaceService,
|
workspaceService,
|
||||||
docsService,
|
docsService,
|
||||||
compatibleFavoriteItemsAdapter,
|
compatibleFavoriteItemsAdapter,
|
||||||
|
guardService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
DocsService,
|
DocsService,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
|
GuardService,
|
||||||
});
|
});
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
@@ -115,13 +117,18 @@ export const useExplorerDocNodeOperations = (
|
|||||||
}, [docId, workbenchService.workbench]);
|
}, [docId, workbenchService.workbench]);
|
||||||
|
|
||||||
const handleAddLinkedPage = useAsyncCallback(async () => {
|
const handleAddLinkedPage = useAsyncCallback(async () => {
|
||||||
|
const canEdit = await guardService.can('Doc_Update', docId);
|
||||||
|
if (!canEdit) {
|
||||||
|
toast(t['com.affine.no-permission']());
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newDoc = createPage();
|
const newDoc = createPage();
|
||||||
// TODO: handle timeout & error
|
// TODO: handle timeout & error
|
||||||
await docsService.addLinkedDoc(docId, newDoc.id);
|
await docsService.addLinkedDoc(docId, newDoc.id);
|
||||||
track.$.navigationPanel.docs.createDoc({ control: 'linkDoc' });
|
track.$.navigationPanel.docs.createDoc({ control: 'linkDoc' });
|
||||||
track.$.navigationPanel.docs.linkDoc({ control: 'createDoc' });
|
track.$.navigationPanel.docs.linkDoc({ control: 'createDoc' });
|
||||||
options.openNodeCollapsed();
|
options.openNodeCollapsed();
|
||||||
}, [createPage, docsService, docId, options]);
|
}, [createPage, guardService, docId, docsService, options, t]);
|
||||||
|
|
||||||
const handleToggleFavoriteDoc = useCallback(() => {
|
const handleToggleFavoriteDoc = useCallback(() => {
|
||||||
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
|
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
|
||||||
@@ -132,18 +139,6 @@ export const useExplorerDocNodeOperations = (
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
inline: true,
|
|
||||||
view: (
|
|
||||||
<IconButton
|
|
||||||
size="16"
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
tooltip={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
|
|
||||||
onClick={handleAddLinkedPage}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
index: 50,
|
index: 50,
|
||||||
view: (
|
view: (
|
||||||
@@ -158,12 +153,17 @@ export const useExplorerDocNodeOperations = (
|
|||||||
{
|
{
|
||||||
index: 99,
|
index: 99,
|
||||||
view: (
|
view: (
|
||||||
<MenuItem
|
<DocPermissionGuard docId={docId} permission="Doc_Update">
|
||||||
prefixIcon={<LinkedPageIcon />}
|
{canEdit => (
|
||||||
onClick={handleAddLinkedPage}
|
<MenuItem
|
||||||
>
|
prefixIcon={<LinkedPageIcon />}
|
||||||
{t['com.affine.page-operation.add-linked-page']()}
|
onClick={handleAddLinkedPage}
|
||||||
</MenuItem>
|
disabled={!canEdit}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-operation.add-linked-page']()}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,17 +217,23 @@ export const useExplorerDocNodeOperations = (
|
|||||||
{
|
{
|
||||||
index: 10000,
|
index: 10000,
|
||||||
view: (
|
view: (
|
||||||
<MenuItem
|
<DocPermissionGuard docId={docId} permission="Doc_Trash">
|
||||||
type={'danger'}
|
{canMoveToTrash => (
|
||||||
prefixIcon={<DeleteIcon />}
|
<MenuItem
|
||||||
onClick={handleMoveToTrash}
|
type={'danger'}
|
||||||
>
|
prefixIcon={<DeleteIcon />}
|
||||||
{t['com.affine.moveToTrash.title']()}
|
onClick={handleMoveToTrash}
|
||||||
</MenuItem>
|
disabled={!canMoveToTrash}
|
||||||
|
>
|
||||||
|
{t['com.affine.moveToTrash.title']()}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
docId,
|
||||||
favorite,
|
favorite,
|
||||||
handleAddLinkedPage,
|
handleAddLinkedPage,
|
||||||
handleDuplicate,
|
handleDuplicate,
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
useDropTarget,
|
useDropTarget,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { RenameModal } from '@affine/component/rename-modal';
|
import { RenameModal } from '@affine/component/rename-modal';
|
||||||
|
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
|
||||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||||
|
import type { DocPermissionActions } from '@affine/core/modules/permissions';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { extractEmojiIcon } from '@affine/core/utils';
|
import { extractEmojiIcon } from '@affine/core/utils';
|
||||||
@@ -81,6 +83,7 @@ export interface BaseExplorerTreeNodeProps {
|
|||||||
interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {
|
interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {
|
||||||
renameable?: boolean;
|
renameable?: boolean;
|
||||||
onRename?: (newName: string) => void;
|
onRename?: (newName: string) => void;
|
||||||
|
renameableGuard?: { docId: string; action: DocPermissionActions };
|
||||||
defaultRenaming?: boolean;
|
defaultRenaming?: boolean;
|
||||||
|
|
||||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||||
@@ -127,6 +130,7 @@ export const ExplorerTreeNode = ({
|
|||||||
active,
|
active,
|
||||||
defaultRenaming,
|
defaultRenaming,
|
||||||
renameable,
|
renameable,
|
||||||
|
renameableGuard,
|
||||||
onRename,
|
onRename,
|
||||||
disabled,
|
disabled,
|
||||||
collapsed,
|
collapsed,
|
||||||
@@ -279,7 +283,24 @@ export const ExplorerTreeNode = ({
|
|||||||
renameable
|
renameable
|
||||||
? {
|
? {
|
||||||
index: 0,
|
index: 0,
|
||||||
view: (
|
view: renameableGuard ? (
|
||||||
|
<DocPermissionGuard
|
||||||
|
permission={renameableGuard.action}
|
||||||
|
docId={renameableGuard.docId}
|
||||||
|
>
|
||||||
|
{can => (
|
||||||
|
<MenuItem
|
||||||
|
key={'explorer-tree-rename'}
|
||||||
|
type={'default'}
|
||||||
|
prefixIcon={<EditIcon />}
|
||||||
|
onClick={() => setRenaming(true)}
|
||||||
|
disabled={!can}
|
||||||
|
>
|
||||||
|
{t['com.affine.menu.rename']()}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</DocPermissionGuard>
|
||||||
|
) : (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={'explorer-tree-rename'}
|
key={'explorer-tree-rename'}
|
||||||
type={'default'}
|
type={'default'}
|
||||||
@@ -293,7 +314,7 @@ export const ExplorerTreeNode = ({
|
|||||||
: null,
|
: null,
|
||||||
] as (NodeOperation | null)[]
|
] as (NodeOperation | null)[]
|
||||||
).filter((t): t is NodeOperation => t !== null),
|
).filter((t): t is NodeOperation => t !== null),
|
||||||
[renameable, t]
|
[renameable, renameableGuard, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { menuOperations, inlineOperations } = useMemo(() => {
|
const { menuOperations, inlineOperations } = useMemo(() => {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import {
|
import {
|
||||||
backoffRetry,
|
backoffRetry,
|
||||||
catchErrorInto,
|
|
||||||
effect,
|
effect,
|
||||||
Entity,
|
Entity,
|
||||||
exhaustMapWithTrailing,
|
exhaustMapWithTrailing,
|
||||||
@@ -12,18 +10,18 @@ import {
|
|||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { EMPTY, mergeMap } from 'rxjs';
|
import { EMPTY, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
import { isBackendError, isNetworkError } from '../../cloud';
|
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
import type { WorkspacePermissionStore } from '../stores/permission';
|
import type { WorkspacePermissionStore } from '../stores/permission';
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:workspace-permission');
|
|
||||||
|
|
||||||
export class WorkspacePermission extends Entity {
|
export class WorkspacePermission extends Entity {
|
||||||
isOwner$ = new LiveData<boolean | null>(null);
|
private readonly cache$ = LiveData.from(
|
||||||
isAdmin$ = new LiveData<boolean | null>(null);
|
this.store.watchWorkspacePermissionCache(),
|
||||||
isTeam$ = new LiveData<boolean | null>(null);
|
undefined
|
||||||
isLoading$ = new LiveData(false);
|
);
|
||||||
error$ = new LiveData<any>(null);
|
isOwner$ = this.cache$.map(cache => cache?.isOwner ?? null);
|
||||||
|
isAdmin$ = this.cache$.map(cache => cache?.isAdmin ?? null);
|
||||||
|
isTeam$ = this.cache$.map(cache => cache?.isTeam ?? null);
|
||||||
|
isRevalidating$ = new LiveData(false);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
@@ -51,27 +49,30 @@ export class WorkspacePermission extends Entity {
|
|||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
backoffRetry({
|
backoffRetry({
|
||||||
when: isNetworkError,
|
|
||||||
count: Infinity,
|
count: Infinity,
|
||||||
}),
|
}),
|
||||||
backoffRetry({
|
|
||||||
when: isBackendError,
|
|
||||||
}),
|
|
||||||
mergeMap(({ isOwner, isAdmin, isTeam }) => {
|
mergeMap(({ isOwner, isAdmin, isTeam }) => {
|
||||||
this.isAdmin$.next(isAdmin);
|
this.store.setWorkspacePermissionCache({
|
||||||
this.isOwner$.next(isOwner);
|
isOwner,
|
||||||
this.isTeam$.next(isTeam);
|
isAdmin,
|
||||||
|
isTeam,
|
||||||
|
});
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
catchErrorInto(this.error$, error => {
|
onStart(() => this.isRevalidating$.setValue(true)),
|
||||||
logger.error('Failed to fetch isOwner', error);
|
onComplete(() => this.isRevalidating$.setValue(false))
|
||||||
}),
|
|
||||||
onStart(() => this.isLoading$.setValue(true)),
|
|
||||||
onComplete(() => this.isLoading$.setValue(false))
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async waitForRevalidation(signal?: AbortSignal) {
|
||||||
|
this.revalidate();
|
||||||
|
await this.isRevalidating$.waitFor(
|
||||||
|
isRevalidating => !isRevalidating,
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.revalidate.unsubscribe();
|
this.revalidate.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,21 @@ export {
|
|||||||
DocGrantedUsersService,
|
DocGrantedUsersService,
|
||||||
type GrantedUser,
|
type GrantedUser,
|
||||||
} from './services/doc-granted-users';
|
} from './services/doc-granted-users';
|
||||||
|
export { GuardService } from './services/guard';
|
||||||
export { MemberSearchService } from './services/member-search';
|
export { MemberSearchService } from './services/member-search';
|
||||||
export { WorkspaceMembersService } from './services/members';
|
export { WorkspaceMembersService } from './services/members';
|
||||||
export { WorkspacePermissionService } from './services/permission';
|
export { WorkspacePermissionService } from './services/permission';
|
||||||
|
export {
|
||||||
|
type DocPermissionActions,
|
||||||
|
type WorkspacePermissionActions,
|
||||||
|
} from './stores/guard';
|
||||||
|
|
||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { WorkspaceServerService } from '../cloud';
|
import { WorkspaceServerService } from '../cloud';
|
||||||
import { DocScope, DocService } from '../doc';
|
import { DocScope, DocService } from '../doc';
|
||||||
import {
|
import {
|
||||||
|
WorkspaceLocalState,
|
||||||
WorkspaceScope,
|
WorkspaceScope,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
@@ -19,10 +25,12 @@ import {
|
|||||||
import { WorkspaceMembers } from './entities/members';
|
import { WorkspaceMembers } from './entities/members';
|
||||||
import { WorkspacePermission } from './entities/permission';
|
import { WorkspacePermission } from './entities/permission';
|
||||||
import { DocGrantedUsersService } from './services/doc-granted-users';
|
import { DocGrantedUsersService } from './services/doc-granted-users';
|
||||||
|
import { GuardService } from './services/guard';
|
||||||
import { MemberSearchService } from './services/member-search';
|
import { MemberSearchService } from './services/member-search';
|
||||||
import { WorkspaceMembersService } from './services/members';
|
import { WorkspaceMembersService } from './services/members';
|
||||||
import { WorkspacePermissionService } from './services/permission';
|
import { WorkspacePermissionService } from './services/permission';
|
||||||
import { DocGrantedUsersStore } from './stores/doc-granted-users';
|
import { DocGrantedUsersStore } from './stores/doc-granted-users';
|
||||||
|
import { GuardStore } from './stores/guard';
|
||||||
import { MemberSearchStore } from './stores/member-search';
|
import { MemberSearchStore } from './stores/member-search';
|
||||||
import { WorkspaceMembersStore } from './stores/members';
|
import { WorkspaceMembersStore } from './stores/members';
|
||||||
import { WorkspacePermissionStore } from './stores/permission';
|
import { WorkspacePermissionStore } from './stores/permission';
|
||||||
@@ -35,13 +43,22 @@ export function configurePermissionsModule(framework: Framework) {
|
|||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
WorkspacePermissionStore,
|
WorkspacePermissionStore,
|
||||||
])
|
])
|
||||||
.store(WorkspacePermissionStore, [WorkspaceServerService])
|
.store(WorkspacePermissionStore, [
|
||||||
|
WorkspaceServerService,
|
||||||
|
WorkspaceLocalState,
|
||||||
|
])
|
||||||
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
|
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
|
||||||
.service(WorkspaceMembersService, [WorkspaceMembersStore, WorkspaceService])
|
.service(WorkspaceMembersService, [WorkspaceMembersStore, WorkspaceService])
|
||||||
.store(WorkspaceMembersStore, [WorkspaceServerService])
|
.store(WorkspaceMembersStore, [WorkspaceServerService])
|
||||||
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService])
|
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService])
|
||||||
.service(MemberSearchService, [MemberSearchStore, WorkspaceService])
|
.service(MemberSearchService, [MemberSearchStore, WorkspaceService])
|
||||||
.store(MemberSearchStore, [WorkspaceServerService]);
|
.store(MemberSearchStore, [WorkspaceServerService])
|
||||||
|
.service(GuardService, [
|
||||||
|
GuardStore,
|
||||||
|
WorkspaceService,
|
||||||
|
WorkspacePermissionService,
|
||||||
|
])
|
||||||
|
.store(GuardStore, [WorkspaceService, WorkspaceServerService]);
|
||||||
|
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import {
|
||||||
|
backoffRetry,
|
||||||
|
effect,
|
||||||
|
exhaustMapWithTrailing,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
Service,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
combineLatest,
|
||||||
|
EMPTY,
|
||||||
|
exhaustMap,
|
||||||
|
groupBy,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
Observable,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import type { WorkspaceService } from '../../workspace';
|
||||||
|
import type {
|
||||||
|
DocPermissionActions,
|
||||||
|
GuardStore,
|
||||||
|
WorkspacePermissionActions,
|
||||||
|
} from '../stores/guard';
|
||||||
|
import type { WorkspacePermissionService } from './permission';
|
||||||
|
|
||||||
|
export class GuardService extends Service {
|
||||||
|
constructor(
|
||||||
|
private readonly guardStore: GuardStore,
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly workspacePermissionService: WorkspacePermissionService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly workspacePermissions$ = new LiveData<
|
||||||
|
Partial<Record<WorkspacePermissionActions, boolean>>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
private readonly docPermissions$ = new LiveData<
|
||||||
|
Record<string, Partial<Record<DocPermissionActions, boolean>>>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
private readonly isAdmin$ = LiveData.computed(get => {
|
||||||
|
const isOwner = get(this.workspacePermissionService.permission.isOwner$);
|
||||||
|
const isAdmin = get(this.workspacePermissionService.permission.isAdmin$);
|
||||||
|
if (isOwner === null && isAdmin === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return isOwner || isAdmin;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* guardService.can$('Workspace_Properties_Update');
|
||||||
|
* guardService.can$('Doc_Update', docId);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
can$<T extends WorkspacePermissionActions | DocPermissionActions>(
|
||||||
|
action: T,
|
||||||
|
...args: T extends DocPermissionActions ? [string] : []
|
||||||
|
): LiveData<boolean> {
|
||||||
|
const docId = args[0];
|
||||||
|
return LiveData.from(
|
||||||
|
new Observable(subscriber => {
|
||||||
|
// revalidate permission
|
||||||
|
if (docId) {
|
||||||
|
this.revalidateDocPermission(docId);
|
||||||
|
} else {
|
||||||
|
this.revalidateWorkspacePermission();
|
||||||
|
}
|
||||||
|
// revalidate workspace permission if it's not initialized
|
||||||
|
if (this.isAdmin$.value === null) {
|
||||||
|
this.workspacePermissionService.permission.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev = false;
|
||||||
|
|
||||||
|
const subscription = combineLatest([
|
||||||
|
(docId
|
||||||
|
? this.docPermissions$.pipe(
|
||||||
|
map(permissions => permissions[docId] ?? false)
|
||||||
|
)
|
||||||
|
: this.workspacePermissions$) as Observable<
|
||||||
|
Record<string, boolean>
|
||||||
|
>,
|
||||||
|
this.isAdmin$,
|
||||||
|
]).subscribe(([permissions, isAdmin]) => {
|
||||||
|
if (isAdmin) {
|
||||||
|
return subscriber.next(true);
|
||||||
|
}
|
||||||
|
const current = permissions[action] ?? false;
|
||||||
|
if (current !== prev) {
|
||||||
|
prev = current;
|
||||||
|
subscriber.next(current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async can<T extends WorkspacePermissionActions | DocPermissionActions>(
|
||||||
|
action: T,
|
||||||
|
...args: T extends DocPermissionActions ? [string] : []
|
||||||
|
): Promise<boolean> {
|
||||||
|
const docId = args[0];
|
||||||
|
|
||||||
|
if (this.isAdmin$.value === null) {
|
||||||
|
await this.workspacePermissionService.permission.waitForRevalidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAdmin$.value === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await (docId
|
||||||
|
? this.loadDocPermission(docId)
|
||||||
|
: this.loadWorkspacePermission());
|
||||||
|
|
||||||
|
return permissions[action as keyof typeof permissions] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly revalidateWorkspacePermission = effect(
|
||||||
|
exhaustMapWithTrailing(() =>
|
||||||
|
fromPromise(() => this.guardStore.getWorkspacePermissions()).pipe(
|
||||||
|
backoffRetry({
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
mergeMap(() => EMPTY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly revalidateDocPermission = effect(
|
||||||
|
groupBy((docId: string) => docId),
|
||||||
|
mergeMap(doc$ =>
|
||||||
|
doc$.pipe(
|
||||||
|
exhaustMap((docId: string) =>
|
||||||
|
fromPromise(() => this.loadDocPermission(docId)).pipe(
|
||||||
|
backoffRetry({
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
mergeMap(() => EMPTY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly loadWorkspacePermission = async () => {
|
||||||
|
if (this.workspaceService.workspace.flavour === 'local') {
|
||||||
|
return {} as Record<WorkspacePermissionActions, boolean>;
|
||||||
|
}
|
||||||
|
const permissions = await this.guardStore.getWorkspacePermissions();
|
||||||
|
this.workspacePermissions$.next(permissions);
|
||||||
|
return permissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly loadDocPermission = async (docId: string) => {
|
||||||
|
if (this.workspaceService.workspace.flavour === 'local') {
|
||||||
|
return {} as Record<DocPermissionActions, boolean>;
|
||||||
|
}
|
||||||
|
const permissions = await this.guardStore.getDocPermissions(docId);
|
||||||
|
this.docPermissions$.next({
|
||||||
|
...this.docPermissions$.value,
|
||||||
|
[docId]: permissions,
|
||||||
|
});
|
||||||
|
return permissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
override dispose() {
|
||||||
|
this.revalidateWorkspacePermission.unsubscribe();
|
||||||
|
this.revalidateDocPermission.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
type GetDocRolePermissionsQuery,
|
||||||
|
getDocRolePermissionsQuery,
|
||||||
|
getWorkspaceRolePermissionsQuery,
|
||||||
|
type WorkspacePermissions,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { WorkspaceServerService } from '../../cloud';
|
||||||
|
import type { WorkspaceService } from '../../workspace';
|
||||||
|
|
||||||
|
export type WorkspacePermissionActions = keyof Omit<
|
||||||
|
WorkspacePermissions,
|
||||||
|
'__typename'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DocPermissionActions = keyof Omit<
|
||||||
|
GetDocRolePermissionsQuery['workspace']['doc']['permissions'],
|
||||||
|
'__typename'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export class GuardStore extends Store {
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly workspaceServerService: WorkspaceServerService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkspacePermissions(): Promise<
|
||||||
|
Record<WorkspacePermissionActions, boolean>
|
||||||
|
> {
|
||||||
|
if (!this.workspaceServerService.server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
const data = await this.workspaceServerService.server.gql({
|
||||||
|
query: getWorkspaceRolePermissionsQuery,
|
||||||
|
variables: {
|
||||||
|
id: this.workspaceService.workspace.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data.workspaceRolePermissions.permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDocPermissions(
|
||||||
|
docId: string
|
||||||
|
): Promise<Record<DocPermissionActions, boolean>> {
|
||||||
|
if (!this.workspaceServerService.server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
const data = await this.workspaceServerService.server.gql({
|
||||||
|
query: getDocRolePermissionsQuery,
|
||||||
|
variables: {
|
||||||
|
workspaceId: this.workspaceService.workspace.id,
|
||||||
|
docId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data.workspace.doc.permissions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,13 @@ import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
|||||||
import { getWorkspaceInfoQuery, leaveWorkspaceMutation } from '@affine/graphql';
|
import { getWorkspaceInfoQuery, leaveWorkspaceMutation } from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { WorkspaceLocalState } from '../../workspace';
|
||||||
|
|
||||||
export class WorkspacePermissionStore extends Store {
|
export class WorkspacePermissionStore extends Store {
|
||||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
constructor(
|
||||||
|
private readonly workspaceServerService: WorkspaceServerService,
|
||||||
|
private readonly workspaceLocalState: WorkspaceLocalState
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,4 +41,20 @@ export class WorkspacePermissionStore extends Store {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watchWorkspacePermissionCache() {
|
||||||
|
return this.workspaceLocalState.watch<{
|
||||||
|
isOwner: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
isTeam: boolean;
|
||||||
|
}>('permission');
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorkspacePermissionCache(permission: {
|
||||||
|
isOwner: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
isTeam: boolean;
|
||||||
|
}) {
|
||||||
|
this.workspaceLocalState.set('permission', permission);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+35
-30
@@ -22,9 +22,11 @@ const getRoleName = (role: DocRole, t: ReturnType<typeof useI18n>) => {
|
|||||||
export const MembersPermission = ({
|
export const MembersPermission = ({
|
||||||
openPaywallModal,
|
openPaywallModal,
|
||||||
hittingPaywall,
|
hittingPaywall,
|
||||||
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
hittingPaywall: boolean;
|
hittingPaywall: boolean;
|
||||||
openPaywallModal?: () => void;
|
openPaywallModal?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [docRole, setDocRole] = useState<DocRole>(DocRole.Manager);
|
const [docRole, setDocRole] = useState<DocRole>(DocRole.Manager);
|
||||||
@@ -63,38 +65,40 @@ export const MembersPermission = ({
|
|||||||
align: 'end',
|
align: 'end',
|
||||||
}}
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
disabled ? null : (
|
||||||
<MenuItem
|
<>
|
||||||
onSelect={selectManage}
|
<MenuItem
|
||||||
selected={docRole === DocRole.Manager}
|
onSelect={selectManage}
|
||||||
>
|
selected={docRole === DocRole.Manager}
|
||||||
<div className={styles.publicItemRowStyle}>
|
>
|
||||||
{t['com.affine.share-menu.option.permission.can-manage']()}
|
<div className={styles.publicItemRowStyle}>
|
||||||
</div>
|
{t['com.affine.share-menu.option.permission.can-manage']()}
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onSelect={selectEdit}
|
|
||||||
selected={docRole === DocRole.Editor}
|
|
||||||
>
|
|
||||||
<div className={styles.publicItemRowStyle}>
|
|
||||||
<div className={styles.tagContainerStyle}>
|
|
||||||
{t['com.affine.share-menu.option.permission.can-edit']()}
|
|
||||||
<PlanTag />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MenuItem>
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
<MenuItem
|
onSelect={selectEdit}
|
||||||
onSelect={selectRead}
|
selected={docRole === DocRole.Editor}
|
||||||
selected={docRole === DocRole.Reader}
|
>
|
||||||
>
|
<div className={styles.publicItemRowStyle}>
|
||||||
<div className={styles.publicItemRowStyle}>
|
<div className={styles.tagContainerStyle}>
|
||||||
<div className={styles.tagContainerStyle}>
|
{t['com.affine.share-menu.option.permission.can-edit']()}
|
||||||
{t['com.affine.share-menu.option.permission.can-read']()}
|
<PlanTag />
|
||||||
<PlanTag />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MenuItem>
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
</>
|
onSelect={selectRead}
|
||||||
|
selected={docRole === DocRole.Reader}
|
||||||
|
>
|
||||||
|
<div className={styles.publicItemRowStyle}>
|
||||||
|
<div className={styles.tagContainerStyle}>
|
||||||
|
{t['com.affine.share-menu.option.permission.can-read']()}
|
||||||
|
<PlanTag />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuTrigger
|
<MenuTrigger
|
||||||
@@ -103,6 +107,7 @@ export const MembersPermission = ({
|
|||||||
contentStyle={{
|
contentStyle={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{currentRoleName}
|
{currentRoleName}
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
|
|||||||
+27
-22
@@ -15,7 +15,7 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const PublicDoc = () => {
|
export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const shareInfoService = useService(ShareInfoService);
|
const shareInfoService = useService(ShareInfoService);
|
||||||
const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
|
const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
|
||||||
@@ -98,27 +98,31 @@ export const PublicDoc = () => {
|
|||||||
align: 'end',
|
align: 'end',
|
||||||
}}
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
disabled ? null : (
|
||||||
<MenuItem
|
<>
|
||||||
prefixIcon={<LockIcon />}
|
<MenuItem
|
||||||
onSelect={onDisablePublic}
|
prefixIcon={<LockIcon />}
|
||||||
selected={!isSharedPage}
|
onSelect={onDisablePublic}
|
||||||
>
|
selected={!isSharedPage}
|
||||||
<div className={styles.publicItemRowStyle}>
|
>
|
||||||
<div>{t['com.affine.share-menu.option.link.no-access']()}</div>
|
<div className={styles.publicItemRowStyle}>
|
||||||
</div>
|
<div>
|
||||||
</MenuItem>
|
{t['com.affine.share-menu.option.link.no-access']()}
|
||||||
<MenuItem
|
</div>
|
||||||
prefixIcon={<ViewIcon />}
|
</div>
|
||||||
onSelect={onClickAnyoneReadOnlyShare}
|
</MenuItem>
|
||||||
data-testid="share-link-menu-enable-share"
|
<MenuItem
|
||||||
selected={!!isSharedPage}
|
prefixIcon={<ViewIcon />}
|
||||||
>
|
onSelect={onClickAnyoneReadOnlyShare}
|
||||||
<div className={styles.publicItemRowStyle}>
|
data-testid="share-link-menu-enable-share"
|
||||||
<div>{t['com.affine.share-menu.option.link.readonly']()}</div>
|
selected={!!isSharedPage}
|
||||||
</div>
|
>
|
||||||
</MenuItem>
|
<div className={styles.publicItemRowStyle}>
|
||||||
</>
|
<div>{t['com.affine.share-menu.option.link.readonly']()}</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuTrigger
|
<MenuTrigger
|
||||||
@@ -128,6 +132,7 @@ export const PublicDoc = () => {
|
|||||||
contentStyle={{
|
contentStyle={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{isSharedPage
|
{isSharedPage
|
||||||
? t['com.affine.share-menu.option.link.readonly']()
|
? t['com.affine.share-menu.option.link.readonly']()
|
||||||
|
|||||||
+31
-22
@@ -8,13 +8,15 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import {
|
import {
|
||||||
DocGrantedUsersService,
|
DocGrantedUsersService,
|
||||||
type GrantedUser,
|
type GrantedUser,
|
||||||
|
GuardService,
|
||||||
} from '@affine/core/modules/permissions';
|
} from '@affine/core/modules/permissions';
|
||||||
import { DocRole, UserFriendlyError } from '@affine/graphql';
|
import { DocRole, UserFriendlyError } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PlanTag } from '../plan-tag';
|
import { PlanTag } from '../plan-tag';
|
||||||
@@ -77,7 +79,6 @@ export const MemberItem = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TODO(@eyhn): add guard here */}
|
|
||||||
<Menu
|
<Menu
|
||||||
items={
|
items={
|
||||||
<Options
|
<Options
|
||||||
@@ -118,6 +119,15 @@ const Options = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const docGrantedUsersService = useService(DocGrantedUsersService);
|
const docGrantedUsersService = useService(DocGrantedUsersService);
|
||||||
|
const docService = useService(DocService);
|
||||||
|
const guardService = useService(GuardService);
|
||||||
|
|
||||||
|
const canTransferOwner = useLiveData(
|
||||||
|
guardService.can$('Doc_TransferOwner', docService.doc.id)
|
||||||
|
);
|
||||||
|
const canManageUsers = useLiveData(
|
||||||
|
guardService.can$('Doc_Users_Manage', docService.doc.id)
|
||||||
|
);
|
||||||
|
|
||||||
const changeToManager = useAsyncCallback(async () => {
|
const changeToManager = useAsyncCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -188,20 +198,17 @@ const Options = ({
|
|||||||
label: t['com.affine.share-menu.option.permission.can-manage'](),
|
label: t['com.affine.share-menu.option.permission.can-manage'](),
|
||||||
onClick: changeToManager,
|
onClick: changeToManager,
|
||||||
role: DocRole.Manager,
|
role: DocRole.Manager,
|
||||||
show: true, // TODO(@eyhn): add guard here
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t['com.affine.share-menu.option.permission.can-edit'](),
|
label: t['com.affine.share-menu.option.permission.can-edit'](),
|
||||||
onClick: changeToEditor,
|
onClick: changeToEditor,
|
||||||
role: DocRole.Editor,
|
role: DocRole.Editor,
|
||||||
show: true, // TODO(@eyhn): add guard here
|
|
||||||
showPlanTag: true,
|
showPlanTag: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t['com.affine.share-menu.option.permission.can-read'](),
|
label: t['com.affine.share-menu.option.permission.can-read'](),
|
||||||
onClick: changeToReader,
|
onClick: changeToReader,
|
||||||
role: DocRole.Reader,
|
role: DocRole.Reader,
|
||||||
show: true, // TODO(@eyhn): add guard here
|
|
||||||
showPlanTag: true,
|
showPlanTag: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -209,26 +216,28 @@ const Options = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{operationButtonInfo.map(item =>
|
{operationButtonInfo.map(item => (
|
||||||
item.show ? (
|
<MenuItem
|
||||||
<MenuItem
|
key={item.label}
|
||||||
key={item.label}
|
onSelect={item.onClick}
|
||||||
onSelect={item.onClick}
|
selected={memberRole === item.role}
|
||||||
selected={memberRole === item.role}
|
disabled={!canManageUsers}
|
||||||
>
|
>
|
||||||
<div className={styles.planTagContainer}>
|
<div className={styles.planTagContainer}>
|
||||||
{item.label} {item.showPlanTag ? <PlanTag /> : null}
|
{item.label} {item.showPlanTag ? <PlanTag /> : null}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null
|
))}
|
||||||
)}
|
<MenuItem onSelect={changeToOwner} disabled={!canTransferOwner}>
|
||||||
{/* TODO(@eyhn): add guard here */}
|
|
||||||
<MenuItem onSelect={changeToOwner}>
|
|
||||||
{t['com.affine.share-menu.member-management.set-as-owner']()}
|
{t['com.affine.share-menu.member-management.set-as-owner']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/* TODO(@eyhn): add guard here */}
|
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
<MenuItem onSelect={removeMember} type="danger" className={styles.remove}>
|
<MenuItem
|
||||||
|
onSelect={removeMember}
|
||||||
|
type="danger"
|
||||||
|
className={styles.remove}
|
||||||
|
disabled={!canManageUsers}
|
||||||
|
>
|
||||||
{t['com.affine.share-menu.member-management.remove']()}
|
{t['com.affine.share-menu.member-management.remove']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+16
-4
@@ -1,7 +1,9 @@
|
|||||||
import { Skeleton } from '@affine/component';
|
import { Skeleton } from '@affine/component';
|
||||||
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import {
|
import {
|
||||||
DocGrantedUsersService,
|
DocGrantedUsersService,
|
||||||
type GrantedUser,
|
type GrantedUser,
|
||||||
|
GuardService,
|
||||||
} from '@affine/core/modules/permissions';
|
} from '@affine/core/modules/permissions';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { ArrowLeftBigIcon } from '@blocksuite/icons/rc';
|
import { ArrowLeftBigIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -30,6 +32,12 @@ export const MemberManagement = ({
|
|||||||
const grantedUserCount = useLiveData(
|
const grantedUserCount = useLiveData(
|
||||||
docGrantedUsersService.grantedUserCount$
|
docGrantedUsersService.grantedUserCount$
|
||||||
);
|
);
|
||||||
|
const docService = useService(DocService);
|
||||||
|
const guardService = useService(GuardService);
|
||||||
|
|
||||||
|
const canManageUsers = useLiveData(
|
||||||
|
guardService.can$('Doc_Users_Manage', docService.doc.id)
|
||||||
|
);
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
@@ -62,11 +70,15 @@ export const MemberManagement = ({
|
|||||||
) : (
|
) : (
|
||||||
<Skeleton className={styles.scrollableRootStyle} />
|
<Skeleton className={styles.scrollableRootStyle} />
|
||||||
)}
|
)}
|
||||||
{/* TODO(@eyhn): add guard here */}
|
|
||||||
<div className={styles.footerStyle}>
|
<div className={styles.footerStyle}>
|
||||||
<span className={styles.addCollaboratorsStyle} onClick={onClickInvite}>
|
{canManageUsers ? (
|
||||||
{t['com.affine.share-menu.member-management.add-collaborators']()}
|
<span
|
||||||
</span>
|
className={styles.addCollaboratorsStyle}
|
||||||
|
onClick={onClickInvite}
|
||||||
|
>
|
||||||
|
{t['com.affine.share-menu.member-management.add-collaborators']()}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Skeleton } from '@affine/component';
|
import { Skeleton } from '@affine/component';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { ServerService } from '@affine/core/modules/cloud';
|
import { ServerService } from '@affine/core/modules/cloud';
|
||||||
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -61,6 +63,16 @@ export const AFFiNESharePage = (
|
|||||||
} = props;
|
} = props;
|
||||||
const shareInfoService = useService(ShareInfoService);
|
const shareInfoService = useService(ShareInfoService);
|
||||||
const serverService = useService(ServerService);
|
const serverService = useService(ServerService);
|
||||||
|
const docService = useService(DocService);
|
||||||
|
const guardService = useService(GuardService);
|
||||||
|
|
||||||
|
const canManageUsers = useLiveData(
|
||||||
|
guardService.can$('Doc_Users_Manage', docService.doc.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const canPublish = useLiveData(
|
||||||
|
guardService.can$('Doc_Publish', docService.doc.id)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shareInfoService.shareInfo.revalidate();
|
shareInfoService.shareInfo.revalidate();
|
||||||
@@ -85,7 +97,7 @@ export const AFFiNESharePage = (
|
|||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.columnContainerStyle}>
|
<div className={styles.columnContainerStyle}>
|
||||||
<InviteInput onFocus={props.onClickInvite} />
|
{canManageUsers && <InviteInput onFocus={props.onClickInvite} />}
|
||||||
<MembersRow onClick={props.onClickMembers} />
|
<MembersRow onClick={props.onClickMembers} />
|
||||||
<div className={styles.generalAccessStyle}>
|
<div className={styles.generalAccessStyle}>
|
||||||
{t['com.affine.share-menu.generalAccess']()}
|
{t['com.affine.share-menu.generalAccess']()}
|
||||||
@@ -93,8 +105,9 @@ export const AFFiNESharePage = (
|
|||||||
<MembersPermission
|
<MembersPermission
|
||||||
openPaywallModal={props.openPaywallModal}
|
openPaywallModal={props.openPaywallModal}
|
||||||
hittingPaywall={!!props.hittingPaywall}
|
hittingPaywall={!!props.hittingPaywall}
|
||||||
|
disabled={!canManageUsers}
|
||||||
/>
|
/>
|
||||||
<PublicDoc />
|
<PublicDoc disabled={!canPublish} />
|
||||||
</div>
|
</div>
|
||||||
<CopyLinkButton workspaceId={workspaceId} />
|
<CopyLinkButton workspaceId={workspaceId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,11 +19,18 @@ export class UserFriendlyError
|
|||||||
readonly status = this.response.status;
|
readonly status = this.response.status;
|
||||||
readonly code = this.response.code;
|
readonly code = this.response.code;
|
||||||
readonly type = this.response.type;
|
readonly type = this.response.type;
|
||||||
override readonly name = this.response.name;
|
readonly rawName = this.response.name;
|
||||||
override readonly message = this.response.message;
|
override readonly message = this.response.message;
|
||||||
readonly data = this.response.data;
|
readonly data = this.response.data;
|
||||||
readonly stacktrace = this.response.stacktrace;
|
readonly stacktrace = this.response.stacktrace;
|
||||||
|
|
||||||
|
override get name() {
|
||||||
|
if (this.rawName in ErrorNames) {
|
||||||
|
return this.rawName;
|
||||||
|
}
|
||||||
|
return ErrorNames.INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
static fromAnyError(response: any) {
|
static fromAnyError(response: any) {
|
||||||
if (response instanceof GraphQLError) {
|
if (response instanceof GraphQLError) {
|
||||||
return new UserFriendlyError(response.extensions);
|
return new UserFriendlyError(response.extensions);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
query getDocRolePermissions($workspaceId: String!, $docId: String!) {
|
||||||
|
workspace(id: $workspaceId) {
|
||||||
|
doc(docId: $docId) {
|
||||||
|
permissions {
|
||||||
|
Doc_Copy
|
||||||
|
Doc_Delete
|
||||||
|
Doc_Duplicate
|
||||||
|
Doc_Properties_Read
|
||||||
|
Doc_Properties_Update
|
||||||
|
Doc_Publish
|
||||||
|
Doc_Read
|
||||||
|
Doc_Restore
|
||||||
|
Doc_TransferOwner
|
||||||
|
Doc_Trash
|
||||||
|
Doc_Update
|
||||||
|
Doc_Users_Manage
|
||||||
|
Doc_Users_Read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -318,6 +318,35 @@ mutation deleteWorkspace($id: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDocRolePermissionsQuery = {
|
||||||
|
id: 'getDocRolePermissionsQuery' as const,
|
||||||
|
operationName: 'getDocRolePermissions',
|
||||||
|
definitionName: 'workspace',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query getDocRolePermissions($workspaceId: String!, $docId: String!) {
|
||||||
|
workspace(id: $workspaceId) {
|
||||||
|
doc(docId: $docId) {
|
||||||
|
permissions {
|
||||||
|
Doc_Copy
|
||||||
|
Doc_Delete
|
||||||
|
Doc_Duplicate
|
||||||
|
Doc_Properties_Read
|
||||||
|
Doc_Properties_Update
|
||||||
|
Doc_Publish
|
||||||
|
Doc_Read
|
||||||
|
Doc_Restore
|
||||||
|
Doc_TransferOwner
|
||||||
|
Doc_Trash
|
||||||
|
Doc_Update
|
||||||
|
Doc_Users_Manage
|
||||||
|
Doc_Users_Read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const forkCopilotSessionMutation = {
|
export const forkCopilotSessionMutation = {
|
||||||
id: 'forkCopilotSessionMutation' as const,
|
id: 'forkCopilotSessionMutation' as const,
|
||||||
operationName: 'forkCopilotSession',
|
operationName: 'forkCopilotSession',
|
||||||
@@ -1551,6 +1580,33 @@ query workspaceQuota($id: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWorkspaceRolePermissionsQuery = {
|
||||||
|
id: 'getWorkspaceRolePermissionsQuery' as const,
|
||||||
|
operationName: 'getWorkspaceRolePermissions',
|
||||||
|
definitionName: 'workspaceRolePermissions',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query getWorkspaceRolePermissions($id: String!) {
|
||||||
|
workspaceRolePermissions(id: $id) {
|
||||||
|
permissions {
|
||||||
|
Workspace_CreateDoc
|
||||||
|
Workspace_Delete
|
||||||
|
Workspace_Organize_Read
|
||||||
|
Workspace_Properties_Create
|
||||||
|
Workspace_Properties_Delete
|
||||||
|
Workspace_Properties_Read
|
||||||
|
Workspace_Properties_Update
|
||||||
|
Workspace_Settings_Read
|
||||||
|
Workspace_Settings_Update
|
||||||
|
Workspace_Sync
|
||||||
|
Workspace_TransferOwner
|
||||||
|
Workspace_Users_Manage
|
||||||
|
Workspace_Users_Read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const approveWorkspaceTeamMemberMutation = {
|
export const approveWorkspaceTeamMemberMutation = {
|
||||||
id: 'approveWorkspaceTeamMemberMutation' as const,
|
id: 'approveWorkspaceTeamMemberMutation' as const,
|
||||||
operationName: 'approveWorkspaceTeamMember',
|
operationName: 'approveWorkspaceTeamMember',
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
query getWorkspaceRolePermissions($id: String!) {
|
||||||
|
workspaceRolePermissions(id: $id) {
|
||||||
|
permissions {
|
||||||
|
Workspace_CreateDoc
|
||||||
|
Workspace_Delete
|
||||||
|
Workspace_Organize_Read
|
||||||
|
Workspace_Properties_Create
|
||||||
|
Workspace_Properties_Delete
|
||||||
|
Workspace_Properties_Read
|
||||||
|
Workspace_Properties_Update
|
||||||
|
Workspace_Settings_Read
|
||||||
|
Workspace_Settings_Update
|
||||||
|
Workspace_Sync
|
||||||
|
Workspace_TransferOwner
|
||||||
|
Workspace_Users_Manage
|
||||||
|
Workspace_Users_Read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1958,6 +1958,37 @@ export type DeleteWorkspaceMutation = {
|
|||||||
deleteWorkspace: boolean;
|
deleteWorkspace: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetDocRolePermissionsQueryVariables = Exact<{
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
docId: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetDocRolePermissionsQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
workspace: {
|
||||||
|
__typename?: 'WorkspaceType';
|
||||||
|
doc: {
|
||||||
|
__typename?: 'DocType';
|
||||||
|
permissions: {
|
||||||
|
__typename?: 'DocPermissions';
|
||||||
|
Doc_Copy: boolean;
|
||||||
|
Doc_Delete: boolean;
|
||||||
|
Doc_Duplicate: boolean;
|
||||||
|
Doc_Properties_Read: boolean;
|
||||||
|
Doc_Properties_Update: boolean;
|
||||||
|
Doc_Publish: boolean;
|
||||||
|
Doc_Read: boolean;
|
||||||
|
Doc_Restore: boolean;
|
||||||
|
Doc_TransferOwner: boolean;
|
||||||
|
Doc_Trash: boolean;
|
||||||
|
Doc_Update: boolean;
|
||||||
|
Doc_Users_Manage: boolean;
|
||||||
|
Doc_Users_Read: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type ForkCopilotSessionMutationVariables = Exact<{
|
export type ForkCopilotSessionMutationVariables = Exact<{
|
||||||
options: ForkChatSessionInput;
|
options: ForkChatSessionInput;
|
||||||
}>;
|
}>;
|
||||||
@@ -3100,6 +3131,33 @@ export type WorkspaceQuotaQuery = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetWorkspaceRolePermissionsQueryVariables = Exact<{
|
||||||
|
id: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetWorkspaceRolePermissionsQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
workspaceRolePermissions: {
|
||||||
|
__typename?: 'WorkspaceRolePermissions';
|
||||||
|
permissions: {
|
||||||
|
__typename?: 'WorkspacePermissions';
|
||||||
|
Workspace_CreateDoc: boolean;
|
||||||
|
Workspace_Delete: boolean;
|
||||||
|
Workspace_Organize_Read: boolean;
|
||||||
|
Workspace_Properties_Create: boolean;
|
||||||
|
Workspace_Properties_Delete: boolean;
|
||||||
|
Workspace_Properties_Read: boolean;
|
||||||
|
Workspace_Properties_Update: boolean;
|
||||||
|
Workspace_Settings_Read: boolean;
|
||||||
|
Workspace_Settings_Update: boolean;
|
||||||
|
Workspace_Sync: boolean;
|
||||||
|
Workspace_TransferOwner: boolean;
|
||||||
|
Workspace_Users_Manage: boolean;
|
||||||
|
Workspace_Users_Read: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type ApproveWorkspaceTeamMemberMutationVariables = Exact<{
|
export type ApproveWorkspaceTeamMemberMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
userId: Scalars['String']['input'];
|
userId: Scalars['String']['input'];
|
||||||
@@ -3137,6 +3195,11 @@ export type Queries =
|
|||||||
variables: CopilotQuotaQueryVariables;
|
variables: CopilotQuotaQueryVariables;
|
||||||
response: CopilotQuotaQuery;
|
response: CopilotQuotaQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getDocRolePermissionsQuery';
|
||||||
|
variables: GetDocRolePermissionsQueryVariables;
|
||||||
|
response: GetDocRolePermissionsQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getCopilotHistoriesQuery';
|
name: 'getCopilotHistoriesQuery';
|
||||||
variables: GetCopilotHistoriesQueryVariables;
|
variables: GetCopilotHistoriesQueryVariables;
|
||||||
@@ -3331,6 +3394,11 @@ export type Queries =
|
|||||||
name: 'workspaceQuotaQuery';
|
name: 'workspaceQuotaQuery';
|
||||||
variables: WorkspaceQuotaQueryVariables;
|
variables: WorkspaceQuotaQueryVariables;
|
||||||
response: WorkspaceQuotaQuery;
|
response: WorkspaceQuotaQuery;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: 'getWorkspaceRolePermissionsQuery';
|
||||||
|
variables: GetWorkspaceRolePermissionsQueryVariables;
|
||||||
|
response: GetWorkspaceRolePermissionsQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Mutations =
|
export type Mutations =
|
||||||
|
|||||||
@@ -1730,6 +1730,7 @@
|
|||||||
"com.affine.settings.workspace.template.page-desc": "New docs will use the specified template, ignoring default settings.",
|
"com.affine.settings.workspace.template.page-desc": "New docs will use the specified template, ignoring default settings.",
|
||||||
"com.affine.settings.workspace.template.page-select": "Template for new doc",
|
"com.affine.settings.workspace.template.page-select": "Template for new doc",
|
||||||
"com.affine.settings.workspace.template.remove": "Remove template",
|
"com.affine.settings.workspace.template.remove": "Remove template",
|
||||||
|
"com.affine.no-permission": "You don't have permission to do this",
|
||||||
"com.affine.settings.workspace.storage.unused-blobs": "Unused blobs",
|
"com.affine.settings.workspace.storage.unused-blobs": "Unused blobs",
|
||||||
"com.affine.settings.workspace.storage.unused-blobs.empty": "No unused blobs",
|
"com.affine.settings.workspace.storage.unused-blobs.empty": "No unused blobs",
|
||||||
"com.affine.settings.workspace.storage.unused-blobs.selected": "Selected",
|
"com.affine.settings.workspace.storage.unused-blobs.selected": "Selected",
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
|
waitForEditorLoad,
|
||||||
waitForEmptyEditor,
|
waitForEmptyEditor,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
|
await waitForEditorLoad(page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await waitForEmptyEditor(page);
|
await waitForEmptyEditor(page);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user