mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2765127a0d | ||
|
|
b4a494589d | ||
|
|
880ed2870d | ||
|
|
cd9c166927 | ||
|
|
549cf7c005 | ||
|
|
e14f499f3d | ||
|
|
b814bd0f12 | ||
|
|
b86abd3d40 | ||
|
|
3b858746e3 | ||
|
|
66b424a3b8 | ||
|
|
ab43031375 | ||
|
|
8c2c49ea6d | ||
|
|
8913d20aa0 |
@@ -18,6 +18,7 @@ export type FieldProps = {
|
||||
rowId: string;
|
||||
readOnly: boolean;
|
||||
onChange: (value: unknown) => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
type FieldShellProps = {
|
||||
@@ -99,9 +100,10 @@ type DetailFieldProps = {
|
||||
row: IBaseRow;
|
||||
readOnly: boolean;
|
||||
onUpdate: (propertyId: string, value: unknown) => void;
|
||||
onEditingChange: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldProps) {
|
||||
export function DetailField({ property, row, readOnly, onUpdate, onEditingChange }: DetailFieldProps) {
|
||||
const descriptor = getDescriptor(property.type);
|
||||
const value = descriptor?.systemAccessor
|
||||
? descriptor.systemAccessor(row)
|
||||
@@ -112,6 +114,7 @@ export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldPr
|
||||
rowId: row.id,
|
||||
readOnly,
|
||||
onChange: (next: unknown) => onUpdate(property.id, next),
|
||||
onEditingChange
|
||||
};
|
||||
|
||||
switch (property.type) {
|
||||
|
||||
@@ -9,7 +9,13 @@ const normalize = (s: string) => {
|
||||
return trimmed.length ? trimmed : null;
|
||||
};
|
||||
|
||||
export function FieldLongText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
export function FieldLongText({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
const text = toText(value);
|
||||
const [draft, setDraft] = useState(text);
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -23,6 +29,7 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(text);
|
||||
@@ -50,7 +57,10 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
|
||||
className={classes.fieldTextarea}
|
||||
classNames={{ input: classes.fieldTextareaInput }}
|
||||
value={draft}
|
||||
onFocus={() => setFocused(true)}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -11,7 +11,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
|
||||
const toDraft = (value: unknown) =>
|
||||
typeof value === "number" ? String(value) : "";
|
||||
|
||||
export function FieldNumber({ property, value, readOnly, onChange }: FieldProps) {
|
||||
export function FieldNumber({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
const typeOptions = property.typeOptions as NumberTypeOptions | undefined;
|
||||
const numValue = typeof value === "number" ? value : null;
|
||||
const [draft, setDraft] = useState(toDraft(value));
|
||||
@@ -36,6 +42,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(toDraft(value));
|
||||
@@ -54,6 +61,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
|
||||
onFocus={() => {
|
||||
setDraft(toDraft(value));
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
|
||||
@@ -5,7 +5,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
|
||||
|
||||
const toText = (value: unknown) => (typeof value === "string" ? value : "");
|
||||
|
||||
export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
export function FieldText({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
const text = toText(value);
|
||||
const [draft, setDraft] = useState(text);
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -20,6 +26,7 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(text);
|
||||
@@ -54,7 +61,10 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
className={classes.fieldInput}
|
||||
value={draft}
|
||||
maxLength={1000}
|
||||
onFocus={() => setFocused(true)}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ type PropertyRowProps = {
|
||||
onMenuOpenChange: (opened: boolean) => void;
|
||||
onMenuDirtyChange: (dirty: boolean) => void;
|
||||
onUpdate: (propertyId: string, value: unknown) => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
autoFocusValue?: boolean;
|
||||
onAutoFocused?: () => void;
|
||||
};
|
||||
@@ -29,6 +30,7 @@ export function PropertyRow({
|
||||
onMenuOpenChange,
|
||||
onMenuDirtyChange,
|
||||
onUpdate,
|
||||
onEditingChange,
|
||||
autoFocusValue,
|
||||
onAutoFocused,
|
||||
}: PropertyRowProps) {
|
||||
@@ -112,6 +114,7 @@ export function PropertyRow({
|
||||
row={row}
|
||||
readOnly={!canEdit}
|
||||
onUpdate={onUpdate}
|
||||
onEditingChange={onEditingChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -75,6 +75,7 @@ export function RowDetailModal({
|
||||
|
||||
const isSaving = updateRowMutation.isPending;
|
||||
const opened = !!openRowId;
|
||||
const [editingField, setEditingField] = useState(false);
|
||||
|
||||
// One field menu open at a time, mirroring the grid header's semantics.
|
||||
// The shared closeRequest atom asks an open dirty PropertyMenuContent to
|
||||
@@ -90,6 +91,7 @@ export function RowDetailModal({
|
||||
useEffect(() => {
|
||||
setOpenMenuId(null);
|
||||
menuDirtyRef.current = false;
|
||||
setEditingField(false);
|
||||
}, [openRowId]);
|
||||
|
||||
const handleMenuDirtyChange = useCallback((dirty: boolean) => {
|
||||
@@ -293,7 +295,7 @@ export function RowDetailModal({
|
||||
row={row}
|
||||
primaryProperty={primaryProperty}
|
||||
canEdit={canEdit}
|
||||
onClose={onClose}
|
||||
onEditingChange={setEditingField}
|
||||
onCommit={(value) => {
|
||||
if (!primaryProperty) return;
|
||||
updateRowMutation.mutate({
|
||||
@@ -317,6 +319,7 @@ export function RowDetailModal({
|
||||
autoFocusValue={property.id === newPropertyId}
|
||||
onAutoFocused={clearNewProperty}
|
||||
menuOpened={openMenuId === property.id}
|
||||
onEditingChange={setEditingField}
|
||||
onMenuOpenChange={(nextOpened) =>
|
||||
handleMenuOpenChange(property.id, nextOpened)
|
||||
}
|
||||
@@ -367,16 +370,38 @@ export function RowDetailModal({
|
||||
) : null}
|
||||
</div>
|
||||
<div className={classes.kbdHint}>
|
||||
{rowIndex >= 0 && rows.length > 1 && (
|
||||
{editingField ? (
|
||||
<>
|
||||
<kbd className={classes.kbd}>↑</kbd>
|
||||
<kbd className={classes.kbd}>↓</kbd>
|
||||
<span>{t("to navigate")}</span>
|
||||
<span className={classes.kbdGroup}>
|
||||
<kbd className={classes.kbd}>Ctrl/Cmd</kbd>
|
||||
<span className={classes.kbdPlus} >+</span>
|
||||
<kbd className={classes.kbd}>Enter</kbd>
|
||||
<span>{t("to save")}</span>
|
||||
</span>
|
||||
|
||||
<span className={classes.kbdSeparator} />
|
||||
|
||||
<span className={classes.kbdGroup}>
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to reset")}</span>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{rowIndex >= 0 && rows.length > 1 && (
|
||||
<>
|
||||
<kbd className={classes.kbd}>↑</kbd>
|
||||
<kbd className={classes.kbd}>↓</kbd>
|
||||
<span>{t("to navigate")}</span>
|
||||
<span className={classes.kbdSeparator} />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to close")}</span>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to close")}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IBaseProperty, IBaseRow } from "@/ee/base/types/base.types";
|
||||
import { timeAgo } from "@/lib/time.ts";
|
||||
@@ -9,7 +9,7 @@ type RowDetailTitleProps = {
|
||||
primaryProperty: IBaseProperty | undefined;
|
||||
canEdit: boolean;
|
||||
onCommit: (value: string) => void;
|
||||
onClose: () => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
export function RowDetailTitle({
|
||||
@@ -17,13 +17,24 @@ export function RowDetailTitle({
|
||||
primaryProperty,
|
||||
canEdit,
|
||||
onCommit,
|
||||
onClose,
|
||||
onEditingChange,
|
||||
}: RowDetailTitleProps) {
|
||||
const { t } = useTranslation();
|
||||
const initial = primaryProperty
|
||||
? (((row.cells ?? {})[primaryProperty.id] as string) ?? "")
|
||||
: "";
|
||||
const [value, setValue] = useState(initial);
|
||||
const cancelRef = useRef(false);
|
||||
|
||||
const commit = () => {
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setValue(initial);
|
||||
return;
|
||||
}
|
||||
if (value !== initial) onCommit(value);
|
||||
};
|
||||
|
||||
// Re-sync when the row changes underneath us (navigation or remote edit).
|
||||
useEffect(() => {
|
||||
@@ -43,18 +54,18 @@ export function RowDetailTitle({
|
||||
aria-label={primaryProperty?.name ?? t("Untitled")}
|
||||
value={value}
|
||||
maxLength={1000}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
onBlur={() => {
|
||||
if (value !== initial) onCommit(value);
|
||||
onFocus={() => {
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === "Escape") {
|
||||
cancelRef.current = true;
|
||||
e.currentTarget.blur();
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
onClose();
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -416,9 +416,25 @@
|
||||
}
|
||||
|
||||
.kbdHint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kbdGroup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.kbdPlus {
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.kbdSeparator {
|
||||
|
||||
@@ -22,7 +22,8 @@ import { TelemetryModule } from './integrations/telemetry/telemetry.module';
|
||||
import { RedisModule } from '@nestjs-labs/nestjs-ioredis';
|
||||
import { RedisConfigService } from './integrations/redis/redis-config.service';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
import KeyvRedis, { defaultReconnectStrategy } from '@keyv/redis';
|
||||
import { parseRedisUrl } from './common/helpers';
|
||||
import { LoggerModule } from './common/logger/logger.module';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import { NoopAuditModule } from './integrations/audit/audit.module';
|
||||
@@ -62,10 +63,20 @@ try {
|
||||
isGlobal: true,
|
||||
useFactory: async (environmentService: EnvironmentService) => {
|
||||
const redisUrl = environmentService.getRedisUrl();
|
||||
const { family, tls } = parseRedisUrl(redisUrl);
|
||||
|
||||
return {
|
||||
ttl: 5 * 1000,
|
||||
stores: [new KeyvRedis(redisUrl)],
|
||||
stores: [
|
||||
new KeyvRedis({
|
||||
url: redisUrl,
|
||||
socket: {
|
||||
family,
|
||||
reconnectStrategy: defaultReconnectStrategy,
|
||||
...tls,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
inject: [EnvironmentService],
|
||||
|
||||
@@ -66,6 +66,7 @@ export class CollaborationGateway {
|
||||
password: this.redisConfig.password,
|
||||
db: this.redisConfig.db,
|
||||
family: this.redisConfig.family,
|
||||
tls: this.redisConfig.tls,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
}),
|
||||
serverId: `collab-${os?.hostname()}-${nanoid(10)}`,
|
||||
|
||||
@@ -30,13 +30,14 @@ export type RedisConfig = {
|
||||
db: number;
|
||||
password?: string;
|
||||
family?: number;
|
||||
tls?: { rejectUnauthorized?: boolean };
|
||||
};
|
||||
|
||||
export function parseRedisUrl(redisUrl: string): RedisConfig {
|
||||
// format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6]
|
||||
// format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6][&rejectUnauthorized=false]
|
||||
const url = new URL(redisUrl);
|
||||
const { hostname, port, password, pathname, searchParams } = url;
|
||||
const portInt = parseInt(port, 10);
|
||||
const { hostname, port, password, pathname, protocol, searchParams } = url;
|
||||
const portInt = port ? parseInt(port, 10) : 6379;
|
||||
|
||||
let db: number = 0;
|
||||
// extract db value if present
|
||||
@@ -54,7 +55,14 @@ export function parseRedisUrl(redisUrl: string): RedisConfig {
|
||||
family = parseInt(familyParam, 10);
|
||||
}
|
||||
|
||||
return { host: hostname, port: portInt, password, db, family };
|
||||
const tls =
|
||||
protocol === 'rediss:'
|
||||
? searchParams.get('rejectUnauthorized') === 'false'
|
||||
? { rejectUnauthorized: false }
|
||||
: {}
|
||||
: undefined;
|
||||
|
||||
return { host: hostname, port: portInt, password: password || undefined, db, family, tls };
|
||||
}
|
||||
|
||||
export function createRetryStrategy() {
|
||||
|
||||
@@ -339,15 +339,25 @@ export class SpaceMemberService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (spaceMember.role === SpaceRole.ADMIN) {
|
||||
await this.validateLastAdmin(dto.spaceId);
|
||||
}
|
||||
await executeTx(this.db, async (trx) => {
|
||||
await trx
|
||||
.selectFrom('spaces')
|
||||
.select('id')
|
||||
.where('id', '=', dto.spaceId)
|
||||
.forUpdate()
|
||||
.executeTakeFirst();
|
||||
|
||||
await this.spaceMemberRepo.updateSpaceMember(
|
||||
{ role: dto.role },
|
||||
spaceMember.id,
|
||||
dto.spaceId,
|
||||
);
|
||||
if (spaceMember.role === SpaceRole.ADMIN) {
|
||||
await this.validateLastAdmin(dto.spaceId, trx);
|
||||
}
|
||||
|
||||
await this.spaceMemberRepo.updateSpaceMember(
|
||||
{ role: dto.role },
|
||||
spaceMember.id,
|
||||
dto.spaceId,
|
||||
trx,
|
||||
);
|
||||
});
|
||||
|
||||
this.auditService.log({
|
||||
event: AuditEvent.SPACE_MEMBER_ROLE_CHANGED,
|
||||
@@ -368,10 +378,14 @@ export class SpaceMemberService {
|
||||
});
|
||||
}
|
||||
|
||||
async validateLastAdmin(spaceId: string): Promise<void> {
|
||||
async validateLastAdmin(
|
||||
spaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
const spaceOwnerCount = await this.spaceMemberRepo.roleCountBySpaceId(
|
||||
SpaceRole.ADMIN,
|
||||
spaceId,
|
||||
trx,
|
||||
);
|
||||
if (spaceOwnerCount === 1) {
|
||||
throw new BadRequestException(
|
||||
|
||||
@@ -396,7 +396,10 @@ export class WorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (updateWorkspaceDto.aiSearch) {
|
||||
if (
|
||||
updateWorkspaceDto.aiSearch &&
|
||||
this.environmentService.getAiVectorDriver() !== 'turbopuffer'
|
||||
) {
|
||||
const tableExists = await isPageEmbeddingsTableExists(this.db);
|
||||
if (!tableExists) {
|
||||
throw new BadRequestException(
|
||||
|
||||
@@ -46,8 +46,10 @@ export class SpaceMemberRepo {
|
||||
updatableSpaceMember: UpdatableSpaceMember,
|
||||
spaceMemberId: string,
|
||||
spaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.updateTable('spaceMembers')
|
||||
.set(updatableSpaceMember)
|
||||
.where('id', '=', spaceMemberId)
|
||||
@@ -92,8 +94,13 @@ export class SpaceMemberRepo {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async roleCountBySpaceId(role: string, spaceId: string): Promise<number> {
|
||||
const { count } = await this.db
|
||||
async roleCountBySpaceId(
|
||||
role: string,
|
||||
spaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<number> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
const { count } = await db
|
||||
.selectFrom('spaceMembers')
|
||||
.select((eb) => eb.fn.count('role').as('count'))
|
||||
.where('role', '=', role)
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: e13af0ce05...6dfbcb9241
@@ -5,6 +5,7 @@ import {
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { EnvironmentService } from '../environment/environment.service';
|
||||
import { Redis } from 'ioredis';
|
||||
import { parseRedisUrl } from '../../common/helpers';
|
||||
|
||||
@Injectable()
|
||||
export class RedisHealthIndicator {
|
||||
@@ -19,8 +20,10 @@ export class RedisHealthIndicator {
|
||||
const indicator = this.healthIndicatorService.check(key);
|
||||
|
||||
try {
|
||||
const redis = new Redis(this.environmentService.getRedisUrl(), {
|
||||
const redisUrl = this.environmentService.getRedisUrl();
|
||||
const redis = new Redis(redisUrl, {
|
||||
maxRetriesPerRequest: 15,
|
||||
tls: parseRedisUrl(redisUrl).tls,
|
||||
});
|
||||
|
||||
await redis.ping();
|
||||
|
||||
@@ -18,6 +18,7 @@ import { GeneralQueueProcessor } from './processors/general-queue.processor';
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db,
|
||||
family: redisConfig.family,
|
||||
tls: redisConfig.tls,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
},
|
||||
defaultJobOptions: {
|
||||
|
||||
@@ -19,6 +19,7 @@ export class RedisConfigService implements RedisOptionsFactory {
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db,
|
||||
family: redisConfig.family,
|
||||
tls: redisConfig.tls,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
|
||||
import { EnvironmentService } from '../environment/environment.service';
|
||||
import { EnvironmentModule } from '../environment/environment.module';
|
||||
import { parseRedisUrl } from '../../common/helpers';
|
||||
import { createRetryStrategy, parseRedisUrl } from '../../common/helpers';
|
||||
import { AUTH_THROTTLER, AI_CHAT_THROTTLER } from './throttler-names';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
@@ -27,6 +27,8 @@ import Redis from 'ioredis';
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db,
|
||||
family: redisConfig.family,
|
||||
tls: redisConfig.tls,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
keyPrefix: 'throttle:',
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ export class WsRedisIoAdapter extends IoAdapter {
|
||||
|
||||
const options: RedisOptions = {
|
||||
family: this.redisConfig.family,
|
||||
tls: this.redisConfig.tls,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
};
|
||||
|
||||
|
||||
@@ -73,9 +73,13 @@ export const embedProviders: IEmbedProvider[] = [
|
||||
id: "vimeo",
|
||||
name: "Vimeo",
|
||||
regex:
|
||||
/^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/,
|
||||
getEmbedUrl: (match) => {
|
||||
return `https://player.vimeo.com/video/${match[4]}`;
|
||||
/^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:\/([\da-zA-Z]+))?/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
// preserve ?h= hash for unlisted videos
|
||||
const hash =
|
||||
match[5] ?? new URL(url, "https://vimeo.com").searchParams.get("h");
|
||||
const base = `https://player.vimeo.com/video/${match[4]}`;
|
||||
return hash ? `${base}?h=${hash}` : base;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user