diff --git a/app/api/migrations/migrations/150-per_namespace_lastSyncs/index.ts b/app/api/migrations/migrations/150-per_namespace_lastSyncs/index.ts new file mode 100644 index 0000000000..9834d70ce8 --- /dev/null +++ b/app/api/migrations/migrations/150-per_namespace_lastSyncs/index.ts @@ -0,0 +1,42 @@ +import { Db } from 'mongodb'; + +export default { + delta: 150, + + name: 'per_namespace_lastSyncs', + + description: 'Note the last synced timestamp on the syncs separately for each namespace.', + + reindex: false, + + async up(db: Db) { + process.stdout.write(`${this.name}...\r\n`); + const syncs = await db.collection('syncs').find().toArray(); + const updateOperations = syncs.map((sync: any) => { + const timestamp = sync.lastSync || 0; + return { + updateOne: { + filter: { _id: sync._id }, + update: { + $set: { + lastSyncs: { + settings: timestamp, + translationsV2: timestamp, + dictionaries: timestamp, + relationtypes: timestamp, + templates: timestamp, + files: timestamp, + connections: timestamp, + entities: timestamp, + }, + }, + $unset: { lastSync: '' }, + }, + }, + }; + }); + if (updateOperations.length) { + await db.collection('syncs').bulkWrite(updateOperations); + } + }, +}; diff --git a/app/api/migrations/migrations/150-per_namespace_lastSyncs/specs/150-per_namespace_lastSyncs.spec.ts b/app/api/migrations/migrations/150-per_namespace_lastSyncs/specs/150-per_namespace_lastSyncs.spec.ts new file mode 100644 index 0000000000..d7fbf62f21 --- /dev/null +++ b/app/api/migrations/migrations/150-per_namespace_lastSyncs/specs/150-per_namespace_lastSyncs.spec.ts @@ -0,0 +1,77 @@ +import { Db, ObjectId } from 'mongodb'; + +import testingDB from 'api/utils/testing_db'; +import migration from '../index'; +import { fixtures } from './fixtures'; + +let db: Db | null; + +beforeAll(async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + jest.spyOn(process.stdout, 'write').mockImplementation((str: string | Uint8Array) => true); + await testingDB.setupFixturesAndContext(fixtures); + db = testingDB.mongodb!; + await migration.up(db); +}); + +afterAll(async () => { + await testingDB.tearDown(); +}); + +describe('migration per_namespace_lastSyncs', () => { + it('should have a delta number', () => { + expect(migration.delta).toBe(150); + }); + + it('should add timestamps per collection to the sync objects', async () => { + const syncs = await db!.collection('syncs').find().toArray(); + expect(syncs).toEqual([ + { + _id: expect.any(ObjectId), + name: 'sync1', + lastSyncs: { + settings: 0, + translationsV2: 0, + dictionaries: 0, + relationtypes: 0, + templates: 0, + files: 0, + connections: 0, + entities: 0, + }, + }, + { + _id: expect.any(ObjectId), + name: 'sync2', + lastSyncs: { + settings: 1700127956, + translationsV2: 1700127956, + dictionaries: 1700127956, + relationtypes: 1700127956, + templates: 1700127956, + files: 1700127956, + connections: 1700127956, + entities: 1700127956, + }, + }, + { + _id: expect.any(ObjectId), + name: 'sync3', + lastSyncs: { + settings: 0, + translationsV2: 0, + dictionaries: 0, + relationtypes: 0, + templates: 0, + files: 0, + connections: 0, + entities: 0, + }, + }, + ]); + }); + + it('should not reindex', async () => { + expect(migration.reindex).toBe(false); + }); +}); diff --git a/app/api/migrations/migrations/150-per_namespace_lastSyncs/specs/fixtures.ts b/app/api/migrations/migrations/150-per_namespace_lastSyncs/specs/fixtures.ts new file mode 100644 index 0000000000..80855bc8e5 --- /dev/null +++ b/app/api/migrations/migrations/150-per_namespace_lastSyncs/specs/fixtures.ts @@ -0,0 +1,17 @@ +import { DBFixture } from '../types'; + +export const fixtures: DBFixture = { + syncs: [ + { + name: 'sync1', + lastSync: 0, + }, + { + name: 'sync2', + lastSync: 1700127956, + }, + { + name: 'sync3', + }, + ], +}; diff --git a/app/api/migrations/migrations/150-per_namespace_lastSyncs/types.ts b/app/api/migrations/migrations/150-per_namespace_lastSyncs/types.ts new file mode 100644 index 0000000000..53cf37f4d8 --- /dev/null +++ b/app/api/migrations/migrations/150-per_namespace_lastSyncs/types.ts @@ -0,0 +1,23 @@ +type OldSyncs = { + name: string; + lastSync: number; +}; + +type FaultyOldSync = { + name: string; +}; + +type NewSyncs = { + names: string; + lastSyncs: { + [name: string]: number; + }; +}; + +type Syncs = OldSyncs | NewSyncs | FaultyOldSync; + +type DBFixture = { + syncs: Syncs[]; +}; + +export type { DBFixture, Syncs, OldSyncs, NewSyncs }; diff --git a/app/api/suggestions/suggestions.ts b/app/api/suggestions/suggestions.ts index 85474c0fff..e22e710419 100644 --- a/app/api/suggestions/suggestions.ts +++ b/app/api/suggestions/suggestions.ts @@ -124,7 +124,7 @@ const updateExtractedMetadata = async (suggestions: IXSuggestionType[]) => { }; } - return files.save(file); + await files.save(file); }); }; diff --git a/app/api/sync/specs/fixtures.ts b/app/api/sync/specs/fixtures.ts index 42937749db..5bf8b5a1af 100644 --- a/app/api/sync/specs/fixtures.ts +++ b/app/api/sync/specs/fixtures.ts @@ -878,6 +878,197 @@ const host2Fixtures: DBFixture = { ], }; +const orderedHostIds = { + settings: db.id(), + translationsV2: db.id(), + dictionaries: db.id(), + relationtypes: db.id(), + templates: db.id(), + prop: db.id(), + files: db.id(), + connection1: db.id(), + connection2: db.id(), + hub: db.id(), + entity1: db.id(), + entity2: db.id(), + syncs: db.id(), + updatelogs: db.id(), +}; +const orderedHostFixtures: DBFixture = { + settings: [ + { + _id: orderedHostIds.settings, + languages: [{ key: 'en' as 'en', default: true, label: 'en' }], + sync: [ + { + url: 'http://localhost:6667', + name: 'target1', + active: true, + username: 'user', + password: 'password', + config: { + templates: { + [orderedHostIds.templates.toString()]: { + properties: [orderedHostIds.prop.toString()], + attachments: true, + }, + }, + relationtypes: [orderedHostIds.relationtypes.toString()], + }, + }, + ], + }, + ], + translationsV2: [ + { + _id: orderedHostIds.translationsV2, + context: { + type: 'Uwazi UI' as 'Uwazi UI', + label: 'User Interface', + id: 'System', + }, + key: 'Search', + language: 'en' as 'en', + value: 'Search', + }, + ], + dictionaries: [ + { + _id: orderedHostIds.dictionaries, + name: 'dict', + values: [ + { + label: 'a', + }, + ], + }, + ], + relationtypes: [{ _id: orderedHostIds.relationtypes, name: 'reltype' }], + templates: [ + { + _id: orderedHostIds.templates, + name: 'template1', + properties: [ + { + _id: orderedHostIds.prop, + name: 'prop', + label: 'prop', + type: 'select' as 'select', + content: orderedHostIds.dictionaries.toString(), + }, + ], + }, + ], + files: [ + { + _id: orderedHostIds.files, + entity: 'entity1', + type: 'attachment' as 'attachment', + filename: 'test.txt', + }, + ], + connections: [ + { + _id: orderedHostIds.connection1, + entity: 'entity1', + hub: orderedHostIds.hub, + template: orderedHostIds.relationtypes, + }, + { + _id: orderedHostIds.connection2, + entity: 'entity2', + hub: orderedHostIds.hub, + template: orderedHostIds.relationtypes, + }, + ], + entities: [ + { + _id: orderedHostIds.entity1, + language: 'en' as 'en', + sharedId: 'entity1', + title: '1', + template: orderedHostIds.templates, + metadata: {}, + }, + { + _id: orderedHostIds.entity2, + language: 'en' as 'en', + sharedId: 'entity2', + title: '2', + template: orderedHostIds.templates, + metadata: {}, + }, + ], + syncs: [ + { + lastSync: 0, + name: 'target1', + }, + ], + updatelogs: [ + { + timestamp: 1000, + namespace: 'settings', + mongoId: orderedHostIds.settings, + deleted: false, + }, + { + timestamp: 700, + namespace: 'translationsV2', + mongoId: orderedHostIds.translationsV2, + deleted: false, + }, + { + timestamp: 600, + namespace: 'dictionaries', + mongoId: orderedHostIds.dictionaries, + deleted: false, + }, + { + timestamp: 500, + namespace: 'relationtypes', + mongoId: orderedHostIds.relationtypes, + deleted: false, + }, + { + timestamp: 40, + namespace: 'templates', + mongoId: orderedHostIds.templates, + deleted: false, + }, + { + timestamp: 30, + namespace: 'files', + mongoId: orderedHostIds.files, + deleted: false, + }, + { + timestamp: 20, + namespace: 'connections', + mongoId: orderedHostIds.connection1, + deleted: false, + }, + { + timestamp: 20, + namespace: 'connections', + mongoId: orderedHostIds.connection2, + deleted: false, + }, + { + timestamp: 1, + namespace: 'entities', + mongoId: orderedHostIds.entity1, + deleted: false, + }, + { + timestamp: 1, + namespace: 'entities', + mongoId: orderedHostIds.entity2, + deleted: false, + }, + ], +}; + export { host1Fixtures, host2Fixtures, @@ -887,6 +1078,8 @@ export { thesauri1Value2, newDoc1, newDoc3, + orderedHostFixtures, + orderedHostIds, relationtype4, relationship9, hub3, diff --git a/app/api/sync/specs/syncWorker.spec.ts b/app/api/sync/specs/syncWorker.spec.ts index 587b9c65aa..e8447cb3ba 100644 --- a/app/api/sync/specs/syncWorker.spec.ts +++ b/app/api/sync/specs/syncWorker.spec.ts @@ -1,3 +1,12 @@ +/* eslint-disable max-statements */ +import { Server } from 'http'; +// eslint-disable-next-line node/no-restricted-import +import { rm, writeFile } from 'fs/promises'; + +import bodyParser from 'body-parser'; +import 'isomorphic-fetch'; +import _ from 'lodash'; + import authRoutes from 'api/auth/routes'; import entities from 'api/entities'; import entitiesModel from 'api/entities/entitiesModel'; @@ -16,19 +25,13 @@ import { appContextMiddleware } from 'api/utils/appContextMiddleware'; import { elasticTesting } from 'api/utils/elastic_testing'; import errorHandlingMiddleware from 'api/utils/error_handling_middleware'; import mailer from 'api/utils/mailer'; -import db from 'api/utils/testing_db'; +import db, { DBFixture } from 'api/utils/testing_db'; import { advancedSort } from 'app/utils/advancedSort'; -import bodyParser from 'body-parser'; import express, { NextFunction, Request, RequestHandler, Response } from 'express'; import { DefaultTranslationsDataSource } from 'api/i18n.v2/database/data_source_defaults'; import { CreateTranslationsService } from 'api/i18n.v2/services/CreateTranslationsService'; import { ValidateTranslationsService } from 'api/i18n.v2/services/ValidateTranslationsService'; import { DefaultSettingsDataSource } from 'api/settings.v2/database/data_source_defaults'; -// eslint-disable-next-line node/no-restricted-import -import { rm, writeFile } from 'fs/promises'; -import { Server } from 'http'; -import 'isomorphic-fetch'; -import _ from 'lodash'; import { FetchResponseError } from 'shared/JSONRequest'; import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults'; import { syncWorker } from '../syncWorker'; @@ -38,6 +41,8 @@ import { hub3, newDoc1, newDoc3, + orderedHostFixtures, + orderedHostIds, relationship9, template1, template2, @@ -55,11 +60,14 @@ async function runAllTenants() { } } -async function applyFixtures() { - await db.setupFixturesAndContext(host1Fixtures, undefined, 'host1'); - await db.setupFixturesAndContext(host2Fixtures, undefined, 'host2'); - await db.setupFixturesAndContext({ settings: [{}] }, undefined, 'target1'); - await db.setupFixturesAndContext({ settings: [{}] }, undefined, 'target2'); +async function applyFixtures( + _host1Fixtures: DBFixture = host1Fixtures, + _host2Fixtures = host2Fixtures +) { + const host1db = await db.setupFixturesAndContext(_host1Fixtures, undefined, 'host1'); + const host2db = await db.setupFixturesAndContext(_host2Fixtures, undefined, 'host2'); + const target1db = await db.setupFixturesAndContext({ settings: [{}] }, undefined, 'target1'); + const target2db = await db.setupFixturesAndContext({ settings: [{}] }, undefined, 'target2'); db.UserInContextMockFactory.restore(); await tenants.run(async () => { @@ -81,6 +89,8 @@ async function applyFixtures() { email: 'user2@testing', }); }, 'target2'); + + return { host1db, host2db, target1db, target2db }; } describe('syncWorker', () => { @@ -497,4 +507,78 @@ describe('syncWorker', () => { }, 'target1'); }, 10000); }); + + it('should sync collections in correct preference order', async () => { + const originalBatchLimit = syncWorker.UPDATE_LOG_TARGET_COUNT; + syncWorker.UPDATE_LOG_TARGET_COUNT = 1; + const { host1db, target1db } = await applyFixtures(orderedHostFixtures, {}); + + const runAndCheck = async ( + currentCollection: string, + nextCollection: string | undefined, + currentExpectation: any[], + syncTimeStampExpectation: number + ) => { + await runAllTenants(); + const syncLog = await host1db!.collection('syncs').findOne({ name: 'target1' }); + + const currentSyncedContent = await target1db! + .collection(currentCollection) + .find({}) + .toArray(); + expect(currentSyncedContent).toMatchObject(currentExpectation); + expect(syncLog!.lastSyncs[currentCollection]).toBe(syncTimeStampExpectation); + + if (nextCollection) { + const nextSyncedContent = await target1db!.collection(nextCollection).find({}).toArray(); + expect(nextSyncedContent).toEqual([]); + expect(syncLog!.lastSyncs[nextCollection]).toBeUndefined(); + } + }; + + await runAndCheck( + 'settings', + 'translationsV2', + [{ languages: [{ key: 'en' as 'en', default: true, label: 'en' }] }], + 1000 + ); + await runAndCheck( + 'translationsV2', + 'dictionaries', + [{ _id: orderedHostIds.translationsV2 }], + 700 + ); + await runAndCheck('dictionaries', 'relationtypes', [{ _id: orderedHostIds.dictionaries }], 600); + await runAndCheck('relationtypes', 'templates', [{ _id: orderedHostIds.relationtypes }], 500); + await runAndCheck('templates', 'files', [{ _id: orderedHostIds.templates }], 40); + await runAndCheck('files', 'connections', [{ _id: orderedHostIds.files }], 30); + await runAndCheck( + 'connections', + 'entities', + [{ _id: orderedHostIds.connection1 }, { _id: orderedHostIds.connection2 }], + 20 + ); + await runAndCheck( + 'entities', + undefined, + [{ _id: orderedHostIds.entity1 }, { _id: orderedHostIds.entity2 }], + 1 + ); + + await applyFixtures(); + syncWorker.UPDATE_LOG_TARGET_COUNT = originalBatchLimit; + }); + + it('should throw an error, when trying to sync a collection that is not in the order list', async () => { + const fixtures = _.cloneDeep(orderedHostFixtures); + //@ts-ignore + fixtures.settings[0].sync[0].config.pages = []; + await applyFixtures(fixtures, {}); + + await expect(runAllTenants).rejects.toThrowError( + new Error('Invalid elements found in ordering - pages') + ); + + await applyFixtures(); + }); }); diff --git a/app/api/sync/syncConfig.ts b/app/api/sync/syncConfig.ts index 2bbc1e805b..aec4e60baa 100644 --- a/app/api/sync/syncConfig.ts +++ b/app/api/sync/syncConfig.ts @@ -2,7 +2,9 @@ import { DataType } from 'api/odm'; import { SyncConfig } from 'api/sync/syncWorker'; import templatesModel from 'api/templates/templatesModel'; import { model as updateLog, UpdateLog } from 'api/updatelogs'; +import { explicitOrdering } from 'shared/data_utils/arrayUtils'; import { PropertySchema } from 'shared/types/commonTypes'; +import { syncedPromiseLoop } from 'shared/data_utils/promiseUtils'; import { ProcessNamespaces } from './processNamespaces'; import syncsModel from './syncsModel'; @@ -54,24 +56,34 @@ const getValuesFromTemplateProperties = async ( ); }; +const COLLECTION_SYNC_ORDER = [ + 'settings', + 'translationsV2', + 'dictionaries', + 'relationtypes', + 'templates', + 'files', + 'connections', + 'entities', +]; +const TEMPLATE_DEPENDENCIES = [ + 'settings', + 'entities', + 'files', + 'connections', + 'dictionaries', + 'translationsV2', + 'relationtypes', +]; + const getApprovedCollections = (config: SyncConfig['config']) => { - const collections = Object.keys(config); - const whitelistedCollections = collections.includes('templates') - ? collections.concat([ - 'settings', - 'entities', - 'files', - 'connections', - 'dictionaries', - 'translations', - 'translationsV2', - 'relationtypes', - ]) + let collections = Object.keys(config); + collections = collections.includes('templates') + ? collections.concat(TEMPLATE_DEPENDENCIES) : collections; + collections = explicitOrdering(COLLECTION_SYNC_ORDER, collections, true); - const blacklistedCollections = ['migrations', 'sessions']; - - return whitelistedCollections.filter(c => !blacklistedCollections.includes(c)); + return collections; }; const getApprovedThesauri = async (config: SyncConfig['config']) => @@ -87,24 +99,27 @@ const getApprovedRelationtypes = async (config: SyncConfig['config']) => { return relationtypesConfig.concat(validTemplateRelationtypes); }; -export const createSyncConfig = async (config: SyncConfig, targetName: string) => { - const [{ lastSync }] = await syncsModel.find({ name: targetName }); +export const createSyncConfig = async ( + config: SyncConfig, + targetName: string, + updateLogTargetCount: number = 50 +) => { + const [{ lastSyncs }] = await syncsModel.find({ name: targetName }); return { - lastSync, + lastSyncs: lastSyncs || {}, config: await removeDeletedTemplatesFromConfig(config.config), - async lastChanges() { - const approvedCollections = getApprovedCollections(this.config); + async lastChangesForCollection(collection: string, lastSync: number, limit: number) { const firstBatch = await updateLog.find( { timestamp: { $gt: lastSync }, - namespace: { $in: approvedCollections }, + namespace: collection, }, undefined, { sort: { timestamp: 1 }, - limit: 50, + limit, lean: true, } ); @@ -118,7 +133,7 @@ export const createSyncConfig = async (config: SyncConfig, targetName: string) = return updateLog.find( { $and: [{ timestamp: { $gt: lastSync } }, { timestamp: { $lte: endTimestamp } }], - namespace: { $in: approvedCollections }, + namespace: collection, }, undefined, { @@ -130,6 +145,24 @@ export const createSyncConfig = async (config: SyncConfig, targetName: string) = ); }, + async lastChanges() { + const approvedCollections = getApprovedCollections(this.config); + let currentLimit = updateLogTargetCount; + const changes: UpdateLog[] = []; + await syncedPromiseLoop(approvedCollections, async collection => { + const lastSync = this.lastSyncs[collection] || 0; + const collectionChanges = await this.lastChangesForCollection( + collection, + lastSync, + currentLimit + ); + changes.push(...collectionChanges); + currentLimit -= collectionChanges.length; + return currentLimit > 0; + }); + return changes; + }, + async shouldSync(change: DataType) { if (change.deleted) return { skip: true }; const templatesConfig = this.config.templates || {}; diff --git a/app/api/sync/syncWorker.ts b/app/api/sync/syncWorker.ts index f7bf8697f1..540a330cb2 100644 --- a/app/api/sync/syncWorker.ts +++ b/app/api/sync/syncWorker.ts @@ -9,13 +9,13 @@ import { synchronizer } from './synchronizer'; import { createSyncConfig } from './syncConfig'; import syncsModel from './syncsModel'; -const updateSyncs = async (name: string, lastSync: number) => - syncsModel._updateMany({ name }, { $set: { lastSync } }, {}); +const updateSyncs = async (name: string, collection: string, lastSync: number) => + syncsModel._updateMany({ name }, { $set: { [`lastSyncs.${collection}`]: lastSync } }, {}); async function createSyncIfNotExists(config: SettingsSyncSchema) { const syncs = await syncsModel.find({ name: config.name }); if (syncs.length === 0) { - await syncsModel.create({ lastSync: 0, name: config.name }); + await syncsModel.create({ lastSyncs: {}, name: config.name }); } } @@ -51,6 +51,8 @@ const validateConfig = (config: SettingsSyncSchema) => { }; export const syncWorker = { + UPDATE_LOG_TARGET_COUNT: 50, + async runAllTenants() { return Object.keys(tenants.tenants).reduce(async (previous, tenantName) => { await previous; @@ -78,7 +80,7 @@ export const syncWorker = { async syncronizeConfig(config: SyncConfig, cookie: string) { await createSyncIfNotExists(config); - const syncConfig = await createSyncConfig(config, config.name); + const syncConfig = await createSyncConfig(config, config.name, this.UPDATE_LOG_TARGET_COUNT); await ( await syncConfig.lastChanges() @@ -100,7 +102,7 @@ export const syncWorker = { 'post' ); } - await updateSyncs(config.name, change.timestamp); + await updateSyncs(config.name, change.namespace, change.timestamp); }, Promise.resolve()); }, diff --git a/app/api/sync/syncsModel.ts b/app/api/sync/syncsModel.ts index 3768c08ca0..affbdeeadb 100644 --- a/app/api/sync/syncsModel.ts +++ b/app/api/sync/syncsModel.ts @@ -2,12 +2,11 @@ import mongoose from 'mongoose'; import { MultiTenantMongooseModel } from 'api/odm/MultiTenantMongooseModel'; const syncSchema = new mongoose.Schema({ - lastSync: Number, + lastSyncs: { type: mongoose.Schema.Types.Mixed, default: {} }, name: String, }); - export interface Sync extends mongoose.Document { - lastSync: number; + lastSyncs: { [key: string]: number }; name: string; } diff --git a/app/api/updatelogs/updatelogsModel.ts b/app/api/updatelogs/updatelogsModel.ts index d732f69489..29c579b2ef 100644 --- a/app/api/updatelogs/updatelogsModel.ts +++ b/app/api/updatelogs/updatelogsModel.ts @@ -9,6 +9,7 @@ const updateLogSchema = new mongoose.Schema({ deleted: Boolean, }); +updateLogSchema.index({ namespace: 1, timestamp: 1 }); export interface UpdateLog extends mongoose.Document { timestamp: number; namespace: string; diff --git a/app/api/utils/testing_db.ts b/app/api/utils/testing_db.ts index 2e9e5c105a..cfe6e55020 100644 --- a/app/api/utils/testing_db.ts +++ b/app/api/utils/testing_db.ts @@ -95,7 +95,7 @@ const testingDB: { fixtures: DBFixture, elasticIndex?: string, dbName?: string - ) => Promise; + ) => Promise; clearAllAndLoadFixtures: (fixtures: DBFixture, dbName?: string) => Promise; } = { mongodb: null, @@ -159,6 +159,8 @@ const testingDB: { testingTenants.changeCurrentTenant({ indexName: elasticIndex }); await elasticTesting.reindex(); } + + return optionalMongo || this.mongodb; }, async createIndices() { diff --git a/app/react/App/scss/modules/_markdown.scss b/app/react/App/scss/modules/_markdown.scss index e7fd071c25..56bf4fc696 100644 --- a/app/react/App/scss/modules/_markdown.scss +++ b/app/react/App/scss/modules/_markdown.scss @@ -7,6 +7,7 @@ text-align: center; font-size: 12px; line-height: 16px; + &:focus-visible { outline: none; } @@ -20,11 +21,13 @@ padding: 15px; font-family: $f-mono; } + .tab-nav { position: absolute; right: 0; bottom: 100%; } + .tab-link { display: inline-block; padding: 5px 5px 0; @@ -34,17 +37,20 @@ cursor: pointer; color: $c-grey-dark; border-bottom: 1px dotted $c-white; + &:hover { text-decoration: none; color: $c-black; border-bottom: 1px dotted $c-black; } + &.tab-link-active { color: $c-primary; font-weight: bold; border-bottom: 1px solid $c-primary; } } + .tab-content { float: left; width: 100%; @@ -59,10 +65,12 @@ font-style: italic; font-weight: 300; border: 0; + p { font-size: $f-size-lg; } } + code { padding: 3px 6px 2px; position: relative; @@ -72,6 +80,7 @@ color: $c-primary; background-color: $c-grey-lighten; } + p a, td a { display: inline; @@ -79,20 +88,24 @@ border-bottom: 1px solid $c-primary-light; text-decoration: none !important; padding: 0 2px; + &:hover { color: $c-primary; border-color: $c-primary; } } + img { max-width: 100%; } + p, li { font-family: $f-regular; font-size: $f-size-lg; line-height: 1.5em; } + li { margin-bottom: 5px; } @@ -101,29 +114,37 @@ width: 100%; table-layout: fixed; } + th, td { padding: 7px 10px; } + th { background-color: $c-grey-lighten; border-bottom: 1px solid $c-grey-light; } + th:not(:last-of-type) { border-right: 1px solid $c-white; } + tr:not(:last-of-type) { border-bottom: 1px solid $c-grey-light; } + h1 { margin-top: 60px; } + h2 { margin-top: 45px; } + h3 { margin-top: 30px; } + h4 { margin-top: 15px; } @@ -133,6 +154,7 @@ .markdownEditor { margin-top: 15px; } + .tab-link { padding: 15px 15px 0; font-size: $f-size-sm; @@ -140,6 +162,7 @@ } .side-panel { + .markdownViewer, .markdownEditor { p { @@ -149,6 +172,7 @@ } .item { + .markdownViewer, .markdownEditor { p { @@ -157,7 +181,7 @@ } } -.video-container > div:first-child { +.video-container>div:first-child { position: relative; padding-bottom: 73.25%; height: 0; @@ -165,6 +189,7 @@ margin-top: 5px; margin-bottom: 5px; } + .video-container iframe, .video-container object, .video-container embed { @@ -174,7 +199,7 @@ height: 100%; } -.video-container.compact > div:first-child { +.video-container.compact>div:first-child { height: 0; padding-bottom: 0; position: relative; @@ -188,6 +213,44 @@ } } +.video-container { + .loader { + padding-top: 0 !important; + width: 100% !important; + min-height: 210px !important; + height: 100% !important; + background-color: $c-grey-lighten; + display: flex; + gap: 4px; + justify-content: center; + align-items: center; + + .bouncing-dots { + .dot { + display: inline-block; + margin-right: 2px; + margin-bottom: 4px; + width: 2px; + height: 2px; + border-radius: 50%; + background-color: black; + + &:nth-last-child(1) { + animation: jumpingAnimation 0.7s 0.1s ease-in infinite; + } + + &:nth-last-child(2) { + animation: jumpingAnimation 0.7s 0.2s ease-in infinite; + } + + &:nth-last-child(3) { + animation: jumpingAnimation 0.7s 0.3s ease-in infinite; + } + } + } + } +} + .video-container .timelink { display: flex; cursor: pointer; @@ -234,7 +297,7 @@ } } -.video-container > .timelinks-form { +.video-container>.timelinks-form { margin-top: 10px; margin-bottom: 10px; @@ -265,13 +328,15 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; + /* <-- Apparently some margin are still there even though it's hidden */ } input[type=number] { - -moz-appearance:textfield; /* Firefox */ + -moz-appearance: textfield; + /* Firefox */ } &:focus-visible, @@ -284,10 +349,12 @@ padding-left: 4px; @include timeline-time(); } + .timestamp-minutes { width: 33%; @include timeline-time(); } + .seperator { width: auto; line-height: 16px; @@ -296,6 +363,7 @@ padding-top: 4px; border: none; } + .timestamp-seconds { width: 33%; padding-right: 4px; @@ -333,7 +401,7 @@ } } -.page-viewer .markdown-viewer > *:not(.hero) { +.page-viewer .markdown-viewer>*:not(.hero) { max-width: 1000px; margin-left: auto; margin-right: auto; @@ -346,10 +414,12 @@ .side-panel, .entity-viewer { .markdown-viewer { + p, li { font-size: $f-size; } + h1, h2, h3, @@ -366,8 +436,9 @@ h1 { font-size: $f-size-xxl; } + h2 { font-size: $f-size-xxl; } } -} +} \ No newline at end of file diff --git a/app/react/App/scss/utilities/_utilities.scss b/app/react/App/scss/utilities/_utilities.scss index 1ee8e17258..a522b529f3 100644 --- a/app/react/App/scss/utilities/_utilities.scss +++ b/app/react/App/scss/utilities/_utilities.scss @@ -31,3 +31,17 @@ display: none; } } + +@keyframes jumpingAnimation { + 0% { + transform: translate3d(0, 0, 0) + } + + 50% { + transform: translate3d(0, 5px, 0); + } + + 100% { + transform: translate3d(0, 0, 0); + } +} \ No newline at end of file diff --git a/app/react/App/styles/globals.css b/app/react/App/styles/globals.css index f2d440db86..c5583bbce3 100644 --- a/app/react/App/styles/globals.css +++ b/app/react/App/styles/globals.css @@ -434,6 +434,179 @@ video { display: none; } +.tooltip-arrow,.tooltip-arrow:before { + position: absolute; + width: 8px; + height: 8px; + background: inherit; +} + +.tooltip-arrow { + visibility: hidden; +} + +.tooltip-arrow:before { + content: ""; + visibility: visible; + transform: rotate(45deg); +} + +[data-tooltip-style^='light'] + .tooltip > .tooltip-arrow:before { + border-style: solid; + border-color: #e5e7eb; +} + +[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='top'] > .tooltip-arrow:before { + border-bottom-width: 1px; + border-right-width: 1px; +} + +[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='right'] > .tooltip-arrow:before { + border-bottom-width: 1px; + border-left-width: 1px; +} + +[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='bottom'] > .tooltip-arrow:before { + border-top-width: 1px; + border-left-width: 1px; +} + +[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='left'] > .tooltip-arrow:before { + border-top-width: 1px; + border-right-width: 1px; +} + +.tooltip[data-popper-placement^='top'] > .tooltip-arrow { + bottom: -4px; +} + +.tooltip[data-popper-placement^='bottom'] > .tooltip-arrow { + top: -4px; +} + +.tooltip[data-popper-placement^='left'] > .tooltip-arrow { + right: -4px; +} + +.tooltip[data-popper-placement^='right'] > .tooltip-arrow { + left: -4px; +} + +.tooltip.invisible > .tooltip-arrow:before { + visibility: hidden; +} + +[data-popper-arrow],[data-popper-arrow]:before { + position: absolute; + width: 8px; + height: 8px; + background: inherit; +} + +[data-popper-arrow] { + visibility: hidden; +} + +[data-popper-arrow]:before { + content: ""; + visibility: visible; + transform: rotate(45deg); +} + +[data-popper-arrow]:after { + content: ""; + visibility: visible; + transform: rotate(45deg); + position: absolute; + width: 9px; + height: 9px; + background: inherit; +} + +[role="tooltip"] > [data-popper-arrow]:before { + border-style: solid; + border-color: #e5e7eb; +} + +.dark [role="tooltip"] > [data-popper-arrow]:before { + border-style: solid; + border-color: #4b5563; +} + +[role="tooltip"] > [data-popper-arrow]:after { + border-style: solid; + border-color: #e5e7eb; +} + +.dark [role="tooltip"] > [data-popper-arrow]:after { + border-style: solid; + border-color: #4b5563; +} + +[data-popover][role="tooltip"][data-popper-placement^='top'] > [data-popper-arrow]:before { + border-bottom-width: 1px; + border-right-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='top'] > [data-popper-arrow]:after { + border-bottom-width: 1px; + border-right-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='right'] > [data-popper-arrow]:before { + border-bottom-width: 1px; + border-left-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='right'] > [data-popper-arrow]:after { + border-bottom-width: 1px; + border-left-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='bottom'] > [data-popper-arrow]:before { + border-top-width: 1px; + border-left-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='bottom'] > [data-popper-arrow]:after { + border-top-width: 1px; + border-left-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='left'] > [data-popper-arrow]:before { + border-top-width: 1px; + border-right-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='left'] > [data-popper-arrow]:after { + border-top-width: 1px; + border-right-width: 1px; +} + +[data-popover][role="tooltip"][data-popper-placement^='top'] > [data-popper-arrow] { + bottom: -5px; +} + +[data-popover][role="tooltip"][data-popper-placement^='bottom'] > [data-popper-arrow] { + top: -5px; +} + +[data-popover][role="tooltip"][data-popper-placement^='left'] > [data-popper-arrow] { + right: -5px; +} + +[data-popover][role="tooltip"][data-popper-placement^='right'] > [data-popper-arrow] { + left: -5px; +} + +[role="tooltip"].invisible > [data-popper-arrow]:before { + visibility: hidden; +} + +[role="tooltip"].invisible > [data-popper-arrow]:after { + visibility: hidden; +} + [type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { -webkit-appearance: none; -moz-appearance: none; @@ -483,10 +656,10 @@ input::placeholder,textarea::placeholder { } select:not([size]) { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right 0.5rem center; + background-image: url("data:image/svg+xml,%3csvg aria-hidden='true' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 10 6'%3e %3cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m1 1 4 4 4-4'/%3e %3c/svg%3e"); + background-position: right 0.75rem center; background-repeat: no-repeat; - background-size: 1.5em 1.5em; + background-size: 0.75em 0.75em; padding-right: 2.5rem; -webkit-print-color-adjust: exact; print-color-adjust: exact; @@ -548,26 +721,38 @@ select:not([size]) { [type='checkbox']:checked,[type='radio']:checked,.dark [type='checkbox']:checked,.dark [type='radio']:checked { border-color: transparent; background-color: currentColor; - background-size: 100% 100%; + background-size: 0.55em 0.55em; background-position: center; background-repeat: no-repeat; } [type='checkbox']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml,%3csvg aria-hidden='true' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 12'%3e %3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M1 5.917 5.724 10.5 15 1.5'/%3e %3c/svg%3e"); + background-repeat: no-repeat; + background-size: 0.55em 0.55em; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; } [type='radio']:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); + background-size: 1em 1em; +} + +.dark [type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); + background-size: 1em 1em; } [type='checkbox']:indeterminate { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); - border-color: transparent; + background-image: url("data:image/svg+xml,%3csvg aria-hidden='true' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 12'%3e %3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M1 5.917 5.724 10.5 15 1.5'/%3e %3c/svg%3e"); background-color: currentColor; - background-size: 100% 100%; + border-color: transparent; background-position: center; background-repeat: no-repeat; + background-size: 0.55em 0.55em; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; } [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { @@ -723,162 +908,6 @@ input:checked + .toggle-bg { border-color: #4f46e5; } -[data-tooltip-style^='light'] + .tooltip > .tooltip-arrow:before { - border-style: solid; - border-color: #e5e7eb; -} - -[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='top'] > .tooltip-arrow:before { - border-bottom-width: 1px; - border-right-width: 1px; -} - -[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='right'] > .tooltip-arrow:before { - border-bottom-width: 1px; - border-left-width: 1px; -} - -[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='bottom'] > .tooltip-arrow:before { - border-top-width: 1px; - border-left-width: 1px; -} - -[data-tooltip-style^='light'] + .tooltip[data-popper-placement^='left'] > .tooltip-arrow:before { - border-top-width: 1px; - border-right-width: 1px; -} - -.tooltip[data-popper-placement^='top'] > .tooltip-arrow { - bottom: -4px; -} - -.tooltip[data-popper-placement^='bottom'] > .tooltip-arrow { - top: -4px; -} - -.tooltip[data-popper-placement^='left'] > .tooltip-arrow { - right: -4px; -} - -.tooltip[data-popper-placement^='right'] > .tooltip-arrow { - left: -4px; -} - -.tooltip.invisible > .tooltip-arrow:before { - visibility: hidden; -} - -[data-popper-arrow],[data-popper-arrow]:before { - position: absolute; - width: 8px; - height: 8px; - background: inherit; -} - -[data-popper-arrow] { - visibility: hidden; -} - -[data-popper-arrow]:before { - content: ""; - visibility: visible; - transform: rotate(45deg); -} - -[data-popper-arrow]:after { - content: ""; - visibility: visible; - transform: rotate(45deg); - position: absolute; - width: 9px; - height: 9px; - background: inherit; -} - -[role="tooltip"] > [data-popper-arrow]:before { - border-style: solid; - border-color: #e5e7eb; -} - -.dark [role="tooltip"] > [data-popper-arrow]:before { - border-style: solid; - border-color: #4b5563; -} - -[role="tooltip"] > [data-popper-arrow]:after { - border-style: solid; - border-color: #e5e7eb; -} - -.dark [role="tooltip"] > [data-popper-arrow]:after { - border-style: solid; - border-color: #4b5563; -} - -[data-popover][role="tooltip"][data-popper-placement^='top'] > [data-popper-arrow]:before { - border-bottom-width: 1px; - border-right-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='top'] > [data-popper-arrow]:after { - border-bottom-width: 1px; - border-right-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='right'] > [data-popper-arrow]:before { - border-bottom-width: 1px; - border-left-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='right'] > [data-popper-arrow]:after { - border-bottom-width: 1px; - border-left-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='bottom'] > [data-popper-arrow]:before { - border-top-width: 1px; - border-left-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='bottom'] > [data-popper-arrow]:after { - border-top-width: 1px; - border-left-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='left'] > [data-popper-arrow]:before { - border-top-width: 1px; - border-right-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='left'] > [data-popper-arrow]:after { - border-top-width: 1px; - border-right-width: 1px; -} - -[data-popover][role="tooltip"][data-popper-placement^='top'] > [data-popper-arrow] { - bottom: -5px; -} - -[data-popover][role="tooltip"][data-popper-placement^='bottom'] > [data-popper-arrow] { - top: -5px; -} - -[data-popover][role="tooltip"][data-popper-placement^='left'] > [data-popper-arrow] { - right: -5px; -} - -[data-popover][role="tooltip"][data-popper-placement^='right'] > [data-popper-arrow] { - left: -5px; -} - -[role="tooltip"].invisible > [data-popper-arrow]:before { - visibility: hidden; -} - -[role="tooltip"].invisible > [data-popper-arrow]:after { - visibility: hidden; -} - .tw-content * { font-family: 'Inter', sans-serif !important; } @@ -1674,6 +1703,10 @@ input:checked + .toggle-bg { bottom: 1.25rem; } +.bottom-\[60px\] { + bottom: 60px; +} + .bottom-\[6px\] { bottom: 6px; } @@ -1738,6 +1771,14 @@ input:checked + .toggle-bg { z-index: 20; } +.z-30 { + z-index: 30; +} + +.z-40 { + z-index: 40; +} + .z-50 { z-index: 50; } @@ -2023,6 +2064,10 @@ input:checked + .toggle-bg { height: 2rem; } +.h-9 { + height: 2.25rem; +} + .h-96 { height: 24rem; } @@ -2102,6 +2147,10 @@ input:checked + .toggle-bg { width: 8.333333%; } +.w-1\/2 { + width: 50%; +} + .w-1\/3 { width: 33.333333%; } @@ -2316,6 +2365,10 @@ input:checked + .toggle-bg { flex: none; } +.flex-shrink { + flex-shrink: 1; +} + .flex-shrink-0 { flex-shrink: 0; } @@ -2357,35 +2410,43 @@ input:checked + .toggle-bg { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.-translate-x-full { + --tw-translate-x: -100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .-translate-y-1\/2 { --tw-translate-y: -50%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.-translate-y-full { + --tw-translate-y: -100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .translate-x-0 { --tw-translate-x: 0px; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.rotate-180 { - --tw-rotate: 180deg; +.translate-x-full { + --tw-translate-x: 100%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.rotate-45 { - --tw-rotate: 45deg; +.translate-y-full { + --tw-translate-y: 100%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.scale-100 { - --tw-scale-x: 1; - --tw-scale-y: 1; +.rotate-180 { + --tw-rotate: 180deg; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.scale-95 { - --tw-scale-x: .95; - --tw-scale-y: .95; +.rotate-45 { + --tw-rotate: 45deg; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @@ -2393,6 +2454,10 @@ input:checked + .toggle-bg { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.transform-none { + transform: none; +} + @keyframes spin { to { transform: rotate(360deg); @@ -2403,6 +2468,10 @@ input:checked + .toggle-bg { animation: spin 1s linear infinite; } +.cursor-default { + cursor: default; +} + .cursor-grab { cursor: grab; } @@ -2429,6 +2498,10 @@ input:checked + .toggle-bg { user-select: none; } +.resize { + resize: both; +} + .snap-x { scroll-snap-type: x var(--tw-scroll-snap-strictness); } @@ -2467,6 +2540,14 @@ input:checked + .toggle-bg { grid-auto-flow: column; } +.grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.grid-cols-7 { + grid-template-columns: repeat(7, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -2833,6 +2914,16 @@ input:checked + .toggle-bg { border-color: rgb(165 180 252 / var(--tw-border-opacity)); } +.border-blue-600 { + --tw-border-opacity: 1; + border-color: rgb(79 70 229 / var(--tw-border-opacity)); +} + +.border-blue-700 { + --tw-border-opacity: 1; + border-color: rgb(67 56 202 / var(--tw-border-opacity)); +} + .border-error-300 { --tw-border-opacity: 1; border-color: rgb(249 168 212 / var(--tw-border-opacity)); @@ -3046,6 +3137,11 @@ input:checked + .toggle-bg { background-color: rgb(238 242 255 / var(--tw-bg-opacity)); } +.bg-blue-700 { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + .bg-error-100 { --tw-bg-opacity: 1; background-color: rgb(252 231 243 / var(--tw-bg-opacity)); @@ -3241,11 +3337,6 @@ input:checked + .toggle-bg { background-color: rgb(253 232 232 / var(--tw-bg-opacity)); } -.bg-red-200 { - --tw-bg-opacity: 1; - background-color: rgb(251 213 213 / var(--tw-bg-opacity)); -} - .bg-red-400 { --tw-bg-opacity: 1; background-color: rgb(249 128 128 / var(--tw-bg-opacity)); @@ -3826,6 +3917,14 @@ input:checked + .toggle-bg { line-height: 1rem; } +.leading-6 { + line-height: 1.5rem; +} + +.leading-9 { + line-height: 2.25rem; +} + .leading-none { line-height: 1; } @@ -4269,6 +4368,11 @@ input:checked + .toggle-bg { --tw-ring-color: rgb(250 202 21 / var(--tw-ring-opacity)); } +.blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + .drop-shadow-md { --tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06)); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); @@ -4302,6 +4406,12 @@ input:checked + .toggle-bg { transition-duration: 150ms; } +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .duration-100 { transition-duration: 100ms; } @@ -4480,6 +4590,11 @@ input:checked + .toggle-bg { border-color: rgb(165 180 252 / var(--tw-border-opacity)); } +.hover\:bg-blue-800:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 48 163 / var(--tw-bg-opacity)); +} + .hover\:bg-error-800:hover { --tw-bg-opacity: 1; background-color: rgb(157 23 77 / var(--tw-bg-opacity)); @@ -4574,6 +4689,11 @@ input:checked + .toggle-bg { background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); } +.hover\:text-blue-600:hover { + --tw-text-opacity: 1; + color: rgb(79 70 229 / var(--tw-text-opacity)); +} + .hover\:text-gray-600:hover { --tw-text-opacity: 1; color: rgb(75 85 99 / var(--tw-text-opacity)); @@ -4654,6 +4774,11 @@ input:checked + .toggle-bg { box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } +.focus\:ring-blue-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(165 180 252 / var(--tw-ring-opacity)); +} + .focus\:ring-blue-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity)); @@ -5167,6 +5292,11 @@ input:checked + .toggle-bg { border-style: none; } +:is(.dark .dark\:border-blue-500) { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity)); +} + :is(.dark .dark\:border-gray-600) { --tw-border-opacity: 1; border-color: rgb(75 85 99 / var(--tw-border-opacity)); @@ -5222,6 +5352,10 @@ input:checked + .toggle-bg { border-color: rgb(4 116 129 / var(--tw-border-opacity)); } +:is(.dark .dark\:border-transparent) { + border-color: transparent; +} + :is(.dark .dark\:border-white) { --tw-border-opacity: 1; border-color: rgb(255 255 255 / var(--tw-border-opacity)); @@ -5242,6 +5376,11 @@ input:checked + .toggle-bg { background-color: rgb(0 0 0 / var(--tw-bg-opacity)); } +:is(.dark .dark\:bg-blue-600) { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + :is(.dark .dark\:bg-gray-200) { --tw-bg-opacity: 1; background-color: rgb(229 231 235 / var(--tw-bg-opacity)); @@ -5407,6 +5546,11 @@ input:checked + .toggle-bg { fill: #D1D5DB; } +:is(.dark .dark\:text-blue-500) { + --tw-text-opacity: 1; + color: rgb(99 102 241 / var(--tw-text-opacity)); +} + :is(.dark .dark\:text-error-400) { --tw-text-opacity: 1; color: rgb(244 114 182 / var(--tw-text-opacity)); @@ -5652,6 +5796,11 @@ input:checked + .toggle-bg { border-color: rgb(107 114 128 / var(--tw-border-opacity)); } +:is(.dark .dark\:hover\:bg-blue-700:hover) { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + :is(.dark .dark\:hover\:bg-gray-300:hover) { --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity)); @@ -5707,6 +5856,11 @@ input:checked + .toggle-bg { background-color: rgb(250 202 21 / var(--tw-bg-opacity)); } +:is(.dark .dark\:hover\:text-blue-500:hover) { + --tw-text-opacity: 1; + color: rgb(99 102 241 / var(--tw-text-opacity)); +} + :is(.dark .dark\:hover\:text-gray-300:hover) { --tw-text-opacity: 1; color: rgb(209 213 219 / var(--tw-text-opacity)); diff --git a/app/react/Markdown/components/MarkdownMedia.tsx b/app/react/Markdown/components/MarkdownMedia.tsx index f257a95154..b93ffb4cd4 100644 --- a/app/react/Markdown/components/MarkdownMedia.tsx +++ b/app/react/Markdown/components/MarkdownMedia.tsx @@ -71,6 +71,7 @@ const MarkdownMedia = (props: MarkdownMediaProps) => { const [isVideoPlaying, setVideoPlaying] = useState(false); const [temporalResource, setTemporalResource] = useState(); const [mediaURL, setMediaURL] = useState(''); + const [isLoading, setIsLoading] = useState(true); const { control, register, getValues } = useForm<{ timelines: TimeLink[] }>({ defaultValues: { timelines: originalTimelinks }, }); @@ -150,7 +151,7 @@ const MarkdownMedia = (props: MarkdownMediaProps) => { { - let hours = parseInt(event.target.value || '0', 10); + const hours = parseInt(event.target.value || '0', 10); setNewTimeline({ ...newTimeline, timeHours: hours <= 0 ? '00' : hours.toString() }); }} className="timestamp-hours" @@ -306,19 +307,25 @@ const MarkdownMedia = (props: MarkdownMediaProps) => { url = URL.createObjectURL(blob); setMediaURL(url); }) - .catch(_e => {}); + .catch(_e => {}) + .finally(() => { + setIsLoading(false); + }); } else if (config.url.match(validMediaUrlRegExp)) { setErrorFlag(false); setMediaURL(config.url); + setIsLoading(false); } else { if (mediaURL && mediaURL.match(validMediaUrlRegExp) && !temporalResource) { setTemporalResource(mediaURL); } setMediaURL(config.url); + setIsLoading(false); } return () => { setErrorFlag(false); + setIsLoading(true); URL.revokeObjectURL(url); setMediaURL(''); }; @@ -357,30 +364,40 @@ const MarkdownMedia = (props: MarkdownMediaProps) => { return (
-
- { - setVideoPlaying(false); - }} - onPlay={() => { - setVideoPlaying(true); - }} - onError={e => { - if (e.target.error.message.search(/MEDIA_ELEMENT_ERROR/) === -1) { - setErrorFlag(true); - } - }} - /> -
- - {!editing &&
{timeLinks(config.options.timelinks)}
} - {editing && ( + {isLoading ? ( +
+ Loading +
+
+
+
+
+
+ ) : ( +
+ { + setVideoPlaying(false); + }} + onPlay={() => { + setVideoPlaying(true); + }} + onError={e => { + if (e.target.error.message.search(/MEDIA_ELEMENT_ERROR/) === -1) { + setErrorFlag(true); + } + }} + /> +
+ )} + {!editing && !isLoading &&
{timeLinks(config.options.timelinks)}
} + {editing && !isLoading && (