* util

* fix page position collation

* support fixed toolbar in templates editor

* date localization

* fix clipped emoji in templates editor

* fix page updated time object

* fix flickers

* fix: remove redundant breadcrumb from destination modal
This commit is contained in:
Philip Okugbe
2026-05-28 16:20:37 +01:00
committed by GitHub
parent 830b5b4d45
commit 33895b0607
38 changed files with 360 additions and 151 deletions
@@ -165,6 +165,21 @@ export class PersistenceExtension implements Extension {
}
if (page) {
document.broadcastStateless(
JSON.stringify({
type: 'page.updated',
updatedAt: new Date().toISOString(),
lastUpdatedById: context?.user?.id,
lastUpdatedBy: context?.user
? {
id: context.user?.id,
name: context.user?.name,
avatarUrl: context.user?.avatarUrl,
}
: undefined,
}),
);
await this.syncTransclusion(pageId, page.workspaceId, tiptapJson);
}
@@ -4,6 +4,11 @@ export enum UserRole {
MEMBER = 'member',
}
export enum InviteUserRole {
ADMIN = 'admin', // can have owner permissions but cannot delete workspace
MEMBER = 'member',
}
export enum SpaceRole {
ADMIN = 'admin', // can manage space settings, members, and delete space
WRITER = 'writer', // can read and write pages in space
@@ -310,6 +310,7 @@ export class PageService {
expression: 'position',
direction: 'asc',
orderModifier: (ob) => ob.collate('C').asc(),
cursorExpression: sql`position collate "C"`,
},
{ expression: 'id', direction: 'asc' },
],
@@ -11,7 +11,7 @@ import {
MaxLength,
MinLength,
} from 'class-validator';
import { UserRole } from '../../../common/helpers/types/permission';
import { InviteUserRole } from '../../../common/helpers/types/permission';
import { NoUrls } from '../../../common/validators/no-urls.validator';
export class InviteUserDto {
@@ -32,7 +32,7 @@ export class InviteUserDto {
@IsUUID('all', { each: true })
groupIds: string[];
@IsEnum(UserRole)
@IsEnum(InviteUserRole)
role: string;
}
@@ -1,5 +1,6 @@
import {
BadRequestException,
ForbiddenException,
Inject,
Injectable,
Logger,
@@ -40,6 +41,7 @@ import {
AUDIT_SERVICE,
IAuditService,
} from '../../../integrations/audit/audit.service';
import { isAdminActingOnOwner } from '../workspace.util';
@Injectable()
export class WorkspaceInvitationService {
@@ -119,6 +121,10 @@ export class WorkspaceInvitationService {
): Promise<void> {
const { emails, role, groupIds } = inviteUserDto;
if (isAdminActingOnOwner(authUser.role, role)) {
throw new ForbiddenException();
}
let invites: WorkspaceInvitation[] = [];
try {
@@ -30,6 +30,7 @@ import { DomainService } from '../../../integrations/environment/domain.service'
import { jsonArrayFrom } from 'kysely/helpers/postgres';
import { addDays } from 'date-fns';
import { DISALLOWED_HOSTNAMES, WorkspaceStatus } from '../workspace.constants';
import { isAdminActingOnOwner } from '../workspace.util';
import { v4 } from 'uuid';
import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
@@ -590,8 +591,8 @@ export class WorkspaceService {
// prevent ADMIN from managing OWNER role
if (
(authUser.role === UserRole.ADMIN && newRole === UserRole.OWNER) ||
(authUser.role === UserRole.ADMIN && user.role === UserRole.OWNER)
isAdminActingOnOwner(authUser.role, newRole) ||
isAdminActingOnOwner(authUser.role, user.role)
) {
throw new ForbiddenException();
}
@@ -695,7 +696,7 @@ export class WorkspaceService {
throw new BadRequestException('You cannot deactivate yourself');
}
if (authUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) {
if (isAdminActingOnOwner(authUser.role, user.role)) {
throw new BadRequestException(
'You cannot deactivate a user with owner role',
);
@@ -753,7 +754,7 @@ export class WorkspaceService {
throw new BadRequestException('User is not deactivated');
}
if (authUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) {
if (isAdminActingOnOwner(authUser.role, user.role)) {
throw new BadRequestException(
'You cannot activate a user with owner role',
);
@@ -805,7 +806,7 @@ export class WorkspaceService {
throw new BadRequestException('You cannot delete yourself');
}
if (authUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) {
if (isAdminActingOnOwner(authUser.role, user.role)) {
throw new BadRequestException('You cannot delete a user with owner role');
}
@@ -0,0 +1,8 @@
import { UserRole } from '../../common/helpers/types/permission';
export function isAdminActingOnOwner(
authUserRole: string,
targetRole: string,
): boolean {
return authUserRole === UserRole.ADMIN && targetRole === UserRole.OWNER;
}
@@ -14,12 +14,14 @@ type SortField<DB, TB extends keyof DB, O> =
| (StringReference<DB, TB> & `${string}.${keyof O & string}`);
direction: OrderByDirection;
orderModifier?: OrderByModifiers;
cursorExpression?: ReferenceExpression<DB, TB>;
key?: keyof O & string;
}
| {
expression: ReferenceExpression<DB, TB>;
direction: OrderByDirection;
orderModifier?: OrderByModifiers;
cursorExpression?: ReferenceExpression<DB, TB>;
key: keyof O & string;
};
@@ -202,11 +204,12 @@ export async function executeWithCursorPagination<
const comparison = field.direction === defaultDirection ? '>' : '<';
const value = cursor[field.key as keyof typeof cursor];
const compareExpr = field.cursorExpression ?? field.expression;
const conditions = [eb(field.expression, comparison, value)];
const conditions = [eb(compareExpr, comparison, value)];
if (expression) {
conditions.push(and([eb(field.expression, '=', value), expression]));
conditions.push(and([eb(compareExpr, '=', value), expression]));
}
expression = or(conditions);